kubectl debug
kubectl debug 是什么
kubectl debug 是一款 k8s pod 诊断工具,能够帮助进行 pod 的排障诊断。在 1.16 ~ 1.22 中为 Alpha,默认关闭,从 1.23 开始为 Beta,默认开启。feature gates == EphemeralContainers
kubectl debug 起源
部分开发者喜欢在生产中使用极致精简的容器镜像,这也是容器技术中的一个最佳实践。这种精简主义也有很多好处,而且在大多数情况下运行良好,可一旦需要在生产中排除一些故障时,这就变得很困难了,因为精简后的容器普遍缺失常用的排障工具,有些甚至没有 bash/sh 解释器。于是就有人在 Kubernetes 社区提出如果有一种方法可以为正在运行的 pod 启用某种调试模式,再附加一套调试工具能在容器中执行,那就最好不过了。但由于改动的涉及面很广,从 16 年就出现了相关的 Issue Support for troubleshooting distroless containers 开始,直至今日 debug 功能才逐渐成熟。
kubectl debug 工作原理
我们知道,容器本质上是带有 cgroup 资源限制和 namespace 隔离的一组进程。因此,我们只要启动一个进程,并且让这个进程加入到目标容器的各种 namespace 中,这个进程就能 “进入容器内部”(注意引号),与容器中的进程 “看到” 相同的根文件系统、虚拟网卡、进程空间了——这也正是 docker exec 和 kubectl exec 等命令的运行方式。
现在的状况是,我们不仅要 “进入容器内部”,还希望带一套工具集进去帮忙排查问题。那么,想要高效管理一套工具集,又要可以跨平台,最好的办法就是把工具本身都打包在一个容器镜像当中。 接下来,我们只需要通过这个 “工具镜像” 启动容器,再指定这个容器加入目标容器的的各种 namespace,自然就实现了“携带一套工具集进入容器内部”。
kubectl debug 怎么用
1,开启功能
1) 根据集群版本判断是否需要手动开启此功能(我的环境为 1.21 所以是需要手动开启的):
a. 进入 master 节点,编辑 /etc/kubernetes/manifests/ 下的 kube-apiserver.yaml,kube-controller-manager.yaml 及 kube-scheduler.yaml,在 command 部分添加 - --feature-gates=EphemeralContainers=true;
b. !!!同时也要在被调试 pod 所在的节点上编辑 /var/lib/kubelet/kubeadm-flags.env,添加 --feature-gates=EphemeralContainers=true;
or
$ cat /etc/sysconfig/kubelet
KUBELET_EXTRA_ARGS="--feature-gates=EphemeralContainers=true"
c. 重启 kubelet:
$ systemctl daemon-reload
$ systemctl restart kubelet
2,使用
2.1 使用临时容器调试
$ kubectl debug -it pod_name --image=busybox:1.28 --target=container_name
// 为 pod 里的具体某个容器添加一个临时容器(镜像为 busybox)进行 debug
实例一:创建一个 pod,该 pod 功能是从桶 mybucket1 获取文件 test.txt 复制到本地目录 /data/test.txt 再从此目录把 test.txt 上传到桶 mybucket2 里。错误状态:桶 mybucket2 并没有相应文件,查找原因。
1,创建 pod1
[centos@ml-k8s-1 test1]$ kubectl apply -f pod1.yaml
secret/pod1-secret created
clusterrole.rbac.authorization.k8s.io/pod1-get created
clusterrolebinding.rbac.authorization.k8s.io/pod1-get-rbac created
serviceaccount/pod1-sa created
pod/pod1 created
[centos@ml-k8s-1 test1]$ kubectl get pod
NAME READY STATUS RESTARTS AGE
pod1 1/1 Running 0 7s
// pod1 一直处于 Running ,正常情况是 10s 内就会 completed ,只有失败的时候才会卡在 Running 不动
// 此时桶 mybucket2 确实也没有数据
2,描述 pod 当前状态
[centos@ml-k8s-1 deploy]$ kubectl describe pod pod1
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 5m44s default-scheduler Successfully assigned default/pod1 to ml-k8s-2.novalocal
Normal Pulling 5m42s kubelet Pulling image "beyond.io:5000/debug-test:0.1.1"
Normal Pulled 5m42s kubelet Successfully pulled image "beyond.io:5000/debug-test:0.1.1" in 59.162221ms
Normal Created 5m42s kubelet Created container pod1
Normal Started 5m42s kubelet Started container pod1
// 看不出任何问题
3,查看 pod 日志
[centos@ml-k8s-1 test1]$ kubectl logs pod1
I0429 10:24:34.913853 1 main.go:18] Test start!
I0429 10:24:34.914013 1 main.go:19] Pulling data from bucket 1 and storing it in bucket 2.
I0429 10:24:34.961361 1 main.go:61] &{0xc000442180}
E0429 10:24:37.968336 1 main.go:70] Wrong in coping object from mybucket1 to localFile: Get "http://10.20.9.60:30009/mybucket1/?location=": dial tcp 10.20.9.60:30009: connect: no route to host
// 程序已启动但是从桶 mybucket1 复制文件到本地时失败
// 看到这个日志大概能猜到网址不对劲,但假设不确定是不是网址问题或没有这条日志
4,进入容器内部排查
[centos@ml-k8s-1 test1]$ kubectl exec -it pod1 -- sh
OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: exec: "sh": executable file not found in $PATH: unknown
command terminated with exit code 126
// 此镜像的基础镜像为 scratch,无法执行 kubectl exec
5,debug 进入容器内部
[centos@ml-k8s-1 test1]$ kubectl debug -it pod1 --image=busybox:1.28 --target=pod1
Defaulting debug container name to debugger-h59bb.
If you don't see a command prompt, try pressing enter.
/ # ls
bin dev etc home proc root sys tmp usr var
/ # cd proc/1/root
(unreachable)/ # ls
app data dev etc proc sys var
(unreachable)/ # cd data
(unreachable)/data # ls
test.txt
(unreachable)/data # cat test.txt
(unreachable)/data # ping 10.20.9.60
PING 10.20.9.60 (10.20.9.60): 56 data bytes
^C
--- 10.20.9.60 ping statistics ---
9 packets transmitted, 0 packets received, 100% packet loss
(unreachable)/data # ping 10.20.9.61
PING 10.20.9.61 (10.20.9.61): 56 data bytes
64 bytes from 10.20.9.61: seq=0 ttl=63 time=13.941 ms
64 bytes from 10.20.9.61: seq=1 ttl=63 time=1.050 ms
64 bytes from 10.20.9.61: seq=2 ttl=63 time=0.462 ms
64 bytes from 10.20.9.61: seq=3 ttl=63 time=0.472 ms
^C
--- 10.20.9.61 ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max = 0.462/3.981/13.941 ms
(unreachable)/data #
// 进入 pod1 容器内部,查看 /data 目录下是否有成功复制文件 test.txt,发现 test.txt 为空,复制未成功
// 测试 minio 网站是否能链接上,确实不行。发现网址输入错误
// 修改好正确网址再次部署 pod1,运行成功,桶 mybucket2 里出现 test.txt
2.2 使用副本调试
2.2.1,创建副本调试
$ kubectl debug myapp -it --image=ubuntu --share-processes --copy-to=myapp-debug
// --share-processes 允许在此 Pod 中的其他容器中查看该容器的进程
// 示例一中的方式关注点是无法进入容器内部,所以通过一个临时容器进入被调试容器内部排查问题
// 此方式重点是原镜像调试工具有限,不能满足调试要求,所以注入一个调试工具更多的镜像来工作,同时创建一个副本不影响原有的服务。
实例二:针对实例一中的网址问题,也可以采用创建副本的方式来注入原镜像不包含的调试工具进行排错
1,进入容器
[centos@ml-k8s-1 test1]$ kubectl debug pod1 -it --image=busybox:1.28 --share-processes --copy-to=pod1-debug
Defaulting debug container name to debugger-w8pqk.
If you don't see a command prompt, try pressing enter.
/ # ping 10.20.9.60
PING 10.20.9.60 (10.20.9.60): 56 data bytes
^C
--- 10.20.9.60 ping statistics ---
4 packets transmitted, 0 packets received, 100% packet loss
/ # ping 10.20.9.61
PING 10.20.9.61 (10.20.9.61): 56 data bytes
64 bytes from 10.20.9.61: seq=0 ttl=63 time=11.655 ms
64 bytes from 10.20.9.61: seq=1 ttl=63 time=0.590 ms
64 bytes from 10.20.9.61: seq=2 ttl=63 time=0.454 ms
64 bytes from 10.20.9.61: seq=3 ttl=63 time=0.434 ms
^C
--- 10.20.9.61 ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max = 0.434/3.283/11.655 ms
/ #
2.2.2,创建副本时改变命令
实例三:创建一个 pod,该 pod 功能是运行一个 shell 脚本打印当前日期。错误状态:日志并没有打印出日期,且 pod 已运行完成处于 competed 状态,排查原因
1,创建 pod3
[centos@ml-k8s-1 test3]$ kubectl apply -f pod3.yaml
pod/pod3 created
2,查看 pod
[centos@ml-k8s-1 test3]$ kubectl get pod
NAME READY STATUS RESTARTS AGE
pod3 0/1 Completed 0 7s
3,查看日志
[centos@ml-k8s-1 test3]$ kubectl logs pod3
Hello ldsdsy
Today is
// 期望日志是会打印出当前日期,但并没有
4,进入容器
[centos@ml-k8s-1 test3]$ kubectl exec -it pod3 -- sh
error: cannot exec into a container in a completed pod; current phase is Succeeded
// pod 已完成,无法再进入容器内部
5,创建副本
[centos@ml-k8s-1 test3]$ kubectl debug pod3 -it --copy-to=pod3-debug --container=pod3 -- sh
If you don't see a command prompt, try pressing enter.
/ # ls
app bin dev etc home proc root sys tmp usr var
/ # cd app
/app # ls
test.sh
/app # cat test.sh
#! /bin/sh
echo "Hello ldsdsy"
time=$(date +"%Y-%m-%d %H:%M:%S")
echo "Today is $ttime"
/app #
// 以 sh 的形式进入容器内部,可以一句一句地运行代码排查是什么地方出错,此处很简单可以看出是错把 time 写成了 ttime
2.2.3,创建副本时改变镜像
实例四:在实例三的基础上,修改 ttime 的拼写问题,重新打包镜像
1,创建 pod 副本,把容器 pod3 的镜像改为新镜像
[centos@ml-k8s-1 test3]$ kubectl debug pod3 --copy-to=pod3-debug --set-image=pod3=beyond.io:5000/debug-test:0.1.4
// --set-image=*=xxx 表示把 pod 的所有容器镜像全换成 xxx
2,查看 pod
[centos@ml-k8s-1 test3]$ kubectl get pod
NAME READY STATUS RESTARTS AGE
pod3 0/1 Completed 0 13m
pod3-debug 0/1 Completed 0 8s
3,查看日志
[centos@ml-k8s-1 test3]$ kubectl logs pod3-debug
Hello ldsdsy
Today is 2022-05-02 09:50:25
[centos@ml-k8s-1 test3]$
// 此时输出正常,新镜像可行
2.3 使用 shell 调试
实例五:创建一个 pod,该 pod 功能是读出其所在节点下 /home/centos/data 目录里的数据。错误状态:没有读出任何数据,排查问题。
1,创建 pod5
[centos@ml-k8s-1 test5]$ kubectl apply -f pod5.yaml
pod/pod5 created
2,查看 pod
[centos@ml-k8s-1 test5]$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod5 0/1 Completed 0 82s 10.245.0.213 ml-k8s-3.novalocal <none> <none>
// pod 运行正常
3,查看 pod 日志
[centos@ml-k8s-1 test5]$ kubectl logs pod5
DIR is /data
/data/*
// 没有数据
4,shell 调试
[centos@ml-k8s-1 debug]$ kubectl debug node/ml-k8s-2.novalocal -it --image=centos
Creating debugging pod node-debugger-ml-k8s-2.novalocal-bb8s2 with container debugger on node ml-k8s-2.novalocal.
If you don't see a command prompt, try pressing enter.
[root@ml-k8s-2 /]# ls
bin dev etc home host lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var
[root@ml-k8s-2 /]# cd host
[root@ml-k8s-2 host]# ls
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
[root@ml-k8s-2 host]# cd home/centos
[root@ml-k8s-2 centos]# ls
beyonlet_0.tar coredns.tar data debug kubeadm nfs-subdir-external-provisioner.tar nfs.tar redis redis-tools-41.tar
[root@ml-k8s-2 centos]# cd data
[root@ml-k8s-2 data]# ls
[root@ml-k8s-2 data]#
// 节点的根文件系统会被挂载在 /host
// 进入节点 2,查看相关目录,发现目录下本就没有数据,导入数据重启 pod ,正常
三种调试的区别
1,第一种使用临时容器,更多的是被调试的容器处于 running 状态但又无法进入到容器内部调试,所以借助临时容器来进入容器内部排查问题。
2,第二种使用 pod 副本,更多的是建立一个被调试容器的副本用来调试,这样无需关心原本被调试容器的状态如何。
3,第三种使用 shell 调试,是在上述方式都不行的情况下,直接进入 pod 所在节点上进行调试。
调研过程中遇到的问题
1,为什么编辑完 kube-apiserver.yaml 及 kube-scheduler.yaml 不用 apply ,pod 就自动重启了?
1) 因为 apiserver,scheduler,etcd,controller manager 是静态 pod。静态 pod 直接由 kubelet 进程管理,而不是 apiserver。
2) kubelet 会自动为每一个静态 pod 在 k8s 的 apiserver 上创建一个镜像 pod,因此可以在 apiserver 中查询到该 pod。
3) 创建静态 pod 有两种方式:配置文件,HTTP。
1. 配置文件:通过 kubelet 配置的环境变量(一个具体目录),让 kubelet 定期去扫描这个目录,根据目录下出现/消失/变动的 yaml/json 文件来创建/删除/更新静态 pod。步骤如下:
a) $ systemctl status kubelet
// 在想要运行静态 pod 的节点上找到 kubelet 的启动配置文件
b) $ cat /usr/lib/systemd/system/kubelet.service.d/10-kubeadm.conf
// Environment="KUBELET_SYSTEM_PODS_ARGS=--pod-manifest-path=/etc/kubernetes/manifests --allow-privileged=true"
// 环境变量 --pod-manifest-path 后即是 kubelet 会定期扫描的目录,把想创建的静态 pod yaml 放在此目录即可
// 如果启动参数里面没有这个参数,添加上重启 kubelet 即可。($ systemctl restart kubelet)
2. HTTP:kulete 会周期性地从 -manifest-url= 参数指定的地址下载文件并翻译成 yaml/json 格式的 pod 定义,此后与配置文件的方式相同,kubelet 会不时地重新下载该文件,当文件变化时对应地操作 pod。
4) 除了上述方式,还可以在 kubelet 配置文件中添加 staticPodPath:<目录> 字段来实现。
// 配置文件在 /var/lib/kubelet/config.yaml,此方法有一定风险。
2,为什么使用临时容器调试时,kubectl debug 后不生效?
1) 如果不配置 apiserver,controller-manager,scheduler 这个三个组件的 yaml 文件设定使用临时容器,则执行 kubectl debug 后会报错。
2) 设置了上述三个 yaml 文件,但没有配置 pod 所在节点上的 kubelet 开启临时容器功能,则在执行 kubectl debug 后会一直卡住,不会显示预期的命令行。
3,kubelet 到底有多少种配置方式
在以上实验过程中,发现每篇博客都有不同 kubelet 的配置方式
根源在于 linux 系统里有很多 system 目录,常见的有 /etc/systemd/system、/lib/systemd/system 以及 /uer/lib/systemd/system 等。其中 后两者指向同一目录,在根目录下执行 ll 可知。
/etc/systemd/system: Local configuratin,是系统管理员安装的单元文件。
[/usr]/lib/systemd/system: Units of installed packages,目录包含的是软件包安装的单元,即通过 yum,dnf,rpm 等软件包管理命令管理的 systemd 单元文件。
还有一个 /run/systemd/system: Runtime units,这个一般是进程在运行时动态创建 unit 文件的目录,一般不修改,除非改程序运行时的一些参数,即 Session 级别的。
/etc/systemd/system, /run/systemd/system, /lib/systemd/system 优先级从高到低
附录
实例一
1,main.go
package main
import (
"context"
"io"
"os"
"time"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/klog/v2"
)
func main() {
klog.Info("Test start!")
klog.Info("Pulling data from bucket 1 and storing it in bucket 2.")
// creates the in-cluster config
config, err := rest.InClusterConfig()
if err != nil {
klog.Errorln("Wrong in creating config: ", err)
}
// create the clientset
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
klog.Errorln("Wrong in creating clientset: ", err)
}
// Get info of minio from s3-secret
secret, err := clientset.CoreV1().Secrets("default").Get(context.TODO(), "minio-secret", metav1.GetOptions{})
if err != nil {
klog.Errorln("Wrong in getting secret: ", err)
time.Sleep(1 * time.Hour)
}
id := string(secret.Data["id"])
key := string(secret.Data["key"])
endpoint := string(secret.Data["endpoint"])
useSSL := false //true 会走 https
// Initialize minio client object.
minioClient, err := minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(id, key, ""),
Secure: useSSL,
})
if err != nil {
klog.Errorln("Wrong in getting minioClient : ", err)
time.Sleep(1 * time.Hour)
}
object, err := minioClient.GetObject(context.Background(), "mybucket1", "test.txt", minio.GetObjectOptions{})
if err != nil {
klog.Errorln("Wrong in getting object from mybucket1: ", err)
time.Sleep(1 * time.Hour)
}
//以读写方式打开文件,如果不存在,则创建(只创建文件,不能创建文件夹)
localFile, err := os.OpenFile("/data/test.txt", os.O_RDWR|os.O_CREATE, 0766)
if err != nil {
klog.Errorln("Wrong in creating /data/test.txt: ", err)
time.Sleep(1 * time.Hour)
}
klog.Info(localFile)
defer localFile.Close()
if _, err = io.Copy(localFile, object); err != nil {
klog.Errorln("Wrong in coping object from mybucket1 to localFile: ", err)
time.Sleep(1 * time.Hour)
}
file, err := os.Open("/data/test.txt")
if err != nil {
klog.Errorln("Wrong in getting object from /data/test.txt: ", err)
time.Sleep(1 * time.Hour)
}
defer file.Close()
fileStat, err := file.Stat()
if err != nil {
klog.Errorln("Wrong in getting fileStat: ", err)
time.Sleep(1 * time.Hour)
}
// Create a bucket at region 'us-east-1' with object locking enabled.
err = minioClient.MakeBucket(context.Background(), "mybucket2", minio.MakeBucketOptions{Region: "cn-north-1", ObjectLocking: false})
if err != nil {
klog.Errorln("Wrong in creating mybucket2: ", err)
time.Sleep(1 * time.Hour)
}
uploadInfo, err := minioClient.PutObject(context.Background(), "mybucket2", "test.txt", file, fileStat.Size(), minio.PutObjectOptions{ContentType: "application/octet-stream"})
if err != nil {
klog.Errorln("Wrong in putting myobject to mybucket2: ", err)
time.Sleep(1 * time.Hour)
}
klog.Infoln("Successfully uploaded bytes: ", uploadInfo)
}
2,Dockerfile
FROM scratch
ADD ./app /
CMD ["/app"]
3,部署文件
- pod1.yaml
apiVersion: v1
stringData:
id: AKIAIOSFODNN7EXAMPLE
key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
endpoint: 10.20.9.60:30009
kind: Secret
metadata:
name: pod1-secret
type: Opaque
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pod1-get
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: pod1-get-rbac
subjects:
- kind: ServiceAccount
namespace: default
name: pod1-sa
roleRef:
kind: ClusterRole
name: pod1-get
apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: pod1-sa
namespace: default
---
apiVersion: v1
kind: Pod
metadata:
name: pod1
labels:
k8s-app: pod1
spec:
serviceAccountName: pod1-sa
restartPolicy: Never
containers:
- name: pod1
image: beyond.io:5000/debug-test:0.1.1
imagePullPolicy: Always
volumeMounts:
- name: volume
mountPath: /data
readOnly: False
volumes:
- name: volume
emptyDir: {}
实例三
1,test.sh
#! /bin/sh
echo "Hello ldsdsy"
time=$(date +"%Y-%m-%d %H:%M:%S")
echo "Today is $ttime"
2,Dockerfile
FROM busybox:1.28
RUN mkdir /app
ADD ./test.sh /app
RUN chmod +x /app/test.sh
CMD ["sh","-c","/app/test.sh"]
3,pod3.yaml
apiVersion: v1
kind: Pod
metadata:
name: pod3
labels:
k8s-app: pod3
spec:
restartPolicy: Never
containers:
- name: pod3
image: beyond.io:5000/debug-test:0.1.3
imagePullPolicy: Always
实例五
1,test.sh
#! /bin/sh
DATA_DIR=${DATA_DIR}
echo "DIR is $DATA_DIR"
for file in $DATA_DIR/*
do
echo $file
if [ -f $file ]; then
cat $file
fi
done
2,pod5.yaml
apiVersion: v1
kind: Pod
metadata:
name: pod5
labels:
k8s-app: pod5
spec:
restartPolicy: Never
containers:
- name: pod5
image: beyond.io:5000/debug-test:0.1.5
imagePullPolicy: Always
env:
- name: DATA_DIR
value: /data
volumeMounts:
- name: volume
mountPath: /data
readOnly: False
volumes:
- name: volume
hostPath:
path: /home/centos/data