∿Метрики и
наблюдаемость
Главы темы 1
1.1

После развёртывания: что происходит с приложением

В главе описан сервис, на котором построена вся тема, и разобрано, какие сведения о программе разработчик теряет, когда она начинает работать на сервере. На двух ответах HTTP 200 видно, что успешный код ответа ничего не говорит о скорости.

Главное в главе

После сбоя разобрать его можно только по тем данным, которые сервис сохранил заранее.

Раздел
Основы наблюдаемости
Уровень
Начальный · Python знаком
Что понадобится
Понимание HTTP-запроса и ответа, основы FastAPI
Зачем это знать
Решать, какие данные о сервисе собирать до первой жалобы пользователя

§1Сервис products-api

Все главы первой темы опираются на один небольшой сервис.

products-api — веб-приложение на FastAPI. На запрос GET /products оно отвечает списком товаров. Настоящего каталога за ним нет: сервис возвращает число товаров и служебные поля. В этой теме разбирается, как сервис обрабатывает запрос и какие данные о себе оставляет.

Настоящие сервисы тормозят и падают в случайные моменты, и повторить сбой по заказу почти невозможно. Поэтому поведение сервиса стенда задаёт параметр scenario. У него три значения.

При значении fast обработчик ждёт зависимость около 60 мс и возвращает HTTP 200 примерно за 85 мс. При значении slow ожидание длится 3 секунды: ответ тот же, HTTP 200, но приходит он через 3 секунды. При значении error ожидание заканчивается исключением TimeoutError, и клиент получает HTTP 500.

Зависимостью называют внешнюю систему, от которой зависит ответ: базу данных, кэш, другой сервис. Базы данных у стенда нет, вместо обращения к ней обработчик ждёт через asyncio.sleep. Об этом сказано и в коде, и на странице стенда. Благодаря этому каждый сценарий можно повторить любое число раз и получить одинаковое поведение.

Кроме /products, у сервиса есть служебные адреса. Они понадобятся в этой главе и дальше.

АдресЧто возвращает
/labСтраница стенда: кнопки сценариев, ответ, запись о событии и этапы обработки
/healthКороткий ответ о том, что процесс работает
/eventsПоследние 2000 записей о запросах, которые процесс хранит в памяти
/metricsСчётчики и гистограмма в текстовом формате Prometheus
/docsДокументация Swagger, которую FastAPI строит по коду

Код сервиса, скрипт эксперимента и инструкция по запуску лежат в архиве telemetry-lab.zip ↓. Для чтения глав запускать его не обязательно: все снимки и измерения в тексте сделаны на этом сервисе.

Сервис описан. Следующий раздел напоминает, как ошибки в нём находят до развёртывания, на своём компьютере.

§2Поиск ошибки на своём компьютере

Пока программа запущена локально, отладка выглядит так: разработчик пишет обработчик, отправляет запрос из браузера или через Swagger и смотрит результат. У него есть четыре способа найти ошибку.

Первый способ — терминал: Uvicorn выводит в него каждый запрос и полный traceback при исключении. Второй — отладчик: в нём ставят точку останова, останавливают выполнение и смотрят значения переменных. Третий — повтор запроса: если результат странный, запрос отправляют ещё раз и сравнивают. Четвёртый — правка кода: в обработчик добавляют print, перезапускают сервер и сразу получают вывод.

Все четыре способа работают при одном условии: разработчик находится рядом с процессом в момент ошибки. Он видит терминал, может остановить программу и повторить действие. После развёртывания это условие уже не выполняется.

Все четыре способа требуют, чтобы разработчик находился рядом с процессом. Следующий раздел показывает, что происходит, когда процесс работает на сервере.

§3Что меняется после развёртывания

Сравнение из жизни
Автомобиль в мастерской и у владельца

Пока машина стоит в мастерской, мастер слышит двигатель, может заглушить его и завести снова. Когда машина уехала к владельцу, мастеру остаются только записи бортового компьютера.

Программа у разработчика и на сервере

Пока программа запущена на компьютере разработчика, он видит терминал и может остановить её. Когда она работает на сервере, остаются только данные, которые она сохранила.

Развёртывание — установка и запуск версии программы в среде, где с ней работают пользователи. Эту среду называют рабочей или продакшеном (production).

На сервере программа работает в другом окружении. Переключите схему ниже и сравните, через что проходит запрос в двух случаях.

Где проходит запрос
Разработчик→FastAPI→Хранилище

Один запрос. Терминал и состояние процесса доступны разработчику.

Рис. 1.1.1 путь запроса на своём компьютере и на сервере

Кнопки переключают схему. «Локально» — один процесс, и разработчик сидит рядом с ним. «После развёртывания» — пользователи, три экземпляра API, база и кэш, очередь и пять обработчиков фоновых задач.

На сервере приложение запущено в нескольких экземплярах. Экземпляр — отдельный запущенный процесс той же программы. Их делают несколько, чтобы выдерживать нагрузку и не останавливать сервис, если упадёт один процесс. Запросы между экземплярами распределяет балансировщик нагрузки. Часть работы вообще идёт вне HTTP-запроса: обработчики очереди формируют отчёты и отправляют письма уже после того, как пользователь получил ответ.

В таком окружении от четырёх способов отладки остаётся немного. В левой колонке способы из прошлого раздела, в правой — то, что происходит с каждым на сервере.

Локально

Всё под рукой

  • Терминал виден сразу.
  • Программу можно остановить отладчиком.
  • Запрос отправили ещё раз.
  • Добавили print и перезапустили.
На сервере

Что с ними происходит

  • Вывод десятков процессов на разных серверах; после перезапуска контейнера он может исчезнуть.
  • Остановка процесса остановит обслуживание пользователей.
  • Неизвестно, какой запрос, от кого и на какой экземпляр пришёл.
  • Нужен выпуск новой версии, а сбой к тому времени может пройти.
Пример

Сбой ночью в 3:10

Ночью на одном экземпляре кончилась память, процесс упал, система оркестрации запустила его заново. Утром разработчик получает жалобу. Процесса, в памяти которого были сведения о сбое, уже нет, а новый процесс в 3:10 ещё не существовал.

Получается, что вопрос «что случилось с приложением в 3:10» сводится к вопросу «какие данные о 3:10 сервис куда-то сохранил». Если не сохранил ничего, разобрать сбой нельзя.

Поэтому данные для разбора сбоя собирают и сохраняют заранее, пока сервис работает. После сбоя собрать их уже не получится.

Данные нужно сохранять заранее. Чтобы выбрать, какие, нужно определить, что считается хорошей работой сервиса.

§4Что сервис обещает пользователю

Чтобы решить, какие данные собирать, нужно сначала определить, что значит «сервис работает хорошо». Для пользователя это складывается из четырёх свойств. Они пересекаются, но проверяются по-разному.

Доступность

Можно ли получить ответ

Нарушение: сервис не отвечает или возвращает 503 Service Unavailable.

Производительность

Сколько времени занимает работа

Нарушение: каталог открывается за 3 секунды вместо 100 мс.

Надёжность

Сохраняется ли поведение во времени

Нарушение: раз в несколько часов теряются фоновые задания.

Корректность

Правилен ли результат

Нарушение: пришёл HTTP 200, но заказ не записан в базу.

Доступность и производительность связаны. Браузер и мобильное приложение не ждут ответа бесконечно: если сервис отвечает дольше предела, заданного у клиента, например 30 секунд, клиент прерывает запрос с ошибкой тайм-аута. Для пользователя медленный сервис становится недоступным, хотя сервер продолжает обрабатывать запрос.

Корректность проверяется отдельно от остальных свойств. Код HTTP сообщает результат на уровне протокола: запрос принят и обработан без исключения. Смысл операции в нём не отражён. Обработчик оформления заказа может вернуть 200 и не записать заказ, и ни один показатель HTTP этого не заметит. Поэтому для важных операций успех проверяют отдельно: заказ появился в базе, письмо ушло, платёж подтверждён.

Первое из четырёх свойств, доступность, обычно проверяют отдельным адресом. Следующий раздел показывает, что эта проверка сообщает и чего она не сообщает.

§5Что проверяет адрес /health

Почти у каждого веб-сервиса есть адрес проверки состояния. Балансировщик и система оркестрации регулярно обращаются к нему и по ответу решают, отправлять ли на экземпляр запросы и не пора ли его перезапустить. У сервиса стенда это /health.

Рис. 1.1.2 ответ /health в браузере

Chrome получил JSON и показал его одной строкой.

  1. Pretty-print. флажок добавляет сам браузер: он форматирует JSON с отступами. К ответу сервиса флажок не относится.
  2. status. значение ok означает только то, что обработчик выполнился.
  3. scope. сервис сам сообщает границу проверки: проверен процесс, зависимости не проверялись.
Открыть в полном размере ↗
Python · обработчик /health из app.pyФайл целиком ↓
@app.get("/health", tags=["Проверка процесса"])
async def health():
    return {"status": "ok", "scope": "process only; dependencies not checked"}

Код подтверждает поле scope. Обработчик не обращается к зависимостям и не выполняет сценарий /products. Он отвечает 200, если процесс принимает HTTP-запросы и может выполнить простую функцию. Даже если /products в этот момент падает в каждом запросе, /health ответит ok.

Для своей задачи такая проверка подходит. Она отвечает на вопрос «жив ли экземпляр», и для перезапуска зависшего процесса этого достаточно. На вопрос «получают ли пользователи товары» она не отвечает: для него нужны данные о настоящих запросах к /products.

Проверка /health не видит рабочий маршрут. Код ответа самого маршрута тоже сообщает не всё, и это видно на двух запросах.

§6Два ответа HTTP 200

На странице стенда /lab выполнены два сценария подряд: сначала fast, затем slow. Страница отправляет настоящий запрос к /products и показывает всё, что сервис сохранил о нём.

Рис. 1.1.3 сценарий fast на странице стенда

Страница стенда после нажатия «Быстрый 200». Метки отмечают шесть частей, о которых идёт речь дальше.

  1. Кнопки сценариев. отправляют настоящий запрос GET /products?scenario=….
  2. Строка итога. код ответа и два времени: сколько ждал браузер и сколько работал обработчик.
  3. HTTP. тело ответа, которое получил клиент.
  4. LOG. запись о событии, которую сервис сохранил у себя.
  5. TRACE. этапы обработчика на общей шкале времени.
  6. Пометка стенда. зависимость моделируется ожиданием, базы данных нет.
Открыть в полном размере ↗

Снимок читается сверху вниз. Строка итога: HTTP 200 · клиент: 84.0 ms · обработчик: 81.8 ms. Время клиента измерил JavaScript в браузере — от отправки запроса до получения ответа. Время обработчика измерил сам сервис — от начала до конца функции products. Разница 2,2 мс ушла на путь запроса и ответа и на работу Uvicorn до и после обработчика.

Блок HTTP — тело ответа, которое получил клиент. status: "ok" и items: 10 — полезные данные. request_id — уникальный идентификатор этого запроса. duration_ms: 81.837 — время обработчика с точностью до тысячных долей миллисекунды.

Блок LOG — запись о событии, которую сервис сохранил у себя. Полей в ней больше: время по UTC в timestamp, уровень важности level, имя сервиса, тип события, маршрут, код ответа, сценарий и поле exception. Значение null в последнем поле означает, что исключения не было. request_id совпадает с блоком HTTP, и по нему запись связывается с ответом.

Блок TRACE показывает три этапа обработчика на общей шкале: validate — 11,1 мс, dependency.wait (simulated) — 60,3 мс, serialize — 10,5 мс. Полосы идут друг за другом и вместе занимают всю шкалу от 0 до 81,8 мс. Слово simulated в имени этапа напоминает, что ожидание смоделировано.

Рис. 1.1.4 сценарий slow на странице стенда

Тот же обработчик, тот же код ответа HTTP 200, то же тело ответа. Отличаются только длительности.

  1. Строка итога. снова HTTP 200, но клиент ждал 3026,6 мс.
  2. duration_ms. тело ответа то же, отличается только длительность.
  3. level. уровень записи INFO, как у быстрого запроса.
  4. validate. 11,1 мс — на этой шкале тонкая черта у левого края.
  5. dependency.wait. 3000,6 мс — полоса на всю ширину.
  6. serialize. 10,6 мс — тонкая черта у правого края.
Открыть в полном размере ↗

Теперь второй снимок рядом с первым. В строке итога снова HTTP 200, но клиент ждал 3026,6 мс, а обработчик работал 3022,3 мс. В блоке HTTP те же status: "ok" и items: 10. В блоке LOG уровень INFO, как у быстрого запроса: по записи не видно, что с запросом что-то не так.

Разница есть только в числах. В блоке TRACE этап dependency.wait занял 3000,6 мс и растянулся почти на всю шкалу, а validate и serialize остались по 11 мс и стали тонкими чертами по краям.

Клиент ждал 84,0 мс в первом случае и 3026,6 мс во втором, при одинаковом коде ответа и одинаковом уровне записи. Если сервис сохраняет только код ответа, эти два запроса выглядят одинаково: оба успешны. А пользователь во втором случае ждал в 36 раз дольше. Чтобы заметить медленную работу, нужно сохранять длительность, а чтобы найти её место — длительность отдельных этапов.

На стенде медленный запрос вызван намеренно. В работе такие запросы редки, и следующий раздел считает, как часто их находит ручная проверка.

§7Почему одна проверка ничего не доказывает

Пользователь пожаловался на медленный каталог. Разработчик открыл страницу, она загрузилась за 90 мс, и жалобу закрыли как неподтверждённую. Расчёт ниже показывает, насколько надёжна такая проверка.

Пример

Вероятность попасть на медленный запрос

Пусть медленным оказывается каждый двадцатый запрос, то есть 5%. Тогда быстрых 95%, и вероятность, что одна ручная проверка окажется быстрой, равна 0,95.

P(все n проверок быстрые) = 0,95n
0,95
доля быстрых запросов
n
сколько раз подряд разработчик проверил страницу
1 проверка
95%
3 проверки
≈ 86%
10 проверок
≈ 60%

Длина полосы — вероятность, что ни одна проверка не покажет медленный запрос.

Даже десять проверок подряд с вероятностью 60% не покажут ни одного медленного запроса. Проблему, которая касается каждого двадцатого пользователя, ручными проверками почти не найти.

Для ответа на жалобу нужны данные за тот интервал, о котором говорит пользователь: сколько было запросов, какая их часть выполнялась медленно, как это менялось во времени. Одна быстрая проверка доказывает только то, что конкретный запрос в конкретный момент выполнился быстро.

Для ответа на жалобу нужны данные за интервал. Прежде чем их перечислить, нужно различить, какие утверждения о сбое доказаны, а какие нет.

§8Симптом, наблюдение, гипотеза и вывод

При разборе проблем различают четыре вида утверждений. Звучат они похоже, но доказано в них разное, и если их путать, получаются ложные выводы.

Симптом

Что заметил пользователь

«Каталог иногда долго открывается».

Наблюдение

Факт, подтверждённый измерением

С 14:00 два из двадцати запросов /products длятся больше 3 секунд, код ответа 200.

Гипотеза

Предположение, которое ещё не проверено

Запросы долго ждут ответа зависимости.

Вывод

Гипотеза, подтверждённая проверкой

Задержка приходится на этап dependency.wait, остальные этапы не изменились.

Чаще всего гипотезу записывают как вывод. Например, на графике загрузка процессора 95%, и в отчёт сразу пишут «сервис тормозит из-за процессора». Но высокая загрузка бывает и причиной задержек, и следствием наплыва запросов, и обычным состоянием вычислительной задачи.

Чтобы отличить одно от другого, нужен контекст: такой ли была загрузка вчера, когда жалоб не было, совпадает ли её рост с ростом длительности, выросло ли число запросов. Пока это не проверено, «CPU 95%» остаётся наблюдением, а «причина в процессоре» — гипотезой.

Вывод опирается на измерения. Последний раздел перечисляет, какие измерения сервис сохраняет для этого заранее.

§9Какие данные нужно сохранять заранее

Выводы главы сводятся к одной таблице. Чтобы после развёртывания можно было разобрать жалобу, сервис во время работы сохраняет четыре вида сведений.

Что сохранятьНа какой вопрос отвечаетПример со стенда
Количество операций и их результаты за интервалКаков масштаб проблемыСколько запросов /products завершилось кодом 200 и сколько кодом 500
Распределение длительностейСколько запросов медленные; одного среднего для этого малоСколько запросов уложилось в 100 мс, сколько длилось больше секунды
Записи о событиях с контекстомЧто случилось с конкретным запросомЗапись с request_id, кодом ответа и текстом исключения
Этапы операцииГде операция потратила времяvalidate, dependency.wait, serialize

Ко всем этим записям одни и те же требования. Каждое значение хранится с отметкой времени, иначе его не поставить в историю. Каждое значение привязано к компоненту: сервису, экземпляру, версии. И всё это хранится вне процесса, который может упасть или перезапуститься.

Регулярный сбор таких сведений называют мониторингом, а сами сведения — телеметрией. Строки таблицы соответствуют трём видам телеметрии: первые две — метрикам, третья — логам, четвёртая — трассам. Метрики разобраны в главе 1.4, логи — в главе 1.5, трассы — в главе 1.6.

Первоисточник: Google SRE Book · Monitoring Distributed Systems ↗

§10Частые ошибки

Что делаютЧто получаетсяКак правильно
Считают HTTP 200 признаком того, что всё хорошоМедленные и некорректные успешные ответы остаются незамеченнымиСохранять длительность и отдельно проверять результат операции
Проверяют работу сервиса только через /healthПроцесс жив, а рабочий маршрут завершается ошибкойСмотреть данные о настоящих запросах к рабочим маршрутам
Закрывают жалобу после одной ручной проверкиРедкие проблемы не воспроизводятся и остаются в сервисеСмотреть данные за весь интервал жалобы
Рассчитывают разобрать сбой по выводу терминалаПосле перезапуска процесса вывод недоступенСохранять телеметрию вне процесса во время работы
Записывают гипотезу как выводИсправляют не то, проблема остаётсяОтмечать, какие утверждения измерены, а какие только предположены
Путают время клиента и время обработчикаЗадержку сети или Uvicorn приписывают кодуУказывать, в какой точке измерена длительность

§11Проверьте себя

Почему после развёртывания нельзя разобрать ночной сбой так же, как локальную ошибку?

Процесс, в котором произошёл сбой, мог упасть и перезапуститься. Разработчика не было рядом в момент ошибки, терминал и память процесса недоступны. Остаются только данные, сохранённые во время работы.

/health отвечает {"status": "ok"}, а /products возвращает 500. Как это возможно?

Обработчик /health проверяет только, что процесс принимает запросы. Он не обращается к зависимости и не выполняет сценарий /products, поэтому сбой рабочего маршрута на нём не отражается.

На снимке fast клиент ждал 84,0 мс, а обработчик работал 81,8 мс. Откуда разница?

Время клиента включает путь запроса и ответа и работу Uvicorn до и после вызова обработчика. Время обработчика измерено только внутри функции products.

Медленным оказывается каждый десятый запрос. С какой вероятностью три ручные проверки подряд будут быстрыми?

0,9³ = 0,729, то есть около 73%. В большинстве случаев три проверки проблему не покажут.

«CPU 90%, поэтому API медленный». Какое это утверждение: наблюдение, гипотеза или вывод?

«CPU 90%» — наблюдение. Связь с медленной работой API — гипотеза, пока не проверено, совпадает ли рост загрузки с ростом длительности и нет ли другой причины.

§12Шпаргалка

  • products-api — сервис на FastAPI: /products со сценариями fast, slow и error.
  • Развёртывание — запуск версии программы в среде, где с ней работают пользователи.
  • После развёртывания нет терминала, отладчика и возможности повторить запрос.
  • Данные для разбора сбоя сохраняют заранее, во время работы сервиса.
  • Качество сервиса: доступность, производительность, надёжность, корректность.
  • /health проверяет процесс, но не рабочие маршруты и зависимости.
  • HTTP 200 не различает быстрый и медленный ответ; нужна длительность.
  • Одна быстрая проверка не опровергает жалобу на редкие задержки.
  • Симптом → наблюдение → гипотеза → проверка → вывод.

Первоисточники