Наблюдаемость: где на самом деле теряются миллисекунды
Дашборд краснеет, а причина не находится: телеметрия собрана не везде, собранное не переведено в решения, а числа прочитаны неверно. Разбираем все три слоя по очереди.

Дашборд краснеет, а причина не находится. У наблюдаемости, которая так себя ведёт, обычно три независимые беды: телеметрия собрана не везде, собранное не переведено в решения, а полученные числа прочитаны неверно.
Разбираем все три слоя: как получить трассировку, не переписывая сервисы, как превратить трассы в метрики, по которым можно действовать, и как не принять смену методики измерения за изменение продукта.
Ключевые выводы
Трассировка через eBPF снимает налог на ручное инструментирование, но требует ядра не ниже 5.8, несрезанной таблицы символов и знания устройства планировщика Go.
При 50 000 запросах в секунду и четырёх точках съёма на запрос получается 200 000 входов в ядро ежесекундно и потолок расхода в 200–600 мс процессорного времени на ядро.
Медленный запрос это не одна проблема, а пять разных: лишняя работа, борьба за ресурсы, давление среды, деградация плана и патологические шаблоны вроде N+1.
Инструменты базы говорят, что дорого внутри неё, но не говорят, какой сервис это вызвал и был ли запрос пользовательским. Этот контекст приносят трассы.
Версия браузера, тип навигации, библиотека сбора данных и версия инструментирования входят в определение метрики: смешав их в одном графике, вы увидите изменение измерения вместо изменения продукта.
Слой первый наблюдаемости: собрать данные, не трогая код
Ручное инструментирование обходится дороже, чем кажется на старте. Каждая точка вызова библиотеки, каждая ветка передачи контекста и каждое извлечение сопутствующих данных — это код, который расходится с реальностью, забывается в горячем пути и стоит процессорного времени на высокой частоте запросов.
Альтернатива, ставшая эксплуатационно пригодной за последние пару лет, — подключение зондов eBPF напрямую к символам рантайма и точкам входа библиотек HTTP и gRPC. Трассы восстанавливаются из событий ядра и пользовательского пространства, а файл бинарника на диске не меняется вовсе: правки идут только в памяти работающего процесса, точечно и обратимо.
Почему с Go это сложнее, чем с C
Зонд работает так: в заданное смещение работающего бинарника ставится инструкция прерывания, при попадании в неё ядро останавливает поток, выполняет свою программу и возобновляет исполнение. Для C и Rust это ложится на прологи функций почти без остатка. Go добавляет три осложнения.
Планирование горутин. Планировщик Go раскладывает множество горутин на меньшее число системных потоков. Один HTTP-запрос может обрабатываться горутиной на одном потоке в момент срабатывания зонда и переехать на другой поток до записи ответа. Наивный зонд, читающий идентификатор потока или процесса, теряет непрерывность на этом переезде. Правильный якорь — идентификатор горутины, а достать его можно либо через карту, построенную по отладочной информации, либо вычислением фиксированного смещения от указателя, лежащего в регистре локального хранилища потока.
Соглашение о вызовах. В Go 1.17 аргументы функций переехали со стека в регистры, причём сначала только на amd64; на arm64 и ppc64 это произошло версией позже. Программа зонда, написанная под прежнее соглашение и читающая контекст со смещения от указателя стека, на новых бинарниках прочитает мусор. Значит, трассировщику нужна либо логика зондов под каждую минорную версию языка, либо вычисление места по отладочной информации самого бинарника.
Встраивание функций. Компилятор Go агрессивно встраивает мелкие функции, и нужный символ может просто отсутствовать по ожидаемому адресу. Зонд, поставленный не туда, даёт пропущенные отрезки трассы или испорченные аргументы, причём молча.
Отдельная ловушка при разборе событий:
Структура на стороне потребителя должна побайтово совпадать со структурой в программе зонда, включая выравнивание. Расхождение в один байт приводит к тому, что все поля после первого декодируются неправильно, а внешне это выглядит как случайный мусор в трассах.
Главная нерешённая проблема: передача контекста между сервисами
Если нельзя вписать заголовок в коде приложения, как передать идентификатор трассы дальше по цепочке вызовов? Есть три подхода, и практически применим один.
- Писать заголовок прямо в пакет на уровне сетевого интерфейса. Требует расширенных привилегий и работает только для нешифрованного HTTP/1.1: шифрование выполняется выше уровня сокета, и программа видит уже зашифрованные байты.
- Пропускать исходящие вызовы через локальный вспомогательный прокси, который держит соответствие идентификатора горутины и контекста трассы и вставляет заголовки перед пересылкой. Добавляется сетевой переход, зато шифрование не ломается.
- Править карту заголовков прямо в памяти процесса. Соответствующая функция ядра помечена как опасная и действительно способна повредить память: каждая её загрузка пишет предупреждение в журнал ядра, работа требует широкой привилегии CAP_SYS_ADMIN, а в режиме блокировки ядра она запрещена совсем.
Единственный вариант, который одновременно совместим с шифрованием, безопасен и разворачивается где угодно, — второй. Его цена в задержке составляет обычно меньше 100 мкс на переход через локальную петлю, что приемлемо, когда сами измеряемые операции занимают миллисекунды.
Что ломается в продакшене
- Идентификаторы горутин переиспользуются. При высокой конкурентности номер может уйти на новую горутину раньше, чем потребитель разобрал события старой.
- Обновление бинарника сдвигает смещения символов. Зонды старого отображения ядро снимает само, и новый бинарник какое-то время работает без трассировки. Слежение за изменением файла сокращает разрыв до секунд, но не убирает его.
- Требования к ядру. Кольцевые буферы нужны ядру от 5.8, а переносимая компиляция зондов требует 5.4 с включённой отладочной информацией о типах. Матрицу ядер стоит проверить до внедрения, а не после.
- Срезанные бинарники. Сборка без таблицы символов и отладочной информации лишает трассировщик возможности сопоставить имена функций со смещениями. Компромисс: оставить хотя бы таблицу символов, заплатив примерно 10–15% размера бинарника.
Сколько это стоит в процессорном времени
Зонды не бесплатны: каждое срабатывание это программное прерывание с входом в ядро. При 50 000 запросах в секунду и четырёх точках съёма на запрос получается 200 000 входов ежесекундно. Опубликованные замеры дают порядка 1–3 мкс на срабатывание на современном оборудовании, то есть верхняя оценка расхода составляет 200–600 мс процессорного времени в секунду на одно ядро.
Цифра ощутимая, но сравнивать её стоит с альтернативой, а не с нулём. Автор разбора оценивает эту альтернативу в десятую часть рабочего времени разработчиков, уходящую на сопровождение кода инструментирования. Потребителя событий при этом лучше держать на отдельной горутине, привязанной к ядру, которое не обслуживает запросы, чтобы выделение памяти под отрезки трасс не мешало обработке.
Правильный вывод не «заменить инструментирование зондами», а «использовать оба». Зонды дают сплошное покрытие и базовое распределение задержек без усилий разработчиков, включая сторонние бинарники, которые вы не можете изменить. Ручное инструментирование даёт смысловое наполнение: идентификатор пользователя, арендатора, флаг функциональности — то, чего из сырых байтов HTTP не синтезировать. Наиболее точные схемы наблюдаемости держат оба слоя, причём слой зондов работает проверкой на непротиворечивость для отрезков, которые приложение теряет под нагрузкой.
Слой второй: превратить трассы в решения
Собранная телеметрия сама по себе ничего не улучшает. Больше телеметрии означает больше объектов для разглядывания, а не больше понимания. Полезной она становится, когда из неё извлечены закономерности, на которые можно действовать.
«Медленный запрос» это пять разных диагнозов
Прежде чем что-то чинить, стоит понять, чем именно болен запрос. Разбор CNCF раскладывает медленные запросы к базе на пять непохожих причин:
- Лишняя работа. Обычно это полный перебор таблицы из-за отсутствующего или неприменимого индекса. Выборка по идентификатору клиента без индекса растёт с 20 мс на десяти тысячах строк до минут на десяти миллионах. Запрос не менялся, изменился объём данных.
- Борьба за ресурсы. Идеально оптимизированный запрос стоит в ожидании блокировок или свободного соединения. Запрос, проводящий 95% времени в ожидании блокировки, оптимизацией текста не лечится: ему нужна перекройка транзакций.
- Давление среды. Насыщение процессора, узкое место ввода-вывода и нехватка памяти замедляют любой запрос с любым планом: тот же текст запроса и тот же план на нагруженной машине отработает в разы дольше, чем на свободной, и оптимизировать тут нечего.
- Деградация плана. Данные и текст запроса те же, а план исполнения изменился. Устаревшая статистика после массовой загрузки заставляет планировщик выбрать заведомо плохую стратегию.
- Патологические шаблоны. Проблема N+1 выполняет сотню быстрых запросов по две миллисекунды последовательно, добавляя двести миллисекунд задержки на одно только выполнение, не считая сетевых издержек на каждый обмен с базой. Ни один запрос не является медленным, а шаблон катастрофичен и в журнал медленных запросов не попадает вовсе.
Чего не хватает встроенным инструментам базы
Журналы медленных запросов, хранилища запросов и разбор планов прекрасно отвечают на вопрос, что дорого внутри базы. Они не отвечают на вопросы, которые задаёт дежурный инженер: какой сервис это вызвал, пользовательская это работа или фоновая, связано ли это со всплеском задержки, который сейчас расследуется.
Этот контекст приносит распределённая трассировка: каждый отрезок обращения к базе вложен в контекст запроса и знает сервис, конечную точку и инициатора. Вместо того чтобы вручную сшивать журналы базы с трассами постфактум, медленные запросы анализируются прямо из трасс со всем прикладным контекстом внутри.
Дальше из отрезков трасс выводятся метрики, и делается это в три приёма: сперва простое обнаружение медленных запросов, затем взвешивание по трафику, чтобы понять, что даст наибольший выигрыш при ускорении, и наконец поиск аномалий для дежурства. Первые два отвечают на вопрос оптимизации, третий — на вопрос реагирования на инцидент, и путать их не стоит: это разные задачи с разными приоритетами.
Слой третий: не принять смену измерения за изменение продукта
Третья ошибка самая обидная, потому что данные при этом верны. Метрика может быть абсолютно точной, а рассказанная по ней история — совершенно неверной.
Дальше примеры пойдут из веб-производительности, потому что там эта ошибка задокументирована лучше всего. Механика же общая: любой дашборд, где смешаны серии с разных популяций или с разных версий сбора, ведёт себя одинаково, будь то задержки бэкенда или отрисовка страницы.
Общий тренд не объясняет вашу просадку
В июньском наборе данных о реальных пользователях Chrome, опубликованном в середине июля, доля источников с хорошей отрисовкой основного содержимого упала до 67,7%, с хорошей задержкой отклика на взаимодействие до 85,9%, а доля проходящих все три ключевых показателя до 55,3%. Исключением оказалась стабильность вёрстки, поднявшаяся до 81,4%.
Google объяснил движение сезонностью, сравнив его с прошлым годом. Контекст полезный, но он не является доказательством того, что у вас локально ничего не сломалось. Релиз, рекламная кампания, изменение формы согласия, сдвиг в составе устройств или новый сторонний скрипт способны совпасть с общеотраслевым движением.
Гарри Робертс формулирует критерий проверки прямо: если общая доля прошедших упала на 1,2 процентного пункта, а конкретный сайт просел на четыре, широкий тренд разницу не объяснил. Верно и обратное: если сайт почти идеально повторяет рынок по браузерам и устройствам, срочное расследование в коде потратит время всей команды впустую.
Правильное сравнение поэтому не «этот месяц против прошлого». Сравнивайте движение сайта с общей дельтой, разделяйте мобильные и настольные устройства, смотрите на показатели отрисовки и отклика по отдельности и разбирайте важные шаблоны страниц, а не источник целиком.
Изменение методики выглядит как изменение продукта
Дальше будет сложнее. По мере того как браузеры начинают показывать переходы внутри одностраничных приложений, которые метрики первоначальной загрузки пропускали, на одном дашборде окажутся жёсткие навигации, мягкие навигации браузера и собственные виртуальные страницы приложения, причём каждая серия собрана с разной популяции пользователей.
Смешав их без оговорок, вы получите скачок графика, вызванный сменой методики, и потратите неделю на поиск несуществующей регрессии. Отсюда практическое требование: версия браузера, тип навигации, библиотека сбора данных и версия инструментирования перестают быть служебными деталями и становятся частью определения метрики. Дашборд должен нести отметки релизов, состав трафика и устройств и внятное указание, какую популяцию представляет каждый график.
Компенсация это не исправление
Ещё одна ловушка того же рода: браузеры учатся скрывать последствия проблем. Привязка прокрутки к содержимому подправляет позицию, когда над областью просмотра что-то вставилось или исчезло, и читателя перестаёт выбрасывать с того места, которое он разглядывал.
Улучшение реальное, но документ по-прежнему перевёрстывается, если у картинки не заданы размеры или объявление меняет высоту. Браузер убирает одно видимое следствие, а причина остаётся на месте. Проверять после такого изменения стоит липкие шапки, ленты, интерфейс согласия и встроенные блоки, а чинить всё равно вёрстку, а не полагаться на маскировку.
Тот же разрыв виден и в метрике отклика. Большинство команд сегодня способны сказать, плохой у них отклик или нет. Заметно меньше способны сказать, какое именно взаимодействие в этом виновато, на каком шаге пользовательского пути оно случилось и что его задержало. Весь разбор Гарри Робертса именно про этот разрыв между наличием числа и наличием понимания.
Часто задаваемые вопросы
Что такое трассировка на eBPF и чем она отличается от обычной?
Это снятие событий зондами, подключёнными к символам работающего бинарника, без изменения кода приложения. Обычная трассировка требует вызовов библиотеки в коде, зато умеет добавлять смысловые атрибуты вроде идентификатора пользователя, которых из сырого трафика не получить.
Какие требования у eBPF-трассировки Go-сервисов?
Ядро не ниже 5.8 для кольцевых буферов и не ниже 5.4 с отладочной информацией о типах для переносимых зондов. Бинарники нельзя собирать с полным срезанием символов: таблицу символов нужно оставить, это добавляет примерно 10–15% к размеру.
Сколько процессорного времени съедают зонды?
Порядка 1–3 мкс на срабатывание. При 50 000 запросах в секунду и четырёх точках съёма это 200 000 входов в ядро ежесекундно и верхняя оценка в 200–600 мс процессорного времени в секунду на одно ядро.
Почему журнала медленных запросов недостаточно?
Он показывает, что дорого внутри базы, но не показывает, какой сервис вызвал запрос, был ли он пользовательским и связан ли со всплеском задержки. Кроме того, шаблон N+1 в него вообще не попадает: сто запросов по две миллисекунды не медленные по отдельности, а вместе дают двести миллисекунд.
Что делать, если метрики упали одновременно со всем рынком?
Сравнивать величину движения, а не направление. Если рынок просел на процент с небольшим, а сайт на четыре, общий тренд разницу не объясняет. Дополнительно стоит разделить мобильные и настольные устройства и смотреть показатели по отдельности, а не по источнику целиком.
Что забрать с собой
Три слоя решают три разные задачи, и провал на любом из них выглядит одинаково: дашборд есть, ответа нет. Без покрытия вы не видите части системы. Без перевода трасс в метрики видите всё и не понимаете ничего. Без дисциплины в чтении принимаете смену методики за регрессию.
Начинать проще всего с третьего слоя: он не требует ни новой инфраструктуры, ни единой строчки кода, только дисциплины чтения. Сверьте движение своих метрик с движением рынка и убедитесь, что расследуете настоящую регрессию, а не смену методики. Дальше уже видно, чего не хватает: покрытия или перевода трасс в решения.
Материалы разбора: устройство трассировки Go-сервисов через eBPF, перевод медленных запросов в метрики надёжности и разбор того, как метрики вводят в заблуждение.












