JavaRush /Курси /Docker for Spring /OOMKilled і стан кон...

OOMKilled і стан контейнера

Docker for Spring
Рівень 23 , Лекція 3
Відкрита

1. OOMKilled: що це таке

Коли йдеться про ліміти CPU, картина зазвичай мʼякша: контейнер живий, але все виконує повільніше. А от під тиском на памʼять фінал може бути значно жорсткішим. Тут сервіс уже не просто гальмує, а може або сам завершитися з OutOfMemoryError, або взагалі не встигнути нічого сказати, бо його вбʼє контейнерний runtime.

Коли ви вперше бачите, що контейнер «раптом помер», дуже хочеться пояснити це однією фразою: «Ну, мабуть, не вистачило памʼяті». Це як діагностувати будь-яку хворобу словами «десь застудився». Інколи вгадаєте, але частіше — ні. У контейнерному світі є принципова різниця між тим, що застосунок сам упав, і тим, що середовище запуску примусово вимкнуло процес.

OOMKilled — якраз про другий випадок. Це не «Java трохи образилася й закрилася», а ситуація, коли контейнер вийшов за ліміт памʼяті, і оточення (насправді ядро ОС через механізм обмеження памʼяті) просто каже процесу: «Друже, ти забираєш забагато памʼяті — до побачення». І каже це максимально прямолінійно: SIGKILL (тобто «вбити без розмов»). У Java в цей момент зазвичай немає ні часу, ні права на «красиву» помилку, лог, shutdown hooks та іншу культуру спілкування.

2. OutOfMemoryError у JVM і OOMKilled контейнера

Важливо заздалегідь домовитися про терміни, бо вони схожі, а наслідки — різні. Коли Java пише java.lang.OutOfMemoryError, це внутрішня подія JVM: сама JVM розуміє, що не може виділити памʼять (наприклад, heap досяг -Xmx, або metaspace вперся у свій ліміт), і викидає виняток. В ідеальному світі ви отримуєте стек-трейс, логи, іноді навіть обробку на рівні застосунку, хоча зазвичай це все одно погано закінчується.

OOMKilled — це зовнішня подія контейнера. Вона не обовʼязково супроводжується охайним Java-логом. Процес можуть «прибити» посеред життя, і максимум, що ви побачите, — обірваний лог. Якщо вам пощастить, ви побачите в Docker-стані, що контейнер було вбито через OOM.

Щоб це стало максимально конкретним, давайте порівняємо ці два сюжети на рівні «що ви реально побачите».

Як виглядає OutOfMemoryError у логах

Якщо застосунок упав сам, за логами зазвичай видно, що він устиг сказати бодай щось осмислене. Наприклад, може бути такий фрагмент:

java.lang.OutOfMemoryError: Java heap space
  at com.example.catalog...

Контейнер при цьому завершиться, але в Docker це не буде «OOMKilled». Це схоже на ситуацію «двигун заглох, і машина зупинилася»: неприємно, але бодай зрозуміло по звуку, що сталося.

Як виглядає OOMKilled за симптомами

За OOMKilled лог може обірватися різко. Наприклад, ви бачите звичайні робочі повідомлення, а потім… нічого. Ні «Exception», ні «Shutting down». Ніби хтось висмикнув вилку з розетки, що в якомусь сенсі недалеко від правди.

І ось тут головний трюк діагностики: ви перестаєте довіряти лише логам і починаєте читати стан контейнера.

3. Де дивитися state контейнера в Docker

Зараз буде важлива думка: контейнер — це процес. А в процесу є дві ключові форми «документа»: що він встиг сказати (логи) і що про нього знає середовище запуску (state). Якщо ви читаєте лише одне джерело, ви часто бачите лише половину картини й домислюєте другу фантазією. А фантазія — відомий конкурент логів за звання «головне джерело правди», і зазвичай вона перемагає в найневдаліший момент.

Docker дає нам змогу дивитися на контейнер «ззовні» через docker ps, docker ps -a і, особливо, docker inspect. Там є блок .State, який можна вважати майже «паспортом смерті» контейнера: чим він завершився, коли завершився, чи був убитий через OOM, який exit code тощо.

Нам потрібні три прості команди, які разом дають достатньо інформації, щоб перестати гадати.

docker logs: що встиг сказати застосунок

# Дивимося, що застосунок встиг вивести в stdout/stderr до смерті
docker logs catalog-service

Ця команда відповідає на запитання: «Чи мав застосунок сили пояснити, що він помирає?» Якщо ви бачите явну причину — чудово. Якщо логи обірвалися, це теж сигнал, просто іншого типу.

docker ps -a: контейнер живий, зупинений чи перезапускався

# Дивимося контейнер, навіть якщо він уже зупинений (без -a він може не показатися)
docker ps -a --filter "name=catalog-service"

Це вже інший рівень: «контейнер узагалі ще існує?» і «в якому він статусі?». Для новачка важливий момент: docker ps без -a показує лише запущені, а питання «чому воно не працює» часто починається з того, що контейнер уже давно не запущений.

docker inspect: стан контейнера

Найкорисніший «швидкий перегляд» стану:

# Швидко витягуємо JSON зі стану контейнера: ExitCode, OOMKilled, StartedAt/FinishedAt тощо
docker inspect --format '{{json .State}}' catalog-service

Ця команда виводить JSON зі станом контейнера. Там зазвичай є поля на кшталт Status, Running, ExitCode, OOMKilled, Error, StartedAt, FinishedAt. Нам не потрібно ставати археологами Docker JSON — досить навчитися дивитися на 3–4 поля.

Якщо хочеться зовсім «точково», можна витягнути окремі поля:

# Точково виводимо лише найкорисніші поля для діагностики
docker inspect --format 'ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Status={{.State.Status}}' catalog-service

4. Exit codes і сигнали: 137 і SIGKILL

Якщо ви раніше не стикалися з exit codes, їх легко сприйняти як якесь випадкове число. Насправді це цілком системна річ, просто вона часто виглядає як шифр, поки ви не знаєте легенду. А легенда така: Linux-процеси можуть завершуватися самі й повертати exit code, а можуть бути завершені сигналом. Тоді exit code часто виглядає як 128 + номер_сигналу.

Найчастіший «контейнерний детектив» виглядає так: ви бачите Exited (137) і думаєте: «Ага, OOMKilled». І ось тут можна помилитися, бо 137 означає «процес убили SIGKILL (9)», а SIGKILL може прилетіти не лише в OOM-сценарії. Наприклад, Docker може надіслати SIGKILL, якщо ви виконали docker stop, але процес не завершився за таймаут, і Docker «добив» його. Це зовсім інший сюжет, і лікується він інакше.

Щоб тримати це в голові, зручно мати маленьку табличку-нагадування: не як «вчити напамʼять», а як «розпізнавати знайомі обличчя».

Що ви бачите Що це часто означає Що перевірити, щоб не помилитися
ExitCode=0 Застосунок завершився нормально Чому він узагалі завершився? Він не мав працювати довше?
ExitCode=1 (або інший невеликий код) Застосунок сам упав з помилкою docker logs, там зазвичай є причина
ExitCode=137 Процес убитий SIGKILL Дивіться .State.OOMKilled у docker inspect
ExitCode=143 Зазвичай SIGTERM (мʼяка зупинка) Часто це результат docker stop за коректного shutdown

Головна мораль: exit code — це «улика», але не «вирок». Вирок виносить звʼязка: exit code + OOMKilled + логи.

5. Діагностика за симптомами: логи + state

Зараз зберемо це в єдиний «маршрут читання», який ви можете повторювати щоразу, коли контейнер раптово помер. Я спеціально не буду оформлювати це як чекліст із двадцяти пунктів, їх ніхто не читає в реальності. Краще дам стійку послідовність думок: ви читаєте не команди, а причинно-наслідковий звʼязок.

Спочатку ви дивитеся логи, бо це най«людськіше» джерело: якщо застосунок помер сам, він зазвичай залишає записку. Потім ви дивитеся статус контейнера, бо іноді застосунок нічого не встиг сказати, а контейнер уже мертвий. І нарешті ви дивитеся inspect, бо там Docker чесно скаже, чи було OOMKilled, який був exit code і коли все закінчилося.

Щоб це зафіксувати візуально, ось маленька блок-схема:

flowchart TD
    A["Контейнер «не працює»"] --> B["docker ps -a: він живий чи вже Exited?"]
    B -->|Running| C["docker ps: є (healthy/unhealthy)?"]
    C --> D["docker logs: є помилки, timeout, stack trace?"]
    B -->|Exited| E["docker logs: є «останні слова»?"]
    E --> F["docker inspect .State: ExitCode + OOMKilled"]
    F -->|OOMKilled=true| G["Це memory limit / загальний memory budget"]
    F -->|OOMKilled=false| H["Це crash/stop/kill іншого типу"]

Тут ключовий момент у вузлі docker inspect: саме там ви не «вгадуєте», а перевіряєте факт. Якщо OOMKilled=true, це майже залізобетонно каже: процес убили через памʼять. Якщо OOMKilled=false, а exit code 137, значить вас убили SIGKILL, але причина не обовʼязково в памʼяті.

6. Три характерні картини «контейнер помер»

Зараз я покажу три «портрети» проблем, які в житті часто виглядають однаково, тобто просто як «не працює», але читаються по-різному.

Застосунок упав через помилку конфігурації або коду

Якщо застосунок падає сам, він зазвичай робить це «виховано»: пише стек-трейс або хоча б повідомлення. Для ілюстрації візьмемо штучний приклад помилки застосунку:

import java.lang.IllegalStateException;

public class DemoValidation {

    // Проста валідація: перевіряємо вхідні параметри ДО того, як вони потраплять у бізнес-логіку
    static void validatePort(int port) {
        // Важливо: порт має бути додатним (у цьому прикладі спрощено)
        if (port < 1) {
            // Явно падаємо зі зрозумілою причиною — це буде видно в логах контейнера
            throw new IllegalStateException("Некоректний порт: " + port);
        }
    }

    public static void main(String[] args) {
        // Демонстрація: передаємо завідомо неправильне значення й отримуємо передбачуване аварійне завершення
        validatePort(0); // BOOM
    }
}

У контейнерному світі це майже завжди виглядає так: docker logs показує зрозумілий текст помилки, контейнер Exited, OOMKilled=false. Exit code частіше невеликий, зазвичай 1. Лікується це не памʼяттю, а виправленням конфігурації або коду.

JVM упала через OutOfMemoryError

Тут docker logs зазвичай показує OutOfMemoryError. Контейнер завершився, але OOMKilled=false, бо це JVM «здалася», а не контейнер її «прибив». Такий сценарій інколи лікується збільшенням -Xmx, інколи — зменшенням навантаження, інколи — виправленням витоку. Але в межах нашого курсу важливо інше: це не OOMKilled.

І так, тут легко сплутати, бо слово «OutOfMemory» звучить дуже схоже. Різниця в тому, хто промовив це слово: JVM всередині чи Docker зовні.

Контейнер OOMKilled через memory limit

А ось тут цікавіше. Ви можете побачити, що контейнер завершився з ExitCode=137. Логи обірвалися різко. І найважливіше: docker inspect покаже OOMKilled=true.

Мініприклад команд, якими це читається:

# 1) Спочатку дивимося, чи встиг застосунок сказати щось у логах
docker logs catalog-service

# 2) Потім перевіряємо state контейнера: ExitCode (як завершилося) і OOMKilled (чому могло бути вбито)
docker inspect --format 'ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}}' catalog-service
# ExitCode=137 OOMKilled=true (приклад)

Це та ситуація, коли «лікувати логами» марно: процес убили, він не встиг пояснитися. Ваше головне джерело істини — стан контейнера.

7. OOMKilled vs SIGKILL з інших причин

Ця частина здається нудною, доки одного разу не потрапите в пастку: ви бачите Exited (137) і починаєте крутити памʼять, а насправді контейнер було вбито не через OOMKilled, а тому, що ви його зупинили, він не завершився за timeout, і Docker застосував «молоток» (SIGKILL).

Як відрізнити? Рівно так само, як ми вчилися відрізняти все інше: не гадати, а дивитися .State. Якщо OOMKilled=false, то це не OOMKilled, навіть якщо exit code 137. І далі ви вже згадуєте лекції про життєвий цикл контейнера та коректний shutdown: можливо, процес не ловить сигнал, можливо, ENTRYPOINT написаний у неправильній формі (shell-form), можливо, застосунку потрібно більше часу на graceful shutdown. Але найважливіше — ви не лікуєте не ту хворобу.

Схожа історія буває, коли контейнер «помирає» через ручний docker kill (який теж шле SIGKILL) або через дії оточення. Усюди логіка одна: ExitCode каже «як», OOMKilled часто каже «чому», а логи допомагають зрозуміти, що відбувалося до цього.

8. Діагностика на нашому сервісі

Усе це не абстракція, а нормальна операційна практика навколо вашого навчального Spring Boot-сервісу.

Наш docker-java-catalog-service у контейнері — це рівно один Java-процес. Якщо контейнер упав, значить упав або був убитий саме він. Тому діагностика завжди починається однаково: читаємо docker logs, перевіряємо docker ps -a, потім дивимося .State через docker inspect. І лише після цього робимо висновок: «це OOMKilled» або «це crash іншого типу». Таке розділення економить вам час, нерви й майбутні хаотичні коміти в дусі «збільшив усе, що можна, раптом допоможе».

9. Типові помилки під час діагностики OOMKilled

Помилка № 1: робити висновок «це OOMKilled» лише за Exited (137).
137 справді часто трапляється в OOM-сценаріях, але сам по собі він означає лише SIGKILL, а SIGKILL може прилетіти з різних причин. Щоб не лікувати не ту проблему, завжди перевіряйте .State.OOMKilled через docker inspect. Якщо там false, це не OOMKilled, навіть якщо число 137 виглядає дуже переконливо.

Помилка № 2: читати лише логи й ігнорувати стан контейнера.
Логи чудові, коли застосунок помер «культурно» й устиг написати стек-трейс. Але в OOMKilled-сценарії лог часто обривається без пояснень, бо процес убивають жорстко. Якщо ви продовжуєте шукати причину лише в логах, ви буквально намагаєтеся отримати інформацію від процесу, який уже не має права голосу. Docker state тут важливіший.

Помилка № 3: плутати JVM OutOfMemoryError і контейнерний OOMKilled.
Обидві історії про памʼять, але це різні рівні. OutOfMemoryError — внутрішня проблема JVM, і за логами зазвичай видно, що JVM сама так вирішила. OOMKilled — зовнішнє вбивство процесу через memory limit, і ключовий маркер — .State.OOMKilled=true. Якщо змішати ці два сюжети, ви без кінця «чинитимете heap», хоча проблема може бути в загальному memory budget контейнера.

Помилка № 4: намагатися «полагодити OOMKilled» збільшенням -Xmx без розуміння ліміту контейнера.
Це майже гарантований спосіб зробити гірше, бо контейнерний ліміт обмежує всю памʼять процесу, а не лише heap. Якщо ви впритул посунете heap до ліміту, ви просто залишите нуль місця для metaspace і native memory, а потім дивуватиметеся, чому все стало падати ще швидше. Спочатку зʼясовуйте зовнішній ліміт контейнера та факт OOMKilled, а вже потім думайте про heap.

Помилка № 5: не відрізняти «контейнер помер» від «контейнер живий, але unhealthy».
Інколи сервіс не відповідає, і здається, що він «упав». Але контейнер може продовжувати працювати, просто healthcheck показує unhealthy. Це інший клас проблеми: найімовірніше, сервіс живий, але не готовий обслуговувати запити. У такому разі docker ps і health-статус дадуть більше користі, ніж спроби шукати OOMKilled.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ