上周五,我的一位同事找到我,问了一个关于如何使用 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 的服务器。
在链的末端,容器运行时在工作节点中执行命令。
评论区