略谈分布式系统中的容器设计模式

略谈分布式系统中的容器设计模式

谭中意

2020/3/5

前言:云原生(Cloud Native)不仅仅是趋势,更是现在进行时,它是构建现代的,可弹性伸缩的,快速迭代的计算网络服务的事实标准。其中容器编排系统Kubernetes和容器是基石。所以每个工程师都需要学习和了解他们。学习过程中,很多工程师可能会问:为什么Pod而不是容器是K8S部署的最小单位基于K8S设计分布式系统有没有什么套路?本文针对这些问题,并参考K8S创始人的很多文档,给出了解答。本文适合进行研发工作2到3年的同学,对架构设计比较感兴趣,有一定架构设计意识,同时对容器(Docker)和容器编排系统(kubernetes)有一定了解。希望可以通过此文,让同学们更深入的了解到分布式容器系统中的几种常见模式,以便以后更好的设计和实现云原生的分布式系统。

 

先从一篇论文说起

首先介绍一篇论文,标题是《Design patterns for container-based distributed systems》,作者是Brendan Burns和David Oppenheimer,论文发表于2016年,是原原生领域系统设计的代表作。

第一作者Brenda Burns,相信熟悉云原生领域的同学都认识他,他之前是Google的工程师,是Kubernetes的三位创始人之一,他在这项目中负责系统设计,很多关键的设计决策都是出自他之手,包括声明式,Pod等。

在论文中,他介绍了基于容器的分布式系统中出现的几种设计模式:单容器模式,单节点多容器模式,以及多节点模式。像之前的面对对象模式一样,这些分布式系统中的pattern可以简化开发,并使使用他们的系统更加可靠。他感言,在google做了很多年的分布式系统设计,从web search做到后来的cloud系统,基本都是从头做起,很费力,因为没有什么成熟可重用的模块;但是进入云原生时代,随着容器(docker)和容器编排(k8s)的出现,极大的改变了分布式系统的设计和开发,从而为大规模的重用提供了巨大的可能。

系统设计是随着计算机体系的发展而发展的,计算机系统从单机发展到Client/Server再到Distributed System,而系统设计也从单一的算法Algorithm(例如Quick sort)发展到OOP(面对对象的编程语言),再发展到现在容器系统。

Design Pattern是a repeatable solution to a problem,即对软件设计中普遍存在(反复出现)的各种问题所提出的解决方案,类似围棋中的定式(围棋爱好者应该很熟悉)。在GANG of Four的名著《Design Patters》中,对用面对对象语言实现的,针对编程语言中Interface级别的各种Design Pattern做了很深入的解释。其中一些设计原则,例如Single Responsibility对后续的系统设计也是非常适用。

为什么我们需要使用Design Pattern?因为绝大多数我们在系统开发中碰到的问题是相似的,可以借鉴别人的经验,所以站在巨人的肩上作创新;其次Design Pattern给我们提供了共享的概念和词汇,很容易沟通,例如我们一说起factory,我们就知道这是一个用来创建其他对象的对象;还有我们可以build可重用的组件。Design Pattern的核心:解耦(Decouple)和重用(Reuse)。这两个核心原则贯穿系统设计的始终,不管我们是基于一种编程语言,使用接口来实现,还是在分布式系统设计中。

分布式系统中之前很少有pattern,mapreduce算是一个,但是在容器和容器编排成为主流后,有了大量的可重用的组件,也总结出了大量的pattern,单容器,单节点多容器,多节点等。限于篇幅,本文不介绍多节点容器设计模式,感兴趣的同学可以参考书后的资料。

 

先说第一个pattern,Single Container Patten.

先从最小的组件容器开始说起,容器简单来说,就是把应用程序和它所需要的依赖库打成一个包。容器(Docker)的出现,极大改变了程序打包和部署的方式,更是彻底改变分布式系统设计的最基础组件。

Container就好比OOP Java编程语言中的Object(Class),是容器分布式系统的最基础对象。有人做过一个简单的类比,在java语言中,最基本的对象载体是Class,class被运行起来就是Object,而在容器系统中,最基本的对象载体是Container Image,当被运行起来后就是Container。Java对象有自己的初始化机制,Container中也有自己的Init Container机制。Java对象有销毁机制,Container也有preStop函数来优雅的停止。

在现实的设计中,需要把一个应用拆为多个容器来实现,这么做的理由有三个:1. 针对资源建立边界(不同的容器需要不同的CPU和内存,根据实际需要进行限制,而且不同容器间资源隔离,互不影响。2.建立团队归属边界,即一个Container有一个敏捷团队来own,最好6到8人。3.提供兴趣隔离(separation of concern)。容器的作用和职责应该满足Single Responsibility的原则,按照Domain Model Design的原则来进行设计,这样容易理解,也容易测试、更新和部署。那么在设计用于生产环境的容器的时候,需要设置内存和CPU的最高和最低限制,同时需要设置Liveness Probe和Readiness Probe。

在设计的时候,需要重点考虑:解耦和重用。不断的问自己几个问题:我的容器足够解耦了吗?是否还可以拿出部分来作为独立的容器?这些容器是否可以很方便的被重用到其他地方?

再说多个容器组合成的pattern。

同学们在学习K8S的过程中,肯定会有疑问:为什么Pod(含有一个或者多个Container)是最小的部署单元,而不能直接是容器。这里就要涉及到K8S中一个非常精巧的设计了。Pod是一组共享生命周期,并部署在同一个节点的容器的组合,他们可以通过共享的volume/network和IPC来进行通讯。之所以不是一个单一容器,而是多个容器来完成特定功能的原因在于:这些容器要完成的职责不同,根据单一职责(single responsibility)的原则,他们应该属于不同的组件;其次因为职责不同,维护他们的team也不同,迭代周期也不一样;最后其中一些容器是可以被复用在其他的环境中的。所以从“解耦”和“复用”的设计原则出发,Kubernetes通过增加一个虚拟层即POD,给系统设计带来了极大的灵活性,同时也产生了多种设计模式。即在一个POD中除了抗流量完成业务的容器(文中称之为app container)外,还存在其他的辅助容器,可以分为两类:1. Init Container 2. Sidecar container。

 

Init Container Pattern

就想Java语言中Object有初始化函数一样,Pod中的Init Container起的作用也是用来初始化。当app container需要满足一些前置条件才能启动,例如它依赖一些外部服务db service ready才能启动,或者需要初始化的更新文件(例如从github clone最新版本的文件)。

我们来看一个简单的例子,如上图。其中myapp-container是app container,它是来执行业务逻辑的,它的启动依赖后台的mydb和myservice两个服务。所以有两个init containers,他们分别执行nslookup来检查依赖服务是否已经启动,如果没有启动,等待2秒之后再检查,如果已经启动,则顺利启动自身容器,然后app container再启动。能看出这个例子中,app container利用init container来force wait,直到依赖的两个后台服务启动之后再启动。这样app container的启动逻辑就无需关心这两个依赖服务是否ready。

使用Init Container的注意事项如下:1. Init container的执行顺序是在pod启动过程中,最先执行,而且是顺序执行,即一个init container执行结束后,再执行下一个init containers。他没有readiness probe check,应该是逻辑简单,而且执行快速的。2. Init Container也是container,它也占CPU/内存系统资源,所以在计算资源消耗的时候,需要把它也加进去,不然调度的时候可能会有问题。

 

Sidecar pattern

下面介绍Sidecar pattern。

所谓sidecar,就是类似这种摩托车。在K8S中,sidecar container是和app container同时启动,并且有自己的职责,并能在别的地方进行复用的容器。Sidecar container和app container之间共享磁盘/网络/IPC等,我们来看一个典型的例子。

App Container是一个web server,sidecar container定期从github sync代码下来,两者通过Pod的volume来共享文件。这样做的好处是把从github定期sync代码的逻辑剥离出来,成为一个可以重用的模块,并且能用到其他的场合。而app container只需要单纯的做web服务就好,不需要考虑sync之类的逻辑。

什么时候考虑使用sidecar呢? 当这两个container需要同时部署,但是各有自己的职责,而且可以分别去迭代和演进,而且有重用的可能性。

那么什么时候不适合sidecar呢?当这两个container有不同的扩容需求时候,即两者需要独立的扩容时候,不要sidecar这种模式;另外,两者的通信可能会带来一些网络的消耗,带来一定的延迟,如果这点延迟是业务无法接受的话,也不要使用sidecar。

 

Ambassadaor Design Pattern

它是一种特殊的sidecar,其实就是app container的一个proxy,它来接流量,然后进行处理,然后把流量转发给app container,让其完成真正的商业逻辑。

 

此外,ambassador pattern还可用于shard a service或者A/B 测试。另外最近新崛起的技术热点ServiceMesh和其实现istio,都是利用sidecar的方式来做服务代理,把流量控制的逻辑下沉到基础架构层,通过ambassador的方式很方便的实现。他们的出现将极大的改变微服务架构,可能让其更关注业务逻辑的高效实现,而把流量控制(包括服务发现,服务限流,小流量等服务路由功能统一交给service mesh来解决。

 

Adapter Pattern

又是一种特殊的sidecar,如果希望对外输出的内容符合下游的要求而不对app container进行修改,可以增加一个adapter的sidecar,由它来做类似日志转换的事情。例如:

App container按照自身的要求生成日志并保存到文件系统中,另外的adapter container通过共享存储读取该日志,然后进行日志转换等工作,以便把内容输出给下游的metrics系统。

总结:

设计模式(Design Pattern)的核心是解耦和重用;在容器和容器编排的分布式系统中,有大量可重用的组件和pattern,其中Single container和multiple container又是最基本的组件。

 

参考资料: