Почему Kubernetes убивает сервис при поднятии лимита памяти пода до 1 ГБ
Что проверяют
Свяжете ли вы лимит cgroup с тем, как JVM выбирает свои размеры. Ожидание простое: кандидат знает, что лимит ограничивает весь контейнер, а не кучу, и умеет читать симптом, то есть код выхода 137 и причину OOMKilled в описании пода.
Как отвечать
Сначала кто убивает. Процесс завершает ядро, когда контрольная группа упирается в свой потолок памяти. Kubelet только фиксирует статус и перезапускает контейнер по правилу рестарта. Вытеснение пода при нехватке памяти на узле это другой механизм с другой причиной в событиях.
Дальше почему именно Java. Память процесса это не только куча: сюда входят metaspace, кэш скомпилированного кода, стеки потоков примерно по мегабайту на поток, прямые буферы NIO и Netty, структуры сборщика, буферы нативных библиотек. Размер кучи современная JVM выбирает от лимита контейнера через -XX:MaxRAMPercentage, и по умолчанию это заметно меньше самого лимита. То есть гигабайт лимита не означает гигабайт кучи, зато всё, что вне кучи, спокойно добирает остаток и пробивает потолок. Если раньше лимита не было вовсе, JVM ориентировалась на память узла и вопрос не возникал.
Что предложить: посмотреть kubectl describe pod и найти там Reason и Exit Code, включить -XX:NativeMemoryTracking=summary и снять jcmd VM.native_memory, явно ограничить -XX:MaxMetaspaceSize и -XX:MaxDirectMemorySize, задать MaxRAMPercentage осознанно, а request сделать равным limit, чтобы под получил класс Guaranteed.
Где спотыкаются
Ждут OutOfMemoryError со стектрейсом и heap dump. Внешнее убийство не даёт ни того, ни другого: процесс исчезает мгновенно, логи обрываются на полуслове, и поиск идёт не там.
Считают, что лимит ограничивает кучу. Отсюда вывод, что памяти хватает с запасом, хотя за пределами кучи живёт вторая половина расхода, которую никто не мерил.
Лечат симптом, поднимая лимит ещё выше. Если течёт metaspace от генерации классов или не освобождаются прямые буферы, новый потолок только отодвинет падение. Отдельный случай: старые сборки Java не читают cgroup и берут за основу память узла, тогда спасает явный -Xmx или обновление.