learn-tech/专栏/周志明的架构课/48_以容器构建系统(上):隔离与协作.md
2024-10-16 06:37:41 +08:00

16 KiB
Raw Blame History

                        因收到Google相关通知网站将会择期关闭。相关通知内容
                        
                        
                        48 _ 以容器构建系统(上):隔离与协作
                        你好,我是周志明。从这节课开始,我们讨论的焦点会从容器本身,过渡到容器编排上。

我们知道自从Docker提出“以封装应用为中心”的容器发展理念成功取代了“以封装系统为中心”的LXC以后一个容器封装一个单进程应用已经成为了被广泛认可的最佳实践。

然而当单体时代过去之后分布式系统里对于应用的概念已经不再等同于进程了此时的应用需要多个进程共同协作通过集群的形式对外提供服务那么以虚拟化方法实现这个目标的过程就被称为容器编排Container Orchestration

而到今天Kubernetes已经成为了容器编排的代名词。不过在课程中我并不打算过多介绍Kubernetes具体有哪些功能也不会为你说明它由Pod、Node、Deployment、ReplicaSet等各种类型的资源组成可用的服务、集群管理平面与节点之间是如何工作的、每种资源该如何配置使用等等如果你想了解这方面信息可以去查看Kubernetes官网的文档库或任何一本以Kubernetes为主题的使用手册。

在课程中我真正希望能帮你搞清楚的问题是“为什么Kubernetes会设计成现在这个样子”“为什么以容器构建系统应该这样做

而要寻找这些问题的答案,最好是从它们设计的实现意图出发。所以在接下来的两节课中,我虚构了一系列从简单到复杂的场景,带你来理解并解决这些场景中的问题。

这里我还想说明一点学习这两节课的内容并不要求你对Kubernetes有过多深入的了解但需要你至少使用过Kubernetes和Docker基本了解它的核心功能与命令另外课程中还会涉及到一点儿Linux系统内核资源隔离的基础知识别担心只要你仔细学习了“容器的崛起”这个小章节就已经完全够用了。

构建容器编排系统时都会遇到什么问题?

好,现在我们来设想一下,如果让你来设计一套容器编排系统,协调各种容器来共同来完成一项工作,你可能会遇到什么问题?会如何着手解决呢?

我们先从最简单的场景开始吧:

场景一假设你现在有两个应用其中一个是Nginx另一个是为该Nginx收集日志的Filebeat你希望将它们封装为容器镜像以方便日后分发。

最直接的方案就将Nginx和Filebeat直接编译成同一个容器镜像这是可以做到的而且并不复杂。不过这样做其实会埋下很大的隐患它违背了Docker提倡的单个容器封装单进程应用的最佳实践。

Docker设计的Dockerfile只允许有一个ENTRYPOINT这并不是什么随便添加的人为限制而是因为Docker只能通过监视PID为1的进程即由ENTRYPOINT启动的进程的运行状态来判断容器的工作状态是否正常像是容器退出执行清理、容器崩溃自动重启等操作Docker都必须先判断状态。

那么我们可以设想一下即使我们使用了supervisord之类的进程控制器来解决同时启动Nginx和Filebeat进程的问题如果因为某种原因它们不停发生崩溃、重启那Docker也无法察觉到它只能观察到supervisord的运行状态。所以场景一关于封装为容器镜像的需求会理所当然地演化成场景二。

场景二假设你现在有两个Docker镜像其中一个封装了HTTP服务为便于称呼叫它Nginx容器另一个封装了日志收集服务叫它Filebeat容器。现在你要求Filebeat容器能收集Nginx容器产生的日志信息。

其实场景二的需求依然不难解决只要在Nginx容器和Filebeat容器启动时分别把它们的日志目录和收集目录挂载为宿主机同一个磁盘位置的Volume即可在Docker中这种操作是十分常用的容器间信息交换手段。

不过,容器间信息交换不仅仅是文件系统。

假如此时我又引入了一个新的工具confd它是Linux下的一种配置管理工具作用是根据配置中心Etcd、ZooKeeper、Consul的变化自动更新Nginx的配置。那么这样的话就又会遇到新的问题。

这是因为confd需要向Nginx发送HUP信号才便于通知Nginx配置已经发生了变更而发送HUP信号自然就要求confd与Nginx能够进行IPC通信才行。

当然尽管共享IPC名称空间不如共享Volume常见但Docker同样支持了这个功能也就是通过docker run命令提供了ipc参数用来把多个容器挂载到同一个父容器的IPC名称空间之下以实现容器间共享IPC名称空间的需求。类似地如果要共享UTS名称空间可以使用uts参数要共享网络名称空间的话就使用net参数。

这就是Docker针对场景二这种不跨机器的多容器协作所给出的解决方案了。

实际上自动地为多个容器设置好共享名称空间就是Docker Compose提供的核心能力。

不过这种针对具体应用需求来共享名称空间的方案确实可以工作但并不够优雅也谈不上有什么扩展性。要知道容器的本质是对cgroups和namespaces所提供的隔离能力的一种封装在Docker提倡的单进程封装的理念影响下容器蕴含的隔离性也多了仅针对于单个进程的额外局限。

然而Linux的cgroups和namespaces原本都是针对进程组而不只是单个进程来设计的同一个进程组中的多个进程天然就可以共享相同的访问权限与资源配额。

所以如果现在我们把容器与进程在概念上对应起来那容器编排的第一个扩展点就是要找到容器领域中与“进程组”相对应的概念这是实现容器从隔离到协作的第一步。在Kubernetes的设计里这个对应物叫做Pod。

额外知识Pod名字的由来与含义- 在容器正式出现之前的Borg系统中Pod的概念就已经存在了从Google的发表的《Large-Scale Cluster Management at Google with Borg》里可以看出Kubernetes时代的Pod整合了Borg时代的“Prod”Production Task的缩写与“Non-Prod”的职能。由于Pod一直没有权威的中文翻译我在后面课程中会尽量用英文指代偶尔需要中文的场合就使用Borg中Prod的译法即“生产任务”来指代。

这样有了“容器组”的概念只需要把多个容器放到同一个Pod中场景二的问题就可以解决了。

Pod的含义与职责

事实上扮演容器组的角色满足容器共享名称空间的需求是Pod两大最基本的职责之一同处于一个Pod内的多个容器相互之间会以超亲密的方式协作。请注意“超亲密”在这里的用法不是什么某种带强烈感情色彩的形容词而是代表了一种有具体定义的协作程度。

具体是什么意思呢?

对于普通非亲密的容器来说它们一般以网络交互方式其他的如共享分布式存储来交换信息也算跨网络协作对于亲密协作的容器来说是指它们被调度到同一个集群节点上可以通过共享本地磁盘等方式协作而超亲密的协作是特指多个容器位于同一个Pod这种特殊关系它们将默认共享以下名称空间

UTS名称空间所有容器都有相同的主机名和域名。 网络名称空间所有容器都共享一样的网卡、网络栈、IP地址等等。因此同一个Pod中不同容器占用的端口不能冲突。 IPC名称空间所有容器都可以通过信号量或者POSIX共享内存等方式通信。 时间名称空间:所有容器都共享相同的系统时间。

也就是说同一个Pod的容器只有PID名称空间和文件名称空间默认是隔离的。

PID的隔离让开发者的每个容器都有独立的进程ID编号它们封装的应用进程就是PID为1的进程开发人员可以通过Pod元数据定义中的spec.shareProcessNamespace来改变这点。而一旦要求共享PID名称空间容器封装的应用进程就不再具有PID为1的特征了这就有可能导致部分依赖该特征的应用出现异常。

而在文件名称空间方面容器要求文件名称空间的隔离是很理所应当的需求因为容器需要相互独立的文件系统以避免冲突。但容器间可以共享存储卷这是通过Kubernetes的Volume来实现的。

额外知识Kubernetes中Pod名称空间共享的实现细节-

Pod内部多个容器共享UTS、IPC、网络等名称空间是通过一个名为Infra Container的容器来实现的这个容器是整个Pod中第一个启动的容器只有几百KB大小代码只有很短的几十行见这里Pod中的其他容器都会以Infra Container作为父容器UTS、IPC、网络等名称空间实质上都是来自Infra Container容器。-

如果容器设置为共享PID名称空间的话Infra Container中的进程将作为PID 1进程其他容器的进程将以它的子进程的方式存在此时就会由Infra Container来负责进程管理比如清理僵尸进程、感知状态和传递状态。-

由于Infra Container的代码除了注册SIGINT、SIGTERM、SIGCHLD等信号的处理器外就只是一个以pause()方法为循环体的无限循环永远处于Pause状态所以它也常被称为“Pause Container”。

除此之外Pod的另一个基本职责是实现原子性调度。这里我们可以先明确一点就是如果容器编排不跨越集群节点那是否具有原子性其实都不影响大局。

但是在集群环境中,容器可能会跨机器调度时,这个特性就变得非常重要了。

如果以容器为单位来调度的话不同容器就有可能被分配到不同机器上。而两台机器之间本来就是物理隔离依靠网络连接的所以这时候谈什么名称空间共享、cgroups配额共享都没有意义了由此我们就从场景二又演化出了场景三。

场景三假设你现在有Filebeat、Nginx两个Docker镜像在一个具有多个节点的集群环境下要求每次调度都必须让Filebeat和Nginx容器运行于同一个节点上。

其实,两个关联的协作任务必须一起调度的需求,在容器出现之前很久就有了。

我举个简单的例子。在传统的多线程或多进程并发调度中如果两个线程或进程的工作是强依赖的单独给谁分配处理时间而让另一个被挂起都会导致某一个线程无法工作所以也就有了协同调度Coscheduling的概念它主要用来保证一组紧密联系的任务能够被同时分配资源。

这样来看的话,如果我们在容器编排中,仍然坚持把容器看作是调度的最小粒度,那针对容器运行所需资源的需求声明,就只能设定在容器上。如此一来,集群每个节点的剩余资源越紧张,单个节点无法容纳全部协同容器的概率就越大,协同的容器被分配到不同节点的可能性就越高。

说实话协同调度是很麻烦的实现起来要么很低效比如Apache Mesos的Resource Hoarding调度策略就要等所有需要调度的任务都完备后才会开始分配资源要么就是很复杂比如Google就曾针对Borg的下一代Omega系统发表过论文《Omega: Flexible, Scalable Schedulers for Large Compute Clusters》其中介绍了它是如何通过乐观并发Optimistic Concurrency、冲突回滚的方式做到高效率且高度复杂的协同调度。

而如果我们将运行资源的需求声明定义在Pod上直接以Pod为最小的原子单位来实现调度的话由于多个Pod之间一定不存在超亲密的协同关系只会通过网络非亲密地协作那就根本没有协同的说法自然也不需要考虑复杂的调度了关于Kubernetes的具体调度实现我会在“资源与调度”这个小章节中展开讲解

Pod是隔离与调度的基本单位也是我们接触的第一种Kubernetes资源。Kubernetes把一切都看作是资源不同资源之间依靠层级关系相互组合协作这个思想是贯穿Kubernetes整个系统的两大核心设计理念之一不仅在容器、Pod、主机、集群等计算资源上是这样在工作负载、持久存储、网络策略、身份权限等其他领域中也都有着一致的体现。

另外我想说的是因为Pod是Kubernetes中最重要的资源又是资源模型中一种仅在逻辑上存在、没有物理对应的概念因为对应的“进程组”也只是个逻辑概念也是其他编排系统没有的概念所以我这节课专门给你介绍了下它的设计意图而不是像帮助手册那样直接给出它的作用和特性。

对于Kubernetes中的其他计算资源像Node、Cluster等都有切实的物理对应物很容易就能形成共同的认知我就不一一介绍了这里你只需要了解下它们的设计意图就行

容器Container延续了自Docker以来一个容器封装一个应用进程的理念是镜像管理的最小单位。 生产任务Pod补充了容器化后缺失的与进程组对应的“容器组”的概念Pod中的容器共享UTS、IPC、网络等名称空间是资源调度的最小单位。 节点Node对应于集群中的单台机器这里的机器既可以是生产环境中的物理机也可以是云计算环境中的虚拟节点节点是处理器和内存等资源的资源池是硬件单元的最小单位。 集群Cluster对应于整个集群Kubernetes提倡的理念是面向集群来管理应用。当你要部署应用的时候只需要通过声明式API将你的意图写成一份元数据Manifests把它提交给集群即可而无需关心它具体分配到哪个节点尽管通过标签选择器完全可以控制它分配到哪个节点但一般不需要这样做、如何实现Pod间通信、如何保证韧性与弹性等等所以集群是处理元数据的最小单位。 集群联邦Federation对应于多个集群通过联邦可以统一管理多个Kubernetes集群联邦的一种常见应用是支持跨可用区域多活、跨地域容灾的需求。

小结

学完了这节课,我们要知道,容器之间顺畅地交互通信是协作的核心需求,但容器协作并不只是通过高速网络来互相连接容器而已。如何调度容器,如何分配资源,如何扩缩规模,如何最大限度地接管系统中的非功能特性,让业务系统尽可能地免受分布式复杂性的困扰,都是容器编排框架必须考虑的问题,只有恰当解决了这一系列问题,云原生应用才有可能获得比传统应用更高的生产力。

一课一思

现在,我们能够明确隔离与协作的含义,也就是容器要让它管理的进程相互隔离,使用独立的资源与配额;容器编排系统要让它管理的各个容器相互协作,共同维持一个分布式系统的运作。但除了协作之外,你认为容器编排系统是否还有其他必须考虑的需求目标呢?

欢迎在留言区分享你的见解。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。