gitbook/深入剖析Kubernetes/docs/23132.md
2022-09-03 22:05:03 +08:00

22 KiB
Raw Permalink Blame History

09 | 从容器到容器云谈谈Kubernetes的本质

你好我是张磊。今天我和你分享的主题是从容器到容器云谈谈Kubernetes的本质。

在前面的四篇文章中我以Docker项目为例一步步剖析了Linux容器的具体实现方式。通过这些讲解你应该能够明白一个“容器”实际上是一个由Linux Namespace、Linux Cgroups和rootfs三种技术构建出来的进程的隔离环境。

从这个结构中我们不难看出一个正在运行的Linux容器其实可以被“一分为二”地看待

  1. 一组联合挂载在/var/lib/docker/aufs/mnt上的rootfs这一部分我们称为“容器镜像”Container Image是容器的静态视图

  2. 一个由Namespace+Cgroups构成的隔离环境这一部分我们称为“容器运行时”Container Runtime是容器的动态视图。

更进一步地说,作为一名开发者,我并不关心容器运行时的差异。因为,在整个“开发-测试-发布”的流程中,真正承载着容器信息进行传递的,是容器镜像,而不是容器运行时。

这个重要假设正是容器技术圈在Docker项目成功后不久就迅速走向了“容器编排”这个“上层建筑”的主要原因作为一家云服务商或者基础设施提供商我只要能够将用户提交的Docker镜像以容器的方式运行起来就能成为这个非常热闹的容器生态图上的一个承载点从而将整个容器技术栈上的价值沉淀在我的这个节点上。

更重要的是只要从我这个承载点向Docker镜像制作者和使用者方向回溯整条路径上的各个服务节点比如CI/CD、监控、安全、网络、存储等等都有我可以发挥和盈利的余地。这个逻辑正是所有云计算提供商如此热衷于容器技术的重要原因通过容器镜像它们可以和潜在用户开发者直接关联起来。

从一个开发者和单一的容器镜像,到无数开发者和庞大的容器集群,容器技术实现了从“容器”到“容器云”的飞跃,标志着它真正得到了市场和生态的认可。

这样,容器就从一个开发者手里的小工具,一跃成为了云计算领域的绝对主角;而能够定义容器组织和管理规范的“容器编排”技术,则当仁不让地坐上了容器技术领域的“头把交椅”。

这其中最具代表性的容器编排工具当属Docker公司的Compose+Swarm组合以及Google与RedHat公司共同主导的Kubernetes项目。

我在前面介绍容器技术发展历史的四篇预习文章中已经对这两个开源项目做了详细的剖析和评述。所以在今天的这次分享中我会专注于本专栏的主角Kubernetes项目谈一谈它的设计与架构。

跟很多基础设施领域先有工程实践、后有方法论的发展路线不同Kubernetes项目的理论基础则要比工程实践走得靠前得多这当然要归功于Google公司在2015年4月发布的Borg论文了。

Borg系统一直以来都被誉为Google公司内部最强大的“秘密武器”。虽然略显夸张但这个说法倒不算是吹牛。

因为相比于Spanner、BigTable等相对上层的项目Borg要承担的责任是承载Google公司整个基础设施的核心依赖。在Google公司已经公开发表的基础设施体系论文中Borg项目当仁不让地位居整个基础设施技术栈的最底层。


图片来源:Malte Schwarzkopf. “Operating system support for warehouse-scale computing”. PhD thesis. University of Cambridge Computer Laboratory (to appear), 2015, Chapter 2.

上面这幅图来自于Google Omega论文的第一作者的博士毕业论文。它描绘了当时Google已经公开发表的整个基础设施栈。在这个图里你既可以找到MapReduce、BigTable等知名项目也能看到Borg和它的继任者Omega位于整个技术栈的最底层。

正是由于这样的定位Borg可以说是Google最不可能开源的一个项目。而幸运的是得益于Docker项目和容器技术的风靡它却终于得以以另一种方式与开源社区见面这个方式就是Kubernetes项目。

所以相比于“小打小闹”的Docker公司、“旧瓶装新酒”的Mesos社区Kubernetes项目从一开始就比较幸运地站上了一个他人难以企及的高度在它的成长阶段这个项目每一个核心特性的提出几乎都脱胎于Borg/Omega系统的设计与经验。更重要的是这些特性在开源社区落地的过程中又在整个社区的合力之下得到了极大的改进修复了很多当年遗留在Borg体系中的缺陷和问题。

所以尽管在发布之初被批评是“曲高和寡”但是在逐渐觉察到Docker技术栈的“稚嫩”和Mesos社区的“老迈”之后这个社区很快就明白了Kubernetes项目在Borg体系的指导下体现出了一种独有的“先进性”与“完备性”而这些特质才是一个基础设施领域开源项目赖以生存的核心价值。

为了更好地理解这两种特质我们不妨从Kubernetes的顶层设计说起。

首先Kubernetes项目要解决的问题是什么

编排?调度?容器云?还是集群管理?

实际上这个问题到目前为止都没有固定的答案。因为在不同的发展阶段Kubernetes需要着重解决的问题是不同的。

但是对于大多数用户来说他们希望Kubernetes项目带来的体验是确定的现在我有了应用的容器镜像请帮我在一个给定的集群上把这个应用运行起来。

更进一步地说我还希望Kubernetes能给我提供路由网关、水平扩展、监控、备份、灾难恢复等一系列运维能力。

等一下这些功能听起来好像有些耳熟这不就是经典PaaS比如Cloud Foundry项目的能力吗

而且有了Docker之后我根本不需要什么Kubernetes、PaaS只要使用Docker公司的Compose+Swarm项目就完全可以很方便地DIY出这些功能了

所以说如果Kubernetes项目只是停留在拉取用户镜像、运行容器以及提供常见的运维功能的话那么别说跟“原生”的Docker Swarm项目竞争了哪怕跟经典的PaaS项目相比也难有什么优势可言。

而实际上在定义核心功能的过程中Kubernetes项目正是依托着Borg项目的理论优势才在短短几个月内迅速站稳了脚跟进而确定了一个如下图所示的全局架构

我们可以看到Kubernetes项目的架构跟它的原型项目Borg非常类似都由Master和Node两种节点组成而这两种角色分别对应着控制节点和计算节点。

其中控制节点即Master节点由三个紧密协作的独立组件组合而成它们分别是负责API服务的kube-apiserver、负责调度的kube-scheduler以及负责容器编排的kube-controller-manager。整个集群的持久化数据则由kube-apiserver处理后保存在Etcd中。

而计算节点上最核心的部分则是一个叫作kubelet的组件。

在Kubernetes项目中kubelet主要负责同容器运行时比如Docker项目打交道。而这个交互所依赖的是一个称作CRIContainer Runtime Interface的远程调用接口这个接口定义了容器运行时的各项核心操作比如启动一个容器需要的所有参数。

这也是为何Kubernetes项目并不关心你部署的是什么容器运行时、使用的什么技术实现只要你的这个容器运行时能够运行标准的容器镜像它就可以通过实现CRI接入到Kubernetes项目当中。

而具体的容器运行时比如Docker项目则一般通过OCI这个容器运行时规范同底层的Linux操作系统进行交互把CRI请求翻译成对Linux操作系统的调用操作Linux Namespace和Cgroups等

此外kubelet还通过gRPC协议同一个叫作Device Plugin的插件进行交互。这个插件是Kubernetes项目用来管理GPU等宿主机物理设备的主要组件也是基于Kubernetes项目进行机器学习训练、高性能作业支持等工作必须关注的功能。

kubelet的另一个重要功能则是调用网络插件和存储插件为容器配置网络和持久化存储。这两个插件与kubelet进行交互的接口分别是CNIContainer Networking Interface和CSIContainer Storage Interface

实际上kubelet这个奇怪的名字来自于Borg项目里的同源组件Borglet。不过如果你浏览过Borg论文的话就会发现这个命名方式可能是kubelet组件与Borglet组件的唯一相似之处。因为Borg项目并不支持我们这里所讲的容器技术而只是简单地使用了Linux Cgroups对进程进行限制。

这就意味着像Docker这样的“容器镜像”在Borg中是不存在的Borglet组件也自然不需要像kubelet这样考虑如何同Docker进行交互、如何对容器镜像进行管理的问题也不需要支持CRI、CNI、CSI等诸多容器技术接口。

可以说kubelet完全就是为了实现Kubernetes项目对容器的管理能力而重新实现的一个组件与Borg之间并没有直接的传承关系。

备注虽然不使用Docker但Google内部确实在使用一个包管理工具名叫Midas Package Manager (MPM)其实它可以部分取代Docker镜像的角色。

那么Borg对于Kubernetes项目的指导作用又体现在哪里呢

答案是Master节点。

虽然在Master节点的实现细节上Borg项目与Kubernetes项目不尽相同但它们的出发点却高度一致如何编排、管理、调度用户提交的作业

所以Borg项目完全可以把Docker镜像看作一种新的应用打包方式。这样Borg团队过去在大规模作业管理与编排上的经验就可以直接“套”在Kubernetes项目上了。

这些经验最主要的表现就是,从一开始Kubernetes项目就没有像同时期的各种“容器云”项目那样把Docker作为整个架构的核心而仅仅把它作为最底层的一个容器运行时实现。

而Kubernetes项目要着重解决的问题则来自于Borg的研究人员在论文中提到的一个非常重要的观点

运行在大规模集群中的各种任务之间,实际上存在着各种各样的关系。这些关系的处理,才是作业编排和管理系统最困难的地方。

事实也正是如此。

其实这种任务与任务之间的关系在我们平常的各种技术场景中随处可见。比如一个Web应用与数据库之间的访问关系一个负载均衡器和它的后端服务之间的代理关系一个门户应用与授权组件之间的调用关系。

更进一步地说同属于一个服务单位的不同功能之间也完全可能存在这样的关系。比如一个Web应用与日志搜集组件之间的文件交换关系。

而在容器技术普及之前传统虚拟机环境对这种关系的处理方法都是比较“粗粒度”的。你会经常发现很多功能并不相关的应用被一股脑儿地部署在同一台虚拟机中只是因为它们之间偶尔会互相发起几个HTTP请求。

更常见的情况则是一个应用被部署在虚拟机里之后你还得手动维护很多跟它协作的守护进程Daemon用来处理它的日志搜集、灾难恢复、数据备份等辅助工作。

但容器技术出现以后,你就不难发现,在“功能单位”的划分上,容器有着独一无二的“细粒度”优势:毕竟容器的本质,只是一个进程而已。

也就是说只要你愿意那些原先拥挤在同一个虚拟机里的各个应用、组件、守护进程都可以被分别做成镜像然后运行在一个个专属的容器中。它们之间互不干涉拥有各自的资源配额可以被调度在整个集群里的任何一台机器上。而这正是一个PaaS系统最理想的工作状态也是所谓“微服务”思想得以落地的先决条件。

当然如果只做到“封装微服务、调度单容器”这一层次Docker Swarm项目就已经绰绰有余了。如果再加上Compose项目你甚至还具备了处理一些简单依赖关系的能力比如一个“Web容器”和它要访问的数据库“DB容器”。

在Compose项目中你可以为这样的两个容器定义一个“link”而Docker项目则会负责维护这个“link”关系其具体做法是Docker会在Web容器中将DB容器的IP地址、端口等信息以环境变量的方式注入进去供应用进程使用比如

    DB_NAME=/web/db
    DB_PORT=tcp://172.17.0.5:5432
    DB_PORT_5432_TCP=tcp://172.17.0.5:5432
    DB_PORT_5432_TCP_PROTO=tcp
    DB_PORT_5432_TCP_PORT=5432
    DB_PORT_5432_TCP_ADDR=172.17.0.5

而当DB容器发生变化时比如镜像更新被迁移到其他宿主机上等等这些环境变量的值会由Docker项目自动更新。这就是平台项目自动地处理容器间关系的典型例子。

可是,如果我们现在的需求是,要求这个项目能够处理前面提到的所有类型的关系,甚至还要能够支持未来可能出现的更多种类的关系呢?

这时“link”这种单独针对一种案例设计的解决方案就太过简单了。如果你做过架构方面的工作就会深有感触一旦要追求项目的普适性那就一定要从顶层开始做好设计。

所以,Kubernetes项目最主要的设计思想是从更宏观的角度以统一的方式来定义任务之间的各种关系并且为将来支持更多种类的关系留有余地。

比如Kubernetes项目对容器间的“访问”进行了分类首先总结出了一类非常常见的“紧密交互”的关系这些应用之间需要非常频繁的交互和访问又或者它们会直接通过本地文件进行信息交换。

在常规环境下这些应用往往会被直接部署在同一台机器上通过Localhost通信通过本地磁盘目录交换文件。而在Kubernetes项目中这些容器则会被划分为一个“Pod”Pod里的容器共享同一个Network Namespace、同一组数据卷从而达到高效率交换信息的目的。

Pod是Kubernetes项目中最基础的一个对象源自于Google Borg论文中一个名叫Alloc的设计。在后续的章节中我们会对Pod做更进一步地阐述。

而对于另外一种更为常见的需求比如Web应用与数据库之间的访问关系Kubernetes项目则提供了一种叫作“Service”的服务。像这样的两个应用往往故意不部署在同一台机器上这样即使Web应用所在的机器宕机了数据库也完全不受影响。可是我们知道对于一个容器来说它的IP地址等信息不是固定的那么Web应用又怎么找到数据库容器的Pod呢

所以Kubernetes项目的做法是给Pod绑定一个Service服务而Service服务声明的IP地址等信息是“终生不变”的。这个Service服务的主要作用就是作为Pod的代理入口Portal从而代替Pod对外暴露一个固定的网络地址

这样对于Web应用的Pod来说它需要关心的就是数据库Pod的Service信息。不难想象Service后端真正代理的Pod的IP地址、端口等信息的自动更新、维护则是Kubernetes项目的职责。

像这样围绕着容器和Pod不断向真实的技术场景扩展我们就能够摸索出一幅如下所示的Kubernetes项目核心功能的“全景图”。

按照这幅图的线索我们从容器这个最基础的概念出发首先遇到了容器间“紧密协作”关系的难题于是就扩展到了Pod有了Pod之后我们希望能一次启动多个应用的实例这样就需要Deployment这个Pod的多实例管理器而有了这样一组相同的Pod后我们又需要通过一个固定的IP地址和端口以负载均衡的方式访问它于是就有了Service。

可是如果现在两个不同Pod之间不仅有“访问关系”还要求在发起时加上授权信息。最典型的例子就是Web应用对数据库访问时需要Credential数据库的用户名和密码信息。那么在Kubernetes中这样的关系又如何处理呢

Kubernetes项目提供了一种叫作Secret的对象它其实是一个保存在Etcd里的键值对数据。这样你把Credential信息以Secret的方式存在Etcd里Kubernetes就会在你指定的Pod比如Web应用的Pod启动时自动把Secret里的数据以Volume的方式挂载到容器里。这样这个Web应用就可以访问数据库了。

除了应用与应用之间的关系外,应用运行的形态是影响“如何容器化这个应用”的第二个重要因素。

为此Kubernetes定义了新的、基于Pod改进后的对象。比如Job用来描述一次性运行的Pod比如大数据任务再比如DaemonSet用来描述每个宿主机上必须且只能运行一个副本的守护进程服务又比如CronJob则用于描述定时任务等等。

如此种种正是Kubernetes项目定义容器间关系和形态的主要方法。

可以看到Kubernetes项目并没有像其他项目那样为每一个管理功能创建一个指令然后在项目中实现其中的逻辑。这种做法的确可以解决当前的问题但是在更多的问题来临之后往往会力不从心。

相比之下在Kubernetes项目中我们所推崇的使用方法是

  • 首先通过一个“编排对象”比如Pod、Job、CronJob等来描述你试图管理的应用
  • 然后再为它定义一些“服务对象”比如Service、Secret、Horizontal Pod Autoscaler自动水平扩展器等。这些对象会负责具体的平台级功能。

这种使用方法就是所谓的“声明式API”。这种API对应的“编排对象”和“服务对象”都是Kubernetes项目中的API对象API Object

这就是Kubernetes最核心的设计理念也是接下来我会重点剖析的关键技术点。

最后我来回答一个更直接的问题Kubernetes项目如何启动一个容器化任务呢

比如我现在已经制作好了一个Nginx容器镜像希望让平台帮我启动这个镜像。并且我要求平台帮我运行两个完全相同的Nginx副本以负载均衡的方式共同对外提供服务。

  • 如果是自己DIY的话可能需要启动两台虚拟机分别安装两个Nginx然后使用keepalived为这两个虚拟机做一个虚拟IP。

  • 而如果使用Kubernetes项目呢你需要做的则是编写如下这样一个YAML文件比如名叫nginx-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

在上面这个YAML文件中我们定义了一个Deployment对象它的主体部分spec.template部分是一个使用Nginx镜像的Pod而这个Pod的副本数是2replicas=2

然后执行:

$ kubectl create -f nginx-deployment.yaml

这样两个完全相同的Nginx容器副本就被启动了。

不过这么看来做同样一件事情Kubernetes用户要做的工作也不少嘛。

别急在后续的讲解中我会陆续介绍Kubernetes项目这种“声明式API”的种种好处以及基于它实现的强大的编排能力。

拭目以待吧。

总结

首先,我和你一起回顾了容器的核心知识,说明了容器其实可以分为两个部分:容器运行时和容器镜像。

然后我重点介绍了Kubernetes项目的架构详细讲解了它如何使用“声明式API”来描述容器化业务和容器间关系的设计思想。

实际上过去很多的集群管理项目比如Yarn、Mesos以及Swarm所擅长的都是把一个容器按照某种规则放置在某个最佳节点上运行起来。这种功能我们称为“调度”。

而Kubernetes项目所擅长的是按照用户的意愿和整个系统的规则完全自动化地处理好容器之间的各种关系。这种功能,就是我们经常听到的一个概念:编排。

所以说Kubernetes项目的本质是为用户提供一个具有普遍意义的容器编排工具。

不过更重要的是Kubernetes项目为用户提供的不仅限于一个工具。它真正的价值乃在于提供了一套基于容器构建分布式系统的基础依赖。关于这一点相信你会在今后的学习中体会越来越深。

思考题

  1. 这今天的分享中我介绍了Kubernetes项目的架构。你是否了解了Docker SwarmSwarmKit项目和Kubernetes在架构上和使用方法上的异同呢

  2. 在Kubernetes之前很多项目都没办法管理“有状态”的容器不能从一台宿主机“迁移”到另一台宿主机上的容器。你是否能列举出阻止这种“迁移”的原因都有哪些呢

感谢你的收听,欢迎你给我留言,也欢迎分享给更多的朋友一起阅读。