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. アジェンダ 概要 kubevirt とは? deep dive Controller deep dive Virtual

    Machine compute network storage は間に合いませんでした 2
  2. Deep Dive Controller source commit: 01df7d485ddb4191395c2cfa4b38545999bd0fa7 kubevirt: v0.22.0 k8s: v1.16.0

    スライドの巻末に Source Code Reference を載せてあります スライド本文の [1] などの表示と対応しています どのファイルの、どの関数に処理が入っているのか書いてあります コードを追いたくなったらそちらを参照してください 6
  3. 構成要素 主に3つのコンポーネントで構成される virt-controller ... deployment CRD や関連リソースの watch virt-handler ...

    daemonset controller から処理を受け取り launcher に流す virt-launcher ... pod libvirtd 経由で実際に VM を操る、Network の設定をするプロセス 7
  4. virt-controller k8s Resource の変更操作を行う CRDs, node の watch, API request

    を受ける 変更を検知し、 virt-handler に処理を受け渡す CRD を watch する VirtualMachine (vm) 仮想マシンの定義を保持 VirtualMachineInterface (vmi) 仮想マシンの状態を保持 他にも node, evacuation, migration などを watch する 8
  5. 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
  6. 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
  7. virt-handler controller からのリクエストを受け, 各種リソース作成をハンドリングする 後述の virt-launcher に low-level な rpc

    を送る /metrics を動かしたり[6] コンソールをハンドリングするサーバを動かしたり[6] 13
  8. 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
  9. VM起動までのフロー kubectl create -f vm01.yaml vm-controller が VM の Status

    を見て,どうするか決める vmi があるなら状態を合わせたり vmi がないならそのまま vmInformer に enqueue される virt-handler が dequeue するも, Status 上では vm は STOP status であるためためなにもしない 17
  10. 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
  11. 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
  12. 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
  13. 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
  14. VM 起動までのフロー vmiInformer が enqueue & virt-handler が dequeue virt-handler

    は,該当 pod の socket に rpc する virt-launcher が VMプロセスを起動させる 22
  15. 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
  16. 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
  17. 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
  18. Compute Pod は安全か? securityContext は絞りぎみ securityContext: capabilities: add: - NET_ADMIN

    - SYS_NICE privileged: false runAsUser: 0 vmi のリクエストに応じて,動的に capabilities を増減させる仕様[11] 29
  19. Bridge 接続 Pod 内部で bridge を作成する[12] Pod interface の MAC

    Address, IP を適当なものに書き換える[12] この時点で Pod 自身は外部通信できなくなる L3 到達可能な情報を積んだ DHCP Server を起動する[12] Pod の MAC Address を持った VM を起動する VM は起動時に DHCP でネットワーク設定を行う 完了 33
  20. 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 <none> <none> 35
  21. vmi の interface 定義 vmi の定義では,interface は以下 起動した Pod が

    Status を Update する interfaces: - ipAddress: 10.244.0.40 mac: b6:9d:5c:92:cd:3d name: default 36
  22. Pod 内部の Network info kubectl exec で中に入り確認する k6t-eth0 という名前の Bridge

    が作成される sh-5.0# ip a ...snip... 4: k6t-eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> 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: <BROADCAST,MULTICAST,UP,LOWER_UP> 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
  23. domain.xml (VM の構成ファイル) vnet0 という名前の tap device を k6t-eth0 から作成するように記述

    <interface type='bridge'> <mac address='b6:9d:5c:92:cd:3d'/> ← vmi の MAC <source bridge='k6t-eth0'/> <target dev='vnet0'/> <model type='virtio'/> <mtu size='1450'/> <alias name='ua-default'/> <address type='pci' domain='0x0000' bus='0x01' slot='0x00' function='0x0'/> </interface> 38
  24. VM 内部の Network info virtctl console でログインし,確認する Pod IP と

    MAC Address がついている $ ip a ...snip... 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> 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
  25. 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
  26. CPU Mode CPU Mode はデフォルトでホストのものを利用するため注意[19] if vmi.Spec.Domain.CPU == nil ||

    vmi.Spec.Domain.CPU.Model == "" { domain.Spec.CPU.Mode = v1.CPUModeHostModel } 42
  27. 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
  28. 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
  29. PodNetwork 以外への参加 マルチインターフェースを実現するための機能拡張が複数用意されている[16][17] MultusNetwork Intel が進める,baremetal k8s + NFV を可能とするプロジェクト

    複数の Interface を Pod にアタッチできる GenieNetwork 複数のネットワーク実装を同時に扱えるようにする CNI プラグイン こちらも複数の Interface を Pod にアタッチできる 47
  30. MultusNetwork への参加 定義されていた場合,Pod に Annotation をつける[18] if network.Multus != nil

    && network.Multus.Default { annotationsList[MULTUS_DEFAULT_NETWORK_CNI_ANNOTATION] = network.Multus.NetworkName } } 48
  31. 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
  32. 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
  33. ソースコードリファレンス [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
  34. ソースコードリファレンス [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