SSD Nodes Learn 🎉 VPS từ $5.50/tháng
Hướng dẫn Matt ConnorBởi Matt Connor

Cách giới hạn CPU và RAM cho service bằng systemd

Hướng dẫn cấu hình MemoryMax và CPUQuota để kiểm soát tài nguyên tiến trình trên Linux. Tránh lỗi treo VPS khi service bị OOM kill hoặc ngốn CPU bằng file drop-in systemd.

Giới hạn bộ nhớ và CPU của tiến trình bằng systemd drop-in

Bạn giới hạn bộ nhớ và CPU của tiến trình trên một VPS Linux bằng cách thêm vài dòng vào unit chạy tiến trình đó. MemoryMax= là giới hạn cứng về bộ nhớ. CPUQuota= là giới hạn về thời gian xử lý của bộ vi xử lý. Cả hai đều được thực thi bởi cgroup v2 (control groups, phiên bản 2), tính năng kernel mà systemd đã sử dụng để quản lý mọi service trên máy chủ.

sudo systemctl edit myapp.service

Lệnh này mở một file drop-in với các hướng dẫn trong phần chú thích. Hãy thêm nội dung này vào phía trên chúng:

[Service]
MemoryHigh=512M
MemoryMax=768M
MemorySwapMax=0
CPUQuota=80%
TasksMax=128
sudo systemctl daemon-reload
sudo systemctl restart myapp.service
systemctl show myapp.service -p MemoryHigh -p MemoryMax -p CPUQuotaPerSecUSec -p TasksMax

systemctl show phải hiển thị lại các con số của bạn theo đơn vị của kernel: MemoryMax=805306368CPUQuotaPerSecUSec=800ms. Nếu nó in ra MemoryMax=infinity, nghĩa là file drop-in chưa được load. Hãy kiểm tra xem file đã nằm tại /etc/systemd/system/myapp.service.d/override.conf chưa, và đảm bảo nó bắt đầu bằng header [Service], vì một dòng cấu hình không có section phía trên sẽ khiến systemd ghi log Assignment outside of section. Ignoring. và khởi động service mà không áp dụng bất kỳ giới hạn nào.

Phần còn lại của hướng dẫn này giải thích cách chọn các con số đó và những lỗi vẫn có thể xảy ra sau khi đã thiết lập.

Tại sao một tiến trình chạy quá tải làm treo VPS dù không đầy bộ nhớ

Một tiến trình chạm ngưỡng giới hạn bộ nhớ cứng sẽ bị hủy trong khoảng một giây và dịch vụ sẽ tự khởi động lại. Đó là trường hợp tốt. Trường hợp xấu là khi không có gì bị hủy: máy chủ vẫn phản hồi ping, SSH chấp nhận kết nối, nhưng dấu nhắc lệnh shell không bao giờ xuất hiện. Máy vẫn hoạt động và bận rộn, nhưng không có công việc nào hữu ích được thực hiện.

Đây là cơ chế hoạt động, vì nó không rõ ràng ngay từ đầu. Khi bộ nhớ trống xuống thấp, kernel sẽ thu hồi các trang (page) thay vì cấp phát trang mới. Các trang rẻ nhất để thu hồi là các trang có file hỗ trợ (file-backed), và page cache chứa mã thực thi của mọi thứ đang chạy. Vì vậy, kernel đẩy các trang text của sshd ra ngoài, và lệnh tiếp theo mà sshd thực thi sẽ gây ra lỗi trang (page fault), buộc hệ thống phải đọc lại các byte đó từ bộ nhớ lưu trữ. Mọi tiến trình cuối cùng đều phải chờ đĩa thay vì được chạy. Các trang này liên tục bị đẩy ra và nạp lại trong một vòng lặp, hiện tượng này gọi là thrashing.

Có hai yếu tố khiến tình trạng này trên VPS tồi tệ hơn so với laptop. Lưu trữ thường là dạng network attached hoặc chia sẻ, nên mỗi lỗi trang tốn nhiều mili giây hơn so với thiết bị NVMe cục bộ. Và kernel không đo thời gian, nó đo sự thất bại: chừng nào việc thu hồi vẫn trả về được một trang, dù chậm đến đâu, kernel vẫn tin rằng nó đang tiến triển và không gọi trình out of memory (OOM) killer. Một máy chủ có thể nằm trong trạng thái đó nhiều phút trước khi có bất kỳ tiến trình nào bị hủy.

Bạn có thể quan sát quá trình này. Kernel xuất thông tin áp lực (pressure stall information - PSI) trên Linux 4.20 trở lên:

cat /proc/pressure/memory
cat /proc/pressure/io
some avg10=63.72 avg60=41.02 avg300=12.33 total=13729481
full avg10=48.15 avg60=30.44 avg300=8.90 total=9114233

Dòng full là dòng quan trọng. full avg10=48.15 có nghĩa là trong mười giây qua, 48% thời gian mọi tác vụ có thể chạy trên máy đều bị treo để chờ xử lý bộ nhớ, vì vậy không có gì được thực thi. Một máy chủ khỏe mạnh sẽ có chỉ số gần bằng không trên full. Trên 10, con người sẽ cảm thấy máy chậm, và từ 40 trở lên là trạng thái mà mọi người thường mô tả là bị treo.

Đây cũng là lý do tại sao một giới hạn (limit) đơn thuần không phải là một lời hứa. Một unit bị giữ dưới MemoryHigh= sẽ bị điều tiết (throttle) thay vì bị hủy, vì vậy nó vẫn sống và vẫn chậm, và không có gì khởi động lại nó vì từ góc nhìn của systemd, nó chưa bao giờ thất bại. Một unit bị giới hạn mà vẫn được phép swap sẽ tạo ra các thao tác đọc và ghi được tính vào unit đó nhưng lại được phục vụ bởi một thiết bị dùng chung, vì vậy nó có thể đẩy /proc/pressure/io lên cao đối với mọi dịch vụ khác trên máy. Các giới hạn chỉ quyết định ai là người phải trả giá cho sự thiếu hụt, chúng không thể tạo ra thêm dung lượng.

Kiểm tra xem VPS của bạn có chạy cgroup v2 không

stat -fc %T /sys/fs/cgroup

cgroup2fs là hệ thống phân cấp thống nhất, đây là yêu cầu bắt buộc cho mọi thiết lập bên dưới. tmpfs nghĩa là máy chủ đã khởi động với cấu trúc v1 cũ, nơi mà MemoryHigh=MemorySwapMax= không tồn tại và hành vi OOM trên mỗi unit sẽ khác biệt. Ubuntu 22.04 trở lên, và Debian 11 trở lên, sử dụng v2 làm mặc định. Một image cũ, hoặc kernel khởi động với systemd.unified_cgroup_hierarchy=0, sẽ không sử dụng v2.

Trên cgroup v2, systemd tự động bật tính năng theo dõi bộ nhớ cho mọi unit, vì vậy các con số đã có sẵn:

systemd-cgtop -m

Lệnh này liệt kê các cgroup được sắp xếp theo mức sử dụng bộ nhớ, đây là cách nhanh nhất để trả lời câu hỏi "cái gì đang ngốn tài nguyên máy chủ" khi hệ thống vẫn còn phản hồi. Nếu đây là server mới, việc thiết lập tài khoản và firewall trong mười phút đầu tiên trên một VPS mới cần được thực hiện trước bước này.

MemoryHigh điều tiết, MemoryMax tiêu diệt.

Sự khác biệt giữa hai thiết lập bộ nhớ này quyết định hình thái của một lỗi.

  • MemoryHigh= là giới hạn mềm. Khi vượt quá mức này, kernel sẽ thu hồi bộ nhớ từ cgroup đó một cách quyết liệt và cố tình làm chậm các yêu cầu cấp phát. Mức sử dụng vẫn có thể vượt quá con số này và không có tiến trình nào bị kill.
  • MemoryMax= là giới hạn cứng. Khi một yêu cầu cấp phát không thể được đáp ứng trong giới hạn này, OOM killer sẽ chạy bên trong cgroup đó và kill một trong các tiến trình của chính unit đó.

Vế thứ hai chính là lý do thực sự để thiết lập MemoryMax= cho bất kỳ thứ gì bạn không hoàn toàn tin tưởng. Nếu không có giới hạn, tình trạng thiếu hụt bộ nhớ sẽ trở thành vấn đề của toàn bộ máy chủ, và OOM killer toàn cục sẽ chọn nạn nhân dựa trên oom_score, thường có nghĩa là tiến trình lớn nhất. Tiến trình lớn nhất thường là database của bạn, chứ không phải script bị leak. Với một giới hạn, việc kill chỉ xảy ra bên trong unit gây ra vấn đề.

Hãy thiết lập cả hai, với MemoryHigh= thấp hơn MemoryMax= khoảng 20 đến 30 phần trăm. Khoảng cách này là vùng cảnh báo: một lỗi leak chậm sẽ vượt qua High và biểu hiện thành một dịch vụ bị chậm, trong khi một spike đột ngột sẽ đi thẳng qua Max và bị kill.

Các giá trị phần trăm được tính dựa trên bộ nhớ vật lý đã cài đặt, vì vậy MemoryMax=25% trên gói 4 GB là 1 GB và vẫn giữ nguyên tỷ lệ một phần tư máy chủ sau khi bạn nâng cấp gói. MemorySwapMax=0 giúp unit đó hoàn toàn không sử dụng swap, biến một quá trình chạy chậm chạp thành một lần kill nhanh chóng và rõ ràng.

Một giới hạn cần đi kèm với chính sách khởi động lại (restart policy), nếu không việc kill chỉ để lại cho bạn một dịch vụ đã dừng.

[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
Restart=on-failure
RestartSec=5s

StartLimit* thuộc về [Unit]Restart= thuộc về [Service]. Đặt sai phần sẽ khiến systemd bỏ qua thiết lập đó. Năm lần khởi động lại trong năm phút là dấu hiệu của lỗi leak thay vì một sự cố thoáng qua, vì vậy sau đó systemd sẽ từ bỏ và để unit ở trạng thái failed, đây là trạng thái bạn muốn thấy sau này thay vì một vòng lặp crash che giấu vấn đề.

Giới hạn CPU với CPUQuota, hoặc chia sẻ tài nguyên với CPUWeight

CPUQuota= lấy một tỷ lệ phần trăm thời gian khả dụng trên một CPU. CPUQuota=50% tương đương với một nửa nhân. CPUQuota=200% tương đương với hai nhân, đơn vị này có thể phân bổ trên bao nhiêu luồng tùy ý. Trên gói 2 vCPU, CPUQuota=200% là toàn bộ máy chủ.

CPUWeight= là lựa chọn mặc định tốt hơn cho hầu hết các dịch vụ. Đây là giá trị chia sẻ tương đối từ 1 đến 10000, với mặc định của kernel là 100. Nó chỉ phát huy tác dụng khi có sự cạnh tranh tài nguyên: một tác vụ backup ở mức CPUWeight=20 sẽ nhường tài nguyên cho web server ở mức 100 khi có tải, nhưng vẫn có thể sử dụng toàn bộ máy chủ khi máy đang rảnh. Một hạn mức cứng (hard quota) sẽ làm lãng phí công suất nhàn rỗi đó.

Hãy thực tế về những gì giới hạn CPU mang lại cho bạn. Một tiến trình chiếm dụng CPU hiếm khi làm treo Linux, vì bộ lập lịch (scheduler) vẫn liên tục phân bổ thời gian cho mọi tiến trình. Bộ nhớ (RAM) mới là thứ làm sập máy chủ. Hãy dùng CPUQuota= khi bạn muốn một mức trần có thể dự đoán được, ví dụ như cho một tác vụ build hoặc một agent mà nếu không giới hạn sẽ chạy hết công suất trong cả giờ đồng hồ. Việc định cỡ cho loại workload đó là một vấn đề riêng, được đề cập trong VPS cho coding agent cần bao nhiêu RAM và CPU.

Nếu CPU báo bận trong khi không có tiến trình nào của bạn hoạt động nhiều, nguyên nhân có thể nằm ở phía bên kia của hypervisor. Đó là CPU steal time từ hàng xóm ồn ào, và không hạn mức nào bạn thiết lập có thể thay đổi được điều đó.

TasksMax ngăn chặn vòng lặp fork

TasksMax= là số lượng tiến trình và luồng mà một unit được phép duy trì. Vì các luồng cũng được tính, một service chạy Java hoặc Go cần nhiều không gian hơn so với danh sách tiến trình thông thường. Đây là cơ chế bảo vệ rẻ nhất trước các script tạo fork trong vòng lặp, vì lệnh fork sẽ thất bại ngay bên trong unit thay vì làm cạn kiệt process ID của toàn bộ máy chủ.

TasksMax=128

Khi một unit chạm ngưỡng giới hạn, kernel sẽ ghi log một dòng chỉ rõ cgroup đó:

cgroup: fork rejected by pids controller in /system.slice/myapp.service

Bản thân chương trình thường sẽ báo lỗi fork: retry: Resource temporarily unavailable. Hãy kiểm tra giá trị mặc định mà trình quản lý áp dụng bằng lệnh systemctl show -p DefaultTasksMax.

Giới hạn một tác vụ chạy một lần bằng systemd-run

Bạn không cần file unit để sử dụng bất kỳ tính năng nào trong số này. systemd-run sẽ tạo một unit tạm thời xung quanh một lệnh duy nhất.

sudo systemd-run --scope -p MemoryMax=1G -p MemorySwapMax=0 -p CPUQuota=50% -p TasksMax=64 ./import-data.sh

--scope chạy lệnh ngay trong terminal của bạn, sau khi in ra Running scope as unit: run-r7c1a....scope. Đầu ra vẫn hiển thị trên màn hình và các giới hạn sẽ mất đi khi lệnh kết thúc. Bất kỳ thuộc tính nào từ systemd.resource-control đều hoạt động sau -p.

Đối với một tác vụ chạy lâu, hãy bỏ --scope và đặt tên cho nó. Nó sẽ chạy dưới nền như một service tạm thời và ghi log vào journal:

sudo systemd-run --unit=nightly-import -p MemoryMax=1G -p CPUWeight=20 ./import-data.sh
journalctl -u nightly-import -f

Các tùy chọn tương tự cũng hoạt động với --user khi bạn không phải là root, mặc dù trình quản lý người dùng của bạn chỉ có các controller được ủy quyền cho nó, vì vậy một thuộc tính có thể bị từ chối ở đó. Hãy chạy nó với sudo nếu điều đó xảy ra. Khi một tác vụ cần một vị trí cố định, các thiết lập sẽ được chuyển nguyên trạng sang một unit thực sự: xem chạy một script dưới dạng systemd service và timer.

Câu hỏi về swap, trả lời một cách trung thực

Swap làm thay đổi hình thái của sự cố thay vì ngăn chặn nó.

Nếu không có swap, khi bộ nhớ bị rò rỉ chạm ngưỡng giới hạn, một tiến trình sẽ chết trong vài giây. Sự cố này diễn ra nhanh, ngắn và dễ đọc log trong journal sau đó. Với swap, kernel ghi các trang anonymous ít dùng ra đĩa và kéo dài thời gian. Nếu tiến trình đó ổn định trở lại, swap sẽ cứu bạn. Nếu đó là một tiến trình chạy mất kiểm soát, swap biến sự cố 5 giây thành 20 phút treo máy, và việc treo máy còn tệ hơn, vì một tiến trình chết vẫn để lại cho bạn một shell hoạt động, còn một hệ thống đang thrashing thì không.

swapon --show
free -h

Một giải pháp trung hòa khả thi trên VPS nhỏ: duy trì một swap file vừa phải cho các trang được cấp phát một lần và không bao giờ dùng lại, đồng thời thiết lập MemorySwapMax=0 trên các unit mà bạn sẵn sàng để mất. Các dịch vụ quan trọng vẫn giữ swap của chúng. Các dịch vụ khó đoán định sẽ chạm ngưỡng nhanh chóng và tự khởi động lại.

Giảm vm.swappiness là một biện pháp yếu, và cần hiểu rõ lý do tại sao. Nó chỉ thay đổi sự cân bằng giữa việc giải phóng page cache và swap các trang anonymous, cả hai đều gây tốn kém chi phí đọc đĩa sau đó. Nó chỉ thay đổi trang nào bị thrash, chứ không thay đổi việc hệ thống có bị thrash hay không.

Một daemon OOM sớm sẽ kill tiến trình trước khi hệ thống bị treo

Kernel đợi cho đến khi quá trình reclaim thất bại hoàn toàn, và trên một VPS nhỏ, khoảng thời gian chờ đó chính là lúc bạn mất quyền kiểm soát máy chủ. Hai daemon userspace giải quyết vấn đề này bằng cách tự theo dõi bộ nhớ và thực hiện kill sớm hơn.

earlyoom theo dõi bộ nhớ khả dụng và swap trống, sau đó kill tiến trình có điểm số cao nhất khi một trong hai chỉ số này giảm xuống dưới ngưỡng quy định.

sudo apt install earlyoom
systemctl status earlyoom

Gói cài đặt trên Debian và Ubuntu sẽ tự động khởi động service sau khi cài. Các tùy chọn của nó nằm trong /etc/default/earlyoom:

EARLYOOM_ARGS="-m 5,2 -s 5,2 --avoid '^(sshd|systemd)$' --prefer '^(node|python3)$'"

-m PERCENT thiết lập mức tối thiểu cho bộ nhớ khả dụng và -s PERCENT thiết lập mức tối thiểu cho swap trống, cả hai đều mặc định là 10 phần trăm. Con số thứ hai trong mỗi cặp là điểm kích hoạt SIGKILL: earlyoom gửi SIGTERM khi bạn giảm xuống dưới giá trị thứ nhất, sau đó gửi SIGKILL khi xuống dưới giá trị thứ hai, mặc định bằng một nửa giá trị thứ nhất. Áp dụng thay đổi bằng sudo systemctl restart earlyoom, và đọc journalctl -u earlyoom để xem tiến trình nào đã bị kill và tiến trình đó đang chiếm bao nhiêu bộ nhớ.

systemd-oomd là tùy chọn còn lại. Trang manual của nó mô tả đây là "một system service sử dụng cgroups-v2 và pressure stall information (PSI) để giám sát và thực hiện hành động khắc phục trước khi tình trạng OOM xảy ra trong không gian kernel". Nó tác động lên toàn bộ cgroup thay vì từng tiến trình đơn lẻ, vì vậy nó sẽ kill một unit thay vì một tiến trình con riêng lẻ. Các unit cần được chọn tham gia bằng ManagedOOMMemoryPressure=kill hoặc ManagedOOMSwap=kill, và các ngưỡng được thiết lập trong /etc/systemd/oomd.conf.

systemctl status systemd-oomd
oomctl

oomctl in ra những gì nó đang giám sát, thường là không có gì trên các image máy chủ, vì cài đặt này yêu cầu chọn tham gia (opt-in) cho từng unit. Hãy chọn một daemon và dừng lại ở đó. Chạy cả hai sẽ dẫn đến việc hai tiến trình tranh giành nhau để chọn nạn nhân, và lý do cho bất kỳ lần kill nào sẽ trở nên khó xác định hơn.

Unit nào đã gây ra sự cố?

Hãy bắt đầu với kernel, vì nó ghi lại mọi tiến trình mà nó đã kill.

journalctl -k --grep "Killed process" --since "2 hours ago"

Một lệnh kill từ OOM killer toàn cục sẽ trông như sau:

Out of memory: Killed process 4127 (node) total-vm:2731084kB, anon-rss:1874232kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:4212kB oom_score_adj:0

anon-rss là dung lượng bộ nhớ mà tiến trình đó đang giữ trong RAM khi bị kill, ở đây là khoảng 1.8 GB. Hãy chú ý đến tên nằm trong ngoặc vuông. Đó là nạn nhân mà kernel đã chọn, và kernel thường chọn tiến trình chiếm nhiều bộ nhớ nhất, không nhất thiết là tiến trình gây ra tình trạng thiếu hụt.

Một lệnh kill từ giới hạn cgroup sẽ có tiền tố khác, và báo cáo in phía trên nó sẽ nêu tên cgroup đã chạm ngưỡng giới hạn của chính nó:

Memory cgroup out of memory: Killed process 8811 (python3) total-vm:1044320kB, anon-rss:769112kB, file-rss:0kB, shmem-rss:0kB, UID:998 pgtables:1720kB oom_score_adj:0

Tiền tố đó là phần lớn nội dung chẩn đoán. Memory cgroup out of memory nghĩa là một unit đã chạm ngưỡng MemoryMax= mà bạn thiết lập, trong khi phần còn lại của máy chủ vẫn ổn. Một thông báo Out of memory đơn thuần nghĩa là toàn bộ máy chủ đã cạn kiệt bộ nhớ, do đó các giới hạn của bạn hoặc là chưa được thiết lập, hoặc là quá rộng so với tổng tài nguyên.

Sau đó, hãy hỏi systemd xem nó đã ghi nhận những gì:

systemctl status myapp.service
journalctl -u myapp.service -n 50
myapp.service: A process of this unit has been killed by the OOM killer.
myapp.service: Main process exited, code=killed, status=9/KILL
myapp.service: Failed with result 'oom-kill'.

systemctl status cho biết cùng một thông tin trên một dòng, dưới dạng Active: failed (Result: oom-kill).

Các bộ đếm cgroup là nguồn thứ ba, và cũng là nguồn duy nhất ghi lại việc điều tiết (throttling), vốn không bao giờ tạo ra dòng log nào cả:

cat /sys/fs/cgroup/system.slice/myapp.service/memory.events
cat /sys/fs/cgroup/system.slice/myapp.service/memory.peak
low 0
high 4213
max 118
oom 12
oom_kill 12

high đếm số lần unit bị đẩy vượt quá MemoryHigh= và bị điều tiết. max đếm số lần nó chạm ngưỡng giới hạn cứng (hard cap), và oom_kill đếm số tiến trình thực sự bị kill. Một giá trị high lớn kèm theo oom_kill 0 là trường hợp im lặng đã đề cập trước đó: dịch vụ vẫn đang chạy, bị chậm đến mức bò, và không báo cáo lỗi cho bất kỳ ai. memory.peak (Linux 5.19 trở lên) lưu giữ mức sử dụng cao nhất mà cgroup từng đạt tới, đây là con số để bạn đối chiếu khi thiết lập MemoryMax=. Cả hai file này đều reset khi unit khởi động lại, vì systemd tạo lại cgroup đó.

Một điều kiện tiên quyết nằm dưới tất cả những điều này. Nếu /var/log/journal không tồn tại, journal sẽ nằm trong RAM, và mọi dòng log sẽ mất sạch sau lần reboot mà bạn cần để khôi phục máy chủ.

sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald
journalctl --list-boots

journalctl --list-boots hiển thị nhiều hơn lần boot hiện tại nghĩa là lịch sử log đã được lưu lại, vì vậy journalctl -k -b -1 có thể cho bạn thấy các thông báo kernel từ lần boot đã bị crash.

Điểm khởi đầu cho một VPS nhỏ

Trên gói 2 GB, hãy để dành 300 đến 400 MB cho kernel và page cache. Đừng để tổng giới hạn của các đơn vị cộng lại bằng đúng 2 GB, vì mọi unit có thể đạt đỉnh cùng một lúc. Hãy ưu tiên cấp tài nguyên lớn nhất cho dịch vụ quan trọng, sau đó giới hạn mọi dịch vụ phụ xung quanh nó.

[Service]
MemoryHigh=256M
MemoryMax=384M
MemorySwapMax=0
CPUWeight=20
TasksMax=64
Restart=on-failure
RestartSec=5s

Việc giữ một lối vào hệ thống xứng đáng với một thiết lập bổ sung. OOMScoreAdjust=-500 trong một file drop-in cho ssh.service giúp giảm đáng kể khả năng OOM killer chọn SSH daemon của bạn làm nạn nhân. Đây là sự khác biệt giữa việc sửa lỗi máy chủ và phải reboot từ control panel. Nó chỉ thay đổi lựa chọn nạn nhân của kernel. Nó không làm giảm thời gian treo máy.

Các container chạy trong cgroup riêng, được tạo bởi container runtime thay vì các unit file của bạn, vì vậy giới hạn trên docker.service không trở thành giới hạn cho một container. Các thiết lập tương đương cho từng container của MemoryMax=CPUQuota= được đề cập trong thiết lập giới hạn bộ nhớ và CPU trong Docker Compose.

FAQ

Tại sao VPS của tôi bị treo thay vì kill tiến trình đang chạy quá tải?

Vì kernel đánh giá tiến trình dựa trên việc reclaim có trả về các page hay không, chứ không dựa trên thời gian thực hiện. Khi bộ nhớ thiếu, nó sẽ giải phóng page cache, bao gồm cả các page thực thi của chương trình đang chạy, sau đó đọc lại chúng ở lệnh tiếp theo. Mọi thứ đều phải chờ storage và về mặt kỹ thuật thì chưa có yêu cầu cấp phát nào bị từ chối, nên OOM killer không được kích hoạt. Hãy kiểm tra /proc/pressure/memory khi tình trạng này xảy ra: full avg10 trên 40 nghĩa là gần như không có tác vụ nào được chạy trong mười giây vừa qua. Một daemon userspace như earlyoom sẽ kill tiến trình trước khi máy chủ rơi vào trạng thái đó.

Sự khác biệt giữa MemoryHigh và MemoryMax là gì?

MemoryHigh= là giới hạn mềm để điều tiết (throttle). Kernel sẽ reclaim mạnh từ unit đó và làm chậm các yêu cầu cấp phát, nhưng mức sử dụng vẫn có thể vượt quá con số này và không có tiến trình nào bị kill. MemoryMax= là giới hạn cứng: một yêu cầu cấp phát không thể đáp ứng trong giới hạn này sẽ kích hoạt OOM killer ngay trong cgroup của unit đó, vì vậy tiến trình gây ra vấn đề sẽ bị kill thay vì tiến trình lớn nhất trên máy chủ. Hãy đặt MemoryHigh= thấp hơn MemoryMax= và coi khoảng cách giữa chúng là vùng cảnh báo.

Làm thế nào để tìm xem service nào bị OOM killer xử lý?

Chạy journalctl -k --grep "Killed process" --since "2 hours ago". Một dòng bắt đầu bằng Memory cgroup out of memory nghĩa là một unit đã chạm ngưỡng MemoryMax= của chính nó, trong khi Out of memory đơn thuần nghĩa là toàn bộ máy chủ đã cạn kiệt bộ nhớ. Sau đó chạy journalctl -u <unit> -n 50 và tìm Failed with result 'oom-kill'. Nếu /var/log/journal không tồn tại trên server của bạn, journal đã được lưu trong RAM và bằng chứng đã mất sau khi reboot, vì vậy hãy tạo thư mục đó trước sự cố tiếp theo.

Tôi có nên thêm swap cho VPS nhỏ không?

Một file swap nhỏ giúp ích cho các cold page được cấp phát một lần và không bao giờ đụng đến nữa. Nó không giúp ích gì với tiến trình chạy quá tải: nó chỉ làm chậm việc kill tiến trình và thay thế một khoảng thời gian gián đoạn ngắn bằng một trạng thái treo dài khiến bạn không thể SSH vào để sửa. Hãy giữ swap ở mức vừa phải và đặt MemorySwapMax=0 trên các unit mà bạn chấp nhận mất, để chúng đạt ngưỡng và khởi động lại nhanh chóng trong khi các service quan trọng vẫn giữ được swap của mình.

Tôi có thể giới hạn một lệnh mà không cần viết file unit không?

Có. sudo systemd-run --scope -p MemoryMax=1G -p CPUQuota=50% ./script.sh chạy lệnh trong terminal của bạn bên trong một transient scope với các giới hạn đó, và các giới hạn này sẽ biến mất khi lệnh kết thúc. Mọi thuộc tính từ systemd.resource-control đều khả dụng sau -p, vì vậy MemorySwapMax=, TasksMax=CPUWeight= cũng hoạt động ở đó. Bỏ --scope và thêm --unit=name để chạy tác vụ ở chế độ nền với output được lưu trong journal.