Обновить

Как я реализовал Blue-Green деплой с нулевым даунтаймом на Docker Compose

Уровень сложностиСредний
Время на прочтение5 мин
Охват и читатели10K
Всего голосов 6: ↑6 и ↓0+6
Комментарии17

Комментарии 17

Как я понимаю, продакшен у вас - это один сервер? Даже в таком случае можно включить Docker Swarm на сервере и тогда вам будет доступен deploy режим с его фичами по плавному апдейту и роллбеку. Есть и минусы - вы сразу теряете depends_on.

Да, на одном сервере. Спасибо за ссылку, посмотрю

не использовал профили. спасибо, посмотрю

K3s поставьте поэкспериментируйте, там совсем другой уровень.

Там хоть сине зеленое, хоть канареечное развертывание можно сделать просто меняя labels у service.

Расписывать не буду, инфы море. Но для старта ваш проект хороший конечно

Спасибо, посмотрю на k3s. Надеюсь 4гб оперативки на сервере хватит)

Не хватит. У нас стартап 7 лет на 4 гигах крутится, я так на куб и не перевез. Хотя к3с только 300 метров ест.

получился вполне рабочий blue‑green деплой с нулевым даунтаймом

Но есть побочка: принудительный NACK посреди обработки может порождать непредвиденые и нетестированные состояния системы.

мы не прерываем уже выполняющиеся задачи, мы просто не принимаем новые, т.е отправляем их обратно в очередь на обработку нашему новому активному фоновому процессу

Возможно я неправильно понял вот это:

если False - переносим задачу на повторную обработку (например, в rabbitmq делаем сразу nack(requeue=true))

В каких случаях потребуется NACK ?

У нас это сделано как обёртка над consumer - проверка is_accepting происходит до начала обработки сообщения. сценарий такой:

  1. мы получаем сообщение в нашем фоновом процессе, назовем его GREEN

  2. мы сразу проверяем is_accepting

  3. если False - мы даже не заходим в бизнес логику - сразу делаем nack(requeue=true)

  4. сообщение уходит обратно в очередь и его обработает уже новый активный фоновый процесс BLUE, у которого при инициализации is_accepting=True

сам GREEN процес при этом находится в режиме graceful shutdown: он перестает принимать новые задачи, но при этом корректно завершает уже запущенные
получается, что выполняющиеся задачи никогда не прерываются - nack выполняется только к новым сообщениям

если False - мы даже не заходим в бизнес логику

Вот я про это - тогда всё пучком (у меня такой же подход).

Неплохое решение, сам что-то подобное сделал. Но, как по мне, реализацию healthcheck лучше вынести в Dockerfile, в Вашем случае может быть так HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 CMD wget -qO- http://localhost:8000/v1/health || exit 1 При этом не придётся выставлять порты наружу. Это более универсальное решение, healthcheck может быть любым и ему самое место в Dockerfile. И если что-то пойдёт не так, то не придётся прогонять все 30 итераций, цикл сразу прервётся. В самом скрипте проверять статус контейнера через docker inspect, вынес в отдельную функцию, :

wait_for_healthy_or_rollback() {  # ($1: название_контейнера)
    for ((i = 1; i <= $MAX_ATTEMPTS; i++)); do
        HEALTH_STATUS=$(docker inspect --format='{{.State.Health.Status}}' $1)

        if [[ "$HEALTH_STATUS" == "healthy" ]]; then
            echo ">>> Контейнер $1 успешно запущен ✅️"
            return 0
        fi

        if [[ "$HEALTH_STATUS" == "unhealthy" ]]; then
            echo ">>> Ошибка при запуске контейнера $1, IMAGE_TAG=$IMAGE_TAG ❌" >&2
            return 0
        fi

        echo ">>> Жизнеспособность $1: '$HEALTH_STATUS'. Следующая проверка через $DELAY сек..."

        if (( i < $MAX_ATTEMPTS )); then
            sleep "$DELAY"
        fi
    done
    echo ">>> Ошибка: превышено максимальное количество попыток ($MAX_ATTEMPTS) ❌" >&2
    return 1
}

Бонусом ко всему можно будет так же проверять, с помощью docker inspect, погасла ли старая версия контейнера. Не гадая, какие задержки нужно выставить. Запуск миграций у меня в том же скрипте, с их откатом, если что-то пошло не так.

Спасибо за идею. Думаю, переработаю подход в эту сторону

Почему бы просто не поднять воркеров с новым образом внутри и не переключить трафик на них? Если все хорошо - старые убиваются, если что-то не так - переключаешь трафик обратно.

В нашем случае “просто поднять новые воркеры и переключить трафик” проблематично, потому что система построена как event-driven DAG-пайплайн с 7-9 стадиями на каждый доменный агрегат. Т.е для обработки каждой стадии у нас есть consumer, который прослушивает определенную очередь. Это сделано для того, чтобы пользователь мог постепенно контролировать и взаимодействовать с процессом генерации.
В rabbitmq переключить трафик можно:

  1. Через bindings (создание + удаление) - сразу отметаем по понятным причинам

  2. через разные blue/green exchange + прокидывать в приложение current_deploy(blue/green) - но это может привести к такому состоянию, когда веб и воркер могут работать с разными версиями current_deploy

в любом случае в самом приложении(не на уровне инфры) мы должны будем реагировать на sigterm и ожидать выполнения текущих задач

и да, если пользователь условно отправит евент через час или день - ничего страшного не будет, его может обработать любой воркер. Т.е я к тому что мы выставляем graceful timeout как максимальное время выполнения стадии в приложении

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации