进程通信
进程管理
进程组
如果说 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
- https://mp.weixin.qq.com/s/cRSDMnRsMcCdNmW-xmRobQK8s 提取最后的 pause 进程共享实验