TL;DR
HMD автоматизирует релизы, управляемые метриками здоровья: решения принимаются по SLI/SLO (например, доля 500-ок за 10 минут) с ранним автооткатом при деградации.
Под глобальной нагрузкой используется распределённое исполнение: агрегации считаются внутри дата-центров и сводятся наверху; типовые выборки для областей релиза предвычисляются recording rules на уровне ДЦ.
Эффект: ответы на зондовые запросы при распределённом исполнении в среднем быстрее в 3–5 раз; длительность пакетных прогонов HMD снижена примерно с ~30 часов до ~2 часов (≈15×); тяжёлые запросы перестали «ронять» центральный Querier.
Пиковая нагрузка от батчей сглаживается прокси с адаптивными лимитами параллелизма (AIMD) и джиттером; интерактивные запросы инженеров в приоритете.
Хранение и доступ: единый слой запросов на Thanos + R2 для долгоживущей историки; эксперимент с хранением временных рядов в Parquet (parquet-tsdb-poc) поверх объектного хранилища.
Вы наверняка видели эту страницу с ошибкой множество раз. Порой Cloudflare возвращает пятисотую ошибку, когда серверы не могут ответить на запрос. Причин у такой неготовности отвечать может быть несколько, включая проблемы из-за бага в одном из сервисов, входящих в программный стек Cloudflare.

Мы понимаем, что наша платформа тестирования неизбежно пропустит часть багов, поэтому сделали страховочные механизмы, которые позволяют постепенно и безопасно выкатывать новый код до того, как функция дойдёт до всех пользователей. Health Mediated Deployments (HMD) — основанное на данных решение Cloudflare для автоматизации обновлений ПО по всей нашей глобальной сети. HMD работает за счёт запросов к Thanos — системе хранения и масштабирования метрик Prometheus. Prometheus собирает детализированные данные о производительности наших сервисов, а Thanos делает эти данные доступными по всей нашей распределённой сети. HMD использует эти метрики, чтобы решать, следует ли продолжать раскатку нового кода, поставить её на паузу для дополнительной оценки или автоматически откатить изменения, чтобы предотвратить масштабные проблемы.
Инженеры Cloudflare настраивают «сигналы» от своих сервисов, такие как правила алертов или целевые показатели уровня обслуживания (SLO). Например, индикатор уровня обслуживания (SLI) проверяет долю ошибок HTTP 500 за 10 минут, которые возвращает один из сервисов в нашем стеке.
sum(rate(http_request_count{code="500"}[10m])) / sum(rate(http_request_count[10m]))
SLO — это сочетание SLI и целевого порогового значения. Например: сервис возвращает ошибки 500 менее чем в 0,1% запросов.
Если доля успешных запросов в зоне выката нового кода неожиданно снижается, HMD откатывает изменение, чтобы стабилизировать систему — раньше, чем люди успеют понять, какой сервис Cloudflare сломался. Ниже HMD обнаруживает деградацию сигнала на раннем этапе релиза и откатывает код к предыдущей версии, чтобы ограничить «радиус поражения».

Сеть Cloudflare обрабатывает миллионы запросов в секунду по всему миру. Откуда мы знаем, что HMD среагирует достаточно быстро в следующий раз, если мы случайно выпустим код с багом? HMD выполняет стратегию тестирования вне процесса релиза, называемую ретроспективным тестированием (backtesting), которая использует исторические данные об инцидентах, чтобы проверить, сколько времени потребуется для реакции на сигналы деградации в будущем релизе.
Мы используем Thanos, чтобы объединить тысячи небольших развёртываний Prometheus в единый слой запросов, сохраняя мониторинг надёжным и экономически эффективным. Для дозаполнения исторических метрик инцидентов, которые вышли за пределы периода хранения Prometheus, мы используем наше объектное хранилище R2.
Сегодня мы храним 4,5 млрд уникальных временных рядов с годовым сроком хранения, что даёт примерно 8 петабайт данных в 17 млн объектов, распределённых по всему миру.

Как это работает в масштабе
Чтобы почувствовать масштаб, оценим, во что выливается один пакет ретроспективных тестов:
Каждый прогон такого теста состоит из нескольких SLO для оценки «здоровья» сервиса.
Каждый SLO оценивается несколькими запросами, которые агрегируют данные по группам дата-центров.
Каждый дата-центр отправляет от десятков до тысяч запросов к R2.
В сумме один пакет может выливаться в сотни тысяч PromQL-запросов и миллионы обращений к R2. Изначально выполнение таких пакетов занимало около 30 часов, но большим трудом мы сократили это время до 2 часов.
Разберёмся, как мы сделали этот процесс эффективнее.
Правила записи
HMD разбивает наш парк машин по нескольким измерениям. В рамках этой статьи назовём их «tier» и «color». Имея пару значений tier и color, мы используем следующий PromQL-запрос, чтобы найти машины, соответствующие этой комбинации:
group by (instance, datacenter, tier, color) (
up{job="node_exporter"}
* on (datacenter) group_left(tier) datacenter_metadata{tier="tier3"}
* on (instance) group_left(color) server_metadata{color="green"}
unless on (instance) (machine_in_maintenance == 1)
unless on (datacenter) (datacenter_disabled == 1)
)
Большинство этих временных рядов имеют кардинальность, примерно равную числу машин. Это означает большой объём данных, который нужно извлечь из объектного хранилища и передать в центральную точку для вычисления запроса, а затем — декодировать и соединить множество рядов между собой.
Поскольку это довольно типовой запрос, который выполняется при каждом запуске HMD, разумно предвычислять его. В экосистеме Prometheus это обычно делается с помощью правил записи (recording rules):
hmd:release_scopes:info{tier="tier3", color="green"}
Помимо того, что такой вариант выглядит заметно аккуратнее, он существенно снижает нагрузку во время выполнения запроса. Поскольку все задействованные объединения (join) возможны только внутри одного дата-центра, корректно вычислять эти правила прямо в экземплярах Prometheus внутри самого дата-центра.
По сравнению с исходным запросом, кардинальность, с которой нам теперь приходится иметь дело, масштабируется с размером области релиза, а не с размером всего парка машин.
Это значительно дешевле и менее чувствительно к сетевым проблемам по пути, что, в свою очередь, в среднем уменьшает количество повторных попыток выполнения запроса.
Распределённая обработка запросов

HMD и Thanos Querier, показанные выше, — это компоненты без состояния, которые можно запускать где угодно; отказоустойчивые развёртывания есть в Северной Америке и Европе. Кратко напомним, что происходит, когда мы вычисляем SLI-выражение, упомянутое во введении статьи:
sum(rate(http_request_count{code="500"}[10m]))
/
sum(rate(http_request_count[10m]))
Получив этот запрос от HMD, Thanos Querier начинает запрашивать сырые временные ряды метрики "http_requests_total" у подключённых по всему миру экземпляров Thanos Sidecar и Thanos Store, ждёт, пока все данные будут переданы, распаковывает их и, наконец, вычисляет результат:

Такой подход работает, но он не оптимален по нескольким причинам. Нам приходится ждать, пока сырые данные из тысяч источников по всему миру соберутся в одной точке, прежде чем мы вообще сможем начать распаковку, а затем всё обрабатывается одним экземпляром. Если удвоить число дата-центров, нам придётся удвоить и объём памяти, выделяемой под выполнение запроса.
Многие SLI — это простые агрегаты, призванные свести аспект «здоровья» сервиса к одному числу, например к проценту ошибок. Как и в случае с упомянутым правилом записи, такие агрегаты часто допускают распределённое вычисление (distributive): их можно посчитать внутри дата-центра, а затем свести промежуточные агрегаты и получить тот же результат.
Для примера, будь у нас правило записи для каждого дата-центра, мы могли бы переписать наш пример так:
sum(datacenter:http_request_count:rate10m{code="500"})
/
sum(datacenter:http_request_count:rate10m)
Это решило бы наши проблемы, потому что вместо запроса сырых временных рядов для метрик с высокой кардинальностью мы бы запрашивали предагрегированные результаты. Как правило, такой предагрегированный результат на порядок меньше по объёму данных, пересылаемых по сети и подлежащих обработке для получения финального ответа.
Однако у правил записи в нашей архитектуре высокая стоимость вычислений на этапе записи: они часто вычисляются на тысячах экземпляров Prometheus в продакшене лишь затем, чтобы ускорить куда более разовый пакетный процесс. Быстро масштабировать набор правил записи вслед за растущим числом SLI «здоровья» сервисов было бы нежизнеспособно. Пришлось вернуться к чертёжной доске.
Идеально было бы уметь удалённо вычислять запросы, ограниченные границами дата-центра, и затем сводить их результаты обратно — для произвольных запросов и прямо во время выполнения. Для иллюстрации мы хотели бы посчитать наш пример так:
(sum(rate(http_requests_total{status="500", datacenter="dc1"}[10m])) + ...)
/
(sum(rate(http_requests_total{datacenter="dc1"}[10m])) + ...)
Именно это и умеет распределённый движок запросов Thanos. Вместо сырых временных рядов мы запрашиваем агрегаты на уровне дата-центров и и возвращаем только их — на стороне Thanos Querier они сводятся в полный результат запроса:

Мы обеспечиваем, что все «дорогие» маршруты данных будут максимально короткими, используя подсказки размещения (location hints) в R2 для указания основного региона доступа.


Чтобы измерить эффективность этого подхода, мы использовали Cloudprober и написали зонды, которые оценивают относительно «дешёвый», но всё же глобальный запрос count(node_uname_info).
sum(thanos_cloudprober_latency:rate6h{component="thanos-central"})
/
sum(thanos_cloudprober_latency:rate6h{component="thanos-distributed"})
На графике ниже по оси Y показано ускорение варианта с распределённым исполнением по сравнению с централизованным. В среднем распределённое исполнение отвечает на зонды в 3–5 раз быстрее.

На практике даже чуть более сложные запросы быстро приводят к таймаутам или падению нашего централизованного развёртывания, тогда как распределённое спокойно их обрабатывает. Для немного более «тяжёлого» запроса вида count(up) примерно для 17 миллионов заданий опроса (scrape jobs) нам с трудом удавалось получить ответ от централизованного Thanos Querier, пришлось ограничиться одним регионом — это заняло около 42 секунд:

Тем временем наши распределённые экземпляры Thanos Querier вернули полный результат примерно за 8 секунд:

Управление перегрузкой
Пакетная обработка HMD порождает «рваные» профили нагрузки, под которые сложно резервировать ресурсы. В идеале она должна генерировать ровный и предсказуемый поток запросов. При этом пакетные запросы HMD для нас менее приоритетны, чем запросы, которые дежурные инженеры запускают для оперативного разбора инцидентов в продакшене. Мы решаем обе задачи с помощью адаптивного механизма управления параллелизмом на основе приоритетов. Изучив работу Netflix по адаптивным лимитам параллелизма, мы реализовали похожий прокси, который динамически ограничивает поток пакетных запросов, когда метрики SLO для Thanos начинают ухудшаться. Например, одно из таких SLO — доля отказов Cloudprober за последнюю минуту:
sum(thanos_cloudprober_fail:rate1m)
/
(sum(thanos_cloudprober_success:rate1m) + sum(thanos_cloudprober_fail:rate1m))
Мы применяем джиттер, то есть случайную задержку, чтобы сгладить всплески запросов внутри прокси. Поскольку в пакетной обработке важнее общая пропускная способность по запросам, чем задержка отдельного запроса, джиттер помогает HMD отправить «залп» запросов, позволяя Thanos обрабатывать их постепенно в течение нескольких минут. Это снижает мгновенную нагрузку на Thanos и повышает общую пропускную способность, даже если задержка отдельных запросов растёт. Параллельно HMD получает меньше ошибок, уменьшается число повторных попыток и растёт эффективность батча.
Наше решение имитирует работу алгоритма управления перегрузкой TCP — аддитивное увеличение/мультипликативное уменьшение (AIMD). Когда прокси-сервер получает успешный ответ от Thanos, в следующий раз он пропустит ещё один параллельный запрос. Если сигналы обратного давления превышают заданные пороги, прокси ограничивает окно перегрузки пропорционально уровню отказов.

По мере роста доли отказов сверх порога «warn», приближаясь к порогу «emergency», прокси экспоненциально приближается к состоянию, при котором не пропускает ни одного дополнительного запроса через систему. Однако, чтобы ошибочные сигналы не останавливали весь трафик, мы ограничиваем снижение, задавая минимально допустимую частоту запросов.
Эксперименты с колоночным хранением
Поскольку Thanos работает с блоками Prometheus TSDB, изначально не рассчитанными на чтение по медленному каналу вроде объектного хранилища, ему приходится выполнять много случайных операций ввода-вывода. Вдохновившись этим докладом, мы начали хранить наши временные ряды в файлах Parquet, и предварительные результаты выглядят обнадёживающе. Проект пока на ранней стадии, чтобы делать уверенные выводы, но мы хотели поделиться реализацией с сообществом Prometheus, поэтому публикуем наш экспериментальный шлюз к объектному хранилищу как parquet-tsdb-poc на GitHub. (Заглядывайте, если вам интересны хранилища временных рядов на основе Parquet и их потенциал для масштабной наблюдаемости)
Заключение
Мы создали Health Mediated Deployments (HMD), чтобы обеспечить безопасные и надёжные релизы ПО, одновременно расширяя границы нашей инфраструктуры наблюдаемости. По пути мы существенно улучшили способность Thanos обрабатывать высоконагруженные запросы, сократив длительность пакетных прогонов в 15 раз.
Но это только начало. Мы продолжаем работать вместе с командами наблюдаемости, отказоустойчивости и R2, чтобы безопасно и в масштабе задействовать инфраструктуру на пределе её возможностей. Один из перспективных векторов — оптимизация хранения временных рядов под объектные хранилища.
Если цель — профессиональный рост в перфоманс-инженерии и перенятие рабочих практик у людей, которые ежедневно держат прод под нагрузкой, — обратите внимание курс «Нагрузочное тестирование». Он аккумулирует опыт экспертов: от постановки гипотез и сценариев до автоматизации прогонов и чтения метрик так, чтобы выводы превращались в архитектурные решения.
Чтобы узнать, подойдет ли вам программа обучения, пройдите вступительный тест. Также в рамках набора на курс преподаватели проведут бесплатные демо-уроки, приходите:
5 ноября: «Открытая vs закрытая модели нагрузки: практика в Gatling, k6 и Locust». Регистрация
18 ноября: «Прохождение собеседования на нагрузочного тестировщика. Что интересует работодателя?». Регистрация
php7
У вас карта странная