乐趣区

关于kubernetes:K8S-故障排错新手段kubectl-debug-实战

K8S INTERNAL 系列

容器编排之争在 Kubernetes 一统天下场面造成后,K8S 成为了云原生时代的新一代操作系统。K8S 让所有变得简略了,但本身逐步变得越来越简单。【K8S Internals 系列专栏】围绕 K8S 生态的诸多方面,将由博云容器云研发团队定期分享无关调度、平安、网络、性能、存储、利用场景等热点话题。心愿大家在享受 K8S 带来的高效便当的同时,又能够如庖丁解牛般领略其内核运行机制的魅力。

本文将为大家介绍一个 K8S 故障排错新伎俩:kubectl debug。

一、kubectl debug 起源

开发者喜爱在生产部署中应用极致精简的容器镜像,这也是容器技术中的一个最佳实际。这种精简主义有很多益处,而且在大多数状况下运行良好,然而一旦须要在生产中排除一些故障时,这就变得很艰难了,因为精简后的容器广泛缺失罕用的排障工具,有些甚至连 bash/sh 解释器都没有。

过来几年,K8S 社区就始终有一个声音,如果有一种办法能够为正在运行的 Pod 启用某种调试模式,再附加一套调试工具能在容器中执行,那就最好不过了。这种新的调试模式波及的改变面很广,从 16 年就呈现了相干的 Issue Support for troubleshooting distroless containers 开始,直至 K8S1.23 版本,kubectl debug 这项性能才逐步成熟。

kubectl debug 是一款 k8s pod 诊断工具,可能帮忙进行 Pod 的排障诊断。在 k8s v1.16 ~ v1.22 中是 Alpha 状态,默认敞开。从 v1.23 开始成为 Beta 状态,默认开启。

二、kubectl debug 工作原理

咱们晓得,容器实质上是带有 cgroup 资源限度和 namespace 隔离的一组过程。因而,咱们只有启动一个过程,并且让这个过程退出到指标容器的各种 namespace 中,这个过程就能“进入容器外部”(留神引号),与容器中的过程“看到”雷同的根文件系统、虚构网卡、过程空间了 —— 这也正是 docker exec 和 kubectl exec 等命令的运行形式。

当初的情况是,咱们不仅要“进入容器外部”,还心愿带一套工具集进去帮忙排查问题。那么,想要高效治理一套工具集,又要能够跨平台,最好的方法就是把工具自身都打包在一个容器镜像当中。接下来,咱们只须要通过这个“工具镜像”启动容器,再指定这个容器退出指标容器的的各种 namespace,天然就实现了“携带一套工具集进入容器外部”。

三、kubectl debug 怎么用

1. 开启性能
在 V1.23 及以上版本中,该性能默认开启。针对 1.23 以下的 K8S 版本,须要通过以下形式,手动开启。

## 管制面开启 EphemeralContainers featureGate.
### 进入 master 节点,编辑 /etc/kubernetes/manifests/ 下的 kube-apiserver.yaml,kube-controller-manager.yaml 及 kube-scheduler.yaml,在 command 局部增加 - --feature-gates=EphemeralContainers=true;## Kubelet 服务开启该性能
### 在节点上编辑 /var/lib/kubelet/kubeadm-flags.env,增加 --feature-gates=EphemeralContainers=true;或者设置 KUBELET_EXTRA_ARGS="--feature-gates=EphemeralContainers=true"

## 重启 Kubelet:### systemctl restart kubelet
  1. 应用
    2.1 应用长期容器调试

通常只须要一条命令即可为 Pod 里的具体某个容器增加一个长期容器(镜像为 busybox),并进行 debug。

$ kubectl debug -it ${pod_name} --image=busybox:1.28 --target=${container_name}

2.2 调试示例一:通过附加调试容器对处于 Running 状态的 Pod 进行调试

创立一个 Pod,该 Pod 性能是从 S3 存储的一个桶 mybucket1 中获取文件 test.txt 并复制到本地目录 /data/test.txt,再从此目录把 test.txt 上传到桶 mybucket2 里。但咱们发现桶 mybucket2 并没有相应文件,那么咱们该如何通过 kubectl debug 查找起因呢?

(1)创立 pod1

[[email protected] 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
[[email protected] test1]$ kubectl get pod
NAME                                       READY   STATUS    RESTARTS   AGE
pod1                                       1/1     Running   0          7s

此时 pod1 始终处于 Running 状态,失常状况大略 10 秒左右就会进入 Completed 状态。只有碰到某种异样故障的时候才会卡在 Running 不动,导致该工作没有实现。此时查看桶 mybucket2,的确也没有数据。

(2)查看 pod1 以后状态,无异样状态

[[email protected] 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)查看 pod1 日志

pod1 日志不够详尽,无异样信息。

[[email protected] 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 mybucket 1 and storing it in mybucket 2.
Wrong in getting object from mybucket1.

(4)进入容器外部排查

容器根底镜像为 scratch,不蕴含 sh,无奈进入容器外部进行调试。此时传统 K8S 提供的常见排错形式均无奈持续追踪此问题。

[[email protected] 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

(5)通过 kubectl debug 模式进入容器外部

通过启动 busybox 容器对出问题的 pod1 进行排错。进入 pod1 容器外部后,查看 /data 目录下曾经有指标文件 test.txt,但该文件 test.txt 内容为空,复制未胜利。此时通过查看容器启动命令发现 S3 服务地址址配置谬误导致文件下载失败。

[[email protected] 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

(unreachable)/data # cat test.txt

(6)批改 Pod 配置,该工作最终失常运行。

2.3 调试示例二:通过复制对处于 Completed 状态的 Pod 进行调试

创立一个 Pod,该 Pod 性能是运行一个 shell 脚本打印以后日期。谬误状态:日志并没有打印出日期,且 Pod 已运行实现处于 Completed 状态,那么如何进行排错呢?

(1)创立 pod3

[[email protected] test3]$ kubectl apply -f pod3.yaml
pod/pod3 created

(2)查看 pod3

[[email protected] test3]$ kubectl get pod
NAME                                      READY   STATUS      RESTARTS   AGE
pod3                                      0/1     Completed   0          7s

(3)查看日志

冀望会打印出以后日期,但发现日期未打印。

[[email protected] test3]$ kubectl logs pod3
Hello ldsdsy
Today is

(4)进入容器

pod3 进入 Completed 状态,无奈执行 exec 进入。

[[email protected] test3]$ kubectl exec -it pod3 -- sh
error: cannot exec into a container in a completed pod; current phase is Succeeded

(5)通过创立正本进行调试

创立正本容器,并以 sh 的模式进入容器外部,能够一句一句地运行代码排查是什么中央出错,此处很简略能够看出是错把 time 写成了 ttime。

[[email protected] 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 #

(6)批改镜像从新以 debug 模式运行

批改 ttime 的拼写问题,从新打包镜像。而后进行 debug 时,设置应用新的镜像。

[[email protected] test3]$ kubectl debug pod3 --copy-to=pod3-debug --set-image=pod3=beyond.io:5000/debug-test:0.1.4

// --set-image=*=xxx 示意把 Pod 的所有容器镜像全换成 xxx

(7)查看新 pod 执行状况,发现程序失常执行。

[[email protected] test3]$ kubectl get pod
NAME                                      READY   STATUS      RESTARTS   AGE
pod3                                      0/1     Completed   0          13m
pod3-debug                                0/1     Completed   0          8s

[[email protected] test3]$ kubectl logs pod3-debug
Hello ldsdsy
Today is 2022-05-02 09:50:25
[[email protected] test3]$

3.kubectl debug 几种调试的区别
第一种模式应用长期容器,更多的是被调试的容器处于 Running 状态但又无奈进入到容器外部调试,所以借助长期容器来进入容器外部排查问题。第二种应用 Pod 正本,更多的是建设一个被调试容器的正本用来调试,这样无需关怀本来被调试容器的状态如何。除了以上两种常见的排错形式,kubectl debug 还反对进入 Pod 所在节点上进行调试。

kubectl debug 为咱们对运行在 K8S 上的业务进行排错提供了多种形式,这几种形式应用起来十分不便灵便,在理论排错过程中要联合具体情况正当应用。

四、附录

实例一

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 mybucket 1 and storing it in mybucket 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)

}
  1. Dockerfile

    `
    `FROM scratch
    ADD ./app /
    CMD ["/app"]``
  2. 部署文件

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"
  1. Dockerfile
FROM busybox:1.28
RUN mkdir /app
ADD ./test.sh /app
RUN chmod +x /app/test.sh
CMD ["sh","-c","/app/test.sh"]
  1. 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 
退出移动版