Lập lịch trên Kubernetes dùng Affinity

Tram Ho

Mở đầu

Hello mọi người đã trở lại với series k8s basic.
Trong bài trước mình đã giới thiệu một số cách thức sử dụng trong lập lịch thực hiện các work load như:

  • Tạo static Pod
  • Sử dụng nodeName
  • Sử dụng nodeSelector
  • Sử dụng Taint/Toleration

Trong bài này mình sẽ tiếp tục giới thiệu một số cách thức nữa thường được sử dụng trong cấu hình ứng dụng phục vụ các yêu cầu lập lịch khác nhau như:

  • NodeAffinity: Lựa chọn node thỏa mã các điều kiện về labels của node
  • PodAffinity: Tạo Pod trên node mà trên đó phải có Pod khác có label thỏa mãn điều kiện cho trước
  • PodAntiAffinity: Tạo Pod trên node mà trên đó phải KHÔNG CÓ Pod khác có label thỏa mãn điều kiện cho trước

image.png

Trong môi trường lab của mình đang cài đặt kubernetest v1.20.7 có 3 master và 3 worker như sau:

Sử dụng NodeAffinity

Nhắc lại trong bài trước, chúng ta có đặt ra bài toán là cấu hình cho Pod chỉ chạy trên các Node có cấu hình lớn (size=large). Tuy nhiên với bài toán ngược lại là chỉ chọn các node có cấu hình vừa và nhỏ để chạy Pod (size != large) thì cách dùng nodeSelector sẽ không giải quyết được bài toán.

Bởi nodeSelector chỉ cho phép chúng ta định nghĩa ra tập node theo một điều kiện label duy nhất. Vậy để giải quyết bài toán này thì chúng ta sẽ phải dùng tới cấu hình nodeAffinity.

Ý tưởng của cấu hình nodeAffinity giống với nodeSelector đó là đều lựa chọn node để thực thi Pod, tuy nhiên điều kiện lựa chọn của nodeAffinity thì linh động và đa dạng hơn so với nodeSelector, chúng ta có thể sử dụng nhiều toán tử so sánh hơn.

Khi sử dụng cấu hình NodeAffinity thì chúng ta có 2 loại:

  • requiredDuringSchedulingIgnoredDuringExecution (hard): Scheduler sẽ không thể lập lịch cho Pod cho đến khi rule của NodeAffinity được thỏa mãn.
  • preferredDuringSchedulingIgnoredDuringExecution (soft): Với cấu hình này thì ta thông báo cho scheduler “cố gắng hết sức” để tìm ra các node thỏa mãn điều kiện. Tuy nhiên khi đã “cố gắng hết sức” rồi mà không tìm dc node nào thỏa mã thì Pod sẽ vẫn được lập lịch.

Sử dụng hard nodeAffinity

Tiếp tục trở lại bài toán ban đầu là làm sao để lựa chọn các node có cấu hình “không lớn” để chạy Pod. Lúc này ta sẽ sử dụng cấu hình NodeAffinity.
image.png

Mấu chốt ở đây nằm ở cấu hình affinity cho Pod như sau:

Lab1: Ta tạo file manifest deployment-nodeaffinity.yaml có nội dung như sau:

Thực hiện apply vào hệ thống và kiểm tra kết quả:

Như vậy các Pod sinh ra của deployment đều chạy trên node viettq-worker2, đây là node có label size=medium thỏa mãn điều kiện của nodeAffinity mà chúng ta đã cấu hình cho deployment.

Và để trực quan hơn nữa thì mình sẽ scale deployment này thành 20 Pod để kiểm tra:

Thì kết quả cho ra 20 Pod vẫn chỉ chạy trên 2 node viettq-worker1viettq-worker2 thỏa mãn điều kiện của nodeAffinity.

Lab2: Thay đổi một chút về yêu cầu bài toán như sau

Cấu hình để Pod chỉ chạy trên node có ổ SSD (disktype=ssd) và có cấu hình không nhỏ (size not small). 2 yêu cầu trên cho ta kết quả Pod chỉ được chạy trên node viettq-worker2. Để thực hiện yêu cầu này ta sẽ vẫn dùng cấu hình NodeAffinity tuy nhiên sẽ phải dùng 2 điều kiện lọc.
image.png

Mấu chốt cho bài toán này nằm ở phần cấu hình nodeAffinity như sau:

Tạo file deployment-multi-nodeaffinity.yaml có nội dung như sau:

Thực hiện apply vào hệ thống và kiểm tra:

Lúc này kết quả cho thấy cả 3 Pod sinh ra đều chạy trên node viettq-worker2 đúng như chúng ta mong muốn.

Lab3: Tiếp tục thay đổi yêu cầu bài toán như sau:
Cấu hình để Pod chỉ chạy trên node có ổ SSD (disktype=ssd) và có cấu hình lớn (size=large). Thực tế với hiện trạng 3 node của chúng thì sẽ không có node nào thỏa mãn cả 2 điều kiện trên ==> Pod sinh ra sẽ ở trạng thái Pending.

Để thực hiện yêu cầu này ta sẽ vẫn dùng cấu hình NodeAffinity.
Tạo file deployment-multi-nodeaffinity.yaml có nội dung như sau:

Thực hiện apply vào hệ thống và kiểm tra:

Đúng như dự đoán, 3 Pod sinh ra bởi deployment này đều ở trạng thái Pending. Nguyên nhân là chúng ta đang cấu hình tham số requiredDuringSchedulingIgnoredDuringExecution do đó khi không tìm được node thỏa mãn thì Pod sẽ ở trạng thái Pending.

Sử dụng soft nodeAffinity

Trong lab trên khi chúng ta thiết lập các cấu hình nodeAffinity và không có node nào thỏa mãn thì ứng dụng của chúng sẽ không được lập lịch và thực hiện.

Do đó thay vì “cứng nhắc” bắt buộc phải tuân theo rule đó thì chúng ta có thể lựa chọn cấu hình “soft rule” tức là cố gắng hết sức để lựa chọn theo điều kiện nhưng nếu không thể tìm được node thỏa mãn thì sẽ vẫn lập lịch thực hiện.

Lab1: Ưu tiên cao nhất cho việc lập lịch cho Pod trên node có size=large, nếu không có thì tiếp tục ưu tiên node có ổ SSD disktype=ssd.
image.png

Với yêu cầu và hiện trạng như trên thì các Pod sinh ra sẽ luôn được ưu tiên chạy trên node viettq-worker3.

Tạo file deployment-prefer-nodeaffinity.yaml có nội dung như sau:

Ta apply file trên vào hệ thống và kiểm tra kết quả:

Đúng như lý thuyết đã phân tích, các Pod sinh ra đều được lập lịch thực hiện trên node viettq-worker3. Để củng cố thêm nữa thì mình sẽ lại scale deployment thành 20 Pod để check xem sao:

Kết quả vẫn vậy, các Pod vẫn chỉ ưu tiên chạy trên node viettq-worker3 trừ khi nó hết tài nguyên 😃

Sử dụng PodAffinity

Khi sử dụng nodeAffinity thì chúng ta đang sử dụng các labels của node để làm điều kiện lựa chọn.
Còn với PodAffinity thì nó vẫn là cách thức lựa chọn node nhưng là dựa vào labels của các Pod chạy trên node.

Hiểu nôm na thì cấu hình PodAffinity tưng tự với việc Pod-A chỉ ưu tiên/yêu cầu chạy trên một node nào đó mà đang có Pod-B đang chạy.

Trong thực tế thì việc này có ý nghĩa gì? Nó giúp chúng ta có thể tùy biến cho các Pod ứng dụng có giao tiếp nhiều với nhau thì ưu tiên chạy trên chung một node để tối ưu kết nối.

Và tương tự như nodeAffinity thì podAffinity cũng có 2 loại:

  • requiredDuringSchedulingIgnoredDuringExecution
  • preferredDuringSchedulingIgnoredDuringExecution

Chúng ta cùng xem xét một ví dụ như sau:
image.png
Trên hệ thống đã triển khai sẵn ứng dụng db, các Pod của DB có label là app=db. Tôi muốn deploy các Pod BE của tôi lên nhưng node nào có đang chạy DB để tối ưu phần kết nối.

Để giải quyết yêu cầu trên chúng ta sẽ sử dụng cấu hình podAffinity.
Đầu tiên ta sẽ setup 2 Pod chạy trên 2 node và có label là app=db để giả định DB đang chạy trên 2 node này.

Ta sẽ tạo 2 Pod sử dụng cấu hình nodeName để assign trực tiếp vào node:

Lưu nội dung trên thành db-pod.yaml và apply vào hệ thống:

Tiếp theo ta sẽ tạo một deployment từ file deployment-pod-affinity.yaml như sau:

Tiến hành apply vào hệ thống và kiểm tra:

Kết quả đúng như mong đợi, các Pod của BE chỉ chạy trên các node có Pod của DB

Tuy nhiên lúc này lại phát sinh ra vấn đề là cả Pod của BE đều chạy trên 1 node. Sẽ là tuyệt vời hơn nếu 2 Pod của BE lại chia đều trên 2 node có chạy Pod DB. Vấn đề này sẽ được giải quyết khi sử dụng cấu hình podAntiAffinity.

Sử dụng PodAntiAffinity

Hiểu nôm na thì cấu hình PodAntiAffinity tưng tự với việc Pod-A chỉ chấp nhận chạy trên một node nào đó mà đang không có Pod-B đang chạy.

Như trong ví dụ trên, best case mà chúng ta muốn là 2 Pod của BE sẽ chạy trên 2 node có Pod DB.

Chúng ta đã giải quyết được 1 nửa bài toán là chỉ cho Pod BE chạy trên node có Pod DB.

Còn nửa còn lại, ý tưởng giải quyết sẽ là “ưu tiên không chạy Pod Be trên node đã có Pod BE“. Nghĩa là một node sẽ chỉ có tối đa 1 Pod Be được chạy, như vậy sẽ giải quyết hoàn toàn bài toán.

image.png

Chúng ta sẽ cập nhật lại deployment bên trên để sử dụng thêm cấu hình podAntiAffinity nữa như sau:

Thực hiện apply lại vào hệ thống vào kiểm tra:

Như vậy là 2 Pod BE đã chia đều đúng trên 2 node có chạy Pod DB cho chúng ta. Trong phần cấu hình podAntiAffinity mình chỉ sử dụng mode là preferredDuringSchedulingIgnoredDuringExecution để ưu tiên cho các Pod chia đều ra các node.

Tuy nhiên thực tế sẽ có các bài toán số lượng replicas (số Pod) nhiều hơn số node do đó nên để tham số prefer để nếu có vi phạm thì Pod sẽ vẫn được lên lịch thực hiện chứ không bị pending.

Để làm rõ hơn điều này, mình sẽ thử scale deployment của Be lên thành 4 pod:

Như vậy thì sau khi scale ứng dụng vẫn được lập lịch thực hiện được. Nếu chúng ta để cấu hình podAntiAffinityrequiredDuringSchedulingIgnoredDuringExecution thì chỗ này Pod BE của chúng ta sẽ chỉ allocate được 2 Pod (tương ứng 2 node) còn lại sẽ ở trạng thái Pending.

Hy vọng qua bài viết này các bạn đã hiểu về các cơ chế trong việc cấu hình lập lịch cho workload trên k8s. Thực tế thì các bạn sẽ gặp các bài toán cụ thể và sẽ cần kết hợp nhiều kỹ thuật để xử lý vấn đề. Quan trọng là các bạn hiểu từng kỹ thuật đó và áp dụng cho hợp lý.

Nếu thấy nội dung hữu ích thì các bạn vui lòng cho mình một nút Upvote vào bài viết để có động lực tiếp tục viết bài nhé! Many thanks 

Chia sẻ bài viết ngay

Nguồn bài viết : Viblo