进程管理
进程组
如果说K8s就是操作系统的话,那么不妨看一下真实的操作系统的例子。例子里面有一个程序叫做Helloworld,这个Helloworld程序实际上是由一组进程组成的,需要注意一下,这里说的进程实际上等同于Linux中的线程。因为Linux中的线程是轻量级进程,所以如果从Linux系统中去查看Helloworld中的pstree,将会看到这个Helloworld实际上是由四个线程组成的,分别是{api、main、log、compute}。也就是说,四个这样的线程共同协作,共享Helloworld程序的资源,组成了Helloworld程序的真实工作情况。这是操作系统里面进程组或者线程组中一个非常真实的例子,以上就是进程组的一个概念。
在K8s里面,Pod实际上正是K8s项目为你抽象出来的一个可以类比为进程组的概念。由四个进程共同组成的一个应用Helloworld,在K8s里面,实际上会被定义为一个拥有四个容器的Pod。就是说现在有四个职责不同、相互协作的进程,需要放在容器里去运行,在K8s里面并不会把它们放到一个容器里,因为这里会遇到两个问题。那么在K8s里会怎么去做呢?它会把四个独立的进程分别用四个独立的容器启动起来,然后把它们定义在一个Pod里面。
所以当K8s把Helloworld给拉起来的时候,你实际上会看到四个容器,它们共享了某些资源,这些资源都属于Pod,所以我们说Pod在K8s里面只有一个逻辑单位,没有一个真实的东西对应说这个就是Pod,不会有的。真正起来在物理上存在的东西,就是四个容器,这四个容器,或者说是多个容器的组合就叫做Pod。并且还有一个概念一定要非常明确,Pod是K8s分配资源的一个单位,因为里面的容器要共享某些资源,所以Pod也是K8s的原子调度单位。
原子调度
假如现在有两个容器,它们是紧密协作的,所以它们应该被部署在一个Pod里面。具体来说,第一个容器叫做App,就是业务容器,它会写日志文件;第二个容器叫做LogCollector,它会把刚刚App容器写的日志文件转发到后端的ElasticSearch中。
两个容器的资源需求是这样的:App容器需要1G内存,LogCollector需要0.5G内存,而当前集群环境的可用内存是这样一个情况:Node_A:1.25G内存,Node_B:2G内存。
假如说现在没有Pod概念,就只有两个容器,这两个容器要紧密协作、运行在一台机器上。可是,如果调度器先把App调度到了Node_A上面,接下来会怎么样呢?这时你会发现:LogCollector实际上是没办法调度到Node_A上的,因为资源不够。其实此时整个应用本身就已经出问题了,调度已经失败了,必须去重新调度。
以上就是一个非常典型的成组调度失败的例子。英文叫做:Task co-scheduling问题,这个问题不是说不能解,在很多项目里面,这样的问题都有解法。
比如说在Mesos里面,它会做一个事情,叫做资源囤积(resource hoarding):即当所有设置了Affinity约束的任务都达到时,才开始统一调度,这是一个非常典型的成组调度的解法。
所以上面提到的“App”和“LogCollector”这两个容器,在Mesos里面,他们不会说立刻调度,而是等两个容器都提交完成,才开始统一调度。这样也会带来新的问题,首先调度效率会损失,因为需要等待。由于需要等,还会有外一个情况会出现,就是产生死锁,即互相等待的一个情况。这些机制在Mesos里都是需要解决的,也带来了额外的复杂度。
另一种解法是Google的解法。它在Omega系统(就是Borg下一代)里面,做了一个非常复杂且非常厉害的解法,叫做乐观调度。比如说:不管这些冲突的异常情况,先调度,同时设置一个非常精妙的回滚机制,这样经过冲突后,通过回滚来解决问题。这个方式相对来说要更加优雅,也更加高效,但是它的实现机制是非常复杂的。这个有很多人也能理解,就是悲观锁的设置一定比乐观锁要简单。
而像这样的一个Task co-scheduling问题,在K8s里,就直接通过Pod这样一个概念去解决了。因为在K8s里,这样的一个App容器和LogCollector容器一定是属于一个Pod的,它们在调度时必然是以一个Pod为单位进行调度,所以这个问题是根本不存在的。
超亲密关系
比如说现在有两个Pod,它们需要运行在同一台宿主机上,那这样就属于亲密关系,调度器一定是可以帮助去做的。但是对于超亲密关系来说,有一个问题,即它必须通过Pod来解决。因为如果超亲密关系赋予不了,那么整个Pod或者说是整个应用都无法启动。
什么叫做超亲密关系呢?大概分为以下几类:
-
比如说两个进程之间会发生文件交换,前面提到的例子就是这样,一个写日志,一个读日志;
-
两个进程之间需要通过localhost或者说是本地的Socket去进行通信,这种本地通信也是超亲密关系;
-
这两个容器或者是微服务之间,需要发生非常频繁的RPC调用,出于性能的考虑,也希望它们是超亲密关系;
-
两个容器或者是应用,它们需要共享某些Linux Namespace。最简单常见的一个例子,就是我有一个容器需要加入另一个容器的Network Namespace。这样我就能看到另一个容器的网络设备,和它的网络信息。
像以上几种关系都属于超亲密关系,它们都是在K8s中会通过Pod的概念去解决的。它解决了两个问题:我们怎么去描述超亲密关系;我们怎么去对超亲密关系的容器或者说是业务去做统一调度,这是Pod最主要的一个诉求。
IPC
Pod中的容器共享相同的IPC名称空间,它们还可以使用标准的进程间通信(例如SystemV信号量或POSIX共享内存)相互通信。在下面的示例中,我们定义了具有两个容器的Pod。我们两者都使用相同的Docker映像。第一个容器(生产者)创建一个标准的Linux消息队列,写入许多随机消息,然后写入特殊的退出消息。第二个容器(消费者)打开相同的消息队列进行读取,并读取消息,直到接收到退出消息为止。我们还将重启策略设置为“从不”,因此Pod在两个容器终止后停止。
apiVersion: v1
kind: Pod
metadata:
name: mc2
spec:
containers:
- name: 1st
image: allingeek/ch6_ipc
command: ["./ipc", "-producer"]
- name: 2nd
image: allingeek/ch6_ipc
command: ["./ipc", "-consumer"]
restartPolicy: Never
然后使用kubectl create创建Pod并查看状态:
$ kubectl get pods --show-all -w
NAME READY STATUS RESTARTS AGE
mc2 0/2 Pending 0 0s
mc2 0/2 ContainerCreating 0 0s
mc2 0/2 Completed 0 29s
然后我们可以查看通信日志:
$ kubectl logs mc2 -c 1st
...
Produced: f4
Produced: 1d
Produced: 9e
Produced: 27
$ kubectl logs mc2 -c 2nd
...
Consumed: f4
Consumed: 1d
Consumed: 9e
Consumed: 27
Consumed: done
Links