lenec ru

← все посты

Go: утечки горутин — как находить и предотвращать в продакшене

12K

Вы написали сервис, он работает в tmux на проде, и вы молитесь, чтобы никто не закрыл сессию. Знакомо? Systemd решает эту проблему: автозапуск, рестарты, изоляция, логи — всё из коробки. Нужен только правильный unit-файл.

Структура unit-файла: [Unit], [Service], [Install]

Unit-файл — INI-подобный конфиг в /etc/systemd/system/. Три секции:

[Unit]
Description=My Application Server
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/server --config /etc/myapp/config.yaml
ExecReload=/bin/kill -HUP $MAINPID

[Install]
WantedBy=multi-user.target

[Unit] — метаданные и зависимости. After определяет порядок запуска, Requires — жёсткую зависимость.

[Service] — как запускать, от какого пользователя, что делать при падении.

[Install] — куда подключить сервис. multi-user.target = запуск при обычной загрузке.

Type=simple vs notify vs forking

Выбор Type определяет, когда systemd считает сервис запущенным:

  • Type=simple (по умолчанию) — сервис считается готовым сразу после fork. Подходит для большинства современных приложений.
  • Type=notify — сервис сам сообщает о готовности через sd_notify("READY=1"). Лучший выбор при долгой инициализации.
  • Type=forking — для legacy-демонов, которые форкаются. Нужен PIDFile=. Избегайте для нового кода.
# Для Go/Node/Python — simple или notify
[Service]
Type=notify
NotifyAccess=main
ExecStart=/opt/myapp/bin/server

# Для legacy-демонов
[Service]
Type=forking
PIDFile=/run/myapp.pid
ExecStart=/opt/myapp/bin/daemon -d

Restart policies и watchdog

Политики перезапуска — главная причина использовать systemd вместо nohup:

[Service]
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=300
StartLimitBurst=5
WatchdogSec=30

Варианты Restart=:

  • no — не перезапускать (по умолчанию)
  • on-failure — только при ненулевом exit code или сигнале
  • always — перезапускать всегда, даже при exit 0
  • on-abnormal — при сигнале, таймауте или watchdog

StartLimitBurst=5 + StartLimitIntervalSec=300 — если сервис упал 5 раз за 5 минут, прекратить попытки. Защита от бесконечного цикла рестартов.

Для watchdog приложение должно периодически вызывать sd_notify("WATCHDOG=1"). В Go — пакет coreos/go-systemd.

Hardening: ProtectSystem, PrivateTmp, NoNewPrivileges

Systemd даёт изоляцию без контейнеров:

[Service]
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/myapp
NoNewPrivileges=true
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
SystemCallFilter=@system-service
MemoryDenyWriteExecute=true
  • ProtectSystem=strict — файловая система read-only, кроме указанных путей
  • NoNewPrivileges=true — процесс не может повысить привилегии
  • PrivateTmp=true — изолированный /tmp
  • SystemCallFilter=@system-service — белый список syscalls

Проверить уровень hardening:

systemd-analyze security myapp.service
# Overall exposure level: 2.1 SAFE

Логирование через journald

Systemd захватывает stdout/stderr в journald автоматически:

[Service]
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
# Follow в реальном времени
journalctl -u myapp.service -f

# Логи за последний час
journalctl -u myapp.service --since "1 hour ago"

# Только ошибки
journalctl -u myapp.service -p err

Реальный пример: Go-сервис с graceful shutdown

Полный unit-файл для продакшена:

[Unit]
Description=Order Processing Service
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service

[Service]
Type=notify
User=orders
Group=orders
WorkingDirectory=/opt/orders
ExecStart=/opt/orders/bin/orders-server
ExecReload=/bin/kill -HUP $MAINPID

Restart=on-failure
RestartSec=5
StartLimitBurst=5
WatchdogSec=30
TimeoutStopSec=30

ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
NoNewPrivileges=true
ReadWritePaths=/var/lib/orders
SystemCallFilter=@system-service

StandardOutput=journal
SyslogIdentifier=orders
MemoryMax=512M

[Install]
WantedBy=multi-user.target

TimeoutStopSec=30 даёт 30 секунд на graceful shutdown. В Go это signal.NotifyContext + http.Server.Shutdown().

Деплой:

cp orders.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now orders.service
systemctl status orders.service

Один unit-файл заменяет supervisor, pm2, docker-compose для простых деплоев. Сервис переживёт ребут, перезапустится при падении и будет изолирован не хуже контейнера.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.