systemd unit không khởi động: đọc exit code
Đọc systemctl status trước tiên để phân biệt 203/EXEC, 226/NAMESPACE và lỗi ứng dụng tự thoát sau khi unit khởi động đúng rồi dừng sau 1 giây.
Vì sao systemd unit không khởi động
systemd unit không khởi động sẽ cho biết nguyên nhân 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. Mã trạng thái trong khoảng 200 có nghĩa là systemd chưa bao giờ chạy đến chương trình của bạn: lỗi xảy ra khi systemd tạo môi trường mà unit file yêu cầu. Mã trạng thái dưới 200 có nghĩa là chương trình của bạn đã chạy rồi tự thoát, vì vậy unit file có thể đúng và lỗi nằm ở ứng dụng.
Đó là luồng quyết định. Các phần bên dưới đều dựa trên phân loại này, theo đúng thứ tự các mã xuất hiện.
Ba lệnh nào trả lời câu hỏi, theo đúng thứ tự
systemctl status myapp.service
journalctl -u myapp.service -b --no-pager
systemd-analyze verify /etc/systemd/system/myapp.servicesystemctl 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à unit đang được enable, 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 đúng 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ị vài dòng log cuối và rút ngắn các dòng quá dài. Journal hiển thị mọi thứ chương trình đã in ra trước khi dừng, thường chính là lỗi thực sự. Thêm -n 100 để xem lịch sử dài hơn, hoặc chạy lệnh với -f trong terminal thứ hai trong khi bạn restart unit.
systemd-analyze verify load unit file mà không chạy nó. Lệnh này cảnh báo về các section và directive không xác định, đồng thời báo lỗi những command trong ExecStart= mà nó không thể thực thi. Cách này phát hiện hai nhóm lỗi khó nhận biết: key bị viết sai, khiến systemd bỏ qua khi load và chỉ đưa ra warning 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. Nếu chưa chạy, systemd vẫn dùng bản copy đã load trước đó, còn systemctl status sẽ thêm warning cho biết file trên disk đã thay đổi. Một bản sửa “không có tác dụng” thường là vì systemd chưa đọc lại file đó.
Có thêm 2 command cũng rất hữu ích. systemctl cat myapp.service in ra unit 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 giá trị theo đúng cách systemd đã parse, tức là cấu hình thực sự sẽ được 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.
- Đường dẫn trong
ExecStart=bị sai hoặc không phải đường dẫn tuyệt đối. Kiểm tra bằngls -lvà đối chiếu với chuỗi chính xác trong unit file. - File chưa có execute bit.
sudo chmod +x /opt/myapp/run.shsẽ 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 đó. - 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 python3sẽ 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. - 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 -Afile 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ã đó.
Tại sao tôi nhận được lỗi 217/USER hoặc 216/GROUP?
217/USER có nghĩa là account được khai báo trong User= không tồn tại tại thời điểm service khởi động. 216/GROUP là lỗi tương tự đối với Group= hoặc SupplementaryGroups=. Xác nhận bằng mỗi lệnh sau.
getent passwd appuser
getent group appgroupMỗi lệnh sẽ in một dòng, hoặc không in gì và trả về mã khác 0. Không có đầu ra nghĩa là system không biết tên đó, nên systemd không thể chuyển sang account đó và dừng trước khi thực thi. Cách sửa là tạo account, không phải đặt User=root. Chạy mỗi service bằng một system account riêng với quyền tối thiểu là mục đích chính của directive đó.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin appuserDynamicUser=yes bỏ qua vấn đề này bằng cách yêu cầu 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 đều cần dùng StateDirectory= cùng với nó, 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/uploadsKhi 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/myappnamei -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. 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 thực tế của chương trình không khớp với những gì Type= yêu cầu.
Type=simple, giá trị mặc định, yêu cầu chương trình chạy ở foreground. Nếu đưa vào một daemon tự fork vào background rồi thoát, systemd sẽ thấy tiến trình chính kết thúc và coi service đã hoàn tất. Hầu hết daemon đều có flag để chạy ở foreground, chẳng hạn nginx -g 'daemon off;'.
Type=forking yêu cầu tiến trình đầu tiên thoát sau khi child của nó sẵn sàng. Nếu đưa vào 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 log timeout.
Type=notify yêu cầu 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 protocol.
Chọn type dựa trên hành vi thực tế của chương trình. Phân biệt simple, forking, oneshot và notify là quyết định duy nhất để xử lý toàn bộ nhóm lỗi này.
Khi service liên tục thoát rồi khởi động lại, systemd sẽ ngừng thử và báo rằng yêu cầu start lặp lại quá nhanh. Unit sẽ vẫn ở trạng thái failed cho đến khi hết khoảng thời gian giới hạn rate hoặc bạn chạy sudo systemctl reset-failed myapp.service. Tăng giới hạn chỉ che giấu triệu chứng. Hãy đọc journal từ lần lỗi đầu tiên thay vì lần gần nhất, và 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 conditionConditionResult=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ì bị fail thì sao?
code=killed khác với code=exited. Có một tác nhân từ bên ngoài đã kết thúc tiến trình. status=9/KILL cho biết out of memory (OOM) killer đã can thiệp, và journal ghi rõ tiến trình mà nó chọn. Giới hạn do bạn tự đặt cũng thực hiện điều tương tự bên trong cgroup (control group), vì vậy hãy kiểm tra bộ nhớ còn trống trên host bằng free -m và kiểm tra unit xem có 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 một 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ờ khi start và terminate tiến trình, vì vậy 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 của system service chỉ 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 này cũng áp dụng cho WorkingDirectory=, EnvironmentFile= và mọi đường dẫn trong ReadWritePaths=.
ExecStart= không phải shell. systemd tách dòng thành các từ rồi tự gọi execve(). Pipe, redirection, glob, &&, backtick và ~ không có ý nghĩa: chúng được truyền đến chương trình dưới dạng các đối số nguyên văn. ExecStart=/usr/bin/myapp --flag > /tmp/out.log truyền > và /tmp/out.log cho myapp, sau đó chương trình này thoát với lỗi usage 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'Chỉ để ghi output thì bạn không cần làm vậy. Output của service được ghi vào journal theo mặc định, còn StandardOutput=append:/var/log/myapp.log ghi trực tiếp vào file mà không cần shell.
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= và EnvironmentFile=, còn không có gì khác được mở rộng. $HOME không được đặt cho system service trừ khi bạn tự đặt. EnvironmentFile= cũng không phải shell script: export không được dùng trong đó, quy tắc quoting của nó khác bash, và file bị thiếu sẽ khiến thao tác thất bại nếu bạn không thêm - trước đường dẫn.
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
systemctl status hiển thị status=203/EXEC 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() thất bại nên chương trình không bao giờ khởi động. Kiểm tra lần lượt 4 điểm: đường dẫn trong ExecStart= phải tồn tại và là đường dẫn tuyệt đối, file phải có quyền execute, shebang phải chỉ đến một interpreter tồn tại trên PATH của service, và file phải dùng ký tự xuống dòng Unix. Với lỗi 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 file.
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 daemon fork vào background sẽ bị xem là đã hoàn tất ngay khi fork. Với Type=forking, systemd chờ process đầu tiên thoát, nên chương trình chạy ở foreground làm start job bị treo cho đến khi TimeoutStartSec= hết thời gian. 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 tế 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 để mở rộng khoảng thời gian, hoặc pipe output vào grep. Nếu ứng dụng tự ghi vào file log, cũng hãy đọc file đó, vì systemd chỉ capture những gì chương trình gửi vào standard output và standard error.
Tại sao unit của tôi inactive nhưng không có thông báo lỗi?
Thông thường một directive Condition* đã bỏ qua unit đó. Các điều kiện này không tạo thông báo: 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 trong journal nêu rõ check đã 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ỡ trạng thái đó.
Tôi có cần chạy daemon-reload sau mỗi lần sửa unit file không?
Có, với mọi thay đổi trong 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, và cũng không cần chạy sau khi thay đổi file cấu hình thuộc về ứng dụng thay vì systemd.