Back

K8s_kubectl_debug

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,部署文件

  1. 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
Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy