281 день молчания: как зависший apt-get привёл меня от EOL-системы к Ubuntu 26.04 LTS
Началось всё с команды, которая должна была отработать за секунду:
Код:
apt update
Error: Could not get lock /var/lib/apt/lists/lock. It is held by process 478781 (apt-get)
Обычно это значит «подожди, кто-то уже обновляется». В этот раз — не значило.
Часть 1. Процесс, который не отпускал лок
Первым делом — не убивать наугад, а посмотреть, что это вообще такое:
Код:
ps -p 478781 -o pid,etime,cmd
PID ELAPSED CMD
478781 281-13:30:12 apt-get -qq -y update
281 день, 13 часов. Обычный `apt-get update` так не живёт никогда — это не «чуть подвис», это труп, который никто не закрыл.
Журнал systemd подтвердил диагноз и сразу показал кое-что более важное, чем сам лок:
Код:
journalctl -u apt-daily -u apt-daily-upgrade -u unattended-upgrades -n 50 --no-pager
Последняя строчка в логе — «Starting apt-daily.service» без последующего «Deactivated successfully». Это и есть тот самый зависший запуск. С тех пор systemd не запускал таймер повторно: пока предыдущий экземпляр считается живым, новый просто не стартует. То есть автообновления на этой машине молчали не «недавно», а буквально девять месяцев — и это важнее, чем сам факт залоченного apt.
Лечится без танцев с lock-файлами — файл лока это обычный flock, ядро снимает его само в момент смерти процесса:
Код:
kill 478781
sleep 3
ps -p 478781 # пусто — процесс мёртв
apt update # лок свободен
Часть 2. Один PPA, два конфликтующих источника
apt отпустило, но сразу же выдало новую порцию проблем:
Код:
Err:5 http://ppa.launchpad.net/ondrej/php/ubuntu jammy InRelease
The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 71DAEAAB4AD4CAB6 NO_PUBKEY 4F4EA0AAE5267A6C
Err:7 https://ppa.launchpadcontent.net/ondrej/php/ubuntu plucky Release
404 Not Found
Два разных провала на один и тот же PPA (sury/ondrej, под PHP). Смотрю, что реально лежит в `/etc/apt/sources.list.d/`:
Код:
grep -rl ondrej /etc/apt/sources.list.d/
/etc/apt/sources.list.d/ondrej-ubuntu-php-plucky.sources
/etc/apt/sources.list.d/php-ondrej-jammy.list
Два файла на один и тот же PPA — классическое наследие нескольких релизов Ubuntu подряд без уборки за собой:
- jammy.list — старый однострочный формат, добавлен ещё когда система была на более старом релизе. У него просто не подтянулся ключ (legacy apt-key truststore на новых apt уже не работает так, как раньше).
- plucky.sources — современный DEB822-файл, автоматически сгенерированный под текущий codename системы. 404 — потому что под этим codename ничего никогда не существовало.
Тут и выяснилась настоящая причина, а не просто «сеть подвела»: система стояла на Ubuntu 25.04 «Plucky Puffin» — а это не LTS, а интерим-релиз с девятью месяцами жизни, который закончил поддержку 15.01.2026. Sury публикует свой PPA только под текущие LTS-релизы (на тот момент — jammy/noble). Под plucky там никогда и не появится сборки — файл был мёртв с рождения, а не «отвалился со временем».
Часть 3. Тактический патч: noble вместо plucky
Раз ondrej/php живёт только на LTS — временно занимаем эту нишу под noble (24.04), это известный и рабочий обход именно для этого PPA:
Код:
mv /etc/apt/sources.list.d/php-ondrej-jammy.list /etc/apt/sources.list.d/php-ondrej-jammy.list.disabled
sed -i 's/^Suites:.*/Suites: noble/' /etc/apt/sources.list.d/ondrej-ubuntu-php-plucky.sources
apt update
apt ожил, но при попытке поставить весь набор php8.4-* зацепился за зависимость:
Код:
php8.4-zip : Depends: libzip4t64 (>= 1.7.0) but it is not installable
[no choices]
Не «не хватает версии» — пакета с таким именем в plucky нет вообще. Дело в истории переименований: в Ubuntu 24.04 (noble) при миграции на 64-битный time_t библиотеку libzip упаковали как `libzip4t64`. В plucky к моменту его сборки libzip успел дойти до версии 1.11.x с другим SONAME — пакет там называется просто `libzip5`. Это не косметика: смена SONAME с 4 на 5 означает реальный ABI-разрыв, поэтому симлинком `.so.4 → .so.5` тут не обманешь — бинарник от noble физически ищет файл с именем `.so.4`, и если его нет — он не загрузится, а не «возможно заработает».
Решение — затащить именно недостающую версию из noble, не трогая остальное:
Код:
echo "deb http://archive.ubuntu.com/ubuntu noble universe" > /etc/apt/sources.list.d/_tmp-libzip.list
apt update
apt install libzip4t64
rm /etc/apt/sources.list.d/_tmp-libzip.list
apt update
`libzip4t64` и `libzip5` спокойно живут рядом — у них разные SONAME, конфликта нет. После этого весь набор php8.4-* встал на сборки sury версии 8.4.22.
Часть 4. ProtectSystem=full — та же мина, но в этот раз заранее
С этой же сборкой php8.4-fpm (8.4.22) на основном сервере sysadmin.guru уже был отдельный инцидент: Sury добавил в юнит `ProtectSystem=full`, который монтирует `/usr` внутри namespace процесса в режиме «только чтение», и любые записи туда тихо превращаются в Read-only file system. В этот раз — проверяю заранее, а не после жалоб:
Код:
systemctl cat php8.4-fpm | grep -i protectsystem
ProtectSystem=full
Подтвердилось — это не особенность одной конкретной машины, а общая практика этой линейки сборок Sury. Дальше смотрю, куда реально пишет пул:
Код:
grep -E '^(chdir|php_admin_value\[(open_basedir|upload_tmp_dir|session\.save_path)\])' /etc/php/8.4/fpm/pool.d/*.conf
Пусто — значит сайт работает с дефолтного докрута, без явных путей в конфиге пула. По аналогии с основным сервером добавляю override:
Код:
mkdir -p /etc/systemd/system/php8.4-fpm.service.d
cat > /etc/systemd/system/php8.4-fpm.service.d/override.conf <<'EOF'
[Service]
ReadWritePaths=/usr/www
EOF
systemctl daemon-reload
systemctl restart php8.4-fpm
Важный момент по методике: «не упало в логе» — не доказательство само по себе, это может просто означать, что никто и не пытался писать на диск с момента рестарта. Доказательство — реальная запись через приложение:
Код:
journalctl -u php8.4-fpm --since "15 minutes ago" | grep -iE "read-only|permission denied"
— и сразу следом загрузка изображения через сам сайт. Прошло чисто, в логе ничего. Override на месте.
Часть 5. Почему латать дальше не имело смысла
К этому моменту stало ясно: patch-on-top больше не стратегия. Plucky мёртв с января, а его прямой потомок — 25.10 «Questing Quokka» — сам подходит к EOL примерно в июле того же года. Сидеть на интерим-релизе, который через месяц снова потребует переезда, бессмысленно.
Официальные релиз-ноуты Ubuntu прямо описывают путь: с интерим-релиза вроде 25.04 нельзя прыгнуть в следующую LTS напрямую — обязательная промежуточная остановка на следующем по очереди релизе. Для этой машины маршрут получился такой:
25.04 (plucky, EOL) → 25.10 (questing) → 26.04 LTS (resolute)
Отдельно проверил по живому community-треду: сразу после релиза 26.04 (23.04.2026) апгрейд с 25.10 на неё был временно закрыт самим инструментом до оценки релиз-командой на стабильность — открылось это окно буквально через пару недель после релиза. К моменту, когда дошли руки, прошло уже почти два месяца, так что путь должен был быть полностью открыт.
Часть 6. Сам переезд
Подготовка перед первым хопом — без неё дальше не стоило и начинать:
- tmux — обрыв ssh-сессии посреди dist-upgrade чинится не переподключением, а восстановлением системы с нуля;
- снэпшот всей машины — сделан перед стартом;
- `apt update && apt upgrade -y && apt autoremove -y` — система должна быть полностью актуальной перед апгрейдом релиза;
- `Prompt=normal` в `/etc/update-manager/release-upgrades` — с `Prompt=lts` инструмент просто не предложит 25.10, она не LTS.
Дальше — `do-release-upgrade`, и по дороге несколько гейтов, на которых стоило притормозить осознанно, а не жать Enter вслепую:
- «Foreign Packages Installed» — список из всех наших php8.4-* и php-* от sury, с рекомендацией поставить «поддерживаемые версии из архива Ubuntu». Продолжил осознанно — это и так временный noble-костыль, который планировалось менять отдельно.
- Кодировка консоли — вопрос про шрифт для локального терминала (Cyrillic/Latin/итд). Это настройка локальной консоли, по ssh её не существует физически — выбрал «23. Guess optimal character set», пусть определяет сам по локали системы.
- sshd_config — конфликт между версией пакета и локально модифицированным файлом. Оставил локальную версию: не время разбирать дифф вживую, сидя в сессии именно через этот sshd. Новая версия осталась рядом как `.dpkg-dist`, сравню отдельно, не на бегу.
- Итоговый summary перед каждым из двух хопов (remove/install/upgrade, объём скачивания) — смотрел перед подтверждением каждый раз, искал в «removed» что-нибудь неожиданное вроде nginx или mysql-client.
Оба хопа прошли без растрат: после первого — `questing`, после второго — финал:
Код:
Welcome to Ubuntu 26.04 LTS (GNU/Linux 7.0.0-22-generic x86_64)
Ребут, ядро загрузилось нужное, сервисы поднялись, тест загрузкой файла повторно прошёл чисто.
Часть 7. Что выжило после двух прыжков — и почему
Первая проверка после апгрейда — что осталось от php-стека:
Код:
ls /etc/apt/sources.list.d/ | grep -i ondrej
ondrej-ubuntu-php-plucky.sources
php-ondrej-jammy.list.disabled
dpkg -l | grep php8.4
[...все одиннадцать пакетов — те же 8.4.22-1+ubuntu24.04.1+deb.sury.org+1...]
Ни один файл, ни один пакет не тронут за оба хопа. На первый взгляд — удача, но за этим стоит конкретный механизм: `do-release-upgrade` отключает сторонние источники, которые соответствуют релизу, с которого идёт апгрейд. А наш файл всё это время был подписан как `Suites: noble` — и ни на одном из двух хопов (plucky→questing, questing→resolute) фактический codename машины не совпадал со строкой в файле. Инструмент его просто не узнал как «источник старого релиза» и не тронул. Сработало — но по случайному совпадению логики, а не потому что так было задумано.
`ProtectSystem=full` и наш `ReadWritePaths=/usr/www` пережили оба хопа без каких-либо действий с моей стороны — drop-in override лежит отдельно от пакета и его обновлениями не перезатирается:
Код:
systemctl cat php8.4-fpm | grep -iE "protectsystem|readwritepaths"
ProtectSystem=full
ReadWritePaths=/usr/www
Часть 8. Старый PPA окончательно мёртв — переезд на собственный канал Sury
Раз уж добрался до настоящей LTS — самое время убрать костыль с noble и поставить нативную сборку под resolute. Но прямой замены `Suites: noble → resolute` в том же файле не получится: Launchpad-PPA
ppa:ondrej/php для 26.04 не публиковался вообще — есть открытый баг-репорт автору с прямой претензией, что репозиторий застрял на noble уже два релиза назад. Сам автор тем временем перенёс основные усилия на отдельный канал — `packages.sury.org` — и там сборки под resolute уже реально есть.Раз источники разные — старый Launchpad-PPA убираю целиком, а не оставляю рядом с новым (иначе рискую получить конфликт `Signed-By` или задвоенные кандидаты):
Код:
rm -f /etc/apt/sources.list.d/ondrej-ubuntu-php-plucky.sources /etc/apt/sources.list.d/php-ondrej-jammy.list.disabled
apt-get install -y lsb-release ca-certificates curl
curl -sSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb
dpkg -i /tmp/debsuryorg-archive-keyring.deb
echo "deb [signed-by=/usr/share/keyrings/debsuryorg-archive-keyring.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list
apt update
Это официальная схема установки с самого `packages.sury.org` — отдельный keyring-пакет вместо ручного ключа, и одна строка источника, привязанная к реальному codename системы через `$(lsb_release -sc)`.
Часть 9. Ловушка с тильдой
Проверка кандидата показала именно то, что нужно:
Код:
apt-cache policy php8.4-fpm
Installed: 8.4.22-1+ubuntu24.04.1+deb.sury.org+1
Candidate: 8.4.22-1+0~20260606.50+ubuntu26.04~1.gbpc1d74e
500 https://packages.sury.org/php resolute/main
Но прямой `apt install php8.4-fpm ...` (без указания версии) выдал по всем одиннадцати пакетам:
Код:
php8.4-fpm is already the newest version (8.4.22-1+ubuntu24.04.1+deb.sury.org+1).
apt не глючил — он честно сравнил версии по своим правилам, и по этим правилам ошибся относительно того, что реально нужно. Дело в символе `~` в хвосте новой версии (`...+0~20260606.50+...`). В системе сравнения версий Debian/Ubuntu тильда сортируется ниже всего, даже ниже отсутствия символа — это специальный маркер для пред-релизных/снапшотных тэгов (`1.0~rc1` формально младше `1.0`). У resolute-сборки `~` стоит прямо в начале хвоста, и по факту более новая и нужная сборка формально считается «более старой» версией, чем уже установленная noble-сборка. apt отказывается понижать версию по доброй воле — и это, в данном случае, неверное решение, основанное на верной логике.
Лечится явным указанием версии вместо доверия к автоматическому выбору:
Код:
PKGS="php8.4-cli php8.4-common php8.4-curl php8.4-fpm php8.4-gd php8.4-mbstring php8.4-mysql php8.4-opcache php8.4-readline php8.4-xml php8.4-zip"
TARGETS=""
for p in $PKGS; do
v=$(apt-cache madison "$p" | awk -F'|' '/packages.sury.org\/php resolute/{gsub(/ /,"",$2); print $2; exit}')
TARGETS="$TARGETS $p=$v"
done
apt install --allow-downgrades $TARGETS
`--allow-downgrades` нужен ровно из-за того же эффекта — без него apt откажется выполнять то, что сам считает понижением версии. Установка прошла, в выводе честно значилось `DOWNGRADING: ...` — название говорит про формальную логику apt, а не про реальное направление изменений.
Часть 10. Последняя паранойя
Пакеты заменены на диске, но мастер-процесс php8.4-fpm крутился ещё с предыдущего рестарта — стоило убедиться, что он реально перечитал новый бинарник, а не продолжает жить со старым, уже удалённым с диска файлом:
Код:
PID=$(systemctl show -p MainPID --value php8.4-fpm)
ls -l /proc/$PID/exe
/proc/15250/exe -> /usr/sbin/php-fpm8.4
Без суффикса `(deleted)` — процесс и текущий файл на диске совпадают, скорее всего постинст пакета сам перезапустил сервис при установке. Но раз сомнение возникло — дешевле просто перезапустить и проверить, чем продолжать рассуждать:
Код:
systemctl restart php8.4-fpm
systemctl status php8.4-fpm | grep -i active
И снова — реальная загрузка файла через сайт, а не просто тишина в логе.
Итог
| Было | Стало |
| Ubuntu 25.04 «plucky», EOL с 15.01.2026 | Ubuntu 26.04 LTS «resolute», поддержка до 2031 |
| apt-daily мёртв молча 281 день | таймеры снова живы |
| php8.4 — кросс-сборка под noble через мёртвый Launchpad-PPA | нативная сборка resolute с packages.sury.org |
| ProtectSystem=full — не диагностирован | диагностирован, ReadWritePaths поставлен и проверен реальной записью |
Что забираю с собой на будущее:
- Зависший процесс — это не «подождать», это сразу `ps -p` на PID из ошибки лока. Секунды не экономишь, а вот потерять девять месяцев патчей — легко.
- Третьесторонние PPA, привязанные к конкретному codename текстом в файле, не следят за релизом системы сами — после `do-release-upgrade` каждый такой файл нужно перепроверять руками, а не доверять автоматике.
- Сравнение версий Debian/Ubuntu — не просто «больше число — новее». Тильда — это не опечатка в номере, это значимый символ сортировки, и от него зависит, поставит apt нужную сборку или тихо откажется.
- «В логе пусто» доказывает отсутствие конкретной ошибки в конкретный момент, а не отсутствие проблемы. Где можно — проверяю реальной операцией (запись файла, рестарт, hash бинарника), а не молчанием журнала.