Наблюдаемость
Журналы, метрики, трассировки и оповещения: узнать об отказе раньше пользователей
Вячеслав Федоров
Лекция 19
Разработка и применение ПО
в физических исследованиях
Изложение. Девятнадцатая лекция посвящена эксплуатации программ. Служба, которую разработчик запустил и проверил вручную, со временем работает без него: ночью, в выходные, под нагрузкой, которой не было при отладке. Её отказ обнаруживается либо её собственными средствами, либо пользователями — сменой установки, которая не может открыть данные последнего запуска, или скриптом обработки, всю ночь получавшим пустые ответы. Термин «наблюдаемость» заимствован из теории управления: в работе Р. Калмана 1960 года система называется наблюдаемой, если её внутреннее состояние можно восстановить по выходным сигналам. Программа является наблюдаемой, если по тому, что она сообщает о себе, — по журналам, метрикам и трассировкам — можно понять, что происходит внутри неё, в том числе в ситуации, которую никто заранее не предусмотрел. Лекция рассматривает, как сделать программу наблюдаемой заранее: какие сигналы она выдаёт, как их собирают и хранят, как по ним строят панели и оповещения. Сквозным примером служат две программы ускорительного комплекса: служба вакуумной системы, опрашивающая контроллер вакуумметров, и архив измерений, выдающий данные запусков по HTTP. Надпись на титуле — p99, девяносто девятый перцентиль задержки: величина, которую в лекции оценивают по гистограмме и по которой записывают цели уровня обслуживания.
Демонстрации. Слайды с розовым выделением слова «Демонстрация» отмечают места, где изложение прерывается для работы в терминале виртуальной машины lab с Ubuntu 24.04, Python 3.12, Prometheus 3.15 и node_exporter 1.12, далее называемой стендом: на слайде проигрывается запись тех же команд. Службы демонстраций — экспортёр вакуумной системы, модель архива, Prometheus и node_exporter — слушают только петлевой адрес 127.0.0.1, а данные модельные и порождаются с фиксированным зерном. Поэтому ряды, счётчики и сработавшие оповещения повторяются на другой машине, а длительности зависят от процессора.
Содержание
От события в программе к оповещению дежурного
Изложение. Лекция состоит из семи частей. Первая объясняет, зачем наблюдать: из каких отрезков складывается простой, какова цена надёжности и чем симптом отличается от причины. Вторая посвящена журналам: структурированной записи, уровням и порогу, контексту запроса, журналу systemd и цене хранения. Третья вводит метрики: модель данных Prometheus, типы метрик, экспортёр, метки и кардинальность, гистограммы и оценку квантилей. Четвёртая рассматривает сбор и хранение метрик: опрос и отправку, Prometheus и язык запросов PromQL, метрики заданий без постоянного процесса. Пятая посвящена трассировкам и OpenTelemetry, шестая — методам RED и USE и правилам построения панелей, седьмая — оповещениям по скорости расходования бюджета ошибок, правилам Prometheus с тестами и организации дежурства.
Цель лекции
Делать программу наблюдаемой заранее, чтобы отказ обнаруживался и локализовался за минуты, а дежурного вызывали только по делу
Журнал, метрика и трасса отвечают на разные вопросы об одной и той же службе. Prometheus собирает метрики и вычисляет правила, а оповещение по скорости расходования бюджета ошибок срабатывает на долгий отказ средней силы и не срабатывает на короткий всплеск.
Изложение. Цель лекции состоит в том, чтобы научиться делать программу наблюдаемой заранее, до первого отказа. Наблюдаемость не исправляет ошибки, но сокращает время, за которое отказ обнаруживают и находят отказавший компонент: без метрик и оповещений об отказе сообщают пользователи, а без журналов и трассировок поиск причины превращается в перебор компонентов. Каждый вид сигналов отвечает на свой вопрос. Журнал позволяет восстановить, что произошло с конкретным запросом или циклом опроса. Метрика показывает, сколько запросов обслужено, как быстро и сколько из них завершились ошибкой. Трасса показывает, на каком участке пути через несколько служб запрос провёл время. Собранные метрики хранит Prometheus, он же вычисляет по ним правила оповещений. Вторая половина цели касается оповещений: дежурного необходимо вызывать по симптомам, которые затрагивают пользователей, и только тогда, когда требуется действие человека. В рамках лекции такое правило записывается по скорости расходования бюджета ошибок и проверяется тестом, как обычный код.
Сигналы
наблюдаемости
Сигнал Вопрос Инструмент курса
журнал что произошло с этим запросом или циклом logging, journald, jq
метрика сколько, как быстро, сколько ошибок prometheus_client, Prometheus
трасса на каком участке запрос провёл время OpenTelemetry
профиль какие функции расходуют процессор py-spy (лекция 20)
проба доступна ли служба снаружи внешняя проверка
Пояснение
Метрики описывают программы; физические величины установки хранит архив измерений, например EPICS Archiver Appliance (часть 7).
Для физика
Сквозной пример: служба вакуумной системы и архив запусков, выдающий данные по HTTP.
Изложение. Таблица перечисляет сигналы, которыми программа сообщает о своём состоянии, и вопросы, на которые отвечает каждый из них. Журнал представляет собой последовательность записей о событиях и отвечает на вопрос, что произошло с конкретным запросом или циклом опроса; в курсе журналы пишет модуль logging, хранит journald, а отбирает утилита jq. Метрика является числом, которое наблюдается во времени: число запросов, доля ошибок, распределение длительности, давление в секторе. Метрики выставляет библиотека prometheus_client, а собирает и хранит Prometheus. Трасса показывает путь одного запроса через несколько служб и время на каждом участке; её записывает OpenTelemetry. Профиль отвечает на вопрос, какие функции работающей программы расходуют процессор; профилировщик py-spy применяется к медленной службе в лекции 20. Наконец, проба проверяет службу снаружи, так же как это делает пользователь, и обнаруживает отказ сети или балансировщика, невидимый изнутри. Журналы, метрики и трассировки называют основными сигналами наблюдаемости, а профили и пробы дополняют их. Отметим разграничение, к которому лекция вернётся в последней части: метрики описывают программы, а значения физических каналов установки хранит архив измерений с иными требованиями к точности и сроку хранения.
Зачем наблюдать
Время восстановления, цена надёжности, симптомы и причины
01
Зачем наблюдать
Журналы
Метрики
Сбор и хранение
Трассировки
Дашборды
Оповещения
Изложение. Первая часть объясняет, зачем программе наблюдаемость. Рассматриваются отрезки, из которых складывается простой, и то, какие из них сокращает наблюдаемость; цена надёжности и девятки доступности; различие симптомов и причин, на котором основано правило «вызывать дежурного по симптомам».
Из чего складывается
простой
сокращает наблюдаемость
Обнаружение
отказ → первый сигнал
Реакция
сигнал → дежурный за работой
Локализация
работа → найден компонент
Восстановление
причина → служба работает
01
Горячая фаза
вернуть работу: обнаружить, локализовать, исправить
02
Холодная фаза
разбор причин и меры против повторения — лекция 20
03
Восстановление
зависит от выкатки и отката, а не от наблюдения
Пояснение
MTTD, MTTA и MTTR — средние времена обнаружения, подтверждения и восстановления; буква R означает то восстановление работы, то устранение причины.
Изложение. Время, в течение которого служба не выполняет свою функцию, складывается из нескольких отрезков. Обнаружение длится от начала отказа до первого сигнала о нём — оповещения или сообщения пользователя. Реакция — от сигнала до момента, когда дежурный приступил к работе. Локализация — от начала работы до момента, когда найден отказавший компонент. Восстановление — от найденной причины до возобновления работы. Эти отрезки составляют горячую фазу инцидента, цель которой — вернуть службу в строй; после неё наступает холодная фаза: разбор причин и меры, исключающие повторение, которым посвящена лекция 20. Наблюдаемость сокращает первые три отрезка. Без метрик и оповещений обнаружение определяется тем, когда пользователь заметит отказ и сообщит о нём, а без журналов и трассировок локализация превращается в перебор компонентов. Восстановление зависит в основном от того, как устроены выкатка и откат версий. В отрасли средние значения отрезков обозначают сокращениями MTTD, MTTA и MTTR — среднее время обнаружения, подтверждения и восстановления, — причём буква R в разных организациях означает то восстановление работы, то устранение причины, и при сравнении чисел необходимо уточнять определение.
Цена
надёжности
бюджет простоя = 1 − SLO 99 % 14 мин 24 с в сутки 99,9 % 1 мин 26 с в сутки 99,99 % 8,6 с в сутки 99,999 % 0,86 с в сутки
Пояснение
Таблица девяток за 30 дней и за год, SLI, SLO и SLA — лекция 14 и глава «Системный дизайн».
01
Резервирование
основной способ повышения надёжности
02
Девятка
каждая уменьшает допустимый простой в 10 раз
03
Цена
растёт быстрее надёжности: девятку обосновывают выгодой
04
Не каждой программе
три девятки нужны не всем службам
Для физика
Ночной пересчёт калибровок, отложенный на час, ничего не нарушает; выдача данных смене во время сеанса требует большего.
Изложение. Стопроцентной доступности не бывает: отказывают диски, сеть, питание, а программы содержат ошибки. Основным способом повышения надёжности является резервирование — запасные экземпляры, реплики, второй канал связи, — и каждый следующий уровень требует ресурсов и людей. Доступность измеряют числом девяток, и каждая дополнительная девятка в десять раз уменьшает допустимый простой. Оценим его для суток: при 99,9 % допускается 86 400 · 0,001 = 86,4 с, то есть 1 мин 26 с, при 99,99 % — 8,6 с, а при пяти девятках — меньше секунды. Цена надёжности растёт быстрее самой надёжности, поэтому каждую следующую девятку обосновывают выгодой, а не стремлением к совершенству. Не каждой программе необходимы три девятки. Ночной пересчёт калибровок, отложенный на час, ничего не нарушает, тогда как выдача данных смене во время сеанса требует большего: отказ на полчаса означает, что смена ведёт установку без данных. Таблица допустимого простоя за 30 дней и за год, а также понятия SLI, SLO и SLA введены в лекции 14 и в главе «Системный дизайн»; в седьмой части эти понятия применяются к оповещениям.
Симптомы
и причины
Симптом Причина
что это видит пользователь объясняет симптом
примеры ошибки, долгие ответы, устаревшие данные диск, память, блокировка, отставшая реплика
сколько немного много, чаще без последствий
сигнал вызов дежурного панель, заявка
Для физика
Симптом — «запуск 42 не открывается в архиве», причина — «реплика базы метаданных отстала на час».
01
Чёрный ящик
проверка снаружи, как у пользователя: отказ уже случился
02
Белый ящик
метрики изнутри: приближение отказа и место причины
03
Правило
вызов — по симптому, причины — на панели
Изложение. Симптом представляет собой то, что видит пользователь: ошибки, долгие ответы, устаревшие данные. Причина объясняет симптом: переполненный диск, исчерпанная память, блокировка в базе, отставшая реплика. Для смены установки симптом выглядит так: «запуск 42 не открывается в архиве», а причина может состоять в том, что реплика базы метаданных отстала на час. Симптомов немного, и каждый из них означает, что пользователи уже пострадали; причин много, и большая часть из них к симптомам не приводит: загрузка процессора 90 % при быстрых ответах не мешает никому. Различают два способа наблюдения. Чёрный ящик проверяет службу снаружи так же, как пользователь: запрашивает страницу и измеряет время ответа. Такая проверка обнаруживает отказ, который уже случился, в том числе отказ сети или балансировщика, невидимый изнутри службы. Белый ящик опирается на сведения, которые служба сообщает о себе: счётчики запросов, длину очередей, время обращения к базе. Он позволяет заметить приближение отказа и указывает, в каком компоненте искать причину. Отсюда вытекает правило книги Google SRE: дежурного вызывают по симптомам, а причины выводят на панели, которые просматривают, когда симптом уже обнаружен; исключение составляют несомненные и неминуемые причины, например почти заполненный диск.
Демонстрация:
MTTR по кварталам
Что наблюдается
Средние по кварталам: 29, 27, 15 и 21 мин до улучшения, 33, 45, 51 и 25 мин после, хотя каждый инцидент стал на 20 % быстрее. В седьмом квартале медиана 11 мин, а максимум 218 мин. Из 10 000 повторений опыта в 30 % среднее не улучшилось.
root@lab — запись со стенда
Демонстрация. Сценарий incidents.py моделирует длительности простоев установки логнормальным распределением с медианой 27 мин; за квартал случается от 8 до 14 инцидентов, а начиная с пятого квартала процесс улучшен так, что длительность каждого инцидента меньше на 20 %. Таблица показывает одну реализацию с фиксированным зерном. Средние по кварталам до улучшения равны 29, 27, 15 и 21 мин, после улучшения — 33, 45, 51 и 25 мин: улучшение на 20 % в средних не видно, а в седьмом квартале среднее почти вдвое выше, чем в первом. Причина видна в соседних столбцах: медиана седьмого квартала составляет 11 мин, а максимум — 218 мин, то есть среднее определил один долгий инцидент. Распределение длительностей имеет тяжёлый хвост, а инцидентов в квартале около десяти, поэтому среднее колеблется от квартала к кварталу сильнее, чем меняется сам процесс. Последняя строка повторяет опыт 10 000 раз и сравнивает среднее за кварталы 7–8 со средним за кварталы 1–2: в 30 % повторений улучшение не обнаружилось. Таким образом, при десятке инцидентов в квартал средние MTTR являются шумом, а не показателем работы. Отчёт Google «Incident Metrics in SRE» приходит к тому же выводу и показывает, что медиана и другие сводные показатели имеют тот же недостаток: в таблице медианы улучшенных кварталов, 15, 42, 11 и 26 мин, в целом выше, чем 9, 29, 12 и 13 мин до улучшения. Поэтому каждый инцидент разбирают отдельно, а изменение оценивают по величине, которую оно затрагивает непосредственно, например по времени обнаружения, или по показателям уровня обслуживания.
Журналы
Структурированная запись, уровни и порог, контекст запроса, журнал systemd, цена хранения
02
Зачем наблюдать
Журналы
Метрики
Сбор и хранение
Трассировки
Дашборды
Оповещения
Изложение. Вторая часть посвящена журналам — основному материалу расследования отказов. Рассматриваются структурированная запись в формате JSON, уровни и порог, который меняется без перезапуска, контекст запроса, журнал systemd и оценка объёма журналов.
Журнал:
запись события
# строка: поля извлекают регулярным выражением WARNING vacuum: VG04 6.2e-06 Pa above threshold # запись JSON: отбор по полям {"t":"12:00:06","lvl":"WARNING","cyc":3, "vg":"VG04","p":6.2e-06,"msg":"порог"} # трассировка стека — в одном поле {"lvl":"ERROR","msg":"нет ответа", "exc":"Traceback …\nTimeoutError: VG06"}
01
Событие
момент времени, источник, уровень, сообщение
02
Строка
удобна человеку, программе нужен разбор текста
03
JSON
одна строка на событие: jq, Loki, Elasticsearch
04
Поля
одинаковые имена во всех службах системы
Изложение. Журнал представляет собой последовательность записей о событиях: служба запущена, вакуумметр не ответил, запрос завершился ошибкой. Каждая запись относится к моменту времени и содержит источник, уровень важности и сообщение. Строка вида «WARNING vacuum: VG04 6.2e-06 Pa above threshold» удобна человеку, но программа извлекает из неё давление и имя датчика регулярным выражением, которое перестаёт работать при первом изменении формулировки. Многострочная трассировка стека в таком журнале распадается на отдельные записи, и связать их с исходной ошибкой удаётся не всегда. Структурированный журнал записывает каждое событие одной строкой JSON с постоянным набором полей: время, уровень, номер цикла, датчик, давление, сообщение. Такая структура позволяет отбирать записи по полям без разбора текста — утилитой jq или системами хранения журналов Loki и Elasticsearch. Трассировку стека при исключении помещают в одно поле: стандартный форматтер возвращает её текст методом formatException, а форматтер JSON записывает его в отдельный ключ, и запись остаётся одной строкой. Состав полей необходимо согласовать для всех служб системы: одинаковые имена времени, уровня и идентификатора запроса позволяют объединить журналы нескольких программ в один поток и отбирать записи одним выражением.
Уровни
и порог
Уровень Число Пример
DEBUG 10 коды АЦП, подробности отладки
INFO 20 изменение состояния: цикл завершён
WARNING 30 требует внимания: давление выше порога
ERROR 40 операция не выполнена: датчик не ответил
CRITICAL 50 служба не может продолжать работу
01
Служба
порог INFO, отладка — по требованию
02
Без перезапуска
перезапуск сбрасывает состояние неисправности
log.setLevel(logging.INFO) # порог log.debug("коды %s", adc) # отброшено сразу log.debug(f"коды {adc}") # строка вычислена
Важно
Порог отбрасывает записи ниже себя: при INFO выводятся INFO, WARNING, ERROR и CRITICAL, а DEBUG — нет.
Пояснение
Повторяющуюся ошибку выводят раз в интервал с числом повторений, частое событие заменяют счётчиком.
Изложение. Модуль logging определяет пять уровней: DEBUG (10), INFO (20), WARNING (30), ERROR (40) и CRITICAL (50). Порог, заданный логгеру или обработчику, отсекает записи с уровнем ниже порога, то есть порог задаёт минимальную важность записи. Вызов log.debug при пороге INFO завершается сразу после сравнения уровней: запись не создаётся, а сообщение не форматируется, если аргументы переданы отдельно, как во второй строке кода. Если же сообщение подставлено заранее в f-строку, строка вычисляется до вызова, хотя запись затем всё равно отбрасывается. Поэтому отладочные вызовы остаются в коде службы, а их сообщения выводятся, только когда порог понижен до DEBUG. Для службы обычно выбирают порог INFO: изменения состояния и всё, что требует внимания. Во время расследования необходимо включить отладочный порог, причём без перезапуска: перезапуск сбрасывает именно то состояние, в котором проявляется неисправность, — заполненную очередь, зависшее соединение с прибором. Объём журнала ограничивают и другими средствами: трассировку стека повторяющейся ошибки выводят один раз за интервал вместе с числом повторений, а частое однотипное событие заменяют счётчиком — метрикой, рассмотренной в третьей части.
Контекст
запроса
cycle = contextvars.ContextVar("cycle", default=0) class JsonFormatter(logging.Formatter): def format(self, record): doc = {"t": ..., "lvl": record.levelname, "cyc": cycle.get()} # контекст doc.update(getattr(record, "fields", {})) doc["msg"] = record.getMessage() return json.dumps(doc, ensure_ascii=False) cycle.set(3) # для всех записей цикла log.warning("порог", extra={"fields": {"vg": "VG04"}})
01
Идентификатор
номер цикла, запроса или trace_id в каждой записи
02
contextvars
значение видно всему коду текущего контекста
03
asyncio
задача получает копию контекста при создании
04
threading
новый поток — с пустым контекстом: copy_context().run
Пояснение
Аналитические события — кто и какой запуск открыл — пишут отдельным потоком или полем, а не уровнем.
Изложение. Отдельная запись редко объясняет отказ; объясняет его цепочка записей одного запроса или цикла, проходящая через несколько функций, потоков и служб. Цепочку связывает идентификатор, присутствующий во всех её записях: номер цикла опроса, идентификатор запроса, назначенный на входе в систему, или идентификатор трассы OpenTelemetry, рассмотренный в пятой части. Передавать идентификатор аргументом через все функции неудобно. Модуль contextvars позволяет хранить его в контекстной переменной: значение, установленное методом set, видно всему коду текущего контекста, а форматтер читает его методом get. Поля самого события передаются аргументом extra: модуль logging превращает ключи этого словаря в атрибуты записи, и форматтер читает атрибут fields. Задачи asyncio получают копию контекста в момент создания, поэтому идентификаторы параллельно обрабатываемых запросов не смешиваются; функция asyncio.to_thread переносит контекст в рабочий поток сама. Новый поток, созданный модулем threading, в обычной сборке Python начинает работу с пустым контекстом, и значение необходимо передать явно: функция contextvars.copy_context возвращает копию текущего контекста, а её метод run выполняет в ней функцию потока. Отметим ещё одно правило: аналитические события, например сведения о том, кто и какой запуск открыл, пишут отдельным потоком или помечают полем, а не уровнем журнала, поскольку уровень описывает важность события для эксплуатации.
Демонстрация:
журнал JSON
02
python3 jsonlog.py | jq -c 'select(.lvl!="INFO")|{t,cyc,vg,msg}'
03
python3 jsonlog.py | jq -r .vg | sort | uniq -c
Что наблюдается
Семь записей трёх циклов, время модельное с 12:00:00. Отбор оставляет две: VG06 «нет ответа» в цикле 2 и VG04 «порог» в цикле 3. По датчикам: VG01 3, VG04 3, VG06 1.
root@lab — запись со стенда
Демонстрация. Сценарий jsonlog.py моделирует службу опроса шести вакуумметров: три цикла опроса, в каждом из которых в журнал попадают датчики VG01 и VG04 и события остальных датчиков. Форматтер на стандартном модуле logging записывает каждое событие одной строкой JSON: время, уровень, номер цикла из контекстной переменной, датчик, давление в паскалях и сообщение. Время модельное — с 12:00:00 с шагом 1 с, поэтому вывод не зависит от момента запуска. Первая команда печатает семь записей: пять в норме с давлением от 1,7·10⁻⁷ до 2,6·10⁻⁷ Па, ошибку «нет ответа» датчика VG06 в цикле 2 — у неё нет поля давления — и предупреждение «порог» датчика VG04 в цикле 3 с давлением 6,2·10⁻⁶ Па. Вторая команда передаёт поток утилите jq: выражение select оставляет записи с уровнем, отличным от INFO, а проекция {t,cyc,vg,msg} — только четыре поля. Остаются две строки: ошибка в 12:00:04 и предупреждение в 12:00:06. Третья команда извлекает поле vg и подсчитывает записи по датчикам конвейером sort и uniq -c: VG01 и VG04 — по три записи, VG06 — одна. Отметим, что ни одна из команд не использует регулярных выражений: отбор выполняется по полям, и изменение текста сообщения его не нарушает. Тот же отбор выполняют системы хранения журналов на потоке записей многих служб.
Демонстрация:
порог на ходу
Что наблюдается
Циклы 1–4 — только INFO. После первого SIGUSR1 запись на 1011 мс сообщает о пороге DEBUG, и в циклах 5–6 появляются коды АЦП; второй сигнал на 1517 мс возвращает INFO. Процесс работает без перезапуска.
root@lab — запись со стенда
Демонстрация. Сценарий loglevel.py моделирует службу опроса: восемь циклов по 0,25 с, журнал с порогом INFO, отладочные записи с необработанными кодами АЦП. Обработчик сигнала SIGUSR1 переключает порог между INFO и DEBUG и оставляет отметку, а сообщение о смене порога записывает основной цикл. Сценарий toggle.sh, показанный первой командой, запускает службу в фоне, через 0,8 с посылает ей SIGUSR1 командой kill с номером последнего фонового процесса, ещё через 0,6 с посылает второй сигнал и дожидается завершения. Отметки времени в выводе — миллисекунды от старта процесса. Циклы 1–4 на 2, 252, 504 и 756 мс выводят только записи INFO. Первый сигнал приходит во время паузы после четвёртого цикла; в начале пятого цикла, на 1011 мс, основной цикл записывает предупреждение «порог журнала: DEBUG (сигнал SIGUSR1)», и в циклах 5 и 6 появляются строки DEBUG с кодами АЦП [2045, 2046, 2008] и [2046, 2045, 2010]. Второй сигнал приходит после шестого цикла, и на 1517 мс порог возвращается к INFO. Таким образом, отладочные записи включены в работающем процессе, без перезапуска, который уничтожил бы интересующее состояние. В службе systemd сигнал посылает команда systemctl kill -s USR1 с именем службы.
stdout
и журнал systemd
SYSLOG = {DEBUG: 7, INFO: 6, WARNING: 4, ERROR: 3, CRITICAL: 2} class PrefixFormatter(logging.Formatter): def format(self, record): prefix = f"<{SYSLOG[record.levelno]}>" return prefix + super().format(record) # stdout службы: <4>vacuum: цикл 4: VG04 давление 7.5e-06 Па выше порога # journald: PRIORITY=4, префикс снят
01
Поток событий
stdout, по The Twelve-Factor App
02
Среда
journald, драйвер журналов Docker, агент Kubernetes
03
Префикс <N>
уровень syslog: SyslogLevelPrefix, по умолчанию включён
04
Поля
_PID, _SYSTEMD_UNIT, PRIORITY: отбор по полям
Пояснение
Объём и срок хранения задаёт среда: SystemMaxUse в journald.conf, logrotate для файлов.
Изложение. Программе не требуется самой управлять файлами журнала: ей достаточно писать записи в стандартный вывод. Методология The Twelve-Factor App рассматривает журнал как поток событий: приложение выводит его, а сбор, хранение и ротацию обеспечивает среда выполнения. Для службы systemd такой средой является journald: стандартный вывод и поток ошибок службы по умолчанию попадают в системный журнал, где каждая запись получает поля _PID, _SYSTEMD_UNIT, PRIORITY и другие. Уровень записи journald определяет по префиксу вида <N> в начале строки: параметр SyslogLevelPrefix по умолчанию включён, 2 означает критическую ошибку, 3 — ошибку, 4 — предупреждение, 6 — информационное сообщение, 7 — отладку. Префикс удаляется из текста сообщения и сохраняется в поле PRIORITY. Форматтер на слайде добавляет такой префикс к каждой записи, что позволяет обойтись без библиотек systemd. Записи затем отбирают командой journalctl по полям, а не по тексту: ключ -p warning оставляет уровни от emerg до warning, а вывод в формате JSON передаёт поля записи программе. Контейнер Docker аналогично передаёт стандартный вывод драйверу журналов — по умолчанию json-file, который без параметров max-size и max-file журнал не ротирует, — а в Kubernetes среда выполнения записывает вывод контейнеров в файлы на узле, откуда их обычно забирает агент, запущенный на каждом узле. Во всех случаях программа пишет в стандартный вывод, а объём и срок хранения журнала задаёт среда: journald ограничивает его параметром SystemMaxUse, файлы журналов обслуживает logrotate.
Демонстрация:
журнал systemd
01
systemd-run -q -u l19-vacuum --collect python3 $PWD/journal_svc.py
02
sleep 4; journalctl -u l19-vacuum -o cat | tail -6
03
journalctl -u l19-vacuum -p warning -S -1min -o cat
04
journalctl -u l19-vacuum -p 4 -S -1min -o json | jq -r .PRIORITY
Что наблюдается
Служба отработала шесть циклов и завершилась. Отбор по уровню warning оставляет две записи: VG06 не отвечает, VG04 выше порога. Поле PRIORITY — 3 и 4, префиксов в тексте нет.
root@lab — запись со стенда
Демонстрация. Сценарий journal_svc.py моделирует службу опроса вакуумметров: шесть циклов по 0,5 с, в цикле 2 датчик VG06 не отвечает, в цикле 4 давление VG04 выше порога. Записи выводятся в стандартный вывод с префиксом уровня <N>. Первая команда запускает сценарий транзиентной службой systemd с именем l19-vacuum: ключ -q подавляет сообщение systemd-run о созданной службе, -u задаёт имя, а --collect выгружает службу после завершения, даже неудачного: без этого ключа менеджер хранит отказавшую службу до команды systemctl reset-failed. Путь к сценарию необходимо указать полностью, через переменную PWD: службу запускает менеджер systemd, а не оболочка, и рабочий каталог оболочки ей не передаётся. Вторая команда через 4 с, когда служба уже завершилась, выводит последние шесть строк её журнала в формате cat — только текст сообщений: циклы 3–6, предупреждение о давлении 7.5e-06 Па и сообщение systemd о завершении службы. Третья команда отбирает записи с уровнем warning и выше; ключ -S -1min ограничивает выборку последней минутой, чтобы в неё не попали записи прежних запусков службы с тем же именем. Остаются две записи — «VG06 не отвечает» и «VG04 давление … выше порога». Четвёртая команда выводит те же записи в формате JSON, а jq извлекает поле PRIORITY: 3 и 4, то есть ошибка и предупреждение. Ключ -p 4 равнозначен -p warning. Таким образом, уровень записи journald получил из префикса, а сам префикс из текста сообщения удалён.
Цена
журналов
Политика записи Записей в сутки В сутки За год Год, gzip
каждый отсчёт: 200 каналов × 10 Гц 1,73·10⁸ 14,6 ГБ 5,3 ТБ 438,9 ГБ
цикл опроса раз в секунду 8,64·10⁴ 7,3 МБ 2,7 ГБ 219,4 МБ
только изменения состояния около 10³ 84,7 кБ 30,9 МБ 2,5 МБ
01
Оценка
записей в сутки × размер записи × срок хранения
02
Запись
85 байт JSON, gzip сжимает в 12,2 раза
03
Срок
по требованию: PCI DSS 10.5.1 — 12 месяцев, три из них с немедленным доступом
Для физика
Отсчёты каналов являются данными и хранятся в архиве измерений; журнал предназначен для событий.
Изложение. У службы без собственных данных журнал обычно является главным потребителем дискового пространства, и его объём оценивают заранее как произведение числа записей в сутки, среднего размера записи и срока хранения. Оценим три политики записи для службы сбора данных с 200 каналами, оцифровываемыми с частотой 10 Гц. Средняя запись JSON того же вида, что в демонстрации, занимает 85 байт и сжимается gzip в 12,2 раза; эти числа получены расчётом на 100 000 модельных записей. Запись на каждый отсчёт канала даёт 1,73·10⁸ записей в сутки — 14,6 ГБ в сутки и 5,3 ТБ за год, а со сжатием 438,9 ГБ. Запись на каждый цикл опроса раз в секунду даёт 7,3 МБ в сутки и 2,7 ГБ за год. Запись только изменений состояния — около тысячи событий в сутки — занимает 84,7 кБ в сутки. Таким образом, отсчёты каналов являются данными и хранятся в архиве измерений, а журнал предназначен для событий. Срок хранения иногда задан внешним требованием: стандарт безопасности платёжных систем PCI DSS в требовании 10.5.1 предписывает хранить журнал аудита не меньше 12 месяцев, из них последние три — с немедленным доступом. Сам срок и ротацию обеспечивает среда, а записи старше нескольких недель хранят сжатыми или удаляют.
Практики
журналирования
01
Одна структура
одинаковые поля во всех службах
02
Идентификатор
запроса, цикла или trace_id в каждой записи
03
Порог без перезапуска
сигнал или внешний ключ
04
stdout или journald
сбор и ротация — дело среды
05
Срок хранения
по требованию и объёму, старое — сжатым
06
Наблюдение за доставкой
число принятых записей в минуту
Изложение. Практики журналирования сводятся к шести правилам. Одна структура записей во всех службах системы позволяет объединять журналы и отбирать записи одним выражением. Идентификатор запроса, цикла или трассы в каждой записи связывает цепочку событий. Порог меняется без перезапуска — сигналом или ключом во внешнем хранилище. Служба пишет в стандартный вывод или в journald, а сбор и ротацию обеспечивает среда. Срок хранения определяется требованиями и объёмом, старые записи хранят сжатыми. Наконец, сам конвейер доставки журналов тоже может отказать незаметно, поэтому за ним наблюдают метрикой — числом принятых записей в минуту: падение этого числа до нуля означает отказ доставки, а не исчезновение событий.
Метрики
Модель данных, типы метрик, экспортёр, метки и кардинальность, гистограммы и квантили
03
Зачем наблюдать
Журналы
Метрики
Сбор и хранение
Трассировки
Дашборды
Оповещения
Изложение. Третья часть посвящена метрикам — числам, которые служба сообщает о себе и которые наблюдаются во времени. Рассматриваются модель данных Prometheus и формат экспозиции, четыре типа метрик, экспортёр для внешнего источника, метки и кардинальность, гистограммы и оценка квантилей по ним.
Модель данных
Prometheus
# HELP vacuum_up Опрос контроллера удался (1) или нет (0) # TYPE vacuum_up gauge vacuum_up 1.0 # HELP vacuum_pressure_pascals Давление # TYPE vacuum_pressure_pascals gauge vacuum_pressure_pascals{gauge="VG01",sector="linac"} 3.133e-07 vacuum_pressure_pascals{gauge="VG02",sector="ring"} 2.095e-07 # TYPE vacuum_gauge_errors_total counter vacuum_gauge_errors_total{gauge="VG01"} 0.0
Пояснение
Ряды VG01 и VG02 относятся к одной метрике, но хранятся отдельно.
01
Временной ряд
имя метрики и набор меток
02
Отсчёт
время с точностью до мс и число float64
03
Опрос
Prometheus сам читает /metrics: pull
04
Единицы
базовые по руководству: _seconds, _bytes, _pascals
05
Счётчик
имя оканчивается на _total
Изложение. Метрика представляет собой число, которое служба сообщает о себе и которое наблюдается во времени. В отличие от журнала, метрика не описывает отдельные события, а сводит их в число, поэтому объём её хранения почти не зависит от нагрузки: счётчик запросов занимает одинаковое место при десяти и при десяти тысячах запросов в секунду. Фактическим стандартом для метрик служб стала модель Prometheus. Временной ряд в ней определяется именем метрики и набором меток — пар «имя — значение»; отсчёт ряда состоит из метки времени с точностью до миллисекунды и числа с плавающей точкой двойной точности. Ряды давления датчиков VG01 и VG02 относятся к одной метрике vacuum_pressure_pascals, но хранятся отдельно. Prometheus сам опрашивает службы: раз в заданный интервал он запрашивает у каждой цели адрес /metrics и получает текст в формате экспозиции. Строки # HELP и # TYPE описывают метрику и её тип, а каждая следующая строка содержит имя, метки в фигурных скобках и значение. Значения выражают в базовых единицах из руководства Prometheus по именованию метрик и указывают единицу суффиксом имени: секунды, а не миллисекунды, байты, а не мегабайты, для давления — паскали. Это не единицы СИ в строгом смысле: массу руководство предписывает выражать в граммах, а температуру — в градусах Цельсия. Имя счётчика оканчивается на _total. Базовые единицы позволяют складывать и делить метрики разных служб без пересчёта, а суффикс избавляет читателя графика от догадок о масштабе.
Типы
метрик
Тип Поведение Пример Обработка
counter только растёт; при перезапуске — с нуля запросы, ошибки, срабатывания триггера rate, increase
gauge растёт и убывает давление, длина очереди, память значение, max_over_time
histogram счётчики корзин _bucket{le}, _sum, _count длительность запроса histogram_quantile
summary квантили вычисляет сама служба длительность в одном экземпляре не агрегируется
Пояснение
Счётчик не теряет событий между опросами: разность двух его значений учитывает всё, что произошло между ними.
Важно
Счётчик для убывающей величины — числа потоков или соединений — нарушает rate: каждое уменьшение выглядит как перезапуск.
Изложение. Библиотеки инструментирования Prometheus предлагают четыре основных типа метрик; сам сервер хранит все типы, кроме нативных гистограмм, как обычные ряды чисел, а тип определяет прежде всего то, какими функциями метрику обрабатывают в запросах. Счётчик, counter, только растёт и обнуляется при перезапуске службы; им считают события — запросы, ошибки, срабатывания триггера, — а скорость событий в секунду получают функцией rate. Мгновенное значение, gauge, растёт и убывает: давление, длина очереди, занятая память. Гистограмма, histogram, хранит счётчики попаданий наблюдений в корзины с верхними границами le, а также сумму и число наблюдений; квантиль по ней вычисляет Prometheus. Сводка, summary, вычисляет квантили в самой службе, и объединить такие квантили нескольких экземпляров корректно нельзя. События считают счётчиком, а не мгновенным значением, и причина здесь не в экономии места. Счётчик не теряет событий между опросами: сколько бы приращений ни произошло между двумя опросами, разность значений их учтёт, а функции rate и increase поправляют сброс при перезапуске. Мгновенное значение в момент опроса ничего не говорит о том, что было между опросами; демонстрация на следующем слайде показывает это числами. Обратная ошибка состоит в счётчике для убывающей величины: если число занятых соединений или рабочих потоков оформить счётчиком, функция rate воспримет каждое его уменьшение как перезапуск, и графики будут показывать несуществующие всплески.
Демонстрация:
счётчик и сброс
Что наблюдается
Опрос раз в 15 с: до перезапуска 5550, после — 750. Наивная разность 750, с учётом сброса 6300, на самом деле 6300. Значение «за секунду» во всех опросах 50, вспышка 400 не видна.
root@lab — запись со стенда
Демонстрация. Сценарий counter.py моделирует систему сбора данных за две минуты: триггер установки срабатывает 50 раз в секунду, на 70-й секунде происходит вспышка 400 срабатываний в секунду длительностью 3 с, а на 90-й секунде служба перезапускается, и счётчик начинается с нуля. Prometheus получает только значения в моменты опроса — раз в 15 с, на 14-й, 29-й и так далее до 119-й секунды. Счётчик растёт на 750 за интервал, а в интервале со вспышкой — на 1800, до 4800; перед перезапуском он равен 5550, а после — 750 и 1500. Наивная разность последнего и первого значений даёт 750 срабатываний. Приращение с учётом сброса вычисляется так же, как функция increase в PromQL: уменьшение значения означает, что счётчик начат заново, и новое значение целиком является приращением. Получим 6300 — ровно столько срабатываний произошло на самом деле. События между последним опросом и перезапуском в общем случае теряются, но в модели перезапуск происходит сразу после опроса. Мгновенное значение «срабатываний за последнюю секунду» во всех восьми опросах равно 50: вспышка пришлась на промежуток между опросами и не видна совсем, хотя её максимум — 400 в секунду. Таким образом, счётчик учитывает каждое событие независимо от того, когда его прочитали, а мгновенное значение показывает лишь моменты опроса.
Экспортёр:
опрос при обращении
class VacuumCollector: def collect(self): # при каждом GET /metrics t0 = time.perf_counter() up = GaugeMetricFamily("vacuum_up", "Опрос удался") p = GaugeMetricFamily("vacuum_pressure_pascals", "Давление", labels=["sector", "gauge"]) try: for gauge, sector in GAUGES.items(): try: v = self.ctrl.read(gauge) p.add_metric([sector, gauge], v) except TimeoutError: # ряда нет self.errors[gauge] += 1 up.add_metric([], 1) except OSError: # контроллер недоступен up.add_metric([], 0) yield from (up, p, err, dur)
01
Синхронный опрос
источник читается в момент запроса, своих таймеров нет
02
vacuum_up
0 — экспортёр работает, а источник недоступен
03
Нет ряда
вместо нуля или прежнего значения
04
Тайм-аут
меньше тайм-аута опроса Prometheus, 10 с
05
Момент события
время старта, а не время работы
Изложение. Если метрики берутся из другой системы — контроллера, прибора, внешнего API, — пишут экспортёр: программу, которая при каждом обращении Prometheus опрашивает источник и переводит ответ в формат экспозиции. Руководство Prometheus по написанию экспортёров предписывает синхронный опрос: экспортёр не запускает собственных таймеров, а читает источник в момент запроса /metrics и при каждом запросе создаёт метрики заново. Тогда возраст данных совпадает с моментом опроса, а отказ источника виден сразу; экспортёр с собственным циклом опроса раз в 2 с при опросе Prometheus раз в 30 с выполняет четырнадцать лишних обращений из пятнадцати и отдаёт значение неизвестного возраста. В prometheus_client синхронный опрос реализуется собственным коллектором — классом с методом collect, который возвращает семейства метрик. Коллектор на слайде опрашивает три вакуумметра. Датчик, не ответивший в этом опросе, ряда давления не даёт, а счётчик неудачных чтений по нему растёт: отсутствие ряда точнее отражает состояние, чем нуль, который выглядел бы как идеальный вакуум, или прежнее значение, которое скрыло бы отказ. Метрика vacuum_up со значением 1 или 0 сообщает, удался ли опрос контроллера, и позволяет отличить отказ источника от отказа самого экспортёра. Обращение к источнику необходимо ограничивать тайм-аутом, меньшим тайм-аута опроса Prometheus, по умолчанию 10 с. Руководство по инструментированию добавляет правило о моментах времени: момент события выставляют значением в секундах времени Unix, а не временем, прошедшим с события, — время старта процесса, а не время работы; прошедшее время Prometheus вычисляет сам выражением time() минус метрика.
Демонстрация:
экспортёр вакуума
01
python3 exporter.py & sleep 1
02
curl -s 127.0.0.1:9101/metrics | grep -v '^#'
03
curl -s 127.0.0.1:9101/metrics | grep -v '^#'
Что наблюдается
Первый опрос: vacuum_up 1.0, три ряда давления от 1,8 до 3,1·10^−7 Па, ошибок нет, опрос занял 36 мс. Во втором опросе ряда VG03 нет, а счётчик ошибок VG03 равен 1.0.
root@lab — запись со стенда
Демонстрация. Сценарий exporter.py содержит модель контроллера вакуумной системы и коллектор со слайда. Модель отдаёт давление трёх вакуумметров двух секторов, чтение занимает 10 мс на датчик, а датчик VG03 не отвечает в каждом втором опросе. Первая команда запускает экспортёр в фоне на адресе 127.0.0.1:9101. Вторая команда запрашивает /metrics утилитой curl и отбрасывает строки комментариев # HELP и # TYPE. Получим vacuum_up 1.0, три ряда давления — 3.133e-07 Па для VG01 в секторе linac, 2.095e-07 и 1.805e-07 Па для VG02 и VG03 в секторе ring, — нулевые счётчики ошибок по всем датчикам и длительность опроса 0,036 с: три чтения по 10 мс и накладные расходы. Третья команда повторяет запрос: давление VG01 и VG02 изменилось, так как контроллер опрошен заново, ряда VG03 нет, а vacuum_gauge_errors_total для VG03 вырос до 1.0. Отказ датчика, таким образом, виден, а не замаскирован старым числом или нулём, тогда как vacuum_up остался равным 1: контроллер в целом ответил. Суффикс _total к имени счётчика библиотека добавила сама. Последняя команда останавливает фоновый процесс. Тот же экспортёр работает и в четвёртой части как цель vacuum для Prometheus.
Метки
и кардинальность
REQS = Counter("archive_requests", "Запросы к архиву", ["method", "route", "code"]) REQS.labels("GET", "/runs/{id}", "200").inc() # шаблон маршрута: один ряд на все запуски REQS.labels("GET", "/runs/4217", "200").inc() # номер запуска в метке: новый ряд на каждый
Пояснение
Номер запуска — поле журнала или атрибут трассы, а не метка метрики.
01
Ряд
каждая комбинация значений меток
02
Цена
память, диск и время каждого запроса к данным
03
Ограниченные
метод, шаблон маршрута, код, сектор
04
Неограниченные
путь с номером, адрес клиента, пользователь
Важно
Документация Prometheus: кардинальность большинства метрик — ниже 10; метрику свыше 100 рядов перепроектируют.
Изложение. Каждая комбинация значений меток образует отдельный временной ряд, который Prometheus хранит в памяти и на диске, а число таких комбинаций называют кардинальностью метрики. Метки с ограниченным набором значений — метод HTTP, шаблон маршрута, код ответа, сектор установки — безопасны: счётчик запросов архива с тремя такими метками порождает единицы или десятки рядов. Метка с неограниченным набором значений — путь запроса с номером запуска, адрес клиента, имя пользователя — создаёт новый ряд для каждого нового значения, и число рядов растёт вместе с числом запусков или пользователей. Даже опечатка в значении метки при постепенной выкатке новой версии порождает второй ряд вместо первого. Документация Prometheus рекомендует держать кардинальность метрик ниже 10, а метрик с большей кардинальностью иметь во всей системе лишь несколько; метрику, кардинальность которой превышает 100 или может её превысить, предлагается перепроектировать — сократить число измерений или перенести анализ из мониторинга в систему обработки данных общего назначения. Номер запуска является полем записи журнала или атрибутом трассы, а не меткой метрики: для поиска по нему достаточно журнала. Демонстрация на следующем слайде оценивает масштаб на модели суточного потока запросов к архиву.
Демонстрация:
число рядов
Что наблюдается
20 000 запросов. Метод, шаблон маршрута и код — 9 рядов и ответ /metrics 2 КиБ; полный путь вместо шаблона — 11 945 рядов и 2051 КиБ; те же три метки и адрес клиента — 1082 ряда и 229 КиБ.
root@lab — запись со стенда
Демонстрация. Сценарий cardinality.py моделирует 20 000 запросов к HTTP-архиву за сутки: пути вида /runs/номер, /runs/номер/channels/канал и /runs/номер/plot для запусков с номерами от 1000 до 4999, 200 клиентов и три кода ответа. Один и тот же счётчик archive_requests учитывает эти запросы с тремя наборами меток, а размер ответа /metrics измеряется по тексту, который выдаёт prometheus_client. С метками метода, шаблона маршрута и кода ответа получается 9 рядов и ответ размером 2 КиБ. Если вместо шаблона маршрута записывать полный путь, рядов становится 11 945 — больше половины числа запросов, — а ответ занимает 2051 КиБ; такой объём Prometheus получает при каждом опросе, то есть раз в несколько секунд. Если к меткам метода, шаблона маршрута и кода ответа добавить адрес клиента, получается 1082 ряда и 229 КиБ: 200 клиентов увеличивают число рядов в 120 раз. Каждый из этих рядов Prometheus хранит в памяти и на диске отдельно, и запрос к метрике обрабатывает их все. Одна неудачная метка, следовательно, увеличивает объём данных метрики на три порядка.
Гистограмма
и квантиль
def hq(q, bounds, counts): # histogram_quantile rank = q * counts[-1] # counts накопительные i = bisect.bisect_left(counts, rank) if i == len(bounds): # ранг в корзине +Inf return bounds[-1] lo, below = ((0.0, 0) if i == 0 else (bounds[i - 1], counts[i - 1])) return lo + (bounds[i] - lo) * (rank - below) \ / (counts[i] - below) histogram_quantile(0.99, sum by (le) ( rate(archive_request_duration_seconds_bucket[5m])))
01
Корзины
накопительные: le="0.3" — все не дольше 0,3 с
02
Интерполяция
линейная: ошибка — до ширины корзины
03
+Inf
ранг за последней границей — её значение
04
Граница на SLO
доля «быстрее 300 мс» без интерполяции
05
Экземпляры
гистограммы складываются, summary — нет
Изложение. Задержку описывают перцентилями, как показано в лекции 14, но служба не может хранить длительность каждого запроса. Гистограмма Prometheus хранит счётчики попаданий в корзины с заданными верхними границами le: ряд с le="0.3" содержит число запросов длительностью не более 0,3 с, ряд с le="+Inf" — число всех запросов; дополнительно хранятся сумма длительностей и их число. Корзины накопительные: каждая включает все меньшие. Функция histogram_quantile находит корзину, в которую попадает искомый ранг, и интерполирует внутри неё линейно, предполагая равномерное распределение наблюдений по корзине; функция hq на слайде повторяет этот расчёт. Ошибка оценки поэтому ограничена шириной корзины, а при попадании ранга в корзину +Inf функция возвращает верхнюю границу последней конечной корзины. Отсюда правило выбора границ: граница, совпадающая с порогом цели уровня обслуживания, позволяет вычислить долю запросов быстрее порога без интерполяции — как отношение счётчика этой корзины к общему числу. Различие гистограммы и сводки существенно при нескольких экземплярах службы. Счётчики корзин разных экземпляров складываются, и выражение PromQL на слайде вычисляет p99 по всем экземплярам сразу, суммируя скорости корзин по метке le. Квантили сводок, вычисленные внутри каждого экземпляра, объединить корректно нельзя: среднее двух p99 не является p99 объединённого потока.
Демонстрация:
p99 по корзинам
Что наблюдается
p99 точно 354 мс; по корзинам по умолчанию — 447 мс, с границей 300 мс — 356 мс. Доля быстрее 300 мс: 96,92 % точно и с границей, 96,23 % по умолчанию. Два экземпляра: среднее p99 229 мс, сумма гистограмм 342 мс, точно 340 мс.
root@lab — запись со стенда
Демонстрация. Сценарий quantile.py моделирует задержки выдачи запуска архивом: 95 % из 20 000 запросов обслуживаются около 20 мс, а 5 % читают холодный файл около 310 мс. Функция hq повторяет histogram_quantile и сравнивает корзины prometheus_client по умолчанию с корзинами, сгущёнными около цели 300 мс. Медиана оценивается удовлетворительно: точно 20 мс, по корзинам 18 и 21 мс. Точный p99 равен 354 мс. Корзины по умолчанию дают 447 мс: соседние границы 0,25 и 0,5 с охватывают почти весь хвост распределения, и интерполяция внутри широкой корзины завышает p99 на 93 мс. Корзины с границей 300 мс дают 356 мс. Доля запросов быстрее 300 мс по корзине с границей на пороге совпадает с точной — 96,92 %, а по корзинам по умолчанию, где ответ приходится интерполировать, получается 96,23 %. Вторая часть моделирует два экземпляра службы: нагруженный, получивший 18 000 запросов с 1 % холодных чтений, и малонагруженный, получивший 2000 запросов, из которых 30 % холодные. Их p99 равны 48 и 410 мс, и среднее этих значений, 229 мс, не имеет отношения к p99 всех запросов, равному 340 мс. Квантиль по сумме гистограмм, 342 мс, близок к точному. Таким образом, границы корзин выбирают по целям уровня обслуживания, а для служб с несколькими экземплярами выбирают гистограмму, а не сводку.
Результат:
корзины и распределение
Что наблюдается
Между 250 и 500 мс у корзин по умолчанию нет границ: интерполяция сдвигает p99 с 354 до 447 мс. Границы 300, 350 и 400 мс делят хвост, и оценка 356 мс отличается от точной на 2 мс.
Демонстрация. Рисунок построен по той же модели, что и демонстрация. Серая гистограмма показывает распределение 20 000 задержек в логарифмических осях: высокий пик около 20 мс — обычные запросы — и пологий горб около 310 мс — холодные чтения, примерно в двадцать раз меньший по высоте. Оранжевым пунктиром отмечены границы корзин prometheus_client по умолчанию, синими точками — границы корзин, сгущённых около цели 300 мс. Вертикальные линии отмечают точный p99, 354 мс, зелёным, оценку по корзинам по умолчанию, 447 мс, оранжевым и оценку по корзинам с границей 300 мс, 356 мс, синим штрихпунктиром; зелёная и синяя линии почти совпадают. Рисунок объясняет ошибку: девять десятых горба холодных чтений попадают в одну корзину по умолчанию от 250 до 500 мс, а интерполяция считает наблюдения в ней распределёнными равномерно, тогда как на самом деле они сосредоточены в левой части. Корзины с границами 300, 350 и 400 мс делят горб на части, и интерполяция внутри узкой корзины почти не ошибается. Отметим, что сгущать корзины во всём диапазоне не требуется: достаточно нескольких границ около порога цели, а лишние корзины увеличивают число рядов гистограммы.
Сбор и хранение
Опрос и отправка, Prometheus на стенде, язык запросов PromQL, метрики заданий
04
Зачем наблюдать
Журналы
Метрики
Сбор и хранение
Трассировки
Дашборды
Оповещения
Изложение. Четвёртая часть посвящена доставке и хранению метрик. Рассматриваются два способа доставки — опрос и отправка, прямая запись и шина, затем Prometheus на стенде, язык запросов PromQL и метрики заданий, которые работают минуты и завершаются.
Опрос
и отправка
Опрос, pull Отправка, push
инициатор хранилище по списку целей служба или агент
живость ряд up после каждого опроса тишину не отличить от отсутствия событий
частота задаёт хранилище задают источники
примеры Prometheus Telegraf и InfluxDB, OTLP, Pushgateway
Пояснение
Pushgateway — для итогов пакетных заданий уровня службы: общая точка отказа, нет ряда up, ряды хранятся до удаления.
01
Прямая запись
проще; при отказе хранилища данные теряются или копятся в памяти агента
02
Шина
Kafka: буфер и несколько читателей, но ещё одна точка отказа и задержка
03
Обогащение
метки машины и площадки, отбор лишнего на сборе
Изложение. Метрики доставляются в хранилище двумя способами. При опросе хранилище само обращается к службам по списку целей; список задают в конфигурации или получают из реестра — Kubernetes, Consul, файла, который обновляет другая программа. Неудачный опрос сам является сигналом: Prometheus записывает для каждой цели ряд up со значением 1 после успешного опроса и 0 после неудачного. Частоту опроса, а тем самым и нагрузку на службы задаёт хранилище; объём же каждого ответа определяет служба, и метрика с неограниченными метками перегружает Prometheus и при опросе, поэтому для целей задают предел числа отсчётов sample_limit, при превышении которого опрос целиком считается неудачным. При отправке службы или агенты на машинах сами передают данные хранилищу, как это делают Telegraf с InfluxDB или агенты OpenTelemetry по протоколу OTLP. Отправка подходит для короткоживущих процессов и для сетей, в которых хранилище не может обратиться к службам; однако отсутствие данных в этом случае не отличается от отсутствия событий без отдельной проверки. Сервер Pushgateway, принимающий отправленные метрики для Prometheus, документация допускает по существу только для итогов пакетных заданий уровня службы, не связанных с определённой машиной; при отправке многих экземпляров через один сервер он становится общей точкой отказа и узким местом, лишает Prometheus проверки экземпляров по ряду up и хранит присланные ряды, пока их не удалят явно. Между источником и хранилищем данные передают напрямую или через шину сообщений, например Kafka. Прямая запись проще, но при недоступности хранилища данные теряются или накапливаются в памяти агента. Шина хранит данные, пока хранилище недоступно, и позволяет нескольким потребителям читать один поток, но добавляет компонент, который сам нуждается в наблюдении, и задержку доставки. На том же пути данные обогащают метками машины и площадки и отбрасывают лишнее, пока его объём не попал в хранилище.
Prometheus
на стенде
global: scrape_interval: 5s evaluation_interval: 5s rule_files: - rules.yml scrape_configs: - job_name: prometheus static_configs: - targets: ["127.0.0.1:9090"] - job_name: node static_configs: - targets: ["127.0.0.1:9100"] - job_name: vacuum static_configs: - targets: ["127.0.0.1:9101"] - job_name: archive static_configs: - targets: ["127.0.0.1:8403"]
01
Цели
prometheus — 9090, node — node_exporter, vacuum — exporter.py, archive — red.py serve
02
Хранение
каталог data: блоки по 2 ч, 15 суток по умолчанию, 1–2 байта на отсчёт
03
Проверка
promtool check config — до запуска, вместе с правилами
04
Правила
rules.yml: доли ошибок и оповещения — часть 7
Для физика
10 000 рядов при опросе раз в 15 с — 5,76·10⁷ отсчётов и около 86 МБ в сутки.
Изложение. Конфигурация Prometheus перечисляет интервалы опроса и вычисления правил, файлы правил и задания опроса с целями. Конфигурация стенда опрашивает раз в 5 с четыре цели на одной машине: сам Prometheus на порту 9090, node_exporter — экспортёр Prometheus, выставляющий метрики машины, — на порту 9100, экспортёр вакуумной системы на порту 9101 и модель архива на порту 8403. Каждая цель получает метки job и instance — имя задания и адрес. Утилита promtool из поставки Prometheus позволяет проверить конфигурацию вместе с файлами правил до запуска. Опрошенные ряды Prometheus записывает в локальную базу временных рядов: свежие отсчёты хранит в памяти и в журнале упреждающей записи, а каждые два часа сбрасывает их на диск сжатым блоком, который затем объединяется с соседними. По умолчанию данные хранятся 15 суток, а сжатый отсчёт занимает в среднем 1–2 байта. Оценим объём для установки, службы которой выставляют 10 000 рядов при опросе раз в 15 с. За сутки каждый ряд получает 86 400 / 15 = 5760 отсчётов, все ряды — 5,76·10⁷, что при 1,5 байта на отсчёт составляет около 86 МБ в сутки и 1,3 ГБ за 15 суток. Для хранения в течение лет Prometheus передаёт данные во внешнее хранилище параметром remote_write, например в VictoriaMetrics или Thanos.
Демонстрация:
Prometheus и цели
01
promtool check config prometheus.yml
03
curl -s 127.0.0.1:9090/api/v1/targets | jq -r -f targets.jq
04
promtool query instant http://127.0.0.1:9090 up
Что наблюдается
Четыре цели up; через минуту после старта — 39,2 и 0,8 запроса/с, p99 0,46 с, два ряда давления.
root@lab — запись со стенда
Демонстрация. Перед демонстрацией сценарий start.sh запустил в фоне node_exporter, экспортёр вакуумной системы и модель архива red.py serve, которая обслуживает 40 запросов в секунду. Первая команда проверяет конфигурацию: файл корректен, найден один файл правил, в нём 10 правил. Сценарий prom.sh запускает Prometheus в фоне через nohup с конфигурацией, каталогом данных data и адресом 127.0.0.1:9090 и печатает номер процесса. Через 15 с запрос к программному интерфейсу /api/v1/targets с фильтром targets.jq выводит для каждой цели задание, состояние и длительность последнего опроса: все четыре цели в состоянии up, опрос экспортёра вакуума занимает 33 мс — три чтения датчиков по 10 мс, — а модели архива 0,5 мс. Запрос up утилитой promtool возвращает четыре ряда со значением 1 и метками instance и job. Сценарий q.sh находит короткое имя выражения в файле queries.txt, печатает выражение и выполняет его. Запрос rate даёт 39,2 запроса в секунду с кодом 200 и 0,8 — с кодом 500, вместе ровно 40, а доля ошибок — 2 %. Запрос выполнен через минуту после запуска Prometheus намеренно: функция rate делит приращение счётчика на длину всего окна [1m], и пока окно заполнено отсчётами лишь частично, результат занижен. Запрос p99 по гистограмме даёт 0,46 с — так же, как демонстрация RED в шестой части. Запрос vac сворачивает давление по метке датчика и возвращает два ряда: VG03 в последнем опросе не ответил, и его ряда нет.
PromQL:
rate, sum by, квантиль
up == 0 # цель не ответила sum by (code) (rate(archive_requests_total[5m])) sum(rate(archive_requests_total{code=~"5.."}[5m])) / sum(rate(archive_requests_total[5m])) histogram_quantile(0.99, sum by (le) (rate(archive_request_duration_seconds_bucket[5m]))) max_over_time(vacuum_pressure_pascals[10m]) predict_linear(node_filesystem_avail_bytes[6h], 4 * 3600) < 0 time() - calib_last_success_timestamp_seconds
01
rate
скорость в секунду за окно, сброс учтён
02
Окно
несколько интервалов опроса; rate делит на длину окна
03
sum by
свёртка по экземплярам: доля ошибок службы
04
max_over_time
пики при крупном шаге графика
05
predict_linear
линейная регрессия: заполнение диска через 4 ч
Изложение. Данные Prometheus читают языком запросов PromQL. Селектор archive_requests_total{code=~"5.."} выбирает ряды по имени и меткам, диапазон [5m] превращает каждый ряд в набор отсчётов за пять минут, а функции и операторы агрегации сводят эти наборы в числа. Выражение up == 0 возвращает цели, не ответившие на последний опрос. Функция rate вычисляет среднюю скорость роста счётчика в секунду за окно с поправкой на сбросы и экстраполяцией к границам окна; окно необходимо выбирать длительностью не менее нескольких интервалов опроса, чтобы в него попадало несколько отсчётов. Оператор sum by сворачивает ряды по выбранным меткам: число ответов с ошибкой интересно по службе в целом, а не по каждому из сотен экземпляров, и отношение двух сумм даёт долю ошибок службы — так записывают индикатор уровня обслуживания. Четвёртое выражение вычисляет p99 длительности по всем экземплярам. Для графиков, где важны пики, применяют max_over_time, а не усреднение: среднее за окно сглаживает короткий выброс, что рассмотрено в шестой части. Функция predict_linear строит линейную регрессию по отсчётам окна и продолжает её на заданное время вперёд: выражение на слайде находит разделы, которые при текущем темпе заполнятся за четыре часа. Последнее выражение вычисляет время с последней успешной калибровки; метрика содержит момент события, а прошедшее время вычисляет запрос.
Метрики
заданий
reg = CollectorRegistry() last = Gauge("calib_last_success_timestamp_seconds", "Последняя успешная калибровка", registry=reg) chans = Gauge("calib_channels", "Число откалиброванных каналов", registry=reg) chans.set(calibrate()) # минуты работы last.set_to_current_time() write_to_textfile("tf/calib.prom", reg) # внутри: временный файл и os.rename
01
Задание
итог исчезает с процессом раньше опроса
02
textfile collector
файл *.prom в каталоге node_exporter
03
Атомарность
временный файл и переименование
04
Оповещение
нет успеха дольше 26 ч
Пояснение
Время последнего успеха выявляет ошибку, незапуск и зависание; выключенную машину — оповещение по up node_exporter.
Изложение. Ночная калибровка датчиков положения пучка или расчёт на кластере работают минуты и завершаются, и результат, известный только в момент завершения, исчезает вместе с процессом раньше, чем Prometheus успевает его опросить. Итог такого задания записывают в файл формата экспозиции в каталог, который читает node_exporter; каталог задаёт параметр --collector.textfile.directory. Файл необходимо писать во временный и затем переименовывать, иначе экспортёр может прочитать его наполовину; функция write_to_textfile библиотеки prometheus_client обеспечивает это сама. Главная метрика задания — момент последнего успешного завершения. Оповещение по ней формулируется как отсутствие успеха дольше заданного срока, например 26 ч для ежесуточного задания, и срабатывает, если задание завершилось ошибкой, не запустилось или зависло. Счётчик ошибок задания такого не обеспечивает: незапущенное задание ошибок не сообщает. Выключенную машину это правило не обнаруживает: вместе с ней останавливается node_exporter, ряд метрики пропадает, и выражение оповещения не возвращает ничего; для этого случая необходимо отдельное оповещение — по ряду up самого node_exporter или по выражению с функцией absent, которое срабатывает при отсутствии ряда. Второй способ — сервер Pushgateway, которому задание отправляет метрики перед завершением, с его ограничениями, рассмотренными на слайде об опросе и отправке.
Демонстрация:
метрики задания
02
curl -s 127.0.0.1:9100/metrics | grep '^calib_'
Что наблюдается
Файл tf/calib.prom: три метрики, формат подтверждён разборщиком. node_exporter отдаёт те же ряды. С последнего успеха — 28,3 ч: больше 26 ч, оповещение CalibrationStale сработает.
root@lab — запись со стенда
Демонстрация. Сценарий calibjob.py моделирует ночное задание калибровки датчиков положения пучка: оно работает 0,2 с и записывает итог в файл tf/calib.prom функцией write_to_textfile — время последнего успешного завершения, длительность и число откалиброванных каналов, 40. Сценарий проверяет файл разборщиком формата prometheus_client: разобрано три семейства метрик. Каталог tf — тот, который node_exporter стенда читает как textfile collector, поэтому вторая команда находит в ответе node_exporter те же три ряда; экспортёр упорядочивает их по имени и печатает целое число каналов без дробной части. Метка времени последнего успеха фиксирована, 1,791·10⁹ с — 03.10.2026 04:00 UTC, — чтобы вывод не зависел от момента записи. Сценарий q.sh выполняет запрос calib: максимум разности time() и метки, делённый на 3600. Получим 28,3 ч, то есть калибровка не завершалась успешно дольше 26 ч, и правило CalibrationStale из седьмой части при такой метке срабатывает. Это показательный случай: оповещение по времени последнего успеха выявило бы и задание, которое не запускалось вовсе.
Трассировки
Трасса и спан, заголовок traceparent, OpenTelemetry, сэмплирование и цена трассировки
05
Зачем наблюдать
Журналы
Метрики
Сбор и хранение
Трассировки
Дашборды
Оповещения
Изложение. Пятая часть посвящена трассировкам. Журнал описывает отдельные события, метрика — их число и распределение, но ни то, ни другое не показывает, на что ушло время конкретного медленного запроса, прошедшего через несколько служб. Этот вопрос решает трассировка: рассматриваются трасса и спан, передача контекста в заголовке traceparent, OpenTelemetry и цена трассировки.
Трасса
и спан
traceparent: 00-bdd640fb06671ad11c80317fa3b1799d-1a3d1fa7bc8960a9-01 00 версия bdd640fb06671ad11c80317fa3b1799d trace_id, 32 hex 1a3d1fa7bc8960a9 span_id, 16 hex 01 флаги: sampled
Пояснение
Идентификатор запроса связывает записи журналов, трасса — ещё и время участков; trace_id в журнале объединяет оба.
01
Трасса
дерево спанов одного запроса с общим trace_id
02
Спан
имя, начало и конец, атрибуты, события, статус
03
Родитель
спан той же трассы; у корня его нет
04
Span link
связь между трассами: отправка в очередь и обработка
05
traceparent
W3C Trace Context: контекст в заголовке HTTP
Изложение. Трасса представляет собой дерево операций, выполненных при обработке одного запроса. Каждая операция — спан — имеет имя, моменты начала и конца, атрибуты, например номер запуска или размер ответа, события и статус. Спан несёт три идентификатора: собственный span_id, идентификатор трассы trace_id, общий для всех спанов запроса, и идентификатор родительского спана той же трассы; у корневого спана родителя нет. Связь между разными трассами, например между отправкой сообщения в очередь и его обработкой другой службой, оформляют отдельно — ссылкой, span link. Трассу отображают водопадом: полосами спанов на оси времени с отступами по глубине вложенности. Контекст трассы передаётся между службами вместе с запросом. Рекомендация W3C Trace Context определяет HTTP-заголовок traceparent из четырёх полей: версии, 32 шестнадцатеричных цифр trace_id, 16 цифр span_id вызывающего спана и флагов, младший бит которых, sampled, означает, что вызывающая сторона, возможно, записала данные трассы. Служба, получившая запрос, извлекает контекст из заголовка и открывает свои спаны как потомков вызывающего. Идентификатор запроса в журналах решает похожую задачу, но связывает только записи; трасса показывает ещё и время каждого участка. Запись trace_id текущего спана в каждую строку журнала позволяет по идентификатору из медленной трассы найти все записи этого запроса во всех службах.
Open
Telemetry
provider = TracerProvider(resource=Resource.create( {"service.name": "gateway"})) provider.add_span_processor(BatchSpanProcessor(exp)) tracer = provider.get_tracer("gateway") with tracer.start_as_current_span("GET archive", kind=SpanKind.CLIENT): inject(headers) # traceparent исходящего запроса # архив: родитель — клиентский спан шлюза with tracer.start_as_current_span("GET /runs/{id}", context=extract(self.headers)): ...
Пояснение
opentelemetry-instrument и eBPF-инструментация OBI работают без изменения кода, но контекст переносит код или библиотека.
01
API и SDK
библиотеки зависят от API, программа настраивает SDK
02
Пропагатор
по умолчанию — заголовок W3C traceparent
03
OTLP и Collector
передача, обработка и пересылка сигналов
04
Не хранилище
трассы хранят Jaeger, Tempo, Zipkin
05
SDK для Python
трассы и метрики стабильны, журналы — в разработке
Изложение. OpenTelemetry является открытым проектом, который определяет программный интерфейс, API, и его реализацию, SDK, для трассировок, метрик и журналов, протокол передачи OTLP и Collector — промежуточную службу приёма, обработки и пересылки сигналов. Хранилищем OpenTelemetry не является: трассы принимают системы Jaeger, Grafana Tempo, Zipkin и другие, и смена хранилища не требует изменения кода. Разделение API и SDK позволяет библиотекам зависеть только от API, а настраивать SDK — провайдер, процессор и экспортёр — один раз в программе, которая их использует. В SDK для Python трассировки и метрики имеют статус стабильных, а журналы находятся в разработке. Код на слайде повторяет устройство демонстрации, кроме процессора. Провайдер с атрибутом service.name создаётся при старте службы, а пакетный процессор BatchSpanProcessor передаёт законченные спаны экспортёру из фонового потока; демонстрация вместо него применяет синхронный SimpleSpanProcessor с экспортёром в память, чтобы напечатать водопад сразу после запроса. Шлюз открывает клиентский спан вызова архива, и функция inject записывает его контекст в заголовки исходящего запроса. Архив функцией extract восстанавливает контекст из заголовков входящего запроса, и его спан получает родителем клиентский спан шлюза. Автоматическая инструментация библиотек urllib, requests и httpx открывает клиентский спан и вызывает inject сама, а команда opentelemetry-instrument подключает её без изменения кода; с 2025 года развивается и инструментация на eBPF, OBI, которая регистрирует HTTP-вызовы без участия программы. Однако перенос контекста от входящего запроса к исходящим всё равно необходимо обеспечить кодом или библиотекой-инструментацией, иначе спаны не свяжутся в одну трассу.
Демонстрация:
трасса шлюз → архив
02
python3 tracing.py sampling
Что наблюдается
Записи шлюза и архива — с одним trace=bdd640fb; traceparent несёт trace_id и span_id клиентского спана шлюза. Водопад: запрос 55 мс, из них чтение HDF5 — 35 мс. Доли 1; 0,1 и 0,01 сохраняют 10 000, 1000 и 98 трасс из 10 000.
root@lab — запись со стенда
Демонстрация. Сценарий tracing.py поднимает две службы на 127.0.0.1: шлюз на порту 8401 проверяет права около 5 мс и запрашивает архив, архив на порту 8402 читает метаданные из базы около 10 мс и файл HDF5 около 30 мс. Идентификаторы трасс и спанов генерируются с фиксированным зерном, поэтому trace_id совпадает с листингом главы. Первая команда отправляет один запрос. Журналы обеих служб пишут сокращённый trace_id, и обе записи — «запуск 42 прочитан» и «ответ архива: 6 байт» — содержат trace=bdd640fb, хотя созданы разными службами. Заголовок traceparent запроса к архиву содержит версию 00, 32 цифры trace_id, 16 цифр span_id клиентского спана шлюза «GET archive» и флаги 01. Водопад, построенный по законченным спанам, показывает дерево из шести спанов с длительностями, округлёнными до 5 мс: запрос к шлюзу — 55 мс, проверка прав — 5 мс, вызов архива и обработка в архиве — по 45 мс, запрос к базе — 10 мс, чтение HDF5 — 35 мс начиная с 15-й миллисекунды. Вторая команда показывает сэмплирование в голове: сэмплер TraceIdRatioBased при долях 1; 0,1 и 0,01 сохраняет 10 000, 1000 и 98 трасс из 10 000, а дочерний спан получает то же решение, что и корень, поскольку сэмплер вычисляет его по тому же trace_id. Решение по значению trace_id обеспечивает согласованность: все службы, получившие тот же контекст, принимают одинаковое решение.
Результат:
водопад трассы
Что наблюдается
Запрос — 52,8 мс; проверка прав — 5,1 мс, вызов архива — 47,4 мс. В архиве 11,2 мс занимает запрос к базе и 34,8 мс, 66 % всего запроса, — чтение HDF5. Сеть между службами — меньше миллисекунды.
Демонстрация. Рисунок построен по спанам того же прогона, что и запись; синие полосы относятся к шлюзу, оранжевые — к архиву, ось — время от начала запроса в миллисекундах. Запрос к шлюзу длится 52,8 мс. Проверка прав занимает первые 5,1 мс, затем шлюз открывает клиентский спан вызова архива длительностью 47,4 мс. Спан архива начинается на 0,6 мс позже клиентского спана и заканчивается раньше его на 0,2 мс: это время передачи запроса и ответа по петлевому интерфейсу. Внутри архива запрос к базе метаданных длится 11,2 мс, а чтение файла HDF5 — 34,8 мс, то есть 66 % времени всего запроса. В модели паузы заданы как 10 и 30 мс; на стенде виртуальная машина выдерживает их с превышением до 5 мс. Таким образом, ускорять необходимо чтение файла, а не шлюз и не сеть, и этот вывод получен по одной трассе без отдельных замеров каждого участка. Нормой длительности участка служит не интуиция, а его обычное распределение и цель уровня обслуживания.
Цена
трассировки
В голове, head В хвосте, tail
решение при создании корневого спана в Collector по всей трассе
цена почти нулевая буфер всех спанов на время решения
ошибки, медленные с той же вероятностью сохраняются все
согласованность TraceIdRatioBased по trace_id одно решение на трассу
Важно
Наблюдение не должно останавливать службу: ограниченный буфер, отбрасывание вместо блокировки, тайм-аут отправки.
01
Очередь
BatchSpanProcessor: 2048 спанов, OTEL_BSP_MAX_QUEUE_SIZE
02
Переполнение
старые спаны вытесняются, служба работает
03
Профили
четвёртый сигнал: Pyroscope; py-spy — лекция 20
Изложение. Каждый спан требует памяти, времени на запись атрибутов и передачи в хранилище, и при большой нагрузке сохраняют только часть трасс. При сэмплировании в голове решение принимается при создании корневого спана. Сэмплер TraceIdRatioBased принимает его детерминированно по значению trace_id, поэтому все службы, получившие тот же контекст, принимают одинаковое решение, а сэмплер по умолчанию, ParentBased с корневым AlwaysOn, сохраняет все корневые спаны и для остальных повторяет решение родителя. В спецификации OpenTelemetry сэмплер TraceIdRatioBased объявлен устаревшим в пользу составного ProbabilitySampler, однако в SDK для Python 1.45 составные сэмплеры находятся в экспериментальном модуле. Такой способ почти не требует затрат, но решение принимается до того, как известен исход запроса, и редкие ошибочные запросы сохраняются с той же вероятностью, что и обычные. При сэмплировании в хвосте решение принимается по всем или большинству спанов трассы, обычно процессором tail sampling в Collector: он сохраняет все трассы с ошибками и медленные, а из остальных — заданную долю, но для этого ему необходимо буферизовать спаны на время принятия решения. Наблюдение не должно нарушать работу самой службы. Процессор BatchSpanProcessor передаёт спаны пакетами из ограниченной очереди — по умолчанию 2048 спанов, параметр OTEL_BSP_MAX_QUEUE_SIZE, — и при её переполнении вытесняет из очереди самые старые спаны, а программа продолжает работу. Тот же принцип применяют к журналам и метрикам: недоступность хранилища не должна останавливать службу. К трём сигналам добавляют четвёртый — профили: непрерывное сэмплирующее профилирование показывает, какие функции расходуют процессорное время в работающей службе; профилировщик py-spy применяется к медленной службе в лекции 20.
Дашборды и методы
RED, USE и золотые сигналы, правила построения панелей, ловушки восприятия
06
Зачем наблюдать
Журналы
Метрики
Сбор и хранение
Трассировки
Дашборды
Оповещения
Изложение. Шестая часть посвящена панелям с графиками. Набор панелей одной системы называют дашбордом, и стандартным инструментом их построения является Grafana. Метрик у системы сотни, графиков на экране — десятки, поэтому метрики отбирают по методу: рассматриваются методы RED и USE, правила построения панелей и систематические ошибки восприятия.
RED, USE
и золотые сигналы
Метод Объект Сигналы
RED, Т. Уилки, 2015 службы с запросами rate, errors, duration
USE, Б. Грегг, 2012 процессор, память, диск, сеть utilization, saturation, errors
золотые сигналы SRE любая служба задержка, трафик, ошибки, насыщение
Для физика
Служба архива — RED; процессор, память и диск машины, на которой она работает, — USE.
01
Utilization
доля времени, когда ресурс занят
02
Saturation
очередь работы: PSI в /proc/pressure, procs_running
03
Errors
ошибки ресурса: диск, сеть, OOM
04
Порядок
три графика RED на службу, три вопроса USE на ресурс
Изложение. Для служб, обрабатывающих запросы, применяют метод RED, предложенный Т. Уилки в 2015 году: частота запросов, доля ошибок и распределение длительности. Для ресурсов — процессора, памяти, дисков, сети — применяют метод USE Б. Грегга: загрузка, utilization, — доля времени, в течение которого ресурс занят; насыщение, saturation, — объём работы, которую ресурс не успевает выполнить и ставит в очередь; ошибки ресурса. Насыщение процессора представляет собой очередь готовых к выполнению задач: в Linux её показывают число процессов в состоянии выполнения procs_running и механизм PSI в /proc/pressure, сообщающий долю времени, в течение которого задачи ожидали процессор, память или ввод-вывод. Средняя загрузка, load average, для этого хуже: в Linux она учитывает и процессы, ожидающие диск. Загрузка диска, близкая к 100 %, для массива из нескольких дисков или твердотельного накопителя с параллельными очередями ещё не означает предела, поэтому насыщение оценивают по длине очереди и времени ожидания. Четыре золотых сигнала книги Google SRE — задержка, трафик, ошибки и насыщение — объединяют оба взгляда: это метод RED, дополненный насыщением. Метод обеспечивает порядок анализа: для каждой службы сначала просматривают три графика RED, для каждого ресурса машины — три вопроса USE, и только затем переходят к остальным метрикам. Метрики USE для машины выставляет node_exporter.
Демонстрация:
RED по метрикам
Что наблюдается
За 5,0 с — 200 запросов. R: 40,0 запроса/с. E: 4 ответа 500, 2,0 %. D: p50 18 мс, p95 25 мс, p99 460 мс. Холодное чтение длится 310 мс: p99 завышен корзиной от 0,3 до 0,5 с, а доля быстрее 0,3 с, 95,0 %, точна.
root@lab — запись со стенда
Демонстрация. Сценарий red.py поднимает модель службы архива на порту 8403. Служба инструментирована счётчиком archive_requests_total с меткой кода ответа и гистограммой archive_request_duration_seconds с границами от 10 мс до 1 с, среди которых есть граница 0,3 с — порог цели. Обычный запрос длится 15 мс, каждый двадцатый читает холодный файл 310 мс, каждый пятидесятый завершается ошибкой 500. Генератор нагрузки отправляет 40 запросов в секунду по расписанию в течение 5 с. Отчёт построен только по двум снимкам /metrics — до и после нагрузки, — так же, как это сделал бы Prometheus функцией rate: разность счётчиков делится на время между снимками. За 5,0 с обработано 200 запросов, частота — 40,0 запроса в секунду. Ответов 500 — четыре, доля ошибок — 2,0 %. Длительность по гистограмме: p50 — 18 мс, p95 — 25 мс, p99 — 460 мс. Холодное чтение на самом деле длится 310 мс; оценка p99 завышена, поскольку пять процентов холодных чтений попадают в корзину от 0,3 до 0,5 с, а интерполяция распределяет их по корзине равномерно. Доля запросов быстрее 0,3 с, 95,0 %, вычислена по границе корзины без интерполяции и точна. Три числа RED, следовательно, описывают службу целиком, а точность третьего определяется границами корзин.
Правила
дашборда
01
Сверху вниз
от функции для пользователя к компонентам
02
Слева направо
по пути запроса: шлюз, служба, база
03
Заголовок
вопрос, единицы и окно усреднения
04
Шкала
логарифмическая для величин на порядки
05
Топ-10
topk вместо сотни рядов
06
Цвет
красный и зелёный — только для состояния
07
No data
исправить или удалить
натекание 5.0e-05 Па на 3 мин, порог защиты 1.0e-05 Па шаг среднее максимум выше порога 1 мин 5.0e-05 5.0e-05 да 5 мин 3.0e-05 5.0e-05 да 1 ч 2.7e-06 5.0e-05 нет 1 сут 3.0e-07 5.0e-05 нет
Важно
Среднее за час опускает трёхминутное натекание ниже порога; max_over_time сохраняет пик.
Изложение. Каждая панель отвечает на определённый вопрос, и вопрос этот записывают в её заголовок вместе с единицами и окном усреднения. Панели располагают сверху вниз — от функции, которую видит пользователь, к компонентам, — и слева направо по пути запроса: шлюз, служба, база. Величину, меняющуюся на порядки, показывают в логарифмическом масштабе: давление в вакуумной камере меняется от 10⁻⁷ Па в норме до 10⁻⁴ Па при натекании, и на линейной шкале значения рабочего диапазона неотличимы от нуля. Из сотни рядов показывают десять наибольших функцией topk, а не все. Цвет используют сдержанно: красный и зелёный — только для состояния, причём с дублированием формой или подписью, поскольку красно-зелёное нарушение цветовосприятия встречается примерно у 8 % мужчин. Панель, которая постоянно показывает «No data», либо исправляют, либо удаляют: она приучает не доверять остальным. Отдельный источник ошибок представляет собой окно усреднения. Рассмотрим сутки записи вакуумметра с фоном около 2·10⁻⁷ Па и натеканием, при котором давление на 3 минуты поднимается до 5·10⁻⁵ Па при пороге защиты 10⁻⁵ Па; расчёт выполнен сценарием smoothing.py. График с шагом 1 мин показывает пик полностью, со средним за 5 мин — 3,0·10⁻⁵ Па, а со средним за 1 ч — 2,7·10⁻⁶ Па, то есть ниже порога; за сутки одной точкой — 3,0·10⁻⁷ Па. Максимум за шаг во всех случаях сохраняет 5,0·10⁻⁵ Па. Grafana увеличивает шаг при выборе более длинного интервала, поэтому одна и та же панель за час и за неделю может давать противоположные выводы о превышении порога.
Что не так
с панелями
Пояснение
Каждая из четырёх панелей модели нарушает одно из правил предыдущего слайда.
Изложение. Четыре панели построены по модельным данным, и каждая нарушает одно из правил предыдущего слайда; их удобно разобрать вместе с аудиторией, прежде чем называть ответы. Панель A «Задержка p99» пуста и показывает «No data»: запрос к несуществующей метрике или выключенный экспортёр. Такая панель приучает не доверять дашборду, а ночью вынуждает дежурного выяснять, что она показывала раньше; её исправляют или удаляют. Панель B «Доступность, %» залита красным, хотя все значения лежат между 99,95 и 99,99 %, а ось начинается с 99,8: красный цвет предназначен для состояния «плохо», а не для любого графика, и без порога цели по такой панели нельзя сказать, хорошо ли это. Панель C «Давление» содержит двенадцать линий без легенды и без единиц, а масштаб 1e−7 указан только в углу оси: определить, какой датчик растёт, невозможно; необходимы подпись датчиков, единицы, логарифмическая шкала или десять наибольших рядов. Панель D «Ошибки, %» показывает долю ошибок, усреднённую с 1-го числа месяца: часовой всплеск до 12 % на 17-е сутки виден лишь как ступенька около 0,03 %, так как окно усреднения в сотни раз длиннее события.
Ловушки
восприятия
01
Привязка
«в прошлый раз было то же самое»: поиск знакомой причины вместо новой
02
Доступность
яркий недавний отказ кажется частым
03
Ложная корреляция
всплески на соседних панелях: совпадение или общая причина
Пояснение
Книга Google SRE, глава «Effective Troubleshooting», называет первую и третью из этих ловушек; приёмы их обхода — лекция 20.
Изложение. Человек, просматривающий панели во время отказа, подвержен тем же систематическим ошибкам мышления, что и в любой другой работе. Привязка к прошлому случаю — «в прошлый раз было то же самое» — заставляет искать знакомую причину и не замечать новой. Эвристика доступности переоценивает частоту ярких, недавно пережитых отказов: инцидент, который восстанавливали двое суток, кажется типичным, хотя случился один раз. Совпадение всплесков на соседних панелях воспринимается как причинная связь, хотя оба графика могут отражать третью причину или расходиться на минуты, незаметные при мелком масштабе. Первую и третью из этих ловушек — привязку к причинам прошлых проблем и поиск ложных корреляций, которые на деле являются совпадениями или следствиями общей причины, — книга Google SRE называет среди типичных ошибок в главе об устранении неисправностей; эвристики доступности в этом перечне нет. Способы их обхода — запись гипотез и их проверка по одной — рассмотрены в лекции 20.
Результат:
панели RED со стенда
Что наблюдается
В эпизоде замедления базы ошибки растут с 2 до 5 %, p50 — с 18 до 153 мс, а p99 остаётся 460 мс: хвост задают холодные чтения 310 мс, и замедления p99 не показывает.
Демонстрация. Рисунок построен по данным Prometheus стенда, полученным в отдельном прогоне модели архива red.py serve 700 incident: служба обслуживала 40 запросов в секунду, а с 240-й по 360-ю секунду работы моделировалось замедление базы архива — обычный запрос длился 120 мс вместо 15, а ошибкой завершался каждый двадцатый запрос вместо пятидесятого. Prometheus опрашивал службу раз в 5 с; панели построены запросами query_range с шагом 5 с за 10 минут, все скорости — rate за одну минуту. Левая панель показывает частоту по кодам ответа в логарифмической шкале: код 200 — 39,2 запроса в секунду, в эпизоде около 38, код 500 — 0,8 запроса в секунду, в эпизоде 2,0. Сумма остаётся равной 40: нагрузка не изменилась, изменилась доля ошибок. Средняя панель — доля ответов 500: 2 % вне эпизода и 5 % в нём; пунктир отмечает бюджет цели 99,9 %, то есть 0,1 %, и модель расходует бюджет в 20 раз быстрее допустимого всё время, а в эпизоде — в 50 раз. Правая панель — длительность по гистограмме: p50 вне эпизода 18 мс, в эпизоде 153 мс — оценка внутри корзины от 0,1 до 0,2 с при настоящих 120 мс, — а p99 всё время равен 460 мс. Хвост распределения определяют холодные чтения длительностью 310 мс, каждый двадцатый запрос, которые попадают в корзину от 0,3 до 0,5 с, и замедление обычных запросов до 120 мс в квантиль 0,99 не попадает. Панель одного p99 этот эпизод не обнаружила бы, как и доля запросов быстрее 300 мс: 120 мс меньше порога. Замедление большинства запросов показывает медиана, поэтому на панели длительности выводят несколько квантилей. Отметим и края эпизода: на рисунке он занимает примерно с 2,6-й по 5,5-ю минуту, хотя длился две минуты, — окно rate в одну минуту растягивает каждый край на минуту.
Оповещения
SLO на практике, скорость расходования бюджета, правила Prometheus и их тесты, дежурство
07
Зачем наблюдать
Журналы
Метрики
Сбор и хранение
Трассировки
Дашборды
Оповещения
Изложение. Седьмая часть посвящена оповещениям. Панели просматривают, когда уже известно, что что-то не так; узнать об этом позволяет оповещение. Рассматриваются индикаторы и цели уровня обслуживания на практике, скорость расходования бюджета ошибок, правила Prometheus и их тесты, дежурство и наблюдаемость физической установки.
SLI, SLO и SLA
на практике
01
SLI
доля хороших событий среди всех
02
SLO
цель на скользящем окне, обычно четыре недели
03
SLA
договор, мягче SLO на доли процента
04
Бюджет
1 − SLO: при 99,9 % — 0,1 % запросов
sum(rate(archive_requests_total{code!~"5.."}[28d])) / sum(rate(archive_requests_total[28d]))
Для физика
Доля выдач запуска быстрее 300 мс; доля минут, когда последний отсчёт в архиве не старше 10 с, — свежесть данных.
Пояснение
Определения, девятки и бюджет ошибок — лекция 14 и глава «Системный дизайн».
Изложение. Определения индикатора, цели и соглашения об уровне обслуживания, бюджета ошибок и девяток даны в лекции 14 и в главе «Системный дизайн»; здесь они применяются к оповещениям. Индикатор достаточно записать как долю хороших событий среди всех: долю ответов без ошибки сервера, долю выдач запуска быстрее 300 мс, долю минут, в течение которых последний отсчёт в архиве не старше 10 с. Выражение PromQL на слайде вычисляет первый из них за 28 суток. Последний индикатор, свежесть данных, для системы сбора данных часто важнее задержки ответа: архив, быстро выдающий данные часовой давности, для смены установки не работает. Цель задают на скользящем окне, обычно четыре недели, а пятиминутные окна применяют не для цели, а для оценки скорости расходования бюджета в оповещениях. Внешнее соглашение, если оно есть, делают мягче внутренней цели на доли процента, например цель 99,95 % при соглашении 99,9 %, чтобы нарушение цели обнаруживалось раньше нарушения соглашения. Разность между единицей и целью является бюджетом ошибок: при 99,9 % это 0,1 % запросов за окно.
Скорость расходования
бюджета
скорость = доля ошибок / (1 − SLO) 1 — бюджет закончится к концу окна SLO 2 % ошибок при SLO 99,9 % — скорость 20 вызов: > 14,4 в окнах 1 ч и 5 мин вызов: > 6 в окнах 6 ч и 30 мин заявка: > 1 в окнах 3 сут и 6 ч обнаружение: t > k · (1 − SLO) · W / e e = 2 %, k = 14,4, W = 60 мин: t > 43,2 мин
01
14,4 за час
2 % месячного бюджета: вызов дежурного
02
Короткое окно
1/12 длинного: оповещение снимается вскоре после конца
03
Ниже 1,44 %
правило 1 ч не сработает: остаётся правило 6 ч
Пояснение
Пороги — SRE Workbook, таблица 5-8; их происхождение — глава «Системный дизайн».
Изложение. Оповещение по мгновенной доле ошибок либо срабатывает на каждый кратковременный всплеск, либо, при высоком пороге, пропускает долгий отказ средней силы. Книга SRE Workbook предлагает оповещать по скорости расходования бюджета ошибок — отношению доли ошибок в окне к допустимой доле, единица минус цель. Скорость 1 означает, что бюджет будет израсходован ровно к концу окна цели, а скорость 14,4 при окне цели 30 дней — что за час расходуется 2 % месячного бюджета. При 2 % ошибок и цели 99,9 % скорость равна 20. Рекомендуемая конфигурация для цели 99,9 % вызывает дежурного при скорости больше 14,4 в окнах 1 ч и 5 мин или больше 6 в окнах 6 ч и 30 мин и заводит заявку при скорости больше 1 в окнах 3 сут и 6 ч. Короткое окно составляет 1/12 длинного и необходимо для того, чтобы оповещение снималось вскоре после окончания инцидента, а не сохранялось, пока длинное окно не очистится. Оценим время обнаружения. Пусть с момента t = 0 доля ошибок постоянна и равна e. В окне длительностью W при t < W средняя доля ошибок равна e·t/W, и правило с порогом k срабатывает, когда e·t/W > k·(1 − SLO). Для e = 2 %, k = 14,4 и W = 60 мин получим t > 43,2 мин; при полном отказе то же правило срабатывает менее чем через минуту, а при доле ошибок 1,44 % и ниже не срабатывает никогда — такой отказ оставлен правилу с окном 6 ч.
Демонстрация:
три инцидента
Что наблюдается
Час с 2 % ошибок: вызов в 02:43, снятие в 03:01. Семь часов с 0,8 %: вызов 6 ч/30 мин в 14:30, заявка в 16:30, снятие вызова в 17:07. Пять минут с 10 % — новых оповещений нет. Расход бюджета: 2,8, 7,8 и 1,2 %.
root@lab — запись со стенда
Демонстрация. Сценарий burnrate.py моделирует сутки работы архива при цели 99,9 % и нагрузке 10 запросов в секунду с тремя инцидентами: с 02:00 до 03:00 — 2 % ошибок, со скоростью расходования 20; с 10:00 до 17:00 — 0,8 %, со скоростью 8, медленное горение; с 20:00 до 20:05 — 10 %, со скоростью 100. Перед моделируемыми сутками заданы трое суток без ошибок, чтобы длинные окна были заполнены. Доли хранятся точными дробями, а суммы по окнам вычисляются через префиксные суммы. Первый инцидент вызывает дежурного в 02:43, через 43 минуты после начала, в согласии с оценкой предыдущего слайда: скорость в окне 1 ч равна 14,7, в окне 5 мин — 20. Оповещение снято в 03:01: в окне 1 ч скорость ещё 19,3, но в окне 5 мин опустилась до 12 — так работает короткое окно. Медленное горение правилу 14,4 не видно вовсе: его вызывает правило 6 ч и 30 мин в 14:30, через четыре с половиной часа, в 16:30 к нему добавляется заявка, а в 17:07 вызов снимается. В 14:29 скорость за 6 ч равна ровно 6, и условие «больше 6» ещё не выполнено; при вычислениях с плавающей точкой результат такого сравнения зависел бы от последнего знака. Пятиминутный всплеск 10 % не вызывает новых оповещений: вызова нет, а заявка медленного горения, заведённая в 16:30, остаётся открытой до конца моделируемых суток. Последние строки переводят инциденты в доли месячного бюджета, 25 920 неуспешных ответов: 2,8 %, 7,8 % и 1,2 %. Таким образом, всплеск израсходовал меньше бюджета, чем каждый из двух других инцидентов, и вызывать из-за него дежурного ночью нецелесообразно.
Результат:
расход бюджета за сутки
Что наблюдается
Окно 1 ч пересекает порог 14,4 только в первом инциденте, окно 6 ч — порог 6 при медленном горении; всплеск в 20:00 даёт 100 в окне 5 мин, но в окне 1 ч лишь 8,3.
Демонстрация. Рисунок построен по той же модели. Серая линия — скорость расходования в окне 5 мин, синяя — в окне 1 ч, фиолетовая — в окне 6 ч; ось скорости логарифмическая с нулём внизу. Пунктиром отмечены пороги 14,4 и 6, заливка показывает интервалы вызова дежурного: оранжевая — с 02:43 до 03:01 по правилу 1 ч и 5 мин, зелёная — с 14:30 до 17:07 по правилу 6 ч и 30 мин. В первом инциденте синяя линия поднимается к 20 и пересекает порог 14,4 за 17 минут до конца инцидента; вызов снимается по серой линии, которая падает сразу после 03:00. Во втором инциденте синяя и серая линии держатся у 8, ниже порога 14,4, а фиолетовая медленно растёт и пересекает порог 6 только в 14:30. Всплеск в 20:00 поднимает серую линию до 100, но синяя поднимается лишь до 8,3, а фиолетовая — ниже 6, и ни одно из двух правил вызова не срабатывает: каждое требует превышения сразу в двух окнах. Рисунок показывает назначение двух окон: длинное окно определяет, израсходована ли значимая часть бюджета, а короткое — продолжается ли расход сейчас.
Правило
и его тест
- record: archive:error_ratio:rate1h expr: sum(rate(archive_requests_total{code=~"5.."}[1h])) / sum(rate(archive_requests_total[1h])) - alert: ArchiveErrorBudgetBurnFast expr: archive:error_ratio:rate1h > 14.4 * 0.001 and archive:error_ratio:rate5m > 14.4 * 0.001 labels: severity: page annotations: summary: "Архив расходует бюджет ошибок быстрее 14.4 нормы" runbook_url: "https://wiki.example.org/runbooks/archive-errors"
01
Правило записи
ряд archive:error_ratio:rate1h — доля ошибок за час
02
Оповещение
expr, for, метка severity, summary и runbook_url
03
promtool test rules
синтетические ряды и ожидаемые оповещения
04
Ряды теста
0+600x180 — успешные; 0x60 12+12x119 — ошибки с 61-й минуты
05
Ожидание
95 мин: 1,15 % — без оповещения; 115 мин: 1,8 % — оповещение
Изложение. Prometheus вычисляет правила двух видов. Правило записи периодически вычисляет выражение и сохраняет результат как новый ряд; имя ряда по соглашению имеет вид «уровень:метрика:операции», где уровень обозначает метки, по которым агрегирован результат, а метрика — исходное имя без _total; в правилах стенда имя archive:error_ratio:rate1h — доля ошибок архива за час — сокращено: первая часть называет службу, а не уровень агрегации. Правило оповещения переводит оповещение в состояние ожидания, когда выражение возвращает непустой результат, и в состояние срабатывания, если результат сохраняется в течение интервала for; правило без for, как оповещение на слайде, срабатывает при первом же вычислении. Оповещение ArchiveErrorBudgetBurnFast на слайде записывает правило 14,4 в окнах 1 ч и 5 мин; метка severity определяет, куда направить сообщение, а аннотации содержат текст с порогом и ссылку на инструкцию. Правила, как и код, проверяют тестами. Утилита promtool проверяет синтаксис командой promtool check rules, а командой promtool test rules вычисляет правила на синтетических рядах и сравнивает сработавшие оповещения с ожидаемыми. Ряды задаются компактной записью: 0+600x180 означает 181 отсчёт от 0 с шагом 600, то есть 600 успешных запросов в минуту, а 0x60 12+12x119 — 61 нулевой отсчёт и затем рост на 12 в минуту. С 61-й минуты служба отвечает ошибкой на 12 запросов из 612, доля ошибок — 1,96 %. На 95-й минуте доля ошибок в окне 1 ч около 1,15 %, ниже порога 1,44 %, и оповещения нет, а на 115-й — около 1,8 %, и оповещение ожидается. Тест фиксирует это поведение и позволяет обнаружить изменение порога или выражения, нарушающее его, до выкатки правил, а не во время ночного отказа.
Демонстрация:
тесты правил
01
promtool check rules rules.yml
02
promtool test rules rules_test.yml
03
curl -s 127.0.0.1:9090/api/v1/rules | jq -r -f alerts.jq
Что наблюдается
10 правил корректны, тесты — SUCCESS. На стенде сработали три оповещения: оба правила бюджета архива — модель отвечает ошибкой на 2 % запросов, в 20 раз больше бюджета 0,1 %, — и CalibrationStale: калибровке больше 28 ч.
root@lab — запись со стенда
Демонстрация. Файл rules.yml содержит четыре правила записи долей ошибок архива за 5 мин, 30 мин, 1 ч и 6 ч и шесть правил оповещения: два правила скорости расходования бюджета архива, отказ экспортёра вакуума, давление выше 10⁻⁵ Па, прогноз заполнения диска за 4 ч и устаревшую калибровку. Первая команда находит все 10 правил. Вторая выполняет три группы тестов файла rules_test.yml — инцидент архива, отказ экспортёра на шестой минуте и раздел, теряющий 100 МБ в минуту, — и печатает SUCCESS: все ожидаемые оповещения сработали в заданные моменты, а лишних не было. Третья команда запрашивает у работающего Prometheus состояние правил оповещения, и фильтр alerts.jq печатает имя и состояние каждого. Сработали три оповещения. ArchiveErrorBudgetBurnFast и ArchiveErrorBudgetBurnSlow сработали потому, что модель архива red.py отвечает ошибкой на каждый пятидесятый запрос: 2 % ошибок — в 20 раз больше бюджета 0,1 % цели 99,9 %, — скорость 20 превышает оба порога. Это свойство модели, а не неисправность стенда, и правила поступают так, как должны: отказ средней силы, длящийся с момента запуска службы. Отношение двух rate за одно окно практически не зависит от того, заполнено ли окно [1h] отсчётами целиком: поправка на неполное окно одинакова для числителя и знаменателя, поэтому правило не ждёт часа. CalibrationStale сработало по фиксированной метке калибровки: в демонстрации метрик задания ей было 28,3 ч, а к этому запросу, выполненному после десяти минут работы стенда, — около 28,5 ч. Остальные три оповещения неактивны: экспортёр отвечает, давление около 2·10⁻⁷ Па, диск не заполняется. Правило давления оставлено на стенде как пример выражения с max_over_time: на установке тревогу по давлению обрабатывает система тревог управления, а не Prometheus, что рассмотрено на слайде о наблюдаемости установки.
Оповещение
и дежурство
01
Действие
каждое оповещение требует действия, иначе — панель или заявка
02
Текст
что, где, порог, значение и ссылка на инструкцию
03
Дежурство
основной и запасной, эскалация по таймеру
04
Рутина
автоматизировать: ротация журналов вместо уборки по оповещению
05
Учения
выключить экспортёр и убедиться, что сообщение дошло
Пояснение
Alertmanager группирует оповещения, подавляет зависимые, отключает их на время работ и направляет по меткам: page — на телефон, ticket — в заявки.
Важно
Нагрузка дежурного — не больше двух инцидентов за двенадцатичасовую смену; разбор инцидента — лекция 20.
Изложение. Для оповещения, которое вызывает дежурного, необходимы два условия: оно сообщает о симптоме, затрагивающем пользователей, и требует действия человека. Оповещение без необходимого действия приучает дежурного не реагировать на оповещения вообще, поэтому такие сигналы выводят на панель или в систему заявок. Текст оповещения сообщает, что произошло, где, при каком пороге и каково текущее значение, и содержит ссылку на инструкцию с первыми действиями. Дежурят по расписанию основной и запасной дежурные, и для каждого оповещения определена эскалация: если основной дежурный не подтвердил оповещение за заданное время, вызывается запасной, затем руководитель. Книга Google SRE ограничивает нагрузку двумя инцидентами за двенадцатичасовую смену: на разбор одного инцидента с последующим отчётом уходит в среднем около шести часов. Рутинная реакция, повторяющаяся из раза в раз, является поводом для автоматизации, а не для нового оповещения: если по оповещению о заполнении диска каждый раз удаляют старые журналы, ротацию настраивают так, чтобы оповещение не возникало. Сработавшие оповещения Prometheus передаёт службе Alertmanager, которая группирует оповещения одной природы, подавляет зависимые при срабатывании корневого, позволяет заранее отключить их на время плановых работ и направляет сообщения получателям по меткам. Цепочку оповещения проверяют целиком, как и резервную копию: на учениях выключают экспортёр и убеждаются, что сообщение дошло до дежурного.
Наблюдаемость
установки
Архив величин Мониторинг программ
хранит значения каналов состояние служб
точность исходная допустимо прореживание
срок годы недели, месяцы
тревоги оператору смены дежурному разработчику
пример EPICS Archiver Appliance Prometheus
Для физика
Главный индикатор системы сбора данных — свежесть: доля минут, когда последний отсчёт в архиве не старше 10 с.
01
Связь
vacuum_up: ответил ли контроллер
02
Цикл опроса
длительность и пропущенные циклы
03
Каналы
ошибки чтения, доля каналов без связи
04
Архив
отставание записи, заполнение дисков, predict_linear
Изложение. На ускорительном комплексе существуют две системы, которые обе называют мониторингом, но которые решают разные задачи. Архив физических величин — EPICS Archiver Appliance или база временных рядов системы медленного контроля, описанная в главе «Работа с базами данных», — хранит значения каналов системы управления для физического анализа: с исходной точностью, годами и с возможностью восстановить состояние установки в любой момент сеанса. Мониторинг программ — Prometheus с его оповещениями — хранит состояние служб: недели или месяцы, с допустимым прореживанием старых данных. Тревоги по физическим величинам, например давление выше порога или перегрев магнита, обрабатывает система тревог управления установкой и показывает оператору смены, а оповещения о состоянии программ направляются дежурному разработчику. Смешение этих систем приводит к ошибкам в обе стороны: каналы с частотой 10 Гц Prometheus не сохраняет без потерь, поскольку опрашивает цели раз в несколько секунд или реже, а срок хранения в недели для физики бесполезен; здоровье же программ в архиве каналов не обеспечено ни оповещениями, ни языком запросов для них. Метрики, которые программы системы управления выставляют для мониторинга, отвечают на вопросы о самих программах: есть ли связь с контроллером, сколько длится цикл опроса и сколько циклов пропущено, сколько ошибок чтения по каналам, насколько отстаёт запись в архив и когда заполнятся его диски. Свежесть данных является для системы сбора данных главным индикатором: доля минут, в течение которых последний отсчёт в архиве не старше 10 с, прямо отвечает на вопрос смены, видит ли она текущее состояние установки.
Итоги
лекции
01
Наблюдаемость сокращает обнаружение и локализацию отказа.
02
Журнал — строка JSON на событие с идентификатором, в stdout.
03
Событие — счётчиком, состояние — gauge, задержка — гистограммой.
04
Экспортёр читает источник при каждом запросе /metrics.
05
Границы корзин — на целях, метки — с ограниченными значениями.
06
Трасса показывает, на каком участке запрос провёл время.
07
Оповещение — по симптому и скорости расходования бюджета, с тестом.
Изложение. Подведём итоги. Наблюдаемость позволяет понять по внешним сигналам, что происходит внутри работающей системы, и тем самым сокращает время обнаружения и локализации отказа; восстановление же зависит от выкатки и отката. Журнал фиксирует события и пишется структурированно — одна строка JSON на событие с идентификатором запроса или цикла — в стандартный вывод, с порогом, который меняется без перезапуска. Метрика сводит события в числа: события считают счётчиком, текущее состояние выражают мгновенным значением, задержки — гистограммой. Данные внешних систем выставляет экспортёр, опрашивающий источник в момент запроса и сообщающий об отказе источника рядом _up. Границы корзин гистограммы выбирают по целям уровня обслуживания, а метки ограничивают по кардинальности. Prometheus опрашивает цели, хранит ряды и вычисляет выражения PromQL, а трассировка OpenTelemetry показывает путь отдельного запроса через службы и время на каждом участке. Наконец, панели строят по методам RED и USE, а оповещения вызывают дежурного только по симптомам и по скорости расходования бюджета ошибок, причём правила проверяются тестами так же, как код.
Упражнения
и задание
01
Журнал JSON
своей программы с идентификатором цикла и отбор записей jq
02
Экспортёр
для прибора или расчёта: collect(), _up, базовые единицы
03
Гистограмма
с границей на своей цели и сравнение p99 с точным
04
Правило и тест
скорость расходования бюджета и promtool test rules
05
Задание
Prometheus, node_exporter, панели USE и RED, оповещение при выключенном экспортёре
Для физика
Для установки — экспортёр контроллера или источника питания с рядом _up и временем последнего успешного опроса.
Пояснение
Конфигурация Prometheus стенда, фрагменты правил и их теста приведены в главе «Наблюдаемость».
Изложение. Упражнения служат самопроверке. Первое — перевести журнал собственной программы на одну строку JSON на событие с полем идентификатора цикла или запроса и отобрать утилитой jq записи одного цикла. Второе — написать экспортёр для прибора или расчёта своей группы с опросом в методе collect, рядом _up и базовыми единицами в именах и проверить его ответ командой curl. Третье — выбрать границы корзин гистограммы длительности собственной службы так, чтобы одна из них совпадала с целью уровня обслуживания, и сравнить оценку p99 с точным значением. Четвёртое — записать для своей службы правило оповещения по скорости расходования бюджета и тест к нему и проверить их командами promtool check rules и promtool test rules. Задание объединяет их: поднять Prometheus и node_exporter, построить панели USE для машины и RED для службы, затем остановить экспортёр и убедиться, что оповещение дошло.
Источники
3
Beyer B. et al. Site Reliability Engineering. – O'Reilly, 2016; The Site Reliability Workbook. – 2018
sre.google/books
4
Документация Prometheus: типы метрик, именование, гистограммы, экспортёры, Pushgateway, тесты правил
prometheus.io/docs
Изложение. Лекции соответствует глава книги «Наблюдаемость»; определения SLI, SLO и SLA, девятки и пороги скорости расходования бюджета введены в главе «Системный дизайн», основы модуля logging — в главе «От скрипта к приложению», архивы медленного контроля — в главе «Работа с базами данных». Порядок изложения частично повторяет лекции «Наблюдаемость» и «Прикладное программирование для SRE» Курсов информационных технологий 2024 года; пример экспортёра второй из них заменён экспортёром вакуумной системы. Книги Google о практике эксплуатации — Site Reliability Engineering и The Site Reliability Workbook — содержат правила оповещения по симптомам, таблицу порогов скорости расходования бюджета и главы о дежурстве. Документация Prometheus описывает типы метрик, именование, гистограммы, написание экспортёров, Pushgateway и тесты правил, документация OpenTelemetry — SDK для Python и сэмплирование, а рекомендация W3C Trace Context — заголовок traceparent. Методы USE и RED описаны их авторами, Б. Греггом и Т. Уилки, а отчёт Š. Davidovič разбирает, почему средние MTTR непригодны для оценки тренда.
Спасибо за внимание
Вопросы
phys-dev.github.io/soft-dev-book
Изложение. Лекция завершена. Вопросы по материалу удобно задавать, опираясь на главу «Наблюдаемость» и на демонстрации: все сценарии используют модельные данные с фиксированным зерном, а службы стенда слушают только адрес 127.0.0.1, поэтому любой результат можно повторить на собственной машине с Prometheus и node_exporter. Следующая лекция, «Поиск неисправностей», рассматривает действия при уже случившемся отказе: метод поиска, инструменты для работающего процесса, управление инцидентом и разбор его причин.