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 处理。

  1. 从 req 中取出 container id、stdin、stdout、stderr、terminal 等;
  2. VirtContainerManager 管理的容器中找出对应 container id 的容器实例 c(类型为 runtimes::virt_container_container_manager_manager::container::Container)
  3. 执行 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 中拿到用户想要执行的命令,然后执行就完事了!