Xin chào độc giả của 2coffee.dev, rất lâu rồi mới gặp lại mọi người. Cách đây một hai tuần trước, tôi gặp phải một vấn đề khá thú vị khi triển khai hệ thống. Định thôi không viết nhưng nghĩ lại chắc sẽ có người gặp phải trường hợp này nên lại cặm cụi viết ra. Âu cũng là một lần ghi chép để nhớ và chia sẻ nó đến với mọi người.
Hệ thống mà tôi đảm nhiệm có một dịch vụ (service) khá cũ, được triển khai dựa trên pm2 bằng hạ tầng VM của GCP. Gọi là cũ vì nó đã chạy rất lâu rồi, từ trước cả khi nhận việc và chẳng có thêm bản cập nhật nào nữa. Tất cả chức năng đều trong giai đoạn ổn định, chỉ dừng lại ở mức duy trì cho một tệp người dùng nhất định. Chuyện chẳng có gì nếu như gần đây số lượng người sử dụng bỗng tăng lên, hoặc cũng có khi là do một nguyên nhân nào đó mà lượng người dùng có logic phức tạp khiến hệ thống thi thoảng lại rơi vào trường hợp quá tải. CPU tăng, RAM tăng đến một mức nào đó... Bùm! Máy chủ lăn ra chết.
VM này chỉ được phân bổ lượng tài nguyên khiêm tốn: 1CPU 2GB RAM. Nên khi CPU hoặc RAM tăng lên đột ngột thì nó sẽ bị treo mà không thể ssh
vào được. Nhận thấy sự cố, ngay lập tức tôi bắt tay vào đi tìm giải pháp. Trước mắt có thể nâng tài nguyên máy chủ lên nhưng thực tế đã chứng minh điều đó không hiệu quả, máy chủ vẫn "quay đơ" ra ở một thời điểm nào đó không thể đoán trước. Cũng không thể sửa lỗi ngay được vì nguồn lực có hạn mà chúng tôi vẫn còn nhiều công việc khác cần ưu tiên hơn. Lúc này điều khả thi nhất nghĩ ra là giới hạn lượng tài nguyên sử dụng xuống cho dịch vụ này.
Thật may mắn vì pm2 có chức năng giới hạn bộ nhớ sử dụng. Khi thiết lập giới hạn này, mỗi khi tiến trình sử dụng bộ nhớ tới hạn thì nó tự khởi động lại nhằm mục đích giải phóng bộ nhớ. Việc tràn bộ nhớ rất nguy hiểm trong VM vì nó khiến máy chủ bị treo mà không thể thao tác được gì, kể cả việc ssh
vào máy chủ để khắc phục sự cố rất khó khăn.
Thiết lập rất đơn giản. Chỉ cần chạy một lệnh.
pm2 start api.js --max-memory-restart 300M
Với --max-memory-restart
là giới hạn bộ nhớ. Mỗi 30 giây, pm2 sẽ quét một lần và khởi động lại dịch vụ nếu cần.
Tưởng chỉ cần giới hạn bộ nhớ là xong nhưng tiếp tục theo dõi thì một vấn đề khác lại xuất hiện: CPU cũng tăng theo.
pm2 không có tính năng giới hạn tài nguyên CPU cho một dịch vụ. Nếu muốn giới hạn cần phải tìm một công cụ khác hoặc công cụ hỗ trợ. Ví dụ nếu dùng Docker thì đã có sẵn cấu hình tài nguyên được phép sử dụng. Rất tiện. Sau một hồi tìm kiếm, tôi tìm thấy cpulimit là một công cụ độc lập giúp giới hạn tài nguyên CPU cho một tiến trình.
Mỗi một dịch vụ trong pm2 chạy trong một tiến trình. Khi gõ pm2 ls
sẽ thấy một cột có tiêu đề PID - tương ứng với Process ID của dịch vụ đó. Khi dùng lệnh ps -fp PID
sẽ thấy thông tin chi tiết của tiến trình.
Sử dụng cpulimit tương đối đơn giản. Sau khi cài đặt, sử dụng lệnh.
$ cpulimit -p PID -l 80 -b
Với PID
là PID của tiến trình cần giới hạn, -l
là mức CPU tối đa và -b
để chạy tiến trình ở dưới nền (background). cpulimit giữ mức sử dụng CPU không vượt quá mức được thiết lập, vì thế vào giờ cao điểm máy chủ có thể xử lý chậm hơn bình thường.
Tưởng sau khi thiết lập xong cả 2 giới hạn là có thể ngủ ngon nhưng không, một vấn đề mới lại xuất hiện.
Mỗi lần pm2 khởi động lại dịch vụ, PID của tiến trình bị thay đổi theo. Theo lẽ thường tìm cách cố định PID nhưng đó là điều bất khả thi vì nó được cấp phát ngẫu nhiên. cpulimit ngoài cấu hình theo PID thì còn cấu hình được theo một vài tiêu chí như đường dẫn của tệp thực thi nhưng trong các lần thử nghiệm đều không thành công. Tưởng đâu đi vào bế tắc thì nhớ ra pm2 có một tính năng nâng cao gọi là PM2 API.
PM2 API là tập hợp các API của pm2 giúp can thiệp vào công cụ quản lý tiến trình này. Một trong số đó là khả năng lắng nghe sự kiện của các tiến trình đang chạy trên pm2. Hiểu đơn giản có thể coi nó như là hook. Mỗi sự kiện phát ra đều có thể lắng nghe và thực thi nhiệm vụ liên quan. Áp dụng vào trường hợp này, mỗi khi dịch vụ khởi động lại, lắng nghe và chạy lại lệnh cpulimit
để đặt lại giới hạn.
Cách làm rất đơn giản, bạn đọc có thể tham khảo tệp js
mà tôi viết như sau.
const pm2 = require("pm2");
const { spawn } = require("child_process");
const fs = require("node:fs");
const PM_CONFIGURATIONS = [{ pm_id: 1, cpu_limit: "80" }];
pm2.connect((err) => {
if (err) {
console.error("PM2 connect error:", err);
process.exit(2);
}
pm2.launchBus((err, bus) => {
console.log("PM2 launchBus");
if (err) {
console.error("PM2 launchBus error:", err);
process.exit(2);
}
bus.on("process:event", (data) => {
// Chỉ xét event start hoặc restart
if (!["start", "restart", "online"].includes(data.event)) return;
let pid = null;
const { pm_id, name } = data.process;
pid = data.process.pid;
if (!pid) {
// lấy pid từ file log pm_pid_path
const pm_pid_path = data.process.pm_pid_path;
const pm_pid = fs.readFileSync(pm_pid_path, "utf8");
pid = pm_pid;
}
console.log(`Event=${data.event} name=${name} pm_id=${pm_id} pid=${pid}`);
// Tìm cấu hình tương ứng
const config = PM_CONFIGURATIONS.find((config) => config.pm_id === pm_id);
if (config) {
// Gắn cpulimit nếu tìm thấy cấu hình
console.log(`→ Áp cpulimit ${config.cpu_limit}% cho PID=${pid}`);
spawn("cpulimit", ["-p", pid, "-l", config.cpu_limit, "-b"]);
} else {
// Không làm gì nếu không tìm thấy cấu hình
console.log(`→ Bỏ qua pm_id=${pm_id}`);
}
});
});
});
PM_CONFIGURATIONS
chứa thông tin cấu hình các dịch vụ cần được lắng nghe để mỗi khi khởi động lại, thực hiện tìm kiếm PID mới được gán cho nó và dùng lệnh cpulimit
để giới hạn CPU.
Qua bài viết này, tôi đã chia sẻ cách tối ưu hóa và kiểm soát tài nguyên dịch vụ trên PM2 trong môi trường hạn chế tài nguyên. Đầu tiên, việc giới hạn bộ nhớ với tham số --max-memory-restart
giúp giảm thiểu nguy cơ tràn bộ nhớ và treo máy chủ, đảm bảo dịch vụ tự động khởi động lại khi cần thiết. Tuy nhiên, khi vấn đề CPU tăng cao xuất hiện, giải pháp bổ sung là sử dụng công cụ cpulimit
để giới hạn mức sử dụng CPU cho từng tiến trình cụ thể. Dù vậy, việc PID thay đổi mỗi lần dịch vụ khởi động lại đã đặt ra một thách thức mới.
Để khắc phục, tôi đã tận dụng PM2 API để tự động lắng nghe các sự kiện như khởi động hoặc khởi động lại dịch vụ, từ đó cập nhật PID và gán lại cpulimit
một cách tự động. Đây không chỉ là một cách tiếp cận thực tế mà còn là một gợi ý hữu ích cho những ai gặp bài toán tương tự. Hy vọng bài viết sẽ giúp ích cho bạn trong việc xử lý các vấn đề liên quan đến quản lý tài nguyên trên pm2.
Bí mật ngăn xếp của Blog
Là một lập trình viên, bạn có tò mò về bí mật công nghệ hay những khoản nợ kỹ thuật về trang blog này? Tất cả bí mật sẽ được bật mí ngay bài viết dưới đây. Còn chờ đợi gì nữa, hãy bấm vào ngay!
Đăng ký nhận thông báo bài viết mới
Bình luận (0)