侧边栏壁纸
博主头像
船长博主等级

专注于云原生运维,致敬每个爱学习的你

  • 累计撰写 35 篇文章
  • 累计创建 10 个标签
  • 累计收到 9 条评论

“kubectl exec”是如何工作的?

船长
2022-05-08 / 0 评论 / 3 点赞 / 863 阅读 / 4,669 字
温馨提示:
本文最后更新于 2022-05-08,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

上周五,我的一位同事找到我,问了一个关于如何使用 client-go 在 pod 中执行命令的问题。 我不知道答案,我注意到我从未想过“kubectl exec”中的机制。 我对它应该如何做有一些想法,但我不是 100% 确定。 我注意到要再次检查的主题,在阅读了一些博客、文档和源代码后,我学到了很多东西。 在这篇博文中,我将分享我的理解和发现。

开始

我克隆了 https://github.com/ecomm-integration-ballerina/kubernetes-cluster 以便在我的 MacBook 中创建一个 k8s 集群。 我修复了 kubelet 配置中节点的 IP 地址,因为默认配置不允许我运行 kubectl exec。 你可以在这里找到根本原因。

  • Any machine = my MacBook
  • IP of master node = 192.168.205.10
  • IP of worker node = 192.168.205.11
  • API server port = 6443

组件

  • kubectl exec process: 当我们在机器上运行“kubectl exec ...”时,会启动一个进程。 您可以在任何可以访问 k8s api 服务器的机器上运行它。
  • Api Server: 暴露 Kubernetes API 的 master 上的组件。 它是 Kubernetes 控制平面的前端。
  • Kubelet: 在集群中的每个节点上运行的代理。 它确保容器在 pod 中运行。
  • container runtime: 负责运行容器的软件。 示例:docker、cri-o、containerd…
  • kernel: 工作节点中负责管理进程的操作系统的内核。
  • target container: 一个容器,它是 pod 的一部分,并且在其中一个工作节点上运行。

发现

客户端活动

  • 在默认命名空间中创建一个 pod
// any machine
$ kubectl run exec-test-nginx --image=nginx
  • 然后运行 ​​exec 命令并sleep 5000进行观察
// any machine
$ kubectl exec -it exec-test-nginx-6558988d5-fgxgg -- sh
# sleep 5000
  • 我们可以观察kubectl进程(本例中pid=8507)
// any machine
$ ps -ef |grep kubectl
501  8507  8409   0  7:19PM ttys000    0:00.13 kubectl exec -it exec-test-nginx-6558988d5-fgxgg -- sh
  • 当我们检查进程的网络活动时,我们可以看到它与 api-server (192.168.205.10.6443) 有一些连接
// any machine
$ netstat -atnv |grep 8507
tcp4       0      0  192.168.205.1.51673    192.168.205.10.6443    ESTABLISHED 131072 131768   8507      0 0x0102 0x00000020
tcp4       0      0  192.168.205.1.51672    192.168.205.10.6443    ESTABLISHED 131072 131768   8507      0 0x0102 0x00000028
  • 让我们检查一下代码。kubectl 创建一个带有子资源的 POST 请求exec并发送一个休息请求。

主节点活动

  • 我们也可以在 api-server 端观察请求。
handler.go:143] kube-apiserver: POST "/api/v1/namespaces/default/pods/exec-test-nginx-6558988d5-fgxgg/exec" satisfied by gorestful with webservice /api/v1
upgradeaware.go:261] Connecting to backend proxy (intercepting redirects) https://192.168.205.11:10250/exec/default/exec-test-nginx-6558988d5-fgxgg/exec-test-nginx?command=sh&input=1&output=1&tty=1
Headers: map[Connection:[Upgrade] Content-Length:[0] Upgrade:[SPDY/3.1] User-Agent:[kubectl/v1.12.10 (darwin/amd64) kubernetes/e3c1340] X-Forwarded-For:[192.168.205.1] X-Stream-Protocol-Version:[v4.channel.k8s.io v3.channel.k8s.io v2.channel.k8s.io channel.k8s.io]]

> Notice that the http request includes a protocol upgrade request. [SPDY](https://www.wikiwand.com/en/SPDY) allows for separate stdin/stdout/stderr/spdy-error "streams" to be multiplexed over a single TCP connection.
  • ApiServer接收请求并将其绑定到一个PodExecOptions
  • 为了能够采取必要的行动,api-server 需要知道它应该联系哪个位置。

当然端点是从节点信息派生的。

明白了!KUBELET 有一个端口 ( node.Status.DaemonEndpoints.KubeletEndpoint.Port),API-SERVER 可以连接到该端口。

主节点通信 > 主节点到集群 > apiserver 到 kubelet

这些连接终止于 kubelet 的 HTTPS 端点。默认情况下,apiserver 不验证 kubelet 的服务证书,这使得连接受到中间人攻击,并且在不受信任和/或公共网络上运行是不安全的。

  • 现在,apiserver 知道端点并打开连接。
  • 让我们检查一下主节点上发生了什么。

首先,查看worker节点的ip。在192.168.205.11这种情况下。

// any machine
$ kubectl get nodes k8s-node-1 -o wide
NAME         STATUS   ROLES    AGE   VERSION   INTERNAL-IP      EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION      CONTAINER-RUNTIME
k8s-node-1   Ready    <none>   9h    v1.15.3   192.168.205.11   <none>        Ubuntu 16.04.6 LTS   4.4.0-159-generic   docker://17.3.3

然后获取 kubelet 端口。在10250这种情况下。

// any machine
$ kubectl get nodes k8s-node-1 -o jsonpath='{.status.daemonEndpoints.kubeletEndpoint}'
map[Port:10250]

然后检查网络。是否与工作节点(192.168.205.11)有连接?连接就在那里。当我杀死 exec 进程时,它消失了,所以我知道它是由 api-server 设置的,因为我的 exec 命令。

// master node
$ netstat -atn |grep 192.168.205.11
tcp        0      0 192.168.205.10:37870    192.168.205.11:10250    ESTABLISHED

Worker 节点中的活动

  • 让我们继续连接到工作节点并检查工作节点上发生了什么。

首先,我们也可以观察到这里的联系。第二行。192.168.205.10是主节点的IP。

// worker node
$ netstat -atn |grep 10250
tcp6       0      0 :::10250                :::*                    LISTEN
tcp6       0      0 192.168.205.11:10250    192.168.205.10:37870    ESTABLISHED

我们的 sleep 命令呢?我们的命令在那里!!!!

// worker node
$ ps -afx
...
31463 ?        Sl     0:00      \_ docker-containerd-shim 7d974065bbb3107074ce31c51f5ef40aea8dcd535ae11a7b8f2dd180b8ed583a /var/run/docker/libcontainerd/7d974065bbb3107074ce31c51
31478 pts/0    Ss     0:00          \_ sh
31485 pts/0    S+     0:00              \_ sleep 5000
  • 等待!kubelet 是如何做到的?
  • kubelet 有一个守护进程,它通过端口为 api-server 请求提供 api。
  • kubelet 计算 exec 请求的响应端点。

不要混淆。它不返回命令的结果。它返回一个用于通信的端点。

kubelet 实现RuntimeServiceClient了接口,它是容器运行时接口的一部分。

它只是使用 gRPC 通过容器运行时接口调用方法。

Container Runtime 负责实现RuntimeServiceServer

  • 如果是这样,我们需要观察 kubelet 和容器运行时之间的联系。对?让我们检查。

在运行 exec 命令之前和之后运行此命令并检查差异。在我的情况下,这是一个差异。

// worker node
$ ss -a -p |grep kubelet
...
u_str  ESTAB      0      0       * 157937                * 157387                users:(("kubelet",pid=5714,fd=33))
...

嗯。在 kubelet(pid=5714) 和其他东西之间有一个通过 unix 套接字的新连接。谁可以?是的。它是Docker(pid = 1186)。

// worker node
$ ss -a -p |grep 157387
...
u_str  ESTAB      0      0       * 157937                * 157387                users:(("kubelet",pid=5714,fd=33))
u_str  ESTAB      0      0      /var/run/docker.sock 157387                * 157937                users:(("dockerd",pid=1186,fd=14))
...

记住。这是运行我们命令的 docker 守护进程(pid=1186)。

// worker node.
$ ps -afx
...
 1186 ?        Ssl    0:55 /usr/bin/dockerd -H fd://
17784 ?        Sl     0:00      \_ docker-containerd-shim 53a0a08547b2f95986402d7f3b3e78702516244df049ba6c5aa012e81264aa3c /var/run/docker/libcontainerd/53a0a08547b2f95986402d7f3
17801 pts/2    Ss     0:00          \_ sh
17827 pts/2    S+     0:00              \_ sleep 5000
...

Container Runtime 中的活动

  • 让我们检查 cri-o 的源代码以了解它是如何发生的。docker中的逻辑类似。

它有一个实现 RuntimeServiceServer 的服务器。

在链的末端,容器运行时在工作节点中执行命令。

来源:https://erkanerol.github.io/

3

评论区