Patch me if you can: как мы отлаживаемся на production. Часть 1

Привет, Хабр! Меня зовут Александр Измайлов. В Badoo я возглавляю команду релиз-инженеров. Я знаю, что во многих компаниях можно присылать изменения кода специально обученному человеку, он их смотрит и добавляет куда следует (например, именно так происходит с кодом Git). А я хочу рассказать о том, как мы автоматизировали этот процесс у нас.

Мой рассказ будет состоять из двух частей. В этой части я расскажу о том, чего мы хотели добиться от новой системы, как она выглядела в первом её варианте, и почему в итоге нам пришлось её переделывать. Во второй части речь пойдёт о процессе переделки системы и о том, какие неожиданные бонусы нам это принесло.

kzrxooelulor2_oeeddmmd-o7xg.png


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

Время шло, компания росла — и нам пришлось задуматься об автоматизации процессов, включая процесс выкладки небольших изменений. Так у нас появилась идея создать систему патчей. В первую очередь мы хотели, чтобы она позволяла любому разработчику присылать свои изменения в Git и раскладывать их на серверы. С нашей стороны мы требовали, чтобы изменения посмотрел ещё один разработчик и чтобы они были привязаны к задаче из нашей багтрекинговой системы (мы используем Jira).

Patch collector 6000
Нужно сказать, что эти требования пришлись по душе не всем разработчикам. Нам казалось, что потратить пару минут на создание задачи — не штука, а для нас это означало бы более обдуманное использование системы. Но разработчики начали сопротивляться, утверждая, что выкладка изменений занимает в разы меньше времени, чем создание нового тикета. В итоге у нас до сих пор появляются «универсальные» задачи, к которым прикрепляют сотни патчей.

Кроме того, система задумывалась с целью чинить срочные проблемы, а найти ревьюера для патча в три часа ночи может быть непросто.


А что нам надо?


Да просто свет в оконце… Нашу проблему можно было условно разделить на две части: нам были необходимы какой-то способ принятия изменений в репозиторий и какой-то способ раскладки этих изменений.

Первый вопрос мы решили достаточно быстро: сделали форму, к которой нужно было прикреплять свои изменения и указывать детали (ревьюера и задачу).

klykgtwk7gjwahm7bittw-jrf58.png

На второй странице можно было посмотреть изменения, отклонить или принять их.

dd2ks86twqszir9vu7jsaiu6cym.png

После подтверждения изменения попадали в master.

Второй вопрос: как эти изменения быстро доставить до серверов? Сегодня многие используют непрерывную интеграцию, и она могла бы хорошо справиться с задачей, если бы наши «честные» сборка и раскладка не занимали так много времени.

Честная сборка


Наша сборка всегда была достаточно сложной. Общий принцип был такой: в отдельной директории мы раскладывали файлы так, как они будут лежать на конечных серверах; затем мы сохраняли это состояние в snapshot (снимок файловой системы) и раскладывали его.

Мы складывали в директорию PHP-код, который брали из репозитория как есть, к нему добавляли сгенерированные файлы (например, шаблоны и переводы). Статику мы раскладывали отдельно. Это довольно сложный процесс, и ему можно посвятить целую статью, но в результате у нас была карта версий, чтобы генерировать для пользователей ссылки на файлы, которая уезжала вместе с основным кодом.

Дальше состояние директории нужно было куда-то сохранить. Для этого мы использовали блочное устройство, которое мы называли лупом. Вся директория копировалась на пустое устройство, которое затем архивировалось и доставлялось на отдельные «главные» серверы. С этих серверов мы забирали архивы в процессе раскладки. Каждый архив был объёмом 200 Мб, а в распакованном виде луп весил 1 Гб. На сборку без статики у нас уходило около пяти минут.

Честная раскладка


Сначала нам нужно было доставить архив на конечные серверы. Их у нас тысячи, поэтому вопрос доставки для нас всегда был большой проблемой: у нас несколько платформ (дата-центров), и на самой «толстой» порядка тысячи серверов с кодом. В попытках добиться лучших показателей (минимум времени и ресурсов) мы перепробовали разные методы: от простого SCP до торрентов. В итоге мы остановились на использовании UFTP. Метод был быстрым (при хорошей погоде — минута времени), но, к сожалению, не беспроблемным. Периодически что-то ломалось, и нам приходилось бежать к админам и сетевикам.

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

Никакой сборки


Итак, честная выкладка изменений занимала много времени, а для системы патчей нам была очень важна скорость доставки, ведь предполагалось, что ею будут пользоваться, когда что-то уже не будет работать. Поэтому мы вернулись к идее с использованием MSCP: быстро и просто в реализации. Таким образом, после того как изменения оказывались в мастере, на отдельной странице можно было по очереди разложить изменённые файлы.

6vxrzhjqovb_itqx7g8caopp5zq.png

Оно живо


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

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

Один патч на несколько файлов


Пример предсказуемой проблемы. Новые файлы раскладывались по очереди. Что делать, если нужно изменить несколько файлов и изменения в них связаны? Например, я хочу добавить новый метод в одном файле и сразу использовать его в других. Пока нет закольцованного использования методов (см. взаимная рекурсия), достаточно помнить о правильном порядке раскладки файлов.

Честное решение
Чтобы решить проблему, нам нужно было заменить несколько файлов атомарно. В случае с одним файлом решение известно: нужно использовать файловую операцию rename. Допустим, у нас есть файл Ф, и нам нужно заменить его содержимое. Для этого создаём файл TMP, записываем в него необходимую информацию, а затем делаем rename TMP Ф.

Усложним задачу. Допустим, у нас есть директория Д, и нам нужно заменить её содержимое. Операция rename нам не поможет, потому что она не может заменить непустую директорию. Однако есть обходное решение: можно заранее заменить директорию Д на так называемую символическую ссылку (symlink). Тогда само содержимое будет лежать в другом месте, например, в директории Д_1, а Д будет ссылкой на Д_1. В тот момент, когда потребуется замена, новое содержимое записывается в директорию Д_2, на которую создаётся новая ссылка TMP. Теперь rename TMP Д сработает, потому что эту операцию можно применять к ссылкам.

Это решение выглядит подходящим: можно менять целиком директорию с кодом, копируя старые файлы и записывая новые сверху. Проблема в том, что копировать весь код — долго и дорого. Можно заменять только поддиректорию, где менялись файлы, но тогда все поддиректории с кодом должны быть ссылками, ведь заменить заполненную директорию на что-либо в процессе раскладки мы не сможем. Мало того, что это решение выглядит очень сложным, — нужно не забыть добавить какие-то ограничения, чтобы два процесса не могли менять одновременно одну и ту же директорию или директорию и ее поддиректории.


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

Несколько патчей на один файл


Сложнее, если файл один, а разработчиков, которые хотят его изменить, — несколько. Первый патч мы применили, но не разложили. В этот момент приходит второй патч, и его просят разложить. Что делать? Ещё интереснее, если второй патч применился, и в этот момент нас просят разложить первый.

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

Действительно хорошего решения этой проблемы мы не придумали. Мы показывали разработчикам diff между тем, что они раскладывают, и тем, что есть на машинах в данный момент времени, но это не всегда работало. Например, изменений банально могло быть много, а разработчик мог спешить или просто лениться (всякое бывает).

Много патчей, и все меняют одни и те же файлы


Это худший вариант, на котором даже останавливаться не хочется. Если изменения нескольких разработчиков затрагивали несколько одних и тех же файлов, наша система патчей особенно помочь не могла — оставалось рассчитывать на внимательность разработчиков и их умение общаться друг с другом. Но в теории тут вполне можно получить «рыбу», когда при любом порядке раскладки в определённый момент на серверах окажется частично неработающий код.

n-qnfsbv6myxc8d7oav-yhyitu4.png


Картинка: источник

Проблемы с железом


Ещё одна проблема возникала, когда по каким-либо причинам один из серверов становился недоступен. У нас был механизм исключения таких серверов из раскладки, который срабатывал достаточно хорошо; сложности появлялись после их возвращения в строй. Дело в том, что версии конфигов и кода на работающих серверах у нас проверяются (целый отдел мониторинга есть!), и мы следим за тем, чтобы при возвращении сервера в строй все версии были актуальными. Вот только для патчей никакого версионирования у нас не было — мы просто копировали новые файлы на текущий код.

Аккуратного способа версионировать разложенные патчи мы не придумали, а проблему пытались решать обходными путями. Например, rsync с соседней машины в конце процесса раскладки. Но вот проверить как-то, что всё хорошо, мы всё равно не могли.

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

Ложка мёда


Но, кроме проблем, были и положительные моменты.

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

Во-вторых, больше не нужен был какой-то специальный человек с правами, чтобы что-то починить. Любой разработчик сам мог выложить свои правки. Но и это не всё: выкладки билдов в целом стали проще, теперь проблемы делились на критические и те, которые можно починить с помощью патчей. Это позволяло реже откатываться и быстрее принимать решение о том, удачно ли мы выложились.

Проще говоря, система нам нравилась и набирала популярность. Мы и дальше пытались усовершенствовать её, но с описанными проблемами пришлось прожить ещё несколько лет. А о том, как мы их решили, как система работает сейчас, и как мы в процессе обновлений чуть не угробили новогодние каникулы, я расскажу во второй части статьи.

© Habrahabr.ru