Kubernetes

1. 定义

Kubernetes脱胎于Google内部的大规模集群管理工具Borg,并且Kubernetes项目早期的主要贡献者正是参与过Borg项目的工程师,所以业内也基本认同Kubernetes的很多概念和架构“代表了Google过去十余年设计、构建和管理大规模容器集群的经验”。

Kubernetes还是一个管理跨主机容器化应用的系统,实现了包括应用部署、高可用管理和弹性伸缩在内的一系列基础功能并封装成为一套完整、简单易用的RESTful API对外提供服务。Kubernetes的设计哲学之一就是维护应用容器集群一直处于用户所期望的状态。为了践行这一价值观,Kubernetes建立了一套健壮的集群自恢复机制,包括容器的自动重启、自动重调度以及自动备份等。所以从这一层面上来看,Kubernetes与Fleet或者Mesos都相差无几。

相比Flynn、Deis这样的Docker容器云,Kubernetes对外提供容器服务的模式则更偏向于Mesos的方式,即用户提交容器集群运行所需资源的申请(通常是一个配置文件),然后由Kubernetes负责完成这些容器的调度任务,即自动为这些容器选择运行的宿主机。对于应用的架构以及开发运维人员对整个软件环境的控制力,Kubernetes并不做其他的限制。

用户在创建容器时,会给每个容器指定一个用来分组的标签(label):比如所有PHP网站容器都属于name=frontend这个组。在调度的时候,Kubernetes就可以根据这些标签来进行一些决策。比如,从高可用角度出发,用户可以指定所有name=frontend标签的容器不会被调度在同一台node上。这些容器可以直接使用IP:Port的方式进行通信,但是由于重新调度或者重启之后Docker容器的IP会发生变化,所以Kubernetes内置了一个名为service的代理组件。当为某些容器(同样可以使用label来指定)分配了一个service代理后,这些容器就可以统一使用一个固定IP(cluster IP)被访问到,这就为用户省去了自己搭建负载均衡的麻烦,当然,如有需要,也可以使用自己的负载均衡组件来代替这些service。最后也是最重要的,上述replication controller、label和service,真正操作的对象都是一个称为pod的逻辑对象,而非容器,这是Kubernetes与其他编排调度平台最大的区别。

pod可以想象成一个篮子,而容器则是篮子里的鸡蛋,当Kubernetes需要调度容器时,它直接把一个篮子(连同里面的鸡蛋)从一个宿主机调度到另一个宿主机,而不是一个一个地搬运里面的鸡蛋。篮子和鸡蛋的关系主要表现为以下几点。

  • 一个pod里的容器能有多少资源也取决于这个篮子的大小。
  • label也是贴在篮子上的。
  • IP分配给篮子而不是容器,篮子里面的所有容器共享这个IP。
  • 哪怕只有一个鸡蛋(容器), Kubernetes仍然会给它分配一个篮子。在Kubernetes中,pod、replication controller和service统称为“对象”或者“资源”,并通过APIServer组件提供了一套对它们按照RESTful格式的增、删、改、查和监听接口。Kubernetes支持多个API版本,如/api/v1和/apis/extensions/v1beta1,对应的资源对象分别定义在/pkg/api/{apiversion}/types.go和/pkg/apis/extensions/{apiversion}/types.go中。

2. pod设计解读

在Kubernetes中,能够被创建、调度和管理的最小单元是pod,而非单个容器。前面已经说过,一个pod是由若干个Docker容器构成的容器组(pod意为豆荚,里面容纳了多个豆子,很形象)。这里需要强调的是,pod里的容器共享network namespace,并通过volume机制共享一部分存储。

  • pod是IP等网络资源的分配的基本单位,这个IP及其对应的networknamespace是由pod里的容器共享的;
  • pod内的所有容器也共享volume。当有一个volume被挂载在同属一个pod的多个Docker容器的文件系统上时,该volume可以被这些容器共享。

另外,从Linux namespace的角度看,同属一个pod的容器还共享以下namespace。

  • IPC namespace,即同一个pod内的应用容器能够使用System V IPC或POSIX消息队列进行通信。
  • UTS namespace,即同一个pod内的应用容器共享主机名。

这样做的好处是什么呢?要知道,使用Fleet这样的编排调度工具就已经能够保证若干个容器始终运行在同一个宿主机上了。仔细思考一下,可以发现同一个pod里的容器有如下两个特性:

  • 通过Kubernetes volume机制,在容器之间共享存储;
  • 可以通过localhost直接访问另一个容器。

这是一个怎么样的场景呢?如果运行起来的是一个网站系统,似乎并不需要这些特性,因为没必要读取超过网站内容外的文件,况且如需连接其他容器(比如MySQL),直接采用IP访问不也能实现吗?

但试想一下,有一个应用叫“云控制器”,它不是一个简单的网站,而是由3个独立的进程组成的。其中进程A是一个负责响应和处理用户请求的Web服务器;进程B是一个负责转发A产生日志的定时任务;进程C负责监控A和B的PID存活情况并发送定时心跳给监控组件。

在传统的部署方法里,该服务的3个进程一定是被部署在同一个机器里面,这个机器也会被命名为“云控制器服务节点”。可是采用容器方案之后呢?

如果把这3个进程分别部署在3个容器里,处理它们之间的联系就会非常麻烦。至少需要做到以下几条:B需要同A建立一个共享的volume(很可能还得通过中间容器), C需要知道A和B的PID(PID的值和位置有可能还会变),还必须小心翼翼地维护这些脆弱的容器关系(怎么也得记在小本上吧)。任何重新调度、重启或者更新某个容器的操作,都有可能破坏既定的容器间联系,更不用提多副本、水平扩展这种高难度的复杂操作了。修改B的代码和配置自然也是避免不了的,这时维护起来的难度就相当大。再回想Docker在广告里宣称的“应用无缝迁移”和“操作系统无关”,恐怕也只能苦笑了。

如果把3个进程分别部署在3个容器里会存在这样那样的问题,那么把这些进程封装在同一个容器镜像中,是不是所有的问题都迎刃而解了呢?不见得。由于Docker容器是严格的单进程模型,我们不得不在所有的容器里内置一个进程管理软件(比如supervisord、m/monit),接着会惊奇地发现,进程启动命令全变了。以m/monit为例,进程A的启动命令从./startup.sh变成了/usr/bin/monit start tomcat,而且还需要为进程A编写供m/monit使用的进程描述文件。说好的“原生支持,无缝迁移”呢?更遗憾的是,当我们把负责不同职责的进程合并在同一个容器中时,就失去了微服务的优势,比如我们已经无法做到每个模块单独开发、部署和运维。任何一个进程的代码如果要更新,都得更新整个容器镜像,无法实现增量部署,滚动更新。此外,现在这些进程都在一起运行了,一旦哪个进程有内存泄露之类的问题,整个容器都有被拖垮掉的风险。

如果仅仅需要发布一个小网站,以上种种问题可能并没有造成太大的麻烦。但实际情况是,绝大多数企业级应用和服务正是上面描述的复杂应用。如果只需支持一个“规范”的十二要素应用(12-Factor application),经典PaaS早已完成了大部分工作。

pod正是为了解决“如何合理使用容器支撑企业级复杂应用”这个问题而诞生的。这也是Kubernetes明显区别于其他各类容器编排调度工具的显著特性。Kubernetes也是目前唯一一个没有大谈特谈所谓“轻应用”和“十二要素应用”概念的一个Docker容器编排系统,毕竟它的设计理念就是要支持绝大多数应用的原生形态。我们可以看到它的官方示例中有很多演示如何发布诸如HBase、Hazelcast、Cassandra等分布式服务的,我们团队的很多实际经验也是在如何使用Kubernetes和Docker发布Cloud Foundry这样的分布式系统上。从这一点上来讲,Kubernetes的设计确实让大多数竞争者望尘莫及。

前面也讲到,当系统运行着数量庞大的pod时,用户或者系统管理员如何有效地定位与组织这些pod就成了一个重要问题。Kubernetes的解决方案是label。每个pod都有一个属性”labels”——一组键/值对,形如:

image-20220403212617804

通过label,可以在Kubernetes集群管理工具kubectl中方便地实现pod等资源对象的定位和组织,只要传入-l key=value参数即可,例如列举所有匹配标签{“name”: “nginx”}的pod可以这么操作:

image-20220403212627870

label的用途并非仅限于此,Kubernetes中的其他对象,如replicationcontroller和service,同样可以通过label对pod进行定位和组织。事实上,在Kubernetes中,label是一种重要的且被广泛应用的组织、分类和选择Kubernetes对象的机制,因此下文将详细介绍Kubernetes label和labelselector的工作原理。