原文链接:https://d2iq.com/blog/running-kind-inside-a-kubernetes-cluster-for-continuous-integration
导读
在D2iQ,我们进行了大量验证与测试。包括要进行创建企业级软件所需的大量测试,需要数十个价格低廉、兼容且易于每天启动数百次的集群。
KIND是一款可以解决大规模Kubernetes集成测试问题的优秀工具。它可以在一分钟内完成对Kubernetes集群创建(以Docker容器作为节点),即使用您的笔记本电脑上也一样,这极大地改善开发人员测试体验。D2iQ已经在多个内部项目中充分应用该技术。
我们许多项目都使用Dispatch(基于Tekton)作为CI工具,并且运行在一个生产Kubernetes集群中。当尝试在Kubernetes pod中运行KIND集群时,很多人会遇到障碍,难以完成。本文将分享运行KIND集群的经验。
在Pod中设置Docker守护进程
KIND(https://kind.sigs.k8s.io/)目前依赖于Docker(尽管它们计划很快将支持其它容器运行时,如podman)。因此,第一步是创建一个容器镜像,允许您在Pod内运行Docker daemon(Dokcer容器的守护进程),以便诸如‘docker run’之类的命令可以在Pod内运行(又名Docker-in-Docker或DIND)。
Docker-in-Docker原本有很多已经的坑,而且大多数问题都被各路大神解决到了。尽管这样,当我们尝试在生产Kubernetes集群中设置Docker-in-Docker时,仍然遇到了很多问题。
MTU(Maximum Transmission Unit)问题
MTU问题的本质实际上取决于生产Kubernetes集群的网络提供商。我们使用的Kubernetes发行版是Konvoy。Konvoy使用Calico作为默认的网络插件,并且默使用IPIP模式。IPIP封装会产生20字节的报头长度。换言之,如果集群中主机网络主网络接口的MTU为1500,则Pod中网络接口的MTU为1480。如果您的生产集群运行在某些云提供商(如GCE)上,则Pod的MTU甚至会更低(1460-20=1440)。
很重要的一点是我们要确保默认Docker网络MTU值(dockerd的--mtu值)等于或者小于我们在Pod内配置MTU。否则,系统将无法与外界建立链接(例如:无法从互联网下载容器镜像)。
PID 1 问题
我们需要在容器中运行Docker Daemon并构建一些复杂的的集成测试场景。在容器中运行多种服务的默认方法是使用systemd。但是,由于以下几个原因,它并不适用于我们的应用实例:
因此,我们在容器镜像中使用了以下启动脚本:
dockerd &
# Wait until dockerd is ready.
until docker ps >/dev/null 2>&1
do
echo "Waiting for dockerd..."
sleep 1
done
exec "$@"
然而,这里有一个需要注意的地方。不能简单地使用上面的脚本作为容器中的入口点。容器镜像中定义的入口点在单独的pid命名空间中作为PID 1在容器中运行。PID 1是内核中的一个特殊进程,其行为与其它进程不同。
其本质是,接收信号的进程是PID 1:内核对其进行了特殊处理;如果进程没有为信号注册处理程序,内核将不会执行默认行为(即终止进程),并且什么也不会发生。进程可能不会为SIGTERM注册信号处理程序,以为当接收到SIGTERM时,内核的默认行为将终止进程。如果发生这种情况,当Kubernetes试图终止Pod时,SIGTERM将被吞噬,您会发现Pod处于”终止”状态。
这不是一个新问题,但是令人惊讶的是,没有多少人知道这一点,反而继续构建有此问题的容器。而解决方案是使用tini(一个为容器构建的小型init程序)作为容器镜像的入口点,如Dockerfile中的以下示例。
ENTRYPOINT ["/usr/bin/tini", "--", "/entrypoint.sh"]
这一程序将正确注册信号处理程序并转发信号,此外还执行其它一些PID 1的操作,例如在容器中获取僵尸进程。
Cgroup挂载
Docker daemon操纵cgroup,因此需要将cgroup文件系统挂载在容器中。由于cgroup与主机共享,我们需要确保Docker daemon操作的cgroup不会影响其它容器或主机进程使用的其它cgroup。我们还需要确保在容器终止后,由Docker daemon在容器中创建的cgroup不会泄漏。
Docker daemon公开了一个标志——cgroup parent,它告诉守护进程将所有嵌套在指定cgroup下的容器cgroup放入其中。在Kubernetes集群中运行容器时,我们在容器中设置Docker daemon的--cgroup-parent标志,以便其所有cgroup都能正确嵌套在Kubernetes为容器创建的cgroup下。
从历史上看,为了使cgroup文件系统在容器中可用,一些用户将主机上的mount/sys/fs/cgroup绑定到容器中的同一位置(即,在Kubernetes中使用hostPath,类似于Docker中的-v/sys/fs/cgroup:/sys/fs/cgroup)。如果是这种情况,您需要在容器启动脚本中将--cgroup parent设置为以下内容,以便Docker daemon创建的cgroup能正确嵌套。
CGROUP_PARENT="$(grep systemd /proc/self/cgroup | cut -d: -f3)/docker"
(注意:/proc/self/cgroup显示调用进程的cgroup路径)
可以看出,绑定挂载主机/sys/fs/cgroup相当危险,因为它将整个主机cgroup层次结构公开给容器。为了在早期解决此问题,Docker使用了一个技巧来“隐藏”容器中不相关的cgroup。Docker为每个cgroup系统执行从容器的cgroup到cgroup层次结构的root的绑定挂载。
$ docker run --rm debian findmnt -lo source,target -t cgroup
SOURCE TARGET
cpuset[/docker/451b803b3cd7cd2b69dde64cd833fdd799ae16f9d2d942386ec382f6d55bffac] /sys/fs/cgroup/cpuset
cpu[/docker/451b803b3cd7cd2b69dde64cd833fdd799ae16f9d2d942386ec382f6d55bffac] /sys/fs/cgroup/cpu
cpuacct[/docker/451b803b3cd7cd2b69dde64cd833fdd799ae16f9d2d942386ec382f6d55bffac] /sys/fs/cgroup/cpuacct
blkio[/docker/451b803b3cd7cd2b69dde64cd833fdd799ae16f9d2d942386ec382f6d55bffac] /sys/fs/cgroup/blkio
memory[/docker/451b803b3cd7cd2b69dde64cd833fdd799ae16f9d2d942386ec382f6d55bffac] /sys/fs/cgroup/memory
cgroup[/docker/451b803b3cd7cd2b69dde64cd833fdd799ae16f9d2d942386ec382f6d55bffac] /sys/fs/cgroup/systemd
因此,cgroup通过将主机cgroup文件系统上的/sys/fs/cgroup/memory/memory.limit_in_bytes映射到/sys/fs/cgroup/memory/docker//memory.limit_in_bytes来控制容器内cgroup层级结构中root下的文件。此映射可防止容器进程意外修改主机cgroup。
然而,这个技巧有时会混淆像cadvisor和kubelet这样的程序。这是因为绑定挂载并不会更改/proc//cgroup中的内容。
$ docker run --rm debian cat /proc/1/cgroup
14:name=systemd:/docker/512f6b62e3963f85f5abc09b69c370d27ab1dc56549fa8afcbb86eec8663a141
5:memory:/docker/512f6b62e3963f85f5abc09b69c370d27ab1dc56549fa8afcbb86eec8663a141
4:blkio:/docker/512f6b62e3963f85f5abc09b69c370d27ab1dc56549fa8afcbb86eec8663a141
3:cpuacct:/docker/512f6b62e3963f85f5abc09b69c370d27ab1dc56549fa8afcbb86eec8663a141
2:cpu:/docker/512f6b62e3963f85f5abc09b69c370d27ab1dc56549fa8afcbb86eec8663a141
1:cpuset:/docker/512f6b62e3963f85f5abc09b69c370d27ab1dc56549fa8afcbb86eec8663a141
0::/
像cadvisor这样的程序将通过查看/proc//cgroup来获取给定进程的cgroup,并尝试从相应cgroup获取cpu/内存统计信息。但是,由于Docker守护进程完成了绑定挂载,cadvisor找不到容器进程的相应cgroup。为了解决这个问题,我们在容器内部进行了另一次从/sys/fs/cgroup/memory/到/sys/fs/cgroup/memory/docker//(适用于所有cgroup子系统)的绑定挂载。这在实践中非常有效。我们还将此解决方案升级为KIND,它有一个类似的问题。
解决此问题的现代方法是使用cgroup命名空间。Cgroup命名空间支持最近已添加到runc和docker中, 只要您是在一个不太旧的内核(Linux 4.6+)上运行。但是,在编写本文时,Kubernetes尚不支持cgroup命名空间,不过它很快就会成为cgroups v2支持的一部分。
IPtables
我们观察到,当在生产Kubernetes集群中运行时,容器内的docker daemon启动的嵌套容器有时无法访问internet。但是,它在开发人员的笔记本电脑上可以正常运行。
我们发现,当出现这个问题时,来自嵌套的Docker容器的数据包没有命中iptables中的POSTROUTING链,因此没有进行适当的伪装。
结果发现,问题在于包含Docker daemon的容器镜像是基于Debian buster的。默认情况下,Debian buster使用nftables作为iptables命令的默认后端。不过,Docker本身还不支持nftables。尽管nftable应该向后兼容iptables,但也存在一些可能导致问题的边缘情况,特别是在CentOS 7内核(3.10)上,而我们的生产Kubernetes集群中就是用该版本。
要解决此问题,只需在容器镜像中切换到旧版iptables命令。
RUN update-alternatives --set iptables /usr/sbin/iptables-legacy || true && \
update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy || true && \
update-alternatives --set arptables /usr/sbin/arptables-legacy || true
完整的Dockerfile和启动脚本可以在这里找到。您也可以使用用此容器镜像jieyu/dind-buster:v0.1.8 来启动。
docker run --rm --privileged jieyu/dind-buster:v0.1.8 docker run alpine wget google.com
可以在Kubernetes集群中使用相同的容器镜像。
apiVersion: v1
kind: Pod
metadata:
name: dind
spec:
containers:
- image: jieyu/dind-buster:v0.1.8
imagePullPolicy: Always
name: dind
stdin: true
tty: true
args:
- /bin/bash
volumeMounts:
- mountPath: /var/lib/docker
name: varlibdocker
securityContext:
privileged: true
volumes:
- name: varlibdocker
emptyDir: {}
在Pod中运行KIND
一旦我们成功设置了Docker-in-Docker(DinD),下一步就是在该容器中启动KIND集群。我们在笔记本电脑上尝试了以下操作,它可以完美运行!
$ docker run -ti --rm --privileged jieyu/dind-buster:v0.1.8 /bin/bash
Waiting for dockerd...
[root@257b543a91a5 /]# curl -Lso ./kind https://kind.sigs.k8s.io/dl/v0.7.0/kind-$(uname)-amd64
[root@257b543a91a5 /]# chmod +x ./kind
[root@257b543a91a5 /]# mv ./kind /usr/bin/
[root@257b543a91a5 /]# kind create cluster
Creating cluster "kind" ...
✓ Ensuring node image (kindest/node:v1.17.0) ?
✓ Preparing nodes ?
✓ Writing configuration ?
✓ Starting control-plane ?️
✓ Installing CNI ?
✓ Installing StorageClass ?
Set kubectl context to "kind-kind"
You can now use your cluster with:
kubectl cluster-info --context kind-kind
Have a nice day! ?
[root@257b543a91a5 /]# kubectl get nodes
NAME STATUS ROLES AGE VERSION
kind-control-plane Ready master 11m v1.17.0
然而,当我们尝试在CI(在生产Kubernetes集群中)中运行此命令时,却失败了。
$ kubectl apply -f dind.yaml
$ kubectl exec -ti dind /bin/bash
root@dind:/# curl -Lso ./kind https://kind.sigs.k8s.io/dl/v0.7.0/kind-$(uname)-amd64
root@dind:/# chmod +x ./kind
root@dind:/# mv ./kind /usr/bin/
root@dind:/# kind create cluster
Creating cluster "kind" ...
✓ Ensuring node image (kindest/node:v1.17.0) ?
✓ Preparing nodes ?
✓ Writing configuration ?
✗ Starting control-plane ?️
ERROR: failed to create cluster: failed to init node with kubeadm: command "docker exec --privileged kind-control-plane kubeadm init --ignore-preflight-errors=all --config=/kind/kubeadm.conf --skip-token-print --v=6" failed with error: exit status 137
我们发现,在KIND节点容器(嵌套)中运行的kubelet正在随机终止顶层容器中的进程。为什么会产生这样的结果?答案实际上与我们在前面同名章节讨论的cgroup挂载有关。
当顶层容器(Docker-in-Docker容器,又名DIND)在Kubernetes pod中运行时,对于每个cgroup子系统(例如内存),从主机的角度来看,它的cgroup路径是/kubepods/burstable// 。
当KIND在DIND容器内的嵌套节点容器中启动kubelet时,相比于嵌套的KIND节点容器的root cgroup,kubelet将在/kubepods/burstable/下操作其pod的cgroup。从主机的角度来看,cgroup路径是/kubepods/burstable///docker//kubepods/burstable/。
这完全正确,但是,您会注意到,在嵌套的KIND节点容器中,甚至在kubelet启动之前,相比于嵌套的KIND节点容器的root cgroup,在/kubepods/burstable///docker/ 下还存在另一个cgroup。这是由我们刚刚所讨论的通过KIND入口点脚本进行的设置的cgroups挂载解决方案(在前面的同名章节)引起的。如果您在KIND节点容器内执行cat /kubepods/burstable//docker//tasks ,您将看到DIND容器中的进程。
这就是罪魁祸首。KIND节点容器中的kubelet看到了这个cgroup,并认为它应该由它来进行管理。但它找不到与此cgroup相关联的Pod,并试图通过终止属于该cgroup的进程来删除cgroup,此操作导致随机进程被终止。解决这个问题的方法是通过设置kubelet标志--cgroup-root来指示KIND节点容器中的kubelet为其pod使用不同的cgroup root(例如/kubelet)。
之后,在我们的生产Kubernetes集群中启动一个KIND集群。您可以通过将以下yaml应用于Kubernetes集群来启动该修复程序。
apiVersion: v1
kind: Pod
metadata:
name: kind-cluster
spec:
containers:
- image: jieyu/kind-cluster-buster:v0.1.0
imagePullPolicy: Always
name: kind-cluster
stdin: true
tty: true
args:
- /bin/bash
env:
- name: API_SERVER_ADDRESS
valueFrom:
fieldRef:
fieldPath: status.podIP
volumeMounts:
- mountPath: /var/lib/docker
name: varlibdocker
- mountPath: /lib/modules
name: libmodules
readOnly: true
securityContext:
privileged: true
ports:
- containerPort: 30001
name: api-server-port
protocol: TCP
readinessProbe:
failureThreshold: 15
httpGet:
path: /healthz
port: api-server-port
scheme: HTTPS
initialDelaySeconds: 120
periodSeconds: 20
successThreshold: 1
timeoutSeconds: 1
volumes:
- name: varlibdocker
emptyDir: {}
- name: libmodules
hostPath:
path: /lib/modules
一旦pod准备就绪(需要一分钟),exec即可进入pod中进行验证。
$ kubectl exec -ti kind-cluster /bin/bash
root@kind-cluster:/# kubectl get nodes
NAME STATUS ROLES AGE VERSION
kind-control-plane Ready master 72s v1.17.0
您还可以使用Docker CLI直接启动容器:
$ docker run -ti --rm --privileged jieyu/kind-cluster-buster:v0.1.0 /bin/bash
Waiting for dockerd...
Setting up KIND cluster
Creating cluster "kind" ...
✓ Ensuring node image (jieyu/kind-node:v1.17.0) ?
✓ Preparing nodes ?
✓ Writing configuration ?
✓ Starting control-plane ?️
✓ Installing CNI ?
✓ Installing StorageClass ?
✓ Waiting ≤ 15m0s for control-plane = Ready ⏳
• Ready after 31s ?
Set kubectl context to "kind-kind"
You can now use your cluster with:
kubectl cluster-info --context kind-kind
Have a nice day! ?
root@d95fa1302557:/# kubectl get nodes
NAME STATUS ROLES AGE VERSION
kind-control-plane Ready master 71s v1.17.0
root@d95fa1302557:/#
您可以点击链接,查询完整的Dockerfile和启动脚本。
小 结
如您所见,在此过程中我们克服了许多障碍。其中大多数障碍是由于Docker容器未能提供与主机的完全隔离而引起的。有些内核资源(如cgroup等)在内核中共享,如果许多容器同时操作它们,则可能引起冲突。
一旦我们识别并克服了这些障碍,我们的解决方案在生产中就非常有效。但这只是个开始。我们还围绕KIND在内部构建了一些工具,以进一步提高可用性和效率,我们将在随后的文章中讨论这些工具: