SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor

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

Hướng dẫn cấu hình MemoryMax và CPUQuota trong file drop-in để kiểm soát tài nguyên tiến trình. Tránh lỗi cấu hình sai khiến systemd bỏ qua giới hạn và treo VPS.

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 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 cho bộ nhớ. CPUQuota= là giới hạn cho 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 đó 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=805306368 và CPUQuotaPerSecUSec=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à nó có bắt đầu bằng header [Service] hay không, vì một dòng cài đặt 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 có 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ạ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 prompt của 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.

Cơ chế này diễn ra như sau, vì nó không hiển nhiên. 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. Những trang rẻ nhất để thu hồi là các trang dựa trên file (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 văn bản 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ờ đợi ổ cứng thay vì được thực thi. 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 gắn qua mạng 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 lường thời gian, nó đo lường 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 diệt OOM (out of memory). 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ể theo dõi quá trình này. Kernel xuất thông tin áp lực (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 48% của mười giây vừa qua, tất cả các tác vụ không ở trạng thái nhàn rỗi trên máy đều bị treo do bộ nhớ cùng một lúc, vì vậy không có công việc nào của bạn đạt được tiến triển. Một máy chủ khỏe mạnh sẽ có chỉ số gần bằng 0 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 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 nhưng 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ứ 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 hợp nhất (unified hierarchy), điều mà các thiết lập bộ nhớ bên dưới yêu cầu. tmpfs nghĩa là máy chủ đã khởi động với bố cục v1 cũ, nơi MemoryHigh= và MemorySwapMax= không được hỗ trợ và hành vi OOM trên mỗi unit sẽ khác biệt, mặc dù CPUQuota= vẫn hoạt động ở đó thông qua file cpu.cfs_quota_us kế thừa. Ubuntu 22.04 trở lên, và Debian 11 trở lên, sử dụng v2 theo mặc định. Một image cũ thì không, và trên systemd 257 hoặc cũ hơn thì kernel được khởi động với systemd.unified_cgroup_hierarchy=0 cũng vậy. systemd 258 đã loại bỏ hỗ trợ cgroup v1 và luôn mount hệ thống phân cấp hợp nhất khi khởi động, vì vậy trên các bản phát hành sử dụng phiên bản này, tham số đó không còn mang v1 trở lại được nữa.

Trên cgroup v2, systemd bật tính năng kế toán bộ nhớ (memory accounting) cho mọi unit theo mặc định, vì vậy các con số đã có sẵn ở đó:

systemd-cgtop -m

Lệnh đó 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ủ này" khi nó vẫn còn phản hồi. Nếu máy chủ là mới, công 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ẽ chủ động thu hồi bộ nhớ từ cgroup đó và cố tình làm chậm các yêu cầu cấp phát của nó. Mức sử dụng vẫn có thể vượt qua 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ể đá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ế sau 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ị rò rỉ bộ nhớ. Với một giới hạn, việc kill sẽ 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 rò rỉ 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 cú 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 một phần tư dung lượng máy chủ sau khi bạn nâng cấp gói. MemorySwapMax=0 giữ cho unit đó hoàn toàn không sử dụng swap, vì vậy khi nó đạt đến MemoryMax=, việc kill sẽ diễn ra nhanh chóng thay vì sau một quá trình ì ạch qua swap. Dưới mức đó, MemoryHigh= vẫn có thể điều tiết nó, và việc vượt quá giới hạn cao không bao giờ kích hoạt OOM killer, vì vậy một unit dao động giữa hai mức này sẽ bị chậm thay vì bị kill.

Một vài dịch vụ cho phép bạn quyết định mức tiêu thụ trước thay vì phải đo đạc: một unit Ollama định kích thước KV cache từ context window mà bạn cung cấp, vì vậy hãy đọc chi phí RAM khi tăng num_ctx trước khi bạn chọn mức trần cho nó.

Một giới hạn cần đi kèm với chính sách khởi động lại, 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] và Restart= thuộc về [Service]. Đặt sai phần thì systemd sẽ bỏ qua nó. Năm lần khởi động lại trong năm phút là dấu hiệu của rò rỉ thay vì một sự cố tạm thời, 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 crash loop 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 core. CPUQuota=200% tương đương với hai core, đơ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 service. Đây là tỷ lệ chia sẻ tương đối từ 1 đến 10000, với giá trị mặc định của kernel là 100. Nó chỉ có 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 sử dụng toàn bộ máy chủ khi máy đang rảnh. Một hard quota sẽ làm lãng phí công suất nhàn rỗi đó.

Hãy cân nhắc kỹ lợi ích của việc giới hạn CPU. 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 dung lượng RAM và CPU cần thiết cho một coding agent VPS.

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ừ một hàng xóm ồn ào, và không có quota 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 (thread) mà một unit được phép duy trì. Vì luồng cũng được tính, một service viết bằng 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 tiến trình con (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ộ hệ thống.

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 tạo ra 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 bộ điều khiển đượ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 cài đặt sẽ được chuyển nguyên trạng sang một unit thực sự: xem chạy 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 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ị tràn, một tiến trình sẽ bị kết thúc trong vài giây. Sự cố này diễn ra nhanh, ngắn gọn và dễ đọc trong log sau đó. Với swap, kernel ghi các anonymous page ít dùng ra đĩa và kéo dài thời gian. Nếu tiến trình đó chỉ tạm thời tăng đột biến, swap sẽ cứu bạn. Nếu đó là một tiến trình bị lỗi (runaway), swap sẽ biến sự cố 5 giây thành 20 phút hệ thống bị treo. 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 máy chủ đang bị thrashing thì không.

swapon --show
free -h

Một giải pháp trung gian khả thi trên VPS nhỏ: duy trì một swap file vừa phải cho các page đượ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 chấp nhận để nó bị kill. Các dịch vụ quan trọng vẫn giữ swap. Các dịch vụ không ổn định sẽ chạm ngưỡng giới hạn nhanh chóng, và chính sách restart đi kèm với giới hạn đó sẽ khởi động lại chúng.

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

Daemon OOM sớm xử lý trước khi hệ thống bị treo

Kernel đợi cho đến khi quá trình thu hồi bộ nhớ 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 chạy ở 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 tiến trình sớm hơn.

earlyoom kiểm tra bộ nhớ khả dụng và swap trống tối đa mười lần mỗi giây, và chỉ kill tiến trình có oom_score cao nhất khi cả hai chỉ số này giảm xuống dưới ngưỡng quy định. Trên máy chủ không có swap, chỉ cần ngưỡng bộ nhớ là đủ để quyết định.

sudo apt install earlyoom
systemctl status earlyoom

Gói cài đặt trên Debian và Ubuntu sẽ tự khởi động service ngay 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 cho swap trống, mặc định cả hai đều 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 cả bộ nhớ và swap đều giảm xuống dưới giá trị đầu tiên, sau đó gửi SIGKILL khi cả hai đều dưới giá trị thứ hai, mặc định bằng một nửa giá trị đầu. Hãy để các pattern --avoid và --prefer không có dấu ngoặc kép và không có khoảng trắng, vì systemd truyền ký tự ngoặc kép theo đúng nghĩa đen và một pattern có dấu ngoặc kép sẽ không bao giờ khớp. Á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 hướng dẫn (man page) mô tả nó là "một system service sử dụng cgroups-v2 và thông tin áp lực (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 kernel space". Nó tác động lên toàn bộ cgroup thay vì từng tiến trình riêng lẻ, vì vậy nó kill cả một unit chứ không phải một tiến trình con đơn lẻ. Các unit cần được chọn tham gia (opt-in) bằng ManagedOOMMemoryPressure=kill hoặc ManagedOOMSwap=kill, và các ngưỡng được cấu hình 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 một image máy chủ vì cài đặt này yêu cầu opt-in cho từng unit. Hãy chọn một daemon và chỉ dùng một cái. Chạy cả hai sẽ dẫn đến tình trạng hai tiến trình tranh 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.

Đơn vị nào chịu trách nhiệm?

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ư thế này:

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à bộ nhớ ẩn danh, ví dụ như heap, mà tiến trình đó đang giữ trong RAM khi nó chết, ở đây là khoảng 1.8 GB. Các file mapping thường trú và bộ nhớ chia sẻ được tính riêng, lần lượt là file-rss và shmem-rss. Hãy đọc tên trong ngoặc vuông với sự nghi ngờ. Đó là nạn nhân mà kernel đã chọn, và kernel luôn chọn tiến trình lớn nhất, vốn không phải lúc nào cũng là tiến trình gây ra tình trạng thiếu hụt bộ nhớ.

Một lệnh kill từ giới hạn cgroup 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 quan trọng nhất của việc chẩn đoán. Memory cgroup out of memory nghĩa là một đơn vị đã 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 tài nguyên, 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 về những gì nó đã ghi nhận:

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à là nguồn duy nhất ghi lại việc 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 đơn vị bị đẩy vượt quá MemoryHigh= và bị throttle. 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 đó: kể từ khi cgroup được tạo, đơn vị này đã bị throttle liên tục mà không có tiến trình nào bị kill, vì vậy systemd không ghi nhận lỗi nào. Bộ đếm chỉ tăng dần, vì vậy hãy đọc nó hai lần cách nhau một phút: một giá trị high vẫn đang tăng nghĩa là việc throttling đang diễn ra ngay lúc này. 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 cấu hình MemoryMax=. Cả hai file này đều reset khi đơn vị 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 khi bạn reboot để 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 phiên boot hiện tại nghĩa là lịch sử đã đượ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ừ phiê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, và đừng để tổng các giới hạn cộng lại bằng đúng 2 GB, vì mọi đơn vị có thể đạt đỉnh cùng một lúc. Hãy cấp cho dịch vụ quan trọng nhất phần tài nguyên lớn nhất, sau đó giới hạn mọi thứ mang tính thử nghiệm 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 là xứng đáng với thêm một thiết lập nữa. OOMScoreAdjust=-500 trong một file drop-in cho ssh.service giúp OOM killer toàn cục ít có khả năng chọn SSH daemon của bạn làm nạn nhân hơn, đây chính 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ủa chú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 tùy chọn tương đương cho từng container của MemoryMax= và 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 hụt, 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 chờ đợi vào storage và về mặt kỹ thuật thì không có yêu cầu cấp phát nào thất bại, nên OOM killer không được kích hoạt. Hãy kiểm tra /proc/pressure/memory khi sự cố xảy ra: full avg10 trên 40 nghĩa là trong hơn 40% của 10 giây vừa qua, mọi tác vụ không ở trạng thái idle đều bị treo do bộ nhớ cùng một lúc. 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 (soft cap) dùng để điều tiết. Kernel sẽ reclaim mạnh tay 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 (hard cap): 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 để biết 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ủ đã hế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 khi sự cố tiếp theo xảy ra.

Tôi có nên thêm swap cho VPS cấu hình thấp không?

Một file swap nhỏ giúp ích cho các page ít dùng (cold pages) vốn được cấp phát một lần và không bao giờ đụng tới 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 lần gián đoạn ngắn bằng một lần treo máy dài mà bạn không thể đăng nhập để 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 để chúng bị kill, nhờ đó chúng sẽ chạm ngưỡng nhanh chóng và với Restart=on-failure được thiết lập, chúng sẽ tự khởi động lại 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= và CPUWeight= cũng hoạt động ở đó. Bỏ --scope và thêm --unit=name để chạy job ở chế độ nền với đầu ra được ghi vào journal.