Device Plugin và Extended Resources
Suốt series, pod chỉ xin hai loại tài nguyên: CPU và memory (Bài 22). Nhưng node có thể có thứ khác — GPU, NIC tốc độ cao, FPGA — mà kubelet không biết tới mặc định. Device plugin là khung cho phép node quảng bá những thứ đó dưới dạng extended resource, để pod xin và scheduler chia y như CPU. Bài này dựng một device plugin thật để bắt trọn luồng gRPC giữa nó và kubelet, rồi xem chính cơ chế extended resource bên dưới mà nó dựa vào.
Khung device plugin
Device plugin tách phần biết về phần cứng ra khỏi kubelet: nhà cung cấp viết plugin riêng, kubelet không cần sửa. Hai bên nói chuyện qua gRPC trên Unix socket trong /var/lib/kubelet/device-plugins/, theo hai service:
Registration service (KUBELET phục vụ, tại kubelet.sock)
Register(RegisterRequest{Version, Endpoint, ResourceName, Options})
DevicePlugin service (PLUGIN phục vụ, tại <tên>.sock)
GetDevicePluginOptions() — khai tính năng tùy chọn
ListAndWatch() → stream — báo danh sách thiết bị {ID, Health}, cập nhật khi đổi
Allocate(AllocateRequest) — kubelet gọi lúc tạo container, trả về envs/mounts/devices
Trình tự bắt tay có thứ tự bắt buộc: (1) plugin mở gRPC server trên socket riêng; (2) plugin gọi Register trên kubelet.sock, khai tên resource và tên socket của mình; (3) kubelet quay lại gọi ListAndWatch trên plugin, nhận danh sách thiết bị; (4) kubelet ghi số thiết bị vào Node.status.capacity dưới tên vendor-domain/loại; (5) khi pod xin resource đó được xếp lên node, kubelet gọi Allocate để plugin cấu hình container. Xem điểm khởi đầu — socket kubelet.sock kubelet mở sẵn để nhận đăng ký:
ssh worker-0 'sudo ls /var/lib/kubelet/device-plugins/'
kubelet.sock
Chỉ có nó, vì chưa plugin nào đăng ký. Giờ dựng một cái.
Dựng một device plugin thật
Không cần GPU để thấy luồng gRPC — Kubernetes có một sample device plugin quảng bá một resource giả example.com/resource. Chạy nó trên worker-0, mount /var/lib/kubelet/device-plugins để nó tạo socket riêng và tới được kubelet.sock:
apiVersion: v1
kind: Pod
metadata: {name: sample-dp}
spec:
nodeSelector: {kubernetes.io/hostname: worker-0}
containers:
- name: dp
image: registry.k8s.io/e2e-test-images/sample-device-plugin:1.3
securityContext: {privileged: true}
env:
- {name: PLUGIN_SOCK_DIR, value: /var/lib/kubelet/device-plugins}
volumeMounts:
- {name: device-plugin, mountPath: /var/lib/kubelet/device-plugins}
volumes:
- {name: device-plugin, hostPath: {path: /var/lib/kubelet/device-plugins}}
Log của plugin in ra đúng các bước (1)–(3) ở trên:
kubectl -n dp-demo logs sample-dp
pluginSocksDir: /var/lib/kubelet/device-plugins
Starting to serve on /var/lib/kubelet/device-plugins/dp.1779583118 # (1) mở socket riêng
Deprecation file not found. Invoke registration # (2) gọi Register trên kubelet.sock
ListAndWatch # (3) kubelet gọi ListAndWatch
Socket riêng của plugin xuất hiện cạnh kubelet.sock, và kubelet đã ghi resource vào capacity của node — đây là bước (4), và con số 2 đến từ ListAndWatch của plugin chứ không phải ai gõ tay:
ssh worker-0 'sudo ls /var/lib/kubelet/device-plugins/ | grep -v kubelet'
kubectl get node worker-0 -o json | jq '.status.capacity["example.com/resource"]'
dp.1779583118 # socket plugin
"2" # example.com/resource = 2, do ListAndWatch báo
Giờ tạo một pod xin example.com/resource: 1. Khi kubelet xếp pod lên worker-0, nó gọi bước (5) — Allocate trên plugin, truyền ID thiết bị đã chọn:
# pod limits {example.com/resource: "1"}, ghim worker-0
kubectl -n dp-demo logs sample-dp | grep Allocate
Allocate, &AllocateRequest{ContainerRequests:[]*ContainerAllocateRequest{
&ContainerAllocateRequest{DevicesIDs:[Dev-1],},},}
kubelet đã chọn thiết bị Dev-1 cho pod và hỏi plugin cách cấu hình container. Plugin trả về AllocateResponse chứa envs/mounts/devices để kubelet tiêm vào container — với GPU thật, đây là chỗ plugin mount device node /dev/nvidia0 và đặt biến môi trường driver. Sample plugin này trả response rỗng (không có phần cứng thật để gắn), nhưng RPC Allocate vẫn được gọi đúng lúc tạo container. Đó là toàn bộ vòng đời: plugin đăng ký, báo thiết bị qua ListAndWatch, kubelet quảng bá rồi gọi Allocate khi cấp.
Cơ chế bên dưới: extended resource
Việc plugin làm với node, rút gọn lại, chỉ là đẩy một con số vào status.capacity (qua ListAndWatch). Với tài nguyên logic không gắn phần cứng, có thể làm thẳng bước đó bằng tay — PATCH Node status, đúng cách doc nêu — và đây cũng là cách gọn để thấy scheduler đối xử với extended resource ra sao. Quảng bá kkloud.io/widget: 2 lên worker-0:
kubectl patch node worker-0 --subresource=status --type=json \
-p='[{"op":"add","path":"/status/capacity/kkloud.io~1widget","value":"2"}]'
kubectl get node worker-0 -o json | jq '.status.capacity["kkloud.io/widget"], .status.allocatable["kkloud.io/widget"]'
capacity: 2
allocatable: 2
(~1 là cách JSON Pointer viết dấu / trong tên resource.) Kubelet đẩy nó từ capacity sang allocatable — y như khi plugin báo, chỉ khác nguồn.
Scheduler chia y như CPU
Pod xin extended resource trong resources.limits, và scheduler đối xử với nó hệt CPU/memory — số nguyên, không over-commit, không chia sẻ giữa container. Tạo một pod xin 1 widget và một pod xin 2, cùng ghim worker-0:
# w-one: limits {kkloud.io/widget: "1"} ; w-two: limits {kkloud.io/widget: "2"}
kubectl -n dev-demo get pods -o wide
NAME READY STATUS NODE
w-one 1/1 Running worker-0
w-two 0/1 Pending <none>
w-one lấy 1 trong 2 widget và chạy. w-two xin 2 nhưng chỉ còn 1, nên Pending. Xem lý do:
kubectl -n dev-demo get event --field-selector involvedObject.name=w-two | grep FailedScheduling
Warning FailedScheduling 0/2 nodes are available: 1 Insufficient kkloud.io/widget,
1 node(s) didn't match Pod's node affinity/selector ...
Insufficient kkloud.io/widget là đúng thông báo của plugin NodeResourcesFit (Bài 34) khi thiếu CPU, giờ áp cho widget. Scheduler không phân biệt extended resource với tài nguyên gốc: cùng đếm allocatable, cùng trừ theo pod đã đặt, cùng từ chối khi không đủ. Device plugin chỉ cần đẩy con số vào capacity và cấu hình container qua Allocate(); phần xếp lịch dùng lại cơ chế resource có sẵn. (Từ v1.36, trường allocatedResourcesStatus trong container status còn báo sức khỏe của thiết bị đã cấp — beta.)
🧹 Dọn dẹp
kubectl delete namespace dp-demo dev-demo
# gỡ extended resource ảo + dọn entry plugin để lại (kubelet không tự xóa resource nó thôi quản)
kubectl patch node worker-0 --subresource=status --type=json \
-p='[{"op":"remove","path":"/status/capacity/kkloud.io~1widget"}]'
kubectl patch node worker-0 --subresource=status --type=json \
-p='[{"op":"remove","path":"/status/capacity/example.com~1resource"},
{"op":"remove","path":"/status/allocatable/example.com~1resource"}]'
Một điểm vận hành cần nhớ: khi device plugin dừng, kubelet đặt allocatable của resource về 0 nhưng không xóa entry khỏi status.capacity (nó nhớ trong checkpoint), nên phải PATCH gỡ tay để node về sạch. Manifest ở github.com/nghiadaulau/kubernetes-from-scratch, thư mục 61-device-plugins.
Tổng kết
Device plugin cho node quảng bá phần cứng ngoài CPU/memory thành extended resource vendor/loại, qua gRPC với kubelet: plugin mở socket riêng, gọi Register trên kubelet.sock, kubelet gọi lại ListAndWatch để nhận danh sách thiết bị rồi ghi vào Node.status.capacity, và gọi Allocate khi pod được cấp để plugin cấu hình container (envs/mounts/devices). Ta dựng sample plugin thật và bắt trọn luồng đó: log Starting to serve → Invoke registration → ListAndWatch, socket plugin xuất hiện cạnh kubelet.sock, node hiện example.com/resource: 2 (do ListAndWatch báo, không phải gõ tay), và khi pod xin thì Allocate, DevicesIDs:[Dev-1] được gọi. Dưới cùng, plugin chỉ đẩy một con số vào capacity — với tài nguyên logic có thể PATCH tay, và scheduler chia extended resource y như CPU: pod xin 1 chạy, pod xin 2 (chỉ còn 1) Pending với Insufficient kkloud.io/widget qua NodeResourcesFit (Bài 34).
Part XII khép lại ở đây — bốn cách mở rộng Kubernetes: CRD (kiểu mới trong etcd), admission webhook (chen vào đường ghi), operator (CRD + controller), API aggregation (server thứ hai), và device plugin (tài nguyên phần cứng qua gRPC). Part XIII chuyển sang vận hành cụm: sao lưu và khôi phục etcd, nâng cấp phiên bản, dọn rác, rồi quan sát — logging, metrics, traces. Bài 62 bắt đầu với phần đáng sợ nhất khi mất: sao lưu và khôi phục etcd, nơi cất toàn bộ trạng thái cụm.