Перейти к содержанию

Разбор инцидента: первая версия сервиса не запускалась

Разбор настоящий: это первая выкладка приложения на стенд, и она не удалась. Времена — по журналу узла, московские.

Кратко

Первая версия сервиса не смогла запуститься: контейнер падал сразу после старта и уходил в цикл перезапусков. Внешнего влияния не было — сервис ещё не был опубликован. Разбирательство заняло 34 минуты, из них 20 ушло на неверную гипотезу.

Длительность 34 минуты
Влияние на посетителей нет: адреса ещё не были опубликованы
Первопричина точка входа образа не соответствовала способу распаковки слоёв
Что помогло сообщение исключения оказалось точным
Что помешало вера в то, что канареечный релиз спасёт от любой плохой версии

Как это выглядело

15:07. Применены манифесты приложения. Argo Rollouts создаёт поды новой версии.

15:09. Поды в состоянии CrashLoopBackOff. В журнале контейнера:

Error: Could not find or load main class org.springframework.boot.loader.launch.JarLauncher
Caused by: java.lang.ClassNotFoundException: org.springframework.boot.loader.launch.JarLauncher

15:11. Первая гипотеза — не та версия Java или не тот базовый образ. Двадцать минут ушло на её проверку: сборка образа заново, сверка версий, запуск контейнера вручную. Гипотеза оказалась неверной.

15:31. Проверено содержимое образа изнутри:

buildah run <образ> -- ls -l /app

В каталоге лежит metroskop.jar и распакованные слои lib/, application/. Точка входа при этом вызывала JarLauncher — класс, который живёт внутри неразобранного архива Spring Boot. После распаковки слоёв его там уже нет.

15:35. Точка входа заменена на прямой запуск архива, в pom.xml закреплено итоговое имя файла, чтобы оно не зависело от версии проекта.

15:38. Новый образ собран, поды поднимаются, проверка готовности проходит.

15:41. Обнаружено, что старые поды всё ещё числятся в релизе: контроллер ждал, пока станет здоровой версия, которой не суждено было запуститься. Релиз пересоздан, после чего поды пришли в рабочее состояние.

Первопричина

Многоэтапная сборка распаковывает архив Spring Boot по слоям — так изменения приложения не тянут за собой переслойку зависимостей, и образ обновляется быстрее. Но после распаковки запускать нужно не встроенный загрузчик архива, а сам архив: загрузчик — часть неразобранной упаковки.

Ошибка формально была в одной строке ENTRYPOINT, а по сути — в переносе рецепта из статьи без проверки того, что получилось внутри образа.

Почему обнаружение заняло столько времени

Сообщение об ошибке было точным и указывало прямо на причину. Двадцать минут были потеряны потому, что первая гипотеза звучала правдоподобнее, чем читалась ошибка: «не та Java» — знакомое объяснение, ClassNotFoundException про загрузчик Spring Boot — незнакомое. Правильным первым шагом было посмотреть, что лежит в образе, а не пересобирать его.

Второй урок, менее очевидный

Канареечный релиз с автоматическим откатом не помог — и не мог. Откатываться было некуда: работающей прежней версии не существовало, это была первая выкладка. Контроллер добросовестно ждал, пока новая версия станет здоровой, и удерживал релиз в переходном состоянии.

Канарейка защищает от плохой новой версии при наличии работающей старой. Она не защищает от первой выкладки, и это не её недостаток — просто её граница, которую стоит знать заранее.

Что изменено после разбора

Действие Состояние
Точка входа образа исправлена, имя архива закреплено в сборке сделано
В scripts/deploy.sh добавлена сухая сборка манифестов до применения сделано
В инструкцию дежурного добавлен раздел «Под не запускается»: сначала журнал контейнера и содержимое образа, потом гипотезы сделано
Проверка запуска образа в конвейере до выкладки не сделано: требует запуска контейнера с базой, а на узле нет памяти под ещё один экземпляр PostgreSQL

Последний пункт оставлен незакрытым сознательно. Записанное и неисполненное действие честнее молчаливо пропущенного: следующий человек увидит, что вопрос рассматривался, и поймёт, чего это стоило бы.