Как мы оптимизировали деплой и льем код в любое время суток

Kate

Administrator
Команда форума
Привет! Я работаю CTO в компании Poster, облачной системе автоматизации для кафе и ресторанов. За шесть лет мы из днепровского стартапа выросли до второй по величине планшетной POS-системы в Европе.

В статье я поделюсь нашим опытом и расскажу о том, как мы перестраивали процессы, стремясь к удобному и качественному деплою. Когда-то давно СТО (раньше эту должность занимал нынешний CEO Родион Ерошек) заходил на единственный сервер и вручную делал git pull origin master. Сейчас же наш серверный парк перевалил за 100, причем любой программист, прошедший испытательный срок, может задеплоить свой код на продакшен в любое время дня и ночи, и даже CTO обязан прислать соответствующей гильдии код на ревью перед деплоем на первых клиентов.

Статья будет интересна разработчикам и девопсам, которые ищут простые и удобные способы оптимизировать деплой.

Немного контекста​

Poster — это высоконагруженный продукт: с помощью системы ежемесячно пробивается 17 млн чеков в 11 тыс. ресторанах в 90 странах мира. Система состоит из POS-терминала для официантов и админпанели для владельцев. Программа работает на iPad- и Android-планшетах, desktop-версии запускаются под Windows и macOS, есть и веб-версия.

Ядро системы работает на JS + PHP. Нативные приложения используют Objective-C и Swift, Java и Kotlin, Electron и React Native. Часть микросервисов написаны на Python и Node.js. Вся инфраструктура размещается примерно на 75 dedicated и 25 виртуальных и cloud-серверах. В Dev-команде 40 человек.

Система позволяет сторонним разработчикам интегрироваться с ней на четырех уровнях:

  • API с веб-хуками;
  • кастомные страницы в админпанели;
  • отдельные JS-приложения, внедренные в POS-терминал и перехватывающие различные события;
  • другие iOS- и Android-приложения на иных планшетах, взаимодействующие c POS-терминалом внутри локальной сети по отдельному протоколу.
Сегодня в маркетплейсе находится порядка 40 сторонних приложений, плюс еще примерно 400 закрытых личных интеграций.

Техническая автоматизация деплоя​

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

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

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

Сейчас же тесты стали стандартом и входят в Definition of Done практически во всех командах. Хотя мы все еще против бездумного покрытия тестами ради самих тестов.

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

Ручной деплой​

В первый год жизни компании, когда в ней работало лишь несколько программистов, CTO выливал код банальным git pull origin master. Этого было достаточно, ведь деплой чего-то нового происходил не каждый день.

Первой ласточкой стала автоматизация миграций. Данные каждого клиента Poster хранятся в отдельной базе. В первый год работы компании, чтобы провести миграцию баз, программисты писали новый метод в контроллере :) Затем происходило переподключение по всем базам и выполнение какого-то SQL-ника или куска PHP-кода. А передача изменений между разработчиками, которым также надо было локально обновить у себя базу, осуществлялась через Skype.

Эта схема накрылась примерно тогда, когда я пришел в компанию. Кто-то забыл скинуть в Skype один из SQL-запросов, и после деплоя система начала работать совсем не так, как надо. Я выбил неделю для устранения этой проблемы и написал библиотеку, которая могла бы гибко мигрировать все базы клиентов или какую-то конкретную.

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

Capistrano​

С Capistrano мы прожили около года. Постепенно добавляли туда все больше команд, и время деплоя снова увеличилось. Архитектура Capistrano не позволяла нам гибко ускорять процесс. К тому же, чтобы повысить отказоустойчивость, мы созрели к канареечному деплою. В итоге начали искать альтернативу и перешли на Deployer.

Deployer​

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

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

/*
* Deploy task
*/
task('deploy', [
'deploy:info',
'deploy:lock',
'deploy:prepare',
'deploy:release',
'deploy:update_code',
'deploy:last_commit',
'deploy:copy_dirs',
'deploy:copy_files',
'deploy:npm_install',
'deploy:build',
'deploy:shared',
'deploy:shared_local',
'deploy:check_migrate_pos',
'deploy:check_migrate',
'deploy:symlink_previous',
'deploy:appcache_generate_data',
'deploy:prepare_migrate',
'deploy:symlink',
'deploy:generate_webp',
'deploy:reset_opcache',
'deploy:migrate_pos',
'deploy:migrate',
'deploy:update_appcache',
'cleanup',
'deploy:unlock',
]);
После перехода на Deployer мы деплоили примерно два года с локальных машин. На тот момент у нас толком не было тестов, поэтому такой процесс нас хоть и с натяжкой, но устраивал. Когда же мы наняли первого Automation QA, то вариантов не создавать полноценный CI-/CD-процесс уже не было. У нас появился выделенный DevOps-инженер, который этим и занялся.

GitLab​

CI-/CD-процесс мы организовали в рамках своего GitLab-сервера. Сначала создали pipeline для прохождения тестов, а после этого добавили отдельный pipeline непосредственно для деплоя после принятия MR.

image3_WsSyied.png


В итоге сейчас все, что нам надо сделать, чтобы вылить какую-то ветку, это поставить MR в GitLab, а затем принять его после прохождения тестов. Либо же сразу выбрать автоматический деплой после прохождения тестов. И все.

4 часа, die(); и 11 514 чеков​

Однажды года четыре назад я деплоил ночью большое изменение, которое нельзя было выливать днем: задача требовала вынужденной блокировки аккаунтов для проведения сложных миграций в базах. Хоть я и проверил, чтобы POS-терминалы клиентов запускались, но в сонном состоянии не учел, что это был кеш, который позволяет нашим клиентам работать offline. Довольный результатом работы я пошел спать. А в это время система лежала четыре часа. Тогда у нас еще не было круглосуточной технической поддержки, и до утра меня никто не разбудил... До сих пор с ужасом вспоминаю то утро.

Три года назад после окончания отлова одной из ошибок разработчик забыл <? die();?> в библиотеке, отвечающей за синхронизацию POS-терминалов с бэкендом. Благодаря дикой случайности этот код не ушел на серверы и не поломал нашим клиентам синхронизацию.

В тот же год наш будущий DevOps-инженер вылил код, который за 20 минут поломал закрытие 11 514 чеков. Мы потратили 12 часов, чтобы достать детали клиентских данных из логов и починить их.

К счастью, такие случаи были единичными, но именно благодаря им у нас появились E2E-тесты, а затем API- и unit-тесты. Только пройдя через боль и страдания, бизнес принимает необходимость автоматического тестирования.

Но если быть честным, хотя pipeline c автоматическим прохождением тестов появился у нас полтора года назад, к тому времени мы уже лили код по пятницам и выходным. Причиной этого была культура тестирования, которой мы придерживались давно. Задолго до этого нам пришлось также внедрить ряд инструментов, чтобы автотестирование стало реальностью.

Клонирование баз​

Первое, что мы сделали, это автоматизировали клонирование баз с боевых серверов на локальные машины разработчиков. Сначала это был один маленький bash-скрипт, затем второй, третий и так далее, пока все скрипты не превратились в библиотеку poster-core-tools. Сейчас любой разработчик может законтрибутить туда изменение, которое автоматизирует его работу или работу всего отдела. Клонирование баз привело к тому, что мы смогли локально тестировать написанный нами код на данных лояльных клиентов.

QA-инженер​

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

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

Stage-сервер​

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

Когда запросы на stage-серверы превысили десять штук, в poster-core-tools была добавлена возможность разворачивать их автоматически. Сейчас у нас 40 серверов для ручного тестирования и еще 10 для прогона автоматических тестов. При необходимости можем добавить еще пару десятков запуском одной утилиты.

AQA-инженер​

Два года назад мы наняли первого AQA-инженера. Нам понадобилось примерно 8 месяцев, чтобы покрыть все основные функции E2E-тестами и создать свой первый pipeline для проверки работоспособности системы.

image2_5O9l3cv.png


Параллельно с E2E-тестами мы начали писать API-тесты и развивать культуру написания unit-тестов. Нельзя сказать, что все идет гладко, но практически ежеквартально у нас происходят какие-то изменения в процессе тестирования.

Примером такого изменения стало появление QA-гильдии. QA-инженеры изо всех команд собираются раз в спринт, обсуждают то, что было сделано за последние две недели, составляют план написания тест-кейсов на следующий спринт, а также синхронизируются с AQA-инженером.

Канареечный деплой​

Отдельно хочу рассказать о канареечном деплое. Мы ввели его четыре года назад, чтобы максимально быстро доставлять пользователям изменения продукта. Такие пререлизы позволяют безопасно тестировать новые функции на небольшом проценте пользователей. А если с обновлением что-то идет не так, можем быстро откатиться назад, и большинство клиентов ничего не заметит.

Сначала такой деплой состоял из beta- и production-веток. Со временем к ним добавились testing, а примерно год назад stable-ветки. В итоге сейчас у нас следующая схема разлива кода:

Beta 5% → Testing 10% → Production 15% → Stable 70%

Все разработчики ежедневно льют свой код в beta, на следующий день он переливается из beta в testing, а еще через день — из testing в production. Каждый понедельник код меняет распложение — из production в stable.

Клиенты находятся в постоянной ротации между ветками согласно простому алгоритму: остатку от деления их ID на 5. При этом на ветки ниже stable стараемся переносить клиентов с наибольшим ID, чтобы «старики» оставались на stable. На beta находится примерно 5% клиентов с остатком, равным 1; на testing — 10% с остатком, равным 2; на production — 15% с остатком, равным 3; а все остальные — на stable.

У нас есть еще три ветки — beta 2, beta 3 и beta 4. На них выливаются большие или сложные изменения, которые требуют неопределенного времени тестирования в боевых условиях на работающих лояльных клиентах. После этого код переносится в стандартную beta-ветку.

Доверяй, но проверяй​

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

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

Когда тогдашнему CTO надоело смотреть мой код и мы научились деплоить с помощью Capistrano, все ревью кода и право деплоя негласно перешли ко мне. Так продолжалось примерно полтора года, пока я не стал самым узким местом в ядре и из-за меня не начала падать скорость доставки фич клиентам.

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

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

Такая система просуществовала примерно полтора года, пока в одной команде по ряду причин не остался один бэкенд-техлид на трех бэкенд-разработчиков. Проводя все время за ревью и деплоем, он начал слегка зашиваться.

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

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

Примерно три месяца назад на собрании Dev-отдела, я, CEO и Head of Engineering получили запрет на деплой кода без ревью соответствующей гильдии... Так и живем, создавая отдел по бирюзовой методологии.

Подведем итог​

Сейчас из инструментов мы используем GitLab (для хранения репозитория) и Continuous Integration. Для Continuous Deployment применяем Deployer, а стартует все также из GitLab.

Каждый раз, когда разработчик пушит свой код в репозиторий, мы прогоняем unit-тесты и проверяем code style для JS и PHP.

После того как ветка готова, разработчик ставит MR. В этот момент запускается расширенный pipeline вместе с деплоем ветки на stage-серверы, зарезервированные под автотесты. В качестве тестов сейчас гоняем API-тесты, приемочные на Codeception и E2E-тесты на Selenium.

Далее порядок работы уже по желанию разработчика: ждет он прохождения автотестов или нет, но должен отдать ветку на ревью и, возможно, на мануальное тестирование. Для проведения мануального тестирования QA-инженеры выливают код на stage-серверы и прогоняют там свои тест-кейсы.

Как только все виды тестов и ревью пройдены, в GitLab принимается MR и код автоматически выливается на клиентов, находящихся на beta-ветке. На следующий день код будет перемещен на testing, а затем на production. С наступлением ближайшего понедельника код из production попадет на stable.

При необходимости разработчик может вылить код не на beta, а на beta 2 / beta 3 / beta 4 и перевести туда нескольких заинтересованных клиентов, чтобы они какое-то время потестили фичу. Но потом она все равно будет влита в beta.

image1_Th8UB6C.png


Что дальше​

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

  1. Добавить в pipeline SonarQube, чтобы ускорить и упростить дальнейшее ревью кода.
  2. Сделать автоматический, а не ручной ежедневный перелив кода между ветками beta ⟶ testing ⟶ production ⟶ stable в рамках канареечного деплоя. Сейчас MR создаются автоматически, но после прохождения всех тестов подтверждаются вручную.
  3. Улучшить алгоритм и автоматизировать выбор клиентов для канареечного деплоя. Уйти от «глупого» остатка от деления и начать использовать более качественное распределение. Например, чтобы на beta, testing и production были клиенты как с большим, так и маленьким количеством чеков, с большим и маленьким меню, клиенты, работающие со всеми типами оборудования, и так далее. Это позволит находить неточности в новом коде раньше, не дожидаясь их попадания на всех клиентов.
  4. Деплоить только после получения определенного количества лайков со стороны гильдии.
  5. Автоматизировать выбор тех, кто будет ревьюить код.
И наконец, мы созрели для полного переосмысления своего текущего подхода к CI/CD, чтобы его можно было масштабировать с основного проекта на сателлитные подпроекты и микросервисы. Пока большая часть из них по-прежнему деплоится вручную без своих pipeline. Этим мы и займемся в скором времени.


 
Сверху