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