超好用的K8s诊断工具:kubectl-debug
超好用的K8s诊断工具:kubectl-debug
超好用的K8s诊断工具:kubectl-debug
https://github.com/aylei/kubectl-debug/releases
背景
容器技术的一个最佳实践是构建尽可能精简的容器镜像。但这一实践却会给排查问题带来麻烦:精简后的容器中普遍缺失常用的排障工具,部分容器里甚至没有 shell (比如 FROM scratch )。 在这种状况下,我们只能通过日志或者到宿主机上通过 docker-cli 或 nsenter 来排查问题,效率很低。Kubernetes 社区也早就意识到了这个问题,在 16 年就有相关的 Issue Support for troubleshooting distroless containers 并形成了对应的 Proposal。 遗憾的是,由于改动的涉及面很广,相关的实现至今还没有合并到 Kubernetes 上游代码中。而在 一个偶然的机会下(PingCAP 一面要求实现一个 kubectl 插件实现类似的功能),我开发了 kubectl-debug: 通过启动一个安装了各种排障工具的容器,来帮助诊断目标容器 。
原理
kubectl-debug 本身非常简单,因此只要理解了它的工作原理,你就能完全掌握这个工具,并且还能用它做 debug 之外的事情。
我们知道,容器本质上是带有 cgroup 资源限制和 namespace 隔离的一组进程。因此,我们只要启动一个进程,并且让这个进程加入到目标容器的各种 namespace 中,这个进程就能 "进入容器内部"(注意引号),与容器中的进程"看到"相同的根文件系统、虚拟网卡、进程空间了——这也正是 docker exec 和 kubectl exec 等命令的运行方式。
现在的状况是,我们不仅要 "进入容器内部",还希望带一套工具集进去帮忙排查问题。那么,想要高效管理一套工具集,又要可以跨平台,最好的办法就是把工具本身都打包在一个容器镜像当中。 接下来,我们只需要通过这个"工具镜像"启动容器,再指定这个容器加入目标容器的的各种 namespace,自然就实现了 "携带一套工具集进入容器内部"。事实上,使用 docker-cli 就可以实现这个操作:
1export TARGET_ID=666666666
2# 加入目标容器的 network, pid 以及 ipc namespace
3docker run -it --network=container:$TARGET_ID --pid=container:$TARGET_ID --ipc=container:$TARGET_ID busybox
这就是 kubectl-debug 的出发点: 用工具容器来诊断业务容器 。背后的设计思路和 sidecar 等模式是一致的:每个容器只做一件事情。
具体到实现上,一条 kubectl debug <target-pod>
命令背后是这样的:
步骤分别是:
- 插件查询 ApiServer:demo-pod 是否存在,所在节点是什么
- ApiServer 返回 demo-pod 所在所在节点
- 插件请求在目标节点上创建 Debug Agent Pod
- Kubelet 创建 Debug Agent Pod
- 插件发现 Debug Agent 已经 Ready,发起 debug 请求(长连接)
- Debug Agent 收到 debug 请求,创建 Debug 容器并加入目标容器的各个 Namespace 中,创建完成后,与 Debug 容器的 tty 建立连接
接下来,客户端就可以开始通过 5,6 这两个连接开始 debug 操作。操作结束后,Debug Agent 清理 Debug 容器,插件清理 Debug Agent,一次 Debug 完成。效果如下图:
安装
在K8s环境部署应用后,经常遇到需要进入pod进行排错。除了查看pod logs和describe方式之外,传统的解决方式是在业务pod基础镜像中提前安装好procps、net-tools、tcpdump、vim等工具。但这样既不符合最小化镜像原则,又徒增Pod安全漏洞风险。
kubectl-debug是一个简单、易用、强大的 kubectl 插件, 能够帮助你便捷地进行 Kubernetes 上的 Pod 排障诊断。它通过启动一个排错工具容器,并将其加入到目标业务容器的pid, network, user 以及 ipc namespace 中,这时我们就可以在新容器中直接用 netstat, tcpdump 这些熟悉的工具来解决问题了, 而业务容器可以保持最小化, 不需要预装任何额外的排障工具。 kubectl-debug 包含两部分: kubectl-debug:命令行工具; debug-agent:部署在K8s的node上,用于启动关联排错工具容器;
k8s 1.12 支持kubectl插件(kubectl debug 命令,其实就是执行了kubectl-debug),之前使用kubectl-debug命令
1curl -Lo kubectl-debug.tar.gz https://github.com/aylei/kubectl-debug/releases/download/v0.1.1/kubectl-debug_0.1.1_linux_amd64.tar.gz
2
3tar -zxvf kubectl-debug.tar.gz kubectl-debug
4mv kubectl-debug /usr/local/bin/
5
6# 可选 安装 debug-agent DaemonSet
7kubectl-debug 包含两部分, 一部分是用户侧的 kubectl 插件, 另一部分是部署在所有 k8s 节点上的 agent(用于启动"新容器", 同时也作为 SPDY 连接的中继). 在 agentless 中, kubectl-debug 会在 debug 开始时创建 debug-agent Pod, 并在结束后自动清理.(默认开启agentless模式)
8
9agentless 虽然方便, 但会让 debug 的启动速度显著下降, 你可以通过预先安装 debug-agent 的 DaemonSet 并配合 --agentless=false 参数来使用 agent 模式, 加快启动速度:
10
11# 如果你的kubernetes版本为v1.16或更高
12kubectl apply -f https://raw.githubusercontent.com/aylei/kubectl-debug/master/scripts/agent_daemonset.yml
13# 如果你使用的是旧版本的kubernetes(<v1.16), 你需要先将apiVersion修改为extensions/v1beta1, 可以如下操作
14wget https://raw.githubusercontent.com/aylei/kubectl-debug/master/scripts/agent_daemonset.yml
15sed -i '' '1s/apps\/v1/extensions\/v1beta1/g' agent_daemonset.yml
16kubectl apply -f agent_daemonset.yml
17# 或者使用helm安装
18helm install kubectl-debug -n=debug-agent ./contrib/helm/kubectl-debug
19# 使用daemonset agent模式(关闭agentless模式)
20kubectl debug --agentless=false POD_NAME
21
22
23
24# 简单使用:
25
26# kubectl 1.12.0 或更高的版本, 可以直接使用:
27kubectl debug -h
28# 假如安装了 debug-agent 的 daemonset, 可以使用 --agentless=false 来加快启动速度
29# 之后的命令里会使用默认的agentless模式
30kubectl debug POD_NAME
31
32# 假如 Pod 处于 CrashLookBackoff 状态无法连接, 可以复制一个完全相同的 Pod 来进行诊断
33kubectl debug POD_NAME --fork
34
35# 当使用fork mode时,如果需要复制出来的pod保留原pod的labels,可以使用 --fork-pod-retain-labels 参数进行设置(注意逗号分隔,且不允许空格)
36# 示例如下
37# 若不设置,该参数默认为空(既不保留原pod的任何labels,fork出来的新pod的labels为空)
38kubectl debug POD_NAME --fork --fork-pod-retain-labels=<labelKeyA>,<labelKeyB>,<labelKeyC>
39
40# 为了使 没有公网 IP 或无法直接访问(防火墙等原因)的 NODE 能够访问, 默认开启 port-forward 模式
41# 如果不需要开启port-forward模式, 可以使用 --port-forward=false 来关闭
42kubectl debug POD_NAME --port-forward=false --agentless=false --daemonset-ns=kube-system --daemonset-name=debug-agent
43
44# 老版本的 kubectl 无法自动发现插件, 需要直接调用 binary
45kubectl-debug POD_NAME
46
47# 使用私有仓库镜像,并设置私有仓库使用的kubernetes secret
48# secret data原文请设置为 {Username: <username>, Password: <password>}
49# 默认secret_name为kubectl-debug-registry-secret,默认namspace为default
50kubectl-debug POD_NAME --image calmkart/netshoot:latest --registry-secret-name <k8s_secret_name> --registry-secret-namespace <namespace>
51
52# 在默认的agentless模式中,你可以设置agent pod的resource资源限制,如下示例
53# 若不设置,默认为空
54kubectl-debug POD_NAME --agent-pod-cpu-requests=250m --agent-pod-cpu-limits=500m --agent-pod-memory-requests=200Mi --agent-pod-memory-limits=500Mi
55
56
57
58
59kubectl debug podname
60
61ps -ef # 查看进程
62
63netstat
64
65logout #相当于会把相应的这个 debug pod 杀掉,然后进行退出,此时对应用实际上是没有任何的影响的
66
67...
1apiVersion: apps/v1
2kind: DaemonSet
3metadata:
4 labels:
5 app: debug-agent
6 name: debug-agent
7spec:
8 selector:
9 matchLabels:
10 app: debug-agent
11 template:
12 metadata:
13 labels:
14 app: debug-agent
15 spec:
16 hostPID: true
17 tolerations:
18 - key: node-role.kubernetes.io/master
19 effect: NoSchedule
20 containers:
21 - name: debug-agent
22 image: aylei/debug-agent:latest
23 imagePullPolicy: Always
24 securityContext:
25 privileged: true
26 livenessProbe:
27 failureThreshold: 3
28 httpGet:
29 path: /healthz
30 port: 10027
31 scheme: HTTP
32 initialDelaySeconds: 10
33 periodSeconds: 10
34 successThreshold: 1
35 timeoutSeconds: 1
36 ports:
37 - containerPort: 10027
38 hostPort: 10027
39 name: http
40 protocol: TCP
41 volumeMounts:
42 - name: cgroup
43 mountPath: /sys/fs/cgroup
44 - name: lxcfs
45 mountPath: /var/lib/lxc/lxcfs
46 mountPropagation: Bidirectional
47 - name: docker
48 mountPath: "/var/run/docker.sock"
49 # hostNetwork: true
50 volumes:
51 - name: cgroup
52 hostPath:
53 path: /sys/fs/cgroup
54 - name: lxcfs
55 hostPath:
56 path: /var/lib/lxc/lxcfs
57 type: DirectoryOrCreate
58 - name: docker
59 hostPath:
60 path: /var/run/docker.sock
61 updateStrategy:
62 rollingUpdate:
63 maxUnavailable: 5
64 type: RollingUpdate
典型案例
kubectl debug 默认使用 nicolaka/netshoot 作为默认的基础镜像,里面内置了相当多的排障工具,包括:
使用 iftop 查看容器网络流量:
1➜ ~ kubectl debug demo-pod
2
3root @ /
4 [2] 🐳 → iftop -i eth0
5interface: eth0
6IP address is: 10.233.111.78
7MAC address is: 86:c3:ae:9d:46:2b
使用 drill 诊断 DNS 解析:
1root @ /
2 [3] 🐳 → drill -V 5 demo-service
3;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 0
4;; flags: rd ; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0
5;; QUESTION SECTION:
6;; demo-service. IN A
7
8;; ANSWER SECTION:
9
10;; AUTHORITY SECTION:
11
12;; ADDITIONAL SECTION:
13
14;; Query time: 0 msec
15;; WHEN: Sat Jun 1 05:05:39 2019
16;; MSG SIZE rcvd: 0
17;; ->>HEADER<<- opcode: QUERY, rcode: NXDOMAIN, id: 62711
18;; flags: qr rd ra ; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 0
19;; QUESTION SECTION:
20;; demo-service. IN A
21
22;; ANSWER SECTION:
23
24;; AUTHORITY SECTION:
25. 30 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2019053101 1800 900 604800 86400
26
27;; ADDITIONAL SECTION:
28
29;; Query time: 58 msec
30;; SERVER: 10.233.0.10
31;; WHEN: Sat Jun 1 05:05:39 2019
32;; MSG SIZE rcvd: 121
使用 tcpdump 抓包:
1root @ /
2 [4] 🐳 → tcpdump -i eth0 -c 1 -Xvv
3tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
412:41:49.707470 IP (tos 0x0, ttl 64, id 55201, offset 0, flags [DF], proto TCP (6), length 80)
5 demo-pod.default.svc.cluster.local.35054 > 10-233-111-117.demo-service.default.svc.cluster.local.8080: Flags [P.], cksum 0xf4d7 (incorrect -> 0x9307), seq 1374029960:1374029988, ack 1354056341, win 1424, options [nop,nop,TS val 2871874271 ecr 2871873473], length 28
6 0x0000: 4500 0050 d7a1 4000 4006 6e71 0ae9 6f4e E..P..@.@.nq..oN
7 0x0010: 0ae9 6f75 88ee 094b 51e6 0888 50b5 4295 ..ou...KQ...P.B.
8 0x0020: 8018 0590 f4d7 0000 0101 080a ab2d 52df .............-R.
9 0x0030: ab2d 4fc1 0000 1300 0000 0000 0100 0000 .-O.............
10 0x0040: 000e 0a0a 08a1 86b2 ebe2 ced1 f85c 1001 .............\..
111 packet captured
1211 packets received by filter
130 packets dropped by kernel
访问目标容器的根文件系统:
容器技术(如 Docker)利用了 /proc 文件系统提供的 /proc/{pid}/root/ 目录实现了为隔离后的容器进程提供单独的根文件系统(root filesystem)的能力(就是 chroot 一下)。当我们想要访问 目标容器的根文件系统时,可以直接访问这个目录:
1root @ /
2 [5] 🐳 → tail -f /proc/1/root/log_
3Hello, world!
这里有一个常见的问题是 free top 等依赖 /proc 文件系统的命令会展示宿主机的信息,这也是容器化过程中开发者需要适应的一点(当然了,各种 runtime 也要去适应,比如臭名昭著的 Java 8u121 以及更早的版本不识别 cgroups 限制 问题就属此列)。
诊断 CrashLoopBackoff
排查 CrashLoopBackoff 是一个很麻烦的问题,Pod 可能会不断重启, kubectl exec 和 kubectl debug 都没法稳定进行排查问题,基本上只能寄希望于 Pod 的日志中打印出了有用的信息。 为了让针对 CrashLoopBackoff 的排查更方便, kubectl-debug 参考 oc debug 命令,添加了一个 --fork 参数。当指定 --fork 时,插件会复制当前的 Pod Spec,做一些小修改, 再创建一个新 Pod:
- 新 Pod 的所有 Labels 会被删掉,避免 Service 将流量导到 fork 出的 Pod 上
- 新 Pod 的 ReadinessProbe 和 LivnessProbe 也会被移除,避免 kubelet 杀死 Pod
- 新 Pod 中目标容器(待排障的容器)的启动命令会被改写,避免新 Pod 继续 Crash
接下来,我们就可以在新 Pod 中尝试复现旧 Pod 中导致 Crash 的问题。为了保证操作的一致性,可以先 chroot 到目标容器的根文件系统中:
1➜ ~ kubectl debug demo-pod --fork
2
3root @ /
4 [4] 🐳 → chroot /proc/1/root
5
6root @ /
7 [#] 🐳 → ls
8 bin entrypoint.sh home lib64 mnt root sbin sys tmp var
9 dev etc lib media proc run srv usr
10
11root @ /
12 [#] 🐳 → ./entrypoint.sh
13 # 观察执行启动脚本时的信息并根据信息进一步排障
kubectl get pod --v=8 加上--v=8 可以查看详细