Pipeline
Автор: Тимур Алиев (tg: @talievv)
Пайплайны в контексте процессов разработки в компаниях — то, что происходит с кодом от момента зарождения идеи до релиза на продакшене.
Системы контроля версий: Git и монорепозитории
Проблемы Git в микросервисной архитектуре
В крупных компаниях программирование часто устроено так, что используется Git, и проект делится на множество репозиториев. Например, компания вроде mini Netflix с 20 микросервисами может иметь 20 репозиториев. Преимущества такого подхода с Git и множеством репозиториев:
- Гибкость и удобство Git.
- Доступ к разным репозиториям может быть ограничен для разных разработчиков.
- Репозитории независимы, представляют разные микросервисы.
- У каждого микросервиса свой релизный цикл и тесты.
- Новые версии микросервисов обратно совместимы и не ломают другие.
Недостатки:
- Куда девать общие компоненты? (Хотя это решается созданием отдельного общего репозитория, например,
lib
). - Проблемы с обратной совместимостью, когда один микросервис выкатывается с багом или ломает API, а другие сервисы не успели обновиться. В реальном мире сервисы не являются по-настоящему слабосвязанными, если они взаимодействуют. Человеческие ошибки и архитектурные недочеты усложняют поддержание обратной совместимости. Например, сервис, использующий устаревший API, требует сложной координации для обновления. Сложно управлять такими зависимостями без доступа ко всем репозиториям.
Монорепозитории: преимущества и недостатки
Большие компании используют концепцию монорепозитория, где все репозитории объединены и доступны всем разработчикам. Преимущества монорепозитория:
- Легко использовать библиотеки, разработанные в другом сервисе, переместив их в общий
lib
. - Легко видеть и глобально изменять код.
- Упрощается управление зависимостями.
- Версии компонентов не устаревают неравномерно.
- Все унифицировано.
Недостатки:
- Основная проблема: Git плохо работает с огромными монорепозиториями. У каждой крупной корпорации (Яндекс, Facebook, Google) возникает проблема, что репозиторий просто не помещается или Git не справляется с его размером.
- На каждый коммит и добавление файла в Git создается хэш, который занимает много места на диске.
- Происходит размытие ответственности, так как у всех есть доступ ко всему, хотя сломать чужой микросервис напрямую сложно, можно сломать общую библиотеку.
Ограничения Git для больших монорепозиториев
Медианный размер репозитория на GitHub составляет пару мегабайт. Размер репозитория Linux - гигабайт, Chromium - 10 гигабайт, а Yandex Arcadia - терабайт. Это в 1000 раз больше, чем Linux, и работа с Git становится невыносимо долгой. В Google, где репозитории еще в 100 раз больше, пользоваться Git в принципе невозможно. Сравнение производительности старых систем (SVN, Mercurial) и Git на больших репозиториях показывает, что Git может просто не завершить операции.
Решение проблемы: виртуальные файловые системы
Системы, используемые крупными компаниями (такие как те, что разрабатываются в Google, Facebook, Яндексе), решают проблему большого размера монорепозитория, храня большую часть информации на сервере и загружая ее лениво (только то, что используется в данный момент). Они создают виртуальную файловую систему. В отличие от Git, эти системы требуют постоянного подключения к серверу.
Концепции разработки: Trunk Based Development vs. GitFlow
Trunk Based Development
Trunk Based Development (Разработка на основе магистрали) — это концепция разработки, при которой основная работа ведется в одной ветке, называемой Master или Trunk.
- Feature ветки: Создаются от Master для разработки конкретной фичи.
- Master ветка: Содержит весь актуальный код, куда мерджатся фичи.
- Release ветка: Отводится от Master для подготовки релиза.
Процесс в Trunk Based Development:
- Создать фичер-ветку от Master.
- Разработать фичу.
- Перед мерджем выполнить rebase на последнюю версию Master.
- Смерджить обратно в Master.
- Для релиза просто отводится ветка от Master.
Этот подход считается максимально простым и удобным. Другие подходы, например, когда отдельные команды разрабатывают в своих ветках, менее удобны из-за сложности поддержки и отсутствия унификации.
Горячие исправления (Hotfixes)
Если после релиза обнаруживается баг, вместо создания новой Feature-ветки от Master, мерджа в Master и создания нового релиза, можно черепикнуть (cherry-pick) изменения с исправлением сразу в Release ветку. Это гораздо быстрее. Позже эти изменения также попадут в Master.
GitFlow (сравнение)
GitFlow отличается от Trunk Based Development использованием дополнительной ветки Develop. Процесс в GitFlow:
- Разработка фич ведется в фичер-ветках, которые мерджатся в ветку Develop.
- От Develop отводится ветка Release.
- Релизная ветка тестируется, выкатывается на тестовые стенды.
- После успешного релиза ветка Release мерджится в Master.
- Горячие исправления (Hotfixes) отводятся от Master, а затем мерджатся как в Release ветку, так и в Develop.
Отличия от Trunk Based Development:
- Фичи мерджатся в Develop, а не напрямую в Master.
- Релиз отводится от Develop, а не от Master.
- Master ветка не всегда содержит самый актуальный код разработки; она обновляется только после успешного релиза.
GitFlow может приводить к большему количеству конфликтов из-за наличия дополнительных веток. Некоторые считают его менее удобным.
Тестирование
Любой комит проходит множество тестов, количество которых зависит от размера проекта. Даже в маленьких проектах код нужно проверить на тестовом окружении перед выкаткой.
Основные типы тестов
- UT (Unit Tests): Проверяют работу конкретной функции или небольшого участка кода. Дается вход (input), проверяется выход (output). Примеры: встроенные тесты в Go.
- FT (Functional Tests): Проверяют работу всего сервиса или программы целиком. Имитируют взаимодействие пользователя или другого сервиса с тестируемым сервисом (например, отправка HTTP запроса). Включают проверку взаимодействия с другими сервисами или базами данных.
- Fuzzing Tests: Новая концепция (придумана в Google), заключается в подаче случайных (рандомных) данных на вход программе. Смысл не в простом рандоме чисел, а в подаче случайных байтов для проверки корректной обработки ошибок и некорректных входных данных.
- Perf Tests (Performance Tests): Сравнивают производительность сервиса до и после применения комита.
Важность тестов
Тесты — это ваш главный друг, а не враг. Они нужны не только для самопроверки, но и для того, чтобы никто в будущем не сломал логику вашего кода. Даже для очевидной логики (например, сортировки) стоит написать тест, чтобы отловить баг, который может появиться позже из-за изменений другого разработчика.
Существует концепция Test Driven Development (TDD), когда сначала пишутся тесты, а затем код. Важно найти баланс: программа не должна быть совсем без тестов, но и тестов не должно быть больше, чем самого кода. В некоторых случаях (простое изменение названия переменной, простая логика, которую видно в метриках/логах) тесты могут быть излишни.
Дополнительные аспекты тестирования
Тесты часто запускаются несколько раз.
- Под много систем: Проверка под разными операционными системами или конфигурациями.
- С санитайзерами: Проверка с использованием специальных инструментов.
Санитайзеры
Санитайзеры — это инструменты для обнаружения ошибок, связанных с памятью или потоками.
- ASAN (Address Sanitizer).
- MSAN (Memory Sanitizer).
- Thread Sanitizers (для многопоточных программ).
Санитайзеры работают, например, создавая “теневые байты” для каждого байта данных для проверки корректности обращений к памяти. Они сильно замедляют выполнение программы (может занимать вдвое больше памяти и дольше работать), что часто приводит к таймаутам. Решение — увеличение таймаутов для таких тестов.
Тесты могут собираться с различной информацией для отладки, например, Release with Debug Info(relvisdebinfo
).
Проблемы с большим количеством тестов
В огромных сервисах количество тестов может быть бесконечно большим.
- Muted тесты: Тесты, которые постоянно падают, не проходят и поэтому игнорируются в CI (Continuous Integration). Причины: разработчик уволился или ушел, другие разработчики сломали тест (возможно, не поведение программы, а ожидания теста), лень чинить старый тест, если написан новый.
- Timeout тесты: Тесты не укладываются в заданное время выполнения. Часто связано с санитайзерами.
Flaky тесты
Flaky тесты (флапающие тесты) — это тесты, которые иногда проходят, а иногда падают без видимой причины.Основные причины:
- Плохо написанный рандом: Использование рандома без seed’а, что приводит к невоспроизводимым результатам (например, тестирование логики, срабатывающей с небольшим шансом).
- Измерение времени выполнения или использование системного времени (
time
). - Обращения к внешним сервисам, которые могут быть нестабильны.
- Старые тесты, которые давно должны быть muted, но иногда проходят.
Чтобы определить, является ли тест падающим или flaky, он часто запускается несколько раз при провале.
Ограничения стандартных тестов
Стандартные тесты (UT, FT) не всегда достаточны для огромных сервисов, таких как поиск или реклама, которые работают с гигантскими базами данных и сложным поведением. Сложно создать маленькую фейковую базу данных, которая бы воспроизводила все сложные сценарии.
Специализированные системы тестирования
Для тестирования сложного поведения существуют отдельные команды, которые пишут специальные сервисы для тестирования. Например, в Яндексе есть большой отдел, который пишет тестирующую систему для рекламы.
Крупномасштабное тестирование (регрессионное)
Такие системы могут работать следующим образом:
- Собирается большой набор реальных запросов (например, 100 000).
- Поднимаются две виртуальные машины: на одной разворачивается текущая версия (trunk), на другой — версия с тестируемым изменением.
- 100 000 запросов отправляются в обе машины.
- Сравниваются ответы (должны быть одинаковыми) и производительность (никто не должен просесть). Это похоже на FT, но выполняется в масштабе, невозможном локально (нельзя поднять огромную базу данных и множество сервисов локально).
Проблемы с такими системами включают flakiness из-за нестабильности микросервисов, к которым обращается тестируемый сервис. Этим занимаются целые команды.
Верификация кода вне тестов
Логирование и Метрики
Помимо тестов, для уверенности в работе кода используются логирование и метрики. Это похоже на отладку с помощью cout
или cerr
на курсах, но в промышленных масштабах.
- Логирование: Запись информации о выполнении программы (например, фича запустилась, параметры фичи).
- Метрики: Запись числовых показателей (например, количество отсортированных элементов, их параметры). Логи и метрики позволяют смотреть в проде, как ведет себя код в реальном времени. Системы логирования и метрик сложны, требуют отдельных отделов для разработки и поддержки (например, обработка терабайтов логов с быстрой доставкой).
Алерты
Поверх логов и метрик настраиваются алерты, которые срабатывают при обнаружении аномалий, сигнализируя о возможных проблемах.
Тестовые окружения (Staging/Preprod/Mirrors)
Когда тестов, логов и метрик недостаточно, или когда сложно написать тесты (например, из-за сложной логики или зависимостей), код выкатывают на тестовые стенды.
- Preprod или Preretable: Окружение, куда выкатывается кандидат на продакшен-версию. Туда направляется небольшой процент реальных запросов (например, 1%) для проверки на небольшой выборке.
- Тестовые окружения или Mirrors: Отдельные тестовые сервисы, куда разработчики могут выкатывать что угодно для тестирования. Могут отражать часть реального трафика. Создание mirrors сложно для сервисов с реальным финансовым или физическим воздействием (реклама, маркетплейсы, такси), чтобы избежать нежелательных последствий.
Отладка (GDB)
На тестовых окружениях (mirrors) единственным способом отладки, если другие методы не помогли, является подключение GDB к запущенному сервису. Можно поставить брейкпоинт, отправлять запросы вручную или использовать специальные инструменты для воспроизведения сломанных запросов, и пошагово отлаживать. Знание GDB полезно в сложных ситуациях, когда баг проявляется только в большом тесте или на реальном трафике. Для отладки с GDB сервисы на mirrors собираются со специальным флагом (-g
), который добавляет отладочные символы.
Профилирование
Профилирование — это анализ производительности программы. Используются инструменты для построения графиков вызовов функций (например, Flame Graphs). Пример работы простого профайлера (Sampler): периодически останавливает программу (например, 100 раз в секунду) и снимает стек вызовов. По собранным стекам строятся графики, показывающие, сколько времени программа проводит в тех или иных функциях. Профилирование используется для поиска узких мест в коде, чтобы понять, какие части программы работают медленнее всего и где требуется оптимизация. Продовые сервисы часто собираются с флагом профилирования для автоматического сбора данных.
Процесс ревью кода (Code Review)
Ревью кода происходит только после того, как код прошел все тесты и визуальную проверку (например, по метрикам или на тестовом стенде). Нет смысла отправлять на ревью нерабочий код.
В процессе ревью другие разработчики оставляют комментарии. Если комментарии требуют значительных изменений (не просто очевидный рефакторинг), приходится повторять цикл: внесение изменений, выкатка на тестовое окружение, прогон тестов, повторное ревью. Этот процесс может быть очень долгим и итеративным (пример: 177 комментариев, 21 итерация ревью для сложного комита). В больших компаниях стараются оптимизировать этот процесс, но не всегда успешно.
Релизный цикл
Роль дежурных и релизных команд
После мерджа кода в главную ветку, его выкатка на продакшен осуществляется дежурными или специальными релизными командами. В крупных сервисах (например, реклама) есть целые команды, отвечающие за релизные циклы и стабилизацию кода (уменьшение количества ошибок и флагов, увеличение частоты релизов).
Процесс релиза
Релизы могут происходить по расписанию. Процесс включает:
- Отведение релизной ветки от Master (Trunk).
- Длительное тестирование релизной ветки на всех инструментах и тестовых стендах (может занимать недели).
- Выкатка релиза.
Поиск ломающего комита (бинпоиск)
Если в релизе (содержащем, например, 200 комитов) что-то сломалось, для поиска ломающего комита используется бинпоиск. Откатывается половина комитов, проверяется, присутствует ли баг. Затем процесс повторяется на той половине, где баг остался. Это может быть долго, так как каждый шаг требует запуска длительных тестов.
Благодаря усилиям команд стабилизации, частота релизов может быть увеличена (например, с двух недель до одного раза в день).
Этапы выкатки
Ваш комит сначала выкатывается на Preretable (Preprod), где некоторое время (например, день) наблюдают за его поведением. Если все хорошо, его выкатывают на Stable (Prod).
Сложные сценарии выкатки
- Беспилотники: После выкатки кода на preprod, его тестируют на реальной машине, которая ездит по улице. Только после этого его раскатывают на остальные машины.
- Внешние сервисы (Open Source): Например, ClickHouse, рожденный в Яндексе. Релизы делаются в open source, создается зеркальный комит на GitHub. Релизная ветка отводится, ожидается ночь, если все хорошо, релиз выпускается; иначе — откат и поиск бага бинпоиском.
- Облако: Тестовый стенд сложно выделить, так как все виртуалки с кодом находятся в самом облаке. При релизе выкатка происходит только в тестовую зону внутри облака. Код держится там много дней, чтобы отловить странное поведение на машинах, куда он выкатился.
А/Б эксперименты (A/B Testing)
Цель A/B тестирования
Некоторые изменения нельзя мерджить сразу или проверять только тестами. A/Б эксперименты нужны, чтобы честно измерить, как изменение влияет на ключевые метрики (например, трафик). Это позволяет отличить эффект вашего изменения от эффекта изменений других разработчиков в том же релизе.
Механизм A/B тестирования
Трафик разделяется на выборки. Например, для эксперимента на 5% трафика создаются две выборки по 5%:
- Экспериментальная группа: В ней включается тестируемое изменение.
- Контрольная группа: В ней изменение не включается. Наблюдают за разницей в метриках между экспериментальной и контрольной группами.
Риски без A/B тестирования
Некоторые патчи, даже прошедшие тесты, могут быть опасны при полной выкатке. Пример: изменение метрики взвешенной балансировки. Если новая метрика дает значительно больший “вес” по сравнению со старой, весь трафик может направиться на поды с новой версией, перегрузить их и вызвать каскадное падение всего сервиса. Такие случаи приводили к значительным финансовым потерям.
Выкатка по кластерам
Чтобы не положить весь сервис сразу, релизы раскатываются по кластерам — группам серверов. Сначала выкатывают на один кластер, проверяют, что все хорошо, затем на следующий и так далее. В облаке выкатка по кластерам также сложна, так как отказ кластера может затронуть пользователей, чьи сервисы размещены именно в этом дата-центре.
Микросервисная архитектура может позволить быстрее выкатывать и откатывать изменения для маленьких сервисов, так как последствия поломки менее масштабны.
Планирование задач и чтение кода
Откуда берутся задачи
После завершения задачи, следующая редко придумывается самим разработчиком, особенно на начальном этапе. Задачи назначает руководитель, который формирует их на основе планов своего руководителя, а тот — на основе планов своего руководителя, который определяет направление развития сервиса. Существует иерархия планирования задач.
Важно выполнять задачи, которые действительно нужны сервису, а не те, которые кажутся интересными или позволяют применить новые знания, но не приносят пользы.
Начало работы над задачей (чтение кода)
Получив тикет с описанием задачи, не стоит сразу писать код. Сначала нужно прочитать участок кода, в который будут вноситься изменения. Нередко код очень сложный. Важно соблюдать баланс: нельзя сразу идти спрашивать, что делает код, но если после прочтения все равно непонятно, нужно обратиться за помощью к коллегам, возможно, к автору кода. Сложности возникают, когда автор кода уволился, и никто не может объяснить его работу.
Документация
“Документация - это миф”
Документация как всеобъемлющее и актуальное описание кода практически не существует. Компании стараются ее поддерживать, но это сложно.
Почему документация не всегда актуальна/полна
- Огромный объем кодовой базы: Документирование каждой функции увеличило бы объем кода вдвое, что не имеет смысла. Документация описывает общую логику сервиса, но не детали реализации конкретных участков кода (алгоритмы, причины выбора).
- Разные представления разработчиков: То, что кажется очевидным одному, требует документации для другого. Документация может фокусироваться на продуктовой логике (“что делает код”) или на реализации (“как код это делает”). Идеальной документации нет.
- Приоритеты: Написание документации часто не оценивается при планировании задач, поэтому разработчики не уделяют ей время, если это не является обязательной практикой.
В результате, многие ответы на вопросы по сложным библиотекам или протоколам приходится искать не в документации, а в интернете.
Внутренние инструменты и библиотеки
В крупных компаниях часто приходится разрабатывать собственные инструменты и библиотеки, так как стандартные (например, Git или даже некоторые стандартные библиотеки C++) не выдерживают нагрузок или имеют недостатки. Например, в Яндексе редко используется стандартная библиотека C++ (STL) напрямую, она полностью обернута из-за проблем с многопоточностью. Есть отдельные команды, которые занимаются развитием этих внутренних инструментов.
Заключение
Весь процесс разработки, тестирования, релиза, наблюдения и планирования задач цикличен. Описанные процессы актуальны не только для разработчиков бэкенда, но и для других IT-специальностей (аналитиков, ML-разработчиков) с поправкой на специфику их работы.