Trong mô hình Cloud VPS nhiều tầng, balancer thường là máy nhỏ nhất nhưng gánh trách nhiệm lớn nhất: mọi website đều đi qua nó. Khi RAM cạn, kernel kích hoạt OOM killer và tiến trình lớn nhất trên balancer gần như luôn là nginx. Đó là sự cố OOM nginx: toàn bộ site phía sau ngừng phục vụ dù minion và database vẫn sống. Bài này mô tả một tình huống điển hình, quy trình điều tra theo từng bước (kể cả hướng nghi sai hay gặp), nguyên nhân gốc, cách khắc phục và các biện pháp phòng ngừa.
Triệu chứng
Tình huống: một balancer RAM khoảng 1 GB, không có swap, đứng trước hàng chục website. Một buổi tối, mọi site sau balancer cùng không truy cập được, và tới khi có người phát hiện thì đã nhiều giờ trôi qua. Balancer là cổng vào duy nhất (xem lại Cloud VPS: mô hình Balancer – Minion – Database cho hàng trăm website PHP), nên khi nginx ở đây chết thì các tầng phía sau còn sống cũng vô ích. Dòng trạng thái đầu tiên cần đọc:
$ systemctl status nginx
Active: failed (Result: oom-kill) since ...
Process: ... ExecStop ... code=killedResult: oom-kill xác nhận đây là OOM nginx: tiến trình bị kernel giết vì hết bộ nhớ, không phải do cấu hình sai hay tự crash. Nếu trường Duration cho thấy nginx đã chạy liên tục nhiều ngày trước đó, có thể loại trừ lỗi phát sinh ngay sau một lần reload.
Điều tra sự cố OOM nginx theo từng bước
Thứ tự nên đi theo cách tiếp cận trong bài Điều tra sự cố nhiều tầng: từ balancer tới database và Kubernetes: xác nhận trạng thái dịch vụ, rồi tài nguyên, rồi cấu hình, rồi lưu lượng.
Bước 1: xác nhận OOM nginx và kiểm tra RAM
journalctl -k | grep -iE "out of memory|oom-kill"
journalctl -u nginx --since "-12h"
free -h
swapon --showNếu swapon --show không in gì, máy không có swap. Khi bộ nhớ cạn, kernel không có chỗ đẩy trang nhớ ít dùng ra, nên chỉ còn cách chọn một tiến trình để giết. Theo tài liệu tham số vm của Linux kernel, OOM killer chọn tiến trình có điểm "xấu" cao nhất, chủ yếu tính theo lượng bộ nhớ đang chiếm. Trên balancer, đó gần như luôn là nginx. Lưu ngay output của journalctl -k trước khi log xoay vòng: bảng bộ nhớ kernel in ra lúc kill là bằng chứng quý nhất.
Bước 2: đọc nginx.conf sau OOM nginx
worker_processes auto;
events {
worker_connections 16384;
}
keepalive_timeout 65;
client_max_body_size 64M;Cần hiểu đúng: worker_connections chỉ là trần số kết nối đồng thời của mỗi worker, nginx không cấp sẵn bộ đệm cho từng ấy kết nối. Phép tính kiểu "số worker × worker_connections × kích thước bộ đệm" không phải là lượng RAM đang dùng. Vấn đề của giá trị lớn là nó không giới hạn gì trên máy 1 GB: kết nối thật tăng lên thì mỗi kết nối kéo theo bộ đệm riêng, và không có lớp nào chặn lại trước khi RAM cạn.
Bước 3: lưu lượng trước lúc chết
# đếm request theo giờ trong access log (định dạng combined)
awk '{print substr($4, 2, 14)}' /var/log/nginx/access.log | sort | uniq -c | tailNếu số request tăng vọt trong một hai giờ trước khi nginx bị kill, có hai hướng nghi ngờ quen thuộc: bot cào dữ liệu hoặc một site có lưu lượng bất thường.
Bước 4: nghi phạm ồn ào nhất chưa chắc là thủ phạm
Đếm request theo từng file log của từng site, site đứng đầu thường lộ ra một client đáng ngờ:
awk -F'"' '{print $6}' /var/log/nginx/site-a.example.access | sort | uniq -c | sort -rn | head
awk '{print $9}' /var/log/nginx/site-a.example.access | sort | uniq -c | sort -rnMột tình huống hay gặp: công cụ quét lỗ hổng gõ cửa Tomcat Manager, JBoss, thử tải file backup. Nghe rất giống thủ phạm. Nhưng nếu phần lớn request của nó nhận 404 ngay lập tức với phản hồi vài trăm byte, và lượng byte của site đó còn giảm dần trước lúc máy chết, thì nó không thể là tác nhân làm cạn RAM. Một nguồn gây OOM nginx phải có lưu lượng tăng tới tận phút cuối.
Bước 5: tìm nguồn OOM nginx bằng cách đo byte
Đây là bước quyết định. Trường thứ 10 trong định dạng log combined là số byte trả cho client ($body_bytes_sent). Cộng theo từng site trong khung giờ nghi vấn:
WINDOW='<dd/Mon/yyyy:HH>' # khung giờ trước lúc nginx bị kill
for f in /var/log/nginx/*.access; do
total=$(grep "$WINDOW" "$f" 2>/dev/null | awk '{sum+=$10} END{print sum+0}')
[ "$total" -gt 0 ] && echo "$total $(basename "$f")"
done | sort -rn | headThường sẽ có một site ít request hơn site ồn ào nhất nhưng truyền nhiều dữ liệu hơn hẳn, nghĩa là mỗi phản hồi của nó rất lớn.
Bước 6: soi site truyền nhiều byte nhất
# URL có phản hồi lớn hơn 1 MB
awk '$10 > 1048576 {print $10, $7}' /var/log/nginx/site-b.example.access | sort -rn | head
# phân bố mã trạng thái
awk '{print $9}' /var/log/nginx/site-b.example.access | sort | uniq -c | sort -rnDấu hiệu điển hình: file video .m4v, .mp4 vài MB đi thẳng qua reverse proxy, nhiều phản hồi 206 Partial Content (trình duyệt tải theo dải byte), kèm 499 khi người xem tua hoặc đóng tab. Lưu lượng byte tăng liên tục tới lúc nginx bị kill là ứng viên số một cho OOM nginx.
Nguyên nhân gốc
Sự cố OOM nginx kiểu này thường là kết quả của ba yếu tố cùng lúc:
- Phục vụ file lớn qua proxy. Video, file tải về đi qua balancer, nhiều phiên đồng thời.
- Không có swap. Không có vùng đệm nào giữa "RAM gần đầy" và "kernel giết tiến trình".
- Cấu hình không có trần thực tế:
worker_connectionsvàkeepalive_timeoutlớn cho phép giữ rất nhiều kết nối, mỗi kết nối giữ lâu.
Cần thận trọng khi kết luận. Theo tài liệu module proxy của nginx, khi proxy_buffering bật, mỗi kết nối chỉ giữ trong RAM một số bộ đệm cố định, phần dư ghi ra file tạm trên đĩa. Lưu lượng đi qua không bằng lượng RAM nginx giữ. Nếu không lưu được bảng bộ nhớ kernel lúc kill, chỉ nên ghi là "nghi vấn mạnh", không phải "đã xác định".
Thời gian gián đoạn dài sau OOM nginx là một nguyên nhân riêng. Unit systemd của nginx từ gói Debian không đặt Restart=, nên khi bị kill, nginx nằm im ở trạng thái failed. Nếu lớp tự chữa như bài Self-healing tiết kiệm: tự phát hiện và khởi động lại dịch vụ chết không bao phủ balancer, hoặc chính nó cũng chạy trên máy đang hết RAM, sẽ không có gì bật lại dịch vụ.
Khắc phục OOM nginx
Tạm thời
Khởi động lại nginx, kiểm tra các site sau balancer trả lời bình thường, và lưu log kernel. Bước này đưa dịch vụ trở lại nhưng không sửa được gì: điều kiện gây OOM nginx vẫn còn nguyên.
Thêm swap cho máy RAM nhỏ
Swap là biện pháp rẻ nhất để OOM nginx không còn là phản ứng đầu tiên của kernel.
Một script idempotent tạo swapfile bằng nửa RAM, tối thiểu 512 MB, tối đa 1 GB, với vm.swappiness=10 để kernel chỉ dùng swap khi thật cần:
[ -f /swapfile ] && exit 0
TOTAL_RAM_MB=$(awk '/MemTotal/ { printf "%d", $2/1024 }' /proc/meminfo)
SWAP_MB=$(( TOTAL_RAM_MB / 2 ))
[ "$SWAP_MB" -lt 512 ] && SWAP_MB=512
[ "$SWAP_MB" -gt 1024 ] && SWAP_MB=1024
fallocate -l "${SWAP_MB}M" /swapfile || dd if=/dev/zero of=/swapfile bs=1M count="${SWAP_MB}"
chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
echo 'vm.swappiness=10' > /etc/sysctl.d/99-swappiness.conf
sysctl --systemCho systemd tự bật lại nginx
Một override nhỏ theo tài liệu systemd.service:
# /etc/systemd/system/nginx.service.d/override.conf
[Service]
Restart=on-failure
RestartSec=3sSau đó chạy systemctl daemon-reload. Tự restart không thay cho điều tra: lần OOM nginx sau, dịch vụ lên lại sau vài giây, nhưng vẫn phải có cảnh báo để người vận hành biết nó vừa bị kill.
Giảm tải và đặt trần
- Đưa video ra object storage hoặc CDN, balancer chỉ trả HTML và tài nguyên nhỏ.
- Hạ
worker_connectionsvàkeepalive_timeoutvề mức hợp với máy nhỏ, dựa trên số kết nối đồng thời đo được (stub_status), không chọn theo cảm tính.
Phòng ngừa OOM nginx
- Mọi balancer và minion RAM dưới 2 GB nên có swap để tránh OOM nginx khi tải tăng đột ngột. Kiểm tra định kỳ bằng
swapon --show. - Cảnh báo phải đến từ bên ngoài máy: phép thử HTTP vào vài site đại diện của từng balancer. Nginx chết thì chính máy đó không tự báo được.
- Kiểm tra
systemctl show nginx -p Restarttrên mọi máy quan trọng. - Đưa đoạn đo byte
awk '{sum+=$10}'vào sổ tay xử lý sự cố. - Rà các site phục vụ file media lớn qua proxy và lên kế hoạch tách ra.
Bài học
- Đừng tin số request, hãy đo byte. Khi điều tra OOM nginx, client ồn ào nhất thường không phải thủ phạm. Thủ phạm hay là site ít request nhưng mỗi phản hồi nặng hàng MB.
- Máy không swap không có vùng đệm. Swap không làm máy nhanh hơn, nhưng biến "chết ngay" thành "chậm đi", đủ thời gian để cảnh báo kịp.
- Mặc định của systemd là không restart. Đừng giả định dịch vụ quan trọng sẽ tự lên lại.
- Balancer là điểm lỗi đơn. Một lần OOM nginx làm mọi site phía sau ngừng. Các biện pháp trên chỉ giảm xác suất, không xoá được rủi ro của mô hình một balancer cho cả cụm.
- Ghi lại cả hướng nghi sai. Phần loại trừ thường tốn nhiều thời gian nhất, và chính nó dạy cách đo đúng thứ cần đo.
Bài tiếp theo
PHP-FPM treo: lần theo strace tới cuộc gọi S3 đồng bộ. Pool PHP-FPM đầy worker dù gần như không có traffic, và cách lần từ một file descriptor tới đúng dòng code đang chờ mạng.
