Socket activation — механизм systemd, при котором сокет создаёт и держит systemd, а сервис запускается только при первом подключении. Для домашнего сервера и небольшого офиса это способ оставлять десятки служб выключенными, экономить память и не запускать процессы, к которым никто не обращается. Разберём устройство, рабочий пример на Python, режим Accept=yes, диагностику и ошибки, из-за которых сервис не стартует.
Что именно делает systemd
Обычный сервис сам вызывает bind() и listen(), поэтому должен работать постоянно. В схеме socket activation юнит с расширением .socket заранее создаёт сокет, а сервисный юнит получает его уже готовым. systemd слушает порт, держит очередь подключений и при первом входящем соединении запускает процесс. Пока соединений нет, процесса нет: память не занята, но порт открыт и подключение не теряется.
Ключевой момент: в системных юнитах сокет принадлежит systemd (PID 1), а не приложению. Перезапуск или падение сервиса не закрывает порт, поэтому клиент не получает «connection refused» в момент перезапуска. Это же свойство используют docker.socket, cups.socket и ssh.socket в дистрибутивах, где sshd переведён на активацию по требованию.
- Accept=no — сервис получает слушающий сокет и сам принимает соединения; подходит демонам, умеющим работать с переданным дескриптором.
- Accept=yes — systemd принимает каждое соединение и запускает отдельный экземпляр шаблонного юнита
foo@.service; аналог inetd. - Дескрипторы передаются как fd 3 и далее; приложение узнаёт о них через
LISTEN_FDS,LISTEN_PIDиsd_listen_fds().
Первый сервис: ответ по требованию
Сделаем минимальный сервер, который принимает одно соединение, отдаёт HTTP-ответ и завершается. Слушающий сокет создаст systemd, поэтому в коде нет bind() и listen().
#!/usr/bin/env python3
import socket
with socket.socket(fileno=3) as listener:
conn, _ = listener.accept()
with conn:
conn.sendall(
b"HTTP/1.1 200 OK\r\n"
b"Content-Length: 13\r\n"
b"Connection: close\r\n\r\n"
b"hello, socket"
)
Сохраните файл как /usr/local/bin/hello-socket.py, выдайте права на запуск и создайте два юнита. Обратите внимание на Requires=hello.socket и After=hello.socket: сервис должен запускаться после того, как сокет уже существует.
sudo install -m 0755 hello-socket.py /usr/local/bin/hello-socket.py
# /etc/systemd/system/hello.socket
[Unit]
Description=Hello socket
[Socket]
ListenStream=127.0.0.1:8080
SocketMode=0660
[Install]
WantedBy=sockets.target
# /etc/systemd/system/hello.service
[Unit]
Description=Hello service
Requires=hello.socket
After=hello.socket
[Service]
Type=exec
ExecStart=/usr/local/bin/hello-socket.py
User=nobody
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
Включайте именно сокет, а не сервис: systemctl enable --now hello.socket. Если включить hello.service, он запустится при загрузке и схема потеряет смысл. После правки юнитов выполните systemctl daemon-reload.
sudo systemctl daemon-reload
sudo systemctl enable --now hello.socket
systemctl status hello.socket --no-pager
systemd-analyze verify hello.socket hello.service
Проверка активации
До первого запроса hello.service неактивен, а порт уже слушается. Убедимся в этом и отправим запрос.
systemctl is-active hello.service
ss -ltnp 'sport = :8080'
curl -v http://127.0.0.1:8080/
systemctl status hello.service --no-pager
journalctl -u hello.service -n 20 --no-pager
В выводе ss владельцем сокета будет systemd с PID 1, а не Python. После curl сервис отработает один запрос и снова станет inactive — это нормально, сокет остался у systemd. Счётчики подключений видны в свойствах юнита:
systemctl show hello.socket -p NConnections -p NAccepted
systemctl list-sockets --all | grep hello
systemctl reset-failed hello.socket
NConnections показывает текущие соединения, NAccepted — сколько всего принято. Если сокет попал под ограничитель частоты запусков, он переходит в failed; reset-failed возвращает его в строй. Причину смотрите в journalctl -u hello.socket.
Режим Accept=yes
Если приложение не умеет принимать готовый дескриптор, используйте посессионный запуск. systemd сам принимает соединение и подключает его к stdin/stdout процесса. Шаблонный юнит обозначается символом @, а в сокете указывается Accept=yes.
# /etc/systemd/system/echo@.service
[Unit]
Description=Echo connection
[Service]
ExecStart=/bin/cat
StandardInput=socket
StandardOutput=socket
StandardError=journal
# /etc/systemd/system/echo.socket
[Unit]
Description=Echo socket
[Socket]
ListenStream=127.0.0.1:9099
Accept=yes
[Install]
WantedBy=sockets.target
Каждое подключение получает свой экземпляр echo@<номер>.service, поэтому состояние между клиентами не разделяется. Это удобно для простых обработчиков, но плохо для демонов с общим кэшем. Учитывайте и цену запуска процесса на каждое соединение: для высоконагруженного HTTP это лишние миллисекунды.
Отладка без установки юнита
Проверить, поддерживает ли программа переданный сокет, можно утилитой systemd-socket-activate. Она создаёт сокет и запускает команду при подключении — удобно, когда юнит ещё не написан. Запустите её в одном терминале, а запрос отправьте из другого.
systemd-socket-activate -l 127.0.0.1:8081 /usr/local/bin/hello-socket.py
curl -v http://127.0.0.1:8081/
Без флага --accept дескриптор передаётся как fd 3; с ним утилита работает как inetd и подключает сокет к stdin/stdout. Если команда работает под утилитой, но не под systemd, сравните окружение и путь к исполняемому файлу.
Типичные ошибки
- Сервис сам вызывает bind() — получает
Address already in use. Демону нужен режим работы с переданным fd; проверьте--helpи документацию. - Включены и сокет, и сервис — сервис стартует при загрузке и держит порт сам. Оставьте
WantedBy=sockets.targetтолько у сокета. - Нет
Requires=hello.socket— при ручном запуске сервиса сокета может ещё не быть. Связка делает порядок предсказуемым. - Сокет слушает не тот адрес —
ListenStream=0.0.0.0:8080открыт наружу. Для локальных служб указывайте127.0.0.1или Unix-сокет в/run. - Права на Unix-сокет — по умолчанию доступ зависит от umask; задайте
SocketMode=0660,SocketUserиSocketGroup. - Забыли daemon-reload — systemd продолжает использовать старую версию юнита.
Когда это действительно полезно
Socket activation хорошо подходит для редко используемых служб: веб-интерфейса домашнего сервера, принт-сервера, API для автоматизации, сокета Docker. Экономия памяти заметна на слабом железе и в контейнерах, где каждый процесс на счету. Но для постоянно нагруженного сервиса выгоды нет: процесс всё равно работает, а дополнительный слой добавляет задержку первого ответа. Не используйте схему и как замену очереди задач: сокет не хранит запросы после перезагрузки системы.
Проверка на практике занимает минуту: остановите сервис, оставьте сокет, подключитесь клиентом и посмотрите systemctl status и journalctl. Если процесс поднялся ровно на соединение и завершился — схема работает.
Итог
Socket activation переносит владение портом в systemd и запускает сервис по требованию. Минимальная конфигурация — пара юнитов, Accept=no и приложение, умеющее принять fd. Для несовместимых программ есть Accept=yes с шаблоном @.service. Главное — включать сокет, не дублировать bind() в сервисе и проверять состояние через systemctl show, ss и журнал.