Z.S.K.'s Records

Kubernetes学习(kubectl的工作机制分析)

看到一篇关于kubectl运行的机制,觉得写得非常不错,图文并茂很形象,就翻译成了中文记录一下,原文地址: https://erkanerol.github.io/post/how-kubectl-exec-works/

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

如果出现问题,可在以下频道找到我 :https://twitter.com/erkan_erol_

Setup

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

下文提到的关于机器的对应关系如下:

  • any machine = MacBook
  • master IP = 192.168.205.10
  • work IP = 192.168.205.11
  • API server端口 = 6443

Component

组件

  • kubectl exec:当我们在机器上运行“ kubectl exec…”时,可以在任何有权限访问k8s api服务器上运行。
  • api serverapi server上的组件,用于公开Kubernetes API。它是Kubernetes控制平面的前端。
  • kubelet:在集群中每个节点上运行的代理。确保容器在容器中运行。
  • container runtime:负责运行容器的软件。例如:docker,cri-o,containerd…
  • kernel:工作节点中操作系统的内核,负责管理进程。
  • target container:作为Pod的一部分并在其中一个工作程序节点上运行的容器。

Findings

1. Client端

  • 在默认名称空间中创建容器

    1
    2
    // any machine
    $ kubectl run exec-test-nginx --image=nginx
  • 然后运行exec命令并sleep 5000进行观察

    1
    2
    3
    // any machine
    $ kubectl exec -it exec-test-nginx-6558988d5-fgxgg -- sh
    # sleep 5000
  • 我们可以观察到kubectl过程(在这种情况下为pid = 8507)

    1
    2
    3
    // 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)有连接

    1
    2
    3
    4
    // 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并发送一个rest请求。

休息请求

2. apiserver端

  • 我们可以在api服务器端观察请求。

    1
    2
    3
    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]]

    请注意,http请求包括协议升级请求。SPDY允许将单独的stdin / stdout / stderr / spdy-error流通过单个TCP连接进行多路复用。

  • Api服务器收到请求并将其绑定到 PodExecOptions

  • 为了能够采取必要的措施,api-server需要知道应该请求哪个node。

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

    发现了,KUBELET具有可node.Status.DaemonEndpoints.KubeletEndpoint.Port连接API服务器的端口。

    master/node之间的通信> master集群通信 > apiserver到kubelet

    这些连接在kubelet的HTTPS端点处终止。默认情况下,API服务器不验证kubelet的服务证书,这使得连接容易遭受中间人攻击,不安全的公共网络。

  • 现在,api服务器知道了端点并打开了连接。

  • 让我们检查master上发生了什么。

首先,得到worker节点的ip。正是192.168.205.11在这种情况下。

1
2
3
4
// 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

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

然后检查网络。是否存在到工作节点(192.168.205.11)的连接?可以看到,当我杀死exec进程时,它消失了,所以我知道它正是由于exec命令而由api-server设置的

1
2
3
4
// master node
$ netstat -atn |grep 192.168.205.11
tcp 0 0 192.168.205.10:37870 192.168.205.11:10250 ESTABLISHED
...

api服务器到kubelet

  • 现在,kubectl和api-server之间的连接仍然打开,并且api-server和kubelet之间还有另一个连接。

3. work节点

  • 让我们通过连接到work节点并检查正在发生的事情。

首先,我们也可以在此处观察连接。第二行。192.168.205.10是master的IP。

1
2
3
4
// 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命令呢?可以通过ps命令找到!!!

1
2
3
4
5
6
7
// 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有一个守护程序,该守护程序通过10250端口与api server通信。
  • kubelet计算执行请求的响应端点。

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

kubelet实现的RuntimeServiceClient接口是Container Runtime Interface的一部分。

它仅使用gRPC通过Container Runtime Interface调用方法。

container runtime 负责实施 RuntimeServiceServer

Kubelet到容器的运行时

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

在运行exec命令之前和之后运行此命令,并检查diff。

1
2
3
4
5
// 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)。

1
2
3
4
5
6
// 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)。

1
2
3
4
5
6
7
8
// 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
...

4. Docker runtime

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

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

在链的最后, container runtime在work 节点中执行命令。

容器运行时到内核

最后,kernel执行命令

内核输入

注意事项

  • api-server也可以初始化与kubelet的连接。
  • 这些连接将一直持续到交互式执行程序结束。
    • kubectl和api server之间的连接
    • api server和kubelet之间的连接
    • kubelet与container runtime之间的连接
  • kubectl或api-server无法在work节点中运行任何内容。kubelet可以运行,但也可以与container runtime时交互以进行此类操作。

参考

Z.S.K. wechat
Scan Me To Read on Phone
I know you won't do this,but what if you did?