Автообновление Linux

У одного и того же механизма — пакетных автообновлений — два типичных артефакта в эксплуатации.

На одиночном сервере — сервис «моргает» утрами. По журналу видно, что в 06:xx или около того он был остановлен и сразу же запущен, причиной никто конкретно не назначен, и админ месяцами ищет «плавающий ночной даунтайм», списывая его то на сеть, то на GC.

На кластере из трёх–пяти узлов — кластер устроен правильно с точки зрения избыточности, переживает падение одного узла, и в какой-то момент сам себя роняет: на всех узлах в одно и то же утро прилетело обновление с перезапуском сервиса. Кворума не осталось, клиент видит «недоступно».

Источник у обоих один. Разберём.

Откуда вообще приходят автообновления

Debian / Ubuntu — unattended-upgrades

В Ubuntu 24.04 и Debian 12 пакет unattended-upgrades устанавливается по умолчанию и включён, как минимум для канала security. Раскладка такая:

  • /etc/apt/apt.conf.d/20auto-upgrades — общий рубильник: включено ли вообще автоматическое обновление и нужно ли каждый день делать apt update.
  • /etc/apt/apt.conf.d/50unattended-upgrades — политика: какие репозитории трогать (Origins-Pattern), какие пакеты исключить (Package-Blacklist), писать ли отчёт на почту, разрешать ли автоматическую перезагрузку хоста (Automatic-Reboot).

Сам процесс запускается двумя systemd-таймерами:

  • apt-daily.timer — обновляет индекс пакетов и скачивает то, что разрешено. Срабатывает дважды в сутки, со случайной задержкой до 12 часов.
  • apt-daily-upgrade.timer — собственно ставит обновления. По умолчанию OnCalendar=*-*-* 6:00 с RandomizedDelaySec=60m.

Здесь уже видна потенциальная ловушка: окно установки — один час после шести утра, плюс-минус секундное распределение. У вас три одинаково настроенных узла — все три попали в это окно. Если для двух из них таймер случайно выпал в один и тот же 10-минутный интервал — обновление пакета с вашим сервисом запустилось на обоих почти синхронно.

Fedora и RedOS — dnf-automatic

В семействе RPM есть свой аналог: пакет dnf-automatic. В отличие от Debian-семейства, он не установлен по умолчанию ни в Fedora, ни в RedOS — администратор включает его сознательно:

sudo dnf install dnf-automatic
sudo systemctl enable --now dnf-automatic.timer

Конфигурация — в /etc/dnf/automatic.conf. Ключевые поля:

  • download_updates — скачивать ли заранее.
  • apply_updates — устанавливать или только скачать (по умолчанию no — то есть «только скачать», но это часто меняют).
  • upgrade_typedefault (всё) или security (только security-канал).

Триггер — таймер dnf-automatic.timer. По умолчанию OnCalendar=*-*-* 6:00 с RandomizedDelaySec=1h. Та же узкая одночасовая полоса, что и в Ubuntu, та же проблема для кластера.

В RedOS Murom 7.x используется тот же dnf-automatic. Семантически ничего нового.

Что происходит при обновлении пакета

Допустим, в репозиторий вышла новая версия пакета, в нём — бинарь сервиса с systemd-юнитом. Дальше пакетный менеджер делает следующее:

  1. Распаковывает новый бинарь поверх старого.

  2. Запускает скриптлеты пакета (postinst в .deb, %post / %posttrans в .rpm). Посмотреть их можно через dpkg -e или rpm -q --scripts <pkg>.

  3. В типовом случае postinstall дёргает systemctl daemon-reload, а дальше — systemctl try-restart <service> или restart, если юнит был запущен. Точная команда зависит от того, как пакет собрали: dh_installsystemd в Debian-семействе по умолчанию делает перезапуск; в RPM это решение мейнтейнера.

  4. В Ubuntu 22.04+ поверх этого работает ещё needrestart: он сканирует все процессы в системе и через /proc/PID/maps находит те, у которых обновлённый (а на самом деле — уже удалённый из файловой системы и заменённый новым) исполняемый файл или библиотека остались примонтированными в адресное пространство. Такие процессы он помечает к перезапуску.

    Важная деталь: перезапуск касается не только сервиса, чей пакет обновился. Если в очередное unattended-upgrade попал security-фикс на популярную системную библиотеку — libssl3, libcurl4, libsystemd0, libc6, zlib1g — за ней перезапустится всё, что с ней динамически слинковано: nginx, postgresql, sshd, redis, ваши собственные демоны. На практике это самая частая причина «кластер падает раз в две недели по непонятной причине»: обновляется не сам сервис, а маленький пакет в его транзитивных зависимостях.

    Посмотреть, кто что держит:

    sudo needrestart -b           # сводный список того,
                                  # что собирается перезапустить
    sudo lsof | grep '(deleted)'  # процессы с уже удалёнными
                                  # файлами в адресном пространстве
    

    Поведение needrestart в неинтерактивном режиме (то есть в unattended-upgrades) задаётся в /etc/needrestart/needrestart.conf ключом $nrconf{restart}:

    • 'a' — перезапускать всё автоматически (типовая настройка в Ubuntu Server по умолчанию);
    • 'i' — спрашивать у пользователя (на сервере без TTY означает «не перезапускать»);
    • 'l' — только записать список в журнал, ничего не трогать.

    Заодно полезно знать: статически слинкованные бинари (Go с CGO_ENABLED=0, Rust с musl-target, нативные с -static) от обновления системных библиотек не страдают — у их процессов в /proc/PID/maps нет внешних .so, и needrestart их не трогает.

Итог: внутри одного apt upgrade или dnf upgrade ваш сервис — и не один он — действительно может быть перезапущен. Без вашего вмешательства, без уведомления и одновременно с обновлением на других узлах того же кластера.

Артефакт 1: одиночный сервер моргает по утрам

Одиночный сервер. На нём крутится сервис: веб-приложение, прокси, база, брокер очередей — не важно. Жалоба от пользователей или из мониторинга: «каждое утро около шести сервис недоступен 10–30 секунд». В течение дня — никаких проблем. Утром — повторяется.

Это не утечка памяти, не GC-пауза, не сетевой провал и не зависание после первой большой нагрузки. Скорее всего это автообновление какого-то пакета, в чьих транзитивных зависимостях оказался ваш сервис или одна из его системных библиотек.

Проверить за две минуты:

# Что вообще запускалось ночью?
sudo journalctl -u apt-daily-upgrade.service --since "7 days ago"
sudo journalctl -u dnf-automatic.service --since "7 days ago"

# Что обновлялось — список пакетов с временем
sudo tail -n 200 /var/log/apt/history.log
sudo tail -n 200 /var/log/dnf.log

# Перезапускался ли ваш сервис в эти же моменты?
sudo journalctl -u <ваш-сервис>.service --since "7 days ago" \
    | grep -E "Stopping|Started|Stopped"

Если временные метки совпадают — диагноз подтверждён. Дальше варианты:

  • Принять. Если сессии переподключаются автоматически и пользователь это не замечает — пусть моргает; зато безопасность накатывается сама.
  • Разнести по времени (см. ниже), чтобы окно обновления было не во время бизнес-часов и не в одинаковое время каждый день.
  • Запретить трогать конкретный пакет: добавить его в Package-Blacklist в /etc/apt/apt.conf.d/50unattended-upgrades или в exclude= в /etc/dnf/automatic.conf. Сервис не будет обновляться вообще, перезапусков не будет — но и security-фиксов не будет; принимаемое решение.
  • Запретить автоматический перезапуск сервисов через needrestart в режиме «только список» (см. ниже) — обновлять пакеты можно, но рестарт делает админ или CI.

Артефакт 2: кластер теряет большинство по совпадению окон

Простой счёт. Допустим:

  • Три узла кластера, одинаково настроенные.
  • Стандартный таймер apt-daily-upgrade.timer с RandomizedDelaySec=60m. Распределение равномерное по 60-минутному окну.
  • Обновление пакета сервиса занимает 20 секунд, перезапуск — ещё 5–10.

Вероятность того, что два узла из трёх попадут в окно перекрытия в 30 секунд — порядка 1,5–2 % на одно ночное обновление. На дистанции в год это срабатывает примерно раз. На пяти узлах — уже заметно чаще; и достаточно одного раза, чтобы кластер на короткое время потерял большинство и оборвал текущие сессии.

Если ваше приложение допускает потерю одного узла, но не двух одновременно (типовая ситуация для систем с кворумом — Raft, Paxos, большинство брокеров с N+1 резервом), это проблема обнаружения: вы её увидите как редкие, плохо воспроизводимые «ночные провалы» доступности.

Как разнести во времени

Идея простая: расписание автообновления не должно быть одинаковым на всех узлах. Дальше вопрос — насколько системно вы это разносите.

Минимум: разный OnCalendar на каждом узле

На каждом узле кластера переопределяете таймер локальным drop-in:

sudo systemctl edit apt-daily-upgrade.timer

Содержимое drop-in для первого узла:

[Timer]
OnCalendar=
OnCalendar=*-*-* 03:00
RandomizedDelaySec=10m

Для второго:

[Timer]
OnCalendar=
OnCalendar=*-*-* 05:00
RandomizedDelaySec=10m

И так далее. Пустая OnCalendar= обязательна — она сбрасывает значение из юнита, заданное по умолчанию, иначе расписаний будет два. Применить:

sudo systemctl daemon-reload
sudo systemctl restart apt-daily-upgrade.timer
sudo systemctl list-timers --all | grep apt-daily-upgrade

Для dnf-automatic.timer всё то же самое, имя только другое: systemctl edit dnf-automatic.timer.

Получше: убрать автоматический перезапуск сервиса

Если вы не хотите, чтобы пакетное обновление трогало ваш сервис в принципе — даже если бинарь обновился — можно настроить needrestart в режиме «только список, без действий»:

sudo $EDITOR /etc/needrestart/needrestart.conf
# $nrconf{restart} = 'l';   # list only

В режиме l он только запишет в журнал, какие сервисы следовало бы перезапустить — и оставит решение администратору.

Для Debian/Ubuntu без needrestart можно временно блокировать запуск сервисов через policy-rc.d:

sudo install -m 0755 /dev/stdin /usr/sbin/policy-rc.d <<'EOF'
#!/bin/sh
exit 101
EOF

exit 101 означает «действие не разрешено» — dh_installsystemd это понимает и перезапуск не выполняет. Сервис при этом продолжит работать со старым бинарём, пока вы не перезапустите его вручную. На системах с настроенными Linux capabilities и долго живущими подключениями это часто как раз то, что нужно — перезапуск делает дежурный или CI.

Серьёзно: координация через внешний инструмент

Когда узлов много или они в нескольких регионах — разные OnCalendar становятся хрупкими (легко забыть один при добавлении). Тогда нужна координация уровнем выше пакетного менеджера:

  • Ansible с serial: 1 (или serial: "30%") — обновляет кластер по одному, с проверкой работоспособности между шагами;
  • Kubernetes через PodDisruptionBudget и rolling-update — гарантирует, что одновременно недоступно не более N узлов;
  • Salt orchestrate / Chef runlist — то же.

Автообновление пакета при этом обычно отключают совсем:

# Debian/Ubuntu
sudo systemctl disable --now apt-daily-upgrade.timer
# Fedora/RedOS
sudo systemctl disable --now dnf-automatic.timer

И полагаются на оркестратор, у которого есть карта кластера и понимание кворума.

На первый взгляд кажется лишней суетой ради «всё равно когда обновится» — но цена разъехавшегося NAS, ушедшего в split-brain посреди ночи, обычно сильно выше, чем час, разово потраченный на правильный конфиг расписания и перезапусков.