Перевод статьи Дена Бравендера Simple 0-Downtime Blue Green Deployments
Поработав в шести e-commerce проектах (половина из которых зарабатывает миллионы долларов год), я могу с уверенностью сказать, что периодическая неработоспособность серверов - это железобетонный способ развалить бизнес любой компании. Как ни крути, время – деньги. Я работал с командами которые пытались минимизировать время даунтайма во время релиза множеством различных способов. Вот некоторые из них:
На одном конце спектра вы можете избегать простоя серверов, выполняя деплой только во время техобслуживания. Недостатки здесь довольно очевидны – что если релиз содержит ошибку и вы не узнаете об этом до тех пор, пока на сайт не пойдет пиковый траффик? Я видел людей, вскидывающих руки со словами “Я думаю, что наши пользователи не смогут использовать новую функциональность и будут получать ошибку, пока мы не выкатим исправление завтра утром” в магазинах, где деплоились так. Я так же наблюдал, как сайт над которым я работал был “выключен” для экстренного деплоя и мы были завалены жалобами от клиентов.
На другом конце спектра я видел попытки blue/green phoenix деплоя - переустановка на каждой виртуальной машине с таким же ПО, но с новой версией проекта. После окончания тестирования на новых виртуальных машинах вы можете изменить настройки аппаратного свитча или сервиса, вроде HAProxy, что бы он указывал на новую версию сайта. Само собой разумеется, используя этот метод занимает очень много времени, если все, что вы хотите сделать, это развернуть исправление одной строки. Если вы не знакомы с blue/green деплоем, обязательно прочтите статью Мартина Фаулера об этом.
Существует золотая середина в решении этой проблемы, при которой сайт продолжает работать и выкатка не занимает так много времени, как blue/green phoenix деплой. Тем не менее, как и со всеми техническими решениями, она не лишена своих недостатков и не везде применима.
Вот тривиальное Flask-приложение, которое я буду развертывать для примера:
1 2 3 4 5 6 7 8 9 10 |
|
А вот fabfile, который мы будем использовать:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
Обновления в развертывании должны быть идемпотентными (то есть, если вы запускаете деплой несколько раз, то результат должен быть таким же, каждый раз, за исключением pid-ов запускаемых процессов). Здесь есть один тонкий момент - когда используете git
для деплоя, вам нужно очистить удаленную рабочую копию репозитория. Я не делал этого здесь, но вы можете использовать команду git clean
, чтобы быть уверенным, что в рабочей копии только то, что в репозитории. Я сделал этот пример для python-приложения, но вы можете использовать любой язык, который не требует компиляции в бинарный файл и и у которого есть возможность установки изолированных пакетов. Я предполагаю, что это может быть сделано с ruby
и RVM. У меня также есть пример на nodejs в репозитории gitric.
Структура каталогов, куда будет деплоиться проект выглядит следующим обазом:
1 2 3 4 5 6 7 8 9 10 |
|
Для того, чтобы сделать первичное развертывание все что вам нужно это пользователь на удаленном сервере и конфигурация хостов в Nginx
вроде такой:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
После этого вы можете запустить это:
1 2 |
|
Эти шаги намеренно разделены, что бы вы могли проверить работоспособность проекта перед тем как переключиться на новый релиз.
Здесь я переключаюсь на новый релиз во время работы ab
и постоянного обращения к серверу с помощью curl
что бы видеть, что возвращает сервер:
% ab -c 100 -n 5000 http://my.server.here/
This is ApacheBench, Version 2.3 <$Revision: 1528965 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking my.server.here (be patient)
Completed 500 requests
Completed 1000 requests
Completed 1500 requests
Completed 2000 requests
Completed 2500 requests
Completed 3000 requests
Completed 3500 requests
Completed 4000 requests
Completed 4500 requests
Completed 5000 requests
Finished 5000 requests
Server Software: nginx/1.4.1
Server Hostname: my.server.here
Server Port: 80
Document Path: /
Document Length: 28 bytes
Concurrency Level: 100
Time taken for tests: 33.180 seconds
Complete requests: 5000
Failed requests: 2576
(Connect: 0, Receive: 0, Length: 2576, Exceptions: 0)
Total transferred: 922576 bytes
HTML transferred: 142576 bytes
Requests per second: 150.69 #/sec (mean)
Time per request: 663.607 ms (mean)
Time per request: 6.636 ms (mean, across all concurrent requests)
Transfer rate: 27.15 Kbytes/sec received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 164 326 87.5 321 1393
Processing: 161 308 188.7 284 4045
Waiting: 161 307 186.5 284 4045
Total: 338 635 216.9 646 4409
Percentage of the requests served within a certain time (ms)
50% 646
66% 675
75% 689
80% 699
90% 723
95% 758
98% 789
99% 899
100% 4409 (longest request)
Мой сервер - маленькая виртуалка на Linode (хостинг) и я нахожусь на другой стороне Земли от него, так что я не очень обеспокоен его производительностью. Я проверяю, что все запросы были обработаны, пока осуществлялся деплой, и приложение продолжало работать. Вы можете увидеть, что ab
насчитал 2576 запросов с ошибочной длинной (failing length requests) - на самом деле это не ошибки - ab
считает, что если содержимое ответа сервера отличается от первоначального ответа, то это ошибка; на середине нагрузочного тестирования я переключился на новый релиз.
% for x in $(seq 100); do curl -s -S http://my.server.here/ && echo; done
Hello 0-downtime blue World!
Hello 0-downtime blue World!
Hello 0-downtime blue World!
Hello 0-downtime blue World!
Hello 0-downtime blue World!
Hello 0-downtime blue World!
Hello 0-downtime blue World!
Hello 0-downtime green World!
Hello 0-downtime green World!
Hello 0-downtime green World!
Hello 0-downtime green World!
Hello 0-downtime green World!
Hello 0-downtime green World!
Hello 0-downtime green World!
Бонусом идет эффективное использование перезагрузки, предоставляемое большинством веб-серверов (Apache
, Nginx
). Текущие процессы веб-сервера перестаю обрабатывать новые запросы, а новые созданные процессы веб-сервера направляют весь траффик на новую версию проекта. Вот лог моего веб-сервера сразу после переключения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Процесс Nginx с pid 29381 (помеченый как “nginx: worker process is shutting down”) обрабатывает старый запрос, направляя его на старый релиз, и будет завершен после окончания обработки. Запрос, который поступил после выкатки направляется на порт 8888 (новый релиз). Все последующие запросы будут поступать на новые процессы nginx-a, которые передадут их на 8888-й порт. Вот так nginx выполняет мягкую перезагрузку, но для использования этого метода нет необходимости понимать как это происходит.
Использование git
для развертывания кода для языков, не требующих сборки, вроде Python и Руби, сокращает время, необходимое для создания пакетов и развертывания. Я писал об этом несколько лет назад. Совместное испольование с blue/green развертывания на том же сервере, привело к очень приятному опыт развертывания для меня и моей команды за прошедший год-полтора. Наш процесс деплоя был последовательным, но когда наш парк серверов вырос, процесс развертывания не стал медленнее, так как мы начали использовать декоратор @parallel
во время фазы обновления.
Дополнительно требуется немного планирования для написания кода и миграций, которые могут быть развернуты без выключения сервиса, но со времнем, экспериментируя, вы поймете что совершаете не так уж и много допонительной работы. Вот классное видео от команды Disqus. Вам нужно добавить префикс в виде короткой git-ссылки для ключей в memcache и прогреть кеш перед переключением. Для Postgres можно без проблем создавать новые таблицы и даже новые столбцы с Null по умолчанию, но вы определенно захотите протестировать работспособность миграций на тестовом окружении с эмуляцией блокировки строк (если вы используете SELECT FOR UPDATE
, что бы быть уверенным в консистентности данных). Если вы испольуете фоновый процесс, вроде celery-задач, то процесс может какое-то время использовать старую версию кода, поэтому вам всегда нужно обрабатывать случай вызова старого кода:
1 2 3 |
|
Если в очереди существует задача process_order
с функцией, имеющую старую сигнатуру, то если вы не передадите появившиеся параметры; поэтому выставляйте значения по умолчанию. Это одно из множества предостережений, которое я смог придумать. Всегда проверяйте деплой и откат назад, если сомневаетесь, для того что бы обнаружить потенциальную ошибку.
Есть мноество причин, по которым вы захотите обновлять API или веб-сайт без простоя используя blue/green деплой:
-
Удовлетворение потребностей клиентов - живя и работая на другом конце света (Корея), я очень расстраиваюсь, когда разработчики сервисов, нужных мне для выполнения работы, считают, что технические работы можно проводить когда у меня разгар рабочего дня, так как у них ночь.
-
Вы можете откатить назад релиз с ошибками без переразвертывания - старый релиз по прежнему все еще там, так что вы можете переключиться на него, если у вас возникнут проблемы в новом релизе.
-
Возможность быстро исправить непредвиденные проблемы - вы можете выяснить что есть ошибка, которая не является достаточно большой, что бы возвращаться к старому релизу и вы можете выложить исправлеиние ошибки даже когда ваш сервис используют тысячи и миллионы клиентов, не прерывая их.
-
Вы на один шаг ближе к непрерывному развертыванию.
Как я говорил выше, существует множество техник, которые могут быть использованы для деплоя ПО и они имеют свои компромиссы. Пакеты уровня операционной системы нельзя обновить так же изолированно, как virtualenv
. Критики могут сказать, что это работает только для пакетов на уровне языка программирования, но не для пакетов уровня ОС или даже обновления ОС. Я полностью понимаю эту точку зрения, и я думаю, что это просто компромисс. Будущее выглядит очень ярким, когда речь идет о методах, которые обеспечивают еще большую изоляцию и более быстрое развертывание, вроде докера (Docker
) и других аналогичных проектов. Я с нетерпением жду, чтобы с помощью подобных инструментов, делать деплой проектов в будущем даже с меньшим временем простоя. В то же время эта статья написана как раз для того типа проектов над которыми я работаю сейчас.