open atlas
↑ К треку
Linux: операционная система LIN · 03 · 04

Socket activation и кастомные target'ы

Socket activation позволяет systemd держать сокет открытым пока сервис стартует лениво при первом подключении — обеспечивая рестарты без простоя и более быструю загрузку. Кастомные target группируют связанные сервисы в именованную точку синхронизации.

LIN Senior ◷ 24 min
Уровень
ОсновыJuniorMiddleSenior

Ты перезапускаешь API-сервис для деплоя нового бинарника. За две секунды пока новый процесс стартует, каждое входящее соединение получает “connection refused”. Балансировщик нагрузки может повторить попытку, но мобильный клиент скорее всего покажет ошибку. В systemd есть примитив, который полностью устраняет этот разрыв: socket unit. systemd держит слушающий сокет открытым во время рестарта — ядро ставит соединения в очередь в своём backlog — и передаёт сокет новому процессу в момент его готовности. Клиент никогда не видит разрыва. Это socket activation, и он встроен в systemd без каких-либо изменений кода в твоём сервисе.

Цель

После этого урока ты сможешь объяснить как работает socket activation и почему он обеспечивает рестарты без простоя, написать парные юниты .socket и .service, описать когда использовать Accept=yes vs Accept=no, и создать кастомный .target юнит для группировки и синхронизации связанных сервисов.

1

Socket activation разделяет “держать сокет” и “запускать сервис.” systemd владеет сокетом; он запускает сервис по требованию. Ключевая идея: TCP-backlog ядра может буферизовать входящие соединения даже пока процесс ещё не слушает. systemd создаёт сокет, ядро принимает соединения в backlog, и когда приходит первое соединение, systemd запускает сервис и передаёт ему файловый дескриптор. connect() клиента завершается успешно немедленно — он лишь кратко блокируется в backlog ядра.

Socket unit и парный service unit разделяют имя (всё до расширения):

sshd.socket   → systemd держит сокет
sshd.service  → systemd запускает это при входящем соединении

Socket unit всегда включён; service unit запускается лениво.

2

Написание socket unit. Минимальная форма для TCP-сокета на порту 8080:

# /etc/systemd/system/myapi.socket
[Unit]
Description=My API socket

[Socket]
ListenStream=8080
# Accept=no (по умолчанию): один экземпляр сервиса обрабатывает все соединения
# Accept=yes: systemd форкает новый экземпляр сервиса для каждого соединения (inetd-стиль)

[Install]
WantedBy=sockets.target

ListenStream= для TCP (stream-сокеты). Для UDP используй ListenDatagram=. Для Unix domain socket используй путь: ListenStream=/run/myapi.sock.

Accept=no (по умолчанию) — почти всегда правильный выбор: systemd запускает один экземпляр сервиса, передаёт ему слушающий сокет через файловый дескриптор 3 (протокол SD_LISTEN_FDS), и сервис сам обрабатывает все соединения. Accept=yes порождает новый экземпляр сервиса для каждого соединения — полезно только для очень простых per-connection обработчиков (классическая модель inetd). Большинство современных сервисов используют Accept=no.

3

Парный сервис получает сокет через SD_LISTEN_FDS. Сервис не вызывает bind() или listen() сам — он получает уже привязанный, уже слушающий сокет от systemd через переменную окружения и унаследованные файловые дескрипторы:

# /etc/systemd/system/myapi.service
[Unit]
Description=My API service
Requires=myapi.socket
After=myapi.socket

[Service]
Type=notify
ExecStart=/opt/myapi/server
# Порт НЕ указывать здесь — сокет наследуется от myapi.socket
Restart=on-failure
User=myapi

[Install]
# Нет WantedBy здесь — socket unit подтягивает сервис
Also=myapi.socket

Директива Also= в [Install] означает “когда включаешь этот сервис, также включи myapi.socket”. Это держит пару в синхронизации.

Для рестартов без простоя: systemctl restart myapi.service останавливает и запускает сервис пока myapi.socket продолжает слушать. Ядро ставит в очередь входящие соединения во время разрыва. Новый процесс сервиса стартует, вызывает sd_listen_fds() для получения fd сокета, и соединения из очереди обслуживаются нормально.

# Включить оба вместе (Also= обрабатывает сокет)
sudo systemctl enable --now myapi.service

# Перезапустить только сервис — сокет продолжает слушать всё время
sudo systemctl restart myapi.service

# Проверить что сокет держится открытым
systemctl status myapi.socket
4

Кастомные target’ы группируют связанные сервисы и предоставляют именованную точку синхронизации. Target — просто unit-файл с расширением .target и секцией [Unit] — без секции [Service]. Его назначение — быть тем, против чего другие юниты могут объявлять After=, Requires= или WantedBy=.

Пример: target “бэкенд-стека”, группирующий базу данных, кеш и API:

# /etc/systemd/system/backend.target
[Unit]
Description=Full backend stack
Requires=postgresql.service redis.service myapi.service
After=postgresql.service redis.service myapi.service

[Install]
WantedBy=multi-user.target

Теперь можно:

# Поднять весь бэкенд-стек
sudo systemctl start backend.target

# Остановить весь стек
sudo systemctl stop backend.target

# Изолировать только этот target (останавливает всё остальное — использовать осторожно)
sudo systemctl isolate backend.target

Другие юниты могут объявлять After=backend.target чтобы гарантировать что весь стек поднят перед их стартом.

5

PartOf= заставляет сервис останавливаться при остановке владеющего target’а, без жёсткой зависимости. Это правильная директива для семантики “член группы”:

# /etc/systemd/system/myworker.service
[Unit]
Description=Background worker
PartOf=backend.target
After=backend.target

С PartOf=backend.target, если запустить systemctl stop backend.target, worker тоже останавливается. Но если worker падает сам по себе, backend.target не затрагивается. Это отличается от Requires= (которое остановило бы target при падении члена). Используй PartOf= когда юниты логически являются частью группы, но их индивидуальный сбой не должен разрушать всю группу.

# Проверить распространение остановки
sudo systemctl start backend.target
sudo systemctl stop backend.target
systemctl status myworker.service
# Active: inactive (dead) — остановлен потому что его target остановился
Разбор примера

Реализовать рестарты без простоя для Go HTTP-сервера через socket activation.

Стандартная библиотека Go поддерживает socket activation через net.FileListener:

// main.go — получает сокет от systemd вместо его привязки
package main

import (
    "net"
    "net/http"
    "os"
)

func main() {
    // SD_LISTEN_FDS: systemd передаёт сокеты начиная с fd 3
    f := os.NewFile(3, "socket")
    ln, err := net.FileListener(f)
    if err != nil {
        // Fallback: привязать свой сокет (для локальной разработки без systemd)
        ln, err = net.Listen("tcp", ":8080")
        if err != nil {
            panic(err)
        }
    }
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("ok"))
    })
    http.Serve(ln, nil)
}

Unit-файлы:

# myapp.socket
[Socket]
ListenStream=8080

[Install]
WantedBy=sockets.target
# myapp.service
[Service]
Type=simple
ExecStart=/opt/myapp/server
Restart=on-failure

Деплой нового бинарника:

# Скопировать новый бинарник
sudo cp myapp-v2 /opt/myapp/server

# Перезапустить сервис — сокет слушает всё время рестарта
sudo systemctl restart myapp.service

# Подтвердить: соединения во время рестарта были в очереди, не отклонены
curl http://localhost:8080/   # должно успешно ответить сразу после рестарта
Почему это работает

macOS launchd имеет socket activation с OS X 10.4 (2005) — это на несколько лет раньше аналога systemd. Ключ Sockets в launchd plist эквивалентен ListenStream= в .socket unit. Системные демоны macOS, например sshd, используют launchd socket activation — вот почему ssh соединяется с macOS-машиной мгновенно, хотя sshd мог не работать непрерывно. systemd принял ту же модель и стандартизировал её через переменную окружения SD_LISTEN_FDS и функцию sd_listen_fds() из libsystemd.

Частая ошибка

Не указывай ListenStream=0.0.0.0:8080 — префикс 0.0.0.0: не нужен и игнорируется в новых версиях systemd, которые могут выводить предупреждение. Просто используй ListenStream=8080 для слушания на всех интерфейсах или ListenStream=127.0.0.1:8080 для ограничения localhost. Для Unix domain socket используй абсолютный путь: ListenStream=/run/myapi/myapi.sock. Также: не забудь Also=myapi.socket в секции [Install] сервиса, иначе systemctl enable myapi.service не включит socket unit и сервис никогда не будет активирован.

Проверь себя
Викторина

Ты запускаешь `systemctl restart myapi.service` пока активен парный `myapi.socket`. Что происходит с входящими TCP-соединениями за две секунды рестарта сервиса?

Итог

Socket activation разделяет “держать сокет” (.socket unit, всегда активен) и “запускать сервис” (.service unit, запускается по требованию). systemd владеет сокетом; ядро ставит соединения в очередь в своём backlog при рестартах сервиса, обеспечивая поведение без простоя без координации на уровне приложения. Сервис получает уже привязанный сокет через fd 3 (SD_LISTEN_FDS). Accept=no (по умолчанию) — правильный выбор для сервисов с собственным циклом обработки соединений. Кастомные target’ы — именованные точки синхронизации: объявляй Requires= и After= внутри target, а другие юниты используют WantedBy= или PartOf= для присоединения. PartOf= распространяет остановку (но не сбой) от target к члену — правильная директива для членства в группе без жёсткой связи по сбоям.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 4 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources1
expand
  1. 01

Trademarks belong to their respective owners. Editorial reference only.