В 17 выпуске Perl-журнала PragmaticPerl опубликована моя статья “Яндекс.Директ: как мы деплоим наши Perl-web-приложения”: ссылка. Здесь находится дополняемая и улучшаемая версия этой статьи.
Это примерная расшифровка моего доклада на YAPC::Russia 2014, аннотацию см. на сайте конференции.
Привет! У меня будет “нестандартное” выступление: обычный доклад – это “какую крутую штуку мы сделали”, или “какие крутые штуки можно сделать”, или “какие крутые библиотеки можно использовать, чтобы делать еще более крутые штуки”, а я собираюсь рассказать, что происходит у нас после того, как мы все это использовали и запрограммировали.
Немного контекста: кто это – “мы”?
Это Яндекс.
На Яндексе есть реклама.
Чтобы реклама была, нужен интерфейс, в котором рекламодатель ее создаст.
Может показаться, что это плевое дело – интерфейс для приема рекламных материалов. Однако даже принять у пользователя объявления не так легко, если их ОЧЕНЬ много. Кроме того, есть несколько способов рекламу заливать и управлять ею (API, web-интерфейс, десктопное приложение, …) И категорий пользователей тоже несколько: рекламодатели, рекламные агентства, менеджеры по продажам и другие, так что возникает сложная система ролей. Помимо собственно создания объявлений есть еще прогнозирование рекламных кампаний, всяческие проверки, оптимизации, оплаты, отправка в показывающую часть, предоставление рекламодателям разнообразной статистики, внутренние отчеты и т.д. Так что данных у нас много, запросов много, общения со смежными сервисами тоже много, функциональность обширная, разработка активная.
О технологиях. У нас более-менее LAMP: живем на Ubuntu; Apache используем, Nginx и Starman – тоже; данные храним в основном в mysql, есть немного mongodb и для тяжелой аналитики YT – Яндексовый map-reduce1; много кода на Perl, есть серверный javascript, немного Python. Для очередей задач у нас есть Gearman (не очень нравится), немного используем Ubic, у нас есть SOAP (не нравится, но приходится), xmlrpc, jsonrpc (нравится).
Среды у нас делятся на разработческие, тестовые и продакшен. На разработческих – упрощенная инфраструктура (apache запускается прямо над рабочей копией), тестовые среды по возможности точно воспроизводят продакшен. В качестве разработческих и тестовых баз данных используем обезличенные бекапы продакшена. Это очень полезная практика, так как заодно регулярно проверяется пригодность бекапов к восстановлению. Кроме того, настоящая база – отличный источник готовых заковыристых сочетаний данных для тестирования.
Возможно, неочевидный факт: акции Яндекса размещаютя на бирже NASDAQ2, поэтому мы обязаны выполнять все их требования, касающиеся публичных кампаний3. Например, доступы к разным средам разделены, продакшен обновляют и обслуживают только админы, не разработчики. На релизы есть подробный строгий регламент.
Для управления серверами (выкладок и прочего) есть собственная утилита, похожая на Rex4. Работает поверх ssh, на управляемых машинах никаких специальных агентов не требуется. То есть для того, чтобы выложить релиз, админу не надо заходить на все серверы, достаточно выполнить нужные команды с центральной машины.
Опенсорсить утилиту не собираемся, в ней слишком много проектной специфики.
Релизы у нас живут в таск-трекере. Релиз-менеджер записывает, что и как выкладываем, тестировщики пишут о найденных проблемах, там же потом собираются все нужные подтверждения – от QA, от заказчика, от релиз-менеджера.
Каждый релиз тестируется на тестовой среде. Проверяется новая функциональность и то, что старая не сломалась (“регрессия”). Для changelog’а берем svn log от прошлого релиза до текущего head’а. Для транковых коммитов в коммит-сообщении обязательно есть ссылка на таск-трекер, на тикет, который этим коммитом решается. Это тоже очень хорошая практика: коммиты в транк связаны с перепиской в трекере, так что даже спустя несколько лет можно узнать, какие обсуждения предшествовали внесению определенного изменения, какие были проблемы при тестировании и т.п.
Код выкладываем deb-пакетами. Это работает, это универсально, позволяет работать с зависимостями, в том числе и с неперловыми. Много готовых инструментов: и для сборки пакетов, и для поддержания своего репозитория, и для чего угодно.
Есть неудобья: apt плохо приспособлен к тому, чтобы устанавливать не самую новую версию пакета, имеющуюся в репзитории. А если с выкладкой что-то пошло не так, и надо вернуть все “как было”, то это нетривиальная задача.
Транк у нас “зеленый”, то есть туда попадают уже проверенный и проревьюенный код. Пакетируется для выкладки либо транк, либо релизный бранч, который отводится от транка и в который, если требуется, домерживаются отдельные хотфиксы.
Версии присваиваются так, что по ним однозначно понятно, из какой точки в истории репозитория пакеты были собраны.
Ведется лог: когда на какой сервер выкладывались какие версии каких пакетов. Доступен этот лог в виде небольшого веб-сервиса с фильтрациями, отбором, сортировками и т.п. В частности, такой лог помогает, если надо восстановить на сервере старое состояние пакетов.
Кстати, зависимости лучше указывать с ‘>=’, а не с точными версиями – так, по крайней мере, apt не будет ломаться при откате на предыдущую версию приложения.
Очень полезная практика – заранее проверять пригодность зависимостей к установке. Если этого не делать, получаются неприятные неожиданности в момент, когда релиз надо выкладывать на ТС. У нас работает проверка после каждого коммита в транк, и в случае чего разработчик может быстро все поправить.
Опасные и/или сложнооткатываемые обновления полезно делать отдельно от прочих релизов – чтобы проблемы, если они появятся, не аффектили бы другие задачи.
Помимо выкладки на серверы нового кода в релизе могут требоваться другие действия: изменения структуры таблиц в БД (DDL-запросы: alter’ы, create’ы, drop’ы), конвертации данных (запуск скриптов), нестандартные ручные действия (специфические обновления, рестарты и т.п.).
Все такие действия, которые требуется выполнить в дополнение к доставке на серверы нового кода, мы называем миграциями.
Работа с миграциями у нас устроена похоже на django-вый south, на dbdeploy, да это и закономерно, я думаю.
Миграции описываются в файлах специального формата, которые коммитятся в репозиторий вместе с кодом. Все миграции хранятся в каталоге /deploy, пишут их разработчики параллельно с написанием кода. Формат – перловые структуры, хеши и массивы. Это с одной стороны достаточно формально и пригодно к валидации, с другой – человекочитаемо.
В миграции указывается тип (выполнение sql-запроса, запуск скрипта или другие действия), когда надо ее применить (до выкладки кода, после или не имеет значения), оценка длительности выполнения и если требуется – комментарии об особенностях выполнения и отмены.
Применяются миграции полуавтоматически на ТС и вручную в продакшене. Пока не хватает автоматического определения примененных миграций и возможности запускать простые скрипты без участия человека.
Поскольку миграции пишутся в виде перловых хешей и массивов, парсить их можно простыми eval’ами. Для безопасности (чтобы в миграцию нельзя было вписать выполнение внешнего кода), это делается через Safe::reval, в котором запрещены все операции, кроме самых простых.
Миграции обязательно ревьюятся и аппрувятся. Это важно, так как миграции – это потенциально очень опасные изменения, да к тому же и сложно поддаются классическому тестированию. Менять структуру БД – тяжело, гораздо тяжелее, чем рефакторить код, поэтому схема данных заслуживает особо тщательного проектирования.
Кроме того, автоматически проверяется синтаксис миграций, наличие всех нужных полей и отсутствие ненужных. Синтаксис sql-запросов проверяется с помощью DBIx::MyParsePP, и это очень полезно.
Чтобы не запутаться в названиях полей, есть специальный скрипт-хелпер. Он задает серию вопросов (одно действие или несколько? Sql-запрос или скрипт? До кода или после?) и генерирует шаблон миграции, в который остается вписать свои фактические запросы и другие данные.
Для удобного хранения истории изменения структуры БД и для документирования таблиц и полей мы используем следующую систему: в каталоге /db_schema для каждой таблицы хранится файлы table_name.schema.sql и table_name.text. В первом хранится запрос create table, которым можно создать таблицу, а во втором – описание таблицы в целом и ее полей по отдельности.
Есть скрипт, который показывает расхождения между файлами .schema.sql и реальной базой, отчет о таких расхождениях в продакшене регулярно приходит на общую рассылку.
В итоге получается удобно: хочешь узнать, какой смысл у поля в таблице – читаешь описание. Хочешь узнать, когда поле в таблице появилось – смотришь svn annotate на файл .schema.sql.
Регулярные запуски скриптов делаются через cron. Кронтабы генерируются при сборке пакетов из специальных pod-секций в самих скриптах, и с пакетами устанавливаются в /etc/cron.d одновременно с установкой основного кода.
Кроме тестов, проверяющих поведние кода, есть еще много разных проверок, которые полезно регулярно запускать в проекте. На слайдах выписаны самые полезные на мой взгляд – те, что срабатывают чаще всего или ловят ошибки, которые в другом случае могли бы долго и незаметно портить работу продакшена.
Вот и всё. Если прочитали что-то новое и полезное – пожалуйста, пользуйтесь. Если умеете что-то делать удобнее и надежнее – расскажите.
Про YT был доклад на YAC 2013: http://tech.yandex.ru/events/yac/2013/talks/1091 ↩
См. SOX, SOX 404 top–down risk assessment ↩