目标
当一个launcher pod被创建时,它会请求资源 ,如下
Requests:cpu: 16devices.kubevirt.io/kvm: 1devices.kubevirt.io/tun: 1devices.kubevirt.io/vhost-net: 1ephemeral-storage: 50Mhugepages-2Mi: 8Gimemory: 1574961152nvidia.com/GA102_GEFORCE_RTX_3090: 2
并且会在pod的环境变量中写入请求的显卡的ID :
declare -x PCI_RESOURCE_NVIDIA_COM_GA102_GEFORCE_RTX_3090="0000:e1:00.0,0000:e1:00.1,0000:01:00.0,0000:01:00.1"
我们可以看到两个3090的显卡被请求了,那么这个调度就会进入到k8s的调度系统中,k8s要负责为这个pod分配两块3090出来。 那么现在问题来了:
- k8s是如何知道的哪个节点有空闲显卡的。
- 谁给pod的环境变量中写入了显卡的id
- 为什么请求了两个显卡却有4个Id,很明显,第2、4个id应该是声卡的id
k8s是如何知道哪个节点有空闲显卡的
根据copilot的解释,k8s会有个device选择的过程,这个是由k8s来处理的。 这个我们暂时放下不表。
谁给Pod的环境变量写入了显卡的id
我们合理的猜想有应该有三个地方:
virt-controller的updateStatus
virt-handler
virt-launcher
virt-controller(不是)
根据流程,sync函数主要是进行pod的编辑、创建和启动:编辑初始化的yaml,创建pod,并且启动pod。 其中创建和启动可以理解为一个步骤。 这时候pod还没有根据k8s的调试到任一节点,所以我们认为updateStatus会去操作获取pod。
但这也有点不对,因为virt-controller是一个调度级别的程序,它没有办法获取到每个节点的pci信息。
virt-handler(不是)
我们到现在还没有了解到virt-handler最终的目标是什么,为什么要有virt-handler这个程序。
根据官方的流程图,我们可以看到virt-handler才是真正的主导启动的一个程序,难道不是virt-launcher?
经过研究源码后,至少virt-handler也不是。
virt-launcher(不是)
virt-launcher是最后的接受方,他并不处理这些东西
gpu-device-plugin(是)
最终,发现还是gpu-device-plugin这个程序在处理这些东西,在allocate时,会返回一个ContainerResponses,其中包含envs变量,应该是k8s会将它的返回交给pod
type ContainerAllocateResponse struct {// List of environment variable to be set in the container to access one of more devices.Envs map[string]string `protobuf:"bytes,1,rep,name=envs,proto3" json:"envs,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`// Mounts for the container.Mounts []*Mount `protobuf:"bytes,2,rep,name=mounts,proto3" json:"mounts,omitempty"`// Devices for the container.Devices []*DeviceSpec `protobuf:"bytes,3,rep,name=devices,proto3" json:"devices,omitempty"`// Container annotations to pass to the container runtimeAnnotations map[string]string `protobuf:"bytes,4,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`// CDI devices for the container.CDIDevices []*CDIDevice `protobuf:"bytes,5,rep,name=cdi_devices,json=cdiDevices,proto3" json:"cdi_devices,omitempty"`XXX_NoUnkeyedLiteral struct{} `json:"-"`XXX_sizecache int32 `json:"-"`
}
那么最合理的流程就是:
- 1、virt-controller负责生成pod,规定pod的各个属性。
- 2、k8s接收到pod后,会根据pod的请求进行调度,比如去各个节点查看gpu的剩余,如果有剩余,则请求分配gpu。
- 3、gpu-device-plugin接收到请求后,会返回一个带有env的response,其中env就是最后virt-launcher pod中有的环境变量。
- 4、k8s接收到gpu-device-plugin的返回后,会将该信息加入到Pod的初始的环境变量中,最后再启动pod。
- 5、virt-handler在发现pod创建信息后,会继续一系列的处理,而不是由virt-launcher直接进行创建了。 可以理解virt-handler+virt-launcher才是一整套的nova-compute。
- 6、最后,由virt-launcher进行启动,是的,virt-launcher还是会进行启动的。 只不过是由virt-handler来触发启动这个程序。
virt-handler
大概有这么几个作用:
virt-handler遍历pods目录,看看哪个是有virt-launcher的socket的,如果有,就证明这是个主机的Pod。
从k8s中拿到vmi,判断vmi要做的事儿,比如开机、关机、迁移啥的,通过socket去通知virt-launcher,让它去执行要做的操作。
声卡id的问题
前面我们说过,vfio-manager在使用vfio驱动gpu的时候,实际上是会连同声卡也给驱动上的。
总结
gpu的分配是由gpu-device-plugin来分配的,在使用allocated