Headless Chrome ngốn RAM: cách đo và đặt giới hạn

Headless Chrome là cỗ máy vô hình ngốn RAM khủng khiếp nhất mà nhiều người mới dùng VPS không ngờ tới. Chạy một con bot Puppeteer lên, vài phút sau free -h báo đã dùng hết 4 GB. Mình từng thấy cảnh VPS 4 GB chết ngất chỉ vì 3 tab headless render một trang web React. Nếu bạn đang chạy VPS OpenClaw hoặc bất kỳ hệ thống automation nào dùng Chromium, bài này là dành cho bạn: làm sao đo đúng headless Chrome RAM, đặt rào cản, và dọn dẹp khi nó chạy quá đà.
Yêu cầu trước khi bắt đầu
- VPS chạy Linux (Ubuntu 24.04 LTS, Debian 12 hoặc AlmaLinux 9) với quyền sudo hoặc root.
- Đã cài headless Chrome hoặc Puppeteer/Playwright.
- Cài sẵn Docker nếu bạn chạy container (dùng
docker statsđể đo). - Hiểu cơ bản về
ps,killvà biên tập file vớinano/vim.
Vì sao headless Chrome ngốn RAM không kiểm soát?
Không giống Chrome bản desktop có thanh task manager, headless mode chạy trình duyệt hoàn chỉnh (V8 engine, renderer, GPU compositor, network stack). Mỗi tab tương đương một process riêng. Trang web hiện đại (SPA, video, WebSocket, iframe) dễ kéo Chrome từ 100 MB lên 600 MB mỗi tab. Cộng dồn: 5 tab song song = 2-3 GB RAM. Không giới hạn gì, OOM killer sẽ tóm gọn process của bạn khi swap cũng không kịp trữ.
Phần lớn lập trình viên mới dùng Puppeteer thường bỏ qua flag --disable-dev-shm-usage và không đóng browser sau mỗi lần scrape. Hậu quả: Chrome giữ bộ nhớ đã cấp phát, không trả về OS dù không còn dùng.
Cách 1 - Đo headless Chrome RAM bằng ps và smem
ps cho cái nhìn nhanh. smem cho báo cáo chính xác hơn vì nó tính RSS scaled, không chỉ resident set.
Đo nhanh với ps
ps aux --sort=-%mem | grep -i chrome | head -10
Lệnh này liệt kê top 10 process Chrome đang ngốn nhiều RAM nhất theo %MEM. Cột RSS (resident set size) in KB, chia 1024 để ra MB. Nhưng ps đếm trùng shared pages giữa các process, nên số thường cao hơn mức thực tế.
Đo chính xác với smem
sudo apt install smem -y # Debian/Ubuntu
sudo dnf install smem -y # AlmaLinux/Rocky
smem -t -p -k | grep chrome | sort -k4 -n -r | head -10
smem trả về RSS, USS (Unique Set Size, bộ nhớ chỉ riêng process đó dùng, không share), và PSS (Proportional Set Size, chia đều shared memory). USS là con số đáng tin nhất để biết một process Chrome ngốn thực sự bao nhiêu. Cột PSS cho biết tổng gánh nặng lên hệ thống.
Ví dụ output mong đợi:
PID User USS PSS RSS Command
1234 root 245.8M 278.4M 340.2M /usr/lib/chromium/chrome --type=renderer
Nhìn cột USS: renderer này chiếm ~246 MB không share được. Nếu có 5 renderer như thế, hãy gấp 5.
Cách 2 - Đo RAM headless Chrome trong container với docker stats
Nếu bạn chạy headless Chrome trong Docker, docker stats là công cụ dễ nhất. Nó hiển thị real-time CPU, RAM, NET I/O mỗi container.
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}"
Output mẫu:
NAME CPU % MEM USAGE / LIMIT MEM %
chrome_scraper_1 12.5% 1.245GiB / 2GiB 62.25%
Lưu ý: docker stats tính total memory của container (bao gồm cache). Nếu thấy container dùng gần hết limit, process sắp bị OOM kill. Bạn có thể kiểm tra log: docker logs chrome_scraper_1 2>&1 | grep -i "killed\|oom".
Mình khuyên: luôn dùng Docker với flag --memory khi chạy headless Chrome để tránh container ăn hết RAM host:
docker run -d --memory="2g" --memory-swap="2g" --name chrome_scraper my-chrome-image
Giá trị --memory-swap="2g" tắt swap cho container, đảm bảo Chrome bị kill đúng lúc thay vì chậm dần rồi swap đầy.
Cách 3 - Giới hạn headless Chrome RAM bằng flag và ulimit
Flag --disable-dev-shm-usage
Flag này là quan trọng nhất. Khi không set, Chrome ghi dữ liệu render vào /dev/shm (RAM disk, thường chỉ 64-256 MB trên VPS). Nếu trang lớn, Chrome crash vì hết /dev/shm. Set flag này để Chrome dùng /tmp thay thế:
const browser = await puppeteer.launch({
headless: "new",
args: ['--disable-dev-shm-usage', '--no-sandbox', '--disable-setuid-sandbox']
});
Với Playwright (C# hoặc Python), cũng thêm --disable-dev-shm-usage vào launch args.
Giới hạn process con bằng --max-old-space-size
Bạn có thể giới hạn heap V8 của Node.js process chạy Puppeteer. Thêm vào lệnh chạy:
NODE_OPTIONS="--max-old-space-size=512" node scraper.js
Con số 512 là MB. Nếu process vượt quá, nó tự crash thay vì treo cả hệ thống. Nhưng lưu ý: giới hạn quá thấp (dưới 256 MB) sẽ khiến Chrome hay bị crash khi render trang nặng.
Dùng ulimit shell
Bạn có thể đặt giới hạn toàn cục cho process headless Chrome trong script startup:
#!/bin/bash
ulimit -v 1048576 # virtual memory 1 GB
ulimit -m 524288 # resident set 512 MB
node scraper.js
Nhưng lưu ý: ulimit không phân biệt shared memory, dễ kill nhầm process khác nếu chạy chung script. Mình chỉ dùng trong tình huống quick-fix tạm thời.
Cách 4 - Giới hạn số tab đồng thời, dừng đúng và dọn zombie
Dùng pool song song
Đừng bao giờ mở 20 tab headless một lúc. Thiết kế queue hoặc pool: chỉ chạy tối đa 2-3 tab song song trên VPS 4 GB, 4-5 tab trên VPS 8 GB. Dùng thư viện p-limit (Node.js) hoặc asyncio.Semaphore (Python):
const pLimit = require('p-limit');
const limit = pLimit(3); // 3 tab tối đa
const tasks = urls.map(url => limit(() => scrapeOne(url)));
await Promise.all(tasks);
Đóng browser sau mỗi lần dùng
Đây là lỗi phổ biến nhất: để browser mở sau mỗi lần render. Luôn gói trong try/finally:
let browser;
try {
browser = await puppeteer.launch({ headless: "new", args: ['--disable-dev-shm-usage'] });
const page = await browser.newPage();
await page.goto('https://example.com');
// ... xử lý
} finally {
if (browser) await browser.close();
}
Nếu bạn dùng nhiều page, đóng từng page khi không còn dùng: await page.close(). Browser parent process vẫn tồn tại nhưng không ngốn thêm bộ nhớ.
Dọn zombie process
Zombie xuất hiện khi process con Chrome chết nhưng process cha chưa gọi waitpid(). Dùng ps aux | grep -w Z để kiểm tra. Nếu có, tìm PID cha và kill nó (hoặc restart service). Cách nhanh: kill toàn bộ Chrome còn sót:
pkill -f "chrome" --signal SIGTERM
sleep 2
pkill -f "chrome" --signal SIGKILL # nếu vẫn còn
Để tránh zombie, đảm bảo script gọi browser.close() đúng cách (đã nói ở trên). Nếu chạy Chrome qua child_process, dùng child.on('exit') để thu dọn.
Mình cũng thêm cron job mỗi 30 phút dọn Chrome treo (nếu process tồn tại quá 30 phút không có activity):
* * * * * pgrep -f "chrome" -a | awk '{print $1}' | while read pid; do
if [ $(ps -o etimes= -p $pid) -gt 1800 ]; then
kill -9 $pid
fi
done
Cách 5 - Dùng systemd resource control cho headless Chrome
Nếu bạn chạy headless Chrome như service systemd, có thể giới hạn RAM bằng MemoryMax và TasksMax. Sửa file service:
[Service]
ExecStart=/usr/bin/node /opt/scraper/index.js
MemoryMax=1.5G
TasksMax=20
Chạy systemctl daemon-reload && systemctl restart scraper. Systemd sẽ gửi SIGTERM khi process vượt MemoryMax, và dùng cgroups để đảm bảo kill đúng process.
Kiểm tra sau khi áp dụng: systemd-cgtop xem bộ nhớ thực tế của service.
Xử lý lỗi thường gặp
| Lỗi | Dấu hiệu | Khắc phục |
|---|---|---|
| Chrome crash vì /dev/shm | Log: "FATAL:dev_shm_handle.cc" | Thêm --disable-dev-shm-usage |
| OOM kill | dmesg | tail -20 thấy "oom-killer" + tên process | Tăng RAM host, giới hạn parallel tab, đặt memory limit Docker/systemd |
| Zombie process đầy | ps aux | grep -w Z ra nhiều dòng | Kill cha của zombie, sửa code đóng browser đúng |
| Container chạy chậm dần | docker stats thấy memory leak | Đặt --memory và --memory-swap bằng nhau, hạn chế dùng tab mới liên tục |
Câu hỏi thường gặp
Headless Chrome có dùng RAM nhiều hơn bản GUI không?
Có thể ít hơn vì không có GPU render thật, nhưng vẫn ngon RAM tương đương vì Chrome vẫn load đầy đủ renderer, V8, network stack. Bản headless thường thiếu giới hạn tab nên dễ bị "out of control".
Nên đặt memory limit bao nhiêu cho headless Chrome trên VPS 4 GB?
Để dành 1 GB cho OS và các service khác, còn 3 GB cho Chrome. Giới hạn 2 GB cho container/service, chạy max 2-3 tab song song. Nếu trang nặng, thử giảm còn 1 GB + 1 tab.
smem và ps cái nào đúng hơn?
smem cho USS/PSS chính xác hơn. ps chỉ cho RSS có share pages nên dễ cao ảo. Dùng smem để ước lượng thực tế, dùng ps để phát hiện process ngốn bất thường nhanh.
Dùng crontab dọn Chrome treo có an toàn không?
An toàn nếu bạn chỉ kill process quá thời gian cho phép (>30 phút). Nhưng nếu script của bạn đang crawl trang dài, 30 phút có thể chưa đủ, hãy điều chỉnh threshold cho phù hợp.
Có nên dùng --max-old-space-size thấp quá không?
Không nên. Dưới 256 MB Node.js sẽ không đủ heap chứa Chrome subprocess. hàng đầu 512 MB đến 1 GB tuỳ VPS.
Headless Chrome có leak RAM không?
Bản thân Chromium không leak nhiều, nhưng code Puppeteer/Playwright viết sai (không đóng page, không clear buffer) là nguyên nhân chính. Luôn gói lệnh trong try/finally.
Bài viết liên quan
- Tự dùng AI chatbot riêng với Ollama và Open WebUI trên VPS
- 10 workflow n8n hữu ích webmaster nên biết và tự host
- Giám sát VPS với Prometheus và Grafana tự host


