Chuyện bắt đầu từ đâu
Một service Node chạy nhiều năm trên VM dưới PM2, giờ được đóng container và đưa lên Kubernetes. Dockerfile giữ nguyên dòng lệnh vốn vẫn chạy tốt: bật nó bằng PM2, cluster mode, mỗi core một worker. Cái chart thì có thứ chart nào cũng có — một autoscaler gắn vào deployment. Không nửa nào viết sai cả. Mỗi nửa là một câu trả lời đúng cho đúng cái câu hỏi mà nửa kia cũng đang trả lời, và câu hỏi đó là nên có bao nhiêu bản của tiến trình này đang chạy.
Hai hệ số nhân trên cùng một trục
PM2 cluster mode fork ra các worker bên trong container. Autoscaler thì nhân số replica của pod. Mức concurrency thật là tích của hai thứ đó, và cái tích ấy là một con số không ai chọn. Người đặt số fork đang tính theo cỡ một cái máy. Người đặt khoảng replica đang tính theo cỡ một cái service. Không ai đang tính phép nhân cả. Sau này chỉnh một bên thôi là concurrency thật nhảy theo hệ số, trong khi người đi chỉnh vẫn tin mình chỉ dịch nó lên một nấc.
Limit thuộc về cgroup, còn heap thì không biết điều đó
Memory limit của container áp cho cả cgroup — tức là tất cả worker cộng lại. Mỗi worker, vốn là một tiến trình riêng, tự tính heap của nó như thể nó sở hữu nguyên cái máy. Một mức trần cho mỗi worker, nhìn riêng ra thì rất thoải mái, đem nhân với số fork là vượt luôn limit của pod. Vượt cái là kernel lấy nguyên container: pod bị OOM-kill, chứ không phải runtime ném ra lỗi heap, nên cái hỏng đến bằng ngôn ngữ của platform chứ không phải của Node.
Đúng cái tín hiệu mà các fork làm phẳng
Autoscaler nhìn metric ở mức pod, tức là đã bình quân qua mọi thứ bên trong pod. Một worker căng cứng trong khi mấy anh em còn lại rảnh, bình quân lại ra một con số đọc như tải vừa phải. Việc chia không đều giữa các fork mới là chuyện thường — một request dài, một job nặng — nên mức bình quân của pod im ắng nhất đúng lúc có một worker đang có vấn đề. Các fork đang làm phẳng chính cái tín hiệu mà autoscaler sinh ra để đọc.
Crash loop nấp trong một pod khoẻ mạnh
Đây là chỗ làm mình mất lòng tin vào cách dựng này nhất. PM2 thấy worker chết thì dựng lại tại chỗ. Container không bao giờ thoát, nên pod vẫn Ready và số restart không hề nhúc nhích. Một service có thể đang crash loop liên tục trong khi mọi chỗ mà một người vận hành Kubernetes nhìn vào đều báo là nó khoẻ.
Đó không phải lỗi của PM2. Đó là PM2 làm đúng việc của nó, ở một nơi đã có sẵn một thứ khác làm việc đó rồi. Hợp đồng của container là một tiến trình, và nó hẹp một cách có chủ đích: signal, exit code và câu trả lời của probe là toàn bộ vốn từ mà platform có để biết rằng đang có gì đó sai. Một supervisor đứng chắn ở giữa sẽ nuốt cả ba. SIGTERM rơi vào supervisor. Exit code là của supervisor. Liveness probe thì do worker nào vớ được kết nối trả lời, và câu trả lời đó chẳng nói gì về những worker còn lại.
Chọn một tầng để nó nhân
Ở trong Kubernetes, tầng đó là Kubernetes. Mỗi container một tiến trình. Replica là cái núm chỉnh concurrency, và là cái duy nhất. Cứ để tiến trình chết và để pod chết theo, vì một pod chết là một pod mà platform còn xử lý được — dựng lại, xếp sang node khác, báo động. Những gì PM2 đang cho ở đây thì orchestrator đã cho sẵn, mà cho ở dạng phần còn lại của hệ thống nhìn thấy được.
Hai chi tiết cần mang theo. Readiness phải phản ánh đúng cái thứ thật sự đang phục vụ traffic, không phải ý kiến của một supervisor về nó. Và mức trần bộ nhớ phải tính từ limit của cgroup chia cho số tiến trình dùng chung nó, tuyệt đối không tính từ bộ nhớ của cái máy — đó là con số mà container không sở hữu.
Không có chỗ nào ở đây khai tử PM2 cả; chỉ là đổi chỗ nó. Trên một con VM thường, một service nội bộ nhỏ, một cái máy mà bên dưới chẳng có ai điều phối gì, thì phần quản tiến trình của PM2 chính là platform, và là một platform tốt. Xung đột không nằm ở PM2. Nó nằm ở chỗ chạy hai bộ điều phối mà không nói cho bên nào biết về bên kia.
Có một việc mình đã không làm: mình chưa bao giờ đo cái giá của việc chồng hai tầng. Không benchmark fork-trong-pod so với một tiến trình một container, không có gì để đăng lên đây cả. Lập luận ở đây là lập luận về cấu trúc — về chuyện thành phần nào được phép biết chuyện gì — và nếu bạn muốn một con số cho phần hao thì mình không có.
Điều rút ra
Hai hệ thống cùng tin rằng mình nắm quyền scale thì không cộng vào nhau, mà triệt tiêu nhau. Bên nào cũng đang suy luận trên một workload mà bên kia âm thầm nắn lại, và tầng nằm dưới là tầng chịu thiệt, vì nó là tầng đáng lẽ phải được báo mà không bao giờ được báo.
Triệu chứng không bao giờ là một dòng ghi rằng scale đang sai. Nó là một pod báo Ready, một dashboard báo tải vừa phải, một số restart không nhúc nhích từ lần deploy gần nhất, và request thì đang xếp hàng phía sau tất cả chỗ đó.