Docker及Kubernetes下device使用和分析

  • 2019 年 10 月 4 日
  • 筆記

Docker下使用device

默認情況下,Docker容器內無法訪問宿主機上的設備,比如/dev/mem

Docker有兩種方式訪問設備,一種是使用特權模式,一種是通過--device指定要訪問的設備。

非特權模式下,容器內的root用戶相當於宿主機上的普通用戶,使用特權模式後,容器內的root用戶將真正獲得root權限,可以訪問很多host上的設備,包括/dev/mem,GPU等

使用特權模式會將一些容器不需要用到的權限也放開,存在較大風險。所以在設備上,一般使用--device來指定容器可使用的設備

需要說明的是,使用--device掛載的設備,容器內的進程通常沒有權限操作,需要使用--cap-add開放相應的權限,如下

Kubernetes下使用device

Kubernetes支持--device問題在社區上討論了很久,感興趣的可以看下#5607。當前的解決方案是使用device plugins機制來註冊要訪問的設備,典型的如GPU(https://github.com/NVIDIA/k8s-device-plugin)。同樣,如果pod要使用/dev/mem,也需要有一個device plugin將/dev/mem註冊到Kubernetes中,註冊成功後,可在相應節點中查看到該設備資源信息,這時就可以在pod中使用了。

Kubernetes device plugin設計實現可見https://github.com/kubernetes/community/blob/master/contributors/design-proposals/resource-management/device-plugin.md

由於/dev下有很多的設備,每個device都寫一個device plugin確實很麻煩,有一給力的哥們開源了個k8s-hostdev-plugin項目(https://github.com/honkiko/k8s-hostdev-plugin),可基於該項目掛載/dev下的一些設備(該項目當前有個缺陷,後面源碼分析會提到)。

下載k8s-hostdev-plugin包,編輯 hostdev-plugin-ds.yaml中的containers.*.args,如下

執行kubectl create -f hostdev-plugin-ds.yaml創建daemonset對象。

當daemonset的pod起來後,執行kubectl describe node檢查/dev/mem是否有註冊到Kubernetes中。當node的Capacity和Allocatable有hostdev.k8s.io/dev_mem時,說明/dev/mem註冊成功

在業務pod中使用/dev/mem,與使用cpu等resource一樣。需要注意的是,擴展資源僅支持整型的資源,且容器規格中聲明的 limitrequest 必須相等

k8s-hostdev-plugin實現分析

k8s-device-plugin是怎麼實現將/dev/mem掛載到容器內的呢?我們先用docker inspect CONTAINERID看pod的容器

和直接用docker run --device跑起來的容器一樣。由此可知k8s-device-plugin最終還是基於Docker的--device來指定容器可訪問的設備

Kubernetes device plugin API 提供了以下幾種方式來設置容器

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" 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" json:"mounts,omitempty"`  	// Devices for the container.  	Devices []*DeviceSpec `protobuf:"bytes,3,rep,name=devices" json:"devices,omitempty"`  	// Container annotations to pass to the container runtime  	Annotations map[string]string `protobuf:"bytes,4,rep,name=annotations" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`  }

其中Envs表示環境變量,如NVIDIA GPU device plugin就是通過這個來指定容器可運行的GPU。Devices則對應容器的--device,k8s-hostdev-plugin就是通過該方式來指定容器可使用的設備。

看下k8s-hostdev-plugin的代碼實現

// NewHostDevicePlugin returns an initialized HostDevicePlugin  func NewHostDevicePlugin(devCfg *DevConfig) (*HostDevicePlugin, error) {  	normalizedName, err := NomalizeDevName(devCfg.DevName)  	if err != nil {  		return nil, err  	}      //要註冊到Kubernetes的設備信息  	devs := []*pluginapi.Device {  		&pluginapi.Device{ID: devCfg.DevName, Health: pluginapi.Healthy},  	}    	return &HostDevicePlugin{  		DevName: 		devCfg.DevName,  		Permissions:    devCfg.Permissions,  		NormalizedName: normalizedName,  		ResourceName:   ResourceNamePrefix + normalizedName,  		UnixSockPath:   pluginapi.DevicePluginPath + normalizedName,  		Dev:			devs,  		StopChan: 		make(chan interface{}),  		IsRigistered: false,  	}, nil  }

上面的pluginapi.Device表示一個設備,包含設備ID和設備狀態兩個字段。需要注意的是,擴展資源僅支持整型的資源,因為這裡只new了一個設備,所以最多只能有一個pod能使用這個resource。如果要運行多個使用該resource的pod,可以多new幾個pluginapi.Device,確保DeviceID不一樣就可以了。(目前該項目還未支持該功能,需要使用者自己去修改擴展)。

k8s-hostdev-plugin向kubelet註冊device resource信息後,kubelet會調用ListAndWatch()方法獲取所有設備信息。ListAndWatch()將device信息發送給kubelet後,會定時上報device的狀態。實現如下

// ListAndWatch lists devices and update that list according to the health status  func (plugin *HostDevicePlugin) ListAndWatch(e *pluginapi.Empty, s pluginapi.DevicePlugin_ListAndWatchServer) error {    	s.Send(&pluginapi.ListAndWatchResponse{Devices: plugin.Dev})    	ticker := time.NewTicker(time.Second * 10)    	for {  		select {  		case <-plugin.StopChan:  			return nil  		case <-ticker.C:  			s.Send(&pluginapi.ListAndWatchResponse{Devices: plugin.Dev})  		}  	}  	return nil  }

當pod的resources.limits中使用該resource時,kubelet會調用Allocate()方法請求資源信息,Allocate()方法可根據請求的DeviceID返回相應的信息。這裡因為要將/dev下的設備掛載到容器中,使用了ContainerAllocateResponse.Devices。在pluginapi.DeviceSpec中可指定host和容器的device路徑,以及讀寫權限。具體實現如下

// Allocate which return list of devices.  func (plugin *HostDevicePlugin) Allocate(ctx context.Context, r *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {  	//spew.Printf("Context: %#vn", ctx)  	spew.Printf("AllocateRequest: %#vn", *r)    	response := pluginapi.AllocateResponse{}      //指定host和容器的device路徑,以及讀寫權限  	devSpec := pluginapi.DeviceSpec {  		HostPath: plugin.DevName,  		ContainerPath: plugin.DevName,  		Permissions: plugin.Permissions,  	}    	//log.Debugf("Request IDs: %v", r)  	var devicesList []*pluginapi.ContainerAllocateResponse      //構建返回給kubelet的device resource信息  	devicesList = append(devicesList, &pluginapi.ContainerAllocateResponse{  		Envs: make(map[string]string),  		Annotations: make(map[string]string),  		Devices: []*pluginapi.DeviceSpec{&devSpec},  		Mounts: nil,  	})    	response.ContainerResponses = devicesList    	spew.Printf("AllocateResponse: %#vn", devicesList)    	return &response, nil  }

參考

https://github.com/kubernetes/kubernetes/issues/5607

https://github.com/kubernetes/community/blob/master/contributors/design-proposals/resource-management/device-plugin.md

https://github.com/honkiko/k8s-hostdev-plugin