Один вечер, три проблемы: почта упала, память кончилась, не тот docker arch
Published: 2026-08-13
Всё крутится на одной маленькой VM: 4GB RAM, k0s, один нод. Вечером mail.antonnovikov.com начал отдавать 503. Починил, но по пути выяснилось, что дело не в почте, а в памяти на всей ноде. А потом ещё сломал маленький сайт при редеплое из-за не той архитектуры процессора. Это пост про весь вечер по порядку, с командами, которые реально запускал.
Проблема 1: mail.antonnovikov.com — 503
Первая команда всегда одна и та же:
bashkubectl get pods -A | grep -v Running
Вывод:
webmail snappymail-7fd545b5bf-tvhxx 0/1 CrashLoopBackOff 221 (13s ago) 251d
221 рестарт. Под падает давно, не сегодня. Просто сегодня он завис в упавшем состоянии достаточно долго, чтобы APISIX начал отдавать 503 (нет живого backend'а).
bashkubectl describe pod -n webmail snappymail-7fd545b5bf-tvhxx
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
OOMKilled. Лимит памяти был 256Mi, контейнер медленно растёт (обычная утечка PHP-FPM воркеров) и его убивает где-то раз в день, 251 день подряд. Никто не замечал, потому что под быстро поднимается обратно — просто сегодня момент был неудачный и он провисел дольше обычного.
Фикс: поднять лимит.
yamlresources:
limits:
memory: "384Mi" # было 256Mi
cpu: "500m"
bashkubectl patch deployment snappymail -n webmail --type='json' \
-p='[{"op":"replace","path":"/spec/template/spec/containers/0/resources/limits/memory","value":"384Mi"}]'
Почта поднялась. Но это заставило посмотреть не только на под, а на ноду целиком.
Проблема 2: на ноде почти нет свободной памяти
bashssh root@$SERVER_IP free -h
total used free shared buff/cache available
Mem: 3.8Gi 3.4Gi 120Mi 27Mi 553Mi 404Mi
Swap: 2.0Gi 801Mi 1.2Gi
120Mi свободно, 800Mi уже в свопе. Не очень для коробки на 4GB, где крутится весь control-plane k0s плюс ~30 подов (VPN-прокси, мониторинг, почта, пара мелких сайтов).
bashkubectl describe node antonnovikov.com | grep -A5 "Allocated resources"
Resource Requests Limits
-------- -------- ------
cpu 1105m (18%) 7250m (120%)
memory 1909048192 (47%) 6240Mi (163%)
163% overcommit по лимитам памяти. На одной ноде это само по себе не катастрофа (переезжать всё равно некуда), но значит: если два-три пода вырастут одновременно — кого-то убьёт OOM.
Что реально урезал
Я не стал просто везде занижать лимиты — на одной ноде это ничего не меняет по факту, реально занятая память есть реально занятая память вне зависимости от того, что написано в limits. Искал настоящую, безопасную экономию.
VictoriaMetrics (vmsingle) — жрал 396Mi.
bashhelm get values vmsingle -n monitoring -a > /tmp/vmsingle-live.yaml
# memory.allowedPercent: 30 -> 20 (потолок кэша, данные не теряются)
helm upgrade vmsingle vm/victoria-metrics-single -n monitoring -f /tmp/vmsingle-live.yaml
Позже ещё поднял глобальный scrape interval 30s -> 60s. Это вдвое снизило скорость записи метрик — меньше CPU, меньше записи на диск, меньше давление на память. Заодно это важно для проблемы 3 ниже.
Sidecar дашбордов в Grafana — контейнер k8s-sidecar, который следит за ConfigMap'ами и живьём перезагружает дашборды. Стоил ~90-130Mi, просто сидел и смотрел. Я не так часто правлю дашборды, поэтому:
yamlsidecar:
dashboards:
enabled: false # было true
bashhelm upgrade grafana grafana/grafana -n monitoring -f platform/monitoring/values/grafana.yaml
Дашборды как работали, так и работают. Если теперь что-то поправлю в ConfigMap — нужен kubectl rollout restart deployment/grafana -n monitoring, чтобы подхватилось. Небольшая цена за -100Mi.
tor-proxy — ротирующий Tor HTTP-прокси, крутился с TOR_INSTANCES=2 (2 tor-демона), 223Mi.
bashkubectl set env deployment/tor-proxy -n tor TOR_INSTANCES=1
Сэкономил ~85Mi. Работает так же, просто меньше параллельных exit-цепочек.
Что НЕ трогал
APISIX, хотя он сам по себе самый прожорливый (~400Mi). В своём же install-скрипте нашёл комментарий из прошлого инцидента:
«Под реальной нагрузкой (blackbox-пробы + реальный трафик) CPU упёрся в лимит 500m и застрял — http и https оба перестали отвечать».
После этого лимиты подняли с 256Mi/500m до 512Mi/1000m. Возвращаться туда не буду. Если когда-то захочу урезать APISIX — нужен второй нод, а не меньший лимит на этом.
Стек алертинга (alertmanager, vmalert, kube-state-metrics, blackbox-exporter) — чуть было не предложил его тоже вырезать, потому что Slack/PagerDuty коннекторы не настроены. Ошибочное предположение: алерты идут напрямую в Telegram, стек реально работает. Вывод: «коннектор X не настроен» не значит «фича не используется». Нужно смотреть в реальный конфиг ресивера, а не только на то, что сам когда-то подключал.
Проблема 3: диск читает ~40-70 МБ/с весь вечер
В Grafana ровный, устойчивый график чтения с диска. Первая мысль — «что-то сканирует много данных». Мысль неверная. Сначала проверил I/O по процессам, но ничего не объясняло 40-70 МБ/с:
bash# снять /proc/<pid>/io read_bytes дважды с разницей 5 секунд, посчитать дельту
Ничего не выделялось. Тогда проверил своп напрямую:
bashvmstat 2 6
procs -----------memory---------- ---swap-- -----io----
r b swpd free buff cache si so bi bo
5 0 758556 119432 9736 672540 7836 74 44346 252
si (swap in) и bi (чтение блоков) растут вместе. Вот и ответ: это не один болтливый процесс, это своп-трэшинг. При ~120Mi свободной памяти ядро постоянно подкачивает страницы процессов из свопа туда-обратно, и каждый page-in — это реальное чтение с диска. График ровный, потому что давление на память постоянное, а не разовый всплеск.
То есть проблема 3 и проблема 2 — одна и та же проблема. Освобождение реальной памяти (vmsingle, grafana sidecar, tor-proxy) чинит и график диска тоже, отдельно диск тюнить не нужно было.
Проблема 4: передеплоил сайт, сломал его не той архитектурой
Параллельно со всем этим ещё передеплоил маленький статический сайт (мне-похуй.рф) с обновлённым дизайном. Обычный docker build && push && kubectl apply, делал так сто раз. В этот раз сломалось:
bashkubectl get pods -n default -l app=mne-pohui-site
mne-pohui-site-587fc87586-gmp62 0/1 CrashLoopBackOff
bashkubectl logs -n default -l app=mne-pohui-site
exec /docker-entrypoint.sh: exec format error
exec format error почти всегда значит: не та архитектура процессора. Мой ноутбук — Apple Silicon (arm64), нода k0s — amd64. Деплой-скрипт обычно собирает через docker buildx build --platform linux/amd64, но docker buildx на этой машине вообще не был установлен — сборка молча откатилась на обычный docker build, который собрал под arm64 (родная архитектура ноутбука), запушил это, а amd64-нода такое запустить не смогла.
Заодно по пути нашёл: у локального Docker (colima, не Docker Desktop) не был прописан insecure-registries, поэтому пуш во внутренний registry 91.184.248.13:30500 падал с ошибкой HTTPS ещё до того, как дошло до проблемы с архитектурой.
Починил оба момента:
bash# 1. поставить недостающий плагин buildx
brew install docker-buildx
ln -sf /opt/homebrew/opt/docker-buildx/bin/docker-buildx ~/.docker/cli-plugins/docker-buildx
yaml# 2. ~/.colima/default/colima.yaml
docker:
insecure-registries:
- 91.184.248.13:30500
bashcolima stop && colima start
Пересобрал уже честно под --platform linux/amd64, запушил, kubectl rollout restart. Старый под всё это время продолжал отдавать трафик (Kubernetes не убирает старую реплику, пока новая не станет Ready), так что для посетителей реального даунтайма не было — просто застрявший rollout, который нужно было заметить и починить.
Что может пойти не так (если повторяешь у себя)
- Поднять лимит памяти контейнеру — не значит починить утечку, это значит отсрочить падение. Если snappymail снова упадёт по OOM на 384Mi, утечка никуда не делась — нужно либо периодически рестартовать PHP-FPM воркеры, либо искать реальную утечку.
- Не режь ресурсы у компонента, про который у тебя же есть задокументированный инцидент про урезание. Читай свои старые комментарии в скриптах перед «оптимизацией». Чуть не полез в APISIX, пока не перечитал свой же install-скрипт.
- График памяти и график диска могут быть одной и той же причиной. Перед тем как гоняться за конкретным процессом по диску — проверь своп (
vmstat,si/so). На ноде с нехваткой памяти это почти всегда своп. exec format error= не та архитектура, а не битый образ. Проверяй, чтоdocker buildx build --platformреально используется, а не тихо пропускается, потому что buildx не установлен.- Rolling update защищает больше, чем кажется. Сломанный новый под под Deployment сам по себе не роняет сайт — старая реплика живёт, пока новая не станет Ready. Чинить всё равно надо, но это не аварийная ситуация сама по себе.
Итого
- Почта 503 → snappymail упал по OOM, поднял лимит 256Mi → 384Mi
- Реальная причина — нехватка памяти на всей ноде (163% overcommit по лимитам, 120Mi свободно, 800Mi в свопе)
- Освободил реальную память: потолок кэша vmsingle 30%→20% + scrape interval 30s→60s, sidecar дашбордов grafana выключен (~100Mi), tor-proxy 2→1 демон (~85Mi)
- APISIX не трогал — прошлый инцидент уже доказал, что урезание его лимитов роняет сайт под нагрузкой
- Стек алертинга оставил — он подключён к Telegram, «нет коннектора Slack/PagerDuty» было ложным сигналом о неиспользуемости
- Высокий disk I/O оказался своп-трэшингом из-за нехватки памяти, а не конкретным процессом — та же причина, что и всё остальное
- Отдельно: сломал и починил редеплой из-за arm64/amd64 — buildx не был установлен,
docker buildмолча собрал не под ту архитектуру