Все записи
6 мин

Снёс бы этот Kubernetes к чертям: как OOMKilled в CI убил полдня и почему я жалею про оверинж

Building in Publicинфраструктура

Если коротко, то я хотел задеплоить один маленький фикс на dev, а в итоге потратил полдня только на то, чтобы CI-пайплайн вообще согласился собрать образ, потому что сборка стабильно падала с exit code 137 OOMKilled на этапе npm ci, и раннеру банально не хватало памяти. Подняли лимиты — и под тут же стал Unschedulable, потому что столько памяти на единственной ноде физически нет. И вот сидишь ты на проде с разовыми апрувами по одной команде, дёргаешь систему, которая вроде должна была сделать тебе жизнь проще, а она тебе её делает невыносимой, и единственный честный вывод дня такой: для текущего масштаба весь этот Kubernetes — оверинжиниринг, можно было бы тупо без докера пушить на сервер, и всё бы работало. Дальше расскажу, как именно я к этому пришёл и почему не жалею, что психанул.

Сборка падает на ровном месте

Стек у проекта Mixello такой: GitLab runner крутится на Kubernetes (точнее на K3s), а образы собираются через kaniko. Звучит солидно, по-взрослому, как в больших командах. А на деле я просто пушу код и жду, пока пайплайн соберёт мне контейнер, и вот эта сборка раз за разом валилась на самом банальном шаге — на npm ci, где ставится 539 пакетов. В логах ERROR, exit code 137, reason OOMKilled. Для тех, кто не в теме: это значит, что процессу не хватило памяти и его прибило ядро, причём виновато не приложение, а раннер, который собирает образ и упёрся в потолок по RAM прямо во время установки зависимостей.

И знаете, что тут самое весёлое? Ретрай не помогает. Ты жмёшь retry, как будто это интернет на телефоне в лифте, авось со второго раза прокатит, а оно с той же стабильностью падает в том же самом месте. Параллельно прилетел ещё один привет от системы — 400 Bad Request при попытке запушить образ postgres в реестр. То есть мало того что сборка не лезет в память, так ещё и пуш отдельно ломается, два независимых сбоя в одном пайплайне, и оба не имеют отношения к моему коду, я-то всего лишь хотел залить маленький фикс.

Лечим симптомы, а болезнь не уходит

Первое, что делаешь в такой ситуации, — пытаешься ужать саму сборку, чтобы она влезла в имеющуюся память. Я код руками не пишу, я оркеструю агентов, и тут как раз тот случай: Claude Code предложил очевидное — npm ci с флагами --no-audit и --no-fund, чтобы npm не тратил ресурсы на аудит и на болтовню про донаты, плюс npm cache clean --force и rm -rf по кэшам, чтобы вычистить всё лишнее и снизить пиковое потребление. Логика здравая, меньше делаешь во время установки — меньше жрёшь памяти.

Помогло частично, но не до конца, и тогда мы пошли другим путём и подняли лимиты самому раннеру. CPU отдали в диапазоне 1 на запрос и 2 на лимит, память выкрутили до 6Gi request и 8Gi limit. Казалось бы, вот оно, дай процессу памяти и пусть собирает спокойно, и я жму деплой и жду, что сейчас всё поедет.

Под становится Unschedulable

А поехало вот что: под просто не запустился. Статус Unschedulable, в описании честная и беспощадная строчка — 1 Insufficient memory, no preemption victims. Перевожу на человеческий: я попросил у кластера 6 гигов памяти под раннер, а на единственной ноде, которая у проекта есть, столько свободной памяти просто нет и взять её неоткуда, вытеснять тоже некого, потому что нода одна. То есть я лечил OOMKilled добавлением памяти, а в ответ получил под, который вообще не может встать, потому что памяти физически нет.

И вот тут до меня доходит вся ирония ситуации. У меня кластер. У меня оркестратор, который умеет шедулить поды по нодам, переживать падения, масштабироваться, гениальная инженерная машина, придуманная гуглом, чтобы рулить тысячами серверов. А у меня одна нода. Одна. Кому этот оркестратор оркестрирует, что он шедулит, если шедулить некуда? Это как купить грузовой тягач, чтобы возить на нём один пакет с пельменями из магазина — технически едет, но ты потратил кучу денег и нервов на то, что решалось велосипедом.

Когда психуешь — это тоже данные

В тот момент я написал примерно следующее, и причёсывать это не буду, потому что это честная реакция: я бы этот кубер вообще снёс, нахрена он нужен, может реально перенести всё на другой сервер и сделать всё в докерах. И дальше в том же духе: можно было бы даже без докера просто на сервер всё пушить, и всё бы работало, и какой смысл был во всём этом оверинже. Грубо, зато по делу. Потому что когда ты полдня воюешь не с задачей, а с инфраструктурой вокруг задачи, это и есть определение оверинжиниринга — система сложнее, чем проблема, которую она решает.

Отдельная приправа ко всему этому в том, что работал я на проде, с точечными апрувами по одной команде, и на чтение, и на запись, то есть каждое действие шло через подтверждение. Представьте: вы пытаетесь потушить пожар, а перед каждым ведром воды надо подписать бумажку. Безопасно? Безопасно. Но когда ты соло-разработчик и тебе надо просто проверить, влезет ли сборка в память, этот режим превращает пятиминутную проверку в марафон.

Почему так вообще вышло

Тут надо честно сказать: инфру с Kubernetes на этом проекте настраивал не я. И я не валю на человека, потому что на момент, когда это поднималось, идея «давайте по-взрослому, кластер, CI, kaniko, реестр» выглядела вполне разумно, так делают серьёзные команды, так пишут в гайдах. Проблема не в том, что кто-то накосячил, проблема в том, что выбрали инструмент не под масштаб. Kubernetes решает реальные боли, когда у вас десятки сервисов, нагрузка скачет и нужно автоматом подниматься и падать. А когда у вас один проект, одна нода и пара контейнеров, вы получаете весь геморрой большого решения без единой его выгоды.

Я вообще заметил такую штуку про себя и про вайбкодинг в целом: когда код тебе пишет нейросеть, а инфру кто-то поднимает по best practices, очень легко набрать сложности, которую ты потом не тянешь. Claude Code тебе с радостью соберёт хоть кластер, хоть микросервисы, хоть что, он не устаёт и не платит за это нервами — я ему как-то целую фичу под ключ по процессу STC отдал, от спеки до прода. Платишь ты, когда в три часа дня на проде у тебя под Unschedulable и ты не понимаешь, в какую сторону вообще копать. Простое решение почти всегда лучше красивой архитектуры, особенно когда ты один и у тебя десятки пользователей, а не миллион.

Что в итоге и что я понял

По цифрам день выглядел так: сборка падала с exit 137 OOMKilled на установке 539 пакетов, отдельно ловили 400 при пуше образа в реестр, пробовали ужать npm ci флагами и чисткой кэша, подняли раннеру память до 6Gi/8Gi и упёрлись в Unschedulable, потому что на единственной ноде столько нет. Полдня на то, чтобы инфраструктура согласилась сделать то, что без неё делается одной командой.

Главный вывод тут не про Kubernetes как технологию, она шикарная, когда реально нужна. Вывод про то, что выбирать стек надо под сегодняшний масштаб, а не под воображаемый завтрашний, где у тебя миллион юзеров и сто серверов. Для маленького проекта правильный ответ часто скучный: арендовать обычный сервер, поднять пару контейнеров через docker compose, пушить туда код напрямую и не строить себе космодром ради запуска бумажного самолётика. Если хочется почитать, как я в другом проекте наоборот выстраивал процесс вокруг AI-агента и зачем, гляньте STC + Guardian MCP, там про обратную крайность, когда процесс помогает, а не мешает.

Перенесу ли я Mixello с кластера на простой сервер в докерах? Очень похоже что да, и чем дольше я смотрю на эту историю, тем меньше у меня сомнений. Вот и делайте выводы про то, нужен ли вам Kubernetes, если у вас одна нода и горящий дедлайн.