Выкатка большой системы

Выкатка (или deployment) новых версий Web приложений имеет ряд трудностей, т.к. необходимо быстро и одновременно выполнять группы действий на разных серверах. Процесс обычно включает в себя обновление кода (php) и статики (js/css/картинки), изменение баз данных и настроек системы.

Когда-то давно, новые версии появлялись очень редко (раз в год или даже реже). Тогда происходили сложные и длительные процессы обновления, а пользователи получали сразу огромный пакет изменений. Такой трудоемкий процесс иногда уничтожал целые бизнесы. Сейчас понятие новой версии минимизировано до малейших изменений. Динамика разработки современных приложений огромная, а выкатки обновлений могут происходит каждый день.

Поэтому к процессу выкатки добавился целый ряд требований:

  • Минимум (лучше ноль) ручного вмешательства.
  • Максимальная скорость выполнения (секунды или минуты, но не часы или дни).
  • Возможность быстрого возврата на последнюю рабочую версию.
  • Масштабируемость, а значит независимость скорости выполнения от количества серверов.

Основные компоненты любой крупной Web системы — это фронтенды, бекенды, базы данных и сервера специального назначения (например, почтовые либо медиа-хранилища).

Выкатка фронтендов

Фронтенды обычно выполняют две функции:

  • Раздают клиентам статические файлы (css, js, картинки).
  • Балансируют запросы к приложению и проксируют их на бекенды.

Таким образом, для обновления фронтенда необходимо загрузить новые файлы статики. После этого, выполнить минификацию css/js при необходимости.

На практике это обычно делают так:

  1. Создают дубликат папки проекта на сервере (например /production_b, если основная папка /production_a).
  2. Обновляют эту папку (/production_b) до последней версии. Удобно использовать системы контроля версий, чтобы упростить обновление. Например, Git.
  3. Выполняют необходимые манипуляции (минификация, склеивание и т.п.).
  4. Меняют папку назначения в конфигурации 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

# Ключ для безопасности

Миграции баз данных

Изменение данных и их структуры (или миграция) — наиболее сложная задача при выкатке новых версий. Во-первых изменение структуры данных может занимать достаточно много времени. Во-вторых, ошибки в выкатке могут привести к потерям данных.

Прежде, чем построить систему миграции данных, необходимо обеспечить выполнение следующих правил:

  1. Избегайте изменений в структуре больших таблиц. По возможности, используйте дополнительные таблицы для хранения данных. Например, вместо того, чтобы иметь таблицу users с колонками [name, gender, email, password], можно иметь две таблицы:
    • user_auth [email, password]
    • user_info [name, gender]
  2. Никогда не используйте удаление колонок, таблиц или данных в миграциях. Эти операции должны выполняться отдельными процедурами под внимательным наблюдением администраторов. Миграции должны содержать только добавление колонок/таблиц/данных.
  3. Не добавляйте создание индексов в миграции кроме логически требуемых (например, уникальные ключи). Индексы должны создаваться исключительно под профиль нагрузки на рабочей базе данных. И также под пристальным наблюдением администраторов.
  4. Используйте вертикальные таблицы для случаев, когда колонки таблицы могут часто изменяться. Например, структура таблицы для разных свойств продукта:
    • 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-тестов, имеет смысл использовать их часть для валидации выкатки. В случае обнаружения ошибок лучше всего автоматически откатиться на предыдущую версию, после чего чинить неисправности.

Самое важное

В качестве самого важного — описание общего процесса выкатки:

  1. Обновление файлов в специально отведенной папке (/production_b) с помощью системы контроля версий (Git, SVN и т.п.).
  2. Предварительная подготовка (минификация, разогрев кэшей, миграция данных).
  3. Переключение аудитории или ее части на новую версию (смена пути с /production_a на /production_b на Web сервере).
  4. Проверка основных компонент системы (например, код HTTP ответа от основных страниц) и возврат к предыдущей версии, если обнаружены ошибки.

Подсистема выкатки — это такая же динамическая компонента приложения, как и любая другая. Ее постоянно необходимо дорабатывать и усовершенствовать. Хорошее правило — иметь автономную систему выкатки, которая не требует ручных операций.

Не торопитесь использовать навороченные системы управления выкатками с кучей интеграций всего во все. Собственные решения часто являются намного более простыми, поэтому легче в управлении и более гибкие в использовании.

Не смотря на максимальную автоматизацию, любая выкатка должна всегда происходить под контролем администраторов или разработчиков. Никогда не делайте слепые выкатки, всегда дожидайтесь "Deployment finished successfully" от своей системы.

Подпишитесь на Хайлоад с помощью Google аккаунта
или закройте эту хрень