Kubernetes中的多容器Pod和Pod内容器间通信
容器(Container)常被用来解决比如微服务的单个问题,但在实际场景中,问题的解决往往需要多容器方案。本文会讨论将多个容器整合进单个Kubernetes Pod 中,以及Pod中的容器之间是如何通信的。
1. 关于Kubernetes Pod
1.1 Kubernetes Pod 是什么?
首先我们来探讨下什么是Pod。Pod是Kubernetes中最小的可部署和管理单元。换句话讲,如果需要在Kubernetes中运行单个容器,那么你就得为这个容器创建一个Pod。同时,一个Pod可以包含多个容器,这些容器往往是紧耦合的。怎么样个紧耦合法呢?试着想象这么一个场景,一个Pod中的多个容器代表需要运行在同一个服务器上的多个进程。这种类比是合理的,因为在许多方面,Pod就类似于一台服务器。比如,通过localhost每个容器可以访问它所在Pod中的其它容器。
1.2 为什么Kubernetes将Pod而不是单个容器作为最小可部署单元呢?
尽管直接部署单个容器也许会更容易,但增加Pod这个新的抽象层会带来新的好处。容器是一个真实存在的实体,它代表一个具体的东西。这个“东西”可以是一个Docker容器,也可以是一个rkt容器。每种“东西”都有不同的用途。为了管理容器,Kubernetes需要更多的信息,比如重启策略(restart policy),它定义了当容器终止了时怎样重启容器;还有活性检测(liveness probe),它定义了如何从应用视角去检测容器中的进程是否活着,比如Web服务器进程是否能响应HTTP请求。
为了避免在容器这个已有的实体上增加这些新的属性,Kubernetes架构师们决定使用一个新的实体,那就是Pod。它逻辑地包含一个或多个容器。
1.3 为什么Kubernetes允许Pod中存在一个或多个容器?
Pod中的容器们运行在一个逻辑“主机”上。他们使用同一个网络命名空间(network namespace,换句话讲,就是同样的IP地址和端口空间),以及同样的IPC(inter-process communication,进程间通信)命名空间,他们还使用共享卷(shared volume)。这些特征使得Pod内的容器能互相高效地通信。同时,Pod使得你可以将多个紧耦合的应用容器当做一个实体来管理。
那么,如果一个应用需要在同一台服务器上运行多个容器,为什么不把所有东西放在一个容器里面呢?好吧,首先,这会违反“一个容器一个进程”规范。这个规范很重要,因为当一个容器中有多个进程时,调试会变得非常困难,因为不同进程的日志会混在一起,而且很难去管理这些进程的生命周期。其次,为一个应用使用多个容器会更简单、更直接、能解耦软件依赖。而且,更细粒度的容器可以在团队间复用。
1.4 多容器Pod的用例
多容器Pod的主要目的是为了支持同时存在的(co-located)及同时被管理的(co-managed)帮助进程(helper process)。帮助进程有几种通用场景:
边车容器(sidecarcontainer):比如日志或数据变化监视器等。一个团队创建日志监视器(log watcher)后,它可以被各种应用使用。另一个边车容器的例子是文件或数据加载器,它负责为主容器产生数据。
代理(Proxy)、桥(bridge)和适配器(adapter):它们将主容器连接到外部世界。比如,Apache HTTP 服务器或nginx 会读取静态文件。它们还能被用作主容器中的web应用的反向代理(reverseproxy)。
当你在Pod中运行多层应用(比如WordPress)时,推荐的方式是为每层使用单独的Pod。最简单的理由是这样你就可以独立地扩展每层,并将他们分布在不同节点上。
2. Pod 中容器间的通信
在Pod中运行多个容器,使得它们之间的通信非常直接。他们自己的通信有几种方法。
2.1 通过共享卷通信
在Kubernetes中,Pod中的容器可以将共享卷当做一种简单和高效的共享数据方式。在大多数场景中,使用主机上的一个目录,并在多个容器间共享,是一种高效的方式。
Kubernetes volume(卷)使得在容器重启后数据能被保存下来。卷具有和Pod一样的生命周期。这意味着,只要Pod存在,卷就存在。如果Pod被删除了,即使一模一样的Pod被创建出来,原来Pod的共享卷也会被销毁,一个新的共享卷会被创建出来。
Pod中的多个容器使用共享卷的一个标准用例是,当一个容器向共享目录写入日志或其它文件时,其它容器从共享目录中读取数据。比如我们创建一个下面的Pod:
- apiVersion: v1
- kind: Pod
- metadata:
- name: mc1
- spec:
- volumes:
- -name: html
- emptyDir: {}
- containers:
- -name: 1st
- image: nginx
- volumeMounts:
- -name: html
- mountPath: /usr/share/nginx/html
- -name: 2nd
- image: debian
- volumeMounts:
- -name: html
- mountPath: /html
- command: ["/bin/sh", "-c"]
- args:
- - while true; do
- date >> /html/index.html;
- sleep 1;
- done
本例中,定义了一个名为html的卷。它的类型是 emptyDir,这意味着当Pod被分配到一个节点上时,卷会被基于一个空目录创建出来,只要该Pod一直运行着,这个卷就会一直存在。1st容器运行nginx服务器,它将共享卷挂载到/usr/share/nginx/html 目录。2nd容器使用Debian镜像,它将共享卷挂载到 /html目录。每秒钟,2nd容器会将当前日期和时间写入到共享卷之中的index.html文件。当用户向Pod发送HTTP请求时,Nginx读取这个文件的内容并返回给用户。
你可以通过暴露nginx端口或使用浏览器访问它来检查该Pod,或者直接查看容器额共享目录:
- $ kubectl exec mc1 -c 1st -- /bin/cat/usr/share/nginx/html/index.html
- ...
- FriAug 25 18:36:06 UTC 2019
- $kubectl exec mc1 -c 2nd -- /bin/cat /html/index.html
- ...
- FriAug 25 18:36:06 UTC 2019
- FriAug 25 18:36:07 UTC 2019
2.2 进程间通信(Inter-processCommunication,IPC)
Pod中的容器共享同一个IPC命名空间,这意味着它们可以使用标准的进程间通信方式来互相通信,比如SystemV信号量和POSIX共享内存。
在下面的例子中,我们会定义一个包含两个容器的Pod。它们使用同样的镜像。第一个容器是生产者(producer),它会创建一个标准的Linux消息队列,并向该队列中写入一些随机字符串,最后写入一个特定的退出字符。第二个容器是消费者(consumer),它打开同一个队列,读取字符,直到读到特殊的退出字符为止。我们将Pod的重启策略设置为“Never”,因此在两个容器都终止后Pod会停止。
- apiVersion: v1
- kind: Pod
- metadata:
- name: mc2
- spec:
- containers:
- -name: producer
- image: allingeek/ch6_ipc
- command: ["./ipc", "-producer"]
- -name: consumer
- image: allingeek/ch6_ipc
- command: ["./ipc", "-consumer"]
- restartPolicy: Never
Pod 运行后,查看每个容器的日志,确认2nd容器收到了1st容器的全部消息,包括特定的退出消息:
- $ kubectl logs mc2 -c producer
- ...
- Produced: f4
- Produced: 1d
- Produced: 9e
- Produced: 27
- $ kubectl logs mc2 -c consumer
- ...
- Consumed: f4
- Consumed: 1d
- Consumed: 9e
- Consumed: 27
- Consumed: done
默认情况下,Pod中的所有容器都是并行启动的,因为没有办法去指定一个容器在另一个容器启动后才启动。比如,在IPC例子中,有可能第二个容器在第一个容器启动完成并创建消息队列前就启动完毕了。此时,第二个容器会失败,因此它需要消息队列在其启动时就已经存在了。
有一些方法去控制容器的启动顺序,比如 Kubernetes Init Containers(初始化容器),初始化容器会被首先启动。但是在云原生环境中,最好能为所有不可控的失败都做好准备。比如,要修复上述问题,最好的办法是修改应用去等待,直到消息队列被创建出来为止。
2.3 容器间的网络通信
Pod中的容器可以通过“localhost”来互相通信,因为他们使用同一个网络命名空间。而且,对容器来说,hostname就是Pod的名称。因为Pod中的所有容器共享同一个IP地址和端口空间,你需要为每个需要接收连接的容器分配不同的端口。也就是说,Pod中的应用需要自己协调端口的使用。
在下面的例子中,我们会创建一个多容器Pod,其中一个容器中运行Nginx,它作为另一个容器中运行的web应用的反向代理。
(1)步骤1,为nginx配置文件创建一个ConfigMap。从80端口进来的HTTP请求会被转发到localhost上的5000端口。
- apiVersion: v1
- kind: ConfigMap
- metadata:
- name: mc3-nginx-conf
- data:
- nginx.conf: |-
- user nginx;
- worker_processes 1;
- error_log /var/log/nginx/error.log warn;
- pid /var/run/nginx.pid;
- events {
- worker_connections 1024;
- }
- http {
- include /etc/nginx/mime.types;
- default_type application/octet-stream;
- sendfile on;
- keepalive_timeout 65;
- upstream webapp {
- server 127.0.0.1:5000;
- }
(2)步骤2:创建一个两容器Pod,一个容器运行nginx,另一个容器运行简单的web应用。注意我们只为Pod定义了80端口。端口5000不能被从Pod外部访问到。
- apiVersion: v1
- kind: Pod
- metadata:
- name: mc3
- labels:
- app: mc3
- spec:
- containers:
- -name: webapp
- image: training/webapp
- -name: nginx
- image: nginx:alpine
- ports:
- -containerPort: 80
- volumeMounts:
- -name: nginx-proxy-config
- mountPath: /etc/nginx/nginx.conf
- subPath: nginx.conf
- volumes:
- -name: nginx-proxy-config
- configMap:
- name: mc3-nginx-conf
查看pod中的端口空间,能看到有80 和 5000端口。
- # netstat -lntp
- Active Internet connections (only servers)
- Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
- tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN -
- tcp 0 0 0.0.0.0:5000 0.0.0.0:* LISTEN 1/python
(3)步骤3:将Pod暴露为一个 NodePort服务
- $ kubectl expose pod mc3 --type=NodePort--port=80
- service "mc3" exposed
(4)步骤4:确认服务
- [root@master1 ~]# kubectl describe svc mc3
- Name: mc3
- Namespace: testproject
- Labels: app=mc3
- Annotations: <none>
- Selector: app=mc3
- Type: NodePort
- IP: 172.30.34.232
- Port: <unset> 80/TCP
- TargetPort: 80/TCP
- NodePort: <unset> 32728/TCP
- Endpoints: 10.130.0.190:80
- Session Affinity: None
- External Traffic Policy: Cluster
- Events: <none>
现在,就可以使用浏览器或者curl工具来访问这个web应用了。
- [root@master1 ~]# curl 10.70.209.68:32728
- Hello world!
nginx容器的80端口上收到的HTTP请求会被转发到web应用容器的5000端口。
上面的例子只展示了在Pod中一个容器去访问其它容器,实际上,更常见的是Pod中的多个容器会在不同的端口上监听,所有这些端口都会被暴露出去。要实现这种形式,要么你创建一个暴露多个端口的服务,要么为每个要被暴露的端口创建一个服务。
3. 小结
通过创建Pod概念,Kubernetes为编排容器的行为以及容器间互相通信都提供了极大的便利。容器们可以共享存储卷,以及可以通过网络甚至IPC互相通信。