От пул-реквеста до релиза. Доклад Яндекс.Такси
В релизном цикле сервиса есть критически важный период — с момента, когда новая версия подготовлена, до момента, когда она становится доступна пользователям. Действия команды между этими двумя контрольными точками должны быть единообразны от релиза к релизу и, по возможности, автоматизированы. В своём докладе Сергей Помазанов alberist описал процессы, которые следуют за каждым пул-реквестом в Яндекс.Такси.
— Добрый вечер! Меня зовут Сергей, я руководитель группы автоматизации в Яндекс.Такси. Если вкратце, основная задача нашей группы — минимизация времени, которое разработчики тратят на решение своих задач. Сюда входит все: от CI до процессов разработки и тестирования.
Что наша разработка делает, когда код написан?
Чтобы протестировать новую функциональность, мы для начала проверяем все локально. Для локального тестирования у нас есть большой набор тестов. Если появляется новый код — его тоже нужно покрыть тестами.
Покрытие тестами у нас не такое хорошее, как хотелось бы, но мы стараемся поддерживать его на достаточном уровне.
Для тестирования у нас используется Google Test и самописный фреймворк на pytest, которым мы тестируем не только «питонячью» часть, но и «плюсовую». Наш фреймворк позволяет запускать сервисы, заливать данные в базу перед каждым тестом, обновлять кэши, мокать все внешние запросы и т. д. Достаточно функциональный фреймворк, который позволяет запустить все как угодно, что угодно замокать, чтобы у нас случайно не получились какие-то запросы вовне.
Помимо функциональных тестов, у нас есть интеграционные тесты. Они позволяют решить другую проблему. Если вы не уверены, что ваш сервис будет правильно взаимодействовать с другими сервисами, то можно запустить стенд и прогнать набор тестов. Пока у нас набор тестов базовый, но потихоньку расширяется.
Стенд построен на технологии Docker и Docker Compose, где в каждом контейнере поднимаются свои сервисы, и все они взаимодействуют друг с другом. Происходит это в изолированном окружении. У них своя изолированная сеть, своя БД, свой набор данных. И тесты проходят в таком виде, как будто кто-то запускает мобильное приложение, нажимает на кнопочки, делает заказ. В этот момент виртуальные машинки ездят, довозят пассажира, дальше у пассажира списываются деньги, и всё в этом духе. В основном все тесты требуют взаимодействия сразу множества сервисов и компонент.
Естественно, мы тестируем только наши сервисы и только наши компоненты, потому что тестировать внешние сервисы мы не должны, и все внешнее мы мокаем.
Стенд оказался достаточно удобен, чтобы его можно было запустить локально и получить из него карманное такси. Можно взять этот стенд, запустить на локальной машине либо на виртуалке или любой другой разработческой машине. Запустив стенд, можно взять мобильное приложение, адаптированное под карманное такси, настроить его на свой компьютер и делать заказы. Все точно так же, как в продакшене или где-либо еще. Если нужно проверить новую функциональность, можно просто подсунуть свой код, он подхватится и запустится во всем окружении.
Опять-таки, можно просто взять и запустить нужный сервис. Для этого потребуется поднять БД, наполнить ее нужным содержимым, либо взять из имеющихся окружений базу и подключить ее к сервису. И дальше можно просто обращаться к нему, поделать какие-то запросы, посмотреть, правильно он работает или нет.
Еще один важный момент — проверка стиля. Если для «плюсов» все просто, мы используем clang-format и проверяем, соответствует ли ему код или нет, то для Python мы используем целых четыре анализатора: Flake8, Pylint, Mypy и, кажется, autopep8.
Эти анализаторы мы используем в основном в стандартной поставке. Если есть возможность выбрать какой-то стиль оформления, то мы используем Google style. Единственное, что мы поправили, добавив свое, это проверку на сортировку импортов, чтобы импорты правильно сортировались.
После того, как у вас создан код, проверен локально, вы можете сделать пул-реквест. Пул-реквесты у нас создаются в GitHub.
Создание пул-реквеста в GitHub дает множество возможностей, которые предоставляет нам TeamCity. TeamCity автоматически запускает все вышеозвученные тесты, проверяет их в автоматическом режиме, и в самом пул-реквесте отписывается о статусе прохождения, прошли ли тесты или нет. То есть можно не заходя в TeamCity увидеть, прошло или нет, и перейдя по ссылке, понять, что пошло не так, и что нужно поправить.
Если вам недостаточно карманного такси и тестов, вы хотите проверить реальное взаимодействие с каким-то реальным сервисом, у нас есть тестовое окружение, которое повторяет продакшен. Этих тестовых окружений у нас два. Одно предназначено для мобильной разработки тестировщиков, а второе — для разработчиков. Тестовое окружение максимально близко к продакшену, и если делаются какие-то запросы во внешние сервисы, также они делаются из тестовых окружений. Единственное ограничение, что тестовое окружение по возможности ходит в тестинг внешних ресурсов. А продакшен-окружение ходит в продакшен.
Еще про тестовое окружение, у нас делается достаточно просто через TeamCity. Надо в GitHub поставить соответствующий лейбл, а после того как он поставлен, нажать на кнопку «Собрать кастом». Так он у нас называется. Затем смержатся все пул-реквесты с этим лейблом, и дальше начинается автоматическая сборка пакетов с разливкой по кластерам.
Помимо обычного тестирования иногда требуется проведение нагрузочного тестирование. Если вы правите код, который является частью высоконагруженного сервиса, для этого у нас можно сделать нагрузочные тесты. В Python высоконагруженных сервисов мало, часть из них мы переписали на С++, но тем не менее, они еще остались, иногда имеют место быть. Нагрузочное тестирование происходит через систему Лунапарк. Она использует Яндекс.Танк, он в свободном доступе, можно его скачать и посмотреть. Танк позволяет проводить стрельбы по какому-то сервису, строить графики, делать разные способы нагрузки и показывать, какая нагрузка была в данный момент на сервисе и что какие ресурсы использовало. Достаточно через TeamCity нажать на кнопочку, соберется пакет, и дальше его можно будет раскатить куда надо. Или просто вручную залить и запустить его там.
Пока вы тестируете свой код, кто-то из разработчиков может в этот момент начать смотреть ваш код, заниматься его ревью.
На что мы обращаем внимание в процессе:
Один из важных моментов — функциональность должна быть отключаемая. Это означает, что что бы с кодом ни было, были в нем баги или нет, возможно, эта функциональность работает не так, как изначально задумывалось, возможно, менеджеры хотели другого, а может, эта функциональность пытается положить другой сервис, который не был готов к новым нагрузкам, и нужна возможность быстро его отключить и перевести все в нормальное состояние.
Также у нас есть правило, что при выкатке новая функциональность должна быть выключена, и включаться только после того, как она раскатится на все кластера и все дата-центры.
Не стоит забывать, что у нас есть API, которым пользуются мобильные приложения, которые могут достаточно долго не обновляться. Если мы будем делать какие-то обратно несовместимые изменения в нашем API, то у нас могут сломаться какие-то приложения, и мы не можем заставить все приложения просто взять и обновиться. Это достаточно негативно скажется на нашей репутации. Поэтому вся новая функциональность должна быть обратно совместима. Это касается не только внешнего API, но и внутреннего, потому что нельзя выкатить одновременно весь код на все дата-центры, на все машины, на все кластера. В любом случае у нас будет одновременно жить как старый код, так и новый. В итоге у нас получатся какие-то запросы, которые не смогут где-то обработать, и будут у нас ошибки.
Также следует подумать про следующую вещь, что если вдруг ваш код не работает или вы написали новый микросервис, в котором потенциальные проблемы, нужно быть готовым к последствиям и уметь деградировать. Про это расскажет мой коллега на следующей презентации.
Если вы делаете изменение в высоконагруженных сервисах, и вам не обязательно ждать окончания каких-то операций, то вы какие-то вещи можете сделать асинхронно где-то в фоне или отдельным процессом. Лучше сделать это так, потому что отдельный процесс менее влияет на продакшен, и система будет работать в целом стабильнее.
Также важен момент, что все данные, которые мы получаем извне, мы не должны им доверять, мы должны их каким-то образом валидировать, проверять и т. д. Все данные, которые у нас есть, должны делиться на группы, которые сформированы нами, или сырые данные, которые не проходили валидацию. Сюда входят все данные, которые потенциально могли быть получены из каких-то других внешних сервисов или непосредственно от пользователей, потому что прийти потенциально могло все, что угодно. Возможно, кто-то специально отправил зловредный запрос, и у нас все должно проверяться.
Еще есть случаи, что при запросе сервис может не ответить за нужное время. Возможно, разорвалось соединение или что-то пошло не так, ситуаций может быть много. Мобильное приложение не знает, что в итоге произошло, просто делает перезапрос.
Очень важно, чтобы в процессе этих перезапросов, сколько бы их ни было, в итоге все отработало так, как изначально ожидалось при одном запросе. Не должно у нас проявиться каких-то особых спецэффектов. При этом также нужно учитывать, что у нас не один сервис, у нас множество машин, множество дата-центров, у нас базы распределенные, и возможны гонки при этом всем. Код должен быть написан так, чтобы если он запускается в нескольких местах одновременно, чтобы у нас не было гонок.
Не менее важный момент — умение диагностировать проблемы. Проблемы бывают всегда, во всем, и нужно понять, где они произошли. В идеальной ситуации о существовании проблемы узнали не через службу поддержки, а через мониторинги. А при разборе каких-то ситуаций мы могли бы в итоге понять то, что произошло, просто прочитав логи, не читая код. Даже тот человек, который никогда не видел код, чтобы по логам в итоге это смог получить.
А в идеальном случае, если ситуация очень сложная, по логам нужно уметь проверять, по какому пути пошла в итоге программа, и что же произошло, чтобы сильно упростить разбор полетов. Потому что ситуация прошла в итоге в прошлом, а сейчас это уже вряд ли получится воспроизвести, уже нет данных или данные другие, либо другие ситуации.
Если вы делаете новые операции в БД или создаете новую, нужно учитывать, что данных может быть много. Возможно, вы будете записывать в эту базу бесконечное количество записей, и если не подумать об их архивировании, то возможны проблемы, база просто начнет бесконечно расти, и не хватит уже никаких ресурсов, никаких дисков и шардирования. Важно уметь архивировать данные, и хранить только оперативные данные, нужные в данный момент. И также обязательно нужно делать во все базы запросы по индексам. Запрос не по индексу может положить весь продакшен. Один маленький запрос в самую нагруженную центральную коллекцию может положить все. Надо быть очень аккуратными.
У нас не приветствуются преждевременные оптимизации. Если кто-то пытается сделать какую-то фабрику каким-то очень универсальным методом, который потенциально обрабатывает случаи на будущее, возможно, когда-нибудь кто-то захочет это расширять — так у нас не принято, потому что, возможно, что развиваться она будет совсем не так, а возможно этот код в итоге закопают, а возможно, Это все не нужно, а только усложняет чтение и понимание кода. Потому что чтение и понимание кода — это очень важно. Важно, чтобы код был очень простым и легким.
Если вы в своем коде добавляете новую базу либо производите изменение в API, у нас есть документация, которая частично генерируется из кода, частично делается на Вики. Эту информацию важно держать в актуальном состоянии. Если нет — это может ввести кого-то в заблуждение или вызывать проблемы у других разработчиков. Потому что код пишет один, а поддерживают его много.
Немаловажная часть — соблюдение общего стиля. Главное в этом случае — единообразие. Когда весь код написан единообразно, то его легко понять, легко прочитать, и не нужно вникать во все подробности и нюансы. Единообразно написанный код позволяет ускорить весь процесс разработки потенциально в будущем.
Еще момент, который мы не проверяем на ревью целенаправленно — мы не ищем баги. Потому что поиском багов должен заниматься автор. Если при ревью находятся баги, естественно, про это напишут, но целенаправленного поиска не должно быть, это полностью на ответственности человека, который пишет код.
Далее, когда у вас код написан, ревью пройдено, вы уже готовы замержить его, но часто бывает, что нужно провести дополнительные действия, миграции в БД.
Для проведения миграций мы пишем скриптик на Python, который умеет общаться с бэкендом. Бэкенд, в свою очередь, имеет подключение ко всем нашим базам и может провести все нужные операции. Скрип запускается через админку запуска скриптов, далее выполняется, можно посмотреть его лог и результаты. И если вам требуются длительные балковые операции, то нельзя обновлять сразу все, нужно делать это чанками по 1000–10000 с некоторыми паузами, чтобы случайно не положить данными операциями базу.
Когда код написан, отревьюен, протестирован, проведены все миграции, можно смело мержить его в GitHub и дальше приступать к релизу.
Для некоторых сервисов у нас существует регламент, по которому мы должны выкатываться в определенное время, но значительная часть сервисов у нас может выкатываться в любое время.
Это все делается при помощи TeamCity.
Начинается все со сборки пакетов. TeamCity делает git flow или его подобие. Мы потихоньку уходим от git flow на свои наработки, которые нам показались более удобными. TeamCity это все производит, собирает пакеты, заливает их. Дальше ждем, когда на этих пакетах пройдут тесты. Прохождение тестов является обязательным для выкатки релиза. Если тесты не пройдены, то сначала надо разобраться и посмотреть, что в итоге пошло не так. Тесты используются те же, обычные и интеграционные. На них проверяется уже собранный пакет, готовый, именно то, что пойдет в продакшен. Это на всякий случай, вдруг в собранном пакете есть проблемы, вдруг что-то недокопировано, вдруг что-то оказалось пропущено.
Также есть требование, что мы создаем релизный тикет в нашем трекере, где каждый разработчик должен отписаться, как он тестировал данный код, и в нем должны быть приведены все задачи, которые подлежат выполнению.
Это все тоже делается автоматически в TeamCity, который проходит по списку коммитов. У нас есть требование, что в каждом коммите должно быть ключевое слово «Relates», после которого идет имя задачи. Скрипт, написанный на Python, автоматически по этому всему проходится, составляет список задач, которые были решены, формирует список авторов и создает релизный тикет, призывая всех авторов, чтобы они отписались о своем тестировании и подтвердили, что они готовы «ехать» в релизе.
Когда все готовы, подтверждения собраны, то происходит выкатка, сначала — в пре-стейбл. Это маленькая часть продакшена. Для каждого сервиса используется несколько дата-центров, в каждом дата-центре может быть несколько машин. Одна из машин является пре-стейблом, и выкатка кода происходит сначала только на одну или пару-тройку машин.
Когда код выкачен, следим за графиками, за логами, за тем, что же в итоге происходит на сервисе. Если все хорошо, если графики показывают, что все стабильно, и каждый проверил, что его функциональность работает так, как должна, то происходит раскатка на оставшуюся часть окружения, которая у нас называется стейбл. При выкатке в стейбл все аналогично: смотрим графики, логи и проверяем, что у нас все хорошо.
Выкатка прошла, все замечательно. А если что-то пошло не так, если вдруг проблемы?
Собираем хотфикс. Он делается по такому же принципу, как git flow, то есть ответвлением от ветки мастер. Создается отдельный пул-реквест от мастера, который вносит исправления, а дальше запускаемый из TeamCity скрипт его подмерживает, делает все необходимые операции, точно таким же образом собирает все пакеты и раскатывает дальше.
Под конец я хотел бы рассказать о направлении, в котором мы движемся. Движемся мы в сторону единого репозитория, когда в одном репозитории живет сразу множество сервисов. У каждого из них есть независимые выкладки: в тестинг, в релизы. Для пул-реквестов, даже когда используется TeamCity, мы проверяем, какие файлы были затронуты, к каким сервисам они относятся. По графу зависимости мы определяем, какие тесты нам в итоге нужно запустить и что проверить. Стремимся мы к максимальной изоляции сервисов друг от друга. Пока не очень получается, но мы стремимся к этому, чтобы множеству сервисов можно было жить в одном репозитории, иметь некий общий код и чтобы это не вызывало проблем и упрощало жизнь разработке. На этом все, всем спасибо.