JavaRush /Курсы /Docker for Spring /Readiness вместо sleep

Readiness вместо sleep

Docker for Spring
17 уровень , 3 лекция
Открыта

1. Пишем sleep 20 по инерции

Правильная модель у нас уже есть: PostgreSQL сама объявляет readiness через healthcheck, а app стартует по condition: service_healthy. Именно здесь очень легко свернуть в соблазнительный shortcut: “ну база же стартует чуть дольше, давайте просто подождём 20 секунд и поедем дальше”.

Здравое зерно тут есть: проблема правда упирается во время. Но fixed-delay всё равно бьёт мимо. Нам нужно не угадать число секунд, а понять, когда зависимость действительно готова.

Ещё sleep кажется привлекательным потому, что он не требует “разбираться в Compose”. Он как пластырь: быстро наклеил — и вроде кровь не видно. Увы, в backend-инфраструктуре пластырь часто приклеивается поверх датчика давления, и вы перестаёте видеть, что пациент вообще-то всё ещё не очень живой.

Вот как обычно выглядит такой “ремонт” прямо в compose.yaml (плохой пример, но жизненный):

services:
  app:
    # Остальные поля сервиса опущены: здесь важен именно anti-pattern запуска
    entrypoint: ["sh", "-c", "sleep 20 && exec java -jar /app/app.jar"] # Shell-обёртка + таймер = гадание

На первый взгляд всё красиво: “подождали — запустились”. На второй взгляд — мы только что заменили инженерный сигнал готовности на гадание “20 секунд должно хватить”.

2. sleep измеряет время, а readiness — состояние

Чтобы не воевать со своей же инфраструктурой, полезно проговорить вслух простую мысль: readiness — это не “прошло N секунд”, readiness — это “сервис действительно готов делать то, что от него ждут”. Для PostgreSQL в нашем контексте это означает хотя бы одно: она принимает подключения и отвечает так, как ожидает клиент.

Docker Compose в базовой модели не ждёт готовности “по смыслу”; при старте он ориентируется на то, что контейнер просто запущен (running), и этого недостаточно для баз данных и других зависимостей. Именно поэтому попытка “лечить” проблему временем выглядит естественно: Compose сам по себе вам не даёт “ждать готовность”, если вы не добавили соответствующий механизм.

sleep — это fixed-delay ожидание. Оно не проверяет состояние базы, не может “проснуться раньше”, если база уже готова, и не может “подождать дольше”, если база стартует медленнее. Он просто вычитает из вашей жизни 20 секунд (или 60, или 120 — в зависимости от степени отчаяния).

Если выразить разницу почти математически, получится так:

  • sleep 20 отвечает на вопрос: “прошло ли 20 секунд?”
  • readiness отвечает на вопрос: “может ли PostgreSQL сейчас принять соединение?”

И вот это второй вопрос, который на самом деле волнует Spring Boot приложение.

3. Fixed-delay: слишком много и мало

Fixed-delay ломается в обе стороны, и это одна из причин, почему он так коварен. На быстрой машине база может быть готова за 23 секунды, а вы каждый раз будете дисциплинированно стоять и смотреть на sleep 20, как на загрузку игры в 2005 году.

Схематично это выглядит так:

sequenceDiagram
    %% Демонстрация fixed-delay: мы не проверяем состояние, просто ждём "на удачу"
    participant DB as postgres
    participant App as app

    Note over DB: База готова за ~3 сек
    App->>App: sleep 20 сек
    Note over App: 17 сек — ожидание "на всякий случай"
    App->>DB: попытка подключиться
    DB-->>App: OK

На медленной машине (или при “холодном” старте, или когда диск занят чем-то тяжёлым) PostgreSQL может стартовать дольше, и тогда sleep 20 не помогает вообще. Вы получили задержку + всё ту же ошибку. Причём хуже всего то, что вы теперь ждёте дольше, чтобы увидеть ту же проблему. И дальше начинается самая опасная игра: “а давай 30… а давай 45… а давай 60…”.

Типичная картина в логах при недостаточном ожидании выглядит примерно так (упрощённо):

postgres-1 | database system is starting up
app-1      | org.postgresql.util.PSQLException: Connection to postgres:5432 refused
app-1      | Application run failed

И обратите внимание: sleep не делает ошибку понятнее. Он лишь меняет момент, когда вы её увидите.

4. Нестабильность и воспроизводимость

Самое неприятное в sleep — он делает локальную среду “флаки”: сегодня завелось, завтра не завелось, послезавтра “у меня работает, у тебя нет”. А Docker и Compose мы как раз заводим, чтобы избавиться от этой магии и сделать окружение воспроизводимым.

Время старта PostgreSQL — это не константа. На него влияет куча вещей, и вам не нужно становиться DBA, чтобы это принять. Достаточно одного факта: контейнер может стартовать на разных машинах, в разных условиях нагрузки, с разным состоянием тома, и “один и тот же” sleep 20 вдруг перестаёт быть “тем же”.

Есть ещё чисто командная боль: onboarding. Новый разработчик клонирует репозиторий, запускает docker compose up, видит падение приложения, и первая мысль — “я что-то не так настроил”. Он начинает трогать SPRING_DATASOURCE_URL, профили, порты, хотя проблема вообще не в конфигурации, а в том, что вы поставили таймер, который иногда “не угадывает”.

Самое обидное, что sleep создаёт иллюзию решения. Он может “помочь” 9 раз из 10, и это хуже, чем если бы он не помогал никогда. Потому что он приучает команду думать: “ну Compose такой, надо просто подождать”. А потом этот же паттерн перекочёвывает в другие сервисы, и в какой-то момент вы обнаруживаете, что ваш проект стартует 3 минуты, потому что у каждого сервиса есть “на всякий случай” свой sleep.

5. Диагностика и маскировка причин

Важный побочный эффект fixed-delay — он ухудшает ваш цикл обратной связи. Без sleep вы получили ошибку за 23 секунды, быстро поняли “проблема на старте”, проверили логи базы и пошли исправлять модель readiness. Со sleep вы ждёте 20 секунд (или 60), чтобы получить ту же ошибку. Это уже не “быстрый костыль”, это “костыль с таймером”.

Ещё коварнее становится, когда проблема вообще не в readiness. Представьте, что у вас неверный пароль, или база реально не поднимается, или вы случайно указали не тот host. sleep не выявляет это, он просто откладывает момент истины. И новичок вполне может сделать неправильный вывод: “База просто долго стартует, увеличу ожидание”. А в реальности база может быть хоть мгновенной — пароль всё равно неверный.

Есть и UX-уровень внутри Docker: контейнер app будет в статусе Up, но сервис ещё не запущен — он спит. Снаружи это выглядит как “контейнер жив, но порт не отвечает”. И вы снова возвращаетесь в состояние “что-то магически не так”, хотя на самом деле вы сами встроили “магическую паузу” в запуск.

6. Shell-обёртка и проблемы PID 1

Для Java-процесса в контейнере baseline простой: запуск должен быть максимально прямым и прозрачным. Мы избегаем лишней shell-магии, потому что она ломает доставку сигналов, усложняет graceful shutdown и превращает процесс запуска в мини-скрипт, который живёт своей жизнью.

А теперь смотрите, что делает sleep в Compose. Чтобы написать “подожди и потом запусти”, люди почти всегда используют что-то вроде:

entrypoint: ["sh", "-c", "sleep 20 && exec java -jar /app/app.jar"]

То есть мы возвращаемся к модели “у нас есть shell, который что-то делает, а потом запускает Java”. С точки зрения контейнера это лишний посредник. И это не просто “неаккуратно”: в один прекрасный день вы начнёте ловить странности с остановкой контейнера, с сигналами, с тем, как процесс завершается, и будете вспоминать, почему прямой запуск Java-процесса вообще считался базовым правилом.

Да, можно написать exec java внутри shell-команды, можно пытаться аккуратно прокинуть сигналы, можно добавить обработчики — но, во‑первых, это уже сценарий “мы пишем мини-init систему”, а во‑вторых, вам всё равно нужно решить главный вопрос: “а откуда мы знаем, что база готова?” Shell-скрипт со sleep это не решает.

7. sleep в compose.yaml: путь к хаосу

Часто sleep появляется сначала как невинная строчка, а потом начинает разрастаться. Например, кто-то добавляет ещё одну проверку, потом ещё одну, потом дополнительные переменные, а потом выясняется, что “а давайте сделаем по-разному для Windows и Linux”. И вот у вас уже не compose.yaml, а “список шаманских ритуалов, чтобы оно завелось”.

Чаще всего это заканчивается тем, что shell-обёртка въезжает прямо в entrypoint и подменяет канонический способ запуска приложения. Выглядит “более серьёзно”, но по сути ничего не меняется: вы всё ещё угадываете готовность временем, а shell по‑прежнему становится участником старта Java.

Проблема здесь двойная. Во-первых, вы меняете способ запуска приложения на уровне окружения, и он перестаёт совпадать с тем, что вы считаете каноном внутри Dockerfile. Во-вторых, вы снова пытаетесь угадывать готовность временем. У вас всё ещё нет сигнала “PostgreSQL действительно готова”, есть только “мы честно подождали”.

В итоге sleep чаще всего приводит к тому, что у команды появляется “свой правильный sleep”. У одного — 10 секунд, у другого — 30, у третьего — “я вообще просто перезапускаю Compose второй раз, оно тогда заводится”. И это как раз то, ради чего Compose запускали: чтобы не было такого “народного фольклора”.

8. Readiness через healthcheck и depends_on

Самая приятная новость: вам не нужно изобретать ожидание вручную. Возвращаться нужно не к “ещё одному способу подождать”, а к уже собранному канону: readiness объявляется у postgres, а app стартует только после service_healthy.

Минимальный скелет (без всех остальных настроек) выглядит примерно так:

services:
  app:
    depends_on:
      postgres:
        condition: service_healthy # Стартуем app только когда postgres стал healthy

  postgres:
    healthcheck:
      # Значения должны совпадать с POSTGRES_USER и POSTGRES_DB у сервиса postgres
      test: ["CMD", "pg_isready", "-U", "catalog", "-d", "catalog"]

Остальные поля сервисов остаются прежними; здесь важна сама readiness-связка.

Здесь сразу видно, что ожидание — не догадка. Это контракт: healthcheck базы проверяет готовность, а app привязан к “здоровью” postgres. И это не просто красивее, это наблюдаемо: в docker compose ps вы увидите healthy, а не “мы где-то в недрах sleep-скрипта ещё не проснулись”.

Плюс у такого подхода есть инженерная честность: если база не становится готовой, вы это увидите явно (healthcheck не проходит), а не получите ситуацию “приложение стартует, падает, стартует, падает, но зато мы подождали 60 секунд”.

9. Типичные ошибки при использовании sleep

Даже если после сегодняшней лекции вы твёрдо решили “никогда больше sleep 20”, реальность иногда пытается подсунуть его обратно — обычно под видом “ну это же просто временно”. Важно заранее знать, какие ошибки чаще всего встречаются, чтобы узнавать их по запаху (в программировании это называется “code smell”, но по сути — “пахнет костылём”).

Ошибка №1: увеличить sleep, не проверив, что проблема вообще в readiness.
Если приложение падает из-за неверного SPRING_DATASOURCE_URL, неправильного пользователя или пароля, sleep не поможет. Он только заставит вас ждать дольше перед тем, как увидеть ту же ошибку. Правильнее сначала научиться отличать “не готово” от “неправильно настроено” по логам базы и приложения.

Ошибка №2: прятать sleep в “каноническую” команду запуска, чтобы никто не заметил.
Иногда sleep запекают в entrypoint, в скрипт или даже в Dockerfile, чтобы “всем было удобно”. Это превращает обходной путь в стандарт команды. Потом никто уже не помнит, зачем это было сделано, но все продолжают платить задержкой и нестабильностью.

Ошибка №3: использовать sh -c ради sleep и случайно сломать корректный жизненный цикл Java-процесса.
В контейнере важно, чтобы Java была главным процессом, нормально получала сигналы остановки и корректно завершалась. sleep почти всегда тянет shell-цепочку, и вы незаметно откатываетесь назад к проблемам PID 1 и “почему приложение странно останавливается”.

Ошибка №4: оставить sleep “на всякий случай” даже после того, как readiness уже реализован через healthcheck.
Это выглядит безобидно, но на практике портит UX: стек начинает стартовать медленнее без причины. А главное — вы перестаёте доверять собственным сигналам готовности. Если healthcheck есть, то либо он отражает готовность, либо его надо исправлять. Дублировать его таймером — значит признать, что healthcheck “для красоты”.

Ошибка №5: считать, что sleep — это и есть “контроль стабильности”.
sleep не делает систему стабильной; он делает её менее предсказуемой, потому что скрывает реальную причину нестабильности. Стабильность появляется тогда, когда стартовые условия выражены как явные проверки состояния (readiness), а не как “подождём и надеемся”.

1
Задача
Docker for Spring, 17 уровень, 3 лекция
Недоступна
Лишняя задержка из-за fixed delay
Лишняя задержка из-за fixed delay
1
Задача
Docker for Spring, 17 уровень, 3 лекция
Недоступна
Убрать `sleep` и вернуть нормальный запуск Java-процесса
Убрать `sleep` и вернуть нормальный запуск Java-процесса
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ