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

Vì sao systemd không restart service của bạn?

Restart= chỉ theo dõi process chính, nên child bị chết trong cùng cgroup vẫn không kích hoạt restart. Xem cách Type=, giới hạn restart và journal hoạt động.

Câu trả lời ngắn gọn: chính sách restart của systemd chỉ theo dõi một tiến trình

Chính sách restart của systemd chỉ theo dõi một tiến trình trong mỗi unit: tiến trình chính. Restart= đọc exit status của riêng tiến trình đó và không đọc thông tin nào khác. Control group của một unit có thể chứa hai mươi tiến trình. Một tiến trình trong số đó có thể dừng, nhưng unit vẫn ở trạng thái active (running) vì tiến trình chính vẫn còn. Theo systemd, không có lỗi nào xảy ra, nên không có gì được restart.

systemd vẫn biết các tiến trình khác. Nó dừng chúng khi unit dừng, tính bộ nhớ của chúng vào giới hạn của unit, áp dụng CPU quota của unit cho chúng và hiển thị chúng trong systemctl status. Tuy nhiên, systemd không bao giờ đọc exit status của các tiến trình đó. Logic restart và cgroup là hai cơ chế khác nhau. Phần lớn nội dung của hướng dẫn này nói về khoảng cách giữa hai cơ chế đó.

cgroup chứa gì và logic restart đọc gì

Một cgroup (control group) là đối tượng của kernel, quản lý một nhóm process. Mỗi service unit có một cgroup riêng, được đặt tên theo unit. Process không thể tự rời khỏi cgroup đó. Process con kế thừa cgroup của process cha, còn process không có đặc quyền không thể tự chuyển sang nơi khác. Vì vậy systemd có thể dọn sạch một daemon fork hai lần, điều mà các init script cũ không thể thực hiện một cách đáng tin cậy.

Hãy đặt hai thông tin này cạnh nhau:

systemd-cgls --unit myapp.service
systemctl show -p MainPID -p NRestarts -p Restart -p RestartUSec myapp.service

systemd-cgls liệt kê mọi process trong unit. MainPID là con số duy nhất mà restart policy đọc. Khi hai thông tin này không khớp với mô hình bạn đang hình dung, đó chính là lỗi. MainPID=0 còn nghiêm trọng hơn PID sai: nó có nghĩa là systemd không theo dõi gì cả, nên không giá trị Restart= nào có thể kích hoạt.

Có một ngoại lệ thực sự đối với quy tắc main process. Nếu OOM killer của kernel kill bất kỳ process nào bên trong cgroup của unit, systemd vẫn phát hiện được vì nó theo dõi file memory.events của cgroup. OOMPolicy= quyết định điều xảy ra tiếp theo, và giá trị mặc định là stop: toàn bộ unit bị dừng, kết quả được ghi nhận là oom-kill, và điều đó được tính là một lỗi, nên Restart=on-failure được kích hoạt. Journal ghi rõ điều này.

myapp.service: A process of this unit has been killed by the OOM killer.
myapp.service: Failed with result 'oom-kill'.

Vì vậy, child bị kill do thiếu bộ nhớ sẽ làm unit dừng, còn cùng child đó nếu kết thúc do segmentation fault thì không. Nếu đặt memory limit cho một unit, hãy đọc cách MemoryMax và CPUQuota áp dụng cho cgroup của unit trước khi tinh chỉnh restart policy, vì hai tính năng này chỉ giao nhau tại đây.

Type= quyết định tiến trình chính

Type= trong phần [Service] không chỉ liên quan đến thứ tự khởi động. Đây là quy tắc quyết định PID (process ID) nào trở thành MainPID, tức cũng quyết định Restart= có thể nhìn thấy gì.

  • Type=simple là giá trị mặc định. Tiến trình mà systemd fork từ ExecStart= là tiến trình chính. systemd đánh dấu unit đã khởi động ngay lập tức, trước khi biết exec có thực sự chạy thành công hay không. Lỗi gõ sai đường dẫn binary tạo ra một start job thành công, rồi Main process exited, code=exited, status=203/EXEC mới fail sau đó một lúc.
  • Type=exec hoạt động giống simple, nhưng start job sẽ chờ đến khi exec chạy thành công. Nhờ vậy, lỗi gõ sai ở trên trở thành lỗi khởi động thực sự. Tính năng này cần systemd 240 trở lên, phiên bản mà mọi distribution còn được hỗ trợ đều có. Nên ưu tiên dùng nó thay cho simple.
  • Type=forking giả định tiến trình từ ExecStart= sẽ fork một daemon chạy nền rồi thoát. systemd chờ tiến trình cha thoát, sau đó tìm daemon thực sự. Hãy cung cấp PIDFile=. Nếu không có, GuessMainPID= (được bật mặc định) chỉ hoạt động khi còn đúng một tiến trình trong cgroup. Nếu còn lại hai tiến trình, MainPID vẫn ở trạng thái 0.
  • Type=notify có nghĩa là service gọi sd_notify(3) và gửi READY=1 khi đã có thể nhận network traffic. Service cũng có thể gửi MAINPID= để bàn giao cho systemd một tiến trình khác cần theo dõi. NotifyAccess= mặc định là main, vì vậy notification do tiến trình con gửi sẽ bị bỏ qua và journal sẽ ghi PID đã gửi notification đó.
  • Type=oneshot không có tiến trình chính tồn tại lâu dài. Unit chuyển sang inactive ngay khi ExecStart= hoàn tất, trừ khi bạn đặt RemainAfterExit=yes. Restart=alwaysRestart=on-success bị từ chối ở đây, với thông báo Service has Restart= set to either always or on-success, which isn't allowed for Type=oneshot services. Refusing.. Các giá trị khác, bao gồm on-failure, đều được chấp nhận.

Có hai lỗi Type=forking cần ghi nhớ, vì mỗi lỗi đều khiến unit trông như bị hỏng mà không có lý do rõ ràng:

myapp.service: Can't open PID file /run/myapp.pid (yet?) after start: No such file or directory
myapp.service: New main PID 4711 does not belong to service, and PID file is not owned by root. Refusing.

Lỗi đầu tiên có nghĩa là daemon ghi file PID ở nơi khác hoặc ghi file đó muộn hơn thời điểm systemd kiểm tra. Lỗi thứ hai có nghĩa là file PID chỉ đến một tiến trình nằm ngoài cgroup của unit. systemd từ chối nhận tiến trình đó, vì nếu file PID có thể ghi tùy ý thì nó sẽ trở thành cách để gửi signal đến bất kỳ tiến trình nào trên máy.

Vì sao wrapper script che giấu việc các tiến trình con đã thoát

Đây là dạng cấu hình dẫn đến câu hỏi trong tiêu đề.

#!/bin/bash
/usr/local/bin/myapp-web &
/usr/local/bin/myapp-worker &
wait

Unit là Type=simple, nên process chính là shell. wait không có argument chỉ trả về sau khi mọi process con đã thoát. Kill worker nhưng shell vẫn chờ web process, nên shell không thoát, MainPID không thoát và Restart= không bao giờ được kiểm tra. Cgroup lúc này ít hơn một process, systemctl status in ra cây process ngắn hơn, còn unit vẫn ở trạng thái active (running). systemd không theo dõi cây process đó để phát hiện thay đổi.

Một phiên bản khác của cùng lỗi này khó nhận ra hơn:

ExecStart=/bin/sh -c 'export APP_ENV=production; /usr/local/bin/myapp'

Process chính là shell, không phải myapp. Khi systemctl stop, systemd gửi SIGTERM đến process chính. Shell đang chờ một child chạy foreground sẽ không chuyển tiếp signal đó. Quá trình stop sau đó mất toàn bộ TimeoutStopSec, mặc định là 90 giây, rồi kết thúc như sau:

myapp.service: State 'stop-sigterm' timed out. Killing.
myapp.service: Killing process 4711 (myapp) with signal SIGKILL.

Cách sửa là exec. Viết exec /usr/local/bin/myapp để shell được thay thế bằng program. Khi đó MainPID là program và signal sẽ đến đúng process. Tốt hơn nữa, xóa shell và dùng Environment= hoặc EnvironmentFile= trong unit. Lưu ý rằng lỗi này không lộ ra khi chuỗi -c chỉ chứa một command, vì bash và dash đều tối ưu trường hợp đó thành một exec trực tiếp. Thêm command thứ hai vào chuỗi thì shell sẽ tiếp tục chạy phía trước program.

Tái hiện trên một VPS test trong hai phút

Lưu wrapper ở trên thành /usr/local/bin/two-children.sh, cấp quyền thực thi bằng chmod +x, rồi thay hai đường dẫn program bằng sleep 3600. Trỏ một unit đến wrapper bằng Type=simpleRestart=on-failure, sau đó systemctl daemon-reload và start unit. Chạy systemd-cgls --unit two-children.service và ghi lại 3 PID: shell và hai process con của nó. Kill một process con bằng sudo kill <pid>. Kiểm tra lại unit. Cây process ngắn hơn một process, trạng thái vẫn là active (running) và journal không có thông tin mới. Bây giờ chạy sudo kill -9 <shell pid>. Unit fail, process con còn lại được dọn dẹp vì KillMode=control-group là giá trị mặc định, và journal hiển thị Scheduled restart job, restart counter is at 1.

Toàn bộ các giá trị của Restart= và khi nào on-failure phù hợp hơn always

Restart= nhận một trong bảy giá trị. Điểm khác biệt giữa chúng là cách xác định một lần thoát có sạch hay không. systemd xem mã thoát 0, mọi mã được liệt kê trong SuccessExitStatus=, cùng các signal SIGHUP, SIGINT, SIGTERM và SIGPIPE là thoát sạch. Mọi trường hợp khác, bao gồm SIGKILL và SIGSEGV, đều là thoát không sạch.

  • no là giá trị mặc định. Unit không bao giờ tự khởi động lại. Vì vậy, unit không có dòng Restart= sẽ dừng ở lần crash đầu tiên và tiếp tục ở trạng thái dừng.
  • on-success chỉ khởi động lại sau khi thoát sạch.
  • on-failure khởi động lại khi mã thoát khác 0, signal không sạch, timeout khi start hoặc stop, hoặc watchdog hết hạn.
  • on-abnormal khởi động lại khi có signal không sạch, timeout hoặc watchdog hết hạn, nhưng không bao giờ khởi động lại chỉ vì mã thoát khác 0.
  • on-abort chỉ khởi động lại khi có signal không sạch, tức là khi bị crash.
  • on-watchdog chỉ khởi động lại khi WatchdogSec= hết hạn.
  • always khởi động lại trong mọi trường hợp nêu trên, kể cả khi thoát sạch với status 0.

on-failure là giá trị mặc định phù hợp cho daemon chạy lâu dài. Nó khởi động lại daemon sau crash nhưng không can thiệp khi daemon chủ động thực hiện exit 0. always phù hợp với chương trình thoát sạch vì những lý do nằm ngoài quyền kiểm soát của nó, chẳng hạn tunnel client trả về 0 khi đầu xa ngắt kết nối. Điểm bất lợi của always là nó che giấu bug: một service khởi động, đọc file cấu hình bị hỏng, ghi log lỗi rồi thoát với mã 0 sẽ lặp vô hạn. Dấu hiệu duy nhất có thể là bộ đếm số lần restart liên tục tăng.

SuccessExitStatus= thay đổi ranh giới giữa thoát sạch và thoát không sạch. Borg thoát với mã 1 khi có cảnh báo và mã 2 khi có lỗi. Vì vậy, backup unit không có SuccessExitStatus=1 sẽ bị đánh dấu failed mỗi khi bỏ qua một file không thể đọc. RestartPreventExitStatus= liệt kê các mã ngăn không cho restart ngay cả khi dùng always. Đây là cách sạch để chương trình báo rằng nó không nên được khởi động lại. RestartForceExitStatus= làm điều ngược lại. Backup job nên chạy trong một unit Type=oneshot được timer kích hoạt, thay vì nằm trong vòng lặp restart. Cặp service và timer chạy job theo lịch là mẫu cần dùng trong trường hợp này.

Lưu ý khi kiểm thử. Dùng kill <pid> đơn thuần để kill service sẽ gửi SIGTERM. Signal này nằm trong danh sách thoát sạch, nên Restart=on-failure hoạt động đúng và không làm gì cả. Khi đó, bạn có thể kết luận sai rằng cấu hình bị hỏng. Thay vào đó, hãy dùng kill -9 <pid> hoặc systemctl kill -s SIGKILL myapp.service. Cũng cần nhớ rằng không giá trị nào của Restart= được kích hoạt sau systemctl stop, hoặc khi unit bị stop vì dependency BindsTo= hoặc PartOf= không còn tồn tại. Một stop job không phải là lỗi.

RestartSec và giá trị mặc định 100 mili giây

RestartSec= là khoảng dừng giữa lúc unit dừng và lúc systemd khởi động lại unit đó. Giá trị mặc định là 100 mili giây. Kiểm tra giá trị thực tế mà unit đã nạp:

systemctl show -p RestartUSec -p StartLimitIntervalUSec -p StartLimitBurst myapp.service

Unit chưa đặt giá trị này sẽ in ra RestartUSec=100ms. Giá trị mặc định đó phù hợp với service chỉ crash một lần rồi hoạt động lại. Nó không phù hợp với service hoàn toàn không thể khởi động, vì khi đó 5 lần restart sẽ xảy ra trong chưa đầy nửa giây. Đây chính là nguyên nhân kích hoạt rate limit được mô tả ở phần tiếp theo. Với mọi service phải chờ database, mount hoặc network route, hãy đặt RestartSec=5s hoặc cao hơn.

Tính đến tháng 8 năm 2026, systemd 254 trở lên cũng cung cấp RestartSteps=RestartMaxDelaySec=. Hai tùy chọn này tăng thời gian trễ từ RestartSec= lên dần đến một giới hạn trong số lần thử đó. Ubuntu 24.04 đi kèm systemd 255 và có các tùy chọn này. Debian 12 đi kèm systemd 252 và không có. Thời gian trễ tăng dần là lựa chọn phù hợp khi dependency có thể ngừng hoạt động trong thời gian dài.

“start request repeated too quickly” thực sự có nghĩa gì

Đây là trạng thái khiến người đọc nghĩ rằng systemd tự ý bỏ cuộc. Thực ra đây là một bộ đếm. Quy tắc là: nếu một unit được khởi động quá StartLimitBurst= lần trong StartLimitIntervalSec=, systemd sẽ từ chối khởi động lại unit đó và đưa nó vào trạng thái failed. Giá trị mặc định là 5 lần khởi động trong 10 giây.

Journal hiển thị chuỗi sự kiện:

myapp.service: Scheduled restart job, restart counter is at 5.
myapp.service: Start request repeated too quickly.
myapp.service: Failed with result 'start-limit-hit'.
Failed to start myapp.service - My application.

systemctl start đã đưa sẵn cách khắc phục:

Job for myapp.service failed because start of the service was attempted too often. See "systemctl status myapp.service" and "journalctl -xeu myapp.service" for details. To force a start use "systemctl reset-failed myapp.service" followed by "systemctl start myapp.service" again.

systemctl reset-failed myapp.service xóa bộ đếm và trạng thái failed. Không lệnh nào khác làm việc này, vì vậy một lệnh systemctl start thông thường vẫn bị từ chối cho đến khi bạn chạy lệnh đó. Các lần khởi động thủ công cũng được tính vào giới hạn. Vì vậy, vài lần chạy systemctl restart thiếu kiên nhẫn trong lúc bạn chỉnh file cấu hình cũng có thể kích hoạt giới hạn này mà hoàn toàn không có crash.

Điểm dễ gây hiểu nhầm là: start-limit-hit không bao giờ cho biết tại sao service bị lỗi. Nó chỉ cho biết service đã fail nhiều lần và liên tiếp trong thời gian ngắn. Nguyên nhân thực sự nằm trong các dòng journal phía trên.

Cả hai thiết lập này phải nằm trong section [Unit]. Bạn sẽ thấy một số ví dụ đặt chúng trong [Service]. Các phiên bản systemd cũ chấp nhận cách này, và đây là nguyên nhân gây nhầm lẫn. Hãy ghi chúng vào [Unit], rồi yêu cầu systemd cho biết nó đã nạp gì bằng systemctl show, vì chỉ giá trị đã được nạp mới có hiệu lực.

[Unit]
Description=My application
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
Type=exec
ExecStart=/usr/local/bin/myapp
Restart=on-failure
RestartSec=10s

Cấu hình này cho phép unit thử 5 lần trong khoảng thời gian 5 phút trước khi bỏ cuộc. StartLimitIntervalSec=0 tắt hoàn toàn giới hạn này. Bạn cần hiểu rõ lựa chọn của mình: một service không thể khởi động sẽ tiếp tục retry vô hạn và ghi log vào journal sau mỗi lần thử. Các giá trị mặc định trên toàn máy nằm trong /etc/systemd/system.conf, với tên DefaultStartLimitIntervalSec=DefaultStartLimitBurst=.

Có một thiết lập liên quan cần cảnh báo. StartLimitAction= quyết định điều gì xảy ra khi chạm giới hạn. Thiết lập này nhận các giá trị như reboot, reboot-forcepoweroff. Giá trị mặc định là none, làm unit fail nhưng không tác động đến máy. Trên một VPS từ xa, poweroff có nghĩa là máy sẽ tắt cho đến khi bạn mở console của nhà cung cấp.

Fix một: mỗi unit chỉ chạy một process

Đây là cách xử lý đúng trong hầu hết trường hợp. Nếu phải chạy hai chương trình, hãy viết hai unit. Mỗi unit khi đó có một process chính thực sự, một exit status thực sự và policy restart riêng. Bạn cũng có log riêng, giới hạn tài nguyên riêng và bộ đếm restart riêng. Đây là những thứ cần có lúc 3 giờ sáng.

Khai báo quan hệ giữa các unit trong file unit, không dùng shell script.

  • After= chỉ sắp xếp thứ tự khởi động. Nó không cho biết gì về lỗi.
  • Requires= khởi động unit kia cùng với unit này và dừng unit này nếu unit kia bị dừng một cách rõ ràng.
  • BindsTo=Requires= cộng thêm trường hợp bạn cần: unit này dừng khi unit kia dừng vì bất kỳ lý do nào, kể cả crash. Hãy dùng cùng After=, nếu không thứ tự sẽ không được xác định.
  • PartOf= truyền thao tác dừng và restart xuống các unit phụ thuộc, vì vậy systemctl restart myapp.target sẽ áp dụng cho mọi unit PartOf= nó.
  • Upholds= (systemd 249 trở lên, tức Ubuntu 22.04 trở lên) giữ cho unit được chỉ định luôn chạy: nếu unit dừng, systemd sẽ khởi động lại. Unit này vẫn chịu cùng giới hạn tần suất khởi động như mọi unit khác.

Một worker không được chạy khi thiếu API server và được systemd giữ cho luôn hoạt động bất cứ khi nào API đang chạy:

# /etc/systemd/system/myapp-api.service
[Unit]
Description=myapp API server
Wants=network-online.target
After=network-online.target
Upholds=myapp-worker.service

[Service]
Type=exec
User=myapp
ExecStart=/usr/local/bin/myapp serve
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target
# /etc/systemd/system/myapp-worker.service
[Unit]
Description=myapp background worker
BindsTo=myapp-api.service
After=myapp-api.service
StartLimitIntervalSec=120
StartLimitBurst=5

[Service]
Type=exec
User=myapp
ExecStart=/usr/local/bin/myapp worker
Restart=on-failure
RestartSec=5s

Worker không có section [Install] và không bao giờ được enable thủ công. API unit kéo worker vào bằng Upholds=, vì vậy systemctl enable --now myapp-api.service là lệnh duy nhất bạn cần chạy. Reload rồi kiểm tra systemd đã xử lý cặp unit này như thế nào:

sudo systemctl daemon-reload
systemd-analyze verify /etc/systemd/system/myapp-worker.service
systemctl list-dependencies myapp-api.service

systemd-analyze verify hoàn toàn không in gì nếu file không có lỗi. Mọi output đều là vấn đề, thường là một key mà systemd không nhận diện trong section nơi bạn đã viết key đó, hoặc dependency trỏ đến một unit không tồn tại.

Sửa hai: Type=notify để systemd biết nhiều hơn chỉ một PID

Nếu chương trình hỗ trợ giao thức thông báo của systemd, hãy dùng giao thức đó. Với Type=notify, service báo cho systemd biết khi nào nó đã sẵn sàng. Nhờ vậy, thứ tự khởi động dựa trên trạng thái thực tế thay vì chỉ dự đoán. Service cũng có thể gửi MAINPID= để chỉ cho systemd đúng tiến trình cần theo dõi thay vì tiến trình launcher.

WatchdogSec= là phần đáng để cấu hình. Khi bật tùy chọn này, service phải gửi WATCHDOG=1 qua sd_notify(3) ít nhất thường xuyên như vậy. Khi các thông báo dừng lại, systemd sẽ kết thúc service bằng SIGABRT và đánh dấu service là failed. Khi đó, Restart=on-failure hoặc Restart=on-watchdog sẽ khởi động lại service. Đây là cách tích hợp duy nhất để khởi động lại một tiến trình vẫn còn sống nhưng bị treo. Không policy nào dựa trên exit status có thể phát hiện trường hợp này.

[Service]
Type=notify
NotifyAccess=main
ExecStart=/usr/local/bin/myapp serve
WatchdogSec=30s
Restart=on-failure
RestartSec=5s

Khi watchdog hết hạn, journal sẽ ghi myapp.service: Watchdog timeout (limit 30s)! rồi mới ghi sự kiện kill. Nếu unit thay vào đó nằm ở trạng thái activating (start) cho đến khi TimeoutStartSec hết thời gian, thì READY=1 chưa bao giờ được gửi. Nguyên nhân có thể là chương trình không hỗ trợ giao thức này, hoặc NotifyAccess=main từ chối notification được gửi từ một child process. Journal sẽ ghi cả hai PID trong trường hợp đó.

Với phần mềm có HTTP health endpoint nhưng không hỗ trợ sd_notify, có hai lựa chọn phù hợp: dùng một timer unit nhỏ để kiểm tra endpoint rồi gọi systemctl restart, hoặc để container runtime thực hiện việc kiểm tra. Đây là mục đích của healthcheck của Compose và cơ chế restart của chúng.

Sửa thứ ba: chạy supervisor bên trong unit, chỉ khi không còn lựa chọn khác

Một số phần mềm thực sự được phát hành dưới dạng một nhóm process phía sau một launcher mà bạn không thể tách riêng. Khi đó, bạn chạy một supervisor bên trong unit và chấp nhận hệ quả: systemd theo dõi supervisor, supervisor theo dõi mọi thứ khác, còn chính sách restart nằm trong hai file.

Trường hợp phổ biến của mô hình này là container runtime. Một unit docker compose hoặc podman chính là mẫu này. Chính sách restart của từng container được khai báo trong file Compose, còn systemd unit chỉ đảm bảo runtime luôn chạy. Nếu hệ thống của bạn có mô hình như vậy, unit khởi động Compose stack khi boot trình bày phiên bản hoạt động được, bao gồm lý do Type=oneshot cùng với RemainAfterExit=yes thường là lựa chọn đúng trong trường hợp đó.

cgroup vẫn có lợi cho bạn. Mọi thứ supervisor khởi động đều nằm trong cgroup của unit, nên MemoryMax=, CPUQuota= và thao tác cleanup khi stop vẫn áp dụng cho toàn bộ cây process. Chỉ quyết định restart được ủy quyền cho supervisor.

Dù chọn supervisor nào, đừng đặt Restart=always trên unit bên ngoài đồng thời cấu hình chính sách restart tích cực bên trong mà chưa cân nhắc. Hai lớp logic restart, mỗi lớp có cơ chế backoff riêng, có thể khiến service liên tục flapping trong nhiều phút và journal không giải thích được nguyên nhân.

ExitType=cgroup không có nghĩa là “khởi động lại khi bất kỳ tiến trình nào dừng”

ExitType= (có trong systemd 250 trở lên, nên cả Ubuntu 24.04 và Debian 12 đều hỗ trợ) là tùy chọn mọi người thường tìm thấy khi tra cứu vấn đề này. Tuy nhiên, nó hoạt động ngược với điều tên gọi gợi ý. Giá trị mặc định, ExitType=main, nghĩa là service được xem là đã dừng khi tiến trình chính thoát. ExitType=cgroup nghĩa là service được xem là vẫn đang chạy cho đến khi tiến trình cuối cùng trong cgroup thoát.

Vì vậy, ExitType=cgroup làm unit ít nhạy hơn với việc một tiến trình dừng, chứ không nhạy hơn. Đây là tùy chọn phù hợp với chương trình fork worker thực tế rồi cho tiến trình cha thoát mà không ghi PID file, khiến Type=forking không tìm thấy daemon. Đây là tùy chọn không phù hợp với lỗi được mô tả ở đây.

Không có giá trị Restart= nào mang nghĩa “khởi động lại unit khi bất kỳ tiến trình nào trong cgroup dừng”. Nếu cần hành vi đó, bạn phải chạy mỗi tiến trình trong một unit riêng. Nếu không thể tách chương trình và bạn kiểm soát wrapper script, tùy chọn gần nhất là wait -n. Tùy chọn này trả về ngay khi child đầu tiên thoát:

#!/bin/bash
/usr/local/bin/myapp-web &
/usr/local/bin/myapp-worker &
wait -n
exit 1

Khi đó, bất kỳ child nào dừng cũng khiến wrapper dừng với status khác 0, nên Restart=on-failure sẽ hoạt động. Đây chỉ là giải pháp thỏa hiệp, không phải cách khắc phục. Bạn vẫn dùng chung một restart counter cho hai chương trình, chung một log stream và không thể tự khởi động lại phần bị lỗi.

Cách kiểm tra thực tế đã xảy ra điều gì

Bốn lệnh, theo đúng thứ tự này.

systemctl status myapp.service
systemd-cgls --unit myapp.service
systemctl show -p MainPID -p NRestarts -p Result -p ExecMainStatus myapp.service
journalctl -u myapp.service -b -o short-precise

systemctl status hiển thị trạng thái, PID chính và cây cgroup trên cùng một màn hình. Một unit khỏe mạnh sẽ hiển thị Active: active (running) với dòng Main PID: nêu đúng tiến trình bạn mong đợi. Nếu cây ở cuối liệt kê các tiến trình bạn không nhận ra hoặc thiếu một tiến trình mà bạn biết là phải có, bạn đã tìm ra vấn đề.

systemd-cgls --unit in cùng cây đó nhưng không cắt ngắn. Điều này bắt đầu quan trọng khi một unit quản lý nhiều hơn vài tiến trình.

systemctl show cung cấp các thông tin có thể đọc bằng máy. NRestarts= là bộ đếm số lần restart, và đây là cách nhanh nhất để phân biệt một service đã restart forty lần với một service đã chạy từ lúc boot. Result= chứa lý do lỗi gần nhất: exit-code, signal, timeout, oom-kill, watchdog hoặc start-limit-hit. ExecMainStatus= là exit status thô của main process gần nhất.

Journal lưu lại toàn bộ trình tự sự kiện. Đây là ba dòng cần tìm:

myapp.service: Main process exited, code=exited, status=1/FAILURE
myapp.service: Failed with result 'exit-code'.
myapp.service: Scheduled restart job, restart counter is at 1.

code=exited, status=N nghĩa là chương trình tự chọn trả về N, vì vậy lỗi nằm trong chương trình hoặc cấu hình của nó. code=killed, signal=SEGV nghĩa là chương trình đã crash. code=killed, signal=TERM thường nghĩa là một thành phần khác yêu cầu chương trình dừng; đây không phải lỗi và sẽ không kích hoạt Restart=on-failure. code=dumped nghĩa là chương trình đã tạo core file; coredumpctl list sẽ hiển thị file này khi systemd-coredump được cài đặt.

Khi quản lý nhiều máy, NRestarts là giá trị đáng thu thập theo lịch. Một unit có bộ đếm tăng mỗi ngày đang lỗi mỗi ngày, dù không ai nhận ra. Khi đã quản lý quá hai hoặc ba máy, cách nhất quán để chạy một lệnh trên mọi server sẽ biến phỏng đoán đó thành báo cáo.

FAQ

Tại sao systemctl báo service đang active khi tiến trình đã chết?

systemd theo dõi một tiến trình cho mỗi service unit, đó là tiến trình chính, và Restart= chỉ đọc exit status của tiến trình đó. Mọi tiến trình khác do unit khởi động đều nằm trong cùng cgroup. systemd sẽ kill các tiến trình đó khi unit dừng, nhưng không theo dõi trạng thái thoát của chúng. Chạy systemctl show -p MainPID myapp.service và so sánh số này với systemd-cgls --unit myapp.service. Nếu tiến trình đã chết xuất hiện trong cây nhưng không phải là MainPID, systemd đã hoạt động đúng theo thiết kế. Cách sửa là dùng một tiến trình cho mỗi unit, đồng thời khai báo quan hệ giữa các unit bằng BindsTo=Upholds=.

“start request repeated too quickly” nghĩa là gì?

Điều đó nghĩa là unit đã được start hơn StartLimitBurst= lần trong StartLimitIntervalSec=. Giá trị mặc định là 5 lần start trong 10 giây, nên systemd đã dừng việc thử lại. Đây là rate limit và không cho biết service fail vì lý do gì, vì vậy hãy đọc các dòng journal bên trên. Xóa trạng thái bằng systemctl reset-failed myapp.service, sau đó sửa lỗi gốc. Nếu service phải chờ một thành phần khởi động chậm, hãy tăng RestartSec=, vì khoảng chờ mặc định 100 milliseconds khiến cả năm lần thử hết trong chưa đến một giây.

Nên dùng Restart=always hay Restart=on-failure?

Dùng on-failure cho hầu hết trường hợp. Tùy chọn này restart service khi process crash, exit với mã khác 0, timeout hoặc watchdog bị kích hoạt, nhưng không restart khi process chủ động exit 0. Chỉ dùng always khi chương trình exit bình thường vì một nguyên nhân nằm ngoài quyền kiểm soát của nó, chẳng hạn client trả về 0 khi peer ngắt kết nối. Nhược điểm của always là service đọc một config bị hỏng, ghi một lỗi rồi exit với mã 0 sẽ lặp vô hạn. Triệu chứng duy nhất có thể thấy là NRestarts tăng trong systemctl show.

Tại sao kill process thủ công không kích hoạt restart?

Vì systemd xem SIGHUP, SIGINT, SIGTERM và SIGPIPE là các lần exit bình thường, còn lệnh kill <pid> đơn giản sẽ gửi SIGTERM. Với Restart=on-failure, exit bình thường không được xem là failure, nên không có gì được restart và config trông như bị lỗi dù thực tế không phải vậy. Hãy kiểm tra bằng kill -9 <pid> hoặc systemctl kill -s SIGKILL myapp.service. Đây là các kiểu termination không bình thường và sẽ kích hoạt policy. Quy tắc tương tự giải thích tại sao systemctl stop không bao giờ can thiệp vào restart policy của bạn.

StartLimitIntervalSec và StartLimitBurst nằm ở đâu?

Trong section [Unit]. Tài liệu cũ và các phiên bản systemd cũ hơn đặt chúng trong [Service], nên các ví dụ được copy có thể không thống nhất. Không được đoán phiên bản của bạn công nhận section nào. Sau systemctl daemon-reload, hãy hỏi systemd đã load gì bằng systemctl show -p StartLimitBurst -p StartLimitIntervalUSec myapp.service và xem các giá trị đó là thông tin chính xác. systemd-analyze verify /etc/systemd/system/myapp.service sẽ phát hiện các key mà systemd hoàn toàn không nhận diện, và không in gì nếu file hợp lệ.

#systemd#restart#service-unit#cgroups#reliability