Kubernetes cạn kiệt inode: tại sao cảnh báo dung lượng đĩa bỏ sót mối đe dọa thực sự
Kubelet thiếu cơ chế cảnh báo sớm khi inode sắp hết, dẫn đến việc các pod bị trục xuất đột ngột dù dung lượng đĩa vẫn còn dư. Tìm hiểu cách các tệp tin nhỏ trong image container gây ra lỗi âm thầm này.
Được dịch tự động từ bản gốc tiếng Anh.
Một node worker Kubernetes gần đây đã kích hoạt cảnh báo nghiêm trọng về việc hệ thống tệp đầy, nhưng các chẩn đoán tiêu chuẩn lại cho thấy vẫn còn nhiều dung lượng đĩa trống. Thủ phạm không phải là mức sử dụng byte mà là sự cạn kiệt inode, một giới hạn tài nguyên mà kubelet chỉ giám sát khi xảy ra khủng hoảng thay vì quản lý chủ động như đối với dung lượng lưu trữ.
Chuyện gì đã xảy ra
Một kỹ sư SRE điều tra cảnh báo NodeFilesystemFilesFillingUp đã phát hiện ra trạng thái mâu thuẫn: đĩa cứng đã đầy 83%, nhưng bảng inode chỉ được sử dụng 67% với 5,3 triệu inode trống còn lại. Cảnh báo được kích hoạt do xu hướng tiêu thụ inode, chứ không phải do mức độ tuyệt đối. Trong khi mức sử dụng đĩa được giám sát bằng hai cơ chế—thu gom rác nền (background garbage collection) và trục xuất cứng (hard eviction)—thì inode chỉ có một lớp phòng thủ duy nhất: trục xuất cứng.
Quá trình thu gom rác image của Kubelet chạy khi mức sử dụng byte vượt quá 85%, xóa các image không sử dụng để giảm mức sử dụng xuống 80%. Tuy nhiên, quy trình này hoàn toàn bỏ qua số lượng tệp tin. Trên Linux, kubelet có theo dõi nodefs.inodesFree và imagefs.inodesFree, nhưng chỉ kích hoạt hành động khi số inode trống giảm xuống dưới 5%. Điều này có nghĩa là phản ứng tự động đầu tiên đối với áp lực inode cũng chính là biện pháp gây gián đoạn nhất: trục xuất các pod đang chạy để cứu node.
Cuộc điều tra cho thấy node không gặp vấn đề do log phình to hay rò rỉ volume. Thay vào đó, sự tiêu hao inode đến từ kho lưu trữ snapshot của containerd. Mỗi layer image được giải nén sẽ tạo ra một thư mục chứa các tệp tin, và những image chứa hàng nghìn tệp tin nhỏ sẽ tiêu thụ inode rất nhanh. Trong trường hợp này, một gói Node.js đơn lẻ đã đóng góp hơn 21.000 tệp tin cho mỗi snapshot. Với nhiều phiên bản của cùng một image được giữ lại trên node, hàng triệu inode đã bị tiêu thụ trong khi dung lượng đĩa vẫn còn tương đối khả dụng.
Cơ chế hoạt động
Hệ thống tệp phân bổ inode tại thời điểm tạo dựa trên tỷ lệ inode, thường là một inode cho mỗi 16.384 byte dung lượng trong ext4. Con số này là cố định; nó không thể tăng lên sau này. Các tệp tin nhỏ kém hiệu quả vì mỗi tệp tin không rỗng đều tiêu tốn ít nhất một block 4 KiB và một inode. Nếu một hệ thống tệp được lấp đầy bởi các tệp tin chỉ có một byte, inode sẽ cạn kiệt khi mới chỉ sử dụng 25% dung lượng đĩa.
Trong node gặp sự cố, phép tính gợi ý rằng tỷ lệ inode chặt chẽ hơn, khoảng 8.192 byte mỗi inode, có lẽ được cấu hình trong quá trình cung cấp ban đầu. Ngay cả với điều chỉnh này, inode vẫn sẽ cạn kiệt ở mức sử dụng đĩa 50% nếu được lấp đầy bởi các tệp tin nhỏ. Bộ snapshotter overlayfs của containerd giải nén từng layer riêng biệt vào thư mục riêng của nó. Nó loại bỏ trùng lặp các layer nén theo digest nhưng không chia sẻ bất cứ thứ gì giữa các snapshot đã giải nén. Nếu hai bản build tạo ra các layer có digest khác nhau—ngay cả khi chỉ do thay đổi timestamp—containerd sẽ lưu trữ hai bản sao đầy đủ của mọi tệp tin.
Kubelet không liên kết mức sử dụng byte với mức sử dụng inode. Nó chờ cho đến khi ngưỡng inode trống 5% bị vi phạm trước khi hành động. Lúc đó, node đã ở trong chế độ khẩn cấp. Việc thiếu một bước "mềm" trung gian để trục xuất hoặc thu gom rác cho inode có nghĩa là các kỹ sư không nhận được cảnh báo nào cho đến khi sự ổn định của pod bị đe dọa.
Chi tiết quan trọng
- Thu gom rác image của Kubelet kích hoạt ở mức sử dụng byte 85% nhưng không có ngưỡng tương đương cho mức sử dụng inode.
- Trục xuất cứng cho inode chỉ kích hoạt khi số inode trống giảm xuống dưới 5%, biến nó thành biện pháp cuối cùng.
- Hệ thống tệp Ext4 có số lượng inode cố định được xác định tại thời điểm tạo, thường là một inode cho mỗi 16.384 byte trừ khi được tùy chỉnh.
- Containerd giải nén mỗi digest layer độc nhất vào một thư mục snapshot riêng biệt, làm trùng lặp các tệp tin nhỏ giữa các phiên bản.
- Một gói Node.js đơn lẻ như
@mui/icons-materialcó thể đóng góp hơn 21.000 tệp tin vào một layer image. - Sử dụng lệnh
du --inodes -xS /var | sort -rhgiúp xác định các thư mục có số lượng tệp tin cao mà không vượt qua ranh giới hệ thống tệp.
Tại sao điều này quan trọng
Đối với các kỹ sư hạ tầng, hành vi này phơi bày một điểm mù trong giám sát tiêu chuẩn. Hầu hết các đội ngũ theo dõi chặt chẽ tỷ lệ phần trăm sử dụng đĩa, giả định rằng duy trì dưới 80% đảm bảo an toàn. Tuy nhiên, các ứng dụng tạo ra nhiều tệp tin nhỏ—chẳng hạn như những ứng dụng có node_modules lớn, môi trường ảo Python hoặc các dependency vendored—có thể làm cạn kiệt inode lâu trước khi dung lượng đĩa trở nên nghiêm trọng. Điều này dẫn đến các vụ trục xuất pod bất ngờ và mất ổn định node trông như không liên quan đến các chỉ số lưu trữ.
Nguyên nhân gốc rễ thường nằm ở thực hành build hơn là cấu hình cluster. Các Dockerfile một giai đoạn (single-stage) sao chép mã nguồn và dependency vào image cuối cùng tạo ra các layer chứa đầy tệp tin nhỏ. Nếu không có quy tắc .dockerignore phù hợp hoặc build đa giai đoạn (multi-stage), mỗi lần chạy CI có thể tạo ra các digest layer mới do thay đổi timestamp, buộc các node phải giữ lại nhiều bản sao của các layer nặng về tệp tin này. Hiểu rõ cơ chế này cho phép các đội ngũ chuyển trọng tâm từ gỡ lỗi node phản ứng sang tối ưu hóa image chủ động.
Bạn có thể làm gì
- Kiểm toán các image container của bạn để tìm số lượng tệp tin cao bằng cách sử dụng
du --inodestrên các artifact đã build trước khi đẩy chúng lên registry. - Triển khai Dockerfile đa giai đoạn để đảm bảo chỉ các artifact đã biên dịch và dependency sản xuất mới đi vào image cuối cùng.
- Sử dụng
.dockerignoređể loại trừnode_modules,.gitvà cache build khỏi các chỉ thịCOPYnhằm ngăn chặn sự trùng lặp tệp tin không cần thiết. - Giám sát mức sử dụng inode song song với dung lượng đĩa trong hệ thống quan sát (observability) của bạn.



