分布式技术的发展,深刻地改变了我们编程的模式和思考软件的模式。值 2019 岁末,PingCAP 联合 InfoQ 共同策划出品“分布式系统前沿技术 ”专题, 邀请众多技术团队共同参与,一起探索这个古老领域的新生机。本文出自微众银行大数据平台负责人邸帅。
在当前的复杂分布式架构环境下,服务治理已经大行其道。但目光往下一层,从上层 APP、Service,到底层计算引擎这一层面,却还是各个引擎各自为政,Client-Server 模式紧耦合满天飞的情况。如何做好“计算治理”,让复杂环境下各种类型的大量计算任务,都能更简洁、灵活、有序、可控的提交执行,和保障成功返回结果?计算中间件 Linkis 就是上述问题的最佳实践。
分布式架构,指的是系统的组件分布在通过网络相连的不同计算机上,组件之间通过网络传递消息进行通信和协调,协同完成某一目标。一般来说有水平(集群化)和垂直(功能模块切分)两个拆分方向,以解决高内聚低耦合、高并发、高可用等方面问题。
多个分布式架构的系统,组成分布式系统群,就形成了一个相对复杂的分布式架构环境。通常包含多种上层应用服务,多种底层基础计算存储引擎。如下图 1 所示:
<center>图 1</center>就像《微服务设计》一书中提到的,如同城市规划师在面对一座庞大、复杂且不断变化的城市时,所需要做的规划、设计和治理一样,庞大复杂的软件系统环境中的各种区域、元素、角色和关系,也需要整治和管理,以使其以一种更简洁、优雅、有序、可控的方式协同运作,而不是变成一团乱麻。
在当前的复杂分布式架构环境下,大量 APP、Service 间的通信、协调和管理,已经有了从 SOA ( Service-Oriented Architecture )到微服务的成熟理念,及从 ESB 到 Service Mesh 的众多实践,来实现其从服务注册发现、配置管理、网关路由,到流控熔断、日志监控等一系列完整的服务治理功能。服务治理框架的“中间件”层设计,可以很好的实现服务间的解耦、异构屏蔽和互操作,并提供路由、流控、状态管理、监控等治理特性的共性提炼和复用,增强整个架构的灵活性、管控能力、可扩展性和可维护性。
但目光往下一层,你会发现在从 APP、Service,到后台引擎这一层面,却还是各个引擎各自为政,Client-Server 模式紧耦合满天飞的情况。在大量的上层应用,和大量的底层引擎之间,缺乏一层通用的“中间件”框架设计。类似下图 2 的网状。
<center>图 2</center>计算治理,关注的正是上层应用和底层计算(存储)引擎之间,从 Client 到 Server 的连接层范围,所存在的紧耦合、灵活性和管控能力欠缺、缺乏复用能力、可扩展性、可维护性差等问题。要让复杂分布式架构环境下各种类型的计算任务,都能更简洁、灵活、有序、可控的提交执行,和成功返回结果。如下图 3 所示:
<center>图 3</center>更详细的来看计算治理的问题,可以分为如下治( architecture,架构层面)和理( insight,细化特性)两个层面。
紧耦合问题,上层应用和底层计算存储引擎间的 CS 连接模式。
所有 APP & Service 和底层计算存储引擎,都是通过 Client-Server 模式相连,处于紧耦合状态。以 Analytics Engine 的 Spark 为例,如下图 4:
<center>图 4</center>这种状态会带来如下问题:
引擎 client 的任何改动(如版本升级),将直接影响每一个嵌入了该 client 的上层应用;当应用系统数量众多、规模庞大时,一次改动的成本会很高。
直连模式,导致上层应用缺乏,对跨底层计算存储引擎实例级别的,路由选择、负载均衡等能力;或者说依赖于特定底层引擎提供的特定连接方式实现,有的引擎有一些,有的没有。
随着时间推移,不断有新的上层应用和新的底层引擎加入进来,整体架构和调用关系将愈发复杂,可扩展性、可靠性和可维护性降低。
重复造轮子问题,每个上层应用工具系统都要重复解决计算治理问题。
每个上层应用都要重复的去集成各种 client,创建和管理 client 到引擎的连接及其状态,包括底层引擎元数据的获取与管理。在并发使用的用户逐渐变多、并发计算任务量逐渐变大时,每个上层应用还要重复的去解决多个用户间在 client 端的资源争用、权限隔离,计算任务的超时管理、失败重试等等计算治理问题。
<center>图 5</center>想象你有 10 个并发任务数过百的上层应用,不管是基于 Web 的 IDE 开发环境、可视化 BI 系统,还是报表系统、工作流调度系统等,每个接入 3 个底层计算引擎。上述的计算治理问题,你可能得逐一重复的去解决 10*3=30 遍,而这正是当前在各个公司不断发生的现实情况,其造成的人力浪费不可小觑。
扩展难问题,上层应用新增对接底层计算引擎,维护成本高,改动大。
在 CS 的紧耦合模式下,上层应用每新增对接一个底层计算引擎,都需要有较大改动。
以对接 Spark 为例,在上层应用系统中的每一台需要提交 Spark 作业的机器,都需要部署和维护好 Java 和 Scala 运行时环境和变量,下载和部署 Spark Client 包,且配置并维护 Spark 相关的环境变量。如果要使用 Spark on YARN 模式,那么你还需要在每一台需要提交 Spark 作业的机器上,去部署和维护 Hadoop 相关的 jar 包和环境变量。再如果你的 Hadoop 集群需要启用 Kerberos 的,那么很不幸,你还需要在上述的每台机器去维护和调试 keytab、principal 等一堆 Kerberos 相关配置。
<center>图 6</center>这还仅仅是对接 Spark 一个底层引擎。随着上层应用系统和底层引擎的数量增多,需要维护的关系会是个笛卡尔积式的增长,光 Client 和配置的部署维护,就会成为一件很令人头疼的事情。
应用孤岛问题,跨不同应用工具、不同计算任务间的互通问题。
多个相互有关联的上层应用,向后台引擎提交执行的不同计算任务之间,往往是有所关联和共性的,比如需要共享一些用户定义的运行时环境变量、函数、程序包、数据文件等。当前情况往往是一个个应用系统就像一座座孤岛,相关信息和资源无法直接共享,需要手动在不同应用系统里重复定义和维护。
典型例子是在数据批处理程序开发过程中,用户在数据探索开发 IDE 系统中定义的一系列变量、函数,到了数据可视化系统里往往又要重新定义一遍; IDE 系统运行生成的数据文件位置和名称,不能直接方便的传递给可视化系统;依赖的程序包也需要从 IDE 系统下载、重新上传到可视化系统;到了工作流调度系统,这个过程还要再重复一遍。不同上层应用间,计算任务的运行依赖缺乏互通、复用能力。
<center>图 7</center>除了上述的架构层面问题,要想让复杂分布式架构环境下,各种类型的计算任务,都能更简洁、灵活、有序、可控的提交执行,和成功返回结果,计算治理还需关注高并发,高可用,多租户隔离,资源管控,安全增强,计算策略等等细化特性问题。这些问题都比较直白易懂,这里就不一一展开论述了。
计算中间件 Linkis,是微众银行专门设计用来解决上述紧耦合、重复造轮子、扩展难、应用孤岛等计算治理问题的。当前主要解决的是复杂分布式架构的典型场景-数据平台环境下的计算治理问题。
Linkis 作为计算中间件,在上层应用和底层引擎之间,构建了一层中间层。能够帮助上层应用,通过其对外提供的标准化接口(如 HTTP, JDBC, Java …),快速的连接到多种底层计算存储引擎(如 Spark、Hive、TiSpark、MySQL、Python 等),提交执行各种类型的计算任务,并实现跨上层应用间的计算任务运行时上下文和依赖的互通和共享。且通过提供多租户、高并发、任务分发和管理策略、资源管控等特性支持,使得各种计算任务更灵活、可靠、可控的提交执行,成功返回结果,大大降低了上层应用在计算治理层的开发和运维成本、与整个环境的架构复杂度,填补了通用计算治理软件的空白。(图 8、9 )
<center>图 8</center> <center>图 9</center>要更详细的了解计算任务通过 Linkis 的提交执行过程,我们先来看看 Linkis 核心的“计算治理服务”部分的内部架构和流程。如下图 10:
<center>图 10</center>计算治理服务:计算中间件的核心计算框架,主要负责作业调度和生命周期管理、计算资源管理,以及引擎连接器的生命周期管理。
公共增强服务:通用公共服务,提供基础公共功能,可服务于 Linkis 各种服务及上层应用系统。
其中计算治理服务的主要模块如下:
入口服务 Entrance,负责接收作业请求,转发作业请求给对应的 Engine,并实现异步队列、高并发、高可用、多租户隔离。
应用管理服务 AppManager,负责管理所有的 EngineConnManager 和 EngineConn,并提供 EngineConnManager 级和 EngineConn 级标签能力;加载新引擎插件,向 RM 申请资源, 要求 EM 根据资源创建 EngineConn ;基于标签功能,为作业分配可用 EngineConn。
资源管理服务 ResourceManager,接收资源申请,分配资源,提供系统级、用户级资源管控能力,并为 EngineConnManager 级和 EngineConn 提供负载管控。
引擎连接器管理服务 EngineConn Manager,负责启动 EngineConn,管理 EngineConn 的生命周期,并定时向 RM 上报资源和负载情况。
引擎连接器 EngineConn,负责与底层引擎交互,解析和转换用户作业,提交计算任务给底层引擎,并实时监听底层引擎执行情况,回推相关日志、进度和状态给 Entrance。
如图 10 所示,一个作业的提交执行主要分为以下 11 步:
1. 上层应用向计算中间件提交作业,微服务网关 SpringCloud Gateway 接收作业并转发给 Entrance。
2. Entrance 消费作业,为作业向 AppManager 申请可用 EngineConn。
3. 如果不存在可复用的 Engine,AppManager 尝试向 ResourceManager 申请资源,为作业启动一个新 EngineConn。
5. EngineConnManager 启动新 EngineConn,并主动回推新 EngineConn 信息。
6. AppManager 将新 EngineConn 分配给 Entrance,Entrance 将 EngineConn 分配给用户作业,作业开始执行,将计算任务提交给 EngineConn。
7. EngineConn 将计算任务提交给底层计算引擎。
8. EngineConn 实时监听底层引擎执行情况,回推相关日志、进度和状态给 Entrance,Entrance 通过 WebSocket,主动回推 EngineConn 传过来的日志、进度和状态给上层应用系统。
9. EngineConn 执行完成后,回推计算任务的状态和结果集信息,Entrance 将作业和结果集信息更新到 JobHistory,并通知上层应用系统。
10. 上层应用系统访问 JobHistory,拿到作业和结果集信息。
11. 上层应用系统访问 Storage,请求作业结果集。
在复杂分布式环境下,一个计算任务往往不单会是简单的提交执行和返回结果,还可能需要面对提交失败、执行失败、hang 住等问题,且在大量并发场景下还需通过计算任务的调度分发,解决租户间互相影响、负载均衡等问题。
Linkis 通过对计算任务的标签化,实现了在任务调度、分发、路由等方面计算任务管理策略的支持,并可按需配置超时、自动重试,及灰度、多活等策略支持。如下图 11。
<center>图 11</center>说完了业务架构,我们现在来聊聊技术架构。在计算治理层环境下,很多类型的计算任务具有生命周期较短的特征,如一个 Spark job 可能几十秒到几分钟就执行完,EngineConn ( EnginConnector )会是大量动态启停的状态。前端用户和 Linkis 中其他管理角色的服务,需要能够及时动态发现相关服务实例的状态变化,并获取最新的服务实例访问地址信息。同时需要考虑,各模块间的通信、路由、协调,及各模块的横向扩展、负载均衡、高可用等能力。
基于以上需求,Linkis 实际是基于 Spring Cloud 微服务框架技术,将上述的每一个模块 /角色,都封装成了一个微服务,构建了多个微服务组,整合形成了 Linkis 的完整计算中间件能力。如下图 12:
<center>图 12</center>从多租户管理角度,上述服务可区分为租户相关服务,和租户无关服务两种类型。租户相关服务,是指一些任务逻辑处理负荷重、资源消耗高,或需要根据具体租户、用户、物理机器等,做隔离划分、避免相互影响的服务,如 Entrance、EnginConn ( EnginConnector ) Manager、EnginConn ;其他如 App Manger、Resource Manager、Context Service 等服务,都是租户无关的。
Eureka 承担了微服务动态注册与发现中心,及所有租户无关服务的负载均衡、故障转移功能。
Eureka 有个局限,就是在其客户端,对后端微服务实例的发现与状态刷新机制,是客户端主动轮询刷新,最快可设 1 秒 1 次(实际要几秒才能完成刷新)。这样在 Linkis 这种需要快速刷新大量后端 EnginConn 等服务的状态的场景下,时效得不到满足,且定时轮询刷新对 Eureka server、对后端微服务实例的成本都很高。
为此我们对 Spring Cloud Ribbon 做了改造,在其中封装了 Eureka client 的微服务实例状态刷新方法,并把它做成满足条件主动请求刷新,而不会再频繁的定期轮询。从而在满足时效的同时,大大降低了状态获取的成本。如下图 13:
<center>图 13</center>Spring Cloud Gateway 承担了外部请求 Linkis 的入口网关的角色,帮助在服务实例不断发生变化的情况下,简化前端用户的调用逻辑,快速方便的获取最新的服务实例访问地址信息。
Spring Cloud Gateway 有个局限,就是一个 WebSocket 客户端只能将请求转发给一个特定的后台服务,无法完成一个 WebSocket 客户端通过网关 API 对接后台多个 WebSocket 微服务,而这在我们的 Entrance HA 等场景需要用到。
为此 Linkis 对 Spring Cloud Gateway 做了相应改造,在 Gateway 中实现了 WebSocket 路由转发器,用于与客户端建立 WebSocket 连接。建立连接成功后,会自动分析客户端的 WebSocket 请求,通过规则判断出请求该转发给哪个后端微服务,然后将 WebSocket 请求转发给对应的后端微服务实例。详见 Github 上 Linkis 的 Wiki 中,“Gateway 的多 WebSocket 请求转发实现”一文。
<center>图 14</center>**Spring Cloud OpenFeign ** 提供的 HTTP 请求调用接口化、解析模板化能力,帮助 Linkis 构建了底层 RPC 通信框架。但基于 Feign 的微服务之间 HTTP 接口的调用,只能满足简单的 A 微服务实例根据简单的规则随机选择 B 微服务之中的某个服务实例,而这个 B 微服务实例如果想异步回传信息给调用方,是无法实现的。同时,由于 Feign 只支持简单的服务选取规则,无法做到将请求转发给指定的微服务实例,无法做到将一个请求广播给接收方微服务的所有实例。Linkis 基于 Feign 实现了一套自己的底层 RPC 通信方案,集成到了所有 Linkis 的微服务之中。一个微服务既可以作为请求调用方,也可以作为请求接收方。作为请求调用方时,将通过 Sender 请求目标接收方微服务的 Receiver ;作为请求接收方时,将提供 Receiver 用来处理请求接收方 Sender 发送过来的请求,以便完成同步响应或异步响应。如下图示意。详见 GitHub 上 Linkis 的 Wiki 中,“Linkis RPC 架构介绍”一文。
<center>图 15</center>至此,Linkis 对上层应用和底层引擎的解耦原理,其核心架构与流程设计,及基于 Spring Cloud 微服务框架实现的,各模块微服务化动态管理、通信路由、横向扩展能力介绍完毕。
Linkis 作为计算中间件,在上层应用和底层引擎之间,构建了一层中间层。上层应用所有计算任务,先通过 HTTP、WebSocket、Java 等接口方式提交给 Linkis,再由 Linkis 转交给底层引擎。原有的上层应用以 CS 模式直连底层引擎的紧耦合得以解除,因此实现了解耦。如下图 16 所示:
<center>图 16</center>通过解耦,底层引擎的变动有了 Linkis 这层中间件缓冲,如引擎 client 的版本升级,无需再对每一个对接的上层应用做逐个改动,可在 Linkis 层统一完成。并能在 Linkis 层,实现对上层应用更加透明和友好的升级策略,如灰度切换、多活等策略支持。且即使后继接入更多上层应用和底层引擎,整个环境复杂度也不会有大的变化,大大降低了开发运维工作负担。
有了 Linkis,上层应用可以基于 Linkis,快速实现对多种后台计算存储引擎的对接支持,及变量、函数等自定义与管理、资源管控、多租户、智能诊断等计算治理特性。
以微众银行与 Linkis 同时开源的,交互式数据开发探索工具 Scriptis 为例,Scriptis 的开发人员只需关注 Web UI、多种数据开发语言支持、脚本编辑功能等纯前端功能实现,Linkis 包办了其从存储读写、计算任务提交执行、作业状态日志更新、资源管控等等几乎所有后台功能。基于 Linkis 的大量计算治理层能力的复用,大大降低了 Scriptis 项目的开发成本,使得 Scritpis 目前只需要有限的前端人员,即可完成维护和版本迭代工作。
如下图 17,Scriptis 项目 99.5% 的代码,都是前端的 JS、CSS 代码。后台基本完全复用 Linkis。
<center>图 17</center>模块化可插拔的计算引擎接入设计,新引擎接入简单快速。
对于典型交互式模式计算引擎(提交任务,执行,返回结果),用户只需要 buildApplication 和 executeLine 这 2 个方法,就可以完成一个新的计算引擎接入 Linkis,代码量极少。示例如下。
(1) AppManager 部分:用户必须实现的接口是 ApplicationBuilder,用来封装新引擎连接器实例启动命令。
1. //用户必须实现的方法: 用于封装新引擎连接器实例启动命令
2. def buildApplication(protocol:Protocol):ApplicationRequest
(2) EngineConn 部分:用户只需实现 executeLine 方法,向新引擎提交执行计算任务:
1. //用户必须实现的方法:用于调用底层引擎提交执行计算任务
2. def executeLine(context: EngineConnContext,code: String): ExecuteResponse
引擎相关其他功能 /方法都已有默认实现,无定制化需求可直接复用。
通过 Linkis 提供的上下文服务,和存储、物料库服务,接入的多个上层应用之间,可轻松实现环境变量、函数、程序包、数据文件等,相关信息和资源的共享和复用,打通应用孤岛。
<center>图 18</center>Context Service ( CS )为不同上层应用系统,不同计算任务,提供了统一的上下文管理服务,可实现上下文的自定义和共享。在 Linkis 中,CS 需要管理的上下文内容,可分为元数据上下文、数据上下文和资源上下文 3 部分。
<center>图 19</center>元数据上下文,定义了计算任务中底层引擎元数据的访问和使用规范,主要功能如下:
提供用户的所有元数据信息读写接口(包括 Hive 表元数据、线上库表元数据、其他 NoSQL 如 HBase、Kafka 等元数据)。
计算任务内所需元数据的注册、缓存和管理。
数据上下文,定义了计算任务中数据文件的访问和使用规范。管理数据文件的元数据。
运行时上下文,管理各种用户自定义的变量、函数、代码段、程序包等。
同时 Linkis 也提供了统一的物料管理和存储服务,上层应用可根据需要对接,从而可实现脚本文件、程序包、数据文件等存储层的打通。
Linkis 计算治理细化特性设计与实现介绍,在高并发、高可用、多租户隔离、资源管控、计算任务管理策略等方面,做了大量细化考量和实现,保障计算任务在复杂条件下成功执行。
Linkis 的 Job 基于多级异步设计模式,服务间通过高效的 RPC 和消息队列模式进行快速通信,并可以通过给 Job 打上创建者、用户等多种类型的标签进行任务的转发和隔离来提高 Job 的并发能力。通过 Linkis 可以做到 1 个入口服务( Entrance )同时承接超 1 万+ 在线的 Job 请求。
多级异步的设计架构图如下:
<center>图 20</center>如上图所示 Job 从 GateWay 到 Entrance 后,Job 从生成到执行,到信息推送经历了多个线程池,每个环节都通过异步的设计模式,每一个线程池中的线程都采用运行一次即结束的方式,降低线程开销。整个 Job 从请求—执行—到信息推送全都异步完成,显著的提高了 Job 的并发能力。
这里针对计算任务最关键的一环 Job 调度层进行说明,海量用户成千上万的并发任务的压力,在 Job 调度层中是如何进行实现的呢?
在请求接收层,请求接收队列中,会缓存前端用户提交过来的成千上万计算任务,并按系统 /用户层级划分的调度组,分发到下游 Job 调度池中的各个调度队列;到 Job 调度层,多个调度组对应的调度器,会同时消费对应的调度队列,获取 Job 并提交给 Job 执行池进行执行。过程中大量使用了多线程、多级异步调度执行等技术。示意如下图 21:
<center>图 21</center>Linkis 还在高可用、多租户隔离、资源管控、计算任务管理策略等方面,做了很多细化考量和实现。篇幅有限,在这里不再详述每个细化特性的实现,可参见 Github 上 Linkis 的 Wiki。后继我们会针对 Linkis 的计算治理-理之路( Insight )的细化特性相关内容,再做专题介绍。
基于如上解耦、复用、快速扩展、连通等架构设计优点,及高并发、高可用、多租户隔离、资源管控等细化特性实现,计算中间件 Linkis 在微众生产环境的应用效果显著。极大的助力了微众银行一站式大数据平台套件 WeDataSphere 的快速构建,且构成了 WeDataSphere 全连通、多租户、资源管控等企业级特性的基石。
Linkis 在微众应用情况如图 22:
<center>图 22</center>我们已将 Linkis 开源,Github repo 地址:https://github.com/WeBankFinTech/Linkis。
欢迎对类似计算治理问题感兴趣的同学,参与到计算中间件 Linkis 的社区协作中,共同把 Linkis 建设得更加完善和易用。
作者介绍:邸帅,微众银行大数据平台负责人,主导微众银行 WeDataSphere 大数据平台套件的建设运营与开源,具备丰富的大数据平台开发建设实践经验。
本文是「分布式系统前沿技术」专题文章,目前该专题在持续更新中,欢迎大家保持关注👇