<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/">
  <channel>
    <language>ru</language>
    <title>OpenTelemetry</title>
    <description>OpenTelemetry — это открытая платформа для наблюдения за распределенными системами, включающая инструменты и стандарты для сбора, обработки и экспорта телеметрических данных. Она объединяет логирование, метрики и трейсинг, позволяя эффективно мониторить и отлаживать приложения. OpenTelemetry поддерживает интеграцию с различными системами мониторинга и аналитики, помогая повысить надежность и производительность сложных IT-инфраструктур.</description>
    <link>https://tproger.ru/tag/opentelemetry</link>
    <atom:link href="https://tproger.ru/tag/opentelemetry/feed" rel="self" type="application/rss+xml"/>
    <lastBuildDate>Sat, 03 Oct 2026 21:21:29 GMT</lastBuildDate>
    <sy:updatePeriod>hourly</sy:updatePeriod>
    <sy:updateFrequency>12</sy:updateFrequency>
    <image>
      <title>OpenTelemetry</title>
      <link>https://tproger.ru</link>
      <url>https://tproger.ru/apple-touch-icon.png</url>
    </image>
    <item>
      <title>Наблюдаемость: где на самом деле теряются миллисекунды</title>
      <link>https://tproger.ru/articles/nablyudaemost-gde-na-samom-dele-teryayutsya-millisekundy</link>
      <comments>https://tproger.ru/articles/nablyudaemost-gde-na-samom-dele-teryayutsya-millisekundy?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Tproger]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/nablyudaemost-gde-na-samom-dele-teryayutsya-millisekundy</guid>
      <description><![CDATA[<p>Как собрать трассы без правки кода через eBPF, превратить их в метрики по медленным запросам и не принять смену методики за регрессию. Разбираем три слоя с цифрами.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/nablyudaemost-gde-na-samom-dele-teryayutsya-millisekundy">Наблюдаемость: где на самом деле теряются миллисекунды</a>»</p>]]></description>
      <category><![CDATA[Производительность]]></category>
      <category><![CDATA[Микросервисы]]></category>
      <category><![CDATA[DevOps]]></category>
      <category><![CDATA[Golang]]></category>
      <category><![CDATA[OpenTelemetry]]></category>
      <category><![CDATA[Grafana]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Wed, 09 Sep 2026 13:00:26 GMT</pubDate>
      <content:encoded><![CDATA[<p>Дашборд краснеет, а причина не находится. У наблюдаемости, которая так себя ведёт, обычно три независимые беды: телеметрия собрана не везде, собранное не переведено в решения, а полученные числа прочитаны неверно.</p><p>Разбираем все три слоя: как получить трассировку, не переписывая сервисы, как превратить трассы в метрики, по которым можно действовать, и как не принять смену методики измерения за изменение продукта.</p><p>Трассировка через eBPF снимает налог на ручное инструментирование, но требует ядра не ниже 5.8, несрезанной таблицы символов и знания устройства планировщика Go.</p><p>При 50 000 запросах в секунду и четырёх точках съёма на запрос получается 200 000 входов в ядро ежесекундно и потолок расхода в 200–600 мс процессорного времени на ядро.</p><p>Медленный запрос это не одна проблема, а пять разных: лишняя работа, борьба за ресурсы, давление среды, деградация плана и патологические шаблоны вроде N+1.</p><p>Инструменты базы говорят, что дорого внутри неё, но не говорят, какой сервис это вызвал и был ли запрос пользовательским. Этот контекст приносят трассы.</p><p>Версия браузера, тип навигации, библиотека сбора данных и версия инструментирования входят в определение метрики: смешав их в одном графике, вы увидите изменение измерения вместо изменения продукта.</p><h2>Слой первый наблюдаемости: собрать данные, не трогая код</h2><p>Ручное инструментирование обходится дороже, чем кажется на старте. Каждая точка вызова библиотеки, каждая ветка передачи контекста и каждое извлечение сопутствующих данных — это код, который расходится с реальностью, забывается в горячем пути и стоит процессорного времени на высокой частоте запросов.</p><p>Альтернатива, ставшая эксплуатационно пригодной за последние пару лет, — <a href="https://dev.to/neeraj_singhi_golang/ebpf-powered-request-tracing-in-go-microservices-without-instrumentation-tax-34kf">подключение зондов eBPF напрямую к символам рантайма</a> и точкам входа библиотек HTTP и gRPC. Трассы восстанавливаются из событий ядра и пользовательского пространства, а файл бинарника на диске не меняется вовсе: правки идут только в памяти работающего процесса, точечно и обратимо.</p><h3>Почему с Go это сложнее, чем с C</h3><p>Зонд работает так: в заданное смещение работающего бинарника ставится инструкция прерывания, при попадании в неё ядро останавливает поток, выполняет свою программу и возобновляет исполнение. Для C и Rust это ложится на прологи функций почти без остатка. Go добавляет три осложнения.</p><p><b>Планирование горутин.</b> Планировщик Go раскладывает множество горутин на меньшее число системных потоков. Один HTTP-запрос может обрабатываться горутиной на одном потоке в момент срабатывания зонда и переехать на другой поток до записи ответа. Наивный зонд, читающий идентификатор потока или процесса, теряет непрерывность на этом переезде. Правильный якорь — идентификатор горутины, а достать его можно либо через карту, построенную по отладочной информации, либо вычислением фиксированного смещения от указателя, лежащего в регистре локального хранилища потока.</p><p><b>Соглашение о вызовах.</b> В Go 1.17 аргументы функций переехали со стека в регистры, причём сначала только на amd64; на arm64 и ppc64 это произошло версией позже. Программа зонда, написанная под прежнее соглашение и читающая контекст со смещения от указателя стека, на новых бинарниках прочитает мусор. Значит, трассировщику нужна либо логика зондов под каждую минорную версию языка, либо вычисление места по отладочной информации самого бинарника.</p><p><b>Встраивание функций.</b> Компилятор Go агрессивно встраивает мелкие функции, и нужный символ может просто отсутствовать по ожидаемому адресу. Зонд, поставленный не туда, даёт пропущенные отрезки трассы или испорченные аргументы, причём молча.</p><p><b>Отдельная ловушка при разборе событий:</b><br />Структура на стороне потребителя должна побайтово совпадать со структурой в программе зонда, включая выравнивание. Расхождение в один байт приводит к тому, что все поля после первого декодируются неправильно, а внешне это выглядит как случайный мусор в трассах.</p><h3>Главная нерешённая проблема: передача контекста между сервисами</h3><p>Если нельзя вписать заголовок в коде приложения, как передать идентификатор трассы дальше по цепочке вызовов? Есть три подхода, и практически применим один.</p><ul><li>Писать заголовок прямо в пакет на уровне сетевого интерфейса. Требует расширенных привилегий и работает только для нешифрованного HTTP/1.1: шифрование выполняется выше уровня сокета, и программа видит уже зашифрованные байты.</li><li>Пропускать исходящие вызовы через локальный вспомогательный прокси, который держит соответствие идентификатора горутины и контекста трассы и вставляет заголовки перед пересылкой. Добавляется сетевой переход, зато шифрование не ломается.</li><li>Править карту заголовков прямо в памяти процесса. Соответствующая функция ядра помечена как опасная и действительно способна повредить память: каждая её загрузка пишет предупреждение в журнал ядра, работа требует широкой привилегии CAP_SYS_ADMIN, а в режиме блокировки ядра она запрещена совсем.</li></ul><p>Единственный вариант, который одновременно совместим с шифрованием, безопасен и разворачивается где угодно, — второй. Его цена в задержке составляет обычно меньше 100 мкс на переход через локальную петлю, что приемлемо, когда сами измеряемые операции занимают миллисекунды.</p><h3>Что ломается в продакшене</h3><ul><li>Идентификаторы горутин переиспользуются. При высокой конкурентности номер может уйти на новую горутину раньше, чем потребитель разобрал события старой.</li><li>Обновление бинарника сдвигает смещения символов. Зонды старого отображения ядро снимает само, и новый бинарник какое-то время работает без трассировки. Слежение за изменением файла сокращает разрыв до секунд, но не убирает его.</li><li>Требования к ядру. Кольцевые буферы нужны ядру от 5.8, а переносимая компиляция зондов требует 5.4 с включённой отладочной информацией о типах. Матрицу ядер стоит проверить до внедрения, а не после.</li><li>Срезанные бинарники. Сборка без таблицы символов и отладочной информации лишает трассировщик возможности сопоставить имена функций со смещениями. Компромисс: оставить хотя бы таблицу символов, заплатив примерно 10–15% размера бинарника.</li></ul><h3>Сколько это стоит в процессорном времени</h3><p>Зонды не бесплатны: каждое срабатывание это программное прерывание с входом в ядро. При 50 000 запросах в секунду и четырёх точках съёма на запрос получается 200 000 входов ежесекундно. Опубликованные замеры дают порядка 1–3 мкс на срабатывание на современном оборудовании, то есть верхняя оценка расхода составляет 200–600 мс процессорного времени в секунду на одно ядро.</p><p>Цифра ощутимая, но сравнивать её стоит с альтернативой, а не с нулём. Автор разбора оценивает эту альтернативу в десятую часть рабочего времени разработчиков, уходящую на сопровождение кода инструментирования. Потребителя событий при этом лучше держать на отдельной горутине, привязанной к ядру, которое не обслуживает запросы, чтобы выделение памяти под отрезки трасс не мешало обработке.</p><p>Правильный вывод не «заменить инструментирование зондами», а «использовать оба». Зонды дают сплошное покрытие и базовое распределение задержек без усилий разработчиков, включая сторонние бинарники, которые вы не можете изменить. Ручное инструментирование даёт смысловое наполнение: идентификатор пользователя, арендатора, флаг функциональности — то, чего из сырых байтов HTTP не синтезировать. Наиболее точные схемы наблюдаемости держат оба слоя, причём слой зондов работает проверкой на непротиворечивость для отрезков, которые приложение теряет под нагрузкой.</p><h2>Слой второй: превратить трассы в решения</h2><p>Собранная телеметрия сама по себе ничего не улучшает. Больше телеметрии означает больше объектов для разглядывания, а не больше понимания. Полезной она становится, когда из неё извлечены закономерности, на которые можно действовать.</p><h3>«Медленный запрос» это пять разных диагнозов</h3><p>Прежде чем что-то чинить, стоит понять, чем именно болен запрос. <a href="https://www.cncf.io/blog/2026/08/21/how-to-turn-slow-queries-into-actionable-reliability-metrics-with-opentelemetry/">Разбор CNCF</a> раскладывает медленные запросы к базе на пять непохожих причин:</p><ul><li>Лишняя работа. Обычно это полный перебор таблицы из-за отсутствующего или неприменимого индекса. Выборка по идентификатору клиента без индекса растёт с 20 мс на десяти тысячах строк до минут на десяти миллионах. Запрос не менялся, изменился объём данных.</li><li>Борьба за ресурсы. Идеально оптимизированный запрос стоит в ожидании блокировок или свободного соединения. Запрос, проводящий 95% времени в ожидании блокировки, оптимизацией текста не лечится: ему нужна перекройка транзакций.</li><li>Давление среды. Насыщение процессора, узкое место ввода-вывода и нехватка памяти замедляют любой запрос с любым планом: тот же текст запроса и тот же план на нагруженной машине отработает в разы дольше, чем на свободной, и оптимизировать тут нечего.</li><li>Деградация плана. Данные и текст запроса те же, а план исполнения изменился. Устаревшая статистика после массовой загрузки заставляет планировщик выбрать заведомо плохую стратегию.</li><li>Патологические шаблоны. Проблема N+1 выполняет сотню быстрых запросов по две миллисекунды последовательно, добавляя двести миллисекунд задержки на одно только выполнение, не считая сетевых издержек на каждый обмен с базой. Ни один запрос не является медленным, а шаблон катастрофичен и в журнал медленных запросов не попадает вовсе.</li></ul><h3>Чего не хватает встроенным инструментам базы</h3><p>Журналы медленных запросов, хранилища запросов и разбор планов прекрасно отвечают на вопрос, что дорого внутри базы. Они не отвечают на вопросы, которые задаёт дежурный инженер: какой сервис это вызвал, пользовательская это работа или фоновая, связано ли это со всплеском задержки, который сейчас расследуется.</p><p>Этот контекст приносит распределённая трассировка: каждый отрезок обращения к базе вложен в контекст запроса и знает сервис, конечную точку и инициатора. Вместо того чтобы вручную сшивать журналы базы с трассами постфактум, медленные запросы анализируются прямо из трасс со всем прикладным контекстом внутри.</p><p>Дальше из отрезков трасс выводятся метрики, и делается это в три приёма: сперва простое обнаружение медленных запросов, затем взвешивание по трафику, чтобы понять, что даст наибольший выигрыш при ускорении, и наконец поиск аномалий для дежурства. Первые два отвечают на вопрос оптимизации, третий — на вопрос реагирования на инцидент, и путать их не стоит: это разные задачи с разными приоритетами.</p><h2>Слой третий: не принять смену измерения за изменение продукта</h2><p>Третья ошибка самая обидная, потому что данные при этом верны. Метрика может быть абсолютно точной, а рассказанная по ней история — совершенно неверной.</p><p>Дальше примеры пойдут из веб-производительности, потому что там эта ошибка задокументирована лучше всего. Механика же общая: любой дашборд, где смешаны серии с разных популяций или с разных версий сбора, ведёт себя одинаково, будь то задержки бэкенда или отрисовка страницы.</p><h3>Общий тренд не объясняет вашу просадку</h3><p>В июньском наборе данных о реальных пользователях Chrome, опубликованном в середине июля, доля источников с хорошей отрисовкой основного содержимого упала до 67,7%, с хорошей задержкой отклика на взаимодействие до 85,9%, а доля проходящих все три ключевых показателя до 55,3%. Исключением оказалась стабильность вёрстки, поднявшаяся до 81,4%.</p><p>Google объяснил движение сезонностью, сравнив его с прошлым годом. Контекст полезный, но он не является доказательством того, что у вас локально ничего не сломалось. Релиз, рекламная кампания, изменение формы согласия, сдвиг в составе устройств или новый сторонний скрипт способны совпасть с общеотраслевым движением.</p><p>Гарри Робертс формулирует критерий проверки прямо: <a href="https://csswizardry.com/2026/07/web-perf-wednesday-002-the-metrics-dont-tell-the-whole-story/">если общая доля прошедших упала на 1,2 процентного пункта, а конкретный сайт просел на четыре, широкий тренд разницу не объяснил</a>. Верно и обратное: если сайт почти идеально повторяет рынок по браузерам и устройствам, срочное расследование в коде потратит время всей команды впустую.</p><p>Правильное сравнение поэтому не «этот месяц против прошлого». Сравнивайте движение сайта с общей дельтой, разделяйте мобильные и настольные устройства, смотрите на показатели отрисовки и отклика по отдельности и разбирайте важные шаблоны страниц, а не источник целиком.</p><h3>Изменение методики выглядит как изменение продукта</h3><p>Дальше будет сложнее. По мере того как браузеры начинают показывать переходы внутри одностраничных приложений, которые метрики первоначальной загрузки пропускали, на одном дашборде окажутся жёсткие навигации, мягкие навигации браузера и собственные виртуальные страницы приложения, причём каждая серия собрана с разной популяции пользователей.</p><p>Смешав их без оговорок, вы получите скачок графика, вызванный сменой методики, и потратите неделю на поиск несуществующей регрессии. Отсюда практическое требование: версия браузера, тип навигации, библиотека сбора данных и версия инструментирования перестают быть служебными деталями и становятся частью определения метрики. Дашборд должен нести отметки релизов, состав трафика и устройств и внятное указание, какую популяцию представляет каждый график.</p><h3>Компенсация это не исправление</h3><p>Ещё одна ловушка того же рода: браузеры учатся скрывать последствия проблем. Привязка прокрутки к содержимому подправляет позицию, когда над областью просмотра что-то вставилось или исчезло, и читателя перестаёт выбрасывать с того места, которое он разглядывал.</p><p>Улучшение реальное, но документ по-прежнему перевёрстывается, если у картинки не заданы размеры или объявление меняет высоту. Браузер убирает одно видимое следствие, а причина остаётся на месте. Проверять после такого изменения стоит липкие шапки, ленты, интерфейс согласия и встроенные блоки, а чинить всё равно вёрстку, а не полагаться на маскировку.</p><p>Тот же разрыв виден и в метрике отклика. Большинство команд сегодня способны сказать, плохой у них отклик или нет. Заметно меньше способны сказать, какое именно взаимодействие в этом виновато, на каком шаге пользовательского пути оно случилось и что его задержало. Весь разбор Гарри Робертса именно про этот разрыв между наличием числа и наличием понимания.</p><h2>Что забрать с собой</h2><p>Три слоя решают три разные задачи, и провал на любом из них выглядит одинаково: дашборд есть, ответа нет. Без покрытия вы не видите части системы. Без перевода трасс в метрики видите всё и не понимаете ничего. Без дисциплины в чтении принимаете смену методики за регрессию.</p><p>Начинать проще всего с третьего слоя: он не требует ни новой инфраструктуры, ни единой строчки кода, только дисциплины чтения. Сверьте движение своих метрик с движением рынка и убедитесь, что расследуете настоящую регрессию, а не смену методики. Дальше уже видно, чего не хватает: покрытия или перевода трасс в решения.</p><p>Материалы разбора: <a href="https://dev.to/neeraj_singhi_golang/ebpf-powered-request-tracing-in-go-microservices-without-instrumentation-tax-34kf">устройство трассировки Go-сервисов через eBPF</a>, <a href="https://www.cncf.io/blog/2026/08/21/how-to-turn-slow-queries-into-actionable-reliability-metrics-with-opentelemetry/">перевод медленных запросов в метрики надёжности</a> и <a href="https://csswizardry.com/2026/07/web-perf-wednesday-002-the-metrics-dont-tell-the-whole-story/">разбор того, как метрики вводят в заблуждение</a>.</p>]]></content:encoded>
    </item>
    <item>
      <title>Как приручить legacy-код: безопасная модернизация без заморозки фич</title>
      <link>https://tproger.ru/articles/kak-priruchit-legacy-kod-bezopasnaya-modernizaciya-bez-zamorozki</link>
      <comments>https://tproger.ru/articles/kak-priruchit-legacy-kod-bezopasnaya-modernizaciya-bez-zamorozki?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[KODE]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/kak-priruchit-legacy-kod-bezopasnaya-modernizaciya-bez-zamorozki</guid>
      <description><![CDATA[<p>Как модернизировать legacy-код без остановки продукта: Strangler Fig Pattern, feature flags, shadow testing и безопасная миграция данных. Практика и антипаттерны.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/kak-priruchit-legacy-kod-bezopasnaya-modernizaciya-bez-zamorozki">Как приручить legacy-код: безопасная модернизация без заморозки фич</a>»</p>]]></description>
      <category><![CDATA[Интернет]]></category>
      <category><![CDATA[API]]></category>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Git]]></category>
      <category><![CDATA[Опрос]]></category>
      <category><![CDATA[Python]]></category>
      <category><![CDATA[Ruby]]></category>
      <category><![CDATA[MySQL]]></category>
      <category><![CDATA[Практика]]></category>
      <category><![CDATA[SQL]]></category>
      <category><![CDATA[Рефакторинг]]></category>
      <category><![CDATA[Работа]]></category>
      <category><![CDATA[Tor]]></category>
      <category><![CDATA[GitHub]]></category>
      <category><![CDATA[Боты]]></category>
      <category><![CDATA[HR]]></category>
      <category><![CDATA[MIT]]></category>
      <category><![CDATA[Финтех]]></category>
      <category><![CDATA[ICO]]></category>
      <category><![CDATA[Redis]]></category>
      <category><![CDATA[QA]]></category>
      <category><![CDATA[Бэкенд]]></category>
      <category><![CDATA[Twitter]]></category>
      <category><![CDATA[Игра]]></category>
      <category><![CDATA[Приложение]]></category>
      <category><![CDATA[Сервер]]></category>
      <category><![CDATA[Kubernetes]]></category>
      <category><![CDATA[Библиотеки]]></category>
      <category><![CDATA[аналитик]]></category>
      <category><![CDATA[OpenTelemetry]]></category>
      <category><![CDATA[PostgreSQL]]></category>
      <category><![CDATA[DeFi]]></category>
      <category><![CDATA[Grafana]]></category>
      <category><![CDATA[Бизнес]]></category>
      <category><![CDATA[Legacy]]></category>
      <category><![CDATA[Техника]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Tue, 23 Jun 2026 06:16:57 GMT</pubDate>
      <content:encoded><![CDATA[<p>Legacy-код — одна из самых болезненных тем в инженерных командах. Обычно все понимают, что система устарела: архитектура мешает быстро выпускать изменения, новые фичи приходится встраивать через обходные пути, тесты либо неполные, либо отсутствуют, а любое изменение в одном модуле неожиданно ломает другой.</p><p>Но при этом к такому коду часто боятся прикасаться. И не без причины. В старых системах редко бывает понятная карта зависимостей. Документация устарела, часть знаний живет только в головах нескольких разработчиков, а бизнес при этом продолжает ждать новых релизов, интеграций и продуктовых экспериментов.</p><p>Так появляется классическая ловушка legacy: систему надо модернизировать, но остановить развитие нельзя. Переписать всё с нуля страшно, поддерживать как есть — всё дороже. В результате продукт обрастает временными решениями, скорость разработки падает, а стоимость каждого следующего изменения растет.</p><p>Хорошая новость в том, что модернизация legacy-кода не обязана быть большим взрывом. Старую систему можно менять постепенно, сохраняя рабочий продукт, не замораживая фичи и не устраивая один критический релиз, от которого зависит всё.</p><h2>Почему Big Bang-переписывание чаще всего заканчивается плохо</h2><p>Когда команда долго живет с устаревшей системой, идея переписать всё с нуля выглядит очень соблазнительно. Кажется, что можно наконец избавиться от технического долга, выбрать нормальную архитектуру, перепроектировать модули, покрыть всё тестами и начать «правильно».</p><p>На старте такой план часто звучит логично. Особенно если текущая система действительно мешает развитию. Например, мобильное приложение растет, у него уже миллионы пользователей, бэкенд написан несколько лет назад как монолит, а каждая новая фича требует изменений в десятке мест. Команда устала чинить регрессии, бизнес устал ждать, и всем хочется «один раз нормально переписать».</p><p>Проблема не в самой идее переписывания, а в условиях, при которых оно проваливается. Большой риск возникает, когда совпадают четыре фактора: переписывание занимает много месяцев, в это время бизнес продолжает развивать старую систему, новая версия покрывает сразу большую часть функциональности, а откат связан с миграцией данных. Если все четыре пункта присутствуют, Big Bang почти гарантированно превратится в долгий и дорогой проект.</p><p>Допустим, команда решила переписать модуль заказов в e-commerce-продукте. В старой версии есть корзина, промокоды, доставка и оплата. Команда планирует за полгода сделать новый сервис заказов. Но за эти полгода бизнес добавляет подписки, подарочные сертификаты, частичную оплату бонусами и новую логику возвратов. В итоге новая система, которую проектировали под старые требования, к моменту релиза уже нуждается в доработке.</p><p>Есть и другая проблема: большой релиз почти всегда несет максимальный риск. Если вы заменяете крупный кусок системы целиком, ошибка влияет сразу на большую часть пользователей. Откат тоже становится сложным, потому что новая логика уже связана с новыми данными, контрактами и интеграциями.</p><p>Big Bang всё-таки бывает оправдан — но в узких условиях. Если кодовая база молодая (год-два), пользователей мало, у системы нет критичного состояния в БД и продукт можно временно заморозить или вести в обоих контурах параллельно, полное переписывание может оказаться дешевле постепенной миграции. Это редкая ситуация, и она быстро исчезает по мере роста продукта. В зрелых системах безопаснее работает другой подход — постепенная архитектурная эволюция.</p><h2>Пример: как команда переписала сервис документов и потеряла полгода</h2><p>Команда сопровождала сервис — старый модуль на aiohttp с Pydantic v1, через который проходила вся обработка путевых листов и актов осмотра транспорта. Сервис существовал шесть лет, был покрыт тестами фрагментарно, а его API использовали мобильное приложение водителей, диспетчерская веб-панель и пакетный импорт.</p><p>Команда решила переписать сервис целиком: перейти на FastAPI, обновить Pydantic до v2, заодно почистить контракты и заменить внутреннее хранилище документов с MongoDB на PostgreSQL. План был рассчитан на четыре месяца.</p><p>Через восемь месяцев проект всё ещё не был готов к выкатке, а к десятому месяцу команда откатила миграцию полностью. Причин было несколько.</p><p>Во-первых, переписывание шло параллельно с продуктовой разработкой. За время миграции бизнес добавил два новых типа документов и изменил правила подписи актов. Новая система проектировалась под старые требования и к моменту готовности уже не соответствовала продукту.</p><p>Во-вторых, команда не написала характеристических тестов. Поведение «как есть» нигде не было зафиксировано, и расхождения находились только в продакшене после переключения.</p><p>В-третьих, у старого сервиса были скрытые побочные эффекты, о которых никто не помнил. При смене статуса документа публиковал событие в Kafka, которое читал биллинг и сервис аналитики. В новой реализации это поведение не было воспроизведено, потому что в коде оно выглядело как «лишний» вызов. После переключения биллинг перестал получать события, и расхождение обнаружили только через две недели — по жалобе финансового отдела.</p><p>В-четвёртых, переключение было сделано «в лоб»: маршрут в API Gateway просто перенаправили на новый сервис. Фича-флага не было, теневого запуска не было, плана отката не было. Когда выяснилось, что новый сервис строже валидирует исторические форматы документов и отклоняет часть старых записей, быстро вернуться на старую реализацию не получилось — её к тому моменту уже отключили на стенде, а в БД успели уйти записи в новом формате.</p><p>В итоге миграцию свернули, потратив около десяти человеко-месяцев и потеряв доверие бизнеса. Сервис до сих пор работает в исходной реализации, а команда переходит к плану, описанному ниже.</p><h2>Strangler Fig Pattern: как заменить систему по частям</h2><p>Один из самых практичных подходов к модернизации legacy-кода — Strangler Fig Pattern. В софтверном виде паттерн был сформулирован Мартином Фаулером в 2004 году под названием StranglerFigApplication. Идея проста: не переписывать систему целиком, а постепенно выносить отдельные части в новую реализацию.</p><p>Название пришло из биологии. Фикус-душитель растет вокруг дерева-хозяина и постепенно вытесняет его. В архитектуре принцип похожий: старая система продолжает работать, новая функциональность появляется рядом, а затем отдельные потоки постепенно переводятся на новую реализацию.</p><p>Представим старый монолит интернет-магазина. Внутри него есть каталог, корзина, заказы, платежи, скидки, личный кабинет и уведомления. Переписать всё сразу — рискованно. Но можно начать с относительно изолированного участка, например с уведомлений.</p><p>Сначала команда описывает текущий контракт: какие события приходят в модуль уведомлений, какие каналы используются, какие шаблоны отправляются, какие ошибки считаются допустимыми. Затем рядом создается новый сервис уведомлений, который реализует тот же контракт. На первом этапе он может даже не отправлять реальные сообщения, а только принимать события и логировать результат. После проверки часть трафика переводится на новую реализацию. Когда сервис стабилизируется, старый код уведомлений удаляется из монолита.</p><p>Strangler Fig хорошо работает там, где между старым и новым кодом есть сетевая граница: HTTP, message bus, RPC. Если такой границы нет — например, нужно постепенно заменить функцию или класс внутри одного процесса — используется родственный паттерн Branch by Abstraction: над старой реализацией создается абстракция, рядом пишется новая реализация, переключение происходит через конфигурацию или фича-флаг, после стабилизации старая ветка удаляется. Снаружи это выглядит как Strangler Fig, но без сетевого прокси.</p><p>Такой подход снижает риск. В системе нет одного большого релиза, где всё меняется сразу. Есть серия небольших контролируемых изменений. Каждое можно протестировать, измерить и откатить.</p><h2>Главное правило: сначала повторить поведение, потом улучшать</h2><p>Одна из частых ошибок при модернизации legacy-кода — попытка одновременно переписать систему и улучшить бизнес-логику. Команда смотрит на старый модуль и думает: «Раз уж мы его трогаем, давайте сразу сделаем нормальную архитектуру, изменим контракты, уберем странные кейсы и перепишем поведение».</p><p>Это опасный путь. В legacy-системах странное поведение часто существует не случайно. За ним может стоять неочевидное бизнес-правило, старый клиент, интеграция с внешней системой или исторический баг, на который уже кто-то завязался.</p><p>Например, в системе расчета налогов может быть правило: для контрактов, заключенных до 2018 года, НДС округляется в меньшую сторону до целого рубля, а для всех остальных — по математическим правилам. Новый разработчик может решить, что это ошибка, и «исправить» округление. Но потом выяснится, что часть крупных клиентов держит это поведение в своих сверках, а смена правила приведет к расхождениям в актах и претензиям.</p><p>Прежде чем менять поведение, его нужно зафиксировать. Для этого пишут характеристические тесты (characterization tests, иногда называемые golden master или approval tests). Идея простая: на реальных данных или их обезличенных копиях прогоняется старая реализация, её ответы сохраняются как эталон, и любые будущие изменения, отклоняющиеся от эталона, отлавливаются автоматически. Тесты пишутся не для красоты, а для того, чтобы зафиксировать существующее поведение — даже странное — перед тем, как его трогать. Подробно эта техника описана у Майкла Физерса в книге Working Effectively with Legacy Code; на практике её удобно реализовать через библиотеки семейства approval-tests (approvaltests-python, approvaltests-java и аналоги).</p><p>Поэтому первый этап модернизации — не улучшение, а воспроизведение текущего поведения. Новая реализация должна вести себя так же, как старая. Даже если старое поведение кажется странным. Только после стабилизации можно отдельно обсуждать, что именно стоит менять.</p><h2>Feature toggles: как включать новую логику без риска</h2><p>Feature toggles, или фича-флаги, — один из главных инструментов безопасной миграции. Они позволяют включать и выключать новую логику без деплоя.</p><p>В обычной разработке релиз часто выглядит бинарно: код либо выкатили, либо нет. При миграции legacy это неудобно. Гораздо безопаснее иметь возможность включить новую реализацию для 1% пользователей, затем для 10%, потом для половины аудитории и только после этого для всех.</p><p>Например, команда переносит расчет стоимости доставки из монолита в новый сервис. С помощью фича-флага это выглядит так:</p><p>user_id передается явно, чтобы решение «попал ли пользователь в новый сегмент» было стабильным от запроса к запросу. Иначе один и тот же клиент будет получать разные ответы при обновлении страницы, и поведение системы станет непредсказуемым.</p><p>На первом этапе флаг включают только для внутренней команды. Потом для тестового сегмента пользователей. Затем для небольшой доли реального трафика. Если метрики стабильны, долю увеличивают. Если появляются ошибки, флаг выключают, и пользователи снова идут в старую реализацию.</p><p>Важно различать два разных типа флагов. Флаг постепенной выкатки (rollout flag) меняется редко и контролирует, какой процент пользователей видит новую логику. Kill switch — отдельный флаг, единственная задача которого — мгновенно выключить новую реализацию при инциденте. Kill switch должен опрашиваться на каждом запросе, его кэширование должно жить секунды, а не минуты, и он принципиально не должен зависеть от той системы, которую он выключает. Иначе в момент аварии может оказаться, что выключатель сам недоступен.</p><p>В качестве инфраструктуры для флагов команды обычно берут одну из платформ: LaunchDarkly, Unleash, Flagsmith, GrowthBook, либо собирают собственную поверх Redis или конфигурационного сервиса. Для миграции важны три свойства: быстрое распространение изменений (секунды, а не минуты), поддержка таргетинга по пользователю/сегменту и аудит — кто и когда менял флаг.</p><p>Важно, что фича-флаг — это не просто if в коде. Для серьезной миграции нужны правила: кто может включать флаг, как быстро его можно отключить, какие метрики отслеживаются, когда флаг должен быть удален.</p><p>Последний пункт особенно важен. Если флаги не удалять, система быстро превращается в набор ветвлений, где никто уже не понимает, какая логика актуальна.</p><h2>Shadow testing: как проверить новую систему на реальном трафике</h2><p>Feature toggles помогают безопасно переключать пользователей. Но перед этим хорошо бы понять, совпадает ли новая логика со старой. Для этого используют shadow testing.</p><p>Shadow testing — это запуск новой реализации параллельно старой, но без влияния на пользователя. Пользовательский запрос по-прежнему обрабатывает старая система, а новая получает копию запроса и считает результат «в тени». Пользователю этот результат не показывается. Команда только сравнивает ответы.</p><p>Например, есть старый модуль расчета скидок. Он учитывает промокоды, сегмент пользователя, историю покупок, регион и партнерские условия. Команда пишет новый сервис скидок. Чтобы не переключать пользователей сразу, можно запустить теневой режим:</p><p>Два момента, на которые стоит обратить внимание в этом коде. Теневой вызов запускается через asyncio.create_task — корутина сразу планируется в event loop и начнёт выполняться, как только функция вернёт управление. И весь блок завернут в try/except: исключение в новой логике не должно ронять основной запрос. Без этих двух свойств shadow testing рискует ухудшить продакшен вместо того, чтобы безопасно его проверить.</p><p>Небольшая оговорка для продакшена: event loop держит на task только слабую ссылку, и без сохранённой ссылки задача может быть собрана сборщиком мусора прямо во время выполнения. В реальном коде Task имеет смысл класть в set фоновых задач и удалять оттуда через add_done_callback. В примере выше эта обвязка опущена для читаемости.</p><p>Для критичной доменной логики — платежей, биллинга, расчета тарифов — допустимый уровень расхождения должен быть около нуля: цель в shadow-режиме не «как можно меньше различий», а «понимаем каждое расхождение». Для менее чувствительных доменов (рекомендации, ранжирование результатов поиска) можно жить с расхождением в долях процента, но и там расхождения нужно классифицировать, а не игнорировать. Возможно, это баги новой реализации. А возможно, старая система содержит устаревшую логику, которую нужно отдельно обсудить с бизнесом.</p><p>Shadow testing особенно полезен для критичных доменных частей: платежей, биллинга, расчета тарифов, персональных предложений, транзакций. Там нельзя просто «попробовать на пользователях» и посмотреть, что будет.</p><p>При этом важно отличать теневую проверку чтения от теневой проверки записи. Чтение проверить относительно дёшево: запрос идёт в обе системы, ответы сравниваются, никаких внешних эффектов нет. С записью всё сложнее. Если новая реализация в shadow-режиме действительно создаст заказ, спишет деньги или отправит письмо, у пользователя возникнут двойные эффекты. Поэтому для writes либо вводят идемпотентные ключи и shadow-режим без реальных побочных действий (внешние вызовы заменены no-op-стабами, БД — отдельной shadow-копией), либо вообще отказываются от теневой проверки записи в пользу постепенной выкатки за фича-флагом.</p><p>Сравнение ответов в реальной системе тоже не сводится к одной функции compare. Нужно отдельно решать, как игнорировать «нормальный» шум (метки времени, идентификаторы, порядок коллекций), как сэмплировать трафик, чтобы не утопить хранилище расхождений, и как организовать триаж — кто и в каком ритме разбирает накопившиеся диффы. Готовые решения этого класса — GitHub Scientist (Ruby и его порты в другие языки), Twitter Diffy, либо собственный лёгкий регистратор поверх Kafka и таблицы расхождений.</p><h2>С чего начинать модернизацию</h2><p>Начинать лучше не с самого больного и не с самого центрального модуля. Это звучит контринтуитивно, потому что обычно хочется сразу взяться за главный источник проблем. Но если начать с ядра системы, команда быстро упрется в максимальное количество зависимостей и рисков.</p><p>Удобный способ выбрать первый кусок — оценить кандидатов по двум осям: насколько модуль критичен для бизнеса (low / high) и насколько сильно он связан с остальной системой (low / high). Начинать стоит с квадранта low-criticality + low-coupling: ошибки в нем не уронят бизнес-показатели, а малое количество зависимостей позволит провести миграцию полностью, не утянув за собой смежные модули. Высоко-критичные и сильно связанные части (платежи, ядро авторизации) трогают в последнюю очередь — на этот момент команда уже наберёт опыт безопасной миграции.</p><p>Хорошие точки входа обычно: уведомления, генерация отчетов, поиск, история операций, профиль пользователя, отдельная часть каталога. Важно, чтобы у команды была возможность описать контракт: какие данные входят, какие выходят, какие ошибки возможны, какие внешние системы участвуют.</p><p>Допустим, в банковском приложении есть старый модуль истории операций. Он медленный, сложно расширяется, но при этом не выполняет сами транзакции. Это хороший кандидат для первой миграции. Ошибка в истории операций неприятна, но обычно менее критична, чем ошибка в списании денег.</p><p>Команда может вынести чтение истории в отдельный сервис, сначала запустить его в shadow-режиме, потом включить для части пользователей, затем полностью перевести чтение на новую реализацию. При этом критичная транзакционная логика останется в старой системе до тех пор, пока команда не наберет опыт безопасной миграции.</p><h2>Миграция данных: самая сложная часть</h2><p>Большая часть статьи говорит о маршрутизации запросов и переключении трафика. Но в реальных проектах основная сложность лежит ниже — в данных. Старая и новая реализации почти всегда работают с общим состоянием: одной БД, одним хранилищем документов, одним набором очередей. Переехать туда «одним коммитом» нельзя.</p><p>Базовый рабочий приём — Expand-Contract (он же Parallel Change). Изменение схемы делается в три такта. На этапе expand в БД добавляются новые поля, таблицы или индексы, при этом старое поведение полностью сохраняется. Затем — migrate: обе реализации начинают писать и в старое, и в новое место (dual writes), а отдельный фоновый процесс делает backfill — заполняет новые поля историческими данными. После этого читатели по одному переключаются на новую схему. Только когда никто из читателей не использует старую структуру, наступает contract — удаление лишних колонок и таблиц.</p><p>Несколько практических деталей, которые часто упускают:</p><p>·         Dual writes — это не бесплатная операция. Две записи означают две точки отказа. Если одна из них упала, нужно решать, что делать: продолжать ли работу, ставить ли событие в очередь на повтор, помечать ли запись как несогласованную. Простое «сначала пишем туда, потом сюда» в продакшене на нагрузке приводит к расхождениям.</p><p>·         Backfill часто длиннее, чем кажется. На большой таблице миграция в одном UPDATE блокирует продакшен. Поэтому backfill делают батчами по N тысяч строк с паузами, отслеживают прогресс и предусматривают возможность остановить и продолжить.</p><p>·         Онлайн-изменения схемы на крупных таблицах делаются не штатным ALTER TABLE, а специализированными инструментами: gh-ost или pt-online-schema-change для MySQL, встроенные онлайн-механизмы PostgreSQL для индексов и колонок, Liquibase/Flyway — для управления версионированием изменений в репозитории.</p><p>·         Shadow testing данные не покрывает. Можно сравнить, что новая реализация возвращает то же, что и старая, но если за этим стоит другая схема в БД, проверка корректности самой миграции данных — это отдельная работа: сверки, контрольные суммы, выборочный аудит исторических записей.</p><p>Без этих шагов любая красивая фасадная архитектура наталкивается на разъезжающиеся данные — и тогда даже идеальный Strangler Fig снаружи не спасает.</p><h2>Прокси-слой как точка контроля</h2><p>Чтобы постепенно заменять legacy-код, нужно управлять маршрутизацией запросов. Для этого часто создают прокси-слой, API Gateway или фасад, через который проходит обращение к старой и новой логике. В терминах Domain-Driven Design такой слой часто называют Anti-Corruption Layer: он защищает новую реализацию от старых контрактов и наоборот, позволяя двум моделям сосуществовать без взаимного «загрязнения».</p><p>Без такой точки контроля миграция становится хаотичной. Часть клиентов ходит напрямую в старый модуль, часть — в новый, часть использует обходные пути, а команда теряет возможность централизованно переключать трафик.</p><p>Прокси-слой решает несколько задач. Он скрывает детали реализации от клиентов, позволяет направлять часть запросов в новую систему, поддерживает фича-флаги, собирает метрики и упрощает откат.</p><p>В качестве технической основы команды обычно берут один из трех вариантов: классический API gateway (Kong, AWS API Gateway), service mesh (Envoy, Istio) или более простой reverse proxy (NGINX, HAProxy). Service mesh особенно удобен, когда трафик уже идёт внутри Kubernetes-кластера: маршрутизацию можно менять конфигурацией, без правок кода клиентов и сервисов.</p><p>Например, мобильное приложение обращается к endpoint /orders/history. Раньше этот endpoint напрямую обслуживал монолит. После введения API Gateway приложение продолжает ходить по тому же контракту, но внутри gateway может решать, куда направить запрос: в legacy-модуль или новый сервис истории заказов.</p><p>Управление маршрутизацией обычно делается не «всё или ничего», а на основании атрибутов запроса: значения заголовка (X-Migration-Cohort: new), куки, хэша от user-id (стабильное разбиение пользователей на сегменты) или географического региона. Это позволяет выкатывать новую реализацию сначала на одну страну, на сотрудников самой компании или на тестовый сегмент — и только потом расширять охват.</p><p>Для клиента ничего не меняется. Для команды появляется управляемость.</p><h2>Наблюдаемость: без метрик миграция превращается в гадание</h2><p>Постепенная модернизация невозможна без нормальной наблюдаемости. Если команда не видит, что происходит внутри системы, она не сможет безопасно переключать трафик.</p><p>Минимальный набор — это логи, метрики и распределенная трассировка (distributed tracing). Нужно понимать, сколько запросов идет в старую и новую реализацию, сколько ошибок возникает, как меняется latency, где появляются таймауты, какие статусы возвращаются, какие бизнес-метрики проседают.</p><p>Технические метрики стоит формулировать не как «средний ответ» и «процент ошибок», а в терминах SLI и SLO: целевые показатели вида «99.9% запросов на /orders/history отвечают быстрее 300 ms за 30 дней» с явным error budget. Latency измеряется по перцентилям (p50, p95, p99) — среднее значение почти всегда обманчиво, а хвосты распределения говорят о реальном опыте пользователя. На время миграции имеет смысл выставить отдельные SLO для нового и старого пути и сравнивать их.</p><p>В качестве инструментов де-факто стандартом стал OpenTelemetry для трассировок, метрик и логов — единый протокол, который пишет в практически любое хранилище. Дальше — Prometheus и Grafana для метрик, Jaeger или Tempo для traces, Sentry или аналог для ошибок. Для миграции важна возможность фильтровать метрики по «варианту» — отдельно по старому и новому пути — иначе все цифры смешаются и реальную динамику будет не видно.</p><p>Технических метрик недостаточно. Если команда переносит оформление заказа, важно смотреть не только на 500 ошибки и время ответа, но и на конверсию в оплату, количество брошенных корзин, повторы запросов, обращения в поддержку.</p><p>Пример: новая система формально отвечает быстрее старой и не дает ошибок. Но после включения на 10% пользователей падает конверсия в оплату. Причина может быть не в серверной ошибке, а в изменении порядка полей, другом тексте сообщения или потере какого-то edge-case. Без бизнес-метрик команда может решить, что миграция успешна, хотя для продукта она уже создает проблему.</p><h2>Практическая последовательность миграции</h2><p>Рабочая последовательность обычно выглядит так.</p><p>Сначала команда выбирает ограниченный участок системы. На этом этапе важно не просто назвать модуль, а описать его границы. Какие сценарии он закрывает? Кто его вызывает? Какие данные он читает и пишет? Какие внешние интеграции использует? Какие неочевидные бизнес-правила в нем есть?</p><p>Затем поверх legacy-логики создается стабильный контракт. Это может быть API, фасад, gateway или отдельный слой внутри приложения. Главная задача — сделать так, чтобы клиенты зависели не от внутренней реализации, а от понятного интерфейса. На этом этапе полезно вспомнить про contract testing (Pact, Spring Cloud Contract): автотесты со стороны потребителей фиксируют, что именно они ожидают от API, и предупреждают о ломающих изменениях до того, как они доедут до продакшена.</p><p>После этого рядом пишется новая реализация. Она должна повторять текущее поведение, а не сразу становиться «идеальной версией будущего». На этом этапе полезно фиксировать все расхождения: где старая система работает странно, где требования не описаны, где бизнес-правила требуют уточнения.</p><p>Следующий этап — shadow testing. Новая система получает копии реальных запросов, считает результат, но пользователю по-прежнему возвращается ответ legacy. Команда сравнивает результаты и устраняет расхождения.</p><p>Когда новая реализация достаточно стабильна, начинается постепенное переключение через feature toggles. Сначала внутренние пользователи, потом 1% реального трафика, затем 5–10%, затем 50% и только после этого 100%.</p><p>На каждом этапе команда смотрит на метрики. Если всё стабильно, движение продолжается. Если появляются проблемы, флаг выключается, трафик возвращается в legacy, а команда разбирает причины.</p><p>Последний этап — удаление старого кода. Это не формальность, а обязательная часть миграции. И «удалить старый код» — это не один коммит, а явный Definition of Done: вырезана старая ветка кода, удалён фича-флаг, обновлена документация и схемы архитектуры, переименованы или удалены устаревшие дашборды и алерты, обновлены runbook’и для on-call и проведено короткое внутреннее обучение. Если этого не сделать, через полгода никто уже не вспомнит, какой путь актуален, и легаси-ветвление останется в коде навсегда.</p><h2>Откат миграций: дешёвый только пока не пошли записи</h2><p>Откатить миграцию, в которой ещё не было записи в БД, легко: достаточно переключить фича-флаг, и трафик снова идёт через старую реализацию. Откатить миграцию, в которой новая система уже неделю писала данные в новые таблицы, — отдельный, гораздо более тяжёлый разговор.</p><p>Поэтому ещё на этапе проектирования каждое изменение должно сопровождаться явным планом отката. Удобно различать три типа шагов.</p><p>Полностью обратимые шаги. Чтение через новый сервис, расчёт «в тени», новые метрики. Откат — выключить флаг. Это самый комфортный режим, и в нём стоит держать миграцию как можно дольше.</p><p>Обратимые с компенсацией. Новая реализация пишет дополнительные данные (например, дублирует операции в новую таблицу), но старый источник тоже обновляется. Откат возможен, но требует решить, что делать с уже записанными данными: оставить, очистить, синхронизировать. План этих действий должен быть написан до выкатки, не во время инцидента.</p><p>Forward-only. После некоторой точки откат становится невозможен — например, после того, как старая схема удалена или внешние интеграции перенастроены на новый сервис. Такие шаги допустимы, но к ним нужно приходить отдельно, осознанно, с особенно строгими SLO в предыдущем этапе. До forward-only-перехода имеет смысл подержать систему в режиме параллельной работы дольше, чем по графику.</p><p>Базовое правило: ни один шаг миграции не должен уходить в продакшен, если у команды нет письменного ответа на вопрос «как мы откатываемся в случае проблемы». Иначе при инциденте откатываться будут на ходу — и не факт, что успешно.</p><h2>Пример: как тот же сервис мигрировали со второй попытки</h2><p>После неудачного опыта команда взялась за тот же сервис заново, но изменила подход.</p><p>На первом шаге они зафиксировали поведение существующего сервиса. На самые часто используемые сценарии (создание путевого листа, подпись акта осмотра, выгрузка пакета документов за период) написали характеристические тесты на реальных продакшен-данных, обезличенных и сохранённых как фикстуры. Любое будущее изменение поведения теперь падало в CI как явное расхождение.</p><p>Параллельно команда провела инвентаризацию побочных эффектов. Из исходного кода и логов выяснилось, что сервис не только хранит документы, но и: публикует событие в Kafka при смене статуса, инкрементирует счётчик в Redis для рейтинга водителей, отправляет webhook во внешнюю систему партнёра, пишет в таблицу аудита. Каждый из этих эффектов попал в отдельный пункт чек-листа «что должно остаться» в новой реализации.</p><p>Затем команда выбрала первый кусок для выноса — не весь сервис, а только чтение документов (GET /documents/{id} и GET /documents/by-driver/{driver_id}). Это была наименее рискованная часть: ошибки в чтении неприятны, но не ломают финансовые потоки.</p><p>Новый сервис написали на FastAPI рядом со старым. На уровне API Gateway появилось правило маршрутизации: запросы на чтение шли в старый сервис, но в фоне дублировались в новый. Ответ пользователю всегда возвращал legacy, а ответ нового сервиса сравнивался с эталоном и записывался в отдельную таблицу для разбора. Использовали обёртку поверх asyncio.create_task — на ответ пользователя теневой вызов не влиял.</p><p>За три недели shadow-режима команда нашла четыре расхождения. Два оказались багами новой реализации (округление времени, неправильная сортировка вложений). Два — давно забытыми особенностями старого сервиса (одно поле возвращалось в UTC, другое — в локальной зоне; так было исторически, бизнес не возражал, но в новой реализации захотели единый формат). Все четыре зафиксировали явно: баги — починили, особенности — согласовали с продуктовой командой как осознанное изменение.</p><p>Когда расхождений не осталось, включили фича-флаг на сотрудников самой компании. Через неделю — на 1% реальных водителей. Дальше шаг по 5%, 25%, 50%, 100% с паузой в несколько дней между этапами. На каждом шаге следили не только за HTTP-ошибками и latency, но и за продуктовыми метриками: количество подписанных актов, время от открытия документа до подписи, доля повторных запросов. Один раз пришлось откатиться с 25% на 5% — в одном из регионов выросло время отклика из-за неэффективного запроса. Исправили, выкатили снова.</p><p>Через два месяца чтение полностью перешло в новый сервис. Старый код чтения и фича-флаг удалили в том же релизе. После этого по той же схеме мигрировали запись документов, потом публикацию событий, потом импорт из внешних систем. Полная миграция заняла девять месяцев — почти столько же, сколько провалившийся Big Bang, — но продукт всё это время продолжал развиваться, инцидентов не было, и в конце команда осталась с системой, которую понимает.</p><h2>Типичные ошибки при работе с legacy</h2><p>Первая ошибка — пытаться улучшить всё сразу. Команда одновременно меняет архитектуру, бизнес-логику, контракты и инфраструктуру. В результате становится невозможно понять, какая именно часть вызвала проблему. Правильнее сначала воспроизвести поведение, стабилизировать новую реализацию и только потом улучшать.</p><p>Вторая ошибка — недооценивать скрытые зависимости и побочные эффекты. Legacy-код часто делает больше, чем кажется. На один и тот же вызов могут быть навешаны: запись в таблицу аудита, инкремент счётчика в кэше, публикация события в очередь, обновление статуса связанной сущности, инвалидация кэша, дёрганье webhook’а во внешнюю систему. Если в новой реализации воспроизвести только явный путь, скрытые потребители молча перестанут получать данные — и узнают об этом через жалобу бизнеса, а не через ошибку в логах. Поэтому перед выносом любого модуля имеет смысл составить инвентаризацию побочных эффектов: пройтись по коду и логам и выписать каждое нелогичное действие отдельным пунктом чек-листа.</p><p>Третья ошибка — отсутствие наблюдаемости. Без логов, метрик и трассировки команда не управляет миграцией, а угадывает. Особенно опасно смотреть только на технические ошибки и игнорировать бизнес-показатели.</p><p>Четвертая ошибка — не договариваться с бизнесом. Модернизация не должна быть невидимой «инженерной активностью в стол». Её нужно встраивать в roadmap, объяснять эффект и договариваться о приоритетах. Если бизнес не понимает, зачем команда тратит время на миграцию, работа будет постоянно проигрывать новым фичам.</p><p>Пятая ошибка — не удалять старый код. Временное сосуществование старой и новой логики нормально. Вечное сосуществование — нет. Если legacy не удаляется, технический долг не уменьшается, а просто меняет форму.</p><p>Шестая ошибка — не удалять фича-флаги после миграции. Флаг, который сыграл свою роль и больше никогда не выключается, превращается в постоянное ветвление в коде. Через год команда не помнит, можно ли удалить такую ветку или там сидит важный edge-case. Через два — кода с такими «мёртвыми» флагами становится больше, чем основной логики. Поэтому каждый флаг должен заводиться с условием удаления («после полной выкатки и двух недель стабильной работы») и иметь ответственного, кто этим удалением займётся.</p><p>Отдельно стоит упомянуть организационную сторону. Закон Конвея работает и в обратную сторону: если новый и старый код владеются разными командами с разными приоритетами, миграция будет тормозиться независимо от выбранного паттерна. На время миграции имеет смысл явно проговорить, кто отвечает за переход, и не разделять старую и новую реализации между несовместимыми roadmap’ами.</p><h2>Компромиссы, к которым нужно быть готовыми</h2><p>Постепенная модернизация безопаснее Big Bang-переписывания, но она не бесплатна. Некоторое время система будет сложнее, чем раньше. В ней появятся старый и новый код, прокси-слой, фича-флаги, дублирование логики, дополнительные метрики.</p><p>Shadow testing увеличит нагрузку на инфраструктуру, потому что часть запросов будет обрабатываться дважды. Команде придется поддерживать дисциплину: документировать контракты, отслеживать флаги, удалять старую реализацию после миграции, поддерживать contract-тесты в актуальном состоянии.</p><p>Но это контролируемая сложность. Она распределена во времени и управляется инженерными практиками. В отличие от Big Bang-риска, где команда долго работает с минимальной обратной связью, а потом выкатывает один большой релиз с максимальной неопределенностью.</p><h2>Когда Strangler Fig особенно оправдан</h2><p>Постепенная миграция особенно хорошо подходит для систем, где downtime невозможен или слишком дорог. Это финтех, e-commerce, биллинг, мобильные бэкенды с большой аудиторией, высоконагруженные продукты, старые монолиты и системы с большим количеством интеграций.</p><p>Если продуктом ежедневно пользуются сотни тысяч или миллионы людей, нельзя позволить себе «переписать и посмотреть, что будет». Нужно менять архитектуру так, чтобы пользователь не замечал процесса миграции.</p><p>Этот подход также полезен там, где бизнес продолжает активно развивать продукт. Если фичи нельзя заморозить на полгода, модернизация должна идти параллельно с продуктовой разработкой.</p><h2>Когда модернизацию лучше не делать</h2><p>Постепенная миграция — мощный инструмент, но у неё тоже есть стоимость, и иногда правильный ответ — оставить систему как есть. Несколько сценариев, в которых модернизация плохо окупается.</p><p>Продукт, который уходит из эксплуатации. Если через год сервис будет выключен или заменён на покупное решение, тратить квартал на его рефакторинг бессмысленно. Достаточно стабилизировать то, что есть.</p><p>Модуль, который никто не трогает. Если код десятилетней давности продолжает работать, не падает, не требует изменений и не вызывает инцидентов, его «уродливость» — не повод его переписывать. Цель модернизации — упростить будущие изменения; если будущих изменений нет, цели тоже нет.</p><p>Регулируемые системы с тяжёлой ресертификацией. В банковских, медицинских и государственных контурах любое изменение в критичной системе может потребовать повторной сертификации, перепрохождения аудитов, обновления договорной обвязки. В таких условиях стоимость модернизации может на порядок превышать стоимость поддержки текущей реализации, и решение нужно принимать вместе с владельцем продукта и юристами, а не только инженерным составом.</p><p>Простой тест: если на вопрос «какой бизнес-сценарий мы откроем после миграции» нет внятного ответа — модернизацию имеет смысл отложить и заняться чем-то другим.</p><h2>Что получает команда</h2><p>Главный результат постепенной модернизации — управляемость. Команда начинает лучше понимать систему, контролировать изменения и снижать риск инцидентов.</p><p>Появляются понятные контракты, наблюдаемость, практика безопасных релизов, культура удаления старого кода. Разработчики перестают бояться legacy, потому что у них появляется метод, а не только желание «когда-нибудь всё переписать».</p><p>Для бизнеса это тоже выгодно. Продукт продолжает развиваться, сроки становятся более прогнозируемыми, риски крупных сбоев снижаются, а технический долг постепенно уменьшается.</p><h2>Модернизация — это процесс, а не проект</h2><p>Legacy нельзя «починить за квартал». Если система развивалась годами, она не станет простой после одного рефакторинга. Но её можно системно улучшать.</p><p>Strangler Fig Pattern, Branch by Abstraction, feature toggles, shadow testing и аккуратная миграция данных дают рабочую модель: выбрать ограниченный участок, описать контракт, реализовать новую версию, проверить её на реальном трафике, постепенно переключить пользователей и удалить старый код.</p><p>Это не самый быстрый путь. Зато он управляемый. А в зрелых продуктах управляемость важнее скорости.</p><p>Потому что цель модернизации — не написать красивую новую систему. Цель — сделать так, чтобы продукт продолжал развиваться, команда могла безопасно вносить изменения, а пользователи не становились участниками инженерного эксперимента.</p>]]></content:encoded>
    </item>
    <item>
      <title>Почему ваше приложение тормозит: архитектурные bottlenecks, которые никто не замечает</title>
      <link>https://tproger.ru/articles/pochemu-vawe-prilozhenie-tormozit--arhitekturnye-bottlenecks--kotorye-nikto-ne-zamechaet</link>
      <comments>https://tproger.ru/articles/pochemu-vawe-prilozhenie-tormozit--arhitekturnye-bottlenecks--kotorye-nikto-ne-zamechaet?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Татьяна Жукова]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/pochemu-vawe-prilozhenie-tormozit--arhitekturnye-bottlenecks--kotorye-nikto-ne-zamechaet</guid>
      <description><![CDATA[<p>Как найти и устранить архитектурные bottleneck'и: причины тормозов, типовые ошибки и пошаговая методика диагностики.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/pochemu-vawe-prilozhenie-tormozit--arhitekturnye-bottlenecks--kotorye-nikto-ne-zamechaet">Почему ваше приложение тормозит: архитектурные bottlenecks, которые никто не замечает</a>»</p>]]></description>
      <category><![CDATA[API]]></category>
      <category><![CDATA[Оптимизация]]></category>
      <category><![CDATA[Парсинг]]></category>
      <category><![CDATA[Инструменты]]></category>
      <category><![CDATA[Производительность]]></category>
      <category><![CDATA[Python]]></category>
      <category><![CDATA[JSON]]></category>
      <category><![CDATA[Микросервисы]]></category>
      <category><![CDATA[Работа]]></category>
      <category><![CDATA[Базы данных]]></category>
      <category><![CDATA[Node.js]]></category>
      <category><![CDATA[Redis]]></category>
      <category><![CDATA[Бэкенд]]></category>
      <category><![CDATA[Аналитика]]></category>
      <category><![CDATA[Дата-центр]]></category>
      <category><![CDATA[Сервер]]></category>
      <category><![CDATA[аналитик]]></category>
      <category><![CDATA[микро]]></category>
      <category><![CDATA[OpenTelemetry]]></category>
      <category><![CDATA[Тимлид]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Thu, 19 Jun 2025 14:00:03 GMT</pubDate>
      <content:encoded><![CDATA[<p>Ваше приложение тормозит, хотя сервер мощный, код вроде нормальный, а метрики — не очень-то и загружены? Возможно, вы столкнулись с архитектурным bottleneck'ом — скрытым ограничением, которое убивает производительность под нагрузкой. Вместе с Никитой Ульшиным, тимлидом команды разработки архитектурных инструментов в Т-Банк и автором тг-каналов <a href="https://t.me/ulshinblog">Никита Ульшин про IT</a> и <a href="https://t.me/tech_fit">ТехнофITнес | Никита Ульшин</a>, разбираем типовые узкие места в системах, от неэффективной аллокации до однопоточных участков, и показываем пошаговый подход к их поиску и устранению.</p><h2>Почему «тормоза» — это не всегда про код</h2><p>Когда пользователь жалуется на «медленное приложение», большинство разработчиков по привычке лезет в код. Профилируем, ищем неэффективные циклы, оптимизируем до запятых. Но в реальности причина часто не в логике, а в том, что её окружает.</p><p>Современное приложение — это сложный организм: десятки микросервисов, базы данных, кэши, брокеры, сети. И тормозить может любой из этих компонентов. А ты в это время профилируешь ни в чём не виноватый for.</p><p>Вот что тормозит, когда код ни при чём:</p><p><b>Архитектура</b></p><ul><li>Цепочки вызовов: каждый хоп — +latency.</li><li>Синхронные зависимости: завис один сервис — тормозят все.</li><li>Общие ресурсы: shared state, глобальные очереди, блокировки.</li></ul><p><b>Базы данных</b></p><ul><li>Нет индексов или плохой план запроса.</li><li>N+1-запросы — особенно при неосторожной ORM.</li><li>Частый доступ к одним таблицам — гонка за IO и блоки.</li><li>Нет шардинга или репликации в масштабируемой нагрузке.</li></ul><p><b>Сеть</b></p><ul><li>Высокая задержка между регионами (особенно multi-cloud).</li><li>Потери пакетов, DNS-проблемы, нестабильность.</li><li>Отсутствие connection reuse: TLS-handshake на каждый вызов.</li></ul><p><b>Очереди и брокеры</b></p><ul><li>Один медленный потребитель тормозит всех.</li><li>Нет защиты от от backpressure или дедупликации.</li><li>Неправильные batch- и ack-настройки.</li></ul><p><b>Аллокаторы и GC</b></p><ul><li>Фрагментация памяти, нагрузка на GC.</li><li>Финалайзеры, слабые ссылки, плохо настроенный heap.</li></ul><p><b>Конфигурации</b></p><ul><li>Маленький пул соединений — пул заканчивается, запросы ждут.</li><li>Агрессивные retry-политики — система перегружается.</li><li>CPU throttling в Kubernetes — контейнеру не хватает ресурсов.</li></ul><p><b>Сериализация и форматы</b></p><ul><li>Перегруженные JSON/Protobuf.</li><li>Вложенные base64 + gzip + JSON — боли сериализации.</li></ul><h3>Почему bottlenecks остаются невидимыми до продакшена</h3><p>Многие из них —  не баги, а последствия архитектурных решений. В дев-среде всё летает, потому что:</p><ul><li>мало данных,</li><li>нет конкуренции за ресурсы,</li><li>нет распределённой нагрузки.</li></ul><p>В проде начинается хаос. Рассмотрим на конкретных примерах:</p><ul><li>На деве — 2 запроса в секунду, в проде — миллион. Кто-то запустил маркетинговую рассылку, аналитик тащит big report, а клиент ретраит ошибки.</li><li>Архитектура «в лоб»: всё синхронно, каждый сервис вызывает по цепочке десяток других. Пока нагрузка мала — все живет. Как только одно звено не выдерживает — перестает жить.</li><li>Иллюзия масштабируемости: до 100 пользователей —  всё окей, на 1000 — каскадная деградация.</li><li>«Невидимая боль» между сервисами: каждый по отдельности быстрый, но блокирует друг друга на базе.</li></ul><h2>Слои архитектуры и где в них зарыты проблемы</h2><p>Когда приложение тормозит, мы часто копаемся в отдельных компонентах — фронте, бэке, базе. Но bottleneck может быть не внутри компонента, а между ними: в конфигурации, связях, сетевых задержках, очередях вызовов.</p><p>Чтобы понять, где искать узкое место, нужно пройтись по всей цепочке — от клиента до внешних сервисов.</p><h3>На уровне клиента</h3><ul><li>Тяжёлые бандлы. Огромный JS, куча аналитики, трекеры — и вот страница загружается 8 секунд на старом телефоне.</li></ul><ul><li>Чрезмерные запросы. После загрузки — 10 fetch в секунду. API захлёбывается, браузер — тоже.</li></ul><ul><li>Нет кеша. Даже статичные справочники каждый раз грузятся заново.</li></ul><ul><li>Лаги рендеринга. Бэкенд дал ответ за 100ms, но интерфейс лагает — сложные таблицы, графики, reflow.</li></ul><h3>На уровне API Gateway / BFF</h3><ul><li>Непрозрачная маршрутизация —  скрытые таймауты, ошибки ретраев.</li><li>Сборка ответов из микросервисов — цепочка из 5 вызовов на каждый клиентский запрос.</li><li>Синхронные вызовы без деградации — если один микросервис недоступен, падает весь endpoint.</li></ul><h3>На уровне Backend / бизнес-логики</h3><ul><li>Цепочки вызовов. Один сервис вызывает другой, тот — ещё один… И так далее.</li><li>Ограниченный пул соединений —  сервис готов работать, но все worker-ы ждут ресурсы.</li><li>Логика в цикле по данным —  N+1-запросы, сериализация, дублирование работы.</li></ul><h3>На уровне базы данных / кэша</h3><ul><li>Full table scan. Работает быстро на 1000 строках. Умирает на 10 миллионах.</li></ul><ul><li>Локи и гонка за ресурсы. Один UPDATE лочит строку, остальные ждут.</li></ul><ul><li>Кэш не промахивается, но и не помогает —  stale данные, повторные чтения, инвалидация не работают, как ожидалось.</li></ul><p><b>На уровне внешних сервисов (API, брокеры, платёжки)</b></p><ul><li>Без timeout и fallback. Внешний API залип — вы вместе с ним.</li><li>QPS-лимиты. Вы не знали, а вас уже зарейт-лимитили.</li><li>Нестабильная сеть. DNS тормозит, соединения рвутся, задержки скачут между регионами.</li></ul><p>Оптимизация кода важна. Но если система тормозит, искать нужно по всей цепочке —  от кнопки на фронте до брокера в соседнем дата-центре.</p><blockquote>Есть архитектурные паттерны риска, которые часто приводят к тормозам через полгода. Во-первых, общий ресурс на всех: один Redis, один Kafka-топик, одна таблица. Пока нагрузка маленькая — ок. Потом начинается бойня за доступ. Во-вторых, микросервисный монолит: формально разделены, но живут как один. Масштабировать невозможно. В-третьих, цепочки вызовов: 5–6 сервисов на путь одного запроса. Один подвис — и вся цепочка тормозит.</blockquote><h2>Базы данных: когда даже SELECT тормозит всё</h2><p>В бэкенде одна из самых коварных фраз — «ну это же просто SELECT, что с ним будет». Ответ — сначала ничего. Но со временем появляется 10 миллионов строк, и ваш innocuous SELECT превращается в full scan, который лочит диск, ест CPU и тормозит всё, что движется.</p><p>Типичная ситуация: в таблицу пишут JSONB без нормализации и индексов. Пока строк мало — жить можно. Но при росте объёмов каждый запрос превращается в медленное последовательное чтение. Это не просто «долго», а начинает мешать соседним транзакциям: подгружаются stale данные, вытесняется кэш, система начинает проседать вся — вплоть до API.</p><p>Отдельная ловушка — смешение OLTP и OLAP нагрузки. Днём — INSERT и UPDATE от клиентов, ночью — фоновая аналитика, высчитывающая отчёт за год с десятком JOIN и чтением всей таблицы. Если все запросы идут в один инстанс, происходит конкуренция за ресурсы: аналитика сбивает кеш, вызывает блокировки, а продакшн-метрики начинают сыпаться.</p><h3>Как понять, что тормоза идут из базы, а не из бэкенда?</h3><p>Вот несколько типичных признаков:</p><ul><li>Бэкенд «висит» на запросах к базе — в логах видно, что приложение ждёт результат запроса (долгий await, query(), findMany() и т. д.).</li></ul><ul><li>Latency растёт, а CPU и память в норме — сервис сам по себе не перегружен, но ответ приходит медленно.</li></ul><ul><li>В базе фиксируются медленные запросы — например, SELECT или JOIN занимают секунды, хотя раньше проходили за миллисекунды.</li></ul><p>Чтобы подтвердить гипотезу, можно:</p><ul><li>Включить логирование SQL-запросов с таймингом;</li><li>Посмотреть трейс: если шаг DB заметно длиннее остальных — это тревожный сигнал;</li><li>Запустить EXPLAIN ANALYZE на типовые запросы и проверить, где зарыта сложность.</li></ul><h3>Архитектура доступа к данным: как закладываются тормоза</h3><p>Архитектура доступа к данным — фундамент, на котором держится производительность всей системы. Даже идеальный алгоритм или быстрый backend не спасут, если данные извлекаются медленно, неэффективно или избыточно.</p><p>На что здесь стоит обратить внимание:</p><ul><li><b>Количество и характер обращений. </b>Один пользовательский запрос не должен порождать десятки обращений к базе. Планирование кэшей, агрегаций, prefetch, нормализация — всё это часть архитектуры.</li><li><b>Уровень абстракции над данными.</b> ORM может быть удобным, но часто скрывает десятки неэффективных SELECT’ов. Нужно понимать, какие запросы реально идут в базу, и не превращать каждый use case в потенциальный bottleneck.</li><li><b>Стратегия хранения и агрегаций.</b> Если вы пытаетесь на лету агрегировать 100 миллионов строк без шардинга и партиционирования — тормоза неизбежны. Архитектура должна учитывать нагрузку, частоту агрегаций, структуру данных.</li></ul><h2>Очереди и событийные системы: скрытые замедления</h2><p>Событийные архитектуры и очереди вроде Kafka или RabbitMQ часто воспринимаются как универсальное средство от всех проблем: «Сделаем асинхронно — и всё полетит». Но это не серебряная пуля. Очередь не решает проблему — она её откладывает. А иногда сама становится bottleneck’ом.</p><p>Типовые ситуации, когда очередь тормозит систему:</p><ul><li><b>Медленные консьюмеры.</b> Очередь принимает сообщения с высокой скоростью, но обработчики не успевают, и начинается накопление беклога. В результате задержки накапливаются, SLA летит, а вы только недоумеваете, почему всё так тормозит.</li><li><b>Неправильный размер batch’ей.</b> В Kafka и аналогах размер и частота batch'ей критичны. Слишком большие — ждём, пока соберётся партия. Слишком маленькие — тратим ресурсы на лишнюю загрузку и пересылку. В итоге получаем либо высокую задержку, либо низкую производительность.</li><li><b>Неравномерное партиционирование</b>. Если сообщения распределяются по партициям неравномерно, то часть узлов будет простаивать, а часть захлёбываться под нагрузкой. Одна горячая партиция может стать bottleneck’ом всей системы.</li><li><b>Проблемы с retry.</b> Если нет чёткой стратегии повторных попыток, логирования и DLQ (dead-letter queue), битое событие может крутиться в системе бесконечно, тормозя всё остальное. Также если что-то пошло не так и включились retry-механизмы, задержка может вырасти в разы. Особенно если есть backoff или дедлоки.</li></ul><h3>Почему «асинхронно» ≠ «мгновенно»</h3><p>Асинхронность — это не про скорость, а про отложенность. Событие отправлено — но когда оно будет обработано, никто не знает. Через секунду, через минуту, через час?</p><p>Типичные причины задержек в асинхронной обработке:</p><ul><li>Очередь перегружена, и событие просто стоит в хвосте.</li><li>Обработчик падает или рестартится, и событие ждёт.</li><li>Политики повторной обработки не настроены — событие крутится бесконечно.</li><li>Слишком много этапов внутри одного воркера: валидация, запись в БД, вызов API — всё это задержки, которые суммируются.</li></ul><h3>Что происходит, когда очередь переполнена — и почему это тяжело заметить</h3><p>Когда очередь переполняется, события в ней начинают задерживаться или теряться, в зависимости от конфигурации. Но хуже всего то, что это происходит тихо. Формально система работает: события принимаются, воркеры их обрабатывают. Но обработка начинает всё сильнее и сильнее отставать.</p><p>Очередь продолжает принимать события, но обрабатываются они с огромным отставанием. Новое сообщение встаёт в хвост и ждёт своей очереди десятки секунд или минут. Такую ситуацию легко не заметить, если у вас не настроен грамотный мониторинг.</p><p>Для защиты от переполнения очереди нужно мониторить несколько важных показателей:</p><ul><li>Глубина очереди (в Kafka — consumer lag, в RabbitMQ — queue.messages_ready или queue.messages): показывает, сколько сообщений накопилось и ждёт обработки.</li><li>Publish rate: сколько сообщений в секунду публикуется.</li><li>Processing rate: сколько сообщений в секунду обрабатывается.</li><li>Утилизация ресурсов воркеров (CPU, memory, I/O).</li><li>End-to-end latency: сколько времени проходит от публикации события до его обработки.</li></ul><blockquote>Есть самые частые причины накопления сообщений:<br /><br />1) Медленные потребители, которые не успевают обработать весь поток сообщений. <br />2) Шумная архитектура: 10 сообщений там, где можно было обойтись и одним. <br />3) Неправильное масштабирование брокера (например, неправильно выбранный ключ партиционирования в Kafka).</blockquote><h2>API и микросервисы: где тормозит взаимодействие</h2><p>Микросервисная архитектура на бумаге выглядит идеально: каждый сервис автономен, команды независимы, масштабирование прозрачно. Но на практике все часто превращается в болото синхронных вызовов, где каждый сервис делает запросы в пять других.</p><h3>Что тормозит в микросервисах?</h3><p><b>Много лишних вызовов</b></p><p>Один пользовательский запрос вызывает лавину внутренних HTTP/gRPC-запросов. Каждый из них — это:</p><ul><li>сетевые задержки (latency),</li><li>сериализация/десериализация данных,</li><li>ретраи при сбоях,</li><li>риск таймаутов и обрывов.</li></ul><p>Если таких вызовов много, задержка накапливается каскадом.</p><p><b>Цепочка зависимостей</b></p><p>Классика жанра: заказ зависит от оплаты, оплата — от биллинга, биллинг — ещё от трёх микросервисов.</p><p>Если тормозит хотя бы один, сыплется всё. Получается эффект домино и каскадные отказы, где неполадка в одном звене рвёт всю цепочку.</p><p><b>Нет fallback'а и таймаутов</b></p><p>Если вызов к нужному сервису не отвечает, а у вас нет fallback'а или таймаута — всё встаёт. Особенно плохо, если вызов синхронный и блокирует поток.</p><p><b>Наносервисы</b></p><p>Иногда архитекторы увлекаются и дробят сервисы до абсурда. Начинается с «давайте переиспользуем», а заканчивается 10 API-вызовами ради одного действия.</p><p>Вместо бизнес-логики система начинает заниматься оркестрацией — координирует сама себя.</p><h2>Кэш и CDN: почему «ускорители» тоже могут тормозить</h2><p>Кэш традиционно считается палочкой-выручалочкой для производительности. «Добавим кэш — и всё полетит», — думают команды, особенно под нагрузкой. Но в реальности кэш может не только не ускорить, но и наоборот — замедлить или даже уронить систему, если использовать его без понимания.</p><p>Вот несколько типичных ошибок, которые приводят к деградации производительности:</p><h3>Невалидные или слишком агрессивные стратегии</h3><p>Если кэш живёт слишком долго — пользователи получают устаревшие данные. Если обновляется слишком часто — создаются лишняя нагрузка на базу и почти постоянные «промахи».</p><p>А если кэш сбрасывается при каждом изменении, он вообще не успевает выполнять свою роль.
В итоге: нестабильное поведение, непредсказуемая производительность и раздражённые пользователи.</p><h3>Перегрузка кэша</h3><p>Когда кэш настолько загружен, что сам по себе начинает тормозить — это уже не помощь, а вред. Такое часто случается с Redis, Memcached или in-memory решениями, особенно если они:</p><ul><li>работают на одной машине без шардинга;</li><li>содержат тяжёлые объекты;</li><li>используются как универсальный ответ на все проблемы.</li></ul><p>В таких случаях кэш из «ускорителя» может падать под нагрузкой быстрее, чем база данных.</p><h3>Кэш-миссы и лавины запросов</h3><p>Классическая проблема: TTL у популярных ключей истекает одновременно, и тысячи запросов идут мимо кэша в базу.
Это может случиться после деплоя, сброса кэша или при пиковой нагрузке.</p><p>Вместо одного запроса вы получаете 10 000, и тормозит всё. Особенно критично это для систем с CDN, где внезапное истечение кэша популярных страниц может вызвать DDoS-подобную нагрузку.</p><h3>Несогласованность кэшей</h3><p>Если в системе несколько уровней кэширования (CDN, фронтовый кэш, Redis) и они обновляются по-разному, возникает несогласованность. Один пользователь видит новые данные, другой — старые. Один сервис работает с актуальной версией, другой — с устаревшей. Начинается охота за фантомами: баги вроде есть, но не у всех.</p><h2>Вычисления и память: что не так с аллокацией</h2><p>Почему мощные сервера с десятками ядер и гигабайтами памяти тормозят? Потому что производительность упирается не в «железо», а в то, как именно это железо используется. От плохого управления памятью до блокирующих операций — в системе может быть множество узких мест, которые незаметно душат throughput.</p><h3>Частые и крупные аллокации</h3><p>Каждый раз, когда создаётся новый объект, память выделяется из кучи. При интенсивной работе (например, при парсинге JSON или обработке больших структур) это может происходить слишком часто, и тогда запускается сборщик мусора (GC).</p><p>Сборка мусора — это не бесплатная операция: она останавливает выполнение программы. Частые GC-паузы создают «плавающую» производительность, когда всё работает быстро, но время от времени подвисает даже на секунду.</p><h3>GC-паузы</h3><p>Автоматическое управление памятью — благо, но оно же и ловушка. В языках вроде Go, Java или Python невозможно точно контролировать, когда сработает сборщик. Даже задержка в 50 миллисекунд может ощутимо ударить по RPS или нарушить real-time обработку.</p><p>Некоторые компании придумывают обходные пути. Так, в Twitch <a href="https://blog.twitch.tv/en/2019/04/10/go-memory-ballast-how-i-learnt-to-stop-worrying-and-love-the-heap/">использовали</a> технику memory ballast для Go-приложений, чтобы стабилизировать поведение GC и избежать всплесков пауз.</p><h3>Синхронные и блокирующие операции</h3><p>Если операции с файлами, сетью или базой данных выполняются синхронно, поток блокируется — и в это время не может обрабатывать другие задачи. Это особенно критично, если такой поток обслуживает пользовательские запросы.</p><p>Классический пример — однопоточный web-сервер, который просто ждёт ответ от другой системы. Или библиотека, использующая mutex.Lock() в горячем участке, из-за чего десятки горутин встают в очередь.</p><h3>Ограниченные ресурсы и очереди</h3><p>Даже в многопоточном приложении можно легко создать узкое место. Например:</p><ul><li>доступ к объекту синхронизирован через mutex или RW-lock;</li></ul><ul><li>пул соединений имеет жёсткий лимит;</li></ul><ul><li>очередь задач не может масштабироваться под нагрузку.</li></ul><p>В таких случаях нагрузка растёт, а пропускная способность — нет. Всё упирается в искусственное ограничение, введённое «на всякий случай».</p><h3>Однопоточные горячие участки</h3><p>Даже в многопоточной архитектуре может оказаться, что 80% времени тратится в одном месте, которое не масштабируется — например, в процессе сериализации, расчёта или шифрования. Иногда этот участок реализован однопоточно (особенно в Node.js, Python или старом Go-коде) и становится главным тормозом системы. Под высокой нагрузкой это становится особенно заметно.</p><h2>Что делать: подход к поиску и устранению bottlenecks</h2><p>Когда система тормозит, возникает соблазн сразу «что-то сделать» — добавить кэш, увеличить ресурсы, ускорить базу. Но без системного подхода можно попасть в замкнутый круг, где проблема будет возвращаться снова и снова. Чтобы избежать этого, лучше использовать пошаговую стратегию: зафиксировать симптомы, локализовать проблему, подтвердить и только потом оптимизировать.</p><h3>Шаг 1. Зафиксировать симптомы и контекст</h3><p>Важно понять, что именно «тормозит» и в каких условиях. Типовые вопросы:</p><ul><li>Когда проявляется проблема: под нагрузкой, в пиковые часы, у конкретных пользователей?</li><li>Что именно работает медленно: API, отчёты, фоновая обработка?</li><li>Как это влияет на систему: задержки, таймауты, ошибки?</li></ul><p>Пример: пользователи жалуются, что отчёты стали строиться дольше. Вчера — 5 секунд, сегодня — 20.</p><h3>Шаг 2. Проверить очевидные метрики</h3><p>Начинаем с того, что уже доступно в мониторинге:</p><ul><li>латентность API (p50, p95, p99);</li><li>загрузка CPU, использование памяти и диска;</li><li>глубина очередей и backlog;</li><li>паузы GC;</li><li>кэш hit/miss ratio.</li></ul><p>Если латентность растёт, а CPU загружен на 20%, это может указывать на проблемы в синхронных вызовах, аллокации или неэффективной работе кэша.</p><h3>Шаг 3. Сузить область через трейсинг и логирование</h3><p>Чтобы выяснить, не «тормозит» ли другой сервис, используйте распределённый трейсинг (Jaeger, OpenTelemetry, Sentry Performance) или логирование таймингов внутри запроса.</p><p>Пример (Go, обёртка для http.RoundTripper):</p><p>Так можно увидеть, что, например, сторонний сервис отвечает 3 секунды, или Redis стал отдавать ответ за 300 мс вместо обычных 3 мс.</p><h3>Шаг 4. Воспроизвести нагрузку</h3><p>Если проблема зависит от объёма данных, воспроизведите реальный сценарий или нагрузку:</p><ul><li>используйте инструменты вроде k6, Locust, Artillery;</li><li>проверьте реальные условия (например, отчёт за 30 дней, а не за 3).</li></ul><p>Если латентность растёт нелинейно, это может быть признаком N+1-запросов, неоптимального SQL или проблем с аллокацией.</p><h3>Шаг 5. Подтвердить проблему профилированием</h3><p>Если есть подозрение на перегрузку CPU или утечку памяти, используйте профилировщики:</p><ul><li>Go — pprof;</li></ul><ul><li>Java — JFR, VisualVM;</li></ul><ul><li>Node.js — clinic.js, chrome://inspect.</li></ul><p>Пример (Go, локальный pprof):</p><p>Профиль покажет, где тратится CPU, как распределяются аллокации, как часто запускается GC и в каком коде.</p><h3>Шаг 6. Оптимизировать, перепроверить и зафиксировать</h3><p>После подтверждения проблемы можно переходить к исправлениям:</p><ul><li>кэшировать внешний вызов;</li></ul><ul><li>заменить тяжёлую сериализацию (например, на easyjson в Go);</li></ul><ul><li>убрать блокирующую операцию;</li></ul><ul><li>перейти на batch-обработку.</li></ul><p>После изменений обязательно перепроверьте метрики, чтобы убедиться, что оптимизация действительно дала эффект — и не добавила новых узких мест.</p><h2>Итоги: как не попасть в архитектурную ловушку</h2><p>Bottleneck’и возникают даже в самых технологичных проектах. Это не всегда следствие «плохого кода» — куда чаще они появляются из-за архитектурных просчётов: недооценили нагрузку, не предусмотрели масштабирование, сделали ставку на неправильное решение.</p><p>Главная проблема в том, что узкие места проявляются не сразу, а под нагрузкой. В разработке и тестах всё летает, потому что ресурсов хватает. А вот в проде — особенно при росте аудитории — на поверхность всплывает всё, что не выдерживает реального объёма данных или конкуренции за ресурсы.</p><p>Чтобы избежать таких проблем, важно не просто писать чистый код, а мыслить системно: проектировать архитектуру так, чтобы она могла жить под давлением. Вот несколько принципов, которые помогут.</p><h3>Думайте об объёмах и росте</h3><p>Архитектура должна выдерживать не только текущую нагрузку, но и разумный рост. Строить систему на «миллиард пользователей» с первого дня — избыточно. Но если вы рассчитываете, что пользователей станет в 5 раз больше через год — закладывайте это сейчас. Узкие места не любят сюрпризов.</p><h3>Настраивайте метрики с первого дня</h3><p>Без данных вы работаете вслепую. Любой анализ будет превращаться в гадание: виноват код или Redis, где тормозит — непонятно. С самого начала следите за ключевыми метриками: latency, очереди, GC, hit/miss в кэше, ошибки. Это не «доработка потом», а часть живой системы.</p><h3>Проектируйте с учётом деградации</h3><p>Любая система может начать тормозить — вопрос в том, как она себя при этом поведёт. Важно предусмотреть мягкую деградацию: таймауты, очереди, резервные сценарии, ограничение входящего трафика.</p><h3>Не усложняйте архитектуру без повода</h3><p>Микросервисы, шины, очереди, event-driven — всё это мощные инструменты. Но сложность должна быть оправданной. Простая, понятная архитектура с мониторингом и масштабируемыми точками зачастую выигрывает у перегруженной модульной схемы, которую никто не может отладить.</p><p>Bottleneck’и — это не баг, а симптом. И хороший архитектор — это не тот, кто устраняет тормоза, а тот, кто их не допускает.</p><p>Больше боли коддеров — <a href="https://t.me/+ezugB7gnIEsxNGMy">тут</a>.</p>]]></content:encoded>
    </item>
    <item>
      <title>Context Collapse: как микросервисы могут сойти с ума</title>
      <link>https://tproger.ru/articles/context-collapse--kak-mikroservisy-mogut-sojti-s-uma</link>
      <comments>https://tproger.ru/articles/context-collapse--kak-mikroservisy-mogut-sojti-s-uma?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Анна Ельцова]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/context-collapse--kak-mikroservisy-mogut-sojti-s-uma</guid>
      <description><![CDATA[<p>Даже самая идеальная микросервисная архитектура может упасть. В статье обсудим зарубежный материал, где автор рассказывает о проблеме Context Collapse.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/context-collapse--kak-mikroservisy-mogut-sojti-s-uma">Context Collapse: как микросервисы могут сойти с ума</a>»</p>]]></description>
      <category><![CDATA[API]]></category>
      <category><![CDATA[JavaScript]]></category>
      <category><![CDATA[Java]]></category>
      <category><![CDATA[Тестирование]]></category>
      <category><![CDATA[Git]]></category>
      <category><![CDATA[Производительность]]></category>
      <category><![CDATA[Python]]></category>
      <category><![CDATA[JSON]]></category>
      <category><![CDATA[Микросервисы]]></category>
      <category><![CDATA[Отладка]]></category>
      <category><![CDATA[Работа]]></category>
      <category><![CDATA[Node.js]]></category>
      <category><![CDATA[Сервисы]]></category>
      <category><![CDATA[Docker]]></category>
      <category><![CDATA[Redis]]></category>
      <category><![CDATA[OpenTelemetry]]></category>
      <category><![CDATA[Grafana]]></category>
      <category><![CDATA[CI/CD]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Tue, 25 Mar 2025 12:40:02 GMT</pubDate>
      <content:encoded><![CDATA[<p>В феврале на Medium вышла статья <a href="https://levelup.gitconnected.com/context-collapse-the-silent-microservices-killer-76a60058561e">Context Collapse: The Silent Microservices Killer</a>. Перевели ее для вас, так как тема довольно редкая и интересная. Ниже предлагаем обсудить проблему контекста и как он может упасть.</p><h2>Что вообще такое context collapse</h2><p>Есть вероятность, что вы слышите этот термин в первый раз. Немного лирики: в мире микросервисов есть два понятия: технический контекст и доменный контекст (Bounded Context).</p><h2>Технический контекст</h2><p>Технический контекст — это информация, которая передается между микросервисами для выполнения запросов или операций. В нем лежит большое количество данных: идентификаторы запросов (например, TraceID в трассировке), пользовательские сессии, метаданные или параметры аутентификации.</p><p>Без технического контекста микросервисы не могут:</p><ul><li>отслеживать, откуда пришел запрос и куда он направляется,</li><li>сохранять согласованность данных между сервисами,</li><li>выявлять сбои (например, через логи или трассировку).</li></ul><h2>Доменный контекст (Bounded Context)</h2><p>Доменный контекст происходит из методологии Domain-Driven Design (DDD) — это про четкое разделение бизнес-логики между микросервисами. У каждого сервиса должна быть своя «ограниченная область» (Bounded Context), где термины, данные и правила имеют уникальное значение.</p><p>Например, в интернет-магазине слово «заказ» может означать разные вещи для сервиса оплаты (финансовая транзакция) и сервиса доставки (отправка покупателю). Если границы контекста не определены четко, возникает путаница: сервисы начинают дублировать логику или интерпретировать данные по-разному.</p><p>Без доменного контекста:</p><ul><li>код будет дублироваться и появится избыточность,</li><li>усложнится взаимодействие между сервисами,</li><li>систему будет сложнее поддерживать.</li></ul><h2>Вернемся к коллапсу контекста</h2><p>Автор статьи просит нас представить следующую картину: вы сделали новую архитектуру, она масштабируется, разделяется, деплои проходят гладко, CI/CD пайплайны цветут и пахнут — другими словами, не архитектура, а мечта. Все работает как часы, поэтому вы сидите, попивая кофе и раскладывая пасьянс.</p><p>Вдруг приходит пользователь и сообщает, что платеж был проведен дважды. На панели мониторинга появляются задержки, и логи здесь вообще бесполезны. Вы начинаете разбираться и спустя несколько часов споров с терминалом, находите причину: один из сервисов забыл, кто вообще такой этот пользователь, прямо во время проведения транзакции.</p><p>Тра-та-та, это и есть контекстный коллапс. Как называет его автор — тихий убийца микросервисов в 2025 году. Поймать этот баг с помощью breakpoints нельзя, при этом он будет уничтожать вашу производительность и код в целом.</p><h2>Что на самом деле происходит</h2><p>Представьте, что микросервисы — это эстафетный забег: каждый сервис передает «эстафетную палочку» — идентификаторы пользователей, сессионные данные, намерения — следующему участнику. Контекстный коллапс случается, когда эта палочка выпадает на бегу.</p><p>Сервис теряет важное состояние, например:</p><ul><li>«Это корзина Пети»</li><li>"Этот платеж уже прошел"</li></ul><p>И начинается хаос: дублирующиеся API-запросы, потерянные транзакции или, что еще хуже, повреждение данных.</p><p>В 2025 году появляется все больше слишком фрагментированных архитектур и гипермасштабируемых систем. Ваши запросы могут проходить через 10, 20 и даже 30 сервисов, но если на каком-то этапе отвалился контекст, то это конец.</p><p>В статье автор не делает акцента на том, какой именно это контекст — технический или доменный. На самом деле может произойти все что угодно: это может быть как потеря технического контекста, так и нарушение доменных границ (когда сервисы лезут не в свое дело). Оба случая приводят к хаосу: данные становятся несогласованными, ошибки множатся, а разработчики теряют контроль над системой.</p><h2>Как понять, что контекст вот-вот может «коллапснуться»</h2><p>Вы наверняка были в такой ситуации: вы на 100% уверены, что система должна работать, но она не работает, хотя ошибок в коде никаких, казалось бы, нет. Вот основные признаки коллапса, с которыми вы можете столкнуться:</p><ul><li>Всплеск пинга: сервисы запрашивают данные, которые должны бы уже знать, создавая огромное количество лишних вызовов и перегружая API.</li><li>Дублирующиеся действия: повторные списания, задвоенные отправки писем, неожиданные повторные заказы. Пользователи возмущены, рейтинг падает.</li><li>Мистические баги: логи говорят «всё в порядке», но результат явно неверный. Код не видит проблему, а отладка превращается в кошмар.</li></ul><p>Давайте рассмотрим на примере. Клиент решает перевести деньги с одного счёта на другой. Сервис транзакций отправляет запрос в модуль проверки лимитов, затем передаёт его в систему обработки платежей. Но из-за потери контекста на одном из этапов модуль проверки лимитов теряет информацию о сессии клиента и воспринимает его как нового пользователя. В результате:</p><ul><li>Лимиты не распознаются, и транзакция блокируется, даже если у пользователя достаточно денег.</li><li>Либо, наоборот, проверка пропускается, и клиенту позволяют перевести больше лимита.</li><li>Сервис обработки платежей не получает подтверждение проверки и отправляет повторный запрос, а это может привести к двойному списанию средств.</li></ul><p>Клиент идет громить службу поддержки, она — разработчиков, а они размахивают руками, потому что в логах все отлично.</p><p>Опрос CNCF за 2024 год показал, что 68% команд, которые используют микросервисы, сталкиваются с необъяснимыми проблемами производительности. И всему виной в том числе контекстный коллапс.</p><h2>5 решений, как бороться с контекстным коллапсом</h2><p>Да, контекстный коллапс — крайне неприятный баг. Главное — чтобы все сервисы работали слаженно и «знали» друг друга.</p><h3>1. Правильно передавайте контекст</h3><p>Вам нужно передавать критические важные данные — идентификаторы пользователей, состояние транзакций, метаданные запроса — в каждом шаге цепочки. Стоит использовать OpenTelemetry, чтобы распространять контекст по сервисам.</p><p>Вот пример реализации middleware автором на Go:</p><h3>2. Кэшируйте с умом</h3><p>Нет смысла заставлять сервисы угадывать, если они просто могут запомнить. Быстрый кэш, например, с Redis, может хранить временный контекст — например, токены сеанса или состояния запросов, — поэтому сервисы не будут постоянно спрашивать «кто это?» Вот фрагмент кода на Питоне, где используется Redis для хранения и извлечения контекста:</p><p>Это позволяет поддерживать контекст во всех вызовах, не перегружая вашу базу данных. А еще TTL (time-to-live) Redis сам за собой убирает — устаревшие данные не скрываются.</p><h3>3. Используйте Event-Driven Architecture</h3><p>Микросервисы без сохранения состояния выглядят отлично, пока не контекст не коллапснется. Событийная архитектура идет от обратного: вместо того чтобы надеяться, что службы все запомнят, регистрируйте каждый шаг как событие. Если служба все-таки забудет, воспроизведите поток. Вот пример на Node.js с Kafka:</p><p>Здесь также можно использовать RabbitMQ. В общем, больше никаких отговорок со стороны сервиса из разряда «я забыл».</p><h3>4. Проверяйте логи</h3><p>Когда контекст теряется, логи — ваше место преступления. Настройте Grafana Loki или Datadog для поиска «потерянных контекстов». Вот пример с Grafana:</p><p>Тэгните логи с помощью request_id или user_id, а затем вызовите Loki:</p><h3>5. Тестируйте на коллапсы</h3><p>Профилактика лучше, чем лечение. Добавьте хаос-тестирование с помощью, например, Chaos Mesh, чтобы моделировать потери контекста.</p><p>Хаос-тестирование — это метод преднамеренного введения сбоев в систему, чтобы проверить, насколько она устойчива и надежна. Обычное тестирование и мониторинг могут выявить проблемы, но хаос-тестирование (Chaos Engineering) помогает увидеть, как система поведет себя в случае неожиданных отказов.</p><p>Прервите mid-requests к сервисам — сможет ли система восстановится в таком случае? Если нет, значит, ваша передача контекста недостаточно надежна.</p><p>Эти решения не универсальны. Начните с правильного распространения контекста для быстрых улучшений, затем добавьте кэширование или событийную архитектуру для масштабируемости и постоянно аудируйте систему.</p>]]></content:encoded>
    </item>
    <item>
      <title>Что такое OpenTelemetry и как она может улучшить качество ваших сервисов</title>
      <link>https://tproger.ru/articles/chto-takoe-opentelemetry-i-kak-ona-mozhet-uluchwit-kachestvo-vawih-servisov</link>
      <comments>https://tproger.ru/articles/chto-takoe-opentelemetry-i-kak-ona-mozhet-uluchwit-kachestvo-vawih-servisov?scrollTo=tproger-comments</comments>
      <dc:creator><![CDATA[Dmitrii “Ymatigoosa”]]></dc:creator>
      <guid isPermaLink="true">https://tproger.ru/articles/chto-takoe-opentelemetry-i-kak-ona-mozhet-uluchwit-kachestvo-vawih-servisov</guid>
      <description><![CDATA[<p>Разбираем OpenTelemetry: трассировка, логи, метрики и Baggage в микросервисах. Узнайте, как настроить наблюдаемость и повысить надёжность бэкенда.</p><p>— Читать дальше «<a rel="follow" href="https://tproger.ru/articles/chto-takoe-opentelemetry-i-kak-ona-mozhet-uluchwit-kachestvo-vawih-servisov">Что такое OpenTelemetry и как она может улучшить качество ваших сервисов</a>»</p>]]></description>
      <category><![CDATA[Микросервисы]]></category>
      <category><![CDATA[DevOps]]></category>
      <category><![CDATA[Kubernetes]]></category>
      <category><![CDATA[OpenTelemetry]]></category>
      <category><![CDATA[Статьи]]></category>
      <slash:comments>0</slash:comments>
      <pubDate>Wed, 05 Jun 2024 13:00:03 GMT</pubDate>
      <content:encoded><![CDATA[<p>Раньше, когда мы говорили "backend", мы скорее всего имели в виду одно большое приложение с одной большой базой данных, для мониторинга которого было достаточно простого логирования. Теперь, благодаря технологиям вроде Kubernetes, микросервисы стали стандартом, приложений становится больше, приложения становятся распределенными и традиционного логирования уже недостаточно для отладки и диагностики проблем.</p><p>Отличным решением для организации мониторинга является OpenTelemetry - современный инструментарий, который можно применять для отладки и анализа производительности распределенных систем.</p><p>Эта статья предназначена для профессионалов в сфере IT, стремящихся расширить свои знания в области оптимизации работы бэкенда. Ниже мы в деталях рассмотрим, что такое OpenTelemetry, из каких ключевых концептов она состоит, и какие проблемы помогает решить. Если вам интересно, как OpenTelemetry может изменить подход к мониторингу и отладке ваших бэкенд-систем, повысив их надежность и эффективность – читайте дальше!</p><p>OpenTelemetry — важный слой наблюдаемости, но не вся картина целиком. Если нужен полный маршрут DevOps-инженера и место observability в нём, посмотрите <a href="https://tproger.ru/articles/roadmap-devops-inzhenera-v-2026-godu--chto-uchit-i-v-kakom-poryadke">Roadmap DevOps-инженера в 2026 году: что учить и в каком порядке</a>.</p><h2>Коротко об истории OpenTelemetry</h2><p>Первыми с проблемой распределенного логирования и трейсинга столкнулись крупные технологические компании еще в конце 2000-х годов. В 2010 году Google опубликовал статью "<a href="https://research.google/pubs/pub36356/">Dapper, a Large-Scale Distributed Systems Tracing Infrastructure</a>", послужившую основой для разработки компанией Twitter инструмента трейсинга - Zipkin, который был выпущен в 2012 году.</p><p>Затем, в 2014 году, появился Kubernetes, который значительно упростил разработку микросервисов и других облачных распределенных систем. Это привело к тому, что все большее количество компаний начало сталкиваться с проблемой распределенного логирования и трейсинга в микросервисах. Для стандартизации распределенного трейсинга был создан стандарт OpenTracing, принятый <a href="https://www.cncf.io/">CNCF</a>, а также проект OpenCensus от Google, решающий ту же задачу.</p><p>В 2019 году проекты OpenTracing и OpenCensus объявили об объединении под названием OpenTelemetry. Полученная платформа объединяет лучшие практики, накопленные за много лет, и позволяет с минимальными усилиями интегрировать трассировку, логирование и метрику в любые системы, независимо от их сложности.</p><p>Сегодня OpenTelemetry – это не просто проект, это по сути отраслевой стандарт сбора и передачи телеметрических данных. Его разработку и поддержку осуществляет сообщество специалистов и компаний – лидеров рынка, таких как Google, и Microsoft. Проект непрерывно развивается, получая все новые возможности с целью еще большего упрощения процесса интеграции и использования.</p><figure><img src="https://media.tproger.ru/user-uploads/100037/2024-04-14/4c6789c8-4d1f-4332-a672-0aabb31f594c.png" alt="" /></figure><h2>А что внутри?</h2><p>OpenTelemetry - это целый набор практик и инструментов, определяющих, какие <b>сигналы </b>может генерировать приложение для взаимодействия с внешним миром, а также как эти сигналы можно собирать и визуализировать для наблюдения за состоянием приложений и системы в целом. Три основных типа сигналов: трассировка, логирование и сбор метрик. Давайте рассмотрим составляющие части подробнее.</p><figure><img src="https://media.tproger.ru/user-uploads/100037/2024-04-14/af81be8e-6831-4bef-b6a9-50d9d192cb0a.jpg" alt="" /></figure><h3>Контексты</h3><p>OpenTelemetry вводит понятие контекста операции. Контекст в первую очередь включает в себя атрибуты trace_id (идентификатор текущей операции) и span_id (идентификатор подзапроса, при ретраях каждый подзапрос, например, должен иметь уникальный span_id). Кроме того, контекст может содержать статическую информацию, такую как название ноды, на которой развернуто приложение, или название окружения (prod/qa). Такие поля в терминологии OpenTelemetry называются resource и прикрепляются к каждому логу, метрике или трассировке для более удобного поиска. Дополнительно в контексте могут содержаться динамические данные, например, идентификатор текущего эндпоинта ("http_path": "GET /user/:id/info"), которые могут быть выборочно прикреплены к группе логов, метрик или трассировок.</p><p>Контексты OpenTelemetry могут передаваться между различными приложениями с использованием протоколов context propagation. Эти протоколы представляют собой наборы заголовков, которые добавляются в каждый HTTP- или gRPC-запрос, или же в заголовки сообщений для очередей. Это позволяет нижестоящему приложению восстановить контекст операций из этих заголовков.</p><p>Можно выделить следующие виды передачи контекста:</p><ul><li>B3-Propagation: Это набор заголовков вида x-b3-*, изначально разработанный для системы трассировки Zipkin. Он был адаптирован в OpenTracing и использовался многими инструментами и библиотеками. B3-Propagation обеспечивает передачу trace_id/span_id, а также флага, указывающего на необходимость семплирования;</li><li>На смену ему пришел стандарт W3C Trace Context: Разработанный рабочей группой W3C. Он объединяет в себе практики различных подходов к контекстному распространению в единый стандарт и этот подход является стандартным в OpenTelemetry.</li></ul><h3>Tracing</h3><p>Трейсинг - это процесс записи и последующей визуализации таймлайна пути запроса через несколько приложений.</p><figure><img src="https://media.tproger.ru/user-uploads/100037/2024-04-14/0dbcda7c-dc83-489c-9d85-6124408a06a5.png" alt="" /><figcaption>Источник: https://opentelemetry.io/docs/demo/screenshots/</figcaption></figure><p>На визуализации каждая полоска называется "span" и имеет идентификатор "span_id". Корневой span называется "trace" и имеет идентификатор "trace_id", который служит идентификатором всего запроса.</p><p>Такая визуализация позволяет:</p><ul><li>Анализировать время исполнения запроса на каждой из систем и баз данных и выявлять узкие места, требующие оптимизации</li><li>Выявлять циклические зависимости между сервисами</li><li>Находить дублирующие запросы</li></ul><p>Используя данные трейсинга, можно также строить дополнительные аналитики, такие как построение карты микросервисов или распределение времени между разными системами при обработке операций.</p><p>Важно отметить, что даже если не используется выгрузка трейсов для визуализации таймлайнов, OpenTelemetry все равно генерирует trace_id и span_id для использования в других сигналах.</p><h3>Logs</h3><p>Логирование, несмотря на свою кажущуюся тривиальность, остается одним из наиболее мощных инструментов для диагностики проблем. OpenTelemetry расширяет возможности традиционного логирования, добавляя контекстную информацию. В частности, при наличии открытого трейса к логам автоматически добавляются атрибуты trace_id и span_id, позволяющие связать логи с таймлайном операций. Кроме того, в качестве атрибутов лога можно использовать как статическую информацию из контекста OpenTelemetry – например, идентификатор ноды, так и динамическую информацию – например, идентификатор текущего HTTP-эндпоинта ("http_path": "GET /user/:id").</p><p>Используя идентификатор trace_id, можно найти логи всех микросервисов, связанных с текущим запросом, в то время как span_id позволяет разделять подзапросы. Например, в случае ретраев логи от разных попыток будут иметь разные span_id. Использование этих идентификаторов позволяет быстро анализировать поведение всей системы в реальном времени и быстрее диагностировать проблемы, что способствует повышению стабильности и надежности.</p><h3>Metrics</h3><p>Сбор метрик предоставляет количественные данные о работе системы, такие как задержки, количество ошибок, использование ресурсов и т.д. Мониторинг метрик в реальном времени позволяет оперативно реагировать на изменения в производительности системы и предотвращать сбои и перегрузки, гарантируя высокий уровень доступности и надежности приложения для пользователей. А интеграция с хорошо известными инструментами, такими как Prometheus и Grafana,  позволяет визуализировать эти данные, что существенно упрощает мониторинг.</p><figure><img src="https://media.tproger.ru/user-uploads/100037/2024-04-14/6df10692-e64c-45a1-b445-540820e1465c.png" alt="" /><figcaption>Источник: https://grafana.com/blog/2021/06/22/grafana-dashboard-showcase-visualizations-for-prometheus-home-energy-usage-github-and-more/</figcaption></figure><p>Коллекторы метрик OpenTelemetry совместимы со стандартами выгрузки метрик Prometheus и OpenMetrics, что обеспечивает возможность легкого перехода на решения OpenTelemetry без необходимости внесения значительных изменений. SDK OpenTelemetry позволяет вместе с метриками выгружать примеры trace_id, чтобы была возможность связывать метрики с примерами логов и трейсами.</p><h3>Связь между сигналами</h3><p>Вместе логи, метрики и трейсинг создают комплексную картину состояния системы: логи предоставляют информацию о событиях в системе, позволяя быстро находить и исправлять ошибки; метрики отражают качественные и количественные показатели работы системы, например, время отклика или процент ошибок; трейсинг дополняет эту картину, отображая путь выполнения запросов через различные компоненты системы, что помогает понять взаимосвязи между ними. Однозначная связь логов, трейсов и метрик – одна из отличительных особенностей OpenTelemetry.</p><p>Grafana, к примеру, позволяет пользователю, открывшему лог, увидеть соответствующий трейс и метрики запроса, что существенно повышает удобство и эффективность использования платформы.</p><figure><img src="https://media.tproger.ru/user-uploads/100037/2024-04-14/9193fdd5-f59a-400e-9201-9fa7b3102fd9.jpg" alt="" /><figcaption>Источник: https://grafana.com/blog/2020/03/31/how-to-successfully-correlate-metrics-logs-and-traces-in-grafana/</figcaption></figure><p>Помимо этих трех компонентов, OpenTelemetry включает в себя концепции Sampling, Baggage и управления контекстом операций.</p><h3>Sampling</h3><p>В высоконагруженных системах трафик выгружаемых логов и трейсов может достигать огромных значений, требуя невероятных расходов на инфраструктуру и хранение данных. Для решения этой проблемы стандарты OpenTelemetry включают в себя возможность семплирования сигналов – возможность выгружать не все трейсы и логи, а только их часть. Например, можно подробно выгружать только сигналы от некоторого процента запросов или выбрать для выгрузки только сигналы от долгих запросов или только сигналы от запросов, завершившихся с ошибкой. Это позволяет получить достаточную выборку для построения статистики и сэкономить значительные ресурсы.</p><p>Однако, если каждая система самостоятельно принимает решения о том, какие запросы мониторить подробно, а какие – нет, мы столкнемся с тем, что будем всегда видеть фрагментированную картину по каждому запросу – некоторые системы будут выгружать подробные данные, в то время как другие могут решить, выгружать данные частично или вовсе не выгружать их.</p><p>Для решения этой проблемы механизмы распространения контекста OpenTelemetry передают флаг семплирования вместе с trace_id/span_id. Это необходимо для того, чтобы, если сервис, первый принявший запрос от пользователя, решит, что запрос должен быть выгружен подробно, все остальные системы также должны следовать этому решению и также выгружать данные подробно. В противном случае все системы должны выгружать сигналы частично или не выгружать вовсе, чтобы сэкономить ресурсы. Такой подход называется "Head Sampling" - решение принимается в начале обработки запроса случайно или на основе каких-то входных атрибутов.</p><p>Кроме этого, OpenTelemetry поддерживает режим "Tail Sampling", при котором все приложения всегда выгружают все сигналы подробно, но существует некоторый промежуточный буфер. После получения всех данных этот буфер принимает решение о том, стоит ли сохранить данные полностью или оставить лишь частичную выборку. Такой подход позволяет сохранить более репрезентативную выборку по каждой категории запросов (успешные/долгие/ошибочные), но требует настройки дополнительной инфраструктуры.</p><h3>Baggage</h3><p>Механизм Baggage позволяет вместе с trace_id/span_id передавать произвольный набор пар ключ-значение, который автоматически будет передаваться между всеми микросервисами по ходу обработки запроса. Этот механизм полезен для передачи дополнительной информации, которая необходима на протяжении всего пути запроса – например, для передачи информации о пользователе или настройках среды выполнения.</p><p>Пример заголовка для передачи baggage по стандарту w3c</p><p>tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE,userId=1c30032h5</p><p>Вот несколько примеров использования Baggage:</p><ul><li>Передача бизнес контекста операции, такого как userId, productId, deviceId, через все микросервисы. Такую информацию приложения могут автоматически логировать, чтобы получить возможность искать логи любого приложения по контексту юзера, совершившего оригинальный запрос;</li><li>Специфичные настройки параметры для SDK или инфраструктуры;</li><li>Флаги, по который балансировщики могут принимать решение о роутинге запроса – такой подход можно использовать, чтобы автоматически роутить некоторые запросы на mock бекенды во время тестирования. Т.к. baggage передается автоматически – не надо придумывать дополнительных протоколов, достаточно настроить правило на балансировщике.</li></ul><p>Важный момент: хотя влияние Baggage на производительность само по себе минимально, чрезмерное его использование может существенно увеличить нагрузку на сеть и сервисы. Тщательно выбирайте, какие данные вам действительно необходимо передавать через Baggage, если хотите избежать проблем с производительностью.</p><h2>Имплементация</h2><h3>С точки зрения инфраструктуры</h3><p>Имплементация OpenTelemetry на уровне инфраструктуры требует интеграции бекендов OpenTelemetry в архитектуру приложений и настройки инфраструктуры для агрегации данных и состоит из четырех этапов:</p><ol><li><b>Интеграция с приложениями.</b> На первом этапе SDK или агенты OpenTelemetry интегрируются непосредственно в приложения для сбора метрик, логов и трассировок. Это обеспечивает непрерывный поток данных о работе каждого компонента системы.</li><li><b>Конфигурация экспортеров.</b> Собранные данные направляются из приложений через экспортеры во внешние системы для дальнейшей обработки. Это могут быть системы логирования, мониторинга, трассировки или аналитики, в зависимости от ваших потребностей.</li><li><b>Агрегация и хранение.</b> На этом этапе может происходить нормализация данных, обогащение их дополнительной информацией и слияние данных из разных источников для создания единого представления о состоянии системы.</li><li><b>Визуализация данных.</b> Наконец, обработанные данные представляются в виде дашбордов в таких системах, как Grafana (для метрик и трассировок) или Kibana (для логов). Это позволяет командам быстро оценивать состояние системы, выявлять проблемы и тенденции, а также создавать алерты на основе созданных сигналов.<br /></li></ol><h3>С точки зрения приложения</h3><p>Для интеграции с приложением необходимо подключить SDK OpenTelemetry для соответствующего языка или использовать библиотеки и фреймворки, имеющие прямую поддержку OpenTelemetry. OpenTelemetry часто предоставляет свои реализации широко используемых интерфейсов известных библиотек, что позволяет осуществить drop-in замену. Например, в мире Java для сбора метрик часто используется библиотека Micrometer. SDK OpenTelemetry предоставляет свои реализации интерфейсов Micrometer, что позволяет заменить выгрузку метрик без изменения основного кода приложения. Кроме того, OpenTelemetry предоставляет реализации старых интерфейсов OpenTracing и OpenCensus, что позволяет максимально безболезненно мигрировать на OpenTelemetry.</p><h2>В заключение</h2><p>В условиях постоянного развития и усложнения IT-систем OpenTelemetry – ключ к двери в будущее надежных и эффективных бэкендов. Этот инструмент не просто упрощает отладку и мониторинг; он открывает возможности для глубокого понимания работы приложений и оптимизации их производительности на новом уровне. Присоединяйтесь к сообществу OpenTelemetry, чтобы вместе приближать будущее, где каждый из нас может сделать разработку бэкендов проще и эффективнее!</p>]]></content:encoded>
    </item>
  </channel>
</rss>