Делаем SLI доступности сервиса ближе к опыту пользователя
Если вы находитесь на этапе внедрения SLI (Service Level Indicator), с большой вероятностью, первое что вы захотите измерять — это индикатор доступности вашего сервиса (availability).
Для его измерения в сервисах формата запрос-ответ часто используют метрику доли успешных запросов, основанную на соотношении HTTP- или RPC-статусов.
В этой статье попробуем разобраться, что скрывается за этой метрикой и как выжать из неё максимум пользы, чтобы достичь высокой корреляции с пользовательским опытом.
Мы пройдем путь от проектирования базовой метрики доли успешных запросов до более продвинутого показателя времени доступности, когда пользователь может использовать наш сервис.
Измерение, основанное на событиях
Для начала вспомним стандартную формулу SLI:
Во всех примерах PromQL мы будем рассматривать сервис, представляющий собой некий API-бекенд.
На всех демонстрируемых скриншотах источником метрики является либо балансировщик нагрузки, либо API Gateway, в зависимости от сервиса.
Определяем метрику
В общем случае, хорошими событиями можно считать HTTP-запросы, которые завершились со статусами 2хх
, а все события представить в виде суммы запросов со статусами 2xx
и 5xx
.
Здесь я намеренно отсекаю из выборки редиректы (3xx
) и пользовательские ошибки (4хх
), чтобы не искажать индикатор. Ориентируйтесь на то, что лучше отражает использование вашего сервиса, так например, где-то редиректы могут являться важной частью пользовательского пути, а статусы 429
и 499
явно говорить о негативном для пользователя опыте.
Уточняем выборку
Чтобы не смотреть на так называемую «среднюю температуру по больнице», мы можем обогатить выборки фильтрами, которые относятся к критическому пользовательскому пути или основным входным точкам в сервис, например, используя фильтры по URI, методу, контроллеру или другим доступным меткам.
На примере интернет-магазина, нам скорее всего были бы наиболее интересны запросы в каталог, корзину и непосредственно этап оформления заказа.
Если в качестве системы мониторинга вы используете Prometheus и Grafana, то базовый PromQL запрос для индикатора SLI интернет-магазина мог бы выглядеть примерно так:
# good events sum( increase( http_requests_count_total{ status=~"2..", service="market", endpoint=~"^/api/(catalog|cart|order)", }[$__interval] ) ) / # total events sum( increase( http_requests_count_total{ status=~"(2..|5..)", service="market", endpoint=~"^/api/(catalog|cart|order)" }[$__interval] ) )
С помощью этого запроса мы увидим подобное представление на линейной диаграмме:
Действительно, график показывает нашу доступность (и в том числе недоступность), основанную на событиях. Отлично!
Первые шаги сделаны, но в таком виде мы наблюдаем лишь индикатор в моменте, измеряя SLI за динамические интервалы, которые обычно равняются диапазону времени разделенному на максимальное количество точек, помещающихся на диаграмму.
Например, на скриншоте динамический интервал равен 30 секундам.
Используем скользящее окно измерения
Моментальная метрика полезна при реагировании на проблему, но если говорить о планировании и приоритизации на основе сбоев, то решения обычно принимаются на отрезках от недель до кварталов.
На таких диапазонах времени моментальная метрика сглаживается и все выглядит так, словно все было хорошо. Но мы точно знаем, что это не так, поскольку был серьезный сбой.
И здесь нам поможет скользящее окно.
Скользящее окно — это диапазон времени, постоянно сдвигающийся на определенный шаг по мере наступления нового временного периода.
Проще говоря, мы все время смотрим назад на какой-то отрезок времени от текущей точки реального времени. Все, что старше окна измерения — игнорируется в подсчетах для точки, поскольку находится вне области видимости.
Классически, окно сдвигается непрерывно с течением реального времени и как правило сдвиг равен разрешению индикатора, т.е. частоте сбора метрик.
Чтобы улучшить индикатор, мы начнем измерять его в скользящем однодневном окне.
Наш запрос в Prometheus изменится лишь в интервале:
sum( increase( http_requests_count_total{ ... }[1d] # updated interval ) ) / sum( increase( http_requests_count_total{ ... }[1d] # updated interval ) )
Теперь мы видим нашу доступность в скользящем окне за сутки.
Однако, на скриншоте уже заметно, что наш индикатор доступности за 1 день, а вместе с ним и бюджет на ошибки (Error Budget) довольно быстро восстанавливается.
Если в районе 10 утра наш SLI был равен 97.83%, то уже к 22 часам он восстановился до 97.92%.
Мы можем взять большее окно измерения, например, скользящие 14 дней, тогда свежий эффект от сбоя для индикатора будет восстанавливаться немного медленнее, поскольку короткие окна больше подвержены колебаниям.
Мы научились измерять базовый индикатор доступности на основе событий за скользящий период и можем использовать его для принятии решений при планировании.
Индикатор не идеален, поэтому давайте сделаем выводы и продолжим его улучшать.
Выводы по индикаторам, основанным на событиях
На скриншотах выше продемонстрирован реальный пример полной недоступности одного из сервисов компании, в которой я работаю.
Сбой продолжался ~24 минуты, с 9:52 до 10:16, как видно на самом первом изображении моментальной метрики доли успешных запросов.
На протяжении этих 24 минут клиенты не могли пользоваться функциональностью, которую предоставляет этот сервис. И хотя в компании мы применяем микросервисную архитектуру как для бекенда, так и для фронтенда, что позволяет нам получать такие выгоды как graceful degradation, в любом случае, недоступность в важных частях продукта — это негативный пользовательский опыт, который мы стремимся правильно измерять и минимизировать, с целью сохранения удовлетворительного уровня качества обслуживания.
Поскольку сбой выражался в виде 100% неудачных запросов, индикатор за скользящие 14 дней получился достаточно точным — 99.8% и на него действительно можно ориентироваться при принятии решений.
Но стоит иметь в виду, что использование подхода основанного на событиях не всегда отражает пользовательский опыт, поскольку:
- Индикатор на основе событий подвержен искажениям. События вроде HTTP-запросов имеют динамическую природу, так например, кратный рост успешных запросов после сбоя может довольно быстро «оздоровить» индикатор, рисуя метрику, которая не будет коррелировать с опытом пользователя. Это особенно заметно на небольших окнах измерения.
- Когда доля ошибок менее 100%, индикатор на основе событий в скользящем 2-х недельном окне может выглядеть удовлетворительно, но пользователи при этом могут быть крайне недовольны недоступностью, как в примере выше, где она продолжалась 24 минуты.
- Индикаторы, которые кажутся хорошими при долгосрочном рассмотрении, могут не отражать краткосрочные проблемы, сильно влияющие на пользователей.
Далее я расскажу о привязке индикатора к дополнительным условиям и временным срезам, а именно — к длительности негативного события. Это позволит обойти некоторые описанные выше нюансы, присущие подходу основанному на событиях.
Индикатор на основе временных срезов
Временные срезы позволяют нам измерять долю времени, когда сервис доступен пользователю, что отличает его от доли успешных событий за период времени, рассмотренный выше.
Этот индикатор также называют Uptime.
Доля времени доступности сервиса часто является основным SLA (Service Level Agreement) в digital-продуктах.
Здесь мы рассмотрим сбой в другом нашем сервисе, который продолжался по меньшей мере ~35 минут.
Его основное отличие в том, что сбой затрагивал не 100% запросов, как в предыдущем примере, а лишь ~30%. Эта разница позволит нам увидеть отличия между подходами к измерению.
Ниже на изображении используется PromQL запрос, похожий на ранее приведенный пример для моментальной метрики, за исключением того, что мы смотрим долю ошибок, а не долю успехов.
Возможно, подходя к индикатору времени доступности сервиса, вас уже посещал философский вопрос о том, что считать «недоступностью» — 100% неудачных запросов или же достаточно всего лишь 1%?
По своему опыту могу сказать, что все зависит от сервиса и продукта. Где-то мы приходили к выводу, что 5% неудачных запросов за 1 минуту свидетельствуют о недоступности, в другом же месте недоступностью могло считаться 10% или 50% неудачных запросов.
Выбирая порог, мы ориентировались в основном на обратную связь от пользователей во время сбоев и внутренние UX-исследования.
Кроме того, когда я задавался этим вопросом и исследовал опыт других компаний, я часто обращался к публичным SLA. Например, Яндекс Трекер определяет свою недоступность так:
«Недоступность Сервиса» фиксируется при превышении коэффициента ошибок 5% на стороне сервера и/или сетевой недоступности адресов <...> из-за проблем на стороне Яндекса.
«Коэффициент ошибок» измеряется по следующей формуле: запросы, которые возвращают HTTP Status 500-599 или gRPC Status 13, разделенных на общее количество корректных запросов за минуту.
«Корректный запрос» — запрос, соответствующий документации Сервиса, который при работоспособности Сервиса возвращает HTTP Status в диапазоне 200-499 или gRPC Status в диапазоне 0-12
Разбираемся в деталях индикатора на основе срезов
В индикаторах на основе временных срезов появляется базовая единица времени, за которую события агрегируются и возвращают бинарный результат, идентифицирующий событие как успешное или неуспешное.
Проще говоря, мы делим наше окно измерение на срезы, каждый из которых однозначно отвечает на закрытый вопрос:
- Да, этот сервис сделал то, что нужно было пользователю.
- Нет, этот сервис не сделал то, что нужно было пользователю.
Срез может быть 30 секунд, 1 минута или же любой другой, который больше подходит по смыслу индикатора. Важно, что он не может быть меньше разрешения (частоты сбора) самой метрики.
Такой подход может быть несколько сложнее предыдущего, поскольку он включает в себя дополнительную обработку событий из источника, отдавая на выходе, по сути, уже новые события.
Преимущества
Индикаторы, основанные на срезах в ряде случаев обладают наиболее точной корреляцией с фактической удовлетворенностью пользователей, кроме того они удобнее в использовании политики бюджета на ошибки, поскольку время за окно измерения, в отличие от событий, — конечно и заранее известно, что позволяет нам получить ответы на вопросы вроде: «сколько еще минут мы можем полежать в этом месяце?».
В зависимости от % неуспешных событий в этом подходе можно получить наиболее точные индикаторы SLI, которые мы рассмотрим ниже.
Тем не менее, как и в предыдущем методе, корреляция с фактической удовлетворенностью зависит от смысла, который мы вкладываем в индикатор, в некоторых сценариях события гораздо важнее времени.
Привязываем метрику к временным срезам
Допустим, наши исследования пользовательского опыта показали, что 5% ошибок негативно влияют на удовлетворенность качеством уровня обслуживания, если они продолжаются 1 минуту.
Кроме того, исследования подвели нас к цели уровня обслуживания (SLO), которая может звучать так:
Сервис доступен 99.95% времени за период 4-х недель
*это равно приблизительно 22 минутам недоступности
Давайте обновим наш запрос таким образом, чтобы проверять каждую 1 минуту времени в окне измерения на факт превышения порога ошибок в 5%.
Для начала напишем PromQL для окна измерения в 14 дней со срезом в 1 минуту:
# Downtime = 1 minute when success rate < 95% # SLI = (total minutes - downtime minutes) / total minutes ( 14 * 24 * 60 - sum_over_time( sum( ( sum(increase( http_requests_count_total{ status=~"2...", service="market", endpoint=~"^/api/(catalog|cart|order)" }[1m] )) / sum(increase( http_requests_count_total{ status=~"(2..|5..)", service="market", endpoint=~"^/api/(catalog|cart|order)" }[1m] )) ) < bool 0.95 )[14d:1m] # window:slice ) ) / (14 * 24 * 60)
Если вы используете VictoriaMetrics, можно также воспользоваться функцией duration_over_time.
Чтобы рассмотреть результат этого запроса в сравнении с подходом на основе событий, давайте посмотрим на изображение ниже.
Первая диаграмма показывает накопленные «плохие минуты» (длительность недоступности) за скользящее окно в 14 дней, вторая — долю времени, когда сервис был доступен (запрос из примера), а третья — долю успешных запросов, которую мы рассматривали в самом начале этой статьи.
Первое, что должно бросаться в глаза — разные результаты индикаторов SLI.
В окне за 14 дней для подхода на основе срезов худший результат был равен ~99.8%, а для подхода на основе событий ~99.92%.
Стоит отметить, что кроме приведенного выше сбоя, до него был еще один инцидент, который добавился в копилку «плохих минут». Его влияние не отличалось от предыдущего, за исключением продолжительности.
Если бы все эти ~40 минут суммарной недоступности коэффициент ошибок был близок к 100%, то мы бы не заметили разницы, оба индикатора имели бы почти одинаковые значения.
Однако, если коэффициент ошибок далек от 100% и держится на относительно небольшом уровне, скажем от 1% до 30%, при этом такое количество считается фактической недоступностью, подход на основе событий искусственно улучшит индикатор и может ввести вас в заблуждение при принятии решений.
Давайте рассмотрим те же индикаторы, но за скользящие 28 дней, как и сформулировано в нашем SLO. Поправим запрос:
( 28 * 24 * 60 - sum_over_time( # updated sum( ( sum(increase( http_requests_count_total{ ... }[1m] )) / sum(increase( http_requests_count_total{ ... }[1m] )) ) < bool 0.95 )[28d:1m] # updated ) ) / (28 * 24 * 60) # updated
Последовательность диаграмм идентична предыдущей.
Освежим в памяти, что мы стремились к тому, чтобы наш сервис был доступен 99.95% времени для пользователя за окно измерения в 4 недели.
Если при принятии решения о необходимости использования политики бюджета на ошибки мы ориентируемся на индикатор, основанный на событиях, то наше значение на 21 июня ровно попадает в SLO – 99.95%, поскольку бюджет успел восстановиться.
Мы могли бы не предпринимать никаких действий, согласно этой политике, поскольку перерасход бюджета не был зафиксирован. Но мы точно знаем, что эти сбои общей продолжительностью 40 минут были восприняты нашими пользователями крайне негативно, да и калькулятор говорит о том, что норма недоступности при такой цели — 22 минуты.
В этой ситуации напрашивается довольно очевидный вывод — индикатор на основе событий слабо коррелирует с пользовательским опытом. В то время, как индикатор на основе временных срезов отражает наиболее точную картину, наш SLI равен ~99.92%, что ниже целевого значения и свидетельствует о перерасходе нашего бюджета на ошибки в более чем полтора раза.
При действующем Error budget policy мы были бы вынуждены переключить свой фокус на задачи по надежности и временно приостановить работу над новыми функциями.
Подведем итоги
В этой статье я попытался последовательно раскрыть путь улучшения точности индикатора доступности сервиса, приближая его к пользовательскому опыту.
На моей практике чаще всего подход основанный на времени лучше подходил для использования с Availability SLO. Тем не менее, вы можете измерять таким же подходом и долю времени, когда ваш сервис был доступен, но значительно деградировал по отзывчивости.
Не следует воспринимать эти подходы как единственно верные, в конечном счете, вам необходимо ориентироваться на специфику вашего продукта или сервиса, пользовательские сценарии и другие особенности компании.
Подход, основанный на событиях:
- Менее подвержен искажениям из-за динамического трафика, имеющего всплески
- Может дополняться новыми условиями, определяющих единицу времени недоступности, например:
- Общее количество запросов не должно быть меньше 5
- Из измерений исключаются минуты в период с 00:00 до 06:00
- Хорошо подходит для планирования экспериментов, поскольку дает понимание остатка минут, которые мы можем быть недоступными без значимых последствий
В заключение хочу сказать, что следует всегда держать в голове то, что SLI – это показатель, который в первую очередь должен отражать удовлетворенность пользователя качеством обслуживания.
Та же доступность может измеряться не только на основе доли успешных или неуспешных запросов, но и ряде других метрик. Это лишь один из возможных вариантов, который хорошо подходит для первых шагов в сторону подхода, основанного на SLO.
Непрерывно пересматривайте и улучшайте ваши индикаторы.
«Our monitoring and logging do not decide our reliability; our users do.»