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

Systemd unit không chạy: đọc exit code thế nào

Đọc systemctl status trước: phân biệt 203/EXEC, 226/NAMESPACE, code=exited và code=killed, kể cả unit chạy đúng rồi thoát sau một giây.

Lý do systemd unit không khởi động

Một systemd unit không khởi động sẽ cho biết lý do trong một trường. Chạy systemctl status <unit> rồi tìm code= và status= trên dòng báo lỗi. Status number trong khoảng 200 hầu như luôn có nghĩa là systemd chưa chạy đến chương trình của bạn: nó đã lỗi khi tạo môi trường mà unit file yêu cầu. Nếu dòng này ghi code=exited với status thấp hơn 200, chương trình của bạn đã chạy rồi tự thoát. Khi đó unit file có thể đúng, còn application mới là phần bị lỗi. code=killed có nghĩa là một signal đã kết thúc process. Đây là trường hợp riêng được đề cập ở gần cuối.

Đó là hướng quyết định. Phần bên dưới sẽ dựa trên hướng này, theo đúng thứ tự các number xuất hiện.

Ba lệnh nào trả lời câu hỏi này, theo đúng thứ tự

systemctl status myapp.service
journalctl -u myapp.service -b --no-pager
systemd-analyze verify /etc/systemd/system/myapp.service

systemctl status đưa ra kết luận. Đọc dòng Loaded: trước, vì dòng này cho biết tên file mà systemd thực sự đã phân tích và cho biết unit đang được enable, bị mask hay hoàn toàn không được tìm thấy. Sau đó đọc dòng Active: cùng cặp code= và status= bên dưới.

journalctl -u myapp.service -b --no-pager cung cấp thông tin chi tiết. -u lọc chỉ còn unit đó, -b giới hạn output trong lần boot hiện tại để bạn không đọc nhầm lỗi từ tuần trước, còn --no-pager in thẳng ra terminal để bạn có thể pipe vào grep. status chỉ hiển thị một số dòng log cuối cùng và rút ngắn các dòng quá dài. Journal hiển thị mọi nội dung chương trình đã in trước khi thoát, thường đó mới là lỗi thực sự. Nếu không có -n, bạn sẽ nhận toàn bộ entry của lần boot này. Thêm -n 100 để chỉ giữ 100 entry mới nhất, hoặc chạy với -f trong terminal thứ hai trong khi bạn restart unit.

systemd-analyze verify tải unit file nhưng không chạy unit. Lệnh này cảnh báo về các section và directive không xác định, đồng thời đánh dấu những command trong ExecStart= mà nó không thể thực thi. Lệnh này phát hiện hai nhóm lỗi thường bị bỏ sót: key bị gõ sai, khiến systemd bỏ qua khi load và chỉ đưa ra một cảnh báo mà hầu hết mọi người không đọc, và path không tồn tại.

Sau khi chỉnh sửa bất kỳ unit file nào, hãy chạy sudo systemctl daemon-reload. Cho đến khi chạy lệnh này, systemd vẫn dùng bản copy đã load trước đó, còn systemctl status bổ sung cảnh báo rằng file trên disk đã thay đổi. Một bản sửa “không có tác dụng” thường là do systemd chưa đọc bản sửa đó.

Có thêm hai command rất hữu ích. systemctl cat myapp.service in unit đã được áp dụng hiệu lực, gồm file chính và mọi drop-in bên dưới /etc/systemd/system/myapp.service.d/. systemctl show myapp.service -p ExecStart -p User -p WorkingDirectory in các value theo cách systemd đã phân tích chúng; đó là các value thực sự sẽ được dùng khi chạy.

status=203/EXEC nghĩa là gì?

203/EXEC nghĩa là systemd đã hoàn tất phần thiết lập, gọi execve(), nhưng kernel từ chối. Chương trình của bạn chưa chạy dòng code nào của chính nó. Có 4 nguyên nhân bao quát gần như mọi trường hợp.

  1. Đường dẫn trong ExecStart= bị sai hoặc không phải đường dẫn tuyệt đối. Kiểm tra bằng ls -l và đối chiếu với chuỗi chính xác trong unit file.
  2. File chưa có execute bit. sudo chmod +x /opt/myapp/run.sh sẽ sửa việc này. File được giải nén từ archive hoặc copy từ máy khác thường bị mất bit đó.
  3. Dòng shebang bị hỏng. Kernel đọc dòng đầu tiên của script rồi chạy interpreter được chỉ định ở đó. Vì vậy #!/usr/bin/env python3 sẽ fail khi PATH của service không có python3. File được lưu với line ending kiểu Windows sẽ yêu cầu một interpreter tên là /bin/bash\r, nhưng interpreter đó không tồn tại.
  4. File không phải loại mà máy này có thể chạy: sai architecture hoặc là text file hoàn toàn không có shebang.

Hãy reproduce lỗi thủ công bằng user chạy service trước khi thay đổi bất cứ thứ gì.

sudo -u appuser /opt/myapp/run.sh
file /opt/myapp/run.sh
head -1 /opt/myapp/run.sh | cat -A

file cho biết architecture và báo "with CRLF line terminators" khi line ending là nguyên nhân. cat -A cũng hiển thị lỗi tương tự dưới dạng ^M ở cuối. Dùng sed -i 's/\r$//' /opt/myapp/run.sh để loại bỏ chúng.

Có một lưu ý quan trọng về khoảng mã này: từ 200 trở lên chỉ là quy ước, không phải bảo đảm. Chương trình của bạn có thể tự exit với mã 203, và systemd không thể phân biệt hai trường hợp đó. systemd-analyze exit-status 203 in tên và class của mọi code, giúp bạn đọc bảng dễ hơn. Tuy nhiên, nếu ứng dụng của bạn dùng exit code lớn hơn 199, hãy thay đổi các mã đó.

Vì sao tôi nhận lỗi 217/USER hoặc 216/GROUP?

217/USER thường có nghĩa là account được ghi trong User= không tồn tại tại thời điểm service khởi động. Mã này cũng bao gồm lỗi thiết lập user namespacing cho tùy chọn sandboxing như PrivateUsers=. Trong trường hợp đó, journal sẽ ghi "Failed to set up user namespacing" thay vì nêu tên user bị thiếu. 216/GROUP là lỗi tương tự đối với Group= hoặc SupplementaryGroups=. Hãy xác nhận từng trường hợp bằng một lệnh.

getent passwd appuser
getent group appgroup

Mỗi lệnh sẽ in ra một dòng, hoặc không in gì và trả về mã khác 0. Không có kết quả nghĩa là system không biết tên đó. Vì vậy, systemd không thể chuyển sang account đó và dừng trước khi chạy exec. Cách khắc phục là tạo account, không phải đặt User=root. Chạy một service bằng system account riêng với quyền tối thiểu chính là mục đích của directive này.

sudo useradd --system --no-create-home --shell /usr/sbin/nologin appuser

DynamicUser=yes bỏ qua vấn đề này bằng cách để systemd cấp một account tạm thời cho mỗi lần khởi động. Cách này phù hợp với service không lưu state. Bất kỳ service nào ghi file cũng cần dùng StateDirectory= kèm theo, vì user ID thay đổi giữa các lần khởi động và các file trong path thông thường sẽ thuộc về một account không còn tồn tại.

226/NAMESPACE là gì?

226/NAMESPACE xuất phát từ các directive sandboxing. Khi một unit đặt ProtectSystem=, ProtectHome=, PrivateTmp=, ReadWritePaths= hoặc tùy chọn tương tự, systemd sẽ tạo một mount namespace riêng cho service đó trước khi thực thi chương trình. Namespace ở đây là một chế độ xem filesystem riêng cho một process. Nếu bất kỳ mount nào trong kế hoạch đó thất bại, quá trình start sẽ lỗi với mã 226 và chương trình của bạn không bao giờ chạy.

Nguyên nhân thường gặp là một path trong ReadWritePaths= không tồn tại. ProtectSystem=strict mount toàn bộ filesystem ở chế độ chỉ đọc, còn ReadWritePaths= mở lại các path được chỉ định để cho phép ghi. systemd không thể mở lại một directory không tồn tại. Có 2 cách sửa phù hợp. Để systemd tạo directory bằng StateDirectory=. Tùy chọn này tạo /var/lib/<name> ở mỗi lần start và cấp quyền sở hữu cho service user. Hoặc thêm - trước path để yêu cầu systemd bỏ qua entry đó khi source không tồn tại. Cách sửa không nên dùng là xóa phần hardening. Cách này đổi một vấn đề mất 5 phút xử lý lấy một rủi ro tồn tại vĩnh viễn.

[Service]
ProtectSystem=strict
ProtectHome=yes
StateDirectory=myapp
ReadWritePaths=-/srv/uploads

Khi không xác định được dòng nào gây lỗi, hãy xóa toàn bộ block hardening, reload rồi start service. Nếu service khởi động được, hãy thêm lại từng dòng một và restart sau mỗi lần thêm. Hai lỗi gần giống trong nhóm này là 233/RUNTIME_DIRECTORY và 238/STATE_DIRECTORY. Chúng có nghĩa là systemd không thể tạo hoặc nhận quyền sở hữu directory được nêu trong RuntimeDirectory= hoặc StateDirectory=, thường vì path đó đã tồn tại và thuộc về user khác.

Vì sao xuất hiện 200/CHDIR khi WorkingDirectory có vẻ đúng?

200/CHDIR có nghĩa là chdir() vào WorkingDirectory= đã thất bại. Thư mục không tồn tại hoặc user chạy service không thể truy cập thư mục đó. Để truy cập một thư mục, cần có quyền execute trên thư mục đó và trên mọi thư mục cha bên trên. Vì vậy, một /home/deploy/app có quyền đọc đầy đủ vẫn không thể truy cập khi /home/deploy có mode 700 và service chạy dưới appuser.

sudo -u appuser test -x /srv/myapp && echo ok
namei -l /srv/myapp

namei -l in owner và mode của từng thành phần trong path. Đây là cách nhanh nhất để tìm thư mục đang chặn toàn bộ phần còn lại. Đặt WorkingDirectory=-/srv/myapp sẽ khiến việc thiếu thư mục không gây lỗi fatal. Cách này phù hợp với chương trình không quan tâm thư mục khởi động, nhưng không phù hợp với chương trình mở file bằng relative path.

Tại sao service khởi động rồi dừng sau một giây?

Ở đây không có mã 200-series và thường cũng không có nội dung lỗi nào. Unit hiển thị inactive (dead) ngay sau khi khởi động hoặc liên tục chuyển qua activating (auto-restart). systemd đã tạo môi trường đúng cách. Vấn đề nằm ở chỗ hành vi của chương trình không khớp với những gì Type= cam kết.

Type=simple, là giá trị mặc định, cho biết chương trình phải chạy ở foreground. Nếu chạy một daemon tự fork vào background rồi thoát, systemd sẽ thấy process chính kết thúc và coi service đã hoàn tất. Hầu hết daemon đều có flag để giữ process ở foreground, chẳng hạn nginx -g 'daemon off;'.

Type=forking cho biết process đầu tiên sẽ thoát sau khi child của nó sẵn sàng. Nếu cung cấp một chương trình chạy ở foreground, start job sẽ chờ đến khi TimeoutStartSec= hết thời gian, mặc định là 90 giây. Sau đó systemd sẽ kill chương trình và ghi timeout vào log.

Type=notify cho biết chương trình gọi sd_notify() để thông báo đã sẵn sàng. Chương trình không hỗ trợ cơ chế này sẽ không gửi thông báo nào. Vì vậy, thao tác start sẽ timeout và journal ghi nhận kết quả là lỗi giao thức.

Chọn type dựa trên hành vi thực tế của chương trình. Sự khác nhau giữa simple, forking, oneshot và notify là quyết định duy nhất cần đưa ra để xử lý toàn bộ nhóm lỗi này.

Khi một service liên tục thoát, systemd sẽ ngừng thử và báo rằng yêu cầu start đã lặp lại quá nhanh. Unit sau đó vẫn ở trạng thái failed và systemd sẽ không tự động restart unit. Bạn có thể start service thủ công sau khi hết khoảng thời gian giới hạn rate, hoặc chạy sudo systemctl reset-failed myapp.service. Lệnh này xóa trạng thái failed và bộ đếm restart, để thao tác start thủ công có hiệu lực ngay. Tăng giới hạn chỉ che giấu triệu chứng. Hãy đọc journal bắt đầu từ lần fail đầu tiên thay vì lần cuối, rồi xem Restart=on-failure thực sự retry những gì trước khi thay đổi cấu hình.

Vì sao unit ở trạng thái inactive mà không có bất kỳ lỗi nào?

Unit có thể bị bỏ qua thay vì được khởi động. Các directive Condition* được thiết kế để hoạt động im lặng: khi kiểm tra không đạt, systemd đánh dấu job là thành công và không làm gì thêm. Unit có ConditionPathExists=/etc/myapp/config.yml sẽ không bao giờ khởi động khi file đó bị thiếu, đồng thời cũng không báo lỗi.

systemctl show myapp.service -p ConditionResult -p ConditionTimestamp
journalctl -u myapp.service -b --no-pager | grep -i condition

ConditionResult=no xác nhận unit đã bị bỏ qua, còn journal cho biết lần kiểm tra nào không đạt. Hãy dùng directive Assert* khi prerequisite bị thiếu phải tạo lỗi rõ ràng. Conditions, assert và thứ tự unit giải thích kiểm tra nào nên đặt ở vị trí nào.

Một số trường hợp im lặng khác cũng thường gặp. Lỗi “could not be found” thường có nghĩa là file nằm sai thư mục hoặc bạn chưa reload: các unit file do bạn tạo phải nằm trong /etc/systemd/system/. Unit bị mask sẽ từ chối mọi lần start cho đến khi sudo systemctl unmask myapp.service gỡ trạng thái đó. Ngoài ra, systemctl enable sẽ fail với unit không có section [Install], vì vậy hãy thêm WantedBy=multi-user.target.

Nếu tiến trình bị kill thay vì failed thì sao?

code=killed là trường hợp khác với code=exited. Có thứ gì đó từ bên ngoài đã kết thúc tiến trình. status=9/KILL thường chỉ ra OOM (out of memory) killer. Khi đó, journal thường ghi rõ tiến trình được chọn. Tuy nhiên, SIGKILL không cho biết ai đã gửi tín hiệu này. systemd cũng gửi SIGKILL khi một service đang dừng bỏ qua SIGTERM quá TimeoutStopSec=. Vì vậy, hãy kiểm tra xem có thao tác stop hoặc restart nào đang diễn ra hay không trước khi quy kết cho việc thiếu bộ nhớ. Giới hạn do bạn tự đặt cũng gây ra kết quả tương tự bên trong cgroup (control group). Hãy kiểm tra bộ nhớ còn trống trên host bằng free -m và kiểm tra unit có đặt MemoryMax= hay không. MemoryMax, CPUQuota và các giới hạn cgroup khác giải thích giới hạn nào sẽ kill tiến trình và giới hạn nào chỉ làm tiến trình chạy chậm lại.

status=15/TERM ngay sau khi thử start thường có nghĩa là systemd đã hết thời gian chờ khởi động và terminate tiến trình. Khi đó, hãy quay lại Type=.

Hai thói quen giúp ngăn chặn hầu hết các lỗi này

Luôn dùng đường dẫn tuyệt đối. systemd không chạy login shell của bạn, nên không có .bashrc, không có .profile và cũng không có virtual environment đã được kích hoạt. $PATH cho system service là một danh sách tích hợp ngắn, không chứa /opt hoặc các shim của trình quản lý phiên bản ngôn ngữ. Hãy ghi đầy đủ /usr/bin/python3 hoặc /opt/myapp/venv/bin/python. command -v myapp trong shell sẽ in ra đường dẫn để bạn sao chép. Quy tắc tương tự áp dụng cho WorkingDirectory=, EnvironmentFile= và mọi đường dẫn trong ReadWritePaths=.

ExecStart= không phải là shell. systemd tách dòng lệnh thành các từ rồi tự gọi execve(). Pipe, redirect, glob, &&, backtick và ~ không có tác dụng: chúng được truyền đến chương trình dưới dạng các đối số literal. ExecStart=/usr/bin/myapp --flag > /tmp/out.log truyền > và /tmp/out.log cho myapp, sau đó myapp thoát với lỗi sử dụng trông hoàn toàn không giống lỗi của systemd. Khi cần các tính năng của shell, hãy gọi shell.

ExecStart=/bin/sh -c '/usr/bin/myapp --flag | /usr/bin/tee -a /var/log/myapp.log'

Đối với output, bạn không cần làm vậy. Output của service mặc định được ghi vào journal, còn StandardOutput=append:/var/log/myapp.log ghi vào file mà không cần shell tham gia.

Việc mở rộng biến cũng bị giới hạn tương tự. $MYVAR và ${MYVAR} được thay thế từ Environment=, EnvironmentFile= cùng một số biến systemd tự đặt như $USER; các specifier như % và %i cũng được mở rộng. Không có biến nào từ login shell được mở rộng. $HOME chỉ được đặt khi unit có User=, thông qua giá trị mặc định của SetLoginEnvironment=. Vì vậy, service chạy dưới root nhưng không có User= sẽ không có $HOME trừ khi bạn tự đặt biến này. EnvironmentFile= cũng không phải là shell script: export không thuộc về nó, quy tắc quoting của nó khác bash, và file bị thiếu sẽ gây lỗi nghiêm trọng trừ khi bạn thêm tiền tố - vào path.

Xử lý trực tiếp trên server đang chạy

Đọc code, chứng minh nguyên nhân, thay đổi một thứ, rồi restart. Thứ tự này quan trọng hơn việc biết mọi con số, vì nó ngăn bạn thực hiện 3 chỉnh sửa mang tính phỏng đoán cùng lúc rồi không biết thay đổi nào có tác dụng. Cách làm này cũng áp dụng cho các unit do người khác viết. Một timer không bao giờ chạy thường là do service chưa bao giờ khởi động, vì vậy hãy debug service trước: systemd timer và service mà nó kích hoạt sẽ gặp đúng các lỗi nêu trên, còn timer che mất output cho đến khi bạn yêu cầu journal hiển thị nó.

FAQ

status=203/EXEC trong systemctl status có nghĩa là gì?

systemd đã thiết lập mọi thứ mà unit yêu cầu, nhưng lệnh gọi execve() không chạy được, nên chương trình chưa bao giờ khởi động. Hãy kiểm tra lần lượt 4 điều: đường dẫn trong ExecStart= có tồn tại và là đường dẫn tuyệt đối; file có bit execute; shebang chỉ đến một interpreter tồn tại trong PATH của service; và file dùng xuống dòng Unix. Với trường hợp cuối, file báo "with CRLF line terminators", biến tên interpreter thành /bin/bash\r và khiến kernel từ chối chạy.

Tại sao service của tôi khởi động rồi dừng ngay?

Unit file mô tả hành vi mà chương trình không có. Với Type=simple, systemd chờ chương trình chạy ở foreground, nên một daemon fork vào background sẽ bị xem là đã kết thúc ngay khi fork. Với Type=forking, systemd chờ process đầu tiên thoát, nên chương trình chạy ở foreground khiến start job bị treo cho đến khi TimeoutStartSec= hết thời gian. Hãy chọn Type= phù hợp với chương trình. Nếu chương trình có flag chạy ở foreground, hãy dùng flag đó cùng với Type=simple mặc định.

Làm cách nào để xem lỗi thực thay vì output trạng thái ngắn?

systemctl status chỉ in vài dòng cuối của journal và cắt ngắn các dòng dài. Chạy journalctl -u myapp.service -b --no-pager để xem toàn bộ log mà unit ghi trong lần boot này, thêm -n 200 để chỉ giữ lại 200 dòng mới nhất, hoặc pipe output vào grep. Nếu ứng dụng tự ghi log vào file riêng, hãy đọc cả file đó, vì systemd chỉ capture những gì chương trình gửi tới standard output và standard error.

Tại sao unit của tôi ở trạng thái inactive nhưng không có thông báo lỗi?

Thường nguyên nhân là một directive Condition* đã bỏ qua unit. Các kiểm tra này không báo lỗi: khi condition thất bại, start job vẫn được đánh dấu là thành công. Chạy systemctl show myapp.service -p ConditionResult và tìm ConditionResult=no, sau đó đọc dòng journal nêu rõ kiểm tra đã thất bại. Nguyên nhân phổ biến khác là unit bị mask. Unit bị mask sẽ từ chối mọi lần start cho đến khi sudo systemctl unmask gỡ mask.

Có cần chạy daemon-reload sau mỗi lần sửa unit file không?

Có, nếu bạn sửa unit file hoặc drop-in. sudo systemctl daemon-reload yêu cầu systemd đọc lại các file từ disk, sau đó sudo systemctl restart myapp.service áp dụng thay đổi cho service đang chạy. Bạn không cần chạy lệnh này sau systemctl edit vì lệnh đó tự reload. Bạn cũng không cần chạy sau khi sửa file cấu hình thuộc về ứng dụng thay vì thuộc về systemd.

#systemd#troubleshooting#journalctl#exit-codes#linux-fundamentals