10.凤凰架构:构建可靠的大型分布式系统 --- 虚拟化容器
Posted enlyhua
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了10.凤凰架构:构建可靠的大型分布式系统 --- 虚拟化容器相关的知识,希望对你有一定的参考价值。
第11章 虚拟化容器
容器的首要目标是让软件分发部署过程从传统的发布安装包、靠人工部署转变为直接发布意见部署好的、包含整套运行环境的虚拟化镜像。在容器技术成熟之前,主流的软件
部署过程是由系统管理员编译或下载好二进制安装包,根据软件的部署说明文档准备好正确的操作系统、第三方库、配置文件、资源权限等各种前置依赖以后,才能将程序正确的
运行起来。
一个计算机软件要能够正确运行,需要有以下三个方面的兼容性来保障:
1.ISA兼容
目标机器指令集的兼容性,譬如ARM架构的计算机无法直接运行面向x86架构编译的程序。
2.ABI兼容
目标系统或者依赖库的二进制兼容性,譬如Windows系统环境中无法直接运行Linux的程序,又譬如DirectX 12的游戏无法运行在 DirectX 9 之上。
3.环境兼容
目标环境的兼容性,譬如没有正确设置的配置文件、环境变量、注册中心、数据库地址、文件系统的权限等,任何一个环境因素出现错误,都会让你的程序无法正确运行。
ISA(Instruction Set Architecture,指令集架构):
是计算机体系结构中与程序设计相关的部分,包含基本数据类型、指令集、寄存器、寻址模式、存储体系、中断、异常处理以及外部IO。指令集架构包含一系列
操作码(即通常说的机器语言)以及由特定处理器执行的基本命令。
ABI(Application Binary Interface,应用二进制接口):
是应用程序与操作系统之间或者其他依赖库之间的低级接口。ABI覆盖了各种底层细节,如数据类型的宽度大小、对象的布局、接口调用的约定等。ABI不同于API,API
定义的是源代码和库之间的接口,因此同样的代码可以在支持这个API的任何系统中编译,而ABI允许编译好的目标代码在使用兼容ABI的系统中直接运行,无需任何改动。
笔者把仿真以及虚拟化技术来解决上述三项兼容性问题的方法统称为虚拟机化技术。根据抽象目标与兼容性的高低不同,虚拟化技术又分为下面5类:
1.指令集虚拟化(ISA Level Virtualization)
通过软件来模拟不同的ISA架构的处理器的工作过程,典型的代表如QEMU何Bochs。其实指令集虚拟化就是仿真,它能提供几乎完全不受限制的兼容性,甚至能做到
在Web浏览器上运行完整操作系统这种令人惊讶的效果,但由于每条指令都要由软件来转化和模拟,所以也是性能损失最大的虚拟化技术。
2.硬件抽象层虚拟化(Hardware Abstraction Level Virtualization)
以软件或者直接通过硬件来模拟处理器、芯片组、内存、磁盘控制器、显卡等设备的工作过程。既可以使用纯软件的二进制翻译来模拟虚拟设备,也可以由硬件的
Intel VT-d、AMD-Vi 之类虚拟化技术,将某个物理设备直通(Passthrough)到虚拟机中使用,典型的代表就是 VMware ESXi 和 Hyper-V。如果没有预设语境,
一般人们说的"虚拟机"就是指这类虚拟化技术。
3.操作系统层虚拟化(OS Level Virtualization)
无论是指令集虚拟化还是硬件抽象层虚拟化,都会运行一套完整真实的操作系统来解决ABI兼容性和环境兼容性的问题。虽然ISA兼容性是虚拟出来的,但ABI兼容性
和环境兼容性确实真实存在的。操作系统层虚拟化不会提供真实的操作系统,而是采用了隔离手段,使得不同的进程拥有独立的系统资源和资源配额,看起来仿佛是独享了
整个操作系统,但其实系统的内核仍然是被不同的进程共享的。
操作系统层虚拟化的另外一个名字叫 "容器化",由此可见,容器化仅仅是虚拟化的一个子集,只能提供操作系统内核以上的部分ABI兼容性与完整的环境兼容性。
这意味着如果没有其他虚拟化手段的辅助,在Windows系统上是不可能运行Linux的Docker镜像的,反之亦然。同时也决定了如果Docker宿主机的内核版本是Linux
Kernel 5.6,那无论上面运行的镜像是Ubuntu,RHEL 还是任何发行版的镜像,看到的内核一定都是相同的 Linux Kernel 5.6。容器化牺牲了一定的隔离性与
兼容性,换来的是比前两种虚拟化更高的启动速度、运行性能和更低的执行负担。
4.运行库虚拟化(Library Level Virtualization)
与操作系统层虚拟化采用隔离手段来模拟系统不同,运行库层虚拟化选择使用软件翻译的方法来模拟系统,它以一个独立进程来可代替操作系统内核,可提供目标
软件运行所需的全部能力。这种虚拟化方法获得的ABI兼容性高低,取决于软件是否能够准确和全面的完成翻译工作,典型的代表为WINE,和WSL。
5.语言层虚拟化(Programming Language Level Virtualization)
由虚拟机将高级语言生成的中间代码转换为目标机器可以直接执行的指令,典型的代表为Java的JVM和.NET的CLR。虽然厂商肯定会提供在不同系统下都有相同的
接口的标准库,但本质上这种虚拟化并不直接解决任何ABI兼容性和环境兼容性问题。
11.1 容器的崛起
设计容器的最初目的不是部署软件,而是隔离计算机中的各类资源。
11.1.1 隔离文件:chroot
容器的起点要追溯到1979年Unix 7 提供的 chroot 命令,是"Change Root"的缩写,功能是当某个进程经过 chroot 操作之后,它的根目录就会被锁定在命令参数
所指定的位置,以后它或者它的子进程将不能再访问和操作该目录之外的其他文件。
1991年,世界上第一个监控黑客行为的蜜罐程序就是以 chroot 实现的,命令参数指定的根目录当时被作者戏称为"Chroot监狱",而黑客突破chroot限制的方法被称为
"越狱"。后来,FreeBSD 4.0 系统重新实现了 chroot 命令,用它作为系统中进程沙箱隔离的基础,并将其命名为FreeBSD Jail。再后来,苹果公司又以FreeBSD为
基础开发出了ios操作系统。此后,黑客们就将绕过iOS沙箱机制以root全新任意安装程序的方法称为"越狱"。
200年,Linux Kernel 2.3.41 引入了 pivot_root 技术来实现文件隔离,pivot_root 直接切换了根文件系统(rootfs),有效的避免了chroot命令可能出现的
安全漏洞。后续提到的容器技术,如LXC,Docker等也都是优先使用 pivot_root 来实现根文件系统切换的。
无论是 chroot,还是 pivot_root,都不能提供完美的隔离性。原本按照unix的设计哲学,一切资源都可以被设为文件,一切处理都可以视为对文件的操作,理论上,
只要隔离了文件系统,一切资源应该被自动隔离才对。可是哲学归哲学,现实归现实,从硬件层面暴露的低层次资源,如磁盘、网络、内存、处理器,到经过操作系统层面封装的
高层次的资源,如UNIX分时、进程ID、用户ID、进程间通信,都存在大量以非文件形式暴露的操作入口。因此,以chroot为代表的文件隔离,仅仅是容器崛起之路的起点而已。
11.1.2 隔离访问:名称空间
2002年,Linux Kernel 2.4.19 引入了全新的隔离机制:Linux 名称空间(Linux Namespace)。名称空间在许多高级语言汇都存在,用于避免不同开发者提供的API
互相冲突。
Linux 的名称空间是一种由内核直接提供的全局资源封装,是内核针对进程设计的访问隔离机制。进程在一个独立的Linux名称空间中朝系统看去,会觉得自己仿佛就是这片
天地的主人,拥有这台Linux主机上的一切资源,不仅文件系统是独立的,还有着独立的PID编号、UID/GID编号、网络,等等。
如今,对文件、进程、用户、网络等各类信息的访问,都被囊括在Linux的名称空间中,即使一些今天仍然没被隔离的访问(譬如syslog就还没被隔离,容器中可以看到容器
外其他进程产生的内核syslog),日后也可以随着内核版本的更新纳入这套框架中。现在距离完美的隔离性就差最后一步了:资源的隔离。
Linux名称空间支持的8种资源的隔离:
1.Mount
隔离文件系统,功能上大致可以类比 chroot
2.UTS
隔离主机的Hostname,Domain name
3.IPC
隔离进程间通信的渠道
4.PID
隔离进程编号,无法看到其他名称空间中的PID,意味着无法对其他进程产生影响
5.Network
隔离网络资源,如网卡、网络栈、IP地址、端口等
6.User
隔离用户和用户组
7.Cgroup
隔离cgroups信息,进程有自己的cgroups的根目录视图(在 /proc/self/cgroup 不会看到整个系统的信息)
8.Time
隔离系统时间
11.1.3 隔离资源:cgroups
如果要让一台物理计算机中的各个进程看起来像独享整台虚拟计算机,不仅要隔离各自进程的访问操作,还必须能独立控制分配给各个进程的资源使用配额。Linux系统解决上述
问题的方案是 控制群组(Control Groups)。它与名称空间一样都是由内核直接提供的功能,用于隔离或者分配并限制某个进程组能够使用的资源配额,资源配额包括 处理器时间、
内存大小、磁盘IO等。
Linux 控制群组子系统的功能:
1.blkio
为块设备(如磁盘、固态硬盘、USB等)设定IO限额
2.cpu
控制cgroups中进程的处理器占用比率
3.cpuacct
自动生成cgroups中进程所用的处理器时间的报告
4.cpuset
为cgroups中的进程分配独立的处理器(包括多路系统的多处理、多核系统的处理器核心)
5.devices
设置cgroups中进程访问某个设备的权限
6.freezer
挂起或者恢复cgroups中的进程
7.memory
设定cgroups中进程使用内存的限制,并自动生成内存资源使用报告
8.net_cls
使用等级标识符标记网络数据包,可允许Linux流量控制程序识别从具体cgroups中生成的数据包
9.net_prio
用来设置网络流量的优先级
10.hugetlb
主要针对HugeTLB系统进行限制
11.perf_event
允许Perf工具基于cgroups分组做性能检测
11.1.4 封装系统:LXC
当文件系统、访问、资源 都可以被隔离之后,容器已经有了降生所需的全部前置条件,并且Linux的开发者也看到了这一点。为了降低普通用户综合使用namespace、
cgroups 这些低级特性的门槛,2008年 Linux Kernel 2.6.24 刚刚开始提供 cgroups 的同一时间,又马上发布了名为 Linux容器(Linux Container,LXC)的
系统级虚拟化功能。
LXC 的出现肯定受到了 OpenVZ和Linux-VServer的启发,可惜的是,LXC在设定自己的发展目标时,也被前辈们的影响所局限住了。LXC眼中的容器与OpenVZ和
Linux-VServer定义的并无差别,是一种封装系统的轻量级虚拟机,而Docker眼中的容器则是一种封装应用的技术手段。这两种封装理念在技术层面并没有什么本质区别,
但应用效果差异巨大。举例,如果你要建设一个LAMP应用,按照LXC的思路,你应该先编写或者寻找LAMP的template(类似于Docker的Dockerfile),以此构造安装处
一个安装了LAMP的虚拟系统。如果从部署虚拟机的角度来看,这还挺方便,作为那个时代的系统管理员,所有软件、补丁、配置都是自己搞定的,部署一台新虚拟机要花费
1,2天是很正常的,而有了LXC的template,一下子都安装好了。但是,作为一名现代的系统管理员,这里的问题就大了。如果我想把LAMP改为LNMP,该怎么办?如果我
想把LAMP里面的mysql 5 调整为 mysql 8该怎么办?此时只能寻找或者自己编写新的template来解决。但是,这台虚拟机的软件、版本都配置对了,下一台要构建LYME
或者MEAN,又该怎么办?以封装系统为出发点,仍是按照先按照系统再安装软件的思路,就永远无法在一分钟甚至十几秒就构造出一个合乎要求的软件运行环境,也决定了
LXC不可能成为今天的容器生态,所以,接下来的舞台的聚光灯就落到了Docker身上。
11.1.5 封装应用:Docker
2013年宣布开源的Docker毫无疑问是容器发展历史上里程碑式的发明,然而Docker的成功似乎没有太多技术驱动的成分。甚至对早期的Docker而言,确实没有什么
能构成壁垒的技术,它的容器化能力直接来源于LXC,它的镜像分层组合的文件系统直接来源于AUFS。
为什么要用Docker而不是LXC?
Docker除了包装来自Linux内核的特性之外,它的价值还体现在如下几点:
1.跨机器的绿色部署
docker定义了一种将应用及其所有的环境依赖都打包到一起的格式,仿佛它原本就是绿色软件一样。而LXC并没有提供这样的能力,使用LXC部署的新机器的很多
细节都需要人为的介入,部署后虚拟机的环境几乎肯定跟原本部署程序的机器有所区别。
2.以应用为中心的封装
docker封装应用而非封装机器的理念贯穿了它的设计、api、界面、文档等多个方面。相比之下,LXC将容器视为对系统的封装,这限制了容器的发展。
3.自动构建
docker提供了开发人员在容器中构建产品的全部支持,使得开发人员无需关注目标机器的具体配置即可使用任意的构建工具链在容器中自动构建出最终产品。
4.多版本支持
docker支持像Git一样管理容器的连续版本,进行检查版本间差异、提交和回滚等操作。从历史记录中你可以看到该容器是如何一步步构建成的,并且只增量
上传或者下载新版本中变更的部分。
5.组件重用
docker允许将现有任何容器作为基础镜像来使用,以此构建出更加专业的镜像。
6.共享
docker拥有公共的镜像仓库,成千上万的docker用户可以在上面上传自己的镜像,同时也可以使用其他人上传的镜像。
7.工具生态
docker开放了一套可自动化和自行扩展的接口,在此之上还有很多工具来扩展其他功能,譬如容器编排、管理界面、持续集成等。
Docker已经成为软件开发、测试、分发、部署等各个环节都难以或缺的基础支撑,自身的架构也发生了相当大的改变,被分解为由 docker client、docker Daemon、
docker Registry、docker Container等子系统,以及Graph、Driver、libcontainer 等各司其职的模块组成。
2014年,docker 开源了自己用Go语言开发的libcontainer,这是一个越过LXC直接操作namespace和cgroups的核心模块,它使得docker能直接与系统内核打交道,
而不必依赖LXC来提供容器化的隔离能力。
2015年,在docker的主导和倡议下,多家公司联合制定了"开放容器交互标准(Open Container Initiative,OCI)",这是一个关于容器格式和运行时的规范文件,
其中包含运行时标准(runtime-spec)、容器镜像标准(image-spec)和镜像分发标准(distribution-spec,此标准还没发布)。运行时标准定义了应该如何运行一个容器、
如何管理容器的状态和生命周期、如何使用操作系统的底层特性;容器镜像标准规定了容器镜像的格式、配置、元数据的格式,可以理解为对镜像的静态描述;镜像分发标准则
规定了镜像推送和拉取的网络交互过程。
为了符合OCI标准,docker推动自身的架构继续向前演进,首先将libcontainer独立出来,封装重构成runC项目。runC是OCI运行时的首个参考实现,提出了"让
标准容器无处不在"的口号。为了能够兼容所有符合标准的OCI运行时实现,docker进一步重构了docker Daemon子系统,将其中与运行时交互的部分抽象为containerd
项目,这是一个负责管理容器执行、分发、监控、网络、构建、日志等功能的核心模块,内部会为每个容器运行时创建一个containerd-shim适配进程,默认与runC搭配
工作。
Docker 虽然赢得了容器战争,但Docker Swarm却输掉了容器编排战争。
11.1.6 封装集群:Kubernetes
以docker为代表的容器引擎是将软件的发布流程从 分发二进制安装包转变为直接分发虚拟化后的整个运行环境,令应用得以实现跨机器的绿色部署,那以k8s为代表的容器
编排框架就是把大型软件系统运行所依赖的集群环境也进行了虚拟化,令集群得以实现跨数据中心的绿色部署,并能够根据实际情况自动扩容。
k8s的前身是Google内部的Borg,与2014年使用Go重写后开源。2013年,提出"云原生"的概念,但是要实现服务化、具备韧性、弹性、可观测性的软件系统十分困难。
直到k8s的出现,大家认准了这就是云原生时代的操作系统。2015年,k8s发布了第一个正式版本1.0,Google和Linux基金会同时宣布成立云原生基金会(CNCF),并且将
k8s托管到CNCF,成为其第一个项目。
k8s的成功与docker的成功并不相同,docker依靠的是优秀的理念,以一个"好的点子"引爆了一个时代。没有docker也会有Cocker或者Eocket的出现。而k8s的成功
不仅有Google深厚的技术作为支撑,而且有领先时代的设计理念,更加关键的是k8s的出现符合所有云计算大厂的切身利益,有着业界巨头不遗余力的广泛支持。
k8s与docker的关系十分微妙,在k8s开源早期,它是完全依赖且绑定docker的,并没有过多考虑日后有使用其他容器引擎的可能性。直至k8s 1.5版本的出现之前,k8s
管理容器的方式都是通过内部的 DockerManager 向 Docker Engine以http的形式发送指令,通过Docker来完成镜像的增删查改操作。将这个阶段k8s与容器引擎的调用
关系捋直,并结合上一节提到的Docker贡献的containerd与runC项目重构的调用,完整的调用链如下:
Kubernetes Master -> kubelet -> DockerManager -> Docker Engine -> containerd -> runC
2016年,k8s 1.5 版本开始引入了容器运行时接口(Container Runtime Interface,CRI),这是一个定义容器运行时应该如何介入 kebelet的规范标准,从此
k8s内部的DockerManager就被更为通用的 KubeGenericRuntimeManager 所替代了,kubelet与 KubeGenericRuntimeManager之间通过gRPC协议通信。由于CRI是
docker之后才发布的标准,docker是肯定不支持CRI的,所以k8s又提供了DockerShim服务作为docker与CRI的适配层,由它与Docker Engine以http形式通信,实现了
原来的DockerManager的全部功能。此时,docker对k8s来说只是一个默认依赖,而非之前的无可或缺了,它们的调用链为:
Kubernetes Master -> kubelet -> KubeGenericRuntimeManager -> DockerShim -> Docker Engine -> containerd -> runC
2017年,由Google、Red Hat、Inter、SUSE、IBM 联合发起的 CRI-O(Container Runtime Interface Orchestrator)项目发布了首个正式版本。从名字就
可以看出来,一方面,它肯定是完全遵守CRI规范实现的,另一方面,它可以支持所有符合OCI运行时标准的容器引擎,默认也仍然是与runC搭配工作,若要换成Clear Containers,
Kata Containers等其他OCI运行时引擎也完全没有问题。虽然开源版k8s是使用CRI-O、cri-containerd亦或者是DockerShim作为CRI实现,完全可以由用户自己选择(根据
用户的宿主主机的环境选择),但Red Hat自己扩展定制的k8s企业版,即OpenShift 4中,调用链已经没了Docker Engine的身影:
Kubenetes Master -> kubelet -> KubeGenericRuntimeManager -> CRI-O -> runC
由于此时docker在容器引擎的市场份额仍然占据绝对优势,对普通用户来说,如果没有明确的收益,就没有什么动力把docker引擎换了,所以CRI-O即使摆出了直接挖掉
docker根基的态势,也并没有把docker带来太多及时可见的影响。
2018年,由docker捐献给CNCF的containerd项目,在CNCF精心的孵化下发布了1.1版本。与1.0的最大区别是此时它完美的支持了CRI标准,这意味着原本用作CRI适配
器的cri-containerd从此不再需要。此时,再观察k8s到容器运行时的调用链,你会发现调用步骤会比通过DockerShim、Docker Engine 与 containerd交互的步骤减少
2步,这意味着用户愿意抛弃docker,在容器编排上至少可以省略一次http调用,获得性能上的收益。k8s从1.10版本宣布开始支持containerd 1.1,此时在调用链中已经
能够完全抹去Docker Engine 的存在:
Kubernetes Master -> kubelet -> KubeGenericRuntimeManager -> containerd -> runC
今天,要使用哪种容器运行时取决于安装k8s时宿主主机上的容器运行时环境,但对于阿里云ACK,腾讯云TKE等直接提供k8s容器环境的云计算厂商来说,采用的容器运行时
普遍已经都是containerd,毕竟运行性能对它们来说就是核心生产力和竞争力。
未来,随着k8s持续发展壮大,Docker Engine 经历从不可获取、默认依赖、可选择、直到淘汰是大概率事件,这件事表面上是Google、Red Hat等云计算大厂联手所为,
但实际淘汰它的还是技术发展的潮流趋势,就如同Docker诞生时依赖LXC,到最后用libcontainer取代LXC一般。同时,我们也看到事情的另一面,现在连LXC都还没被淘汰,
反倒是发展出了更加专注于与OpenVZ等系统级虚拟化竞争的LXD,相信docker本身也很难彻底消亡,如已经习惯的CLI界面,已经形成成熟生态的镜像仓库等都应该长期存在,
只是在容器编排领域,未来的docker很可能只会以runC和containerd的形式存续下去,比较它们最初都来源于docker。
11.2 以容器构建系统
自从docker提出了"以封装应用为中心"的容器发展理念成功取代LXC的"以封装系统为中心"的理念以后,一个容器封装一个单进程应用已经被广泛认可的最佳实践。然而单体
时代过去后,分布式系统里应用的概念不再等同于进程,此时的应用需要多个进程共同协作,通过集群的方式对外提供服务,而以虚拟化方法实现这个目标的过程就被称为容器
编排(Container Orchestration)。
容器之间顺利的交互通信是协作的核心需求,但容器协作不仅仅是将容易以高速网络互相连接而已。如何调度容器、如何分配资源、如果扩缩规模,如何最大限度的接管系统中
的非功能特性,如何让业务系统尽可能免受分布式复杂性的困扰,都是容器编排框架必须考虑的问题。只有恰当的解决了这些问题,云原生应用才可能获得比传统应用更高的生产力。
11.2.1 隔离与协作
为什么k8s被设计成现在这个样子?为什么以容器构建系统应该这样做?
场景一:
假设你现在有2个应用,一个是nginx,另外一个是Nginx收集日志的Filebeat,你希望将它们封装为容器镜像,以方便日后分发。
最直接的方案就是将nginx和filebeat直接编译成同一个容器镜像,这是可以做到的,而且不复杂,然而这样做会埋下很大的隐患:它违背了docker提倡的单个容器
封装单个进程应用的最佳实践。docker设计的Dockerfile只允许有一个ENTRYPOINT,这并非无辜添加的人为限制,而是因为docker只能通过监视PID为1的进程(即
ENTRYPOINT启动的进程)的运行状态来判断容器的工作状态是否正常,然后根据状态决定是否清理自动重启等操作。设想一下,即使我们使用了supervisord之类的进程
控制器来解决同时启动nginx和filebeat进程的问题,如果它们因某种原因不停的重启、崩溃,那docker也无法察觉到,它只能观察到supervisord的运行状态,因此,
以上需求会理所当然的演化为场景二。
场景二:
假设你现在有2个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名称空间不如共享Volumn常见,但Docker同样支持了该功能。docker run 提供了--ipc
的参数,用于把多个容器挂载到同一个父容器的IPC名称空间下,以实现容器间共享IPC名称空间的需求。类似的,如果要共享UTS名称空间,可以使用--uts参数,如果要
共享网络名称空间,则可以使用--net参数。
以上便是docker针对场景二这种不夸机器的多容器协作所给出的解决方案,自动的为多个容器设置好共享名称空间其实就是Docker Compose提供的核心能力。这种
针对具体应用需求来共享名称空间的方案,确实可以工作,却并不够优雅,也谈不上有什么扩展性。容器的本质是对cgroups和namespaces所提供的隔离能力的一种封装,
在Docker提倡的单进程封装的理念影响下,容器蕴含的隔离性多了针对单个进程的额外限制,而Linux的cgroups和namespace原本都是针对进程组而非单个进程来设计的,
同一个进程组中的多个进程天然可以共享相同的访问权限和资源配额。如果我们现在把容器与进程在概念上对应起来的话,那容器编排的第一个扩展点,就是要找到容器领域
中与"进程组"相对应的概念,这是实现容器从隔离到协作的第一步,在k8s设计中,这个对应物叫Pod。
有了"容器组"的概念,场景二的问题变只需要将多个容器放到同一个Pod中即可解决。扮演容器组的角色,满足容器共享名称空间的需求,是Pod的两大嘴基本职责之一,
同处于一个Pod内的多个容器,相互之间以超亲密的方式协作。请注意,"超亲密"在这里并非某种带强烈感情色彩的形容词,而是一种具有具体定义的协作程度。对于普通
非亲密的容器,它们一般以网络交互的方式(其他譬如共享分布式存储来交换信息也算跨网络)协作;对于亲密协作的容器,它们一般被调度到同一个集群节点上,可以通过
共享本地磁盘的方式协作;而超亲密的协作是指多个容器位于同一个Pod的特殊关系,它们将默认共享如下内容;
1.UTS名称空间:所以容器都有相同的主机名和域名;
2.网络名称空间:所有容器都共享一样的网卡、网络栈、IP地址等。因此,同一个Pod中不同容器占用的端口不能冲突;
3.IPC名称空间:所有容器都可以通过信号量或POSIX共享内存等方式通信;
4.时间空间名称:所有容器都共享相同的系统时间。
同一个Pod的容器,只有PID名称空间和文件名称空间默认是隔离的。PID的隔离令每一个容器都有独立的进程ID编号,它们封装的应用程序就是PID为1的进程,可以
通过Pod元数据定义的 spec.shareProcessNamespace 来改变这点。一旦要求共享PID名称空间,容器封装的应用进程就不再具有PID为1的特征了,这有可能导致部分
依赖该特征的应用出现异常。在文件名称空间方面,容器要求文件名称空间的隔离是很理所当然的需求,因为容器需要互相独立的文件系统来避免冲突,但容器间可以共享
存储卷,这是通过k8s的Volumn来实现的。
K8s中Pod名称空间共享的实现细节:
Pod内部多个容器共享UTS、IPC、网络等名称空间是通过一个名为Infra Container的容器来实现的,这个容器是整个Pod中第一个启动的容器,只有几十万
字节大小(代码只有几十行),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 2个docker镜像,在一个具有多个节点的集群环境下,要求每次调度都必须让filebeat和nginx容器运行在同一个节点上。
两个关联的协作任务必须一起调度的需求在容器出现之前就存在已久,譬如在传统的多线程(或多进程)并发调度中,如果两个线程的工作是强依赖的,单独给其中一个分配
处理时间、而另外一个呗挂起会导致程序无法工作,如此就有了协同调度(Coscheduling)的概念,以保证一组紧密联系的任务能够被同时分配资源。如果我们在容器编排中
仍然坚持将容器视为调度的最小粒度,那对容器运行所需资源的需求声明就只能设定在容器上,这样集群每个节点剩余的资源越紧张,单个节点无法容纳全部协同容器的概率就
越大,协同的容器被分配到不同节点的可能性就越高。
协同调度是十分麻烦的,实现起来要么很低效,譬如Apache Mesos的Resource Hoarding调度策略,就要等待所有需要调度的任务都完成后才会开始分配资源;要么
很复杂,,譬如Google就曾针对Borg的下一个Omega系统发表过论文,接收Omega是如何通过乐观并发锁、冲突回滚的方式做到高效且高度复杂的协同调度的。但是如果将
运行资源的需求声明定义在Pod上,直接以Pod为最小的原子单位来实现调度,由于多个Pod之间必定不存在超亲密的协同关系,只会通过网络非亲密的协作,就没有协同的说法,
自然就不需要考虑复杂的调度了。
Pod是隔离与资源调度的基本单位,也是我们接触的第一种k8s资源。k8s将一切皆视为资源,不同资源之间依靠层级关系互相组合、协作的这个思想是贯穿k8s整个系统的
两大设计核心之一,不仅在容器、Pod、主机、集群等计算资源上是这样,在工作负载、持久存储、网络策略、身份权限等其他领域中也是这样。
由于Pod是k8s中最重要的资源,又是资源模型中一种仅在逻辑上存在、没有物理对应的概念(因为对应的"进程组"也只是个逻辑概念),是其他编排系统没有的概念。对于
k8s中的其他计算资源,像Node、Cluster等都有切实的物理对应物,就很容易理解。
1.容器(Container)
延续了自docker以来一个容器封装一个应用进程的概念,是镜像管理的最小单位。
2.生产任务(Pod)
补充了容器化后缺失的与进程组对应的"容器组"的概念,Pod中的容器与共享UTS、IPC、网络等名字空间,是资源调度的最小单位。
3.节点(Node)
对应于集群中的单台机器,这里的机器既可以是生产环境中的物理机,也可以是云计算环境中的虚拟节点,节点是处理器和内存等资源的资源池,是硬件单元的最
小单位。
4.集群(Cluster)
对应于整个集群,k8s提倡面向集群来管理应用。当你要部署应用的时候,只需要通过声明式API将你的意图写成一份元数据(Metafest),将它提交给集群即可,
而无需关心它具体分配到哪个节点(尽管可以通过标签选择器完全可以控制它分配到哪个节点,但一般不需要这么做)、如何实现Pod间通信、如何保证韧性与弹性,等等,
所以集群是处理元数据的最小单位。
5.集群联邦(Federation)
对应于多集群,通过集群联邦可以统一管理多个k8s集群,它的一种常见应用是能满足跨可用区多活、异地域容灾的需求。
11.2.2 韧性与弹性
控制模式是继资源模型之后,k8s的另外核心设计理念。
场景四:
假设有一个由数十个Node、数百个Pod、近千个Container所组成的分布式系统,要避免系统因为外部流量压力、代码缺陷、软件更新、硬件升级、资源分配等
原因而出现中断,作为管理员,你希望编排系统为你提供哪种支持。
永远不出错是不切实际的,只能让编排系统出错的时候,能自动调节成正确的状态。工业里面叫"控制回路"(Control Loop)。
k8s官方文档是以房间中空调自动调节温度为例来介绍控制回路的一般工作过程:当你设置好温度后,就是告诉空调你对温度的"期望状态"(Desired State),
而传感器测量出的房间的实际温度是"当前状态"(Current State)。根据当前状态与期望状态的差距,由控制器通过控制空调的开关来调节温度,使当前状态逐渐
接近期望状态。
将这种控制回路的思想应用到容器编排上,自然会为k8s中的资源附加上期望状态与实际状态两项属性。不论是用于抽象容器运行环境的计算资源,还是安全、服务、
令牌、网络 等功能的资源,用户想要使用这些资源来实现某种需求,就不提倡像平常编程那样去调用某个或某一组方法来达成目的,而是要通过描述清楚这些资源的期望
状态,由k8s中对应监视这些资源的控制器来驱动资源的实际状态逐渐向期望状态靠拢。这种交互式风格被称为k8s的声明式API。
k8s的资源对象与控制器:
目前,k8s已经支持相当多的资源对象,并且可以使用CRD(Custom Resource Definition,用户资源自定义)来自定义扩充,可以使用kubectl api-resources
来查看它们。
资源对象分类:
1.用于描述如何创建、销毁、更新、扩缩Pod,包括Autoscaling(HPA)、CronJob、DeamonSet、Deployment、Job、Pod、ReplicaSet、StatefulSet。
2.用于配置信息的设置域更新,包括ConfigMap、Secret。
3.用于持久性的存储文件或者Pod之间的文件共享,包括Volume、LocalVolume、PersistentVolume、PersistentVolumeClaim、StorageClass
4.用于维护网络通信和服务访问的安全,包括SecurityContext、ServiceAccount、Endpoint、NetworkPolicy
5.用于定义服务与访问,包括Ingress、Service、EndpointSlice
6.用于划分虚拟集群、节点和资源配额,包括Namespace、Node、ResourceQuota
这些资源对象在控制器管理框架中一般都会有相应的控制器来管理,下面列举一些常见的控制器,并按照它们的启动情况分类如下:
1.必须启用的控制器
2.默认请用的可选控制器
3.默认禁止的控制器
与资源相对应,只要是实际状态有可能发生变化的资源对象,通常都会由对应的控制器进行追踪,每个控制器至少追踪一种类型的资源对象。为了管理众多资源控制器,
k8s设计了统一的控制器管理框架(kube-controller-manager)来维护这些控制器的正常运作,以及统一的指标监视器(kube-apiserver)来为控制器提供其工作时追踪
资源的度量数据。
这里以部署控制器(Deployment Controller)、副本集控制器(ReplicaSet Controller)和自动扩缩控制器(HPA Controller)为例来介绍k8s控制器的工作
模式。
场景五:
通过服务编排,对任何分布式系统自动实现以下三种通用的能力。
1) Pod出现故障时,能够自动恢复,不中断服务;
2) Pod更新程序时,能够滚动更新,不中断服务;
3) Pod遇到压力时,能够水平扩展,不中断服务;
前文提到虽然Pod本身也是资源,完全可以直接创建,但由Pod直接构成的系统是十分脆弱的,在实际生产中并不提倡。正确的做法是通过副本集(ReplicaSet)来创建
Pod。ReplicaSet也是一种资源,属于工作负荷类,代表一个或者多个Pod副本的集合。你可以在ReplicaSet资源的元数据中描述你期望的Pod副本的
数量(即spec.replicas的值)。当ReplicaSet成功创建之后,副本集控制器就会持续跟踪该资源,如果一旦有Pod发生崩溃退出,或者状态异常(默认靠的是进程返回值,
你还可以在Pod中设置探针,以自定义的方式告诉k8s出现何种情况时Pod才算状态异常),ReplicaSet都会自动创建新的Pod来代替异常的Pod;如果异常出现了额外数量的
Pod,也会被ReplicaSet自动回收,总之就是在任何时候集群中的这个Pod副本的数量都向期望状态靠拢。
ReplicaSet本身就能满足场景五的第一项能力,即可以保证Pod出现故障时自动恢复,但是在升级程序版本时,ReplicaSet不得不主动中断旧Pod的运行,重新创建
新的Pod,这会造成服务中断。对于那些不允许中断的业务,以前的k8s曾经提供了 kubectl rolling-update命令来辅助实现滚动更新。
所谓的滚动更新(Rolling Update)是指先停止少量旧副本,维持大量旧副本继续提供服务,当停止的旧副本更新成功后,新副本可以提供服务以后,再重复以上操作,
直到所有副本都更新成功。将这个过程放到ReplicaSet上,就是先创建新版本的ReplicaSet,然后一边让新的ReplicaSet逐步创建新版Pod的副本,一边让旧的
ReplicaSet逐渐减少旧版Pod的副本。
之所以 kubectl rolling-update 命令会被淘汰,是因为这样的命令式交互完全不符合k8s的设计理念。如果你希望改变某个资源的状态,应该将期望状态告诉k8s,
而不是教k8s具体如何操作。因此,新的部署资源(Deployment)与部署控制器被设计出来,由Deployment来创建ReplicaSet,再由ReplicaSet来创建Pod,当你更新
Deployment中的信息(譬如更新了镜像)后,部署控制器就会跟踪到新的期望状态,自动创建新的ReplicaSet,并逐渐减少旧的ReplicaSet的数量,直至升级完成后彻底
删除掉旧的ReplicaSet。
对于场景五的最后一种能力,遇到流量压力时,管理员完全可以手动修改 Deployment 中的副本数量,或者通过 kubectl scale 命令来指定副本数量,促使k8s部署
更多的 Pod副本来应对压力。然后这种扩容需要人工参与,且只依靠人类经验来判断需要扩容的副本数量,不容易做到精确与及时,为此k8s又提供了 Autoscaling资源和
自动扩容控制器,从而自动根据度量指标,如处理器、内存占用率、用户自定义的度量值等,来设置Deployment(或者ReplicaSet)的期望状态,实现当度量指标出现变化时,
系统自动按照"Autoscaling -> Deployment -> ReplicaSet -> Pod"这样的顺序层层变更,最终实现根据度量指标自动扩容/缩容。
故障恢复、滚动更新、自动扩缩 这些特性,在云原生时代常被概括成服务的 韧性(Resilience)与弹性(Elasticity)。不要死记硬背,而是要站在解决问题的角度
去理解为什么k8s要设计这些资源和控制器,为什么这些资源和控制器被设计成这样子。
可以思考以下几个问题:
1.假设想限制某个Pod持有的最大存储卷数量,应该如何设计?
2.假设集群中某个Node发生硬件故障,k8s要让调度任务避开这个Node,应该如何设计?
3.假设这个Node重新恢复,k8s要尽快利用上面的资源,又该如何设计?
只有你真正的接受了资源与控制器是贯穿整个k8s的两大设计理念,即便不查文档,也能大概想出个轮廓。
11.3 以应用为中心的封装
容器的崛起起源于chroot,namespaces,cgroups 等内核提供的隔离能力,系统级虚拟化技术使得同一台机器上互不干扰的运行多个服务成为可能;为了降低用户使用内核
隔离能力的门槛,随后出现了LXC,它是namespaces,cgroups特性的上层封装,使得容器一词真正走出实验室,走到了工业界;为了实现跨机器的软件绿色部署,出现了Docker,
它最初是LXC的上层封装,彻底改变了软件打包分发的方式,并迅速被大量企业广泛使用;为了满足大型系统对服务集群化的需求,出现了k8s,它最初是docker的上层封装,让以
多个容器共同协作构建出健壮的分布式系统,成员今天云原生时代的技术基础设施。
k8s回事容器化崛起之路的终点吗?从能力上可以这么说,k8s被誉为云原生时代的操作系统,自诞生之日起就因其出色的管理能力、扩展性与声明代替命令的交互式理念获得了
无数掌声。但是,从易用性角度来说,差距还很大。云原生基础设施的其中一个重要目的是接管业务系统复杂的非功能特性,让业务研发和运维工作变得非常简单,不受分布式的牵绊,
然后k8s最被诟病的就是复杂。
举例,k8s部署一套Spring Cloud 的微服务,你需要分别部署一个到多个配置中心、注册中心、服务网格、安全认证、用户服务、商品服务、交易服务,为每个微服务都
配置好相应的k8s工作负载与服务访问,为每一个微服务的Deployment,ConfigMap,StatefulSet,HPA,Service,ServiceAcoount,Ingress等资源都编写好元数据配置。
这个过程不仅繁琐,还在于要写出合适的元数据描述文件,既要懂开发(网关中服务调用关系、使用的容器的镜像版本、运行依赖的环境变量这些参数等)还要懂运维(要部署
多少个服务,配置和会哦在那个扩容缩容策略,数据库的密钥文件地址等),有时候还要懂平台(需要什么样的调度策略,如何管理集群资源)。
但以上复杂性不能说是k8s带来的,而是分布式架构本身的特点导致的。这些困难的实质源于docker容器镜像封装了单个服务,k8s通过资源封装了服务集群,却没有一个
载体真正封装整个应用。应用难以管理的原因在于封装应用的方法没能将开发、运维、平台等各种角色的关注点恰当的分离。
11.3.1 Kustomize
最初,由k8s官方给出的"如何封装应用"的解决方案是"用配置文件来配置配置文件",可以理解为一种针对YAML的模板引擎的变体。k8s官方认为应用就是一组具有相同目标的
k8s资源的集合,如果逐一管理、部署每项资源元数据过于繁琐的话,那就提供一种便捷的方式,把应用中不变的信息与易变的信息分离开以解决管理问题,把应用所涉及的资源自动
生成一个多合一(All-in-One)的整包以解决部署问题。
完成这项工作的工具叫 Kustomize,它原本只是一个独立的小程序,从k8s 1.14 版本起,被纳入 kubectl命令之中,成为k8s内置的功能。Kustomize 使用
Kustomization 文件来组织与应用相关的所有资源,Kustomization本身也是一个以YAML格式编写的配置文件,里面定义了构成应用的全部资源,以及资源中根据情况被覆盖的
变量值。
Kustomize 的主要价值是根据环境来生成不同的部署配置。只要建立多个Kustomization文件,开发人员就能以基于基准进行派生(Base and Overlay)的方式,对不同的
模式(譬如生产模式,调试模式)、不同的项目(同一个产品对不同客户的定制化)定制出不同的资源整合包。在配置文件里,无论是开发人员关心的信息,还是运维人员关心的信息,
只要是元数据中描述的内容,最初都是由开发人员来编写,然后在编译期间由负责CI/CD的产品人员针对项目进行定制,最后在部署期间由运维人员通过kubectl的补丁(Patch)机制
更改其中需要运维人员关注的信息,譬如构造一个补丁来增加Deployment的副本个数,构造另外一个补丁来设置Pod的内存限制等。
Kustomize使用Base,Overlay和Patch生成最终配置文件的思路与docker中的分层镜像的思路有些类似,既规避了以"字符替换"对资源元数据文件的入侵,也不需要学习
额外的DSL语法(譬如Lua)。从效果来看,Kustomize编译生成的all-in-one整合包来部署应用是相当方便的,只要一行命令就能够把应用所涉及的服务一次安装好。
但Kustomize毕竟只是一个"小工具"性质的辅助功能,对于开发人员来说,Kustomize 只能简化产品针对不同情况的重复配置,并没有真正解决应用管理复杂的问题,要做的
事、要写的配置,最终都没减少,只是不用重复写罢了;对于运维来说,应用维护不只是安装部署,应用的整个生命周期,除了安装还有更新、回滚、卸载、多版本、多实例、依赖项
维护等诸多问题。这些问题,都需要更强大的工具去解决,如Helm。
11.3.2 Helm与Chart
Deis公司开发的Helm和它的应用格式Chart。如果k8s是云原生操作系统,那Helm就要成为这个操作系统上的应用商店和包管理工具。
对于Linux系统下的包管理工具,如Debian系列的apt-get命令与dpkg格式、RHEL系的yum命令与rpm格式。有了包管理工具,你只要知道应用的名称,就可以很方便的从应用
仓库中下载、安装、升级、部署、卸载、回滚程序,而且包管理工具自己掌握着应用的依赖信息和版本变更情况,具备完整的自管理能力。
Helm模拟的就是上面的做法,它提出了与Linux包管理直接对应的Chart格式和Repository应用仓库,针对k8s特有的一个应用经常要部署多个版本的特点,提出了Release
的专有概念。
Chart用于封装k8s应用涉及的所有资源,通常以目录内的文件集合的形式存在。目录名称就是Chart的名称(没有版本信息)。其中有几个固定的配置文件:Chart.yaml给出了
应用自身的详细信息(名称、版本、许可证、自述、说明、图标,等等),requirements.yaml给出了应用的依赖关系,依赖项指向的是另外一个应用的坐标(名称、版本、
Repository地址),values.yaml给出了所有可配置项目的预定义值。可配置项是指需要运维人员在部署期间调整的那些参数,存储在templates目录下的资源文件中。部署应用
时,Helm会先将管理员设置的值覆盖到values.yaml的默认值上,然后以字符串替换的形式传给templates目录的资源模板,最后生成要部署到k8s的资源文件。由于Chart封装了
足够丰富的信息,所以Helm除了支持命令行操作外,也能很容易根据这些信息自动生成图形化的应用安装、参数设置界面。
Repository仓库用于实现Chart的搜索与下载服务,Helm社区维护了公开的Stable和Incubator的中央仓库,也支持其他人或者组织搭建私有仓库和公共仓库,并能通过
Hub服务把不同的个人或组织搭建的公共仓库聚合起来,形成更大的分布式应用仓库。
Helm提供了应用的全生命周期、版本、依赖项的管理能力,还支持额外的扩展插件,能够加入CI/CD或者其他方面的辅助功能,使得它已经从单纯的工具升级到了应用管理平台。
Helm以模仿Linux包管理的思路去管理k8s应用,在一定程度上是可行的,不过,在Linux与k8s中部署应用时还是存在一些差别。最重要的一点是在Linux中99%的应用都只会
安装一份,而K8s为了保证可用性,同一个应用部署多份副本才是常规操作。Helm为了支持对同一个Chart包进行多次部署,每次安装应用时都会产生一个版本(Release),相当于
该Chart的安装实例。对于无状态的服务,Helm依靠不同的版本就足够支持多个服务并行工作,但对于有状态的服务来说,这些服务会与特定资源或者服务产生依赖关系,譬如要部署
数据库,通常要依赖特定的存储来保存持久化的数据,这样事情就变得复杂了。Helm无法很好的管理这种有状态的依赖关系,所以这类问题变成Operator要解决的痛点。
11.3.3 Operator与CRD
Operator 不应该被称作一种工具或者系统,它应该算是一种封装、部署和管理k8s应用的方法,尤其是针对最复杂的有状态应用去封装运维能力的解决方案。Operator是通过
k8s 1.7 版本开始支持的自定义资源(CRD),把应用封装为另一种更高层次的资源,再把k8s的控制器模式从面向内置资源扩展到面向所有自定义资源,以此来完成度复杂应用的
管理。
Operator 设计理念:
Operator是使用自定义资源(CR,Custom Resource,是CRD的实例),管理应用及其组件的自定义k8s控制器。高级配置和设置由用户在CR中提供。k8s Operator
基于嵌入在Operator逻辑中的最佳实践是将高级指令转换为低级操作。k8s Operator监视CR类型并采取特定于应用的操作,确保当前状态与该资源的理想状态相符。
有状态应用(Stateful Application)和无状态应用(Stateless Application)是指应用程序是否要自己持有运行所需的数据,如果程序每次运行都跟首次运行一样,
不会依赖之前任何操作留下的痕迹,那它就是无状态的;反之,如果程序推倒重来之后,用户能察觉到应用已经发生变化,那它就是有状态的。
站在k8s的角度,是否有状态的本质差异在于有状态的应用会直接依赖于某些外部资源,如es建立实例时必须依赖特定的存储位置,重启后仍然指向同一个数据文件的实例
才被认为是相同的实例。另外,有状态的应用的多个实例之间往往有着特定的拓扑关系与顺序关系,譬如etcd的节点间的选主和投票,各节点都需要知道彼此的存在。为了管理好
那些与应用实例密切相关的状态信息,k8s 从1.9版本开始正式发布了StatefulSet以及对应的StatefulSetController。与普通的ReplicaSet中的Pod相比,由
StatefulSet管理的Pod具有以下几个额外的特征:
1.Pod会按顺序创建和销毁
StatefulSet中的各个Pod会按顺序创建出来,创建后续的Pod前,必须保证前面的Pod已经转入就绪状态。删除StatefulSet中的Pod时会按照与创建顺序的逆序来执行。
2.Pod具有稳定的网络名称
k8s中的Pod都具有唯一的名称,在普通的ReplicaSet中这是靠随机字符产生的,而在StatefulSet中管理的Pod,会以带有顺序的编号作为名称,且能够在重启后
依然保持不变。
3.Pod具有稳定的持久化存储
StatefulSet中的每个Pod都可以拥有自己独立的PersistentVolumeClaim资源,即使Pod被重新调度到其他节点上,它所拥有的持久化磁盘也依然会被挂在到该Pod
当StatefulSet出现以后,k8s就能满足Pod重新创建后仍然保留上一次运行状态的需求,不过有状态的应用的维护不仅限于此,譬如一套es集群来说,通过StatefulSet
最多只能做到创建集群、删除集群、扩容缩容等最基本的操作,其他的运维操作,譬如备份和恢复数据、创建和删除索引、等也十分常用,但是StatefulSet不能提供任何帮助。
如此大量的细节配置,其根本原因在于k8s不知道es是什么,所有k8s不知道的信息、不能启发式推断出来的信息,都必须由用户在资源的元数据中明确定义出来。这种形式
就属于Red Hat在 Operator 设计理念中介绍所说的"低级操作"。
如果我们使用Elastic.co 官方提供的 Operator,就很加单。es Operator提供了一种 kind:Elasticsearch 的自定义资源,在它的帮助下,仅需十几行代码。
有了Elasticsearch Operator的自定义资源,相当于k8s已经学会了怎么操作es,知道了与它相关的参数的含义与默认值,而无需用户手把手的教了,这就是”高级指令“。
Operator 将简洁的高级指令转换为k8s中具体的操作方法,与前面Helm或者Kustomize的方法并不相同。Helm和Kustomize最终仍然是依靠k8s的内置资源来跟k8s
打交道的,Operator则要求开发者自己实现一个专门针对该自定义资源的控制器,在控制器中维护自定义资源的期望状态。通过程序编码来扩展k8s,比只通过内置资源来扩展
要灵活很多。譬如当需要更新集群中Pod对象的时候,由Operator的开发者自己编码实现的控制器完全可以在原地对Pod进行重启,而无需像Deployment那样必须先删除旧Pod,
再创建新的Pod。
使用CRD定义高层次的资源、使用配套的控制器来维护期望状态,带来的好处不仅仅是操作更加便捷,而是遵循k8s一贯基于资源与控制器的设计原则的同时,又不必再
受限于k8s内置资源的表达能力。
目前看来,Operator也许是应对有状态应用的封装运维的最有可行性的方案。
11.3.4 开放应用模型
还有一种封装应用的方案,是阿里云和微软公司联合发布的 开放应用模型(Open Application Model, OAM)。思想的核心是 如何分离开发人员、运维人员与平台人员
的关注点,即开发人员关注业务逻辑的实现,运维人员关注程序平稳的运行,平台人员关注基础设施的能力与稳定性,长期让几个角色关注同一个All-in-One资源文件,并不能
擦除什么火花,反而会将配置工作弄的越来越复杂。
开放应用模型把云原生定义为"由一组相互关联单又离散独立的组件组成,这些组件实例化在合适的运行时上,由配置来控制行为并共同协作提供统一的功能"。
17.凤凰架构:构建可靠的大型分布式系统 --- 技术演示工程实践
1.凤凰架构:构建可靠的大型分布式系统 --- 服务架构演进史