SSD Nodes Learn 🎉 VPS от $5.50/мес
Руководства Matt ConnorАвтор: Matt Connor

Почему не запускается юнит systemd: коды ошибок

Изучите вывод systemctl status для диагностики службы. Узнайте, что значат коды 203/EXEC и 226/NAMESPACE и почему юнит может завершаться сразу после успешного старта процесса.

Почему юнит systemd не запускается

Если юнит systemd не запускается, причина указана в одном из полей. Выполните systemctl status <unit> и найдите code= и status= в строке, сообщающей об ошибке. Код статуса в диапазоне 200 означает, что systemd не дошел до запуска вашей программы: произошел сбой при подготовке окружения, описанного в файле юнита. Статус ниже 200 означает, что программа была запущена и завершилась самостоятельно, поэтому файл юнита, скорее всего, корректен, а проблема заключается в самом приложении.

Это разделение является отправной точкой для диагностики. Все последующие действия зависят от этого выбора и выполняются в порядке возрастания номеров.

Какие три команды отвечают на этот вопрос, в указанном порядке

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

systemctl status выносит вердикт. Сначала прочитайте строку Loaded:, так как она указывает файл, который systemd фактически проанализировал, и сообщает, включен ли юнит, замаскирован или не найден вовсе. Затем прочитайте строку Active: и пару code= и status= под ней.

journalctl -u myapp.service -b --no-pager предоставляет подробности. -u фильтрует вывод по конкретному юниту, -b ограничивает вывод текущей загрузкой, чтобы вы не изучали сбой недельной давности, а --no-pager выводит данные прямо в терминал, чтобы их можно было передать в grep. status показывает только последние несколько строк лога и обрезает длинные строки. Журнал показывает всё, что программа вывела перед завершением, что обычно и является истинной причиной ошибки. Добавьте -n 100 для просмотра более ранней истории или запустите команду с -f во втором терминале во время перезапуска юнита.

systemd-analyze verify загружает файл юнита без его запуска. Команда предупреждает о неизвестных секциях и директивах, а также помечает команды в ExecStart=, которые не может выполнить. Это позволяет выявить два скрытых типа ошибок: опечатку в ключе, которую systemd игнорирует при загрузке с предупреждением, которое большинство пользователей не читает, и несуществующий путь.

После редактирования любого файла юнита выполните sudo systemctl daemon-reload. Пока вы этого не сделаете, systemd продолжит использовать копию, загруженную ранее, а systemctl status добавит предупреждение о том, что файл на диске изменился. Исправление, которое «ничего не дало», часто оказывается исправлением, которое systemd еще не считал.

Еще две команды заслуживают внимания. systemctl cat myapp.service выводит итоговую конфигурацию юнита, то есть основной файл плюс все дополнения из /etc/systemd/system/myapp.service.d/. systemctl show myapp.service -p ExecStart -p User -p WorkingDirectory выводит эти значения в том виде, в котором их распознал systemd, что и будет фактически исполнено.

Что означает статус 203/EXEC?

203/EXEC означает, что systemd завершил подготовку, вызвал execve(), но ядро ответило отказом. Ваша программа не выполнила ни одной строки собственного кода. Почти во всех случаях причина кроется в одном из четырех пунктов.

  1. Путь в ExecStart= указан неверно или не является абсолютным. Проверьте его с помощью ls -l, сравнив с точной строкой в файле юнита.
  2. У файла отсутствует бит исполнения. sudo chmod +x /opt/myapp/run.sh исправляет это. Файлы, распакованные из архива или скопированные с другой машины, часто теряют этот атрибут.
  3. Ошибка в shebang-строке. Ядро считывает первую строку скрипта и запускает указанный там интерпретатор, поэтому #!/usr/bin/env python3 завершается с ошибкой, если в PATH сервиса нет python3, а файл, сохраненный с переносами строк в стиле Windows, запрашивает интерпретатор /bin/bash\r, которого не существует.
  4. Файл не предназначен для запуска на этой машине: неверная архитектура или текстовый файл без shebang-строки.

Воспроизведите запуск вручную от имени пользователя сервиса, прежде чем вносить какие-либо изменения.

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

file указывает архитектуру и сообщает "with CRLF line terminators", если проблема заключается в символах переноса строки. cat -A показывает то же самое в виде завершающего ^M. Удалите их с помощью sed -i 's/\r$//' /opt/myapp/run.sh.

Важное примечание относительно диапазона: коды от 200 и выше — это соглашение, а не гарантия. Ваша программа может завершиться с кодом 203, и systemd не сможет отличить этот случай от ошибки запуска. systemd-analyze exit-status 203 выводит имя и класс любого кода, что помогает при чтении таблицы, но если ваше приложение использует коды завершения выше 199, измените их.

Почему я получаю ошибку 217/USER или 216/GROUP?

217/USER означает, что учетная запись, указанная в User=, не существует в момент запуска службы. 216/GROUP — это аналогичная ошибка для Group= или SupplementaryGroups=. Проверьте это с помощью соответствующих команд.

getent passwd appuser
getent group appgroup

Каждая из них либо выводит строку, либо ничего не выводит и возвращает ненулевой код завершения. Отсутствие вывода означает, что имя неизвестно системе, поэтому systemd не может переключиться на него и останавливается до выполнения exec. Решение заключается в создании учетной записи, а не в удалении User=root. Запуск службы от имени выделенной системной учетной записи с минимальными привилегиями — это основная цель данной директивы.

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

DynamicUser=yes позволяет обойти эту проблему: systemd выделяет временную учетную запись при каждом запуске. Это подходит для службы, которая не хранит состояние. Для всего, что записывает файлы, необходимо использовать StateDirectory=, так как идентификатор пользователя меняется между запусками, и файлы в обычном каталоге остаются во владении учетной записи, которая больше не существует.

Что такое 226/NAMESPACE?

226/NAMESPACE возникает из-за директив песочницы (sandboxing). Когда юнит устанавливает ProtectSystem=, ProtectHome=, PrivateTmp=, ReadWritePaths= или что-то подобное, systemd создает приватное пространство имен монтирования (mount namespace) для этого сервиса перед выполнением программы. Пространство имен здесь — это изолированное представление файловой системы для одного процесса. Если любое монтирование в этом плане завершается неудачей, запуск прерывается с кодом 226, и ваша программа не запускается.

Обычно причина заключается в пути в ReadWritePaths=, который не существует. ProtectSystem=strict монтирует всю файловую систему в режиме только для чтения, а ReadWritePaths= заново открывает указанные пути для записи. systemd не может заново открыть каталог, которого нет. Есть два правильных способа решения. Позвольте systemd создать каталог с помощью StateDirectory=, что создает /var/lib/<name> при каждом запуске и передает его пользователю сервиса, или добавьте префикс - к пути, который указывает systemd игнорировать эту запись, если источник отсутствует. Плохой способ — удалить настройки безопасности, что меняет пятиминутную проблему на постоянную уязвимость.

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

Если вы не можете определить, какая строка вызывает проблему, удалите весь блок настроек безопасности, выполните перезагрузку конфигурации и запустите сервис. Если сервис запустился, добавляйте строки по одной и перезапускайте сервис после каждой. Два близких параметра в этой группе — 233/RUNTIME_DIRECTORY и 238/STATE_DIRECTORY. Они означают, что systemd не смог создать каталог или получить права владения на него, указанный в RuntimeDirectory= или StateDirectory=, обычно из-за того, что этот путь уже существует и принадлежит другому пользователю.

Почему возникает ошибка 200/CHDIR, если параметр WorkingDirectory указан верно?

200/CHDIR означает, что chdir() в WorkingDirectory= завершился неудачей. Директория отсутствует или у пользователя, от имени которого запущен сервис, нет прав на вход в неё. Для входа в директорию требуются права на выполнение (execute) для самой директории и для всех родительских каталогов в пути. Поэтому даже если /home/deploy/app доступна для чтения, она может быть недоступна, если /home/deploy имеет права 700, а сервис запущен от имени appuser.

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

namei -l выводит владельца и права доступа для каждого компонента пути. Это самый быстрый способ найти директорию, которая блокирует доступ к остальным. Указание WorkingDirectory=-/srv/myapp делает отсутствие директории некритичной ошибкой. Это подходит для программ, которым не важно, из какой директории они запущены, но может привести к ошибкам, если программа открывает файлы по относительным путям.

Почему служба запускается и останавливается через секунду?

Здесь нет кодов состояния серии 200, и часто вообще отсутствует текст ошибки. Юнит показывает inactive (dead) сразу после запуска или циклически переходит в activating (auto-restart). systemd корректно подготовил окружение. Несоответствие возникает между тем, что делает ваша программа, и тем, что обещала конфигурация Type=.

Type=simple, значение по умолчанию, подразумевает, что программа остается в основном процессе (foreground). Если передать ей демон, который делает fork в фоновый режим и завершается, systemd увидит завершение основного процесса и посчитает службу выполненной. У большинства демонов есть флаг для работы в основном процессе, например nginx -g 'daemon off;'.

Type=forking означает, что первый процесс завершается, как только его дочерний процесс готов. Если использовать здесь программу, работающую в основном процессе, задача запуска будет ждать, пока не истечет TimeoutStartSec= (по умолчанию 90 секунд), после чего systemd убьет её и запишет в лог ошибку тайм-аута.

Type=notify означает, что программа вызывает sd_notify() для уведомления о готовности. Программа без такой поддержки ничего не сообщает, поэтому запуск завершается по тайм-ауту, а журнал фиксирует результат как сбой протокола.

Выберите тип в соответствии с тем, как программа работает на самом деле. В чем разница между simple, forking, oneshot и notify — это решение, которое устраняет весь этот класс ошибок.

Когда служба завершается снова и снова, systemd прекращает попытки и сообщает, что запрос на запуск повторяется слишком быстро. После этого юнит остается в состоянии сбоя, пока не пройдет окно ограничения частоты или вы не выполните sudo systemctl reset-failed myapp.service. Увеличение лимита лишь скрывает симптом. Читайте журнал начиная с первого сбоя, а не с последнего, и ознакомьтесь с тем, что на самом деле перезапускает Restart=on-failure, прежде чем менять этот параметр.

Почему юнит неактивен, хотя ошибок нет?

Юнит может быть пропущен вместо запуска. Директивы Condition* работают без вывода сообщений: если проверка не проходит, systemd помечает задачу как выполненную успешно и ничего не делает. Юнит с ConditionPathExists=/etc/myapp/config.yml никогда не запустится, пока указанный файл отсутствует, и при этом не сообщит об ошибке.

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

Команда ConditionResult=no подтверждает пропуск, а журнал (journal) указывает на проверку, которая не была пройдена. Используйте директиву Assert*, если отсутствие необходимого компонента должно приводить к явной ошибке. В разделе Условия, утверждения и порядок запуска юнитов описано, какие проверки и где следует применять.

Существует еще несколько ситуаций, когда система молчит. Ошибка "could not be found" обычно означает, что файл находится в неверном каталоге или вы не выполнили перезагрузку конфигурации: созданные вами файлы юнитов должны находиться в /etc/systemd/system/. Замаскированный (masked) юнит не запустится, пока команда sudo systemctl unmask myapp.service не снимет маску. Кроме того, systemctl enable завершается ошибкой для юнита без секции [Install], поэтому добавьте в него WantedBy=multi-user.target.

Что делать, если процесс был принудительно завершен, а не упал с ошибкой?

code=killed — это не то же самое, что code=exited. Кто-то или что-то завершило процесс извне. status=9/KILL указывает на OOM (out of memory) killer, а журнал (journal) называет процесс, который был выбран в качестве жертвы. Ограничение, установленное вами самостоятельно, приводит к тому же результату внутри cgroup (control group), поэтому проверьте объем свободной памяти на хосте с помощью free -m и проверьте юнит на наличие параметра MemoryMax=. В MemoryMax, CPUQuota и другие лимиты cgroup объясняется, какой лимит приводит к завершению процесса, а какой — только к его замедлению.

status=15/TERM сразу после попытки запуска обычно означает, что systemd превысил время ожидания запуска и принудительно завершил процесс, что возвращает вас к Type=.

Две привычки, предотвращающие большинство таких сбоев

Используйте абсолютные пути везде. systemd не запускает вашу оболочку входа, поэтому .bashrc, .profile и активированное виртуальное окружение отсутствуют. $PATH для системной службы — это короткий встроенный список, который не содержит /opt или прослойки менеджера версий языка. Пишите /usr/bin/python3 или /opt/myapp/venv/bin/python полностью. command -v myapp в вашей оболочке выведет путь для вставки. Это же правило распространяется на WorkingDirectory=, EnvironmentFile= и любой путь в ReadWritePaths=.

ExecStart= — это не оболочка. systemd разбивает строку на слова и вызывает execve() самостоятельно. Каналы (pipes), перенаправления, шаблоны (globs), &&, обратные кавычки и ~ не имеют значения: они передаются вашей программе как буквальные аргументы. ExecStart=/usr/bin/myapp --flag > /tmp/out.log передает > и /tmp/out.log в myapp, который затем завершается с ошибкой использования, не имеющей ничего общего с проблемами systemd. Когда вам нужны возможности оболочки, явно вызывайте оболочку.

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

Для вывода данных это не требуется. Вывод службы по умолчанию попадает в журнал, а StandardOutput=append:/var/log/myapp.log записывает данные в файл без участия оболочки.

Раскрытие переменных ограничено таким же образом. $MYVAR и ${MYVAR} заменяются из Environment= и EnvironmentFile=, остальное не раскрывается. $HOME не задана для системной службы, если вы не установите её самостоятельно. EnvironmentFile= также не является скриптом оболочки: export там не к месту, правила кавычек отличаются от bash, а отсутствие файла приводит к критической ошибке, если вы не добавите префикс - к пути.

Работа с сервером в режиме реального времени

Изучите код, подтвердите причину, внесите одно изменение, перезапустите. Этот порядок важнее, чем знание всех параметров, так как он предотвращает наслоение трех гипотетических правок, из-за которых невозможно отследить, какая именно помогла. Тот же путь применим к юнитам, которые писали не вы. Таймер, который никогда не срабатывает, означает, что сервис не был запущен, поэтому сначала отлаживайте сервис: systemd-таймер и запускаемый им сервис дает сбои именно так, как описано выше, при этом таймер скрывает вывод, пока вы не запросите его через journal.

FAQ

Что означает статус status=203/EXEC в systemctl status?

systemd выполнил все требования юнита, но вызов execve() завершился неудачей, поэтому программа не запустилась. Проверьте четыре пункта по порядку: путь в ExecStart= существует и является абсолютным, файл имеет права на выполнение, shebang указывает на интерпретатор, который присутствует в PATH сервиса, и файл использует символы переноса строк Unix. Команда file сообщает "with CRLF line terminators" для последнего случая, что превращает имя интерпретатора в /bin/bash\r, из-за чего ядро отклоняет запуск.

Почему мой сервис запускается и сразу останавливается?

Файл юнита предполагает поведение, которое не свойственно программе. При Type=simple systemd ожидает, что программа останется в фоновом режиме, поэтому демон, который делает fork в фон, выглядит завершившимся в момент создания процесса. При Type=forking systemd ждет завершения первого процесса, поэтому программа, работающая в foreground, заставляет задачу запуска зависнуть до истечения TimeoutStartSec=. Сопоставьте Type= с программой, и если программа предлагает флаг для работы в foreground, используйте его вместе со значением по умолчанию Type=simple.

Как увидеть реальную ошибку вместо краткого вывода статуса?

systemctl status выводит только последние несколько строк журнала и обрезает длинные. Запустите journalctl -u myapp.service -b --no-pager, чтобы получить всё, что юнит записал в журнал во время текущей загрузки, добавьте -n 200 для увеличения окна вывода или перенаправьте его в grep. Если приложение пишет собственный файл журнала, прочитайте и его, так как systemd захватывает только то, что программа отправляет в стандартный вывод и стандартный поток ошибок.

Почему мой юнит неактивен и не выдает сообщений об ошибках?

Чаще всего его пропустила директива Condition*. Эти проверки работают без вывода сообщений: невыполненное условие помечает задачу запуска как успешную. Запустите systemctl show myapp.service -p ConditionResult и найдите ConditionResult=no, затем прочитайте строку журнала, в которой указана проверка. Другая частая причина — маскированный юнит, который отклоняет любой запуск, пока команда sudo systemctl unmask не снимет маску.

Нужно ли выполнять daemon-reload после каждого изменения файла юнита?

Да, для любого изменения файла юнита или drop-in файла. sudo systemctl daemon-reload заставляет systemd перечитать файлы с диска, а затем sudo systemctl restart myapp.service применяет их к работающему сервису. Это не требуется после systemctl edit, который выполняет перезагрузку автоматически, и не требуется после изменения конфигурационного файла, который принадлежит приложению, а не systemd.

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