Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

概要 3

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

Deep Dive Controller 5

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

virt-handler 12

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Deep Dive Virtual Machine 25

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

Compute 27

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

Network 30

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

実際の Pod と VM で確認 34

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Compute おまけ 40

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

Network おまけ 44

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

ソースコードリファレンス [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

Slide 53

Slide 53 text

ソースコードリファレンス [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

Slide 54

Slide 54 text

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