Tại sao phải quản lý tài nguyên trong Kubernetes?
Cái gì muốn hoạt động hiệu quả đều cần phải được quản lý, Kubernetes đã có cơ chế tự động phân phối các pod ra đều các node tuy nhiên đó chỉ là phần của quản lý tài nguyên. Ngoài ra trên thực tế thì có khá nhiều các yếu tố các mà chúng ta sẽ phải suy nghĩ tới do Kubernetes mặc định chưa có các khả năng để làm việc đó. Việc quản lý tài nguyên trong Kubernetes hiệu quả sẽ giúp chúng ta:
- Tiết kiệm chi phí hoạt động (Cost): Đây có lẽ là yếu tố nhiều người quan nhất . Việc quản lý tài nguyên hiệu quả có thể giúp ta tiết kiệm rất nhiều chi phí hoạt động của cluster. Mình đã gặp nhiều trường hợp có thể tối ưu từ 30 đến 70% chi phí chỉ bằng việc quản lý tài nguyên hiệu quả hơn.
- Tăng cường độ ổn định (Stability): Khi các ứng dụng được phân bổ tài nguyên hợp lý thì các tài nguyên này hoạt động sẽ ổn định hơn.
Trong bài viết này, mình sẽ giới thiệu đến các bạn những khái niệm giúp quản lý tài nguyên trong Kubernetes, cùng với đó là một vài kinh nghiệm cá nhân của bản thân trong từng khái niệm, công cụ này.
Allocate Resources (Node)
Allocate Resources là thông tin được thể hiện trên mỗi node của Kubernetes. Thông số này thể hiện lượng tài nguyên bạn có thể sử dụng cho mỗi node. Để có thể xem được thông tin này bạn chạy câu lệnh sau
kubectl describe node <node-name>
Ví dụ output sẽ có dạng như sau:
Name: node-pool-1
Roles: <none>
Labels: beta.kubernetes.io/arch=amd64 beta.kubernetes.io/os=linux kubernetes.io/hostname=node-pool-1 node-role.kubernetes.io/worker=true
Annotations: csi.volume.kubernetes.io/nodeid: {"ebs.csi.aws.com":"i-0a12345b67890cdef"} volumes.kubernetes.io/controller-managed-attach-detach: true
CreationTimestamp: Mon, 10 Aug 2024 10:45:23 +0700
Taints: node.kubernetes.io/unreachable:NoSchedule
Unschedulable: false
Lease: HolderIdentity: node-pool-1 AcquireTime: Mon, 13 Oct 2024 10:45:35 +0700 RenewTime: Mon, 13 Oct 2024 11:00:35 +0700 Capacity: # <= Cấu hình của node cpu: 4000mi ephemeral-storage: 100Gi hugepages-2Mi: 0 memory: 16Gi pods: 110
Allocatable: # <= Lượng tài nguyên có thể sử dụng cpu: 3496mi ephemeral-storage: 95Gi hugepages-2Mi: 0 memory: 15Gi pods: 110 System Info: Machine ID: 1234567890abcdef System UUID: 98765432-1A2B-3C4D-5E6F-789012345678 Boot ID: 11223344-5566-7788-99AA-BBCCDDEEFF00 Kernel Version: 5.11.0-1019-aws OS Image: Ubuntu 20.04.6 LTS Operating System: linux Architecture: amd64 Container Runtime Version: docker://20.10.8 Kubelet Version: v1.24.3 Kube-Proxy Version: v1.24.3 Conditions: Type Status LastHeartbeatTime LastTransitionTime Reason Message ---- ------ ----------------- ------------------ ------ ------- MemoryPressure False Mon, 13 Oct 2024 11:00:05 +0700 Mon, 10 Aug 2024 10:45:23 +0700 KubeletHasSufficientMemory kubelet has sufficient memory available DiskPressure False Mon, 13 Oct 2024 11:00:05 +0700 Mon, 10 Aug 2024 10:45:23 +0700 KubeletHasNoDiskPressure kubelet has no disk pressure PIDPressure False Mon, 13 Oct 2024 11:00:05 +0700 Mon, 10 Aug 2024 10:45:23 +0700 KubeletHasSufficientPID kubelet has sufficient PID available Ready True Mon, 13 Oct 2024 11:00:05 +0700 Mon, 10 Aug 2024 10:45:23 +0700 KubeletReady kubelet is posting ready status Addresses: InternalIP: 192.168.1.15 Hostname: node-pool-1 Non-terminated Pods: (12 in total) Namespace Name CPU Requests CPU Limits Memory Requests Memory Limits AGE --------- ---- ------------ ---------- --------------- ------------- --- kube-system kube-proxy-abc123 100m (2%) 200m (5%) 64Mi (0%) 128Mi (0%) 45d kube-system coredns-12345abc 100m (2%) 100m (2%) 70Mi (0%) 70Mi (0%) 45d kube-system fluentd-abc123 200m (5%) 200m (5%) 200Mi (1%) 400Mi (2%) 45d default nginx-deployment-abc123 500m (12%) 1 (25%) 512Mi (3%) 1Gi (6%) 10d Allocated resources: (Total limits may be over 100 percent, i.e., overcommitted.) Resource Requests Limits -------- -------- ------ cpu 1.6 (40%) 2.5 (62%) memory 846Mi (5%) 1.598Gi (10%) ephemeral-storage 0 (0%) 0 (0%) hugepages-2Mi 0 (0%) 0 (0%) Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal NodeHasSufficientMemory 45d kubelet Node node-pool-1 status is now: NodeHasSufficientMemory Normal NodeHasNoDiskPressure 45d kubelet Node node-pool-1 status is now: NodeHasNoDiskPressure Normal NodeReady 45d kubelet Node node-pool-1 status is now: NodeReady
Hãy chú ý đến 2 phần Capacity
và Allocatable
.
Capacity
để hiện tài nguyên thực tế của Node. Ví dụ trong hình là 1 node có cấu hình 4 vCPU - 16 GB Memory - 100 GB Disk. đây là cấu hình bạn yêu cầu từ Cloud provider nếu bạn sử dụng Cloud.- Tuy nhiên thực tế khi bạn thêm 1 node cấu hình như trên vào cụm K8s thì bạn không thể sử dụng hết 100% lượng tài nguyên này đâu. Lượng tài nguyên thực tế có thể sử dụng để chạy ứng dụng trong K8s trên node này được thể hiện ở
Allocatable
. Nhiều người hay lầm tưởng khi order 1 node 4 vCPU thì có thể sử dụng toàn bộ lượng tài nguyên này, tuy nhiên thực tế K8s sẽ để dư ra 1 phần để chạy các thành phần các như các process hệ thống hay chính những thành phần của K8s. Trong ví dụ này lượng tài nguyên có thể sử dụng sẽ thấp hơnCapacity
là 3496mi CPU - 15GB Memory - 95GB Disk.
Theo mình biết thì không có 1 cách nào tính được lượng
Allocatable
dựa trênCapacity
. Cái này mình sẽ phải tham khảo trực tiếp ở từng loại node. Tuy nhiên thông sốAllocatable/Capacity
sẽ càng lớn ở những node có cấu hình lớn, và ngược lại sẽ nhỏ hơn ở các node có cấu hình nhỏ. Vì vậy đôi khi sử dụng 1 vài node lớn sẽ chứa được nhiều resource hơn chạy ở nhiều node nhỏ có cùng tổng cấu hình CPU - Memory!
Lựa chọn loại node cho Kubernetes cluster cũng là một việc rất quan trọng.
- Nếu cluster của bạn dùng để chạy các microservice có pod sử dụng tài nguyên ở mức nhỏ thì sử dụng nhiều node có cấu hình nhỏ sẽ phù hợp do thuận tiện hơn trong việc autoscale node và sử dụng được toàn bộ tài nguyên trong node đó.
- Nếu cluster bạn chạy nhiều loại ứng dụng khác nhau như microservices + databases thì cluster bạn có thể tiếp cận theo hướng sử dụng nhiều Node Pool cho cluster này. Node pool 1 có cấu hình nhỏ chạy các microservice, Node pool 2 có cấu hình cao hơn để chạy database do thường database sẽ sử dụng 1 lượng lớn memory.
Resource Quota (namespace)
Resource Quota giúp giới hạn tổng tài nguyên mà một namespace có thể sử dụng. Điều này rất hữu ích khi bạn có nhiều nhóm làm việc trong cùng một cluster và muốn đảm bảo rằng không có nhóm nào chiếm dụng toàn bộ tài nguyên. Ví dụ đối với các cluster có nhiều team dev sử dụng, ta có thể phân quyền cho mỗi team dev sử dụng 1 namespace và giới hạn lượng resource có thể sử dụng ở mỗi namespace thông qua Resource Quota
Manifest cho Resource Quota sẽ có dạng:
# resource-quota.yml file
apiVersion: v1
kind: ResourceQuota
metadata: name: dev-team-quota
spec: hard: pods: "10" requests.cpu: "4" requests.memory: "8Gi" limits.cpu: "8" limits.memory: "16Gi"
Với ví dụ trên, toàn bộ pod trong namespace phải thỏa mãn các điều kiện đã định nghĩa trong file manifest:
- Trong namespace có thể có tối đa 10 pods
- Tổng lượng CPU request và limit trong namespace là 4 và 8
- Tổng lượng Memory request và limit trong namespace là 8GB và 16 GB
Chạy command sau để apply Resource Quota:
kubectl apply -f resource-quota.yml --namespace dev-team-a
Resource request/limit
Resource request và limit là cấu hình được cấu hình cho container nằm trong pod khi triển khai trên Kuberntes. Cấu hình này định nghĩa cách tài nguyên sẽ được phân bổ cho container đó. Cấu hình Resource request và limit sẽ có dạng như sau:
apiVersion: v1
kind: Pod
metadata: name: resource-example
spec: containers: - name: app image: busybox resources: requests: memory: "256Mi" cpu: "500m" limits: memory: "512Mi" cpu: "1"
- Resource Requests: Là lượng tài nguyên tối thiểu mà một container yêu cầu. Scheduler sẽ dựa trên thông số này để xác định node có đủ tài nguyên để chạy pod hay không.
- Resource Limits: Là giới hạn tài nguyên tối đa mà một container có thể sử dụng. Nếu container vượt quá giới hạn này, nó sẽ bị giới hạn (throttle đối với CPU hoặc bị OOMKilled đối với bộ nhớ).
Trong ví dụ trên:
- Container yêu cầu ít nhất 0.5 CPU và 256Mi Memory.
- Container không được sử dụng quá 1 CPU và 512Mi Memory. Nếu nó sử dụng quá 512Mi Memory, Kubernetes sẽ kill container này.
Việc cấu hình resource request và limit là một vấn đề khá đau đầu khi cân bằng giữa chi phí và hiệu năng vì chỉ cần cấu hình tài nguyên thấp 1 chút thôi có thể dẫn đến việc pod bị kill do sử dụng quá nhiều Memory.
Trước khi đến với việc cấu hình resource request và limit thế nào cho phù hợp thì chúng ta trước hết cần phải biết đến khái niệm Quality of Service trong Kuberntes
Quality of Service (QoS)
Quality of Service (QoS) giúp Kubernetes ưu tiên việc phân bổ tài nguyên cho các pod dựa trên thông số resource request và limit mà chúng cấu hình. Kubernetes phân loại QoS thành ba nhóm: Guaranteed, Burstable và Best-Effort. Đại khái đây là cách chúng ta đánh độ quan trọng cho từng pod trong Kubernets. Trong trường hợp node bị quá tải tài nguyên nó sẽ kill những pod có mức độ ưu tiên thấp nhất để đẩy sang các node khác cho đến khi node có mức sử dụng ổn định thì dừng lại. Trường hợp này các pod bạn có thể thấy sẽ bị rơi vào trạng thái Evicted
Vậy làm sao để định nghĩa được Quality of Service cho pod?
- Guaranteed: Nếu cả request và limit của CPU và bộ nhớ đều bằng nhau, pod sẽ được gán QoS "Guaranteed". Đây là mức ưu tiên cao nhất khi hệ thống bị thiếu tài nguyên.
- Burstable: Nếu request thấp hơn limit, pod sẽ thuộc nhóm "Burstable", có mức ưu tiên trung bình.
- Best-Effort: Nếu pod không cấu hình request hoặc limit, nó sẽ được gán QoS "Best-Effort", có mức ưu tiên thấp nhất. Pod này sẽ bị ảnh hưởng nhiều nhất khi tài nguyên trở nên khan hiếm.
Những loại ứng dụng nào thì nên sử dụng Quality of Service thế nào?
- Theo kinh nghiệm của mình những ứng dụng Stateful như database, message queue hoặc những ứng dụng critical như payment,... thì nên cấu hình sử dụng QoS là Guaranteed. Mức tài nguyên request = limit nên cấu hình ở ngưỡng resource mà pod sử dụng ở mức cao nhất trong vòng 1 tuần cộng thêm 10%.
Ví dụ: pod Redis sử dụng làm cache có ngưỡng sử dụng tài nguyên memory trong vòng 1 tuần như sau: Min: 1,1 GB, Avg: 1,4 GB Max: 1,9 GB thì mình sẽ cấu hình resource request = resource limit = 2,1 GB
- Đối với QoS Burstable mình sẽ sử dụng cho các ứng dụng Stateless có mức độ chịu lỗi cao (Đơn giản là khi bị kill đột ngột thì ứng dụng vẫn gracefull shutdown, không sinh ra lỗi). Thường là các API, cronjob,...
Ví dụ: Pod API Python có ngưỡng sử dụng tài nguyên memory trong vòng 1 tuần như sau: Min: 300 MB, Avg: 350 MB Max: 440 MB Mình sẽ cấu hình memory request là 350 MB và memory limit là 450MB
Việc cấu hình memory request bằng mức tài nguyên sử dụng trung bình sẽ giúp pod được cung cấp đủ tài nguyên để hoạt động ở trạng thái bình thường. Memory limit sẽ cao hơn 1 chút so với điểm max memory để đảm bảo trong trường hợp peak hours pod cũng ít có khả năng bị kill, đảm bảo hệ thống ổn định hơn.
- Còn lại QoS Best-Effort mình sẽ ưu tiên sử dụng cho các pod cung cấp tính năng phụ cho Kuberntes như: Logging, Tracing, Monitoring,... Do khi các pod này bị kill thì đen nhất là mất metric trong vài giây thôi chứ không có critical issues nào.
Kết
Tóm lại, việc quản lý tài nguyên trong Kubernetes là một yếu tố quan trọng giúp đảm bảo rằng các ứng dụng hoạt động hiệu quả và không gây ảnh hưởng đến các ứng dụng khác trong cùng một cluster. Bằng cách sử dụng Resource Requests và Limits, Resource Quotas, cùng với cơ chế QoS, bạn có thể kiểm soát chặt chẽ cách các tài nguyên được sử dụng, từ đó tối ưu hóa hiệu suất và tính ổn định của hệ thống.
Nếu thấy bài viết hay và đem lại giá trị cho bạn thì đừng ngại ngần cho mình 1 Upvote + 1 Follow nhé. Đó là động lực để mình chia sẻ thêm các kiến thức khác về Kubernetes, Devops
Have a nice day!