Миграция Stream Router: ИИ ускорил рефакторинг в 3000 раз

Арнольд Уакин

Когда хранилище Stream Router упёрлось в жёсткие ограничения, нам пришлось переработать модель данных и перенести систему на новую архитектуру хранения — не прерывая обслуживание живого продакшн-трафика. Без инструментов на базе ИИ мы бы не уложились в отведённые сроки.

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

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

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

В Datadog мы каждую секунду принимаем огромные объёмы метрик в составе платформы, обрабатывающей свыше ста триллионов событий в день. Правильная маршрутизация этих данных не менее важна, чем их приём. Каждую точку данных нужно направить в нужный кластер Kafka, нужный топик и набор партиций, чтобы она корректно сохранилась и могла быть запрошена, — и эти решения о маршрутизации постоянно меняются по мере развития инфраструктуры. (Подробнее о полном конвейере метрик см. в нашем обзоре платформы метрик.)

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

Stream Router — внутренний сервис управляющего уровня (control plane) в Datadog — даёт эти ответы. Он сообщает продюсерам, куда писать, а читателям — откуда читать, и хранит историю того, где находились данные в каждый момент времени. Stream Router не производит и не потребляет сообщения Kafka в рантайме. Вместо этого он управляет решениями о маршрутизации, которые другие сервисы используют для настройки собственных продюсеров и консьюмеров Kafka. При таком масштабе решения о маршрутизации критичны для здоровья конвейера метрик. Неудачное изменение маршрута может иметь последствия далеко за пределами самого Stream Router.

От конфигурационного файла к управляющему уровню

В 2016 году маршрутизация конвейера метрик управлялась через трёхстрочный конфигурационный файл, распространявшийся на каждый сервис. По мере роста Datadog рос и этот файл — от нескольких строк до тысяч. Его редактировали вручную и выкатывали тоже вручную. И несмотря на существующие меры защиты, управление изменениями становилось всё более громоздким и операционно затратным при масштабировании.

Stream Router заменил этот процесс централизованным gRPC-сервисом с автоматической оркестрацией. Маршрутами управляли через API вместо правки файлов, а выкатки стали постепенными и автоматическими.

Для обеспечения высокой доступности и отказоустойчивости Stream Router был спроектирован с архитектурой итоговой согласованности (eventual consistency), разделяющей записи и чтения. Путь записи был основан на FoundationDB с моделью ключ-значение (KV), что хорошо вписывалось в инфраструктурную и операционную модель Datadog того времени. Но по мере роста системы эта модель начала демонстрировать свои ограничения. Путь чтения опирался на RocksDB, который обслуживал статические снимки пути записи для высоконагруженного запросного трафика.

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

Архитектура Stream Router: путь записи от сервиса администрирования через базу данных к объектному хранилищу; дистрибьюторы обслуживают запросы чтения из снимков в памяти

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

Когда KV-модель перестала масштабироваться

Чтобы понять, почему KV-хранилище упёрлось в ограничения, нужно разобраться, что именно хранит Stream Router.

Маршрут (route) — базовая единица работы: он отображает полезную нагрузку клиента на конкретный поток Kafka. Но маршрут не существует в изоляции. Он ссылается на стратегию шардирования (sharding strategy), которая указывает продюсерам, как распределять данные по топику. Маршрут активируется правилом (rule), которое управляет тем, когда и как он вступает в силу для конкретного клиента.

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

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

Цена за это была вполне ощутимой. Часть операций упиралась в ограничения на размер транзакций FoundationDB. Мы сначала изучили очевидный выход: заменить FoundationDB на PostgreSQL, сохранив те же паттерны KV-доступа. Это тоже не помогло. Самые тяжёлые операции, по оценкам, занимали бы 45 минут, поскольку по-прежнему требовали тысяч последовательных обращений к базе данных.

Узкое место находилось в модели данных и построенной вокруг неё логике приложения, а не в самой базе данных.

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

Проектирование схемы, отражающей данные

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

Упрощённая схема ниже передаёт ключевую структуру: потоки и стратегии шардирования связаны с маршрутами, которые в свою очередь связаны с правилами. Каждая сущность напрямую соответствует доменным концепциям, введённым ранее, а связи через внешние ключи заменяют то, что раньше приходилось восстанавливать в коде приложения. (Показаны только первичные и внешние ключи; каждая таблица содержит дополнительные столбцы, опущенные для ясности.)

Диаграмма сущность-связь схемы Stream Router: потоки и стратегии шардирования связаны с маршрутами через внешние ключи; маршруты связаны с правилами

Для пути записи выбор был очевидным. В Datadog создавалась самоуправляемая платформа PostgreSQL, предоставлявшая необходимую реляционную семантику и транзакционную модель, что делало PostgreSQL естественным выбором.

Путь чтения потребовал более глубокого осмысления. Уровень обслуживания чтения загружается из статических снимков и обслуживает запросы из памяти, поэтому нам требовалась встраиваемая база данных. Первым кандидатом был SQLite, но наша схема использует столбцы-массивы, которые SQLite не поддерживает нативно. DuckDB решил обе проблемы: он нативно работает с массивами и имеет SQL-диалект, близко совместимый с PostgreSQL. Это позволило нам разделить логику запросов между обоими движками, а не поддерживать две отдельные реализации.

Это была та часть проекта, которую вёл человек. Всё, что последовало дальше, — метод за методом рефакторинг — и есть то место, где в картину вошёл ИИ.

Что сделало миграцию безопасной

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

Во-первых, модульный код. В Datadog мы стремимся к модульному программному обеспечению, и Stream Router не исключение. Слой хранения находился за внутренним интерфейсом, который мы называем Controller. Существующая реализация использовала FoundationDB; написание новой означало создание второй реализации того же интерфейса поверх PostgreSQL. Остальная часть системы не нуждалась в изменениях.

Во-вторых, тщательный набор тестов. Учитывая критичность того, чем мы занимаемся, мы серьёзно вкладываемся в тестируемый код. Каждый метод Controller был покрыт сквозными тестами с чёткими ожиданиями относительно того, как должно выглядеть состояние хранилища после каждой операции. Этот набор тестов стал бинарным критерием успеха для каждого изменения, сгенерированного ИИ.

В-третьих, параллельная инфраструктура. Реми Кализт, один из наших опытных инженеров, создал сине-зелёную (blue/green) архитектуру развёртывания — по сути A/B-тестирование, но для инфраструктуры. Два полностью независимых экземпляра Stream Router работают бок о бок, обслуживая одни и те же запросы, а клиенты направляются в тот или иной на основе флагов функций. Специальный сервис-валидатор работает в каждом кластере, периодически сравнивая ответы маршрутизации от обоих экземпляров и немедленно оповещая команду о любом расхождении.

Именно эти три составляющие — модульные интерфейсы, исчерпывающие тесты и параллельная инфраструктура — позволили нам безопаснее передать большую часть реализации ИИ.

Картографирование кодовой базы, затем реализация метод за методом

Чтобы ИИ-ассистированный рефакторинг был управляемым, нам требовался способ дать моделям чёткое, понятное человеку представление о том, что делала существующая система и зачем.

Фаза 1: Построение карты

До начала какого-либо рефакторинга мы использовали Claude для построения структурной карты старой кодовой базы. Мы передали Claude существующий KV-контроллер — большой, глубоко вложенный стек вызовов, тесно связанный со слоем хранения, — и попросили создать Markdown-документы, описывающие назначение каждой ключевой функции. Не то, что код делает строчка за строчкой, а зачем он существует и какое поведение защищает.

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

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

Фаза 2: Заглушка, тест, итерация

Основной рабочий процесс рефакторинга следовал воспроизводимому циклу:

  1. Выбрать метод из старого KV-контроллера.

  2. Создать заглушку в новом контроллере на PostgreSQL, возвращающую ошибку.

  3. Запустить набор сквозных тестов.

  4. Передать вывод падающего теста в Claude или Cursor вместе с контекстом: старой реализацией, новой схемой и Markdown-документацией из Фазы 1.

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

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

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

Что не работало — это промпты на более высоком уровне. Когда мы давали моделям более широкие задачи с бо́льшим контекстом, результаты неизменно были хуже: перегрузка контекста, выдуманные интерфейсы, код, который компилировался, но не соответствовал ожидаемому поведению. Ограничение каждого промпта одним методом или конкретным падающим тестом давало значительно лучшие результаты.

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

Фаза 3: Сине-зелёная валидация в продакшне

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

Мы развернули систему на PostgreSQL как «синюю» рядом с существующей «зелёной» на FoundationDB. Сервис-валидатор непрерывно сравнивал их ответы каждые 30 секунд.

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

Где ИИ не справился

Мы хотим честно рассказать об ограничениях.

Claude и Cursor взяли на себя основную рутинную работу: извлечение логики валидации, перевод метода за методом из одной модели данных в другую, подключение к новой схеме. Но когда дело доходило до производительности SQL, они неизменно выдавали корректные, но неоптимальные запросы.

Нишевые оптимизации — батчинг, приёмы с UNNEST, общие табличные выражения — требовали участия человека. Запросы, сгенерированные ИИ, возвращали правильные результаты, но делали гораздо больше обращений к базе, чем необходимо. Оптимизированные версии мы писали сами, и когда модели видели паттерн, они могли воспроизводить его в последующих методах. Но самостоятельно обнаружить эти паттерны они не могли, хотя те давно описаны в литературе.

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

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

Что изменилось после миграции

Проект прошёл путь от первоначального проектирования до продакшна примерно за 3 месяца (декабрь 2025 — февраль 2026), а основное подтверждение концепции было достигнуто за 4 недели.

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

Результаты оказались значительными:

  • Операции, которые, по оценкам, должны были выполняться 45 минут, теперь завершаются примерно за 1 секунду (почти в 3000 раз быстрее). API-вызовы, прежде заканчивавшиеся по таймауту, теперь завершаются примерно за секунду.

  • Набор данных маршрутизации в PostgreSQL уменьшился в 40 раз, а статические снимки DuckDB стали ещё меньше. KV-модель требовала поддерживать связи в коде приложения; PostgreSQL и DuckDB делают это нативно.

  • Общие задержки снизились на один или несколько порядков — с сотен миллисекунд до единиц. PostgreSQL формирует эффективные планы запросов и проталкивает предикаты на сервер через клаузы WHERE, а не восстанавливает связи в коде приложения только для того, чтобы потом их отфильтровать.

  • Потребление CPU и памяти снизилось по всем подам — как на пути чтения, так и на пути записи.

  • Затраты на базу данных снизились на 90% после вывода из эксплуатации управляемых кластеров FoundationDB.

  • Развёртывание в продакшне прошло без инцидентов.

Что мы узнали, мигрируя живую систему с помощью ИИ

Миграция Stream Router — это кейс того, что нужно для целенаправленной эволюции критической производственной системы. Архитектура была спроектирована людьми: реляционная схема, соответствующая предметной области, PostgreSQL и DuckDB, выбранные по конкретным техническим причинам, и сине-зелёная стратегия валидации, которая работала на живых данных несколько недель. ИИ ускорил реализацию: рутинную работу метод за методом по переводу чётко описанного поведения из одной модели данных в другую. Он не принимал архитектурных решений, но мог быть полезен, когда его специально направляли на поиск расхождений. Главное: узкие промпты в сочетании с набором падающих тестов — это то, с чем он справлялся отлично.

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

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

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

© 2026 meganuke