Выкатка (или deployment) новых версий Web приложений имеет ряд трудностей, т.к. необходимо быстро и одновременно выполнять группы действий на разных серверах. Процесс обычно включает в себя обновление кода (php) и статики (js/css/картинки), изменение баз данных и настроек системы.
Когда-то давно, новые версии появлялись очень редко (раз в год или даже реже). Тогда происходили сложные и длительные процессы обновления, а пользователи получали сразу огромный пакет изменений. Такой трудоемкий процесс иногда уничтожал целые бизнесы. Сейчас понятие новой версии минимизировано до малейших изменений. Динамика разработки современных приложений огромная, а выкатки обновлений могут происходит каждый день.
Поэтому к процессу выкатки добавился целый ряд требований:
- Минимум (лучше ноль) ручного вмешательства.
- Максимальная скорость выполнения (секунды или минуты, но не часы или дни).
- Возможность быстрого возврата на последнюю рабочую версию.
- Масштабируемость, а значит независимость скорости выполнения от количества серверов.
Основные компоненты любой крупной Web системы — это фронтенды, бекенды, базы данных и сервера специального назначения (например, почтовые либо медиа-хранилища).
Выкатка фронтендов
Фронтенды обычно выполняют две функции:
- Раздают клиентам статические файлы (css, js, картинки).
- Балансируют запросы к приложению и проксируют их на бекенды.
Таким образом, для обновления фронтенда необходимо загрузить новые файлы статики. После этого, выполнить минификацию css/js при необходимости.
На практике это обычно делают так:
- Создают дубликат папки проекта на сервере (например /production_b, если основная папка /production_a).
- Обновляют эту папку (/production_b) до последней версии. Удобно использовать системы контроля версий, чтобы упростить обновление. Например, Git.
- Выполняют необходимые манипуляции (минификация, склеивание и т.п.).
- Меняют папку назначения в конфигурации Nginx'a и перезапускают сервер.
Такой подход позволяет мгновенно выполнить перевод всех пользователей на новую версию.
Конфигурация Nginx'a
Представим, что мы используем такую конфигурацию Web сервера:
server {
index index.html;
root /production_a;
}
Тогда после выкатки необходимо заменить директиву root на новый путь. Можно использовать простой php скрипт:
<?
$config = file_get_contents('site.conf');
$current_version = strpos($config, '/production_a') ? 'a' : 'b';
$new_version = $current_version == 'a' ? 'b' : 'a';
$config = str_replace('/production_' . $current_version, '/production_' . $new_version, $config);
file_put_contents('site.conf', $config);
# Скрипт для последовательного переключения между папками
После обновления и подготовки кода достаточно будет вызывать этот скрипт. Он изменит текущую папку на соседнюю. Если в текущий момент рабочая папка "production_a", выкатку делаем в "production_b" и переключаемся на нее. Если "production_b", то выкатку делаем в "production_a". Таким образом рабочая папка постоянно меняется, а соседняя всегда будет содержать предыдущую рабочую версию.
После изменения файла конфигурации обновляем настройки Nginx'a:
/etc/init.d/nginx reload
Выкатка бекендов
Выкатка бекендов во многом похожа на выкатку фронтендов.
На всех серверах необходимо обновить код во второстепенной папке (/production_b). После чего переключить конфигурацию Nginx'a на нужную папку:
server {
...
root /production_a;
index index.php;
location ~* \.(php)$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
# Выделенную папку необходимо заменить на "production_b"
Разогрев приложения
PHP использует различные кэши операционного кода, чтобы экономить на повторной интерпретации файлов. Поэтому, перед тем как переключать пользователей в новую папку, полезно сделать "разогрев" кэша.
Для этого достаточно иметь отдельный хост в Nginx, например:
server {
host lambda.ruhighload.com;
root /production_b;
index index.php;
location ~* \.(php)$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
# Отдельный хост lambda.ruhighload.com для разогрева кэша новой версии
После этого открыть специальную страницу, которая просто подключит все файлы проекта (preload.php):
<?
if ( $_GET['key'] != 12345 ) exit;
$it = new RecursiveDirectoryIterator("/production_b");
foreach(new RecursiveIteratorIterator($it) as $file)
{
if ( pathinfo($file, PATHINFO_EXTENSION) == 'php' ) include $file;
}
# Подключаем все php файлы проекта
Вызов этой страницы следует добавить в скрипт выкатки, чтобы не делать этого руками:
wget http://lambda.ruhighload.com/preload.php?key=12345
# Ключ для безопасности
Миграции баз данных
Изменение данных и их структуры (или миграция) — наиболее сложная задача при выкатке новых версий. Во-первых изменение структуры данных может занимать достаточно много времени. Во-вторых, ошибки в выкатке могут привести к потерям данных.
Прежде, чем построить систему миграции данных, необходимо обеспечить выполнение следующих правил:
- Избегайте изменений в структуре больших таблиц. По возможности, используйте дополнительные таблицы для хранения данных. Например, вместо того, чтобы иметь таблицу users с колонками [name, gender, email, password], можно иметь две таблицы:
- user_auth [email, password]
- user_info [name, gender]
- Никогда не используйте удаление колонок, таблиц или данных в миграциях. Эти операции должны выполняться отдельными процедурами под внимательным наблюдением администраторов. Миграции должны содержать только добавление колонок/таблиц/данных.
- Не добавляйте создание индексов в миграции кроме логически требуемых (например, уникальные ключи). Индексы должны создаваться исключительно под профиль нагрузки на рабочей базе данных. И также под пристальным наблюдением администраторов.
- Используйте вертикальные таблицы для случаев, когда колонки таблицы могут часто изменяться. Например, структура таблицы для разных свойств продукта:
- product_id
- property_name
- property_value
Технически миграции обычно организуют в виде набора файлов, содержащих SQL запросы, собранных в отдельной папке:
# ls highloadcomua/data/migrations/ 15.comments.add.post_id.sql 16.comments.add.content.sql 17.comments.add.user_id.sql 18.tags.add.titl.sql ...
Процесс выкатки миграций происходит с использованием отдельного сервера. На нем происходит обновление папки с миграциями (используя систему контроля версий). После этого исполняются SQL-запросы, которые появились в новых файлах миграций.
Пример скрипта, который определит новые файлы после обновления и выполнит миграции из них:
<?
# Получаем список выполненных миграций
$executed = file('executed.migrations');
# Получаем список всех миграций
$files = glob('data/migrations/*');
foreach ( $files as $file )
{
if ( in_array($file, $executed) ) continue;
# Выполняем миграцию
exec('mysql database -u root -p12345 < ' . $file, $o, $r);
# Если нет ошибки, помечаем миграцию, как выполненную
if ( !$r ) $executed[] = $file;
}
file_put_contents('executed.migrations', implode("\n", $executed));
Обновление конфигураций
На серверах специального назначения (например, почтовые сервера) потребность вносить изменения бывает реже, чем на серверах приложения. Однако, иногда приходится делать изменения в конфигурациях. Для таких целей удобно хранить все конфигурации в репозитории. Тогда, для смены настроек, достаточно будет обновить файлы конфигураций на всех серверах.
Например для обновления конфигурации почтового сервера можно было бы использовать приблизительно такой скрипт:
cd /etc/exim4
git pull
update-exim4.conf
/etc/init.d/exim4 restart
Для более сложных задач управления конфигурациями лучше использовать Chef.
Параллельная выкатка
В случае, если в системе присутствуют несколько десятков или более серверов, последовательное обновление кода на всех серверах может занять много времени:
for ip in `cat servers.list`; do
echo "Updating $ip..."
ssh $ip 'git -C /production_b pull'
done
# Последовательное обновление кода
С помощью фоновых процессов можно выполнить все эти команды параллельно:
for ip in `cat servers.list`; do
echo "Updating $ip..."
ssh $ip 'git -C /production_b pull' >> /var/log/deploy.log &
done
# Параллельное обновление кода на всех серверах в фоне
Возврат к рабочей версии
Выкатка может содержать в себе ошибки либо просто быть неудачной с точки зрения бизнеса. В любом случае, всегда необходимо иметь возможность быстро восстановить предыдущую версию системы.
Со всеми узлами в описанном процессе это сделать очень просто. Достаточно изменить рабочую директорию Web сервера с текущей (например, production_b) на предыдущую (production_a).
Для баз данных очень важно соблюдать правило — отсутствие удаления данных любого вида в миграциях. Тогда возврат к предыдущей версии не потребует обратного изменения структуры. Удаление устаревших колонок, таблиц и данных следует планировать с задержкой (например, не ранее, чем через неделю). Это даст запас времени для возможного возврата.
Часть аудитории
Крупные изменения (например, новые функции либо существенные изменения в текущих) могут иметь негативные последствия после выкатки. Это может быть связано с неудачным бизнес или техническим решением. Кроме этого в реальной среде всегда существуют определенные факторы, которые невозможно повторить в среде разработки и тестирования (например, большое количество онлайн-пользователей). Это значит, что выкатка больших изменений всегда содержит в себе риск.
Поэтому, крупные и важные изменения удобно выкатывать таким образом, чтобы они были доступны только для части аудитории. Например, только для одного процента всех пользователей. В этом случае, негативные последствия будут ограничены только небольшой частью аудитории.
На практике это можно реализовать с помощью установки избранным пользователям кук. Например, установим каждому сотому пользователю куку "tester":
<?
if ( session::get('id') % 100 == 1 ) setcookie('tester', 1);
...
Тогда в Nginx'e можно будет использовать это значение, чтобы отправлять пользователей с этой кукой в другую (новую) папку:
server {
server_name ruhighload.com;
set $rt '/production_a';
if ($http_cookie ~* " tester=1") {
set $rt '/production_b';
}
root $rt;
location ~* \.(php)$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $rt$fastcgi_script_name;
}
}
# Если установлена кука tester, изменяем переменную $rt на другой путь
Валидация выкатки
Последним шагом в процессе выкатки следует делать проверку доступности приложения. Это нужно, чтобы убедиться, что нет критических ошибок. В самом простом случае это может быть тестовая страница, которая содержит проверки самых важных компонент:
<?
# проверяем подключение к базе данных
$time = mysql::col('SELET NOW()');
if ( !$time ) echo 'error getting time from mysql';
# проверяем код 200 от основных страниц
$pages = ['/', '/speed', '/server'];
foreach ( $pages as $page )
{
$c = curl_init('http://ruhighload.com' . $page);
$result = curl_exec($c);
$status = curl_getinfo($c, CURLINFO_HTTP_CODE);
curl_close($c);
if ( $status != 200 ) echo 'error on page ' . $page;
}
# еще можно проверить php_error.log на наличие ошибок
# еще можно проверить php-fpm.slow-log на появление медленных скриптов
# и т.п.
# hearbeat.php — простой скрипт для быстрой проверки важных компонент сайта
При наличии unit-тестов, имеет смысл использовать их часть для валидации выкатки. В случае обнаружения ошибок лучше всего автоматически откатиться на предыдущую версию, после чего чинить неисправности.
Самое важное
В качестве самого важного — описание общего процесса выкатки:
- Обновление файлов в специально отведенной папке (/production_b) с помощью системы контроля версий (Git, SVN и т.п.).
- Предварительная подготовка (минификация, разогрев кэшей, миграция данных).
- Переключение аудитории или ее части на новую версию (смена пути с /production_a на /production_b на Web сервере).
- Проверка основных компонент системы (например, код HTTP ответа от основных страниц) и возврат к предыдущей версии, если обнаружены ошибки.
Подсистема выкатки — это такая же динамическая компонента приложения, как и любая другая. Ее постоянно необходимо дорабатывать и усовершенствовать. Хорошее правило — иметь автономную систему выкатки, которая не требует ручных операций.
Не торопитесь использовать навороченные системы управления выкатками с кучей интеграций всего во все. Собственные решения часто являются намного более простыми, поэтому легче в управлении и более гибкие в использовании.
Не смотря на максимальную автоматизацию, любая выкатка должна всегда происходить под контролем администраторов или разработчиков. Никогда не делайте слепые выкатки, всегда дожидайтесь "Deployment finished successfully" от своей системы.