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 и журнал.

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте