Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Deep-dive KubeVirt

Takuya TAKAHASHI
October 24, 2019
1.5k

Deep-dive KubeVirt

k8s を compute resource pool として利用可能にする KubeVirt について、アーキテクチャや仮想マシンを実現する技術について発表しました。
Kubernetes Meetup Tokyo #24 にて発表した内容です

Takuya TAKAHASHI

October 24, 2019
Tweet

Transcript

  1. Deep-Dive kubeVirt
    Takuya TAKAHASHI @ GMO Pepabo, Inc.
    twitter: @takutaka1220
    github: https://github.com/takutakahashi
    1

    View full-size slide

  2. アジェンダ
    概要
    kubevirt とは?
    deep dive Controller
    deep dive Virtual Machine
    compute
    network
    storage は間に合いませんでした
    2

    View full-size slide

  3. kubevirt とは?
    kubernetes (k8s) ノード上に仮想マシンを起動できるエクステンション
    k8s エコシステムにVMをシームレスに参加させることができる
    Auto-healing
    svc によるサービスディスカバリ
    pvc による動的ストレージプロビジョニング
    ingress による外部到達
    4

    View full-size slide

  4. Deep Dive Controller
    5

    View full-size slide

  5. Deep Dive Controller
    source commit: 01df7d485ddb4191395c2cfa4b38545999bd0fa7
    kubevirt: v0.22.0
    k8s: v1.16.0
    スライドの巻末に Source Code Reference を載せてあります
    スライド本文の [1] などの表示と対応しています
    どのファイルの、どの関数に処理が入っているのか書いてあります
    コードを追いたくなったらそちらを参照してください
    6

    View full-size slide

  6. 構成要素
    主に3つのコンポーネントで構成される
    virt-controller ... deployment
    CRD や関連リソースの watch
    virt-handler ... daemonset
    controller から処理を受け取り launcher に流す
    virt-launcher ... pod
    libvirtd 経由で実際に VM を操る、Network の設定をするプロセス
    7

    View full-size slide

  7. virt-controller
    k8s Resource の変更操作を行う
    CRDs, node の watch, API request を受ける
    変更を検知し、 virt-handler に処理を受け渡す
    CRD を watch する
    VirtualMachine (vm)
    仮想マシンの定義を保持
    VirtualMachineInterface (vmi)
    仮想マシンの状態を保持
    他にも node, evacuation, migration などを watch する
    8

    View full-size slide

  8. virt-controller
    vm の変更を検知すると...
    必要に応じて vmi を作成したり,状態を変更する
    作業を vmi-controller に委譲する [1]
    vmi の作成を伴わない場合、vm の Status を変えるだけ
    9

    View full-size slide

  9. virt-controller
    vm の変更を検知すると...
    vmInformer が、virt-handler との shared queue に処理を enqueue する [2] [3]
    c.vmiVMInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
    AddFunc: c.addVm, // 内部では enqueueVm を呼ぶだけ
    DeleteFunc: c.deleteVm,
    UpdateFunc: c.updateVm,
    })
    func (c *VMController) enqueueVm(obj interface{}) {
    vm := obj.(*virtv1.VirtualMachine)
    key, err := controller.KeyFunc(vm)
    if err != nil {
    logger.Object(vm).Reason(err).Error("Failed to extract vmKey from VirtualMachine.")
    }
    c.Queue.Add(key)
    }
    10

    View full-size slide

  10. virt-controller
    vmi の変更を検知すると...
    vm pod の状態を取得して,変更に応じて pod を作る[4]
    func (c *VMIController) sync(vmi *virtv1.VirtualMachineInstance, pod *k8sv1....snip...) {
    ...snip...
    templatePod, err := c.templateService.RenderLaunchManifest(vmi)
    ...snip...
    vmiKey := controller.VirtualMachineKey(vmi)
    c.podExpectations.ExpectCreations(vmiKey, 1)
    pod, err := c.clientset.CoreV1().Pods(vmi.GetNamespace()).Create(templatePod)
    vmiInformer が、virt-handler との shared queue に処理を enqueue する
    11

    View full-size slide

  11. virt-handler
    12

    View full-size slide

  12. virt-handler
    controller からのリクエストを受け, 各種リソース作成をハンドリングする
    後述の virt-launcher に low-level な rpc を送る
    /metrics を動かしたり[6]
    コンソールをハンドリングするサーバを動かしたり[6]
    13

    View full-size slide

  13. virt-handler 処理のながれ
    Queue を pop し、virt-launcher に low-level な処理を投げる[7]
    launcher と通信するための Client を作成する[8]
    func (d *VirtualMachineController) processVmUpdate(origVMI *v1.VirtualMachineInstance) error {
    vmi := origVMI.DeepCopy()
    client, err := d.getLauncherClient(vmi) // grpc client
    if err != nil {
    return fmt.Errorf("unable to create virt-launcher client connection: %v", err)
    }
    err = client.SyncVirtualMachine(vmi, options) // rpc to virt-launcher
    }
    hostPath: virt-share-dir に virt-launcher が sock をつくり、grpc で通信する
    [9]
    14

    View full-size slide

  14. virt-launcher
    VM が起動するコンテナの pid 1 を担うプロセス
    libvirtd の起動と操作を担当する
    ホスト名や nic interface の書き換えなども行う
    15

    View full-size slide

  15. VM起動までのフロー
    vm01 という名前のVMを作成する例
    kubectl create -f vm01.yaml
    virtctl start vm01
    16

    View full-size slide

  16. VM起動までのフロー
    kubectl create -f vm01.yaml
    vm-controller が VM の Status を見て,どうするか決める
    vmi があるなら状態を合わせたり
    vmi がないならそのまま
    vmInformer に enqueue される
    virt-handler が dequeue するも,
    Status 上では vm は STOP status であるためためなにもしない
    17

    View full-size slide

  17. VM起動までのフロー
    virtctl start vm01
    virtctl が vmi 作成の API リクエストを controller に送る
    vmi-controller が virt-launcher を起動させる Pod リソースを作成する
    func (c *VMIController) sync(vmi *virtv1.VirtualMachineInstance, pod *k8sv1.Pod, dataVolumes []*cdiv1.DataVolume) (err syncError) {
    if !podExists(pod) {
    templatePod, err := c.templateService.RenderLaunchManifest(vmi)
    if _, ok := err.(services.PvcNotFoundError); ok {
    return &syncErrorImpl{fmt.Errorf("failed to render launch manifest: %v", err), FailedPvcNotFoundReason}
    } else if err != nil {
    return &syncErrorImpl{fmt.Errorf("failed to render launch manifest: %v", err), FailedCreatePodReason}
    }
    vmiKey := controller.VirtualMachineKey(vmi)
    pod, err := c.clientset.CoreV1().Pods(vmi.GetNamespace()).Create(templatePod)
    18

    View full-size slide

  18. VM起動までのフロー
    Pod が起動した NodeName を vmi に登録する
    func (c *VMIController) updateStatus pkg/virt-controller/watch/vmi.go
    if isPodReady(pod) && vmi.DeletionTimestamp == nil {
    vmiCopy.ObjectMeta.Labels[virtv1.NodeNameLabel] = pod.Spec.NodeName
    vmiCopy.Status.NodeName = pod.Spec.NodeName
    19

    View full-size slide

  19. VM 起動までのフロー
    virt-handler が起動した Pod の IP, MAC Address 等を vmi に登録する
    func (d *VirtualMachineController) updateVMIStatus pkg/virt-handler/vm.go
    for _, domainInterface := range domain.Spec.Devices.Interfaces {
    interfaceMAC := domainInterface.MAC.MAC
    var newInterface v1.VirtualMachineInstanceNetworkInterface
    ...snip...
    } else {
    newInterface = v1.VirtualMachineInstanceNetworkInterface{
    MAC: interfaceMAC,
    Name: domainInterface.Alias.Name,
    }
    }
    20

    View full-size slide

  20. VM 起動までのフロー
    virt-handler が起動した Pod の IP, MAC Address 等を vmi に登録する
    func (d *VirtualMachineController) updateVMIStatus pkg/virt-handler/vm.go
    ...snip...
    for interfaceMAC, domainInterfaceStatus := range domainInterfaceStatusByMac {
    newInterface := v1.VirtualMachineInstanceNetworkInterface{
    Name: domainInterfaceStatus.Name,
    MAC: interfaceMAC,
    IP: domainInterfaceStatus.Ip,
    IPs: domainInterfaceStatus.IPs,
    InterfaceName: domainInterfaceStatus.InterfaceName,
    }
    newInterfaces = append(newInterfaces, newInterface)
    }
    vmi.Status.Interfaces = newInterfaces
    }
    ...snip...
    21

    View full-size slide

  21. VM 起動までのフロー
    vmiInformer が enqueue & virt-handler が dequeue
    virt-handler は,該当 pod の socket に rpc する
    virt-launcher が VMプロセスを起動させる
    22

    View full-size slide

  22. VM 起動までのフロー
    func (l *Launcher) SyncVirtualMachine(ctx context.Context, request *cmdv1.VMIRequest) (*cmdv1.Response, error) {
    vmi, response := getVMIFromRequest(request.Vmi)
    if !response.Success {
    return response, nil
    }
    if _, err := l.domainManager.SyncVMI(vmi, l.useEmulation, request.Options); err != nil {
    log.Log.Object(vmi).Reason(err).Errorf("Failed to sync vmi")
    response.Success = false
    response.Message = getErrorMessage(err)
    return response, nil
    }
    log.Log.Object(vmi).Info("Synced vmi")
    return response, nil
    }
    23

    View full-size slide

  23. VM 起動までのフロー
    func (l *LibvirtDomainManager) SyncVMI(vmi *v1.VirtualMachineInstance, ...snip...){
    ...snip...
    domain := &api.Domain{}
    ...snip...
    // vmi.Spec.Domain を取り出す
    dom, err := l.virConn.LookupDomainByName(domain.Spec.Name)
    domState, _, err := dom.GetState()
    if cli.IsDown(domState) && !vmi.IsRunning() && !vmi.IsFinal() {
    err = dom.Create()
    } else if cli.IsPaused(domState) {
    err := dom.Resume()
    } else {
    // Nothing to do
    }
    24

    View full-size slide

  24. Deep Dive Virtual Machine
    25

    View full-size slide

  25. Deep Dive Virtual Machine
    VM が Pod の中でどのように動作しているかを追います.
    compute
    network
    26

    View full-size slide

  26. Compute
    CPUやメモリなど ... QEMU + kvm + libvirt
    仮想化構成技術のデファクト
    競合製品でできるだいたいの構成を取ることができる
    CPU Pinning[9], SR-IOV[10], GPU Instance[10], etc...
    Hyper-V にも対応する
    start, stop, suspend, resume, migrate すべて libvirt の機能
    container の中で VM プロセスが実行されるため、 CRI の制限がない
    Host kernel を共有するもののほうが効率がいい
    28

    View full-size slide

  27. Compute
    Pod は安全か?
    securityContext は絞りぎみ
    securityContext:
    capabilities:
    add:
    - NET_ADMIN
    - SYS_NICE
    privileged: false
    runAsUser: 0
    vmi のリクエストに応じて,動的に capabilities を増減させる仕様[11]
    29

    View full-size slide

  28. Network
    VM が外部到達性をどう担保しているか,
    Service Discovery からどう見えるのか追います.
    31

    View full-size slide

  29. Network
    Bridge, Masquerade, SLiRP が選択可能
    yaml に宣言
    32

    View full-size slide

  30. Bridge 接続
    Pod 内部で bridge を作成する[12]
    Pod interface の MAC Address, IP を適当なものに書き換える[12]
    この時点で Pod 自身は外部通信できなくなる
    L3 到達可能な情報を積んだ DHCP Server を起動する[12]
    Pod の MAC Address を持った VM を起動する
    VM は起動時に DHCP でネットワーク設定を行う
    完了
    33

    View full-size slide

  31. 実際の Pod と VM で確認
    34

    View full-size slide

  32. Pod の IP
    kubectl get pod -o wide の結果
    NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
    virt-launcher-testvm-62vxl 2/2 Running 0 123m 10.244.0.40 kvm
    35

    View full-size slide

  33. vmi の interface 定義
    vmi の定義では,interface は以下
    起動した Pod が Status を Update する
    interfaces:
    - ipAddress: 10.244.0.40
    mac: b6:9d:5c:92:cd:3d
    name: default
    36

    View full-size slide

  34. Pod 内部の Network info
    kubectl exec で中に入り確認する
    k6t-eth0 という名前の Bridge が作成される
    sh-5.0# ip a
    ...snip...
    4: k6t-eth0: mtu 1450 qdisc noqueue state UP group default
    link/ether b6:9d:5c:cd:f6:84 brd ff:ff:ff:ff:ff:ff ← フェイク MAC
    inet 169.254.75.10/32 brd 169.254.75.10 scope global k6t-eth0 ← フェイク IP
    valid_lft forever preferred_lft forever
    5: vnet0: mtu 1450 qdisc fq_codel master k6t-eth0 state UNKNOWN group default qlen 1000
    link/ether fe:9d:5c:92:cd:3d brd ff:ff:ff:ff:ff:ff ← vmi の MAC
    37

    View full-size slide

  35. domain.xml (VM の構成ファイル)
    vnet0 という名前の tap device を k6t-eth0 から作成するように記述

    ← vmi の MAC







    38

    View full-size slide

  36. VM 内部の Network info
    virtctl console でログインし,確認する
    Pod IP と MAC Address がついている
    $ ip a
    ...snip...
    2: eth0: mtu 1450 qdisc pfifo_fast qlen 1000
    link/ether b6:9d:5c:92:cd:3d brd ff:ff:ff:ff:ff:ff
    inet 10.244.0.40/24 brd 10.244.0.255 scope global eth0
    valid_lft forever preferred_lft forever
    inet6 fe80::b49d:5cff:fe92:cd3d/64 scope link tentative flags 08
    valid_lft forever preferred_lft forever
    39

    View full-size slide

  37. Compute おまけ
    40

    View full-size slide

  38. CPU Cores
    Core の数を指定しなかった場合は,ResourceLimit などから計算される[20]
    resources := vmi.Spec.Domain.Resources
    if cpuLimit, ok := resources.Limits[k8sv1.ResourceCPU]; ok {
    sockets = uint32(cpuLimit.Value())
    } else if cpuRequests, ok := resources.Requests[k8sv1.ResourceCPU]; ok {
    sockets = uint32(cpuRequests.Value())
    }
    }
    41

    View full-size slide

  39. CPU Mode
    CPU Mode はデフォルトでホストのものを利用するため注意[19]
    if vmi.Spec.Domain.CPU == nil || vmi.Spec.Domain.CPU.Model == "" {
    domain.Spec.CPU.Mode = v1.CPUModeHostModel
    }
    42

    View full-size slide

  40. PCI Device への対応
    GPU は HostDevice に抽象化して DomainSpec に書き込まれる[19]
    HostDevice に書き込めば,任意の PCI Device を利用可能となるはず
    if util.IsGPUVMI(vmi) {
    vgpuMdevUUID := append([]string{}, c.VgpuDevices...)
    hostDevices, err := createHostDevicesFromMdevUUIDList(vgpuMdevUUID)
    if err != nil {
    log.Log.Reason(err).Error("Unable to parse Mdev UUID addresses")
    } else {
    domain.Spec.Devices.HostDevices = append(domain.Spec.Devices.HostDevices, hostDevices...)
    }
    gpuPCIAddresses := append([]string{}, c.GpuDevices...)
    hostDevices, err = createHostDevicesFromPCIAddresses(gpuPCIAddresses)
    if err != nil {
    log.Log.Reason(err).Error("Unable to parse PCI addresses")
    } else {
    domain.Spec.Devices.HostDevices = append(domain.Spec.Devices.HostDevices, hostDevices...)
    }
    }
    43

    View full-size slide

  41. Network おまけ
    44

    View full-size slide

  42. Firewall
    vmi.Spec.Domain.Devices.Interfaces[].Ports を指定することで,
    ContainerPort による FW を定義可能[13][14]
    45

    View full-size slide

  43. SR-IOV Device の追加と利用
    PCI Device として接続して,後はすべて対向の設定に任せる
    if iface.SRIOV != nil {
    ...snip...
    hostDev := HostDevice{
    Source: HostDeviceSource{
    Address: &Address{
    ..snip...
    },
    },
    Type: "pci",
    Managed: "yes",
    }
    ...snip...
    log.Log.Infof("SR-IOV PCI device allocated: %s", pciAddr)
    domain.Spec.Devices.HostDevices = append(domain.Spec.Devices.HostDevices, hostDev)
    46

    View full-size slide

  44. PodNetwork 以外への参加
    マルチインターフェースを実現するための機能拡張が複数用意されている[16][17]
    MultusNetwork
    Intel が進める,baremetal k8s + NFV を可能とするプロジェクト
    複数の Interface を Pod にアタッチできる
    GenieNetwork
    複数のネットワーク実装を同時に扱えるようにする CNI プラグイン
    こちらも複数の Interface を Pod にアタッチできる
    47

    View full-size slide

  45. MultusNetwork への参加
    定義されていた場合,Pod に Annotation をつける[18]
    if network.Multus != nil && network.Multus.Default {
    annotationsList[MULTUS_DEFAULT_NETWORK_CNI_ANNOTATION] = network.Multus.NetworkName
    }
    }
    48

    View full-size slide

  46. MultusNetwork への参加
    定義されていた場合,Pod に Annotation をつける[18]
    namespace, networkName := getNamespaceAndNetworkName(vmi, network.Multus.NetworkName)
    ifaceMap := map[string]string{
    "name": networkName,
    "namespace": namespace,
    "interface": fmt.Sprintf("net%d", next_idx+1),
    }
    iface := getIfaceByName(vmi, network.Name)
    if iface != nil && iface.MacAddress != "" {
    ifaceMap["mac"] = iface.MacAddress
    }
    next_idx = next_idx + 1
    ifaceListMap = append(ifaceListMap, ifaceMap)
    ...snip...
    ifaceJsonString, err := json.Marshal(ifaceListMap)
    cniAnnotations[MultusNetworksAnnotation] = fmt.Sprintf("%s", ifaceJsonString)
    49

    View full-size slide

  47. MultusNetwork への参加
    iface を libvirt domain につける[19]
    if value, ok := cniNetworks[iface.Name]; ok {
    prefix := ""
    if net.Multus != nil {
    if net.Multus.Default {
    prefix = "eth"
    } else {
    prefix = "net"
    }
    ...snip...
    domainIface.Source = InterfaceSource{
    Bridge: fmt.Sprintf("k6t-%s%d", prefix, value),
    }
    50

    View full-size slide

  48. GenieNetwork への参加
    MultusNetwork への参加とだいたい同じことをしている
    51

    View full-size slide

  49. ソースコードリファレンス
    [1] func (c *VMController) execute pkg/virt-controller/watch/vm.go
    [2] func (c *VMController) addVirtualMachine pkg/virt-controller/watch/vm.go
    [3] func (c *VMController) enqueueVm pkg/virt-controller/watch/vm.go
    [4] func (c *VMIController) sync
    [5] func (c *VMIController) updateStatus
    [6] func (app *virtHandlerApp) Run cmd/virt-handler/virt-handler.go
    [8] func (d *VirtualMachineController) getLauncherClient
    52

    View full-size slide

  50. ソースコードリファレンス
    [11] func getRequiredCapabilities pkg/virt-controller/services/template.go
    [12] func (b *BridgePodInterface) preparePodNetworkInterfaces pkg/virt-
    launcher/virtwrap/network/podinterface.go
    [13] func getPortsFromVMI pkg/virt-controller/services/template.go
    [14] type Port struct staging/src/kubevirt.io/client-go/api/v1/schema.go
    [15] func (l *LibvirtDomainManager) preStartHook pkg/virt-
    launcher/virtwrap/manager.go
    [16] type NetworkSource struct staging/src/kubevirt.io/client-go/api/v1/schema.go
    [17] func (t *templateService) RenderLaunchManifest pkg/virt-
    controller/services/template.go
    [18] func getCniAnnotations pkg/virt-controller/services/template.go
    [19] func Convert_v1_VirtualMachine_To_api_Domain pkg/virt-
    launcher/virtwrap/api/converter.go
    [20] func getCPUTopology pkg/virt-launcher/virtwrap/api/converter.go 53

    View full-size slide

  51. ソースコードリファレンス
    [21] func (l *LibvirtDomainManager) asyncMigrate pkg/virt-
    launcher/virtwrap/manager.go
    54

    View full-size slide