Распределённая трассировка и событийная обвязка: X-Ray и EventBridge
Метрики и логи говорят, что узлу плохо; распределённая трассировка говорит, какой хоп тормозит, а EventBridge развязывает хопы. Сэмплируй трассы и ограничивай кардинальность логов — иначе observability спишет с тебя дважды.
API оформления заказа начинает отвечать за 1,8 секунды вместо 200 мс. Дашборды издевательски зелёные: CPU в норме, память в норме, p99 каждого сервиса «нормальный». Дежурный час грепает логи пяти сервисов, не находит ничего убедительного и начинает гадать — перезапустить сервис корзины, сбросить кэш, обвинить базу. Настоящей причиной был SDK платёжного провайдера, который тихо добавил синхронный вызов расчёта налога к четвёртому сервису, а тот ждал кросс-региональное чтение из DynamoDB. Ни одна строка лога не сказала «это я медленный», потому что сам по себе медленным не был ни один сервис — латентность жила в рёбрах между сервисами. У команды были метрики и логи, первые два столпа observability. Не хватало третьего: распределённой трассы, которая рисует весь запрос как единый таймлайн и указывает на точный хоп, где уходят миллисекунды. Они включили X-Ray, и следующий инцидент занял четыре минуты вместо часа.
Трассировка: третий столп, и почему одних логов мало
Метрики и логи — покомпонентные. Метрика — число с одного узла; строка лога — одно событие на одном узле. Ни то, ни другое не знает, что этот запрос к оформлению заказа развернулся в вызов корзины, затем платежей, затем сервиса налогов, затем DynamoDB — и что 1,6 секунды лишней латентности ушли на ожидание налогового хопа. Чтобы ответить «где в цепочке вызовов ушло время?», нужна запись, которая пронизывает сервисы и сшивает их в единый таймлайн. Это распределённая трассировка.
AWS X-Ray моделирует запрос как трассу: совокупность всей работы по обслуживанию одного запроса, идентифицируемую единым trace ID. Каждый сервис добавляет сегмент — документ, фиксирующий имя сервиса, запрос и ответ, время начала и конца и любые ошибки или фолты. Внутри сегмента каждый нижестоящий вызов твоего кода — это subsegment: вызов DynamoDB, внешнего HTTP API или SQL-запрос, каждый со своим таймингом. Для нижестоящего сервиса, который сам себя не трассирует (DynamoDB не отправляет сегментов), X-Ray строит выведенный (inferred) сегмент из твоего subsegment, чтобы зависимость всё равно появилась на карте. В этом структурное отличие от логов: трасса — это дерево сегментов и subsegment’ов, а не плоский поток строк, и это дерево в точности повторяет форму твоего графа вызовов.
Клей, связывающий сегменты через границы процессов, — проброшенный заголовок X-Amzn-Trace-Id. Первый интегрированный с X-Ray сервис, на который попадает запрос, штампует его корневым trace ID и решением о сэмплировании, а каждый инструментированный хоп пробрасывает его дальше, добавляя свой parent segment ID, чтобы X-Ray мог восстановить рёбра родитель/потомок.
X-Amzn-Trace-Id: Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1X-Ray сворачивает сегменты всех сервисов по одному trace ID в карту сервисов (service map / service graph): узлы на каждый сервис, рёбра на каждый вызов между ними, и каждое ребро аннотировано латентностью и долей ошибок. Именно карта превращает «оформление тормозит» в «ребро оформление→налог держит 1,6 с p99 и 0% ошибок» — то, что логи структурно показать не могут, потому что строка лога живёт внутри одного узла и ничего не знает о ребре.
Сэмплирование: ты намеренно меняешь полноту на стоимость
Трассировать каждый запрос дорого вдвойне: счёт за трассу и латентность, которую SDK добавляет, собирая и отправляя сегменты. Поэтому X-Ray сэмплирует. Правило по умолчанию — резервуар плюс ставка: записывать первый запрос каждую секунду, затем 5% всего сверх того. Резервуар гарантирует, что ты всегда захватываешь базовый набор трасс даже при низком трафике; ставка 5% не даёт высоконагруженной точке записать миллионы почти одинаковых трасс. Ты настраиваешь правила по сервису или свойству запроса — отключаешь сэмплирование (трассируешь 100%) на путях, меняющих состояние, или платёжных, где важна каждая трасса, и сэмплируешь health-check’и и фоновый поллинг по низкой ставке, потому что это шум.
Стоимость реальна, и это самый частый режим отказа observability. X-Ray тарифицирует за трассу записанную и, отдельно, за трассу извлечённую или просканированную. Иллюстративные цифры со страницы тарифов CloudWatch (US East / Сев. Вирджиния — цены зависят от региона, всегда сверяйся со своим): записанные трассы стоят около $5,00 за миллион ($0,000005 за штуку) сверх free tier в 100 000 в месяц, а извлечённые/просканированные — около $0,50 за миллион сверх 1 000 000 бесплатных. Эти числа кажутся пустяком, пока кто-нибудь не выставит сэмплирование на 100% на пути, делающем 5000 запросов/с: это ~13 миллиардов трасс в месяц, примерно $65 000 только за запись — классическая история «включили полную трассировку в проде и получили пятизначный сюрприз». Сэмплирование — не компромисс по качеству, за который надо извиняться; это намеренный размен, делающий трассировку доступной.
EventBridge: развяжи хопы, которые ты только что протрассировал
У инцидента с налогом был и второй урок: тот синхронный вызов вообще не должен был быть синхронным. Когда сервис платежей вынужден вызвать сервис налогов inline и ждать, эти двое жёстко связаны — медленный тащит за собой быстрый, а деплой любого из них может сломать другой. Amazon EventBridge — это serverless шина событий (event bus), которая ломает эту связанность. Производитель вызывает PutEvents, чтобы опубликовать событие; он не знает и не заботится, кто его потребит. Правила (rules) на шине сопоставляют паттерн события с полями события — source, detail-type и произвольный JSON в detail — и маршрутизируют каждое совпадение к одной или нескольким целям (targets: Lambda, очередь SQS, машина состояний Step Functions, другая шина), которые выполняются параллельно.
{
"Source": "checkout.service",
"DetailType": "OrderPlaced",
"Detail": {
"orderId": "o-4821",
"region": "eu-west-1",
"amountCents": 7400,
"needsTax": true
}
}Правило, разводящее это на Lambda расчёта налога и аналитическую очередь, сопоставляется по паттерну ниже; всё, что не совпало, молча игнорируется, поэтому добавление потребителя никогда не трогает производителя:
{
"source": ["checkout.service"],
"detail-type": ["OrderPlaced"],
"detail": { "needsTax": [true] }
}Шина по умолчанию (default bus) получает события от самих сервисов AWS (смена состояния EC2, загрузка в S3) — так ты строишь автоматизацию, реагирующую на собственную инфраструктуру; кастомные шины несут доменные события твоего приложения, изолируя их от потока событий AWS-сервисов. Реестр схем (schema registry) может вывести и версионировать форму событий, чтобы потребители кодировались под контракт, а не под догадку. Выигрыш — хореография: оформление эмитит OrderPlaced и уходит; налог, аналитика и почта подписываются независимо. Ни один сервис не держит список «кого звать дальше», поэтому ты добавляешь и убираешь потребителей без передеплоя производителей — ровно та связанность, которая превратила изменение стороннего SDK в межсервисный инцидент.
▸Почему это работает
EventBridge, SNS и SQS пересекаются достаточно, чтобы путать, но отвечают на разные вопросы. SNS — pub/sub-фанаут: одно сообщение, много подписчиков, без фильтрации по контенту дальше грубых атрибутов сообщения — бери, когда просто надо разослать быстро и дёшево. SQS — очередь: она буферизует и развязывает во времени, давая медленному потребителю вычерпывать работу в своём темпе с ретраями и dead-letter-очередью — бери, когда нужны надёжность и backpressure ровно между двумя сторонами. EventBridge — маршрутизатор: богатый JSON-матчинг по контенту события, много источников ко многим целям, реестр схем и события AWS-сервисов из коробки — бери, когда логика маршрутизации живёт в контенте события и ты хочешь полностью развязать производителей с меняющимся набором потребителей. Они компонуются: EventBridge маршрутизирует событие в очередь SQS, которая буферизует его для медленного воркера.
Структурированные логи: третий столп, сделанный запрашиваемым
Трассировка говорит, какой хоп; логи всё ещё говорят, что произошло внутри этого хопа — но только если они запрашиваемы. Сеньор эмитит структурированные логи: JSON-объекты с именованными полями, а не человеческую прозу. Строка вроде «пользователь 4821 не оплатил, код 51» непоискабельна; запись вроде {“level”:“error”,“traceId”:“1-5759e988”,“userId”:4821,“event”:“payment_failed”,“declineCode”:51} позволяет вытащить все логи по одному trace ID и сджойнить их с таймлайном X-Ray. Протаскивание trace ID в логи и сплавляет второй и третий столпы. Embedded Metric Format (EMF) (встроенный формат метрик CloudWatch) идёт дальше: эмитишь специально оформленный JSON-лог, и CloudWatch автоматически извлекает из него метрики — так из одной записи получаешь и метрику, и подпирающий её лог.
Но поля стоят денег, и ловушка — кардинальность. CloudWatch Logs тарифицирует за ГБ принятого (иллюстративно около $0,50/ГБ сверх free tier в 5 ГБ в US East, зависит от региона), а EMF превращает высококардинальные измерения в кастомные метрики по примерно $0,30 за метрику в месяц. Поставь userId или requestId измерением EMF — и каждое отдельное значение становится своей тарифицируемой метрикой: миллион пользователей превращается в миллион метрик и четырёхзначную месячную строку счёта из-за одного неосторожного поля. Логируй поля свободно; держи измерения метрик низкокардинальными (статус, маршрут, регион — а не id пользователя или запроса). Переинструментирование — режим отказа ровно как пересэмплирование.
| Какой вопрос задаёшь | Столп, который отвечает | Поверхность AWS | Рычаг стоимости |
|---|---|---|---|
| Сервису плохо прямо сейчас? | Метрики | Метрики / алармы CloudWatch | Кардинальность измерений кастомных метрик (~$0,30/метрика/мес) |
| Что именно произошло внутри узла? | Логи (структурированные) | CloudWatch Logs / EMF | ГБ принятого (~$0,50/ГБ сверх free tier) |
| Какой хоп в цепочке тормозит? | Трассы | Карта сервисов X-Ray | Ставка сэмплирования (~$5/М записанных трасс) |
| Как развязать хопы A и B? | Событийный дизайн | Шина + правила EventBridge | PutEvents за событие + вызовы за цель |
Событие OrderPlaced должно разойтись на три независимых потребителя (расчёт налога, аналитика, почта), каждый принадлежит своей команде, которая приходит и уходит; маршрутизация зависит от полей внутри события (region, needsTax). Выбери примитив развязки.
Цепочка запроса через пять сервисов периодически тормозит, но CPU, память и собственная метрика p99 каждого сервиса выглядят нормально. Какой инструмент observability создан, чтобы локализовать медленный хоп?
Команда выставляет сэмплирование X-Ray на 100% запросов на пути, обслуживающем 5000 запросов/с, и шокирована счётом. Какой принцип они нарушили?
- 01Что распределённая трасса показывает такого, чего метрики и логи структурно не могут, и как X-Ray это моделирует?
- 02Почему observability — это решение о стоимости, и как сюда вписываются сэмплирование и EventBridge?
Observability стоит на трёх столпах, и сеньоры держат их раздельно: метрики отвечают «сервису плохо?» (покомпонентные числа, алармы CloudWatch), структурированные логи отвечают «что именно произошло внутри узла?» (запрашиваемый JSON с протащенным trace ID, EMF для чеканки метрик из логов), а распределённые трассы отвечают «какой хоп в цепочке запроса тормозит?» — то, что логи и метрики структурно не могут, потому что они живут внутри одного узла, а латентность часто живёт в рёбрах между сервисами. X-Ray моделирует запрос как трассу с одним trace ID: каждый сервис эмитит сегмент своей работы, каждый нижестоящий вызов — subsegment, заголовок X-Amzn-Trace-Id пробрасывает трассу через границы процессов, а X-Ray рисует карту сервисов с рёбрами, аннотированными латентностью и ошибками, которая указывает прямо на медленный хоп. Поскольку трассировка и высококардинальное логирование стоят денег и добавляют латентность, observability — это бюджетное решение: сэмплируй трассы (правило по умолчанию «первый запрос/с плюс 5%» намеренно меняет полноту на стоимость; 100% на занятом пути — пятизначная ошибка) и ограничивай кардинальность логов/метрик (userId измерением EMF взрывается в миллион тарифицируемых метрик). Наконец, EventBridge не даёт хопам, которые ты трассируешь, связаться изначально: производители PutEvents и уходят, правила матчат по source/detail-type/detail и разводят на цели параллельно — маршрутизатор по контенту, к которому тянешься вместо SNS (простой фанаут) и SQS (буфер/backpressure), когда маршрутизация живёт в контенте события, а потребители приходят и уходят. Теперь, когда встречаешь многосервисную загадку латентности, где метрики каждого сервиса выглядят нормально, первым делом открываешь карту сервисов X-Ray — а когда видишь синхронный вызов, способный замедлить несвязанный сервис, спрашиваешь себя, стоит ли его развязать через EventBridge.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.