容器在分布式集群中应该如何优雅的组合?

packy

最近有幸拜读了Google在 2016年 发表的一篇论文,《Design patterns for container-based distributed systems》,这篇文章中描绘了几种在容器编排系统中使用容器的方式,即如何将不同服务的容器进行有机组合,并使得这些模式成为最佳实践方式,让整个分布式系统更加可靠和可维护,虽然已经过去5年之久,但文章中的思想仍然影响着对容器技术和分布式系统的发展。

文章中将设计模式分成了三个大类:

  • 用于容器管理的单个容器应该如何实现
  • 单个节点中,多个容器怎样合作共存
  • 多个节点情境下的分布式算法
有兴趣的话欢迎来我的公众号交流:packy的技术杂货铺

单容器管理模式

其实这种模式只是说明单一的容器应该怎样与上下游进行交互,边界应该如何定义。文章中不止一次将容器比作面向对象思想中的对象。说明我们应该按照面向对象的思路来看待这个问题,一个容器应该与一个对象一样有一个边界。对上层要提供接口来暴露应用信息,比如监控的metrics(QPS,健康信息等);对下层应该定义一个原生的生命周期,从而管理系统更容易对容器进行控制,同时易开发一些相关组件。

对上层的统一接口这个比较好理解,就像 K8s 中已经使用就绪探针(readness probe)和存活探针(liveness probe)监测服务。对下层的生命周期呢,文章举了一个例子:集群中有多个任务在执行,这些任务都拥有不同的优先级,有的优先级高,有的优先级低。当集群资源紧张时,就需要让优先级高的任务先执行,但是优先级低的任务已经在执行了,系统该怎么办呢,总不能直接把优先级低的任务 kill -9 吧。这个时候就需要生命周期来规定容器如何停止。也就是说生命周期可以让编排系统更容易不出错的管理容器,生命周期就是开发者、管理系统以及容器的一纸契约。上述例子中,K8s 中通过 graceful deletion 来关闭和驱逐低优先级的任务,K8s 先向容器发送一个 SIGTERM 信号,表示你要马上停止运行,该保存的东西保存,文件该关闭的关闭,经过一定时间后才发送SIGKILL信号来终结任务。

单节点,多容器应用模式

对于多容器应用,即多个容器需要共同协作完成对外的服务,因此多个容器是被视为一个整体调度到一个节点上的,在 K8s 中通过 Pod 的机制来应对这种场景,即多个容器运行在同一个pod中,有一个主容器提供业务服务,其他容器协助。这种场景下,文章中又总结出三种设计模式:sidecar(边车模式)、Ambassador(大使/代理模式)、Adapter(适配器模式)。

Sidecar 模式

了解过 istio 的童鞋对这个词肯定不陌生,但是此处的 sidecar 非 istio中的sidecar 概念。这里的 sidecar 只是扩展并增强主容器。sidecar 可以与主容器共享磁盘空间,因此把日志收集功能作为sidecar容器就是一个很好的选择(例如阿里开源的 log-pilot)。
sidecar-pattern

把增强功能和主业务功能通过容器分离,有以下几个优势:

  1. 容器作为资源分配的单元,可以更好的预估/划分所占用资源,
  2. 不同容器可以由不同团队独立维护,划分开发责任。
  3. sidecar 容器容易被复用
  4. 容器提供了一个故障遏制边界,即使sidecar容器出现问题,也不会影响主容器的业务服务
  5. 可以独立的维护,包括升级、回滚操作。

Ambassador 模式

这种模式其实是 istio的基础,劫持代理外部与主容器的交互流量。如图
ambassador-pattern

这样做的好处显而易见,主容器无需关注外界的环境是复杂还是简单,只需要知道要服务提供方的标识即可,具体如何连接;服务提供方是集群还是单点;甚至使用什么协议连接都不需要关注,这些都由 ambassador 容器来完成。istio 中使用 envoy 来代理流量时,提供更复杂的操作来帮助 istio 构成整个控制面,包括路由、熔断、服务注册发现等。

Adapter 模式

适配器模式与 Ambassador 模式正相反,Ambassador 简化了主容器对外部环境的认知,而适配器模式则是简化了应用对外的呈现形式。举个例子,现在服务的监控是一个必选项,然而不同的服务应用使用的监控方案则是五花八门,有用 Prometheus 的,有 JMX 的。如果我们需要构建一个统一的监控平台,面对这么多不同技术栈的 metrics 着实头疼。这个时候就需要一个容器,将主容器的 metrics 按照统一的标准进行翻译,监控平台就只需要对接一种标准即可。
adapter-pattern

多节点应用模式

除了在单个机器上协作容器之外,模块化容器可以更轻松地构建协作的多节点分布式应用程序。后面的几种设计模式也都是基于 Pod 的概念构建的。

leader 选举

这是一个在分布式系统中很常见的问题,通常leader选举的实现是由不同的编程语言来实现。文章中给出了一种模式,Pod 中的主容器运行着需要进行leader选举的应用服务,另外由单独的一个容器仅执行leader 选举算法(暂时称为leader选举容器),leader选举容器对主容器暴露一个通用的 HTTP 或其他类型的接口,主容器通过调用 localhost 接口即可获知自己当前的身份了。如图
leader-election-pattern

这样做就可以把复杂的实现部分放在独立的容器中,更容易对主容器的进行开发与维护。

工作队列

工作队列所完成的任务类似于任务调度系统(例如 Python 中的 Celery)。与leader选举的模式类似,文章中提出的设计模式,仍是为了将分布式系统中常见的工作队列逻辑从具体的业务逻辑中剥离出来,并使其平台化、语言无关化。容器通过实现run()mount()接口来构建通用的工作队列,即开发者只需要针对工作队列中某个具体阶段的逻辑进行开发,最终的代码打包等动作都可以由此框架来处理。

work-queue

最近CNCF孵化的项目 argoproj/argo-workflows: Workflow engine for Kubernetes (github.com) 的架构设计也借鉴于此。

Scatter/gather 模式

这种模式可以通俗的理解为云原生的 MapReduce。一台根服务器请求大量的分区服务器去并行的处理数据,每个分区服务器返回部分数据,并由根服务器进行汇总返回。这个流程可以归结为一个样板:1.分散请求;2.收集响应数据;3.与客户端交互。因此这种模式只需要两种容器的支持:一种容器做分区数据处理;一种容器做数据聚合(如下图所示)。

总结

总的来说,这篇文章中描述了分布式情况下,容器应该怎样实现、单一应用多容器服务该如何合作共存。相较于SOA架构系统来说,面向容器和SOA都注重于通过网络通信的接口实现组件的复用,但是 SOA 拆分的组件粒度更大,同时耦合度比上文中提到的共生容器更加的松散。另一方面,SOA 更注重与业务,本篇文章描述的面向容器的思想则更注重于构建通用的中间件,从而更容易构建分布式系统。

在近几年落地方案中,其实很多都是由文章中的设计模式发展而来。比如 istio 使用的 sidecar,正如这里的 Ambassador 模式,通过代理劫持流量实现流量的管理,数据面与控制面的分离;以前使用过的阿里开源的用于采集容器日志的工具 log-pilot (AliyunContainerService/log-pilot: Collect logs for docker containers (github.com)),则是使用的 sidecar 模式;以及用来定义容器相关的生命周期的 CRI CNI 这种标准接口。未来肯定还会有更多的应用情景需要思考分布式的容器服务该怎样落地,这篇文章都会成为一种指导和启发。正如论文中总结写到的

We believe that the set of container patterns will only grow, and that in the coming years they will revolutionize distributed systems programming much as object-oriented programming did in earlier decades, in this case by enabling a standardization and regularization of distributed system development.

我们相信容器模式集只会增长,并且在未来几年它们将彻底改变分布式系统编程,就像前几十年面向对象编程所做的那样,从而标准化和规范化分布式系统的开发.

阅读 233
7 声望
5 粉丝
0 条评论
你知道吗?

7 声望
5 粉丝
宣传栏