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

Deep-dive KubeVirt

Avatar for Takuya TAKAHASHI Takuya TAKAHASHI
October 24, 2019
1.8k

Deep-dive KubeVirt

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

Avatar for Takuya TAKAHASHI

Takuya TAKAHASHI

October 24, 2019
Tweet

More Decks by Takuya TAKAHASHI

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