4.6 多容器Pod
容器设计模式中的单节点多容器模型中,初始化容器和Sidecar容器是目前使用较多的模式,尤其是服务网格的发展极大促进了Sidecar容器的应用。
4.6.1 初始化容器
初始化是很多编程语言普遍关注的问题,甚至有些编程语言直接支持模式构造来生成初始化程序,这些用于进行初始化的程序结构称为初始化器或初始化列表。初始化代码要首先运行,且只能运行一次,它们常用于验证前提条件、基于默认值或传入的参数初始化对象实例的字段等。Pod中的初始化容器(Init Container)功能与此类似,它们为那些有先决条件的应用容器完成必要的初始设置,例如设置特殊权限、生成必要的iptables规则、设置数据库模式,以及获取最新的必要数据等。
有很多场景都需要在应用容器启动之前进行部分初始化操作,例如等待其他关联组件服务可用、基于环境变量或配置模板为应用程序生成配置文件、从配置中心获取配置等。初始化容器的典型应用需求有如下几种。
▪用于运行需要管理权限的工具程序,例如iptables命令等,出于安全等方面的原因,应用容器不适合拥有运行这类程序的权限。
▪提供主容器镜像中不具备的工具程序或自定义代码。
▪为容器镜像的构建和部署人员提供了分离、独立工作的途径,部署人员使用专用的初始化容器完成特殊的部署逻辑,从而使得他们不必协同起来制作单个镜像文件。
▪初始化容器和应用容器处于不同的文件系统视图中,因此可分别安全地使用敏感数据,例如Secrets资源等。
▪初始化容器要先于应用容器串行启动并运行完成,因此可用于延后应用容器的启动直至其依赖的条件得以满足。
Pod对象中的所有初始化容器必须按定义的顺序串行运行,直到它们全部成功结束才能启动应用容器,因而初始化容器通常很小,以便它们能够以轻量的方式快速运行。某初始化容器运行失败将会导致整个Pod重新启动(重启策略为Never时例外),初始化容器也必将再次运行,因此需要确保所有初始化容器的操作具有幂等性,以避免无法预知的副作用。
Pod资源的spec.initContainers字段以列表形式定义可用的初始化容器,其嵌套可用字段类似于spec.containers。下面的资源清单(init-container-demo.yaml)在Pod对象上定义了一个名为iptables-init的初始化容器示例。
apiVersion: v1 kind: Pod metadata: name: init-container-demo namespace: default spec: initContainers: # 定义初始化容器 - name: iptables-init image: ikubernetes/admin-box:latest imagePullPolicy: IfNotPresent command: ['/bin/sh','-c'] args: ['iptables -t nat -A PREROUTING -p tcp --dport 8080 -j REDIRECT --to-port 80'] securityContext: capabilities: add: - NET_ADMIN containers: - name: demo image: ikubernetes/demoapp:v1.0 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 80
示例中,应用容器demo默认监听TCP协议的80端口,但我们又期望该Pod能够在TCP协议的8080端口通过端口重定向方式为客户端提供服务,因此需要在其网络名称空间中添加一条相应的iptables规则。但是,添加该规则的iptables命令依赖于内核中的网络管理权限,出于安全原因,我们并不期望应用容器拥有该权限,因而使用了拥有网络管理权限的初始化容器来完成此功能。下面先把配置清单中定义的资源创建于集群之上:
~$ kubectl apply -f init-container-demo.yaml pod/init-container-demo created
随后,在Pod对象init-container-demo的描述信息中的初始化容器信息段可以看到如下内容,它表明初始化容器启动后大约1秒内执行完成返回0状态码并成功退出。
Command: /bin/sh -c Args: iptables -t nat -A PREROUTING -p tcp --dport 8080 -j REDIRECT --to-port 80 State: Terminated Reason: Completed Exit Code: 0 Started: Sun, 30 Aug 2020 11:44:28 +0800 Finished: Sun, 30 Aug 2020 11:44:29 +0800 Ready: True Restart Count: 0
这表明,向Pod网络名称空间中添加iptables规则的操作已经完成,我们可通过应用容器来请求查看这些规则,但因缺少网络管理权限,该查看请求会被拒绝:
~$ kubectl exec init-container-demo -- iptables -t nat -vnL
iptables v1.8.3 (legacy): can't initialize iptables table `nat': Permission denied (you must be root)
Perhaps iptables or your kernel needs to be upgraded.
command terminated with exit code 3
另一方面,应用容器中的服务却可以正常通过Pod IP的8080端口接收并响应,如下面的命令及执行结果所示:
~$ podIP=$(kubectl get pods/init-container-demo -o jsonpath={.status.podIP}) ~$ curl http://${podIP}:8080 iKubernetes demoapp v1.0 !! ClientIP: 10.244.0.0, ServerName: init-container-demo, …
由此可见,初始化容器及容器的postStop钩子都能完成特定的初始化操作,但postStop必须在应用容器内部完成,它依赖的条件(例如管理权限)也必须为应用容器所有,这无疑会为应用容器引入安全等方面的风险。另外,考虑到应用容器镜像内部未必存在执行初始化操作的命令或程序库,使用初始化容器也就成了不二之选。
4.6.2 Sidecar容器
Sidecar容器是Pod中与主容器松散耦合的实用程序容器,遵循容器设计模式,并以单独容器进程运行,负责运行应用的非核心功能,以扩展、增强主容器。Sidecar模式最著名的用例是充当服务网格中的微服务的代理应用(例如Istio中的数据控制平面Envoy),其他典型使用场景包括日志传送器、监视代理和数据加载器等。
下面的配置清单(sidecar-container-demo.yaml)中定义了两个容器:一个是运行demoapp的主容器demo,一个运行envoy代理的Sidecar容器proxy。
apiVersion: v1 kind: Pod metadata: name: sidecar-container-demo namespace: default spec: containers: - name: proxy image: envoyproxy/envoy-alpine:v1.13.1 command: ['/bin/sh','-c'] args: ['sleep 3 && envoy -c /etc/envoy/envoy.yaml'] lifecycle: postStart: exec: command: ['/bin/sh','-c','wget -O /etc/envoy/envoy.yaml https:// raw.githubusercontent.com/iKubernetes/Kubernetes_Advanced_Practical_2rd/ master/chapter4/envoy.yaml'] - name: demo image: ikubernetes/demoapp:v1.0 imagePullPolicy: IfNotPresent env: - name: HOST value: "127.0.0.1" - name: PORT value: "8080"
Envoy程序是服务网格领域著名的数据平面实现,它在Istio服务网格中以Sidecar的模式同每一个微服务应用程序单独组成一个Pod,负责代理该微服务应用的所有通信事件,并为其提供限流、熔断、超时、重试等多种高级功能。这里我们将demoapp视作一个微服务应用,配置Envoy为其代理并调度入站(Ingress)流量,因而在示例中demo容器基于环境变量被配置为监听127.0.0.1地址上一个特定的8080端口,而proxy容器将监听Pod所有IP地址上的80端口,以接收客户端请求。proxy容器上的postStart事件用于为Envoy代理下载一个适用的配置文件,以便将proxy接收到的所有请求均代理至demo容器。
下面说明整个测试过程。先将配置清单中定义的对象创建到集群之上。
~$ kubectl apply -f sidecar-container-demo.yaml pod/sidecar-container-demo created
随后,等待Pod中的两个容器成功启动且都转为就绪状态,可通过各Pod内端口监听的状态来确认服务已然正常运行。下面命令的结果表示,Envoy已经正常运行并监听了TCP协议的80端口和9901端口(Envoy的内置管理接口)。
$ kubectl exec sidecar-container-demo -c proxy -- netstat -tnlp 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:9901 0.0.0.0:* LISTEN 1/envoy tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1/envoy tcp 0 0 127.0.0.1:8080 0.0.0.0:* LISTEN -
接下来,我们向Pod的80端口发起HTTP请求,若它能以demoapp的页面响应,则表示代理已然成功运行,甚至可以根据响应头部来判断其是否有代理服务Envoy发来的代理响应,如下面的命令及结果所示。
~$ podIP=$(kubectl get pods/sidecar-container-demo -o jsonpath={.status.podIP}) $ curl http://$podIP iKubernetes demoapp v1.0 !! ClientIP: 127.0.0.1, ServerName: sidecar-container-demo, …… ~$ curl -I http://$podIP HTTP/1.1 200 OK content-type: text/html; charset=utf-8 content-length: 108 server: envoy date: Sun, 22 May 2020 06:43:04 GMT x-envoy-upstream-service-time: 3
虽然Sidecar容器可以称得上是Pod中的常规容器,但直到v1.18版本,Kubernetes才将其添加作为内置功能。在此之前,Pod中的各应用程序彼此间没有区别,用户无从预测和控制容器的启动及关闭顺序,但多数场景都要求Sidecar容器必须要先于普通应用容器启动以做一些准备工作,例如分发证书、创建存储卷或获取一些数据等,且它们需要晚于其他应用容器终止。Kubernetes从v1.18版本开始支持用户在生命周期字段中将容器标记为Sidecar,这类容器全部转为就绪状态后,普通应用容器方可启动。因而,这个新特性根据生命周期将Pod的容器重新划分成了初始化容器、Sidecar容器和应用容器3类。
所有的Sidecar容器都是应用容器,唯一不同之处是,需要手动为Sidecar容器在lifecycle字段中嵌套定义type类型的值为Sidecar。配置格式如下所示:
spec: containers: - name: proxy image: envoyproxy/envoy-alpine:v1.13.1 lifecycle: type: Sidecar …… - name: demo image: ikubernetes/demoapp:v1.0 ……
另外,可能也有一些场景需要Sidecar容器启动晚于普通应用容器,这种特殊的应用需求,目前可通过OpernKruise项目中的SidecarSet提供的PostSidecar模型来解决。将来,该项目或许支持以DAG的方式来灵活编排容器的启动顺序。