转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。
上节中我们提到了社区生态的发展使得 Kubernetes 得到了良性的发展和传播。比起相对封闭的 Docker 社区开放的 CNCF 社区获得了更大成功,但仅仅是社区的活力对比还不足以让 Docker 这么快的败下阵来,其根本原因是 Kubernetes 的对容器编排技术的理解比起 Docker 更胜一筹。这种优势几乎是压到性的降维打击,Docker 毫无还手之力。
接下来便为大家介绍在这场容器大战之中,Kubernetes 如何占据优势地位。
所谓容器编排,其实就是处理容器和容器之间的关系,在一个分布式的大型系统里,不可能是以多个单一个体存在的,它们可能是一个与多个,一群与一群这样相互交织着。
Docker 构建的是以 Docker 容器为最核心的 PaaS 生态,包括以 Docker Compose 为主的简单容器关系编排,以及以 Docker Swarm 为主的线上运维平台。用户可以通过 Docker Compose 处理自己集群中容器之间的关系,并且通过 Docker Swarm 管理运维自己的集群,可以看到这一切其实就是当初 Cloud Foundry 的 PaaS 功能,所主打的就是和 Docker 容器的无缝集成。
Docker Compose 做到的是为多个有交互关系建立一种“连接”,把它们全部编写在一个 docker-compose.yaml 文件中,然后统一发布(我后面说到的组里的 ELK 功能就是这样做的),这样做也有优点,就是对于简单的几个容器之间的交互来说非常便利。但是对于大型的集群来说却有些杯水车薪,并且这种每出现一种新需求就需要单独做一项新功能的开发模式,将使后期代码维护变得十分困难。
Kubernetes 如果要和 Docker 对抗,肯定不能仅仅只做 Docker 容器管理这种 Docker 本身就已经支持的功能了,这样的话别说分庭抗礼,可能连 Docker 的基本用户都吸引不到。因此在 Kubernetes 设计之初,就确定了不以 Docker 为核心依赖的设计理念。在 Kubernetes 中,Docker 仅是容器运行时实现的一个可选项,用户可以依据自己的喜好任意调换自己需要的容器内容且 Kubernetes 为这些容器都提供了接口。此外,Kubernetes 准确的抓住了 Docker 容器的一个致命性的弱点进行了自身创新。
接下来就让我们一起来了解,这个给 Docker 造成降维打击的内容究竟是什么?
与 Docker 这种站在容器视角上只能处理容器之间的关系所不同,Kubernetes 所做的是从软件工程的设计理念出发,将关系进行了不同类的划分,定义了紧密关系( Pod 之间)和交互关系( Service 之间)的概念,然后再对不同的关系进行特定的编排实现。
乍一听你可能是一头雾水。这里举个不太实际但是一看就懂的例子:如果把容器之间的关系比作人之间的关系,Docker 能处理的是仅仅是站在单一个体的角度上,处理人与人之间的人际关系;而 Kubernetes 确是上帝,站在上帝视角不仅能处理人与人之间的人际关系,还能处理狗与狗之间的狗际关系,最主要的是能处理人与狗之间的交往关系。
而实现上述紧密关系的原理,就是 Kubernetes 创新的 Pod 。
Pod 是 Kubernetes 所创新的一个概念,其原型是 Borg 中的 Alloc,是 Kubernetes 运行应用的最小执行单元,由一个或者多个紧密协作的容器组合而成,其出现的原因是针对容器的一个致命性弱点——单一进程这问题的扩展,让容器有了进程组的概念。通过第一节,我们知道了容器的本质是一个进程,其本身就是超级进程,其他进程都必须是它的子进程,因此在容器中,没有进程组的概念,而在日常的程序运行中,进程组是常常配合使用的。
为了给大家介绍 Pod 处理紧密关系的原理,这里举一个进程组的例子:
Linux 中有一个负责操作系统日志处理的程序 rsyslogd 是由三个模块组成,分别是:imklog 模块、muxsock 模块以及 rsyslogd 自己的 main 函数主进程。这三个进程组一定要运行在同一台机器上,否则它们之间的基于 Socket 的通信和文件的交换都会出现问题。
而上述的这个问题,如果出现在 Docker 中,就不得不使用三个不同的容器分别描述了,并且用户还得自己模拟处理它们三个之间的通信关系,这种复杂度可能比使用容器运维都高的多。并且对于这个问题的运维,Docker Swarm 也有自己本身的问题。以上述的例子为基础,如果三个模块分别都需要 1GB 的内存运行,如果 Docker Swarm 运行的集群中有两个 node,node-1 剩余 2.5GB ,node-2 剩余 3GB 。这种情况下分别使用 docker run 模块运行上述三个容器,基于 Swarm 的 affinity=main 约束,他们三个都必须要调度到同一台机器上,但是 Swarm 却很有可能先分配两个去 node-1,然后剩余的一个由于还剩 0.5GB 不满足调度而使这次调度失败。这种典型的成组调度( gang scheduling )没有被妥善处理的例子在 Docker Swarm 中经常存在。
基于上述的需求,Kubernetes 有了 Pod 这个概念来处理这种紧密关系。在一个 Pod 中的容器共享相同的 Cgroups 和 Namespace,因此它们之间并不存在边界和隔离环境,它们可以共享同一个网络 IP,使用相同的 Volume 处理数据等等。其中的原理就是在多个容器之间创建其共享资源的链接。但是为了解决到底是 A 共享 B,还是 B 共享 A,以及 A 和 B 谁先启动这种拓扑性的问题,一个 Pod 其实是由一个 Infra 容器联合 AB 两个容器共同组成的,其中 Infra 容器是第一个启动的:
Infra 容器是一个用汇编语言编写的、主进程是一个永远处于“暂停”状态的容器,其仅占用极少的资源,解压之后也仅有 100KB 左右。
介绍了一通,接下来我们在实例中为大家演示 Pod 长什么样子。
我们在任意一个装有 Kubernetes 的集群中通过以下的 yaml 文件和 shell 命令运行处一个 Pod,这个 YAML 文件具体是什么意思暂时不用理会,之后我会对这个 YAML 做一说明,我们目前只需要明白:Kubernetes 中的所有资源都可以通过以下这种 YAML 文件或者 json 文件描述的,现在我们只需要知道这是一个运行着 busybox 和 nginx 的 Pod 即可:
创建这个 hello-pod.yaml 文件之后运行以下命令:
通过上述命令,我们就成功创建了一个 pod,我们可以从执行结果看到 infra 容器的主进程成为了此 Pod 的 PID==1 的超级进程,说明了 Pod 是组合而成的:
至此,我们应该要理解 Pod 是 Kubernetes 的最小调度单位这个概念了,并且也应该把 Pod 作为一个整体而不是多个容器的集合来看待。 我们再看看描述这个 Pod 的文件类型 YAML 。
YAML 的语法定义:
YAML 是一种专门编写配置文件的语言,其简洁且强大,在描述配置文件方面远胜于 JSON,因此在很多新兴的项目比如 Kubernetes 和 Docker Compose 等都通过 YAML 来作为配置文件的描述语言。与 HTML 相同,YAML 也是一个英文的缩写:YAML Ain't Markup Language,聪明的同学已经看出来了,这是一个递归写法,突出了满满的程序员气息。其语法有如下特征:
- 大小写敏感
- 使用缩进表示层级关系,类似 Python
- 缩进不允许使用 Tab,只允许使用空格
- 缩进的空格数目不重要,只要相同层级的元素左侧对其即可
- 数组用短横线-表示
- NULL 用波浪线~表示
明确了以上概念,我们把 YAML 改写成一个 JSON,看看这之间的区别:
这两种写法在 Kubernetes 中是等效的,上述的 JSON 可以正常运行,但是 Kubernetes 还是更推荐使用 YAML 。从上面的对比中我们也能发现,在之前的使用中一直很好用的 JSON 现在也略显笨拙,需要些大量的字符串标志。
看完语法,我们再来说说上述 YAML 中的各个节点在 Kubernetes 所表示的意思。Kubernetes 中的有一种类似于 Java 语法万物皆对象的概念,所有内部的资源,包括服务器 node 、服务 service 以及运行组 Pod 在 kubernetes 中皆是以对象的形式存储的,其所有对象都由一下固定的部分组成:
- apiVersion:在官方文档中并没有给出相应的解释,但是从名字可以看出这是一个规定 API 版本的字段,但是此字段不能自定义,必须符合 Kubernetes 的官方约束,目前我们用到的基本都是 v1 稳定版
- kind:指明当前的配置是什么类型,比如 Pod 、Service 、Ingress 、Node 等,注意这个首字母是大写的
- metadata:用于描述当前配置的 meta 信息,比如 name,label 等
- spec:指明当前配置的具体实现
所有的 Kubernetes 对象基本都满足以上的格式,因此最开始 Pod 的 YAML 文件的意思是“使用 v1 稳定版本的 API 信息,类型是 Pod,名称是 hello-pod,具体实现是开启 ProcessNamespace,有两个容器。
知道了 YAML 的概念,让我们在回归主题。为了解决容器单一进程问题,只创建 Pod 的原因之一是 Google 通过 Pod 实现了自己的容器设计模式,而 Google 则为 Kubernetes 编写了最适合的容器设计模式。
举个最常用的例子:
Java 项目并不能像.Net Core 项目那样编译完成后直接自宿主运行,必须要把编译生成的 war 包拷贝到服务宿主程序比如 Tomcat 的运行目录下才可以正常使用。但是在实际情况中越大的公司分工越明确,很大概率负责 Java 项目开发和服务宿主程序开发的团队并不是同一团队。
为了让上述情况中的两个团队可以各自独立开发并且还可以紧密合作,我们可以使用 Pod 解决这个问题。
下面这个 yaml 文件就定义了一个满足上述需求的 Pod:
在这个 yaml 文件中,我们定义了一个 java 程序和 tomcat 程序的容器,并且对这两个容器之间的容器进行了一次挂载操作:将 java 程序的 /app 路径以及 tomcat 程序的 /root/apache-tomcat/webapps 同时挂载到了 sample-volume 这个挂载卷上,并且最后定了这个挂载卷就是一个内存数据卷。并且定义了 java 程序所在的容器是一个 initContainer,说明此容器是在 tomcat 容器之前启动的,并且启动之后执行了一个 cp 的命令。
在上述 Pod 描述了这样一个场景:程序运行开始运行时,Java 容器启动,把自己的 war 包 sample.war 拷贝到了自己的 /app 目录下;之后 tomcat 容器启动,执行启动脚本,执行的 war 包从自己的 /root/apache-tomcat/webapps 路径下获得。
可以看到通过上述的配置描述,我们既没有改动 Java 程序,也没有改动 tomcat 程序,却让它们完美的配合工作了,完成了解耦操作。这个例子就是容器设计模式中的 Sidecar 模式,还有很多设计模式,感兴趣的同学可以去进一步自行学习。
以上介绍的就是 Kubernetes 为了解决紧密关系而抽象出来的概念 Pod 的基础内容了,需要注意的是,Pod 提供的只是一种编排的思想,而不是具体的技术方案,在我们使用的 Kubernetes 框架中,Pod 只不过是以 Docker 作为载体实现了而已,如果你使用的底层容器是虚拟机,比如 virtlet,那这个 Pod 创建时就根本不需要 Infra Container,因为虚拟机天生就支持多进程协同。
在说完了 Pod 的基础的内容,在下一节中我们将会为大家介绍在接下来的容器编排战争之中,Kubernetes 又是如何脱颖而出。