После развёртывания: что происходит с приложением
В главе описан сервис, на котором построена вся тема, и разобрано, какие сведения о программе разработчик теряет, когда она начинает работать на сервере. На двух ответах 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).
На сервере программа работает в другом окружении. Переключите схему ниже и сравните, через что проходит запрос в двух случаях.
Кнопки переключают схему. «Локально» — один процесс, и разработчик сидит рядом с ним. «После развёртывания» — пользователи, три экземпляра 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.
Chrome получил JSON и показал его одной строкой.
- Pretty-print. флажок добавляет сам браузер: он форматирует JSON с отступами. К ответу сервиса флажок не относится.
- status. значение
okозначает только то, что обработчик выполнился. - scope. сервис сам сообщает границу проверки: проверен процесс, зависимости не проверялись.
@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 и показывает всё, что сервис сохранил о нём.
Страница стенда после нажатия «Быстрый 200». Метки отмечают шесть частей, о которых идёт речь дальше.
- Кнопки сценариев. отправляют настоящий запрос
GET /products?scenario=…. - Строка итога. код ответа и два времени: сколько ждал браузер и сколько работал обработчик.
- HTTP. тело ответа, которое получил клиент.
- LOG. запись о событии, которую сервис сохранил у себя.
- TRACE. этапы обработчика на общей шкале времени.
- Пометка стенда. зависимость моделируется ожиданием, базы данных нет.
Снимок читается сверху вниз. Строка итога: 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 в имени этапа напоминает, что ожидание смоделировано.
Тот же обработчик, тот же код ответа HTTP 200, то же тело ответа. Отличаются только длительности.
- Строка итога. снова HTTP 200, но клиент ждал 3026,6 мс.
- duration_ms. тело ответа то же, отличается только длительность.
- level. уровень записи INFO, как у быстрого запроса.
- validate. 11,1 мс — на этой шкале тонкая черта у левого края.
- dependency.wait. 3000,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.
- 0,95
- доля быстрых запросов
- n
- сколько раз подряд разработчик проверил страницу
Длина полосы — вероятность, что ни одна проверка не покажет медленный запрос.
Даже десять проверок подряд с вероятностью 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 не различает быстрый и медленный ответ; нужна длительность.
- Одна быстрая проверка не опровергает жалобу на редкие задержки.
- Симптом → наблюдение → гипотеза → проверка → вывод.