Kata Containers 下的 kubectl exec 流程
Containerd、Shim 和 Agent 交互逻辑
Docker exec 的原理参见 docker exec实现原理,这里讲了一些与 namespace 相关的知识。这是 kubectl exec 为什么可以工作的根本,在 Kata Containers 环境下,Agent 实际在负责类似于 docker exec 的功能。
Containerd
Containerd 在执行 exec 的时候,由 criService::ExecSync()
承接来自 kubelet 的 exec 请求,最终由 criService::execInternal()
完成对 Task::Exec()
方法的调用,构造了 ExecProcess
结构体。
用户传入的 shell 命令最终被保存在了 OCI Spec 中。大概的流程是先获取到当前 container 对应的 spec,然后将用户的命令保存在了 Spec::Process::Args
中,通过 ttrpc 传给了 Kata Shim。
Shim (Runtime)
Shim 的作用很简单,接受来自 containerd 的请求,将 exec process 注册并维护在自己的数据结构中,最后通过 ttrpc 将 exec 请求转发给 Agent。
shim_ctl::main::real_main()
启动了一个异步线程(如何一直等待呢?)去处理 containerd 发来的消息。runtimes::manager::RuntimeHandlerManager::handler_message()
将 req 区分为了创建请求(CreateContainer
)和其他,其他请求由 runtimes::manager::RuntimeHandlerManager::handler_request()
处理。
Exec 的请求是 Request::ExecProcess
,由 runtimes::virt_container_container_manager_manager::VirtContainerManager
处理。
- 从 req 中取出 container id、stdin、stdout、stderr、terminal 等;
- 从
VirtContainerManager
管理的容器中找出对应 container id 的容器实例c
(类型为runtimes::virt_container_container_manager_manager::container::Container)
; - 执行
c.exec_process()
,这里只是保存了与 exec 相关的数据,比如exec_id
类似与主键?,比如stdin
标明 stdin 的输出路径等等。
至此,一个 exec process 被注册在了 shim 中。真正启动的是 start()
,实际承接的是 runtimes::virt_container_container_manager_manager::container_inner::start_exec_process()
。该方法根据 exec_id
找到对应的 Exec 结构体,然后调用 agent 的 exec_process()
。
Agent
Agent 是实际 exec process 的执行方,它的原理是 fork 一份子进程(入口指令是 kata-agent init
),然后在子进程中执行用户传入的命令(存储在 OCI Spec 中)。
Agent 侧的 RPC 接口的实现定义在 rpc::AgentService::exec_process()
,将 req 一股脑构建到 rustjail::process::Process
结构体中,然后从 sandbox 中获取对应的 container 实例 ctr
,调用 ctr.run(p)
执行 exec process。
ctr.run(p)
首先调用了 rustjail::container::LinuxContainer::start()
,这个方法是 exec 流程的关键。Exec 核心就是启动了一个子进程(child
),并在该子进程下调用 exec 的具体 shell 命令(命令就藏在 OCI Spec 中,子进程同时保存了一份 OCI Spec,可以自由获取命令并执行,具体可以看下 kata-agent init 命令)。
在下面的代码中,加入 pid namespace 是 exec 的关键步骤之一,具体可以参见 docker exec 原理。
Kata-agent init 命令实际由 rustjail::container::do_init_child()
方法承接,具体代码不看了(因为又臭又长),从功能上来看,它的作用是从 OCI Spec 中拿到用户想要执行的命令,然后执行就完事了!