Kata Containers 下的 kubectl exec 流程

Containerd、Shim 和 Agent 交互逻辑

Kata Containers 下的 kubectl exec 流程

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 中拿到用户想要执行的命令,然后执行就完事了!