learn-tech/专栏/周志明的架构课/47_容器的崛起(下):系统、应用、集群的封装.md
2024-10-16 06:37:41 +08:00

20 KiB
Raw Blame History

                        因收到Google相关通知网站将会择期关闭。相关通知内容
                        
                        
                        47 _ 容器的崛起(下):系统、应用、集群的封装
                        你好,我是周志明。在理解了从隔离角度出发的容器化技术的发展之后,这节课我们接着从封装的角度来学习容器应用的发展。

封装系统LXC

当文件系统、访问、资源都可以被隔离后容器就已经具备它降生所需要的全部前置支撑条件了并且Linux的开发者们也已经明确地看到了这一点。

因此为了降低普通用户综合使用namespaces、cgroups这些低级特性的门槛2008年Linux Kernel 2.6.24内核在刚刚开始提供cgroups的同一时间就马上发布了名为Linux容器LinuX ContainersLXC的系统级虚拟化功能。

当然在这之前在Linux上并不是没有系统级虚拟化的解决方案比如传统的OpenVZ和Linux-VServer都能够实现容器隔离并且只会有很低的性能损失按OpenVZ提供的数据只会有1~3%的损失),但它们都是非官方的技术,使用它们最大的阻碍是系统级虚拟化必须要有内核的支持。为此,它们就只能通过非官方内核补丁的方式来修改标准内核,才能获得那些原本在内核中不存在的能力。

如此一来LXC就带着令人瞩目的光环登场它的出现促使“容器”从一个阳春白雪的、只流传于开发人员口中的技术词汇逐渐向整个软件业的公共概念、共同语言发展就如同今天的“服务器”“客户端”和“互联网”一样。

不过相信你现在肯定会好奇为什么如今一提到容器大家首先联想到的是Docker而不是LXC为什么去问10个开发人员至少有9个听过Docker但如果问LXC可能只有1个人听说过

那么我们首先可以知道的是LXC的出现肯定是受到了OpenVZ和Linux-VServer的启发摸着巨人的肩膀过河当然没有什么不对。但可惜的是LXC在设定自己的发展目标时也被前辈们的影响所局限了。

其实LXC眼中的容器的定义与OpenVZ和Linux-VServer并没有什么差别它们都是一种封装系统的轻量级虚拟机而Docker眼中的容器的定义则是一种封装应用的技术手段。这两种封装理念在技术层面并没有什么本质区别但在应用效果上差异可就相当大了。

我举个具体的例子如果你要建设一个LAMPLinux、Apache、MySQL、PHP应用按照LXC的思路你应该先编写或者寻找到LAMP的template可以暂且不准确地类比为LXC版本的Dockerfile吧以此构造出一个安装了LAMP的虚拟系统。

如果按部署虚拟机的角度来看这还算挺方便的作为那个时代距今也就十年的系统管理员所有软件、补丁、配置都是要自己搞定的部署一台新虚拟机要花费一两天时间都很正常而有了LXC的template一下子帮你把LAMP都安装好了还想要啥自行车

但是作为一名现代的系统管理员这里的问题就相当大了如果我想把LAMP改为LNMPLinux、Nginx、MySQL、PHP该怎么办如果我想把LAMP里的MySQL 5调整为MySQL 8该怎么办这些都得通过找到或者自己编写新的template来解决。

或者好吧那这台机的软件、版本都配置对了下一台机我要构建LYME或者MEAN又该怎么办呢以封装系统为出发点如果仍然是按照先装系统再装软件的思路就永远无法做到一两分钟甚至十几秒钟就构造出一个合乎要求的软件运行环境这也就决定了LXC不可能形成今天的容器生态。

所以接下来舞台的聚光灯终于落到了Docker身上。

封装应用Docker

在2013年宣布开源的Docker毫无疑问是容器发展历史上里程碑式的发明然而Docker的成功似乎没有太多技术驱动的成分。至少对于开源早期的Docker而言确实没有什么能构成壁垒的技术。

事实上它的容器化能力直接来源于LXC它的镜像分层组合的文件系统直接来源于AUFS在Docker开源后不久就有人仅用了一百多行的Shell脚本便实现了Docker的核心功能名为Bocker提供了docker bulid/pull/images/ps/run/exec/logs/commit/rm/rmi等功能

那你可能就要问了为何历史选择了Docker而不是LXC或者其他容器技术呢对于这个问题我想引用下转述非直译有所精简DotCloud公司当年创造Docker的公司已于2016年倒闭创始人所罗门 · 海克斯Solomon Hykes在Stackoverflow上的一段问答

为什么要用Docker而不是LXCWhy would I use Docker over plain LXC-

Docker除了包装来自Linux内核的特性之外它的价值还在于-

跨机器的绿色部署Docker定义了一种将应用及其所有的环境依赖都打包到一起的格式仿佛它原本就是绿色软件一样。而LXC并没有提供这样的能力使用LXC部署的新机器很多细节都要依赖人的介入虚拟机的环境基本上肯定会跟你原本部署程序的机器有所差别。-

以应用为中心的封装Docker封装应用而非封装机器的理念贯穿了它的设计、API、界面、文档等多个方面。相比之下LXC将容器视为对系统的封装这局限了容器的发展。-

自动构建Docker提供了开发人员从在容器中构建产品的全部支持开发人员无需关注目标机器的具体配置就可以使用任意的构建工具链在容器中自动构建出最终产品。-

多版本支持Docker支持像Git一样管理容器的连续版本进行检查版本间差异、提交或者回滚等操作。从历史记录中你可以查看到该容器是如何一步一步构建成的并且只增量上传或下载新版本中变更的部分。-

组件重用Docker允许将任何现有容器作为基础镜像来使用以此构建出更加专业的镜像。-

共享Docker拥有公共的镜像仓库成千上万的Docker用户在上面上传了自己的镜像同时也使用他人上传的镜像。-

工具生态Docker开放了一套可自动化和自行扩展的接口在此之上用户可以实现很多工具来扩展其功能比如容器编排、管理界面、持续集成等等。- —— Solomon HykesStackoverflow2013

这段回答也被收录到了Docker官网的FAQ上从Docker开源到今天从没有改变过。

其实促使Docker一问世就惊艳世间的并不是什么黑科技式的秘密武器而是它符合历史潮流的创意与设计理念还有充分开放的生态运营。由此可见在正确的时候正确的人手上有一个优秀的点子确实有机会引爆一个时代。

这里我还想让你看一张图片它是Docker开源一年后截至2014年12月获得的成绩。

我们可以发现从开源到现在只过了短短数年时间Docker就已经成为了软件开发、测试、分发、部署等各个环节都难以或缺的基础支撑而它自身的架构也发生了相当大的改变Docker被分解为了几个子系统包括Docker Client、Docker Daemon、Docker Registry、Docker Container等等以及Graph、Driver、libcontainer等各司其职的模块。

所以此时我们再说一百多行脚本就能实现Docker的核心功能再说Docker没有太高的技术含量就不太合适了。

2014年Docker开源了自己用Golang开发的libcontainer这是一个越过LXC直接操作namespaces和cgroups的核心模块有了libcontainer以后Docker就能直接与系统内核打交道不必依赖LXC来提供容器化隔离能力了。

到了2015年在Docker的主导和倡议下多家公司联合制定了“开放容器交互标准”Open Container InitiativeOCI这是一个关于容器格式和运行时的规范文件其中包含了运行时标准runtime-spec 、容器镜像标准image-spec和镜像分发标准distribution-spec分发标准还未正式发布

运行时标准定义了应该如何运行一个容器、如何管理容器的状态和生命周期、如何使用操作系统的底层特性namespaces、cgroup、pivot_root等 容器镜像标准规定了容器镜像的格式、配置、元数据的格式,你可以理解为对镜像的静态描述; 镜像分发标准则规定了镜像推送和拉取的网络交互过程。

由此为了符合OCI标准Docker推动自身的架构继续向前演进。

首先它是将libcontainer独立出来封装重构成runC项目并捐献给了Linux基金会管理。runC是OCI Runtime的首个参考实现它提出了“让标准容器无所不在”Make Standard Containers Available Everywhere的口号。

而为了能够兼容所有符合标准的OCI Runtime实现Docker进一步重构了Docker Daemon子系统把其中与运行时交互的部分抽象为了containerd项目。

这是一个负责管理容器执行、分发、监控、网络、构建、日志等功能的核心模块其内部会为每个容器运行时创建一个containerd-shim适配进程默认与runC搭配工作但也可以切换到其他OCI Runtime实现上然而实际并没做到最后containerd仍是紧密绑定于runC

后来到了2016年Docker把containerd捐献给了CNCF管理。

可以说runC与containerd两个项目的捐赠托管既带有Docker对开源信念的追求也带有Docker在众多云计算大厂夹击下自救的无奈这两个项目也将会成为未来Docker消亡和存续的伏笔到这节课的末尾你就能理解这句矛盾的话了

以上我列举的这些Docker推动的开源与标准化工作既是对Docker为开源乃至整个软件业做出贡献的赞赏也是为后面给你介绍容器编排时讲解当前容器引擎的混乱关系做的前置铺垫。

我们当然很清楚的一个事实就是Docker目前无疑在容器领域具有统治地位但其统治的稳固程度不仅没到高枕无忧说是危机四伏都不为过。

我之所以这么说的原因是因为现在已经能隐隐看出足以威胁动摇Docker地位的潜在可能性而引出这个风险的就是Docker虽然赢得了容器战争的胜利但Docker Swarm却输掉了容器编排战争。

实际上从结果回望当初Docker能赢得容器战争是存在了一些偶然性的而能确定的是Docker Swarm输掉编排战争是必然的。为什么这么说呢下面我就来揭晓答案。

封装集群Kubernetes

如果说以Docker为代表的容器引擎是把软件的发布流程从分发二进制安装包转变为了直接分发虚拟化后的整个运行环境让应用得以实现跨机器的绿色部署那以Kubernetes为代表的容器编排框架就是把大型软件系统运行所依赖的集群环境也进行了虚拟化让集群得以实现跨数据中心的绿色部署并能够根据实际情况自动扩缩。

我们从上节课的容器崛起之路讲到现在Docker和Kubernetes这个阶段已经不再是介绍历史了从这里开始发生的变化都是近几年软件业界中的热点事件也是“容器的崛起”这个小章节我们要讨论的主要话题。不过现在我暂时不打算介绍Kubernetes的技术细节在“容器间网络”“容器持久化存储”及“资源调度”这几个章节中我还会进行更详细的解析。

在今天这节课里我们就先从宏观层面去理解Kubernetes的诞生与演变的驱动力这对正确理解未来云原生的发展方向是至关重要的。

从Docker到Kubernetes

众所周知Kubernetes可谓是出身名门它的前身是Google内部已经运行多年的集群管理系统Borg在2014年6月使用Golang完全重写后开源。自它诞生之日起只要能与云计算稍微扯上关系的业界巨头都对Kubernetes争相追捧IBM、RedHat、Microsoft、VMware和华为都是它最早期的代码贡献者。

此时距离云计算从实验室到工业化应用已经有十个年头不过大量应用使用云计算的方式还是停滞在了传统的IDCInternet Data Center时代它们仅仅是用云端的虚拟机代替了传统的物理机而已。

尽管早在2013年Pivotal持有着Spring Framework和Cloud Foundry的公司就提出了“云原生”的概念但是要实现服务化、具备韧性Resilience、弹性Elasticity、可观测性Observability的软件系统依旧十分困难在当时基本只能依靠架构师和程序员高超的个人能力云计算本身还帮不上什么忙。

而在云的时代,不能充分利用云的强大能力,这让云计算厂商无比遗憾,也无比焦虑。

所以可以说直到Kubernetes横空出世大家才终于等到了破局的希望认准了这就是云原生时代的操作系统是让复杂软件在云计算下获得韧性、弹性、可观测性的最佳路径也是为厂商们推动云计算时代加速到来的关键引擎之一。

2015年7月Kubernetes发布了第一个正式版本1.0版更重要的事件是Google宣布与Linux基金会共同筹建云原生基金会Cloud Native Computing FoundationCNCF并且把Kubernetes托管到CNCF成为其第一个项目。随后Kubernetes就以摧枯拉朽之势消灭了容器编排领域的其他竞争对手哪怕Docker Swarm有着Docker在容器引擎方面的先天优势DotCloud后来甚至把Swarm直接内置入Docker之中都不能稍稍阻挡Kubernetes前进的步伐。

但是我们也要清楚Kubernetes的成功与Docker的成功并不一样。

Docker靠的是优秀的理念它是以一个“好点子”引爆了一个时代。我相信就算没有Docker也会有Cocker或者Eocker的出现但由成立仅三年的DotCloud公司三年后又倒闭做成了这样的产品确实有一定的偶然性。

而Kubernetes的成功不仅有Google深厚的技术功底作支撑、有领先时代的设计理念更加关键的是Kubernetes的出现符合所有云计算大厂的切身利益有着业界巨头不遗余力地广泛支持所以它的成功便是一种必然。

Kubernetes与Docker两者的关系十分微妙因此我们把握住两者关系的变化过程是理解Kubernetes架构演变与CRI、OCI规范的良好线索。

Kubernetes是如何一步步与Docker解耦的

在Kubernetes开源的早期它是完全依赖且绑定Docker的并没有过多地考虑日后有使用其他容器引擎的可能性。直到Kubernetes 1.5之前Kubernetes管理容器的方式都是通过内部的DockerManager向Docker Engine以HTTP方式发送指令通过Docker来操作镜像的增删改查的如上图最右边线路的箭头所示图中的kubelet是集群节点中的代理程序负责与管理集群的Master通信其他节点的含义在下面介绍时都会有解释

现在我们可以把这个阶段的Kubernetes与容器引擎的调用关系捋直并结合前面提到的Docker捐献containerd与runC后重构的调用一起来梳理下这个完整的调用链条

Kubernetes Master → kubelet → DockerManager → Docker Engine → containerd → runC

然后到了2016年Kubernetes 1.5版本开始引入“容器运行时接口”Container Runtime InterfaceCRI这是一个定义容器运行时应该如何接入到kubelet的规范标准从此Kubernetes内部的DockerManager就被更为通用的KubeGenericRuntimeManager所替代了实际上在1.6.6之前都仍然可以看到DockerManagerkubelet与KubeGenericRuntimeManager之间通过gRPC协议通信。

不过由于CRI是在Docker之后才发布的规范Docker是肯定不支持CRI的所以Kubernetes又提供了DockerShim服务作为Docker与CRI的适配层由它与Docker Engine以HTTP形式通信从而实现了原来DockerManager的全部功能。

此时Docker对Kubernetes来说就只是一项默认依赖而非之前的不可或缺了现在它们的调用链为

Kubernetes Master → kubelet → KubeGenericRuntimeManager → DockerShim → Docker Engine → containerd → runC

接着再到2017年由Google、RedHat、Intel、SUSE、IBM联合发起的CRI-OContainer Runtime Interface Orchestrator项目发布了首个正式版本。

一方面我们从名字上就可以看出来它肯定是完全遵循CRI规范来实现的另一方面它可以支持所有符合OCI运行时标准的容器引擎默认仍然是与runC搭配工作的如果要换成Clear Containers、Kata Containers等其他OCI运行时也完全没有问题。

不过到这里开源版的Kubernetes虽然完全支持用户去自由选择根据用户宿主机的环境选择是使用CRI-O、cri-containerd还是DockerShim来作为CRI实现但在RedHat自己扩展定制的Kubernetes企业版即OpenShift 4中调用链已经没有了Docker Engine的身影

Kubernetes Master → kubelet → KubeGenericRuntimeManager → CRI-O→ runC

当然因为此时Docker在容器引擎中的市场份额仍然占有绝对优势对于普通用户来说如果没有明确的收益也并没有什么动力要把Docker换成别的引擎。所以CRI-O即使摆出了直接挖掉Docker根基的凶悍姿势实际上也并没有给Docker带来太多即时可见的影响。不过我们能够想像此时Docker心中肯定充斥了难以言喻的危机感。

时间继续来到了2018年由Docker捐献给CNCF的containerd在CNCF的精心孵化下发布了1.1版1.1版与1.0版的最大区别是此时它已经完美地支持了CRI标准这意味着原本用作CRI适配器的cri-containerd从此不再被需要。

此时我们再观察Kubernetes到容器运行时的调用链就会发现调用步骤会比通过DockerShim、Docker Engine与containerd交互的步骤要减少两步这又意味着用户只要愿意抛弃掉Docker情怀的话在容器编排上就可以至少省略一次HTTP调用获得性能上的收益。而且根据Kubernetes官方给出的测试数据这些免费的收益还相当地可观。

如此Kubernetes从1.10版本宣布开始支持containerd 1.1在调用链中就已经能够完全抹去Docker Engine的存在了

Kubernetes Master → kubelet → KubeGenericRuntimeManager → containerd → runC

而到了今天要使用哪一种容器运行时就取决于你安装Kubernetes时宿主机上的容器运行时环境但对于云计算厂商来说比如国内的阿里云ACK、腾讯云TKE等直接提供的Kubernetes容器环境采用的容器运行时普遍都已经是containerd了毕竟运行性能对它们来说就是核心生产力和竞争力。

小结

学完这节课我们可以试着来做一个判断在未来随着Kubernetes的持续发展壮大Docker Engine经历从不可或缺、默认依赖、可选择、直到淘汰会是大概率的事件。从表面上看这件事情是Google、RedHat等云计算大厂联手所为可实际淘汰它的还是技术发展的潮流趋势。这就如同Docker诞生时依赖LXC到最后用libcontainer取代掉LXC一样。

同时我们也该看到事情的另一面现在连LXC都还没有挂掉反倒还发展出了更加专注于跟OpenVZ等系统级虚拟化竞争的LXD就可以相信Docker本身也是很难彻底消亡的已经养成习惯的CLI界面已经形成成熟生态的镜像仓库等都应该会长期存在只是在容器编排领域未来的Docker很可能只会以runC和containerd的形式存续下去毕竟它们最初都源于Docker的血脉。

一课一思

在2021年1月初Kubernetes宣布将会在v1.23版本中把Dockershim从 Kubelet中移除那么你会如何看待容器化日后的发展呢

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