Эксплуатация производственных сервисов требует понимать, что происходит внутри них, но добавление метрик не является бесплатным. В презентации, опубликованной InfoQ, Brian Martin, соучредитель IOP Systems, объясняет, что разница между реализациями может превратить обновление счётчика из операции стоимостью около 5 наносекунд в операцию продолжительностью более одной микросекунды. Для гистограмм разрыв может увеличиваться примерно с 7 наносекунд до десятков микросекунд при конкуренции между потоками.
Основная идея презентации заключается не в выборе конкретной библиотеки Rust, а в рассмотрении измерений как части проектирования производительности. Метрика, размещённая внутри пути, вызываемого миллионы раз, многократно увеличивает любую небольшую стоимость, тогда как отсутствие измерений усложняет диагностику замедлений и производственных инцидентов, а также оптимизацию производительности.
Начните с понимания типа данных и стоимости их обновления
Martin выделяет три основных типа метрик: счётчик, который обычно не уменьшается, например количество запросов; моментальный показатель, представляющий текущее значение, например глубина очереди; и гистограмма, описывающая распределение значений, например время отклика. Это различие важно, поскольку каждый тип требует разных операций, а гистограммы предоставляют информацию, которую не может дать один общий счётчик.
В простейших случаях для целочисленных счётчиков подходит atomic fetch_add. Циклы сравнения и замены, или CAS, обычно требуют повторной попытки, когда несколько потоков конкурируют за одну и ту же ячейку. Значительная часть стоимости связана с синхронизацией строк кэш-памяти между ядрами. В презентации приводятся измерения на машине AWS Graviton с 32 виртуальными процессорами: теоретический предел обработки составил около 119 миллионов запросов в секунду при низкозатратном атомарном обновлении против примерно 23 миллионов при использовании более дорогой реализации с Prometheus.
Снизьте конкуренцию с помощью разбиения по процессорам
Когда все потоки используют один общий счётчик, строка кэш-памяти постоянно перемещается между ядрами. Вместо этого Martin предлагает создать отдельный счётчик для каждого процессора, чтобы операции записи почти не конкурировали, а затем суммировать значения при чтении. Такой подход немного увеличивает стоимость чтения, но защищает горячий путь записи — ту часть, которая выполняется при каждом запросе.
Здесь следует учитывать явление false sharing. Логически независимых счётчиков недостаточно, если они размещены в одной строке кэш-памяти: размер строки составляет 64 байта, поэтому в ней могут соседствовать восемь счётчиков типа 64-bit. Поэтому в презентации рекомендуется группировать счётчики и добавлять заполнение, чтобы они занимали отдельные строки памяти. Согласно приведённым данным, теоретическая производительность может вырасти примерно со 119 миллионов запросов в секунду для атомарного счётчика до примерно 6,4 миллиарда запросов в секунду при правильном использовании разбиения.
Проектируйте гистограммы с учётом пути обновления
Стоимость гистограммы начинается с определения контейнера, которому принадлежит значение. Линейный поиск в списке контейнеров является самым простым и самым медленным вариантом, тогда как бинарный поиск уменьшает количество сравнений, но по-прежнему зависит от числа контейнеров. Более быстрый вариант — прямая индексация, при которой номер контейнера вычисляется по значению, а не ищется.
У такой индексации есть компромиссы. Разбиение на линейные диапазоны выполняется быстро, но при малых значениях может давать относительно большую ошибку. Логарифмическая индексация лучше сохраняет относительную ошибку, однако само вычисление логарифма является затратным. Martin демонстрирует использование внешних диапазонов на основе Log2 с вложенными контейнерами для настройки точности, как в HDR Histogram и H2Histogram. В тесте без атомарных операций определение контейнера и обновление заняли около 2,65 наносекунды в HDR Histogram и около 2,15 наносекунды в H2Histogram.
Когда приблизительная согласованность приемлема?
Стоимость гистограммы определяется не только способом индексации. Некоторые приложения выполняют несколько атомарных операций при каждой записи, используют CAS для суммирования значений или устанавливают блокировку, чтобы получить согласованный снимок. В презентации отмечается, что некоторые реализации достигали показателей выше двух микросекунд и даже десятков микросекунд на 32 ядрах, тогда как реализация на основе прямой индексации и одной атомарной операции обновления была ближе по стоимости к счётчику.
Альтернативой является приблизительная согласованность: некоторые контейнеры могут измениться во время чтения гистограммы, но разница между двумя последовательными чтениями остаётся полезной, когда сами метрики изначально являются приблизительными. Это не универсальное правило: системы, которым требуется полностью согласованный снимок, могут предпочесть синхронизацию, несмотря на её стоимость.
Редакционный взгляд certi.news
На практике меняется то, что решение о добавлении метрик должно учитывать структуру обновления, а не только названия метрик. Атомарный счётчик, разбиение по процессорам и прямая индексация могут сделать измерения пригодными для использования внутри чувствительных к задержкам путей, тогда как синхронизированные или динамически масштабируемые гистограммы могут создавать значительные затраты под нагрузкой. Презентация не предлагает единого рецепта, подходящего для каждой библиотеки или сервиса; она показывает, что гибкость, возможность использования библиотеки в других проектах, согласованность и производительность являются частично конфликтующими целями. Поэтому следует тестировать фактическую реализацию при целевых уровнях конкуренции и объёме обработки, а не полагаться на название библиотеки или результат в условиях отсутствия конкуренции.
Martin также демонстрирует использование eBPF через проект Rezolus для получения точных метрик из ядра Linux, включая планировщик, пути системных вызовов и стек TCP, без изменения кода ядра. Открытым остаётся вопрос о том, какой уровень точности и согласованности требуется в каждом конкретном случае и останется ли стоимость чтения или объединения частей приемлемой при масштабировании системы.