用户编写的分布式计算应用程序需要部署到生产环境中执行。对于终端用户来说,通常直接与基于计算引擎定制的计算平台交互,提交任务只需要在计算平台上上传打包后的应用或者 SQL 代码,点击运行就可以神奇地执行。对于计算平台来说,如何利用计算引擎提供的功能,高效且安全地将用户应用部署并执行则是一个难题。

实际落实分布式计算应用部署的是计算平台、计算引擎和资源集群,进一步抽象地说,计算平台对应的是提交应用的客户端,计算引擎提供执行应用部署的框架逻辑,而资源集群提供应用运行的物理资源。

其中,部署的灵活性是由计算引擎提供的框架逻辑来实现的。现实世界中的分布式计算引擎,例如 Apache Spark 和 Apache Flink 等,分别提供了不同的部署策略,但是这些策略在类型上却有着极高的相似性。

本文首先介绍 Spark 和 Flink 的部署模式,再综合讨论这些模式的适用场景,以及现实世界的采用情况。

Spark 的部署模式

要想讨论 Spark 应用的部署,首先要了解参与部署过程的角色。

Spark 应用的部署主要涉及三个角色,即 Driver、Executor 和资源集群管理器。

Driver 是控制 Spark 应用的进程,负责控制整个 Spark 应用的执行,并且维护 Spark 应用集群的状态,即 Executor 集群的任务和状态。Driver 必须和资源集群管理器交互,申请物理资源并启动 Executor 执行任务。

Executor 也是一个进程,实际执行 Driver 分配的任务。Executor 接收 Driver 分配的任务,运行接收到的任务,并在完成任务时向 Driver 报告任务的执行结果。

资源集群管理器相对 Spark 应用来说是一个外部进程,负责维护一组运行 Spark 应用的机器。举例来说,可以是 YARN 的 ResourceManager 或者 Kubernetes 的 ApiServer 等。它负责管理物理资源,根据 Driver 的请求分配物理资源以支持 Executor 运行并执行任务。

可以看到,Executor 总是运行在资源集群管理器管理的物理资源上的,而 Driver 没有这样的定义。Spark 的部署模式正是针对 Driver 运行的物理资源位置来区分的,分为客户端部署模式和集群部署模式。

客户端部署模式(Client Mode),顾名思义,即 Driver 运行在提交任务的客户端上,其部署图例如下图所示。

android 分布式部署 分布式应用如何部署_hadoop

集群部署模式(Cluster Mode),则是客户端将 Driver 作为应用程序提交给资源集群管理器,由资源集群管理器分配资源运行 Driver 进程,其部署图例如下图所示。

android 分布式部署 分布式应用如何部署_java_02

Flink 的部署模式

同样的,讨论 Flink 应用的部署模式之前,先介绍参与部署过程的角色。

Flink 应用的部署主要涉及四个角色,即 Client、JobManager、TaskManager 和资源集群管理器。

其中,TaskManager 和资源集群管理器与 Spark 中的 Executor 和资源集群管理器基本是相对应的概念。

JobManager 基本承担了 Spark 中 Driver 的角色。其中的 ResourceManager 组件和和资源集群管理器交互,申请物理资源并启动 TaskManager 进程;其中的 JobMaster 组件管理单个作业的状态,包括向 TaskManager 分配具体的任务,监控 TaskManager 的任务和状态等。

不同之处在于,JobManager 拥有一个 Dispatcher 组件,以支持同时管理多个作业。同时,JobManager 可以被预先启动,并在随后由 Dispatcher 从 Client 接收作业提交并启动一个 JobMaster 组件线程来管理这个作业。即使业务流程只需要提交一个作业,依然会有这样一个 Client 向 Dispatcher 提交作业的流程。

Flink 的部署模式针对 Client 运行的物理资源位置和是否预先启动了 JobManager 分为三种模式。

会话模式(Session)指的是 JobManager 预先启动,随后 Client 从客户端运行并向 JobManager 提交编译后的作业图的模式,其部署图例如下图所示。

android 分布式部署 分布式应用如何部署_编程语言_03

作业模式(PerJob)指的是 Client 从客户端运行,将单个作业编译成作业图后,打包为一个预定义形式的应用程序提交给资源集群管理器,由资源集群管理器分配资源运行 JobManager 进程,JobManager 进程启动后由特殊定制的作业恢复机制以恢复的流程运行上述作业,其部署图例如下图所示。

android 分布式部署 分布式应用如何部署_java_04

应用模式(Application)指的是客户端将绑定了作业的 Client 一并作为应用程序提交给资源集群管理器,由资源集群管理器分配资源运行 Client 和 JobManager 的融合进程,在同一进程中完成作业提交动作并运行作业,其部署图例如下图所示。

android 分布式部署 分布式应用如何部署_hadoop_05

现实世界的部署模式

上面列举了 Spark 的两种部署模式和 Flink 的三种部署模式,在现实世界里,什么情况下会采用哪种模式呢?又或者,哪一种部署模式是最适合生产环境的呢?

前面我们提到,分布式计算应用的部署,跨越了用户、计算平台和资源集群。其中,计算平台作为面向终端用户的界面,通常致力于减轻用户的运营负担,来简化用户应用的提交过程。为了提交分布式计算应用,这些平台通常只公开一个集中式的端点,例如 Web 前端,用于应用提交,也就是上图中的部署器(Deployer),部署器同时代表了逻辑组件和计算平台用于运行逻辑组件的物理资源。

上面列举的不同的部署模式,关键的区分点在于从用户打包的应用到计算集群实际运行起来,这个过程的不同阶段的负担分别由谁来承担。更具体的说,是部署器,还是资源集群?不难理解,对于集中式的部署器来说,平台更希望将这个部署器作为一个轻量级的交互页面,提供提交作业、查询状态和操作作业的服务,而将重量级的实际部署动作都交给资源集群来承担。出于这一观察,我们下面从部署器和资源集群的不同分工带来的不同性质讨论部署模式的适用性。

应用的隔离

Spark 的客户端部署模式和 Flink 的会话模式与作业模式,都有用户的应用逻辑运行在部署器上。在集中式的部署器上运行不同用户的应用,不仅仅代表着更大的资源开销,也代表着在部署器上的不同应用之间难以相互隔离。

例如,采用线程的方式来处理不同的用户应用,由于用户使用 Java 或 Scala 编写的代码具有完备性,代码实际可以做非常有破坏性的行为。例如,在执行逻辑中使用 System.exit(-1) 退出进程。这在本地调试的时候可能无伤大雅,但是在部署器上执行此类逻辑则会将整个部署器进程退出,导致计算平台交互界面不可用,显然也就影响了其他应用和用户的运行和体验。

如果采用进程的方式来处理不同的用户应用,那么平台元数据的共享又将成为一个问题,同时进程的方式显然需要更多的资源,而只要这些应用的部署和监控的客户端运行在同一个物理资源上,那么这种资源占用势必带来潜在的相互影响风险。例如,一个应用错误地占用了所有的网络端口,或者将磁盘写满,都会跨进程的影响其他应用。

反过来,采用 Spark 的集群部署模式和 Flink 的应用模式,不会在部署器运行任何用户逻辑,所有的动作都是计算平台和计算引擎已知的动作,在这两者没有错误的情况下,能够把用户应用的错误隔离在资源集群的运行容器中,也就实现了应用的隔离。

依赖的获取

依赖的获取也是分布式计算应用运行的一个重要步骤。现实世界的应用往往建立在丰富的三方库上,因此在运行时必须要求这些三方库的文件全部就位,此外还有可能在逻辑中依赖其他的文件。在用户、计算平台和资源集群三个实体中,总得有一个角色来收集齐所有的依赖文件。

Spark 的客户端模式要求最迟在计算平台收集齐所有的依赖文件,而 Flink 的会话与作业模式都要求在计算平台收集齐所有的依赖文件,并进一步上传到资源集群以支持 JobManager 的实际运行。在这种背景下,计算平台或者说部署器有非常大的文件上传和下载压力,更加容易成为资源上的瓶颈。

如果收集依赖的动作由用户来完成,则会导致一个超大的打包应用,这个打包应用本身上传到计算平台的开销就不可忽略,而且很多依赖是通用依赖,要求不同应用分别打包一份通常是用户不可接受的。如果收集依赖的动作由计算平台来完成,不仅放大了计算平台的网络压力,同时发现或者说寻找依赖和下载依赖的权限都将是运维上巨大的难题。

反过来,采用 Spark 的集群部署模式和 Flink 的应用模式,由于用户应用的运行推迟到资源集群上发生,那么平台就有机会通过资源集群的运维和定制将通用依赖提前部署到运行容器中,在不影响部署器的情况下减少用户可见的依赖获取的开销。此外,对于定制场景,应用特有的依赖通常与其实际执行的资源集群归属于同一个团队,这样应用就可以在其实际执行的资源集群上内部获取额外的依赖,这通常部署器直接下载要有更快的速度。

Flink 社区在提议应用模式的 FLIP-85 提案中,阿里巴巴和 Uber 的工程师分别给出了这一优化带来的打包应用十倍的瘦身以及部署时间几十倍的提升的数据。

权限的粒度

在依赖获取一节中也有提到,对于重部署器的模式,有些依赖存在于资源集群上,需要被部署器收集到本地,这实际上暗含了资源集群授予部署器拉取其上资源的权限。

现实世界中,分布式计算应用运行的资源集群,未必属于计算平台团队。一个常见的例子是用户提供或购买定制自己的资源集群,并提供这个资源集群作为应用的运行载体。在这种情况下,显然用户只希望计算平台获取提交应用的权限以支持将他们自己的应用提交到资源集群上,走完流程。

但是,依赖的获取要求资源集群额外授予计算平台拉取其上资源的权限,用户应用其他自然的与资源集群交互的逻辑可能要求更多的权限。为了彻底杜绝部署时的问题,往往计算平台会要求客户提供的资源集群对计算平台开放所有权限。这不仅是权限过分泄露给计算平台,同时在大部分计算平台难以或者缺少对应用和资源集群的关联关系管理的情况下,可能发生其他团队的应用以计算平台为跳板攻另一个团队的资源集群的情况。

显然,采用 Spark 的集群部署模式和 Flink 的应用模式,有赖于用户应用执行场所的改变,权限的授予粒度可以收缩到仅支持应用提交上。同时,这样资源集群和应用逻辑的交互,应用逻辑的身份就不再是如同马赛克一般的计算平台,而是具体的应用用户,可以通过鉴权来避免权限提升攻。

小结

显而易见,从资源隔离、部署提速和权限区分等角度来看,计算平台不再代理全部或部分用户应用逻辑,而是将用户应用的全部都运行在资源集群上,是更适合生产环境的部署模式。也就是说,在生产环境上更推荐 Spark 的集群部署模式和 Flink 的应用模式。

Spark 的客户端部署模式,更多时候用于快速调试,将 Driver 的逻辑运行在客户端本地,更容易查询日志和运行调试器。

Flink 的所有模式,其实对应 Spark Driver 的 JobManager 都是运行在资源集群上的。但是会话和作业模式下,用户作业的编译,即用户应用的部分逻辑运行在客户端上,也就导致了同样的客户端执行的问题。

会话模式旨在维护一个跨作业的 Flink 管理集群,尤其是 TaskManager 不随着作业完结销毁,并且可以跨作业共享。但是实际效果甚微,因为流式计算应用通常要求独占资源,而且由于 Flink 的资源抽象缺陷,TaskManager 的资源在回收后其实很难有效复用,不是造成浪费就是需要重新分配。不过,会话模式对于大部分不关心这些资源问题,而且不想自己定制一个计算平台的企业来说,仍然不失为一个简单的开箱即用的计算平台,因为它支持多次作业提交,并且有一个 Web 页面可以查看不同作业的运行情况。

作业模式其实是为了资源独占而实现的一种临时模式,同时作为 adhoc 的优化合并了先启动 JobManager 再提交作业的步骤。但是它不是应用模式这样彻底的将用户应用都交付在资源集群运行,而是残留了作业编译的逻辑在客户端。同时,优化作业提交的方式是引入一个定制的作业恢复逻辑,通过作业恢复的流程提交作业,其实是整体概念的混乱,这种概念的错位传播到了整个作业生命周期的管理上,几乎每一个步骤都需要考虑这种错位带来的变化并对它进行特殊处理。

同时,Flink 的会话模式和作业模式,在客户端提交作业图后,还会保持客户端和 JobManager 的链接以等待作业的结果。为了避免流式计算应用无谓的等待和避免一些长时任务的开销,又引入了 detach 开关以支持在提交后断开客户端和 JobManager 的链接。这些模式混合起来,造成了整个客户端交互抽象极大的混乱和难以理解。

实际上,就系统设计而言,反而是 Spark 这样能够几句话讲清楚,概念明晰的系统更优秀。对于 Flink 这样充满了 trouble shooting 的系统,虽然可能在讲解它的各种明枪暗箭的时候有一种对这个系统很熟悉的快感错觉,但是实际上是因为它的系统设计混乱,从而导致开发者和用户无谓地需要多了解很多底层细节甚至 trouble shooting 的适用场景。

更广泛地讲,一个软件用的很复杂,有很多可以讲的细节和暗礁,未必是它的复杂度带来的固有问题,掌握这些暗礁也未必能提升开发者对整体问题的把握。例如对于有状态的流式计算系统来说,专有的关键问题是时间属性的处理和状态的管理等等;例如 Git 的不少用法其实是将底层的细节暴露给用户,对于许多终端用户来说是不必要的知识负担等等。

由于这种越复杂,暗礁越多,越显示开发者对软件的理解的迷思过于常见,这里特地提出来反驳。

后记

分布式计算应用的部署是我在某厂实现 Flink 的应用模式时调研理解的一个主题。对应的提案 FLIP-85 在社区由 Uber 的工程师 Zhengqiu Huang 提出,我和阿里巴巴的工程师 Yang Wang 根据实践的经验提出了补充,最终由社区的 Kostas Kloudas 完善了落地。

应用的部署可以说是独立于计算引擎的一个模块,对于所有的分布式计算应用乃至分布式应用都有相同或相似可参考的要点。同时,部署的流程也串联了用户应用、计算平台、计算引擎和资源集群,这些不同概念的界面和交互流程。交叉的地方总会蹦出灵感的火花,藉由此机会了解到整个分布式计算生态的多方参与者以及协同流程的执行细节,实在是不可多得的一个好机会。

应用的部署涉及到实际的物理资源,涉及到真实世界的权限管理和资源的开销,不同于计算引擎本身的抽象优化,是一个实打实的实践枢纽。软件开发的业界实践跟校园里教授的知识最大的不同,就是我们工程师在很多时候不再面对抽象的概念,而是实际每一个文件,每一个比特位和每一个现实系统的接口。理论和概念也许能帮助我们走在正确的方向上,但是实践和落地才是输出一个工程师的职业价值的出口。