1、简介
容器经常是为了解决单一的,窄范围的问题,比如说微服务.然而现实中,一些复杂问题的完成往往需要多个容器.这里我们讨论一下如何把多个容器放在同一个pod里以及容器间的通信并进行目录共享。
2、什么是pod
pod是kubernetes里的一个基本概念,可能我们从一开始接触kubernetes的时候就开始接触pod,并被灌输pod是kubernetes里最小的不可分割的工作单元,这里再从多容器的角度对其进行一些基本阐释。
简言之,pod是kubernetes可以部署和管理的最小单元,换言之也就是说如果你想要运行一个容器,你先要为这个容器创建一个pod.同时,一个pod也可以包含多个容器,之所以多个容器包含在一个pod里,前面讲到过的初始容器为普通应用容器准备环境是一种使用场景,往往由于业务上的紧密耦合,一组容器也需要放在同一个pod里。
需要注意的是,以上场景都非必须把不同的容器放在同一个pod里,但是这样往往更便于管理,甚至后面会讲到的,紧密耦合的业务容器放置在同一个容器里通信效率更高.具体怎么使用还要看实际情况,综合权衡。
3、kubernetes为什么使用pod作为最小单元,而不是container
直接部署一个容器看起来更简单,但是这里也有更好的原因为什么在容器基础上抽象一层.容器是一个存在的实体,并指向一个具体的事物.这个具体的事物可能是一个docker容器,但也可能是一个rtk容器,或者一个Virtlet管理的虚拟机.它们有不同的要求。
更深层的原因是为了管理容器,kubernetes需要更多的信息,比如重启策略,它定义了容器终止后要采取的策略;或者是一个可用性探针,从应用程序的角度去探测是否一个进程还存活着。
基于这些原因,kubernetes架构师决定使用一个新的实体,也就是pod,而不是重载容器的信息添加更多属性,用来在逻辑上包装一个或者多个容器的管理所需要的信息。
4、kubernetes为什么允许一个pod里有多个容器
pod里的容器运行在一个逻辑上的"主机"上,它们使用相同的网络名称空间(也就是说,同一pod里的容器使用相同的ip和相同的端口段区间)和相同的IPC名称空间.它们也可以共享存储卷.这些特性使它们可以更有效的通信.并且pod可以使你把紧密耦合的应用容器作为一个单元来管理。
因此一个应用如果需要多个运行在同一主机上的容器时,为什么为把它们放在同一个容器里呢?首先,这样何故违反了一个容器只负责一个应用的原则.这点非常重要,如果我们把多个应用放在同一个容器里,这将使解决问题变得非常麻烦因为它们的日志记录混合在了一起,并且它们的生命周期也很难管理.因此一个应用使用多个容器将更简单,更透明,并且使应用依赖解偶.并且粒度更小的容器更便于不同的开发团队共享和复用。
这里说到为了解偶把应用分别放在不同容器里,前面我们也强调为了便于管理耦合的应用把它们的容器放在同一个pod里.一个强调耦合,一个强调解偶看似矛盾,实际上普遍存在,高内聚低耦合是我们的追求,然而一个应用的业务逻辑模块不可能完全完独立不存在耦合,这就需要我们从实际上来考量,做出决策。
5、多容器pod的使用场景举例
Sidecar containers "帮助"主容器,比如日志文件监视器.一个日志监视器构建完成以后,可以由不同的应用来使用.另一个示例是sidecar 容器为主容器加载文件和运行需要的数据。
代理,桥接和适配器 使主容器与外部世界联通.比如Apache http服务器或者nginx可承载静态文件.也可以做为一个web的反向代理服务器。
你可以使用一个pod来承载一个多层应用(比如wordpress),但是更建议使用不同的pod来承载不同的层,因这这样你可以为每一个层单独扩容并且把它们分布到集群的不同节点上。
6、通过共享卷通信
6.1 通过共享卷通信(事例1)
你可以使用一个共享的存储卷来简单高效的地在容器间共享数据.大多数情况下,使用一个共享目录在同一pod里的不同容器间共享数据就够了。
一个标准的同一pod内容器共享存储卷的用例是一个容器往共享存储卷里写入数据,其它的则从共享目录里读取数据。
[root@yygh-de ~]# vim test.yaml
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:
-whiletrue; do
date >> /html/index.html;
sleep1;
done
------------------------------------------------------------------------------------------------------------
这个示例里我们定义了一个存储卷叫作html,它是emptyDir类型的.当一个pod在一个节点上创建的时候,它就被分配,只要pod一直运行在这个节点上它就一直存在(emptyDir生命周期和pod相同).就像它的名字所暗示的一样,它是一个空的目录,第一个容器运行了一个nginx server并且把它挂载到/usr/share/nginx/html,第二个容器使用了一个Debian镜像并把emptyDir挂载到/html.每一秒钟,第二个容器就会把当前日期写入到index.html里,它位于共享存储卷里.当用户发起一个http请求,nginx就会读取它并响应给用户.
通过把nginx的端口暴露出去然后通过浏览器查看或者查看共享目录里的文件来检测以上是否有效果
[root@yygh-de opt]# kubectl exec mc1 -c 1st -- /bin/cat /usr/share/nginx/html/index.html
Tue Jun 1609:25:28 UTC 2020
Tue Jun 1609:25:29 UTC 2020
Tue Jun 1609:25:30 UTC 2020
Tue Jun 1609:25:31 UTC 2020
Tue Jun 1609:25:32 UTC 2020
Tue Jun 1609:25:33 UTC 2020
Tue Jun 1609:25:34 UTC 2020
Tue Jun 1609:25:35 UTC 2020
[root@yygh-de opt]# kubectl exec mc1 -c 2nd -- /bin/cat /html/index.html
Tue Jun 1609:25:28 UTC 2020
Tue Jun 1609:25:29 UTC 2020
Tue Jun 1609:25:30 UTC 2020
Tue Jun 1609:25:31 UTC 2020
Tue Jun 1609:25:32 UTC 2020
Tue Jun 1609:25:33 UTC 2020
6.2通过共享卷通信(事例2)
[root@yygh-de huqi]# vim pod-mutil.yaml
apiVersion: v1
kind: Pod
metadata:
annotations:
hello: world
labels:
app: nginx
name: nginx-pod-mutil
namespace: k8s-master
spec:
volumes:
-emptyDir: {}
name: ourfile
containers:
-image: nginx
imagePullPolicy: Always
name: nginx
ports:
-containerPort: 80
name: http80
protocol: TCP
volumeMounts:
-mountPath: /usr/share/nginx/html
name: ourfile
-image: alpine
imagePullPolicy: Always
name: alpine
command: ["/bin/sh","-c"]
args:
-whiletrue; do
date > /root/index.html;
sleep1;
done
volumeMounts:
-name: ourfile
mountPath: /root
[root@yygh-de huqi]# kubectl appply -f pod-mutil.yaml
[root@yygh-de huqi]# kubectl get pod -n k8s-master
NAME READY STATUS RESTARTS AGE
nginx-pod-mutil 2/2 Running 0 6h24m
[root@yygh-de huqi]# kubectl exec nginx-pod-mutil -n k8s-master -c nginx -- /bin/cat /usr/share/nginx/html/index.html
Tue Jun 1614:51:23 UTC 2020
[root@yygh-de huqi]# kubectl exec nginx-pod-mutil -n k8s-master -c alpine -- /bin/cat /root/index.html
Tue Jun 1614:43:30 UTC 2020
7、进程间通信(IPC)
同一个pod里的容器共享IPC名称空间,这就意味着他们可以通过进程间通信的手段来进行通信,比如使用SystemV semaphores或者POSIX共享内存。
以下示例中,我们定义一个包含了两个容器的pod.它们使用相同的docker镜像,第一个容器是一个生产者,创建一个标准的linux消息队列,写一些随机的消息,然后写一个特殊的退出消息.第二个容器是一个消费者,打开同一个消息队列来读取数据直到读到退出消息,我们把重启策略设置为Never,这样当两个pod都中止的时候pod就会停止。
[root@yygh-de huqi]# vim test2.yaml
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。它们使用同样的镜像。第一个容器是生产者(producer),它会创建一个标准的Linux消息队列,并向该队列中写入一些随机字符串,最后写入一个特定的退出字符。第二个容器是消费者(consumer),它打开同一个队列,读取字符,直到读到特殊的退出字符为止。我们将Pod的重启策略设置为“Never”,因此在两个容器都终止后Pod会停止。
Pod 运行后,查看每个容器的日志,确认2nd容器收到了1st容器的全部消息,包括特定的退出消息:
[root@yygh-de huqi]# kubectl apply -f test2.yaml
[root@yygh-de huqi]# kubectl get pods --show-all -w
NAME READY STATUS RESTARTS AGE
mc2 0/2 Pending 00s
mc2 0/2 ContainerCreating 00s
mc2 0/2 Completed 029
默认情况下,Pod中的所有容器都是并行启动的,因为没有办法去指定一个容器在另一个容器启动后才启动。比如,在IPC例子中,有可能第二个容器在第一个容器启动完成并创建消息队列前就启动完毕了。此时,第二个容器会失败,因此它需要消息队列在其启动时就已经存在了。
有一些方法去控制容器的启动顺序,比如 Kubernetes Init Containers(初始化容器),初始化容器会被首先启动。但是在云原生环境中,最好能为所有不可控的失败都做好准备。比如,要修复上述问题,最好的办法是修改应用去等待,直到消息队列被创建出来为止。
8、容器间的网络通信
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;
}
server {
listen 80;
location / {
proxy_pass http://webapp;
proxy_redirect off;
}
}
}
(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端口。
(3)步骤3:将Pod暴露为一个 NodePort服务
[root@yygh-de huqi]# kubectl expose pod mc3 --type=NodePort --port=80
service/mc3 exposed
(4)步骤4:确认服务
[root@yygh-de huqi]# kubectl describe svc mc3
Name: mc3
Namespace: default
Labels: app=mc3
Annotations: <none>
Selector: app=mc3
Type: NodePort
IP: 10.96.0.231
Port: <unset> 80/TCP
TargetPort: 80/TCP
NodePort: <unset> 31561/TCP
Endpoints: 10.244.66.84:80
Session Affinity: None
External Traffic Policy: Cluster
Events: <none>
(5)现在,就可以使用浏览器或者curl工具来访问这个web应用了
[root@yygh-de huqi]# curl 10.1.94.173:31561
Hello world!
nginx容器的80端口上收到的HTTP请求会被转发到web应用容器的5000端口。
上面的例子只展示了在Pod中一个容器去访问其它容器,实际上,更常见的是Pod中的多个容器会在不同的端口上监听,所有这些端口都会被暴露出去。要实现这种形式,要么你创建一个暴露多个端口的服务,要么为每个要被暴露的端口创建一个服务。
9、 小结
通过创建Pod概念,Kubernetes为编排容器的行为以及容器间互相通信都提供了极大的便利。容器们可以共享存储卷,以及可以通过网络甚至IPC互相通信。