https://github.com/kata-containers/kata-containers/tree/main/docs/design/architecture
https://github.com/kata-containers/kata-containers/tree/main/docs/design
1. 整体架构
如下图:

kata-containers 的核心 binary 就2个:
- containerd-shim-kata-v2,对应源代码目录 src/runtime
- kata-agent,对应源码目录 src/agent
containerd 实现了 cri 协议,可以天然直接无缝对接到 kublet 上,而 containerd-shim-kata-v2 实现了 containerd 的 shim-v2 协议,直接实现了对 kata-agent 以及 hypervisor 通讯的封装。
2. 内核
xx
3. kata-agent
xx
4. runtime
kata的runtime其实有2个,一个是kata-runtime,一个就是containerd-shim-kata-v2
4.1. kata-runtime
kata-runtime是kata 1.0时代的产物,按照老的架构文档:https://github.com/kata-containers/documentation/blob/master/design/architecture.md,kata-runtime是一个标准的 OCI 实现,但是 kata 2.0 之后,架构发生了很大的变化,kata-runtime 的核心能力被裁剪掉了,逐渐的只剩下 kata-check 功能了

4.2. containerd-shim-kata-v2
containerd-shim-kata-v2 是一个实现了 containerd shim 协议的组件,向上直接对接 containerd,向下和 hypervisor 以及 kata-agent 通信以实现完整的 oci 能力
containerd-shim-kata-v2 的 main 文件在 src/runtime/cmd/containerd-shim-kata-v2/main.go
架构如下:

4.2.1. containerd-shim
containerd-shim-kata-v2 实现了 containerd 定义的 shim 接口,shim 的接口其实是和 cri 非常类似的,只不过是做了一层协议的 proxy,本质上并没有区别
containerd 会将接收到的所有 cri (来自kublet)请求,转换成对 containerd-shim-kata-v2 的 TaskService 调用
详细定义可以参考这个:https://github.com/containerd/containerd/tree/main/runtime/v2
// https://raw.githubusercontent.com/containerd/containerd/master/api/runtime/task/v2/shim_ttrpc.pb.go
type TaskService interface {
    State(context.Context, *StateRequest) (*StateResponse, error)
    Create(context.Context, *CreateTaskRequest) (*CreateTaskResponse, error)
    Start(context.Context, *StartRequest) (*StartResponse, error)
    Delete(context.Context, *DeleteRequest) (*DeleteResponse, error)
    Pids(context.Context, *PidsRequest) (*PidsResponse, error)
    Pause(context.Context, *PauseRequest) (*emptypb.Empty, error)
    Resume(context.Context, *ResumeRequest) (*emptypb.Empty, error)
    Checkpoint(context.Context, *CheckpointTaskRequest) (*emptypb.Empty, error)
    Kill(context.Context, *KillRequest) (*emptypb.Empty, error)
    Exec(context.Context, *ExecProcessRequest) (*emptypb.Empty, error)
    ResizePty(context.Context, *ResizePtyRequest) (*emptypb.Empty, error)
    CloseIO(context.Context, *CloseIORequest) (*emptypb.Empty, error)
    Update(context.Context, *UpdateTaskRequest) (*emptypb.Empty, error)
    Wait(context.Context, *WaitRequest) (*WaitResponse, error)
    Stats(context.Context, *StatsRequest) (*StatsResponse, error)
    Connect(context.Context, *ConnectRequest) (*ConnectResponse, error)
    Shutdown(context.Context, *ShutdownRequest) (*emptypb.Empty, error)
}
// https://github.com/containerd/containerd/blob/8167751f569c9374a6c3ef5b39a42629577a543a/runtime/v2/shim/shim.go#L72
// Shim server interface
// TODO(2.0): Remove unified shim interface
type Shim interface {
    shimapi.TaskService
    Cleanup(ctx context.Context) (*shimapi.DeleteResponse, error)
    StartShim(ctx context.Context, opts StartOpts) (string, error)
}
明显可以看出,TaskService 接口基本定义了 oci 所依赖的基本能力
比如 container 的 create 接口,所有的 container 元数据,会全部封装到 options 字段里面
message CreateTaskRequest {
    string id = 1;
    ...
    google.protobuf.Any options = 10;
}
问题:containerd-shim-kata-v2 为什么不直接对接到 cri 协议?既然接口是类似的,那么如果实现了 cri 协议,那是不是可以直接对接到 kublet 了,连 containerd 都省掉了
答:containerd 本身除了调用 oci 完成容器的创建和删除,还实现了容器的生命周期管理,还支持多种 oci-runtime。这些能力都是通用的,并且是containerd-shim-kata-v2 当前没有实现的,因此没有必要重复造轮子
4.2.2. virtcontainer
virtcontainer 提供了一个统一的 hypervisor 抽象层,为了能够对接不同的vmm,比如qemu,firecracker,同时提供容器的操作原语
因为vmm的实现都是类似的,比如尽管 firecracker 也是一种轻量级的vmm,但本质还是kvm虚拟化的实现(核心的kvm虚拟化能力是由内核提供的,firecracker 只是一个轻量级的壳)
4.2.2.1. Sandbox
由于 kata-container 的所有容器操作,都是在虚机里实现的。因此 Sandbox 是 virtcontainer 的核心,基本上所有的 virtcontainer 对外接口都是由 Sandbox 来完成的
Sandbox 的 API 如下,基本包含了
- 完整的容器操作接口。(不过容器的相关操作实现,在 Container 这个类里面,下面会讲到)
- 以及部分 hypervisor 的接口(正常来说,hypervisor 类的操作应该隐藏在容器的 API 之下)
创建 Sandbox:newSandbox()
删除 Sandbox:s.Delete()
容器类接口:
func (s *Sandbox) GetAllContainers() []VCContainer func (s *Sandbox) GetContainer(containerID string) VCContainer func (s *Sandbox) WaitProcess(ctx context.Context, containerID, processID string) (int32, error) func (s *Sandbox) SignalProcess(ctx context.Context, containerID, processID string, signal syscall.Signal, all bool) error func (s *Sandbox) WinsizeProcess(ctx context.Context, containerID, processID string, height, width uint32) error func (s *Sandbox) IOStream(containerID, processID string) (io.WriteCloser, io.Reader, io.Reader, error) func (s *Sandbox) CreateContainer(ctx context.Context, contConfig ContainerConfig) (VCContainer, error) func (s *Sandbox) StartContainer(ctx context.Context, containerID string) (VCContainer, error) func (s *Sandbox) StopContainer(ctx context.Context, containerID string, force bool) (VCContainer, error) func (s *Sandbox) KillContainer(ctx context.Context, containerID string, signal syscall.Signal, all bool) error func (s *Sandbox) DeleteContainer(ctx context.Context, containerID string) (VCContainer, error) func (s *Sandbox) StatusContainer(containerID string) (ContainerStatus, error) func (s *Sandbox) EnterContainer(ctx context.Context, containerID string, cmd types.Cmd) (VCContainer, *Process, error) func (s *Sandbox) UpdateContainer(ctx context.Context, containerID string, resources specs.LinuxResources) error func (s *Sandbox) StatsContainer(ctx context.Context, containerID string) (ContainerStats, error) func (s *Sandbox) PauseContainer(ctx context.Context, containerID string) error func (s *Sandbox) ResumeContainer(ctx context.Context, containerID string) error func (s *Sandbox) Status() SandboxStatus
虚机类的接口,主要是一些设备的热插拔:
func (s *Sandbox) ID() string func (s *Sandbox) GetHypervisorPid() (int, error) func (s *Sandbox) AddInterface(ctx context.Context, inf *pbTypes.Interface) (*pbTypes.Interface, error) func (s *Sandbox) RemoveInterface(ctx context.Context, inf *pbTypes.Interface) (*pbTypes.Interface, error) func (s *Sandbox) ListInterfaces(ctx context.Context) ([]*pbTypes.Interface, error) func (s *Sandbox) UpdateRoutes(ctx context.Context, routes []*pbTypes.Route) ([]*pbTypes.Route, error) func (s *Sandbox) ListRoutes(ctx context.Context) ([]*pbTypes.Route, error) func (s *Sandbox) HotplugAddDevice(ctx context.Context, device api.Device, devType config.DeviceType) error func (s *Sandbox) HotplugRemoveDevice(ctx context.Context, device api.Device, devType config.DeviceType) error func (s *Sandbox) GetAndSetSandboxBlockIndex() (int, error) func (s *Sandbox) UnsetSandboxBlockIndex(index int) error func (s *Sandbox) AppendDevice(ctx context.Context, device api.Device) error func (s *Sandbox) AddDevice(ctx context.Context, info config.DeviceInfo) (api.Device, error) func (s *Sandbox) GetHypervisorType() string
4.2.2.2. Container
Container 类是 virtcontainer 里的第二个关键数据结构。所有 Sandbox 和容器相关的操作,基本都由 Container 结构来完成
以创建容器为例
Sandbox代码:
// CreateContainer creates a new container in the sandbox
// This should be called only when the sandbox is already created.
// It will add new container config to sandbox.config.Containers
func (s *Sandbox) CreateContainer(ctx context.Context, contConfig ContainerConfig) (VCContainer, error) {
    // Update sandbox config to include the new container's config
    s.config.Containers = append(s.config.Containers, contConfig)
    var err error
    // Create the container object, add devices to the sandbox's device-manager:
    c, err := newContainer(ctx, s, &s.config.Containers[len(s.config.Containers)-1])
    if err != nil {
        return nil, err
    }
    // create and start the container
    if err = c.create(ctx); err != nil {
        return nil, err
    }
    // Add the container to the containers list in the sandbox.
    if err = s.addContainer(c); err != nil {
        return nil, err
    }
    // ...
}
而 c.create() 的实现就比较详细了。(但!!,后面讲 kataAgent 再说)
4.2.2.3. kataAgent
virtcontainer 的 kataAgent 是一个 kata agent 的 proxy,因为真正的 kata-agent 代码是在 src/agent 目录下的,是一个独立运行在每个 vm 内的一个进程,所有容器的操作最终都会由 vm 内的 kata-agent 来完成。
因此,为了实现9和 vm 内的 kata-agent 通信,virtcontainer 实现了一个简单的 grpc 代理,叫 kataAgent
上面我们看到的所有 Sandbox 定义里容器相关的操作,最终都会转换成对 kata-agent 的 sendReq grpc 调用
func (k *kataAgent) createContainer(ctx context.Context, sandbox *Sandbox, c *Container) (p *Process, err error) {
    k.buildContainerRootfs(ctx, sandbox, c, rootPathParent)
    c.mountSharedDirMounts(ctx, sharedDirMounts, ignoredMounts)
    k.handleShm(ociSpec.Mounts, sandbox)
    k.handleEphemeralStorage(ociSpec.Mounts)
    k.handleLocalStorage(ociSpec.Mounts, sandbox.id, c.rootfsSuffix)
    k.replaceOCIMountSource(ociSpec, sharedDirMounts)
    k.removeIgnoredOCIMount(ociSpec, ignoredMounts)
    k.appendDevices(ctrDevices, c)
    k.handleBlockVolumes(c)
    k.replaceOCIMountsForStorages(ociSpec, volumeStorages)
    k.constrainGRPCSpec(grpcSpec, passSeccomp, sandbox.config.VfioMode == config.VFIOModeGuestKernel)
    req := &grpc.CreateContainerRequest{
        ContainerId:  c.id,
        ExecId:       c.id,
        Storages:     ctrStorages,
        Devices:      ctrDevices,
        OCI:          grpcSpec,
        SandboxPidns: sharedPidNs,
    }
    if _, err = k.sendReq(ctx, req); err != nil {
        return nil, err
    }
}
笔者注:不过细看 kataAgent 的实现的时候,我觉得 kataAgent 和 Container 类的边界不是很清晰,kataAgent 其实做了很多 Container 负责的工作,比如构建容器的 rootfs 之类的,这些实现其实可以完全移出来,放到 Container 类里面
4.3. Hypervisor
kata-containers 目前支持 2种hypervisor,一种是最常见的qemu,一种是fc(firecracker)
Hypervisor 类是一个 interface{},定义了要实现一个完整的 vmm,所必要的全部接口能力。不管是fc还是qemu,都需要实现这些接口
贴几个最核心的API
// hypervisor is the virtcontainers hypervisor interface.
// The default hypervisor implementation is Qemu.
type Hypervisor interface {
        // 创建、启动、停止    
    CreateVM(ctx context.Context, id string, networkNS NetworkNamespace, hypervisorConfig *HypervisorConfig) error
    StartVM(ctx context.Context, timeout int) error
    StopVM(ctx context.Context, waitOnly bool) error
        // 暂停、恢复              
    PauseVM(ctx context.Context) error
    ResumeVM(ctx context.Context) error
        // 设备热插拔         
    AddDevice(ctx context.Context, devInfo interface{}, devType DeviceType) error
    HotplugAddDevice(ctx context.Context, devInfo interface{}, devType DeviceType) (interface{}, error)
    HotplugRemoveDevice(ctx context.Context, devInfo interface{}, devType DeviceType) (interface{}, error)
    ResizeMemory(ctx context.Context, memMB uint32, memoryBlockSizeMB uint32, probe bool) (uint32, MemoryDevice, error)
    ResizeVCPUs(ctx context.Context, vcpus uint32) (uint32, uint32, error)
    // generate the socket to communicate the host and guest
    GenerateSocket(id string) (interface{}, error)
}
4.3.1. qemu
qemu 是最常见的 hypervisor,qemu 相关的代码全部在 virtcontainers/qemu*.go 代码里
不过这里的 qemu 只是一个壳,是对原生 qemu 组件的一个 rpc 封装,不真的去写一坨虚拟化相关的代码。启停虚机直接调的是 qemu-system-x86_64 二进制工具,设备热插拔是通过 qmp 协议和 qemu-system-x86_64 进程通信来完成的
qemu-system-x86_64 是 qemu 在 64 位架构上的实现,也是使用最广泛的。kata还支持 amd64 arm64 s390 等其他常见的硬件平台
qemu 结构:
// qemu is an Hypervisor interface implementation for the Linux qemu hypervisor.
// nolint: govet
type qemu struct {
    arch qemuArch
    virtiofsd Virtiofsd 
    store persistapi.PersistDriver 
    state QemuState
    qmpMonitorCh qmpChannel
}
核心就那么几个:
- qemuArch:前面提到 kata-containers 支持多种体系结构,x86 x64 arm s390。但是绝大部分体系架构上的虚拟化实现是一样的,只有一些细微差别
- virtiofsd:我们说的传统意义上的虚拟化,其实说的是计算(cpu、内存)的虚拟化。qemu本身不实现 io 的虚拟化,需要依赖 virtiofsd 来完成 io 的虚拟化。虚机内所有磁盘设备的io操作,都会转发给virtiofsd来完成真正意义上的落盘操作
- PersistDriver:持久化,保存了qemu虚机相关的所有元数据:配置,container信息,设备信息,网络信息,等等
4.3.1. firecracker
fc是aws开源的,轻量级的虚拟化实现。基本上实现了对qemu的深度裁剪,并用rust语言重写了。
fc的结构如下:
// firecracker is an Hypervisor interface implementation for the firecracker VMM.
type firecracker struct {
    pendingDevices []firecrackerDevice // Devices to be added before the FC VM ready
    firecrackerd *exec.Cmd           //Tracks the firecracker process itself
    fcConfig     *types.FcConfig     // Parameters configured before VM starts
    connection   *client.Firecracker //Tracks the current active connection
    
}
这里的 firecrackerd 类似于 qemu_system_x86_64,每个 vm 都会起一个 firecrackerd 进程。这里的 client.Firecracker 类似于 qemu 的 qmp 通信
4.3. kata-monitor
xx
5. 容器网络
xx
6. 存储
xx