vm template 原理浅析

vm template 是一种加速虚机创建、节省vm内存的技术。

1. 背景

我们之所以说虚机是强隔离的,主要是因为虚机有独立的内核。不同vm之间的进程是完全运行在自己的内核空间里面的,相互不可见
引起虚机的启动,需要引导一个完整的内核,以及完整的os
这在传统的虚机场景下,问题不大,一个物理机上启动的vm数量可能就十几个
但是容器场景下,kata-containers,容器粒度要远小于虚机,一个物理机可能启动上百个kata容器,每个kata容器都有自己独立的内核,这样算来,开销就不小了
由于每个kata容器的内核,操作系统镜像,都是相同的。如果虚机之间的内核、操作系统镜像所占用的内存能够共享,这样就能省掉不少内存。
于是,vm template 技术出现了

2. vm template 原理

主要是利用了linux内核fork系统的cow原理(copy on write)
cow:fork一个新的进程,会把原进程的内存空间全部copy一份,但这个copy只是一个引用。只有新进程在写这块内存区域的时候,才会发生真正的copy操作
所以 vm template 的核心思路是:通过一个事先创建好的最小 factory-vm,包含公共的内核、操作系统镜像、以及kata-agent。创建kata容器的时候,从factory-vm fork一个vm出来,然后再通过热插拔的方式,调整vm的规格以符合kata容器的要求
但是这里有一个问题是,template vm 的规格是固定的,但是 Pod 的规格不是固定的,所以必须通过 Pod vm 的热插拔 & resize 能力来实现 vm 规格的调整

3. 系统架构

补充系统流程图
0

3.1. template 是什么

template 本质上是一个内存快照,qemu 可以通过一个内存快照直接拉起一个 vm
当 vm template 特性打开时,qemu 的启动参数会多一行是这样的:
-object memory-backend-file,id=dimm1,size=1024M,mem-path=/run/vc/vm/template/memory
这个 /run/vc/vm/template/memory 就是我们说的 vm template

3.2. 打开 template 特性

打开 vm template 功能非常简单,只需要修改 kata 的以下配置即可
  • qemu-lite is specified in hypervisor.qemu-> path section
  • enable_template = true
  • initrd = is set
  • image = option is commented out or removed
另外,template 的规格是由 configuration.toml 里面的 default_vcpus、default_memory 参数决定

3.3. 创建 template

创建 template 有两种方式:
  1. 一种是 kata-runtime factory init:这个命令会自动把 template 创建出来
  2. 如果1没有执行,创建 sandbox 的时候如果发现 template 不存在,也会自动创建一个
template 的代码实现在 src/runtime/virtcontainers/factory/template/ 里
由于 kata 支持两种 vm 创建加速方案,一种是 VM cache,一种是 vm template,最终都抽象成了 factory

3.3.1. template 创建流程

以下从代码实现层面说明
函数调用栈如下:
containerd-shim-v2/create.go: create()
-> katautils/create.go: HandleFactory()
-> virtcontainers/factory/factory.go: NewFactory()
-> virtcontainers/factory/template/template.go: New() -> createTemplateVM()
NewFactory 的第三个参数是 fetchOnly,如果为 true,则会复用已存在的 template vm,如果不存在,会报错。如果 fetchOnly 为 false,则会直接 New 一个新的。
所以这个地方的代码逻辑很简单:先判断 template vm 是否存在,如果存在就直接复用,否则就 New 一个
    factoryConfig := vf.Config{
        Template:        runtimeConfig.FactoryConfig.Template,
        TemplatePath:    runtimeConfig.FactoryConfig.TemplatePath,
        VMCache:         runtimeConfig.FactoryConfig.VMCacheNumber > 0,
        VMCacheEndpoint: runtimeConfig.FactoryConfig.VMCacheEndpoint,
        VMConfig: vc.VMConfig{
            HypervisorType:   runtimeConfig.HypervisorType,
            HypervisorConfig: runtimeConfig.HypervisorConfig,
            AgentConfig:      runtimeConfig.AgentConfig,
        },
    }
     f, err := vf.NewFactory(ctx, factoryConfig, true)
    if err != nil && !factoryConfig.VMCache {
        kataUtilsLogger.WithError(err).Warn("load vm factory failed, about to create new one")
        f, err = vf.NewFactory(ctx, factoryConfig, false)
    }

真正 New 的时候,我们直接看 template.createTemplateVM() 的实现
从流程上来看,基本上就是 NewVM 去创建一个真正的 vm,创建完了之后,stop掉,通过 vm.Save() 得到内存快照,快照地址就是 MemoryPath,这个路径默认是 /run/vc/vm/template/
一旦得到这个内存快照之后,后续的所有 vm 都可以通过这个快照直接创建出来
func (t *template) createTemplateVM(ctx context.Context) error {
    // create the template vm
    config := t.config
    config.HypervisorConfig.BootToBeTemplate = true
    config.HypervisorConfig.BootFromTemplate = false
    config.HypervisorConfig.MemoryPath = t.statePath + "/memory"
    config.HypervisorConfig.DevicesStatePath = t.statePath + "/state"

    vm, err := vc.NewVM(ctx, config)  // 这个地方会创建一个 vm,并真正的 start 起来
    if err != nil {
        return err
    }
    defer vm.Stop(ctx)

    if err = vm.Disconnect(ctx); err != nil {
        return err
    }

    // Sleep a bit to let the agent grpc server clean up
    // When we close connection to the agent, it needs sometime to cleanup
    // and restart listening on the communication( serial or vsock) port.
    // That time can be saved if we sleep a bit to wait for the agent to
    // come around and start listening again. The sleep is only done when
    // creating new vm templates and saves time for every new vm that are
    // created from template, so it worth the invest.
    time.Sleep(templateWaitForAgent)

    if err = vm.Pause(ctx); err != nil {
        return err
    }

    if err = vm.Save(); err != nil {
        return err
    }
    return nil
}

3.3.2. template 规格

由上面代码我们可以看到创建 template 用的是 runtimeConfig.HypervisorConfig
而 runtimeConfig.HypervisorConfig 是通过读取 configuration.toml 生成的。
配置生成的函数调用栈:
loadRuntimeConfig() -> LoadConfiguration -> updateRuntimeConfig -> updateRuntimeConfigHypervisor -> newQemuHypervisorConfig
    return vc.HypervisorConfig{
        HypervisorPath:          hypervisor,
        HypervisorMachineType:   machineType,
        NumVCPUs:                h.defaultVCPUs(),   --> 对应 default vcpus 参数
        DefaultMaxVCPUs:         h.defaultMaxVCPUs(),
        MemorySize:              h.defaultMemSz(),   --> 对应 default memory 参数
        MemSlots:                h.defaultMemSlots(),
        MemOffset:               h.defaultMemOffset(),

3.3. 从 template 创建 sandbox

有了上述的 template 文件,通过 qemu 创建 vm 的时候,直接带上 -object memory-backend-file,id=dimm1,size=1024M,mem-path=/run/vc/vm/template/memory 参数即可
如下代码:
virtcontainers/qemu.go: CreateVM()
-> virtcontainers/qemu.go: setupTemplate()
func (q *qemu) setupTemplate(knobs *govmmQemu.Knobs, memory *govmmQemu.Memory) govmmQemu.Incoming {
    incoming := govmmQemu.Incoming{}

    if q.config.BootToBeTemplate || q.config.BootFromTemplate {
        knobs.FileBackedMem = true
        memory.Path = q.config.MemoryPath

        if q.config.BootToBeTemplate {
            knobs.MemShared = true
        }
        if q.config.BootFromTemplate {
            incoming.MigrationType = govmmQemu.MigrationDefer
        }
    }
    return incoming
}

3.4. 创建 container

runc里的sandbox其实是个pause容器,不占任何资源。基本上只是用来维护pod的namespace
而在kata里面,sandbox就是一个虚机,Pod内所有容器都在虚机内运行。
前面我们知道,sandbox 创建时是不知道 pod 内会起多少个容器的,因此,只会按照默认规格去启动 sandbox,等到真正需要创建 container 的时候,需要按照 container 规格去调大 vm 的规格
// 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)

    // 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
    }

    // Sandbox is responsible to update VM resources needed by Containers
    // Update resources after having added containers to the sandbox, since
    // container status is requiered to know if more resources should be added.
    if err = s.updateResources(ctx); err != nil {
        return nil, err
    }

    if err = s.cgroupsUpdate(ctx); err != nil {
        return nil, err
    }
    // ....

    return c, nil
}

不过这里我有个地方不明白的是,为啥不是先 updateResources,再 create Container 呢,如果 create Container 时,资源不足 oom 了怎么办??

4. 技术限制

4.1. virtio-fs

使用virtiofs 时,创建虚机时:-m 4G -object memory-backend-file,id=mem,size=4G,mem-path=/dev/shm,share=on -numa node,memdev=mem,会有一个share=on的选项要打开
但是如果用模板启动虚机时,会将share=on去掉。导致virtiofs冲突无法使用
目前 virtio9p 没有这个问题,不过9p性能差是出了名的
发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注