CI/деплой встали: диск VPS 94%, копятся buildx-билдеры (20 штук, до 2 месяцев) #2869

Closed
opened 2026-08-13 12:24:30 +00:00 by bot-backend · 4 comments
Collaborator

Инцидент 13.08, ~12:00

Деплой main после мержа PR #2863 упал:

#15 ERROR: mkdir /var/lib/buildkit/runc-overlayfs/snapshots/snapshots/12/fs/app:
           no space left on device
ERROR: failed to build: failed to solve: ResourceExhausted

build-backend и build-worker — красные, deploy пропущен гейтом (правильно).
То есть правка смержена, но на прод не доехала.

Одна причина, три симптома

В то же окно упали ещё два прогона, и это оказался тот же дефицит диска, а не три разных:

Симптом Что выглядело причиной Что было на самом деле
Deploy / build-backend красный за 1m17s сборка no space left on device
openapi-codegen-check красный на PR #2868 за 2m20s «устарели типы фронта» падение на установке; схема app.openapi() байт-в-байт совпадает с main
backend-tests красный на PR #2865 за 16m42s упавший тест 4646 passed, покрытие 74.57% ≥ 65%, красным job стал после тестов, шаги if: always() не напечатали ничего

Проверять стоило не «что сломала правка», а «что общего у трёх падений».

Куда ушёл диск

/dev/vda1  145G  136G использовано  9.0G свободно  94%

тома docker (72.26G всего):
  tradein-postgres-data                    21.6G   ← данные
  gendesign_postgres_data                  15.2G   ← данные
  buildx_buildkit_builder-*_state × 10     19.3G   ← мусор

Причина накопления: каждый прогон CI поднимает новый buildx-билдер и никогда его не убирает.
Сейчас на хосте живёт 20 контейнеров buildx_buildkit_builder-*, старейшие — «Up 2 months».
Каждый держит свой _state-том с кэшем сборки. Ни один не dangling (контейнеры существуют),
поэтому docker volume prune их не видит, и рост незаметен, пока не упрётся.

Что сделал сейчас (только кэш, ничего не удалял)

buildctl du в каждом старом билдере показал, что кэш реклеймится целиком
активных сборок там нет:

8ea02b67  reclaimable=4.60GB   ae090acf  reclaimable=4.54GB
426ebfb0  reclaimable=1.77GB   a4fd7782  reclaimable=1.38GB
d01068fc  reclaimable=1.38GB   f90c7ef2  reclaimable=1.11GB
15e2c70d  reclaimable=771MB    7d0680ca  reclaimable=37MB

Прогнал buildctl prune --all по этим восьми. Два билдера «Up 24 minutes» не трогал
они принадлежат текущим прогонам.

до:    145G  132G  13G  92%
после: 145G  117G  28G  82%

Контейнеры и тома на месте, удалён только регенерируемый кэш сборки. Деплой перезапущен
через workflow_dispatch.

Что осталось сделать (не автоматизировал — это решение владельца)

  1. Снести старые билдеры. 18 контейнеров возрастом 5 недель–2 месяца + их _state-тома.
    Кэш уже вычищен, так что выигрыш по месту небольшой, но они плодятся дальше.
    Правильный способ — docker buildx rm <name> / docker rm -f + docker volume rm.
  2. Починить корень в пайплайне. Варианты:
    • именованный переиспользуемый билдер (docker buildx create --name ci --use один раз,
      дальше все прогоны в него) — заодно кэш начнёт реально работать между прогонами;
    • либо docker buildx rm в if: always()-шаге в конце job'а;
    • либо периодический buildctl prune --keep-storage по расписанию.
  3. Сторож на диск. Сейчас об исчерпании узнаём по красному деплою постфактум.
    Порог 85% с уведомлением дал бы запас в несколько дней.
  4. 182 dangling-тома — отдельно посмотреть, что это и можно ли чистить.

Почему это надо чинить, а не просто чистить

Рост монотонный: билдер на прогон, тома не убираются. Сегодняшняя чистка выиграла 15 ГБ,
то есть несколько недель. Без п.2 инцидент повторится с тем же набором «трёх разных
загадочных падений».

## Инцидент 13.08, ~12:00 Деплой main после мержа PR #2863 упал: ``` #15 ERROR: mkdir /var/lib/buildkit/runc-overlayfs/snapshots/snapshots/12/fs/app: no space left on device ERROR: failed to build: failed to solve: ResourceExhausted ``` `build-backend` и `build-worker` — красные, `deploy` пропущен гейтом (правильно). То есть **правка смержена, но на прод не доехала**. ## Одна причина, три симптома В то же окно упали ещё два прогона, и это оказался тот же дефицит диска, а не три разных: | Симптом | Что выглядело причиной | Что было на самом деле | |---|---|---| | `Deploy / build-backend` красный за 1m17s | сборка | `no space left on device` | | `openapi-codegen-check` красный на PR #2868 за 2m20s | «устарели типы фронта» | падение на установке; схема `app.openapi()` **байт-в-байт** совпадает с main | | `backend-tests` красный на PR #2865 за 16m42s | упавший тест | `4646 passed`, покрытие 74.57% ≥ 65%, красным job стал **после** тестов, шаги `if: always()` не напечатали ничего | Проверять стоило не «что сломала правка», а «что общего у трёх падений». ## Куда ушёл диск ``` /dev/vda1 145G 136G использовано 9.0G свободно 94% тома docker (72.26G всего): tradein-postgres-data 21.6G ← данные gendesign_postgres_data 15.2G ← данные buildx_buildkit_builder-*_state × 10 19.3G ← мусор ``` Причина накопления: каждый прогон CI поднимает **новый** buildx-билдер и **никогда его не убирает**. Сейчас на хосте живёт **20 контейнеров `buildx_buildkit_builder-*`**, старейшие — «Up 2 months». Каждый держит свой `_state`-том с кэшем сборки. Ни один не dangling (контейнеры существуют), поэтому `docker volume prune` их не видит, и рост незаметен, пока не упрётся. ## Что сделал сейчас (только кэш, ничего не удалял) `buildctl du` в каждом старом билдере показал, что кэш **реклеймится целиком** — активных сборок там нет: ``` 8ea02b67 reclaimable=4.60GB ae090acf reclaimable=4.54GB 426ebfb0 reclaimable=1.77GB a4fd7782 reclaimable=1.38GB d01068fc reclaimable=1.38GB f90c7ef2 reclaimable=1.11GB 15e2c70d reclaimable=771MB 7d0680ca reclaimable=37MB ``` Прогнал `buildctl prune --all` по этим восьми. Два билдера «Up 24 minutes» **не трогал** — они принадлежат текущим прогонам. ``` до: 145G 132G 13G 92% после: 145G 117G 28G 82% ``` Контейнеры и тома на месте, удалён только регенерируемый кэш сборки. Деплой перезапущен через `workflow_dispatch`. ## Что осталось сделать (не автоматизировал — это решение владельца) 1. **Снести старые билдеры.** 18 контейнеров возрастом 5 недель–2 месяца + их `_state`-тома. Кэш уже вычищен, так что выигрыш по месту небольшой, но они плодятся дальше. Правильный способ — `docker buildx rm <name>` / `docker rm -f` + `docker volume rm`. 2. **Починить корень в пайплайне.** Варианты: - именованный **переиспользуемый** билдер (`docker buildx create --name ci --use` один раз, дальше все прогоны в него) — заодно кэш начнёт реально работать между прогонами; - либо `docker buildx rm` в `if: always()`-шаге в конце job'а; - либо периодический `buildctl prune --keep-storage` по расписанию. 3. **Сторож на диск.** Сейчас об исчерпании узнаём по красному деплою постфактум. Порог 85% с уведомлением дал бы запас в несколько дней. 4. **182 dangling-тома** — отдельно посмотреть, что это и можно ли чистить. ## Почему это надо чинить, а не просто чистить Рост монотонный: билдер на прогон, тома не убираются. Сегодняшняя чистка выиграла 15 ГБ, то есть несколько недель. Без п.2 инцидент повторится с тем же набором «трёх разных загадочных падений».
Author
Collaborator

Поправка к разбору выше. Я написал «каждый прогон CI поднимает новый билдер и никогда
его не убирает». Это неверно — проверил по датам создания:

17.05 ×2   30.05 ×2   31.05 ×3   13.06 ×1   17.06 ×1
20.06 ×4   28.06 ×1   05.07 ×4   13.08 ×2

Пять недель между 05.07 и 13.08 — ни одной протечки, при десятках деплоев. Два
сегодняшних созданы в 11:57:43 — тем самым прогоном, что упал с no space left on device.

Правильная формулировка: штатная уборка работает, билдеры текут только когда job умирает
аварийно
(ENOSPC, OOM, отмена concurrency-группой) — контейнер job'а мёртв, завершающие
шаги не выполняются. Отсюда и «пачками по 2-4»: падает разом несколько параллельных
сборочных job'ов одного прогона.

Практическое следствие: уборка в конце job'а (первая версия PR #2870) не помогла бы —
она не отрабатывает ровно в тех случаях, которые и создают мусор. Переделал на подбор
чужого мусора на входе job'а, с порогом по возрасту 6 часов. Плюс каждый деплой теперь
печатает df -h / — чтобы заполнение диска было видно постоянно, а не всплывало красным
раз в три месяца.

Пункт 1 плана («снести 18 исторических билдеров») этим закрывается автоматически на первом
же деплое после мержа — если это нежелательно, скажи, сделаю режим «только отчёт».

Поправка к разбору выше. Я написал «каждый прогон CI поднимает новый билдер и **никогда** его не убирает». Это неверно — проверил по датам создания: ``` 17.05 ×2 30.05 ×2 31.05 ×3 13.06 ×1 17.06 ×1 20.06 ×4 28.06 ×1 05.07 ×4 13.08 ×2 ``` **Пять недель между 05.07 и 13.08 — ни одной протечки**, при десятках деплоев. Два сегодняшних созданы в 11:57:43 — тем самым прогоном, что упал с `no space left on device`. Правильная формулировка: штатная уборка работает, билдеры текут **только когда job умирает аварийно** (ENOSPC, OOM, отмена concurrency-группой) — контейнер job'а мёртв, завершающие шаги не выполняются. Отсюда и «пачками по 2-4»: падает разом несколько параллельных сборочных job'ов одного прогона. Практическое следствие: уборка **в конце** job'а (первая версия PR #2870) не помогла бы — она не отрабатывает ровно в тех случаях, которые и создают мусор. Переделал на подбор чужого мусора **на входе** job'а, с порогом по возрасту 6 часов. Плюс каждый деплой теперь печатает `df -h /` — чтобы заполнение диска было видно постоянно, а не всплывало красным раз в три месяца. Пункт 1 плана («снести 18 исторических билдеров») этим закрывается автоматически на первом же деплое после мержа — если это нежелательно, скажи, сделаю режим «только отчёт».
Author
Collaborator

Прод-проверка после мержа PR #2870 — работает

Первый же деплой отработал подбор, вот его собственный лог:

buildx: убираю протёкший билдер buildx_buildkit_builder-9fe85cfa-... (возраст 929 ч)
buildx: убираю протёкший билдер buildx_buildkit_builder-17270f32-... (возраст 1290 ч)
buildx: убираю протёкший билдер buildx_buildkit_builder-a4fd7782-... (возраст 1795 ч)
...
buildx: не удалось убрать buildx_buildkit_builder-36a1db5a-... (не фатально)

Строки «не удалось» — ожидаемая гонка: три параллельных сборочных job'а подбирают один
и тот же список одновременно, кто-то не успевает. Обработано нефатально, как и задумано.

Второй прогон (workflow_dispatch) уже чистый:

buildx: убрано протёкших 0, оставлено свежих 2
buildx: убираю билдер builder-5b905fbb-...        ← завершающий шаг тоже отработал

Замер:

билдеров:  20 → 3 → 2
диск:      136G занято (94%) → 116G (81%), свободно 9.0G → 29G

Пункт 1 плана (снести исторические билдеры) закрыт автоматически, пункт 2 (корень) — тоже.

Побочное наблюдение: GHCR отдаёт 403 на пуш, и это выглядит как красный деплой

В том же прогоне build-frontend упал:

#19 ERROR: failed to push ghcr.io/lekss361/gendesign-frontend:<sha>:
    denied: permission_denied: HTTP status code 403 "Forbidden"

При этом build-backend и build-worker в ТОМ ЖЕ прогоне, с тем же токеном,
запушились нормально. Повторный прогон через workflow_dispatch — фронт запушился
без единой ошибки (#19 DONE 5.3s, Job succeeded).

То есть это не права на пакет, а разовый отказ реестра. Но выглядит он как «сломалась
сборка фронта», и лечится только ручным перезапуском. За сегодня такое случилось дважды
(второй раз — на buildcache в другом прогоне).

Стоит подумать про retry на шаге пуша: сейчас один 403 от посредника роняет деплой целиком,
а deploy гейтится по build-*.result != 'failure' — то есть ЛЮБОЙ временный отказ реестра
останавливает выкладку до тех пор, пока человек не заметит и не перезапустит.

Отдельный момент: gendesign-frontend:latest на проде датирован 10.08 — сегодняшний
пуш прошёл, но deploy в том прогоне был пропущен гейтом, так что образ на хосте пока
прежний. Для бэкенда это не важно (фронт сегодня не менялся), но если понадобится выкатить
фронт — деплой нужно дожать отдельно.

## Прод-проверка после мержа PR #2870 — работает Первый же деплой отработал подбор, вот его собственный лог: ``` buildx: убираю протёкший билдер buildx_buildkit_builder-9fe85cfa-... (возраст 929 ч) buildx: убираю протёкший билдер buildx_buildkit_builder-17270f32-... (возраст 1290 ч) buildx: убираю протёкший билдер buildx_buildkit_builder-a4fd7782-... (возраст 1795 ч) ... buildx: не удалось убрать buildx_buildkit_builder-36a1db5a-... (не фатально) ``` Строки «не удалось» — ожидаемая гонка: три параллельных сборочных job'а подбирают один и тот же список одновременно, кто-то не успевает. Обработано нефатально, как и задумано. Второй прогон (workflow_dispatch) уже чистый: ``` buildx: убрано протёкших 0, оставлено свежих 2 buildx: убираю билдер builder-5b905fbb-... ← завершающий шаг тоже отработал ``` Замер: ``` билдеров: 20 → 3 → 2 диск: 136G занято (94%) → 116G (81%), свободно 9.0G → 29G ``` Пункт 1 плана (снести исторические билдеры) закрыт автоматически, пункт 2 (корень) — тоже. ## Побочное наблюдение: GHCR отдаёт 403 на пуш, и это выглядит как красный деплой В том же прогоне `build-frontend` упал: ``` #19 ERROR: failed to push ghcr.io/lekss361/gendesign-frontend:<sha>: denied: permission_denied: HTTP status code 403 "Forbidden" ``` При этом `build-backend` и `build-worker` в ТОМ ЖЕ прогоне, с тем же токеном, запушились нормально. Повторный прогон через `workflow_dispatch` — фронт запушился без единой ошибки (`#19 DONE 5.3s`, `Job succeeded`). То есть это не права на пакет, а разовый отказ реестра. Но выглядит он как «сломалась сборка фронта», и лечится только ручным перезапуском. За сегодня такое случилось дважды (второй раз — на buildcache в другом прогоне). Стоит подумать про retry на шаге пуша: сейчас один 403 от посредника роняет деплой целиком, а `deploy` гейтится по `build-*.result != 'failure'` — то есть ЛЮБОЙ временный отказ реестра останавливает выкладку до тех пор, пока человек не заметит и не перезапустит. Отдельный момент: `gendesign-frontend:latest` на проде датирован **10.08** — сегодняшний пуш прошёл, но `deploy` в том прогоне был пропущен гейтом, так что образ на хосте пока прежний. Для бэкенда это не важно (фронт сегодня не менялся), но если понадобится выкатить фронт — деплой нужно дожать отдельно.
Author
Collaborator

Поправка к последнему абзацу предыдущего комментария — я написал, что в успешном прогоне
deploy был пропущен гейтом. Это неверно, я это не проверил.

Прогон 7518 (workflow_dispatch): все пять job'ов зелёные, включая deploy. По его логу
контейнеры реально пересозданы:

Container gendesign-frontend-1 Started   12:54:40
Container gendesign-backend-1  Started   12:54:55
Container gendesign-worker-1   Started   12:54:55
Container gendesign-caddy-1    Started   12:54:58

Дата 2026-08-10 у gendesign-frontend:latest меня и сбила: это время сборки образа,
а не время выкладки. Исходники фронта с 10.08 не менялись, сборка целиком попала в кэш →
слои идентичны → у образа та же дата создания. То есть образ на проде актуальный, просто
его содержимое не изменилось.

Правильный вывод: выкладка прошла полностью, отдельно «дожимать фронт» не нужно.
Всё остальное в том комментарии (подбор билдеров работает, 20 → 3 → 2, диск 9.0G → 29G,
403 от GHCR разовый) — проверено и остаётся в силе.

Поправка к последнему абзацу предыдущего комментария — я написал, что в успешном прогоне `deploy` был пропущен гейтом. **Это неверно, я это не проверил.** Прогон 7518 (workflow_dispatch): все пять job'ов зелёные, включая `deploy`. По его логу контейнеры реально пересозданы: ``` Container gendesign-frontend-1 Started 12:54:40 Container gendesign-backend-1 Started 12:54:55 Container gendesign-worker-1 Started 12:54:55 Container gendesign-caddy-1 Started 12:54:58 ``` Дата `2026-08-10` у `gendesign-frontend:latest` меня и сбила: это **время сборки образа**, а не время выкладки. Исходники фронта с 10.08 не менялись, сборка целиком попала в кэш → слои идентичны → у образа та же дата создания. То есть образ на проде актуальный, просто его содержимое не изменилось. Правильный вывод: выкладка прошла полностью, отдельно «дожимать фронт» не нужно. Всё остальное в том комментарии (подбор билдеров работает, 20 → 3 → 2, диск 9.0G → 29G, 403 от GHCR разовый) — проверено и остаётся в силе.
lekss361 added the
chore
ci
observability
scope/devops
labels 2026-08-16 10:25:28 +00:00
Author
Collaborator

Закрываю по ревизии 01.09.2026: PR #2870 смержен 13.08 (buildx-билдеры убираются в конце job'ов), прод-подтверждение в комментах — первый же деплой снёс протёкшие билдеры возрастом 929–1795 ч, прогон 7518 полностью зелёный.

Закрываю по ревизии 01.09.2026: PR #2870 смержен 13.08 (buildx-билдеры убираются в конце job'ов), прод-подтверждение в комментах — первый же деплой снёс протёкшие билдеры возрастом 929–1795 ч, прогон 7518 полностью зелёный.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2869
No description provided.