Что такое наблюдаемость?
Изначально термин из теории управления, наблюдаемость означает, насколько много мы можем узнать о внутреннем состоянии системы, глядя только на выходные данные, которые появляются за пределами системы. Применительно к программному обеспечению это можно сформулировать так: можем ли мы объяснить проблему, используя только данные, которые производятся в данный момент, не обращаясь напрямую к серверу и не изменяя код для добавления новых логов?
Главная мысль заключается в том, что наблюдаемость не ограничивается ответами на вопросы, сформулированные заранее. «Уведоми меня, когда время ответа превысит три секунды» — это заранее определённый вопрос. А «Почему вчера днём загрузка не выполнялась только на определённом устройстве?» — вопрос, который невозможно было определить заранее. Если позже вы можете задавать подобные вопросы, система наблюдаема. Если нет, вам придётся изменить и развернуть код, а затем ждать, пока проблема возникнет снова.
В словах это может показаться очевидным, но чтобы понять, когда это различие действительно имеет значение, нужно рассмотреть реальный сервис.
Зачем это нужно BanJang Note?
BanJang Note — приложение, которое связывает прорабов и руководителей строительных бригад. Оно переносит в приложение публикацию вакансий, отклики, отправку документов и учёт посещаемости, которыми раньше обменивались через чаты и текстовые сообщения. Когда руководитель бригады публикует вакансию, прораб откликается, руководитель проверяет отправленные документы и запрашивает исправления, если чего-то не хватает, затем назначает выбранных работников в бригаду и фиксирует время их прихода, ухода и ежедневную оплату.
Вот что важно в архитектуре. Для идентификации и аутентификации пользователей, хранения документов, отправки уведомлений, платежей и других задач приложение использует другие микросервисы внутренней платформы. Сервер BanJang Note выполняет собственную бизнес-логику, но также координирует работу каждого микросервиса, чтобы выполнять более сложные функции.
Рассмотрим одну ситуацию. Мы получаем сообщение: «Начиная со вчерашнего дня отправка документов время от времени завершается ошибкой».
Проблема заключается в слове «время от времени». Если попробовать десять раз, девять раз всё сработает. Сервер работает, загрузка CPU в норме, уведомлений об ошибках не поступало. Ни одна из отслеживаемых нами метрик не выглядит необычно. При попытке воспроизвести проблему локально всё работает нормально.
Даже если мы начнём изучать логи, с самого начала окажемся в тупике: какие ключевые слова искать и какой временной диапазон проверять? Кроме того, этот запрос обрабатывается не только нашим сервером. Он проходит через аутентификацию и обращается к сервису хранения файлов, поэтому отсутствие необычных записей в наших логах не означает отсутствие проблемы. Мы должны иметь возможность выбрать один неудачный запрос и проследить, насколько далеко он дошёл и где остановился. Без такой возможности остаётся только гадать.
Это также показывает, почему мониторинг — лишь половина картины. Мониторинг означает поиск известных проблем. Уведоми меня, когда диск заполнится; уведоми меня, когда доля ошибок превысит 1%. Задать порог можно только тогда, когда вы уже знаете, что является опасным. Но многие реальные инциденты возникают из-за сочетаний факторов, которые мы не предвидели. Проблему, возникающую только в определённой версии приложения и в определённый период времени, нельзя охватить заранее заданным оповещением, потому что мы не знали, какое сочетание факторов искать.
Иными словами, мониторинг вызывает оповещение, а наблюдаемость позволяет найти причину после срабатывания оповещения — или обнаружить проблему, которая вообще не вызвала оповещение. Если у вас есть только оповещения, но нет данных для расследования, во время инцидента остаётся лишь гадать. Если же вы только накапливаете данные, но не используете оповещения, пользователь первым обнаружит, что что-то не так. Необходимы оба компонента.
Три столпа наблюдаемости
Метрики— это числа, накапливаемые по временной оси. К ним относятся количество запросов, перцентили времени ответа и доля ошибок. Они мало нагружают систему и могут храниться долго, но отдельные события исчезают. Вы можете знать, что доля ошибок составляет 2%, но не знать, у кого произошла ошибка и почему. Поскольку временной ряд создаётся для каждой комбинации меток, не следует включать значения с высокой кардинальностью, например идентификаторы пользователей.
Логи— это записи, описывающие то, что произошло в определённый момент. Они наиболее подробны, но также занимают много места и дорого обходятся. Если хранить только предложения, удобные для чтения людьми, впоследствии станет сложно искать по условиям, поэтому лучше сохранять их в форме, разделяющей ключи и значения.
Трейсы— это путь, который один запрос проходит через систему. Среди трёх типов данных только они рассматриваются на уровне запроса, заполняя пробел между метриками, в которых теряются отдельные события, и логами, в которых теряется общий поток выполнения.
По отдельности каждый из трёх компонентов даёт лишь половину картины. На практике они соединяются последовательно. Метрики сообщают, что доля ошибок выросла с 0,1% до 4%. Трейсы раскрывают пути неудачных запросов и показывают, что все они остановились на участке сервиса файлов. Логи сообщают, с какими параметрами был выполнен вызов на этом участке и какой ответ был получен. Что является ненормальным, где находится проблема и почему она возникла — ответы на эти вопросы уточняются именно в таком порядке.
Связующим звеном между этими тремя компонентами является correlation ID. Когда поступает запрос, мы создаём trace ID и прикрепляем тот же идентификатор к каждому сервису, через который проходит запрос, и к каждой строке лога. Затем из медленного участка, видимого в трейсе, можно напрямую перейти к логам этого участка. Без этого три компонента превращаются лишь в отдельные массивы данных, и нам приходится видеть всплеск на графике и вручную оценивать временной диапазон в поле поиска логов. Это больше похоже на поиск, чем на наблюдение.
Трейсинг становится необходимым при разделении сервисов
Наблюдаемость стала необходимостью, а не возможностью, когда системы начали разделять на несколько сервисов.
В монолитном сервисе при возникновении исключения stack trace содержал всю причину проблемы. Но после разделения сервисов эта информация исчезает. Stack trace не может пересекать границы между серверами, а в наших логах остаётся лишь запись «вызов завершился ошибкой». Мы можем открыть логи другого микросервиса, но среди всех этих строк невозможно определить, какие из них относятся к нашему запросу. Вопрос не в том, есть ли у нас разрешение на их просмотр, а в том, можем ли мы связать их между собой.
Здесь накладываются несколько факторов. Для отображения одного экрана мы можем вызвать пять сервисов, но если откажет только один из них, ответ всё равно может вернуться со статусом 200, а сбой не отразится в доле ошибок. Циклы развертывания у сервисов также различаются, поэтому то, что работало до вчерашнего дня, может перестать работать, даже если мы ничего не развёртывали. Если мы не можем определить, вызван ли сбой нашим кодом или другим сервисом, реагирование на инцидент превращается в обмен просьбами проверить проблему, а не в попытку проследить её причину.
Кроме того, в такой ситуации трудно полагаться только на метрики и логи. Метрики каждого сервиса показывают, что он работает нормально. Если пять сервисов последовательно выполняются по 200 миллисекунд, пользователь может ждать две секунды, но при просмотре отдельных метрик общая картина не видна. Даже если логи централизованы, хронологический список отличается от потока одного запроса. Когда вызовы выполняются параллельно или асинхронно, одной хронологии недостаточно, чтобы раскрыть причинно-следственную связь.
Трейсинг делает то, чего не могут эти два компонента. Он использует один запрос как единицу наблюдения, фиксирует, какие вызовы привели к другим вызовам, с помощью связей «родитель — потомок», и записывает длительность каждого участка. Он восстанавливает разрозненные факты в причинном, а не хронологическом порядке. Благодаря этому мы можем ответить на такие вопросы: куда на самом деле попал этот запрос? Какой участок занял время из общих двух секунд? Где находилась настоящая точка возникновения проблемы в цепочке сбоев, а не самое шумное место? Вызываем ли мы один и тот же сервис восемь раз в рамках одного запроса?
Это становится понятнее на примере BanJang Note. Отправка одного документа включает один запрос, который проходит через аутентификацию для идентификации пользователя, отправляет файл в сервис хранения файлов, сохраняет результат, а затем отправляет уведомление другой стороне. Написанный нами код отвечает лишь за часть этого процесса, поэтому сбои происходят не «внутри сервера BanJang Note», а между сервисами. Чем ближе функция к потоку отдельных микросервисов, таких как документы и уведомления, тем большая часть её фактической обработки выполняется за пределами нашего процесса.
Без трейсов на нашей стороне остаётся лишь одна строка: «Отправка документа завершилась ошибкой». С трейсами мы сразу видим, что аутентификация завершилась за 20 миллисекунд, а запрос остановился во время вызова сервиса хранения файлов. Нажав на эту точку, можно открыть логи и увидеть, какой запрос был отправлен и какой ответ получен. Поиск причины превращается в проверку того, какой ответ пришёл от какого участка, вместо споров о том, какая команда несёт ответственность.
Процесс длительный. Подача заявки на вакансию, проверка заявки руководителем бригады, отбор, ожидание, назначение в бригаду и подтверждение посещаемости — всё это включает множество шагов. Если один из промежуточных шагов незаметно завершается ошибкой, на экране пользователя может даже не появиться сообщение об ошибке. Просто ничего не происходит. Когда мы получаем запрос: «Я подал заявку, но уже несколько дней ничего не слышал», почти весь ответ заключается в восстановлении того, насколько далеко дошёл запрос и где остановился. Именно здесь важно, сохраняется ли путь на уровне запроса.
Нельзя попросить пользователя предоставить дополнительную информацию. Пользователи BanJang Note — рабочие и руководители, работающие на объекте. Практически невозможно попросить их открыть инструменты разработчика, повторить попытку и рассказать, что произошло. Чтобы начать работу с Customer Service и установить причину, система должна сохранить сведения о том, что произошло в тот момент.
Заключение
Одним предложением: наблюдаемость — это создание системы, которая позволяет впоследствии задавать вопросы, о которых вы не подумали заранее.
Следует помнить, что система не становится наблюдаемой просто потому, что вы подключили несколько инструментов. Добавление библиотеки и запуск сборщика — лишь начало. Достигают ли сигналы хранилища на самом деле, кто просматривает их после поступления и кто получает уведомление, когда что-то выглядит ненормально, — всё это части единого целого. Если отсутствует хотя бы один из трёх компонентов, практически это равносильно полному отсутствию наблюдаемости.
При написании кода мы обычно в первую очередь думаем о случаях, когда всё работает. Но в эксплуатации больше всего времени отнимают ситуации, когда что-то время от времени выходит из строя, а при разделении сервисов бывает трудно даже определить, где возникла проблема. Каждый раз, создавая функцию, стоит спросить себя: если это сломается в три часа ночи, что я буду проверять, чтобы найти причину?
Ted