"Ứng dụng chậm mà CPU chỉ 60%" là một trong những báo cáo sự cố khó chịu nhất. Bài này dựng lại đúng tình huống đó và chỉ ra nguyên nhân.

Điều tiết CPU: CPU trung bình không phát hiện được

Tải theo đợt: làm 15 ms rồi nghỉ 100 ms

Quota 1 nhân. Mỗi vòng, tất cả luồng cùng làm một mẩu việc ~15 ms rồi cả nhóm nghỉ 100 ms — mô phỏng một dịch vụ nhận yêu cầu theo nhịp.

Số luồng CPU trung bình p50 p99 Bị ghim
1 37,5% 60,81 ms 66,95 ms 0
2 62,6% 97,59 ms 113,43 ms 60
4 83,6% 219,36 ms 308,98 ms 160
8 94,2% 703,55 ms 987,06 ms 461

Dòng thứ hai là dòng quan trọng. CPU trung bình 62,6% — mọi bảng điều khiển sẽ hiện màu xanh và ghi "còn 37% dư địa" — trong khi tiến trình đã bị ghim 60 lần và p99 là 113 ms.

Vì sao CPU trung bình không thấy được

Quota được tính theo từng chu kỳ 100 ms, không tính theo phút.

Một luồng: mỗi chu kỳ làm 15 ms, còn dư 85 ms quota. Không bao giờ chạm trần. Ghim 0 lần.

Tám luồng cùng lúc: chu kỳ đầu tiên chúng cần 8 × 15 = 120 ms thời gian CPU, mà quota chỉ cho 100 ms. Đến mili giây thứ 100, cả tám luồng bị treo cho tới chu kỳ sau.

Trung bình một phút vẫn chỉ 94% vì phần nghỉ 100 ms kéo con số xuống. Nhưng độ trễ mà người dùng cảm nhận đã tăng gấp mười lăm lần.

Nói lại lần thứ hai: mức sử dụng CPU trung bình không phát hiện được việc bị ghim. Hai đại lượng này đo hai thứ khác nhau, và chỉ một trong hai liên quan tới độ trễ.

Với tải liên tục cũng vậy, chỉ khác cách nhìn

Cùng quota 1 nhân, 2.000 "yêu cầu" mỗi cái ~2 ms CPU, chạy liên tục:

Số luồng CPU dùng p50 p95 p99 Bị ghim
1 99,7% 7,27 8,24 9,05 ms 26
2 100,1% 7,96 58,92 61,20 ms 157
4 100,5% 8,91 86,45 88,79 ms 172
8 100,3% 94,44 98,50 100,48 ms 163
16 100,4% 193,26 298,70 395,36 ms 205

CPU y hệt nhau ở cả năm dòng — quanh 100% quota. p99 chênh 44 lần.

Chú ý các giá trị p95 và p99: 58,92 / 86,45 / 98,50 / 100,48 ms. Chúng bám quanh bội số của 100 ms — đúng bằng chu kỳ quota. Đó là chữ ký không thể nhầm của việc bị ghim: những yêu cầu chậm không chậm ngẫu nhiên, chúng chậm đúng một chu kỳ.

Nếu biểu đồ độ trễ của bạn có một cụm dồn ở 100 ms, 200 ms hoặc 300 ms trong khi phần lớn yêu cầu xong trong vài mili giây, hãy đi đọc cpu.stat trước khi làm bất cứ điều gì khác.

Chỉ số cần cảnh báo

grep -E 'nr_periods|nr_throttled|throttled_usec' /sys/fs/cgroup/cpu.stat
Trường Nghĩa
nr_periods Số chu kỳ 100 ms đã trôi qua
nr_throttled Số chu kỳ trong đó có ai đó bị treo
throttled_usec Tổng thời gian bị treo

Tỷ lệ đáng theo dõi là nr_throttled / nr_periods. Trên 5% là đã ảnh hưởng tới đuôi độ trễ; trên 20% là dịch vụ đang bị bóp nghẹt.

Với Prometheus và cAdvisor:

rate(container_cpu_cfs_throttled_periods_total[5m])
  / rate(container_cpu_cfs_periods_total[5m])

Chỉ số này có sẵn ở mọi cụm Kubernetes và gần như không ai đặt cảnh báo lên nó.

Ba cách chữa

Một — giảm số luồng cho khớp quota. Đây là cách chữa đúng và rẻ nhất, và bảng đầu tiên cho thấy vì sao: một luồng với quota 1 nhân không bị ghim lần nào. Xem phần 34 cho danh sách biến môi trường của từng runtime.

Hai — tăng quota, không tăng số luồng. Nếu công việc thật sự cần nhiều nhân thì cấp nhiều nhân. Cấp 1 nhân rồi chạy 8 luồng là cấu hình xấu nhất trong mọi bảng ở trên.

Ba — bỏ hẳn giới hạn CPU, chỉ giữ requests. Đây là lựa chọn gây tranh cãi trong cộng đồng Kubernetes, và số liệu ở đây ủng hộ nó: requests đảm bảo phần tối thiểu qua trọng số CFS (phần 6), còn limits chỉ thêm cơ chế ghim. Bỏ limits thì pod dùng được nhân rảnh và không bao giờ bị treo.

Đổi lại là mất khả năng dự đoán: một pod bùng phát có thể ăn hết nhân rảnh của node. Với cụm chạy tải hỗn hợp thì đó là rủi ro thật.

Nếu vẫn giữ limits, hãy đặt nó rộng rãi — gấp hai tới bốn lần requests — chứ đừng đặt sát mức trung bình. Quota sát mức trung bình đảm bảo mọi đợt bùng phát đều bị ghim.

Chỗ tôi không đo được

cpu.max có tham số chu kỳ (mặc định 100 ms). Rút ngắn chu kỳ xuống 10 ms về lý thuyết làm việc ghim mịn hơn — bị treo 10 ms thay vì 100 ms:

echo "100000 10000" > /sys/fs/cgroup/cpu.max     # van 1 nhan, chu ky 10 ms

Docker không phơi tham số này ra và tôi không sửa được cgroup của container từ bên trong, nên không có số liệu. Về lý thuyết đổi lại là chi phí quản lý cao hơn; ai chạy trên máy chủ tự quản có thể tự đo.

Thử ba mươi giây

c=/sys/fs/cgroup
p1=$(awk '/nr_periods/{print $2}' $c/cpu.stat); t1=$(awk '/nr_throttled/{print $2}' $c/cpu.stat)
sleep 30
p2=$(awk '/nr_periods/{print $2}' $c/cpu.stat); t2=$(awk '/nr_throttled/{print $2}' $c/cpu.stat)
awk -v p=$((p2-p1)) -v t=$((t2-t1)) 'BEGIN{
  if (p>0) printf "trong 30 giay: %d chu ky, bi ghim %d (%.1f%%)\n", p, t, t*100/p
  else print "khong doc duoc cpu.stat"
}'
echo "cpu.max: $(cat $c/cpu.max 2>/dev/null)   nproc: $(nproc)"

Chạy lệnh này ngay trong container đang phục vụ thật. Nếu tỷ lệ trên 5% mà biểu đồ CPU của bạn vẫn dưới 70%, bạn vừa tìm ra nguyên nhân của những yêu cầu chậm mà không ai giải thích được.

Phần sau: không gian tên — đo chi phí cách ly.