Kubernetes进阶实战(第2版)
上QQ阅读APP看书,第一时间看更新

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的方式来灵活编排容器的启动顺序。