Jak zdiagnozować błąd jednostki systemd przez exit code
Sprawdź status jednostki za pomocą systemctl status. Wyjaśniamy znaczenie kodów 203/EXEC oraz 226/NAMESPACE i przyczyny, dla których usługa kończy działanie tuż po starcie.
Dlaczego jednostka systemd nie uruchamia się
Jednostka systemd, która nie chce się uruchomić, podaje przyczynę w jednym polu. Uruchom systemctl status <unit> i poszukaj code= oraz status= w wierszu zgłaszającym błąd. Kod statusu z zakresu 200 oznacza, że systemd nigdy nie dotarł do Twojego programu: wystąpił błąd podczas przygotowywania środowiska wymaganego przez plik jednostki. Status poniżej 200 oznacza, że program został uruchomiony i zakończył działanie samodzielnie, co sugeruje, że plik jednostki jest poprawny, a problem leży po stronie aplikacji.
Ten podział stanowi ścieżkę diagnostyczną. Wszystkie poniższe kroki wynikają z tego rozróżnienia i są ułożone w kolejności występowania numerów błędów.
Które trzy polecenia odpowiadają na to pytanie, w kolejności
systemctl status myapp.service
journalctl -u myapp.service -b --no-pager
systemd-analyze verify /etc/systemd/system/myapp.servicesystemctl status dostarcza werdykt. Należy najpierw przeczytać linię Loaded:, ponieważ wskazuje ona plik, który systemd faktycznie przetworzył, oraz informuje, czy jednostka jest włączona, zamaskowana, czy w ogóle nieodnaleziona. Następnie należy sprawdzić linię Active: oraz parę code= i status= poniżej.
journalctl -u myapp.service -b --no-pager dostarcza szczegółów. -u filtruje dane do konkretnej jednostki, -b ogranicza wynik do bieżącego uruchomienia systemu, aby nie analizować błędów sprzed tygodnia, a --no-pager wypisuje dane bezpośrednio na terminal, co pozwala na przekierowanie ich do grep. status wyświetla tylko ostatnie kilka linii dziennika i skraca te zbyt długie. Dziennik pokazuje wszystko, co program wypisał przed zakończeniem działania, co zazwyczaj stanowi rzeczywistą przyczynę błędu. Należy dodać -n 100, aby uzyskać dłuższą historię, lub uruchomić polecenie z -f w drugim terminalu podczas restartowania jednostki.
systemd-analyze verify wczytuje plik jednostki bez jej uruchamiania. Ostrzega o nieznanych sekcjach i dyrektywach oraz oznacza polecenia w ExecStart=, których nie może wykonać. Pozwala to wykryć dwie ciche klasy błędów: literówkę w kluczu, którą systemd ignoruje podczas ładowania z ostrzeżeniem, którego większość osób nie czyta, oraz nieistniejącą ścieżkę.
Po edycji dowolnego pliku jednostki należy wykonać sudo systemctl daemon-reload. Dopóki tego nie zrobisz, systemd będzie korzystał z kopii wczytanej wcześniej, a systemctl status doda ostrzeżenie, że plik na dysku uległ zmianie. Poprawka, która „nic nie zmieniła”, często jest poprawką, której systemd jeszcze nie odczytał.
Dwa kolejne polecenia są równie istotne. systemctl cat myapp.service wyświetla efektywną konfigurację jednostki, czyli główny plik wraz ze wszystkimi plikami uzupełniającymi z /etc/systemd/system/myapp.service.d/. systemctl show myapp.service -p ExecStart -p User -p WorkingDirectory wyświetla te wartości w formie, w jakiej zostały przetworzone przez systemd, co odzwierciedla faktyczne parametry uruchomieniowe.
Co oznacza status=203/EXEC?
203/EXEC oznacza, że systemd zakończył przygotowania, wywołał execve(), a jądro odmówiło wykonania operacji. Program nie uruchomił ani jednej linii własnego kodu. Prawie każdy przypadek wynika z jednej z czterech przyczyn.
- Ścieżka w
ExecStart=jest błędna lub nie jest ścieżką bezwzględną. Należy sprawdzić ją za pomocąls -l, porównując z dokładnym ciągiem znaków w pliku jednostki. - Plik nie posiada bitu wykonywalności.
sudo chmod +x /opt/myapp/run.shnaprawia ten problem. Pliki rozpakowane z archiwum lub skopiowane z innej maszyny często tracą ten atrybut. - Linia shebang jest uszkodzona. Jądro odczytuje pierwszą linię skryptu i uruchamia wskazany w niej interpreter, więc
#!/usr/bin/env python3kończy się niepowodzeniem, gdy zmienna PATH usługi nie zawierapython3, a plik zapisany z zakończeniami linii w stylu Windows odwołuje się do interpretera o nazwie/bin/bash\r, który nie istnieje. - Plik nie jest obsługiwany przez daną architekturę lub jest plikiem tekstowym bez linii shebang.
Przed wprowadzeniem jakichkolwiek zmian należy odtworzyć błąd ręcznie, działając jako użytkownik usługi.
sudo -u appuser /opt/myapp/run.sh
file /opt/myapp/run.sh
head -1 /opt/myapp/run.sh | cat -Afile wskazuje architekturę i zgłasza "with CRLF line terminators", gdy problemem są zakończenia linii. cat -A pokazuje to samo w postaci końcowego ^M. Należy je usunąć za pomocą sed -i 's/\r$//' /opt/myapp/run.sh.
Warto pamiętać o jednym zastrzeżeniu: zakres od 200 wzwyż to konwencja, a nie gwarancja. Własny program może zakończyć się kodem 203, a systemd nie jest w stanie odróżnić tej sytuacji od błędu wykonania. systemd-analyze exit-status 203 wyświetla nazwę i klasę każdego kodu, co ułatwia odczytanie tabeli, jednak jeśli aplikacja używa kodów wyjścia powyżej 199, należy je zmienić.
Dlaczego otrzymuję błąd 217/USER lub 216/GROUP?
217/USER oznacza, że konto wskazane w User= nie istnieje w momencie uruchamiania usługi. 216/GROUP to ten sam błąd dla Group= lub SupplementaryGroups=. Potwierdź to za pomocą odpowiednich poleceń.
getent passwd appuser
getent group appgroupKażde z nich wypisuje linię danych lub nie wypisuje nic i zwraca kod błędu. Brak wyniku oznacza, że nazwa jest nieznana dla systemu, więc systemd nie może przełączyć się na to konto i przerywa działanie przed wykonaniem polecenia exec. Rozwiązaniem jest utworzenie konta, a nie usuwanie User=root. Uruchamianie usługi na dedykowanym koncie systemowym z ograniczonymi uprawnieniami jest głównym celem tej dyrektywy.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin appuserDynamicUser=yes pozwala ominąć ten problem, zlecając systemd przydzielenie tymczasowego konta przy każdym starcie. Jest to odpowiednie dla usług, które nie przechowują żadnego stanu. Wszystko, co zapisuje pliki, wymaga dodatkowo StateDirectory=, ponieważ identyfikator użytkownika zmienia się przy każdym uruchomieniu, a pliki w standardowej ścieżce kończą jako własność konta, które już nie istnieje.
Czym jest 226/NAMESPACE?
226/NAMESPACE wynika z dyrektyw izolacji (sandboxing). Gdy jednostka ustawia ProtectSystem=, ProtectHome=, PrivateTmp=, ReadWritePaths= lub cokolwiek podobnego, systemd tworzy prywatną przestrzeń nazw montowania (mount namespace) dla tej usługi przed wykonaniem programu. Przestrzeń nazw jest tutaj prywatnym widokiem systemu plików dla pojedynczego procesu. Jeśli jakiekolwiek montowanie w tym planie zawiedzie, start kończy się błędem 226, a program nie zostaje uruchomiony.
Typową przyczyną jest ścieżka w ReadWritePaths=, która nie istnieje. ProtectSystem=strict montuje cały system plików w trybie tylko do odczytu, a ReadWritePaths= ponownie otwiera wskazane ścieżki do zapisu. systemd nie może ponownie otworzyć katalogu, którego nie ma. Dobre są dwa rozwiązania. Pozwól systemd utworzyć katalog za pomocą StateDirectory=, co tworzy /var/lib/<name> przy każdym starcie i przypisuje go użytkownikowi usługi, lub poprzedź ścieżkę znakiem -, co instruuje systemd, aby zignorował ten wpis, gdy źródło nie istnieje. Złym rozwiązaniem jest usunięcie zabezpieczeń, co zamienia pięciominutowy problem na trwałe zagrożenie.
[Service]
ProtectSystem=strict
ProtectHome=yes
StateDirectory=myapp
ReadWritePaths=-/srv/uploadsJeśli nie można ustalić, która linia odpowiada za błąd, usuń cały blok zabezpieczeń, przeładuj konfigurację i uruchom usługę. Jeśli usługa wystartuje, dodawaj linie z powrotem pojedynczo, restartując po każdej z nich. Dwoma pokrewnymi ustawieniami w tej grupie są 233/RUNTIME_DIRECTORY oraz 238/STATE_DIRECTORY. Oznaczają one, że systemd nie mógł utworzyć lub przejąć własności katalogu wskazanego w RuntimeDirectory= lub StateDirectory=, zazwyczaj dlatego, że ścieżka ta już istnieje i należy do innego użytkownika.
Dlaczego pojawia się błąd 200/CHDIR, mimo że WorkingDirectory wydaje się poprawny?
200/CHDIR oznacza, że chdir() do WorkingDirectory= zakończyło się niepowodzeniem. Katalog nie istnieje lub użytkownik usługi nie ma uprawnień do wejścia do niego. Wejście do katalogu wymaga uprawnienia wykonywania (execute) dla tego katalogu oraz dla każdego katalogu nadrzędnego w ścieżce. Zatem w pełni czytelny /home/deploy/app jest nieosiągalny, gdy /home/deploy ma tryb 700, a usługa działa jako appuser.
sudo -u appuser test -x /srv/myapp && echo ok
namei -l /srv/myappnamei -l wyświetla właściciela oraz tryb dostępu dla każdego elementu ścieżki, co jest najszybszym sposobem na znalezienie katalogu blokującego dostęp. Ustawienie WorkingDirectory=-/srv/myapp sprawia, że brak katalogu nie powoduje błędu krytycznego. Jest to właściwe rozwiązanie dla programu, któremu nie zależy na katalogu startowym, lecz błędne dla aplikacji otwierającej pliki przy użyciu ścieżek relatywnych.
Dlaczego usługa uruchamia się, a sekundę później zatrzymuje?
W takiej sytuacji nie występuje kod z serii 200, a często brakuje jakiegokolwiek komunikatu o błędzie. Jednostka wskazuje inactive (dead) zaraz po starcie lub przechodzi cykl activating (auto-restart). systemd poprawnie przygotował środowisko. Rozbieżność wynika z różnicy między działaniem programu a tym, co zadeklarowano w Type=.
Type=simple, ustawienie domyślne, zakłada, że program pozostaje na pierwszym planie. Jeśli uruchomiony zostanie demon, który tworzy proces potomny w tle i kończy działanie, systemd uzna, że główny proces zakończył pracę i uzna usługę za wykonaną. Większość demonów posiada flagę wymuszającą pracę na pierwszym planie, taką jak nginx -g 'daemon off;'.
Type=forking oznacza, że pierwszy proces kończy działanie, gdy jego proces potomny jest gotowy. W przypadku programu działającego na pierwszym planie, zadanie startowe czeka do momentu, aż upłynie TimeoutStartSec= (domyślnie 90 sekund), po czym systemd przerywa proces i loguje przekroczenie czasu oczekiwania.
Type=notify oznacza, że program wywołuje sd_notify(), aby zgłosić gotowość. Program bez takiej obsługi nie wysyła żadnego powiadomienia, więc start kończy się przekroczeniem czasu, a dziennik rejestruje błąd protokołu.
Należy wybrać typ zgodny z rzeczywistym działaniem programu. Czym różnią się typy simple, forking, oneshot oraz notify to kluczowa decyzja, która rozwiązuje ten typ problemów.
Gdy usługa kończy działanie wielokrotnie, systemd przestaje podejmować próby i zgłasza, że żądanie startu powtarzało się zbyt szybko. Jednostka pozostaje w stanie awarii do momentu upłynięcia okna limitu częstotliwości lub wykonania polecenia sudo systemctl reset-failed myapp.service. Zwiększenie limitu jedynie maskuje objawy. Należy sprawdzić dziennik od momentu pierwszej awarii, a nie ostatniej, oraz zapoznać się z artykułem co faktycznie ponawia Restart=on-failure przed wprowadzeniem zmian.
Dlaczego jednostka jest nieaktywna bez żadnego błędu?
Jednostka może zostać pominięta zamiast uruchomiona. Dyrektywy Condition* są z założenia ciche: gdy sprawdzenie kończy się niepowodzeniem, systemd oznacza zadanie jako wykonane i nie podejmuje żadnych działań. Jednostka zawierająca ConditionPathExists=/etc/myapp/config.yml nigdy nie wystartuje, dopóki dany plik nie istnieje, i nigdy nie zgłosi przy tym błędu.
systemctl show myapp.service -p ConditionResult -p ConditionTimestamp
journalctl -u myapp.service -b --no-pager | grep -i conditionPolecenie ConditionResult=no potwierdza pominięcie, a dziennik wskazuje sprawdzenie, które nie zostało spełnione. Użyj dyrektywy Assert*, gdy brak wymaganego zasobu powinien skutkować wyraźnym błędem. Artykuł Warunki, asercje i kolejność jednostek wyjaśnia, które sprawdzenie należy stosować w danym przypadku.
Istnieje kilka innych cichych przypadków. Błąd "could not be found" zazwyczaj oznacza, że plik znajduje się w niewłaściwym katalogu lub nie przeładowano konfiguracji: własne pliki jednostek należy umieszczać w /etc/systemd/system/. Zamaskowana jednostka odmawia uruchomienia, dopóki polecenie sudo systemctl unmask myapp.service jej nie odblokuje. Ponadto systemctl enable kończy się niepowodzeniem w przypadku jednostki bez sekcji [Install], dlatego należy dodać WantedBy=multi-user.target.
Co jeśli proces został zabity, a nie zakończył się błędem?
code=killed to inna sytuacja niż code=exited. Coś zakończyło proces z zewnątrz. status=9/KILL wskazuje na mechanizm OOM (out of memory) killer, a dziennik systemd podaje nazwę wybranego procesu. Limit ustawiony samodzielnie powoduje to samo wewnątrz cgroup (control group), dlatego należy sprawdzić wolną pamięć na hoście za pomocą free -m oraz zweryfikować jednostkę pod kątem MemoryMax=. MemoryMax, CPUQuota i inne limity cgroup wyjaśnia, który limit powoduje zabicie procesu, a który jedynie go spowalnia.
status=15/TERM bezpośrednio po próbie startu zazwyczaj oznacza, że systemd przekroczył limit czasu uruchamiania i zakończył proces, co odsyła z powrotem do Type=.
Dwa nawyki zapobiegające większości tych awarii
Używaj wszędzie ścieżek bezwzględnych. systemd nie uruchamia Twojej powłoki logowania, więc nie ma .bashrc, .profile ani aktywowanego środowiska wirtualnego. $PATH dla usługi systemowej to krótka, wbudowana lista, która nie zawiera /opt ani shimów menedżera wersji języka. Zapisuj /usr/bin/python3 lub /opt/myapp/venv/bin/python w pełnej formie. command -v myapp w Twojej powłoce wyświetli ścieżkę do wklejenia. Ta sama zasada dotyczy WorkingDirectory=, EnvironmentFile= oraz każdej ścieżki w ReadWritePaths=.
ExecStart= nie jest powłoką. systemd dzieli linię na słowa i samodzielnie wywołuje execve(). Potoki, przekierowania, znaki wieloznaczne, &&, odwrotne apostrofy i ~ nie mają znaczenia: trafiają do programu jako dosłowne argumenty. ExecStart=/usr/bin/myapp --flag > /tmp/out.log przekazuje > i /tmp/out.log do myapp, który następnie kończy działanie błędem składni, który nie przypomina problemu z systemd. Gdy potrzebujesz funkcji powłoki, wywołaj powłokę.
ExecStart=/bin/sh -c '/usr/bin/myapp --flag | /usr/bin/tee -a /var/log/myapp.log'Do samego przekierowania wyjścia nie jest to potrzebne. Wyjście usługi trafia domyślnie do dziennika, a StandardOutput=append:/var/log/myapp.log zapisuje do pliku bez udziału powłoki.
Rozwijanie zmiennych jest ograniczone w ten sam sposób. $MYVAR i ${MYVAR} są zastępowane przez Environment= i EnvironmentFile=, nic innego nie jest rozwijane. $HOME nie jest ustawione dla usługi systemowej, chyba że zrobisz to samodzielnie. EnvironmentFile= również nie jest skryptem powłoki: export nie powinno się w nim znajdować, zasady cytowania różnią się od bash, a brak pliku jest błędem krytycznym, chyba że poprzedzisz ścieżkę znakiem -.
Praca na działającym serwerze
Należy przeanalizować kod, potwierdzić przyczynę, wprowadzić jedną zmianę i zrestartować usługę. Ta kolejność jest ważniejsza niż znajomość wszystkich parametrów, ponieważ zapobiega nakładaniu na siebie trzech spekulatywnych poprawek, co uniemożliwia ustalenie, która z nich zadziałała. Ta sama ścieżka postępowania sprawdza się w przypadku jednostek, których nie jesteś autorem. Timer, który nigdy się nie uruchamia, oznacza usługę, która nigdy nie wystartowała, dlatego w pierwszej kolejności należy debugować usługę: systemd timer i wyzwalana przez niego usługa zawodzi dokładnie w opisany powyżej sposób, a timer ukrywa dane wyjściowe, dopóki nie zażądasz ich z dziennika systemd.
FAQ
Co oznacza status=203/EXEC w systemctl status?
systemd przygotował wszystko zgodnie z wymaganiami jednostki, ale wywołanie execve() zakończyło się niepowodzeniem, więc program nigdy nie został uruchomiony. Należy sprawdzić cztery elementy w podanej kolejności: ścieżka w ExecStart= istnieje i jest ścieżką bezwzględną, plik posiada uprawnienia do wykonywania, shebang wskazuje na interpreter dostępny w zmiennej PATH usługi oraz plik używa zakończeń linii w formacie Unix. file zgłasza "with CRLF line terminators" w przypadku ostatniego punktu, co zmienia nazwę interpretera na /bin/bash\r i powoduje odrzucenie przez jądro systemu.
Dlaczego usługa uruchamia się i natychmiast zatrzymuje?
Plik jednostki zakłada zachowanie, którego program nie posiada. Przy Type=simple systemd oczekuje, że program pozostanie na pierwszym planie, więc demon, który tworzy proces potomny w tle, wygląda na zakończony w momencie wykonania operacji fork. Przy Type=forking systemd czeka na zakończenie pierwszego procesu, więc program działający na pierwszym planie powoduje zawieszenie zadania startowego do momentu upłynięcia czasu TimeoutStartSec=. Należy dopasować Type= do programu, a jeśli program oferuje flagę uruchomienia na pierwszym planie, należy jej użyć przy domyślnym Type=simple.
Jak zobaczyć rzeczywisty błąd zamiast krótkiego komunikatu statusu?
systemctl status wyświetla tylko kilka ostatnich linii dziennika i skraca długie wpisy. Należy uruchomić journalctl -u myapp.service -b --no-pager, aby uzyskać wszystko, co jednostka zarejestrowała podczas bieżącego rozruchu, dodać -n 200 dla większego zakresu lub przekierować wynik do grep. Jeśli aplikacja zapisuje własny plik dziennika, należy również go sprawdzić, ponieważ systemd przechwytuje tylko to, co program wysyła na standardowe wyjście i standardowe wyjście błędów.
Dlaczego jednostka jest nieaktywna bez komunikatu o błędzie?
Najczęściej przyczyną jest pominięcie jej przez dyrektywę Condition*. Te sprawdzenia są ciche: niespełniony warunek oznacza, że zadanie startowe kończy się sukcesem. Należy uruchomić systemctl show myapp.service -p ConditionResult i wyszukać ConditionResult=no, a następnie przeczytać linię dziennika, która wskazuje na konkretne sprawdzenie. Inną częstą przyczyną jest zamaskowana jednostka, która odmawia uruchomienia do momentu, aż sudo systemctl unmask usunie blokadę.
Czy po każdej zmianie pliku jednostki wymagane jest daemon-reload?
Tak, w przypadku każdej edycji pliku jednostki lub pliku typu drop-in. sudo systemctl daemon-reload powoduje, że systemd ponownie odczytuje pliki z dysku, a następnie sudo systemctl restart myapp.service stosuje zmiany do działającej usługi. Nie jest to wymagane po systemctl edit, który wykonuje przeładowanie automatycznie, ani po zmianie pliku konfiguracyjnego należącego do aplikacji, a nie do systemd.