Application lifecycle management là gì?

Đầu tiên chúng ta nói đến thế nào là quản lý vòng đời ứng dụng. Đây là một thuật ngữ đề cập đến việc làm thế nào mà các nền tảng ứng dụng có thể tương tác với các thành phần mà nó tạo ra ngay sau khi nó khởi động hoặc trước khi nó dừng lại.
Như mọi người đã biết, trong Kubernetes, Pod là một đơn vị cơ bản trong cluster. Pod có thể chứa một hoặc nhiều container cái mà dùng để chạy chương trình ứng dụng của chúng ta. Để hiểu thêm về Pod, các bạn có thể đọc thêm tại đây
Nhưng các Pods luôn được trừu tượng hóa (bao bọc) bởi các thành phần cấp cao hơn, hầu hết là Deployments, và chúng ta thường viết cấu hình cho Deployment, Replica Set v.v... Hiểu được thành phần cốt lõi cho phép ta gỡ rối các vấn đề, tận dụng hệ sinh thái k8s tốt hơn và tự tin thay đổi.
Việc triển khai này rất quan trọng vì đôi khi chúng ta có thể cần thực hiện một số hành động trên Pod, chẳng hạn như kiểm tra khả năng kết nối đến một hoặc nhiều thành phần phụ thuộc của nó. Tương tự, Pod có thể phải trải qua các hoạt động dọn dẹp trước khi Pod bị destroyed.
Ở đây chúng ta sẽ thấy các giai đoạn khác nhau của pod và làm thế nào chúng ta có thể tận dụng chúng.
uc?id=12u_ziVUKPi7XKsdh3IONc-6Oy2fEbMdU&export=download
Url: https://www.magalix.com/blog/kubernetes-patterns-application-process-management-1

Làm thế nào Kubernetes terminates các Pod của nó?

Thông qua vòng đời của nó, một container có thể bị terminated (chấm dứt). Có lẽ bởi vì Pod đang bị tắt hoặc thất bại một hoặc cả hai tín hiệu dò liveness hoặc readiness probes (liveness và readiness probes là 2 thông số dùng cho việc health check Pod, mình sẽ nói rõ hơn ở P2). Trong mọi trường hợp, Kubernetes tuân theo một cách tiêu chuẩn để destroy một container running: bằng cách gửi kill signals. Cuối cùng, một container chỉ là một quá trình chạy trên máy.

  • Đầu tiên, Kubernetes gửi tín hiệu SIGTERM. Tín hiệu SIGTERM được gửi theo mặc định khi bạn cấp lệnh kill đối với một process đang chạy trên hệ thống Linux của bạn.

  • SIGTERM cho phép running process thực hiện bất kỳ hoạt động dọn dẹp cần thiết nào trước khi tắt máy như giải phóng file locks, closing database và kết nối mạng, v.v...

  • Mặc dù vậy, đôi khi process (trong trường hợp này là container) không đáp ứng với tín hiệu SIGTERM. Hoặc là do lỗi code, vòng lặp vô hạn hoặc vì lý do nào đó. Bởi vì điều này, Kubernetes chờ đợi thời gian gia hạn là ba mươi giây (con số này có thể được overide) trước khi nó gửi tín hiệu tích cực hơn là SIGKILL.

  • SIGKILL là tín hiệu tương tự được gửi đến một running process khi bạn cấp lệnh phổ biến kill -9 và gửi cho nó process id. Không phải process nhận được tín hiệu SIGKILL mà chính là hệ điều hành bên dưới.

  • Khi kernel phát hiện tín hiệu này, nó sẽ ngừng cung cấp bất kỳ tài nguyên nào cho process đang bị kill. Kernel cũng dừng bất kỳ luồng CPU nào đang được sử dụng bởi dangling process. Nói cách khác, nó cắt điện khỏi process, buộc nó phải chết.

Cho đến nay, Kubernetes xử lý các container giống như bất kỳ Linux system administrator xử lý running process: gửi tín hiệu đến process hoặc kernel. Nhưng, bởi vì các container là một phần của các ứng dụng lớn hơn với các chức năng và nhiệm vụ phức tạp, tín hiệu là không đủ. Vì lý do đó, Kubernetes cung cấp các postStart và preStop hooks.

Init Container

Vì sao chúng ta lại đề cập đến Init Container ở đây? Có thể nhiều bạn đã biết đến nó hoặc chưa biết, và để tránh nhầm lẫn hay sử dụng sai mục đích giữa Init Container và postStart hook, mình sẽ nói thêm về Init Container.
Init Container là một container bên trong Pod dùng cho mục đích khởi chạy một xử lý nào đó cần thiết trước khi 1 container khác được chỉ định khởi chạy.
Chẳng hạn như mình có Pod A chứa các container như inint container, nginx, frontend, fluentd, v.v.. để chạy 1 web php. Nhưng trước khi nginx khởi động mình muốn có 1 inint container có nhiệm vụ chuẩn bị source code (backup code, switch sang branch cần test, v.v...)
Init Container có đặc trưng sau:

  • Luôn chạy đến khi hoàn thành.
  • Chạy trước khi container được chỉ định bắt đầu
  • Nếu nhiều Init Container được chỉ định, chúng sẽ được thực hiện theo thứ tự
  • Trường hợp thuộc tính restartPolicy trong Pod là Always. OnFailure được sử dụng trong Init Container
  • Nếu Init Container không thành công, Kubernetes liên tục khởi động lại Pod cho đến khi Init Container thành công. Tuy nhiên, nếu Pod có restartPolicy là Never, Kubernetes không khởi động lại Pod.
  • Ngoài ra, các Init Container không hỗ trợ readiness probes vì chúng phải chạy để hoàn thành trước khi Pod có thể sẵn sàng.

Ưu điểm của việc tách riêng Init Container với các container ứng dụng như sau:

  • Từ lý do mang tính security, phân tách các Container (bao gồm các container tool) với các container của Application
  • Trường hợp bạn muốn sử dụng code hay là tool mà nó không được bao gồm trong container của application (chẳng hạn như đoạn xử lý backup source app, switch sang branch cần test) và thực thi một số xử lý đầu tiên (init).

Lưu ý: Init Container có thể được khởi động lại hoặc chạy lại, vì vậy khi thực hiện các xử lý đầu tiên (init) phải xem xét cẩn thận.
( Reference : https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ )

Pod Lifecycle

Pod có một số phase dưới đây:

  • Pending: Approved bởi Kubernetes, nhưng đang ở trạng thái 1 hoặc nhiều container chưa được tạo. Thông thường thì đang ở trạng thái đang pull images hoặc đang xử lý Init Container.
  • Running: Pod đã được liên kết với một node và tất cả các Container đã được create. Ít nhất một Container vẫn đang running hoặc đang trong quá trình starting or restarting.
  • Success: Tất cả các Container trong Pod đã kết thúc thành công và sẽ không được khởi động lại.
  • failed: Tất cả các Container trong Pod đã terminated và ít nhất một Container đã bị terminated in failure. Nghĩa là, Container hoặc exited with non-zero status hoặc bị hệ thống terminated.

Tham khảo thêm tại đây

Lifecycle Events

Như mình đã đề cập phía trên, chúng ta có 2 hook dành cho việc xử lý ở 2 thời điểm: ngay khi container được khởi tạo và trước khi Pod bị stop.

  • postStart: Chạy ngay sau khi container được tạo. Chấm dứt nếu Trình xử lý này không thành công và khởi động lại theo restartPolicy
  • preStop: được thực hiện trước khi container kết thúc. Trình xử lý này được thực thi trước khi SIGTERM được gửi đến tiến trình root container.

Mình sẽ có một sơ đồ khái quát thời điểm Lifecycle Event
uc?id=1dkuhsdLT2_VaXBXvx8wr4jHJfoipurHS&export=download
Url: https://cstoku.dev/posts/2018/k8sdojo-06/

postStart hook (Thực hiện command khi container start)

PostStart là một trong 2 Container hooks mà Kubernetes cung cấp. Hook này được thực thi ngay sau khi container được tạo, tuy nhiên không có gì để đảm bảo rằng hook sẽ được thực thi trước ENTRYPOINT của container. Không có tham số được truyền cho xử lý.
Bạn có thể sử dụng hoặc không sử dụng postStart hook tùy theo nhu cầu của bạn. Mình sẽ đưa ra các trường hợp mà bạn có thể xem xét sử dụng:

  • Nếu như container của bạn là một client để thực thi API, bạn phải đảm bảo rằng API đang hoạt động và có khả năng responding to requests trước khi container của bạn đi vào hoặt động.
  • Khi container cần thực thi một số hoạt động trước khi process chính khởi chạy, chẳng hạn như đặt lại mật khẩu của người dùng hoặc các sự kiện đăng nhập vào log hoặc database.
  • Ví dụ, khi cần phải đáp ứng một điều kiện cụ thể trước khi container đi vào hoạt động, bạn có thể thăm dò API bên ngoài trong vài giây; nếu kiểm tra thất bại sau thời gian nhất định, thăm dò trả về non-zero exit code. Khi non-zero exit code được trả về, Kubernetes sẽ tự động kill main process của container.
    Chúng ta sẽ xem xét ví vụ sau đây, các container cần phải đảm bảo rằng một service phụ thuộc available. Nếu không, toàn bộ container sẽ bị kill:
# poststart.yml
apiVersion: v1
kind: Pod
metadata:
 name: client
spec:
 containers:
   - image: nginx
     name: client
     lifecycle:
       postStart:
         exec:
           command:
             - sh
             - -c
             - sleep 10 && exit 1
[vagrant@ci postStart-preStop]$ kubectl create namespace kuber-best-practice
[vagrant@ci postStart-preStop]$ kubectl apply -f poststart.yml -n kuber-best-practice
pod "client" created
[vagrant@ci postStart-preStop]$ kubectl get pods -n kuber-best-practice
NAME      READY     STATUS              RESTARTS   AGE
client    0/1       ContainerCreating   0          10s
[vagrant@ci postStart-preStop]$ kubectl get pods -n kuber-best-practice
NAME      READY     STATUS              RESTARTS   AGE
client    0/1       ContainerCreating   0          12s
[vagrant@ci postStart-preStop]$ kubectl get pods -n kuber-best-practice
NAME      READY     STATUS              RESTARTS   AGE
client    0/1       ContainerCreating   0          13s
[vagrant@ci postStart-preStop]$ kubectl get pods -n kuber-best-practice
NAME      READY     STATUS              RESTARTS   AGE
client    0/1       ContainerCreating   0          15s
[vagrant@ci postStart-preStop]$ kubectl get pods -n kuber-best-practice
NAME      READY     STATUS              RESTARTS   AGE
client    0/1       ContainerCreating   0          17s
[vagrant@ci postStart-preStop]$ kubectl get pods -n kuber-best-practice
NAME      READY     STATUS              RESTARTS   AGE
client    0/1       ContainerCreating   0          20s

Đây là những gì đã xảy ra như sau:

  • Kubernetes pull image nginx
  • Nó tạo ra container và chuẩn bị khởi động nó
  • Vì chúng ta có một lifecycle trong định nghĩa, Kubernetes thực hiện postStart hook và scheduled up container lên cho đến khi script hook kết thúc.
  • postStart script tạm dừng chuỗi trong mười giây trước khi nó trả về non-zero exit status.
  • Khi Kubernetes phát hiện non-zero exit status, nó sẽ giết và khởi động lại container và toàn bộ chu trình lặp lại vô thời hạn.

Chúng ta có thể làm cho nginx start sau mười giây (mô phỏng mọi hoạt động precheck) bằng cách thay đổi postStart script.

# poststart_fix.yml
apiVersion: v1
kind: Pod
metadata:
 name: client
spec:
 containers:
   - image: nginx
     name: client
     lifecycle:
       postStart:
         exec:
           command:
             - sh
             - -c
             - sleep 10
[vagrant@ci postStart-preStop]$ kubectl apply -f poststart_fix.yml -n kuber-best-practice
pod "client" created
[vagrant@ci postStart-preStop]$ kubectl get pods -n kuber-best-practice
NAME      READY     STATUS              RESTARTS   AGE
client    0/1       ContainerCreating   0          4s
[vagrant@ci postStart-preStop]$ kubectl get pods -n kuber-best-practice
NAME      READY     STATUS              RESTARTS   AGE
client    0/1       ContainerCreating   0          9s
[vagrant@ci postStart-preStop]$ kubectl get pods -n kuber-best-practice
NAME      READY     STATUS    RESTARTS   AGE
client    1/1       Running   0          18s

Như bạn có thể thấy ở trên, Kubernetes đã thực thi tập lệnh postStart sau đó khởi động ENTRYPOINT chính của container, đó là trình nền nginx.

postStart script methods

postStart script sử dụng các phương thức sau để running the checks:

  • exec: Được sử dụng trong ví dụ trên, phương thức exec thực thi một hoặc nhiều lệnh tùy ý đối với container. Exit status xác định xem việc check đã pass hay chưa.
  • httpGet: Mở kết nối HTTP đến một local port trên container. Bạn có thể tùy ý cung cấp một path. Ví dụ: nếu chúng ta có thể sửa đổi ví dụ trước để kiểm tra xem cổng 8080 có mở hay không (giả sử một dịch vụ REST giả định) và path /status endpoint trả về valid success response.
apiVersion: v1
kind: Pod
metadata:
 name: client
spec:
 containers:
   - image: nginx
     name: client
     lifecycle:
       postStart:
         httpGet:
         port: 8080
         path: /status

uc?id=1nGmWPVUOHq-lHmLDAKUP_OiO1b-gTpAv&export=download
Url: https://www.magalix.com/blog/kubernetes-patterns-application-process-management-1

Vì sao không sử dụng init container thay cho postStart?

Good question! init container là một tính năng Kubernetes cho phép một container bắt đầu và thực hiện một hoặc nhiều nhiệm, sau đó nó bị chấm dứt. Init Container bắt đầu và dừng trước khi các container khác thực hiện, làm cho nó trở thành ứng cử viên phù hợp để thực hiện bất kỳ nhiệm vụ nào trước khi Pod khởi động.
Tuy nhiên có sự khác nhau trong cách implement giữa postStart hooks và init containers, chúng ta sẽ có một số so sánh dưới đây:

  • postStart script được thực thi bằng cách sử dụng cùng một images như là main container. Các init container có thể sử dụng cùng hoặc khác images với images được sử dụng bởi các container tiếp theo. Vì vậy, nếu các tác vụ mà bạn cần thực hiện yêu cầu một base images khác, thì bạn tốt hơn nên sử dụng init container.
  • postStart script được thực thi bên trong cùng một container. Vì vậy, nếu tập lệnh bạn cần chạy được kết hợp chặt chẽ với container, ví dụ như bạn cần thực hiện thay đổi cấu hình cho chính container, bạn nên sử dụng postStart script.
  • Tất cả các init container phải kết thúc trước khi các main container bắt đầu. Mặt khác, các postStart script dành riêng cho từng container. Vì vậy, container A sử dụng các tập lệnh postStart và khởi chạy, container B thực thi các tập lệnh postStart của nó và start, v.v. Vì vậy, nếu bạn đang lưu trữ nhiều hơn một container trên cùng một Pod và bạn cần chạy một hoặc nhiều tác vụ không dành riếng cho container nào, bạn nên sử dụng các init container. Tuy nhiên, nếu mỗi tập lệnh init dành riêng cho vùng chứa của nó, thì postStart hook là lựa chọn phù hợp.
  • Init container bắt đầu và dừng lại trước khi bất kỳ container khác bắt đầu. postStart script được thực thi song song với các container của chúng. Điều này có nghĩa là tập lệnh có thể chạy hoặc không chạy trước khi ENTRYPOINT của container khởi động. Nếu bạn cần chắc rằng logic trước khi khởi chạy luôn được thực thi trước khi main container thực hiện, thì hãy sử dụng init container.
  • Do cách chúng được thiết kế, các tập lệnh postStart có thể chạy nhiều lần. Logic ứng dụng sẽ có khả năng xử lý nhiều thực thi kiểu này. Ví dụ: nếu tập lệnh postStart thêm tài mới khoản người dùng tạm thời trước khi container chạy, thì trước tiên, nó sẽ kiểm tra xem người dùng đã được tạo chưa để nó không trả về trạng thái non-zero exit.

Chúng ta có thể làm điều tương tự như postStart hook bên trong container

Vâng, bất kỳ logic nào được triển khai thông qua postStart script đều có thể được áp dụng bằng cách thêm nó vào như một phần của lệnh ENTRYPOINT (lệnh để start container). Nhưng đây không phải là một quyết định tốt từ quan điểm thiết kế.
Nó kết hợp chặc chẽ container với pre-launch logic của nó theo cách mà nó yêu cầu mối container phải được sửa đổi riêng. Sử dụng hooks cho phép bạn thay đổi các container trong khi vẫn giữ nguyên pre-launch logic. Nó cũng cho phép bạn làm việc trên pre-launch logic độc lập với các container mà nó sẽ chạy.

preStop hook (Thực hiện command trước khi container terminates)

Chúng ta đã tìm hiểu về các tín hiệu khác nhau mà Kubernetes gửi đến các container đang chạy bên trong Pods khi muốn tắt chúng. Tuy nhiên, mặc dù container nhận SIGTERM và nó cho phép container tắt trong tối đa ba mươi giây - default, điều này có thể không đủ cho các tình huống phức tạp.
Kubernetes cung cấp preStop hook, được gọi ngay trước khi tín hiệu SIGTERM được gửi đến container. preStop hook cũng cung cấp các phương thức kiểm tra tương tự như postStart hook: httpGet và exec.
Tuy nhiên, không giống như postStart hook, nếu Kubernetes phát hiện trạng thái non-zero exit hoặc mã HTTP không thành công, nó sẽ tiếp tục quy trình tắt máy và gửi tín hiệu SIGTERM.
Chúng ta sẽ cùng xem ví vụ bên dưới, thực hiện một GET request đến endpoint là /shutdown đang chạy ở cổng 8080.

# preStop.yml
apiVersion: v1
kind: Pod
metadata:
 name: client
spec:
 containers:
   - image: nginx
     name: client
     lifecycle:
       preStop:
         httpGet:
         port: 8080
         path: /shutdown

uc?id=13UMXR_SU-ZsKFFwySrAQGoki0OVj_06A&export=download
Url:https://www.magalix.com/blog/kubernetes-patterns-application-process-management-1

Init Containers vs Lifecycle Hooks

uc?id=1qB5Nx_aE1B94W-L0VriZFSaReJO8RboI&export=download
Url: https://www.linkedin.com/pulse/kubernetes-deep-dive-part-1-init-containers-lifecycle-chauthaiwale/

Kết luận

Kết thúc bài viết ở đây mình sẽ tóm tắt lại một số nội dung sau:

  • Các ứng dụng phải phản hồi chính xác các tín hiệu SIGTERM được gửi đến nó và thực hiện clean shutdown.
  • Nếu có một logic startup điển hình cần được áp dụng cho các container trước khi chúng bắt đầu, bạn nên xem xét sử dụng các postStart hook hoặc init container tùy thuộc vào trường hợp sử dụng của bạn (tham khảo so sánh giữa cả hai cách tiếp cận trước đó trong bài viết).
  • Nếu ứng dụng quá phức tạp để thực hiện clean shutdown, chỉ cần chặn tín hiệu SIGTERM, cần có một script hoặc endpoint có thể được yêu cầu để bắt đầu quy trình tắt máy.
    Kubernetes là một dự án liên tục phát triển. Có thể có nhiều hooks hơn trong tương lai để liên lạc với container khi nó sắp được scaled up and down hoặc khi container được yêu cầu giải phóng một số tài nguyên đã tiêu thụ của nó để tránh bị giết.
    Một điều lưu ý nữa là chúng ta cần suy nghĩ nhiều hơn khi thiết kế các ứng dụng của mình để tận dụng tốt những lợi ích đó.
    Thanks all!

Tài liệu tham khảo

  1. https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/
  2. https://kubernetes.io/docs/concepts/workloads/pods/init-containers/
  3. https://dzone.com/articles/kubernetes-lifecycle-of-a-pod
  4. https://cstoku.dev/posts/2018/k8sdojo-06/
  5. https://www.magalix.com/blog/kubernetes-patterns-application-process-management-1?fbclid=IwAR1OcWEo5o77Lzu4-zw5Jh0V0gedVwvre_IjKI1wuvlCTeJ_oRxWhVJsp6Y