Привет, Хабр! На связи команда Seller API, а именно её тимлид Саша Валов и старший разработчик Никита Денисенко. В этой статье мы разберём одну из проблем большого API и расскажем, как мы её решили.
Вступление
Seller API — это продукт, предоставляющий программный интерфейс для работы с маркетплейсом Ozon. Он позволяет системам продавца и Ozon обмениваться информацией.
Seller API насчитывает более 200 методов. Эти методы удовлетворяют множество бизнес-потребностей и предоставляют доступ к широкому спектру сервисов Ozon. В основном методы API проксируют запросы к этим сервисам.
Проблема
Нашей команде необходимо оперативно давать ответы на вопросы а-ля «В какие сервисы мы, разработчики команды Seller API, ходим под капотом этой API-ручки?» или, наоборот, «В контексте каких API-ручек мы ходим в конкретный сервис или даже метод сервиса?». Эти вопросы с завидной регулярностью поступают от ребят из технической поддержки, которым необходимо расследовать неполадку, и ребят из продукта, которые систематизируют требования к новым функциональностям или к рефакторингу старых.
Чтобы ответить на любой из этих вопросов, нам чаще всего приходится смотреть код. Дежурный разработчик сначала идёт в proto-файлы, описывающие контракт API (да, мы в Ozon описываем и внешние, и внутренние API с помощью proto-файлов). Погружаясь глубже вплоть до кода клиентов нижележащих сервисов, он находит нужные методы, в которые мы проксируем запросы. И, наоборот, из кода клиента необходимого сервиса, поднимаясь по слоям, разработчик понимает, где и как используется метод нижележащего сервиса.
Да, у нас есть трейсинг (мы используем Jaeger), который может помочь в поиске ответов, но этот процесс тоже превращается в небольшое исследование, поскольку каждый конкретный трейс не даёт общей картины, а связан с одним методом API или даже отдельным кейсом в рамках метода.
Документация у нас тоже есть, но её ведение и организация актуальной навигации по ней, учитывая объём часто меняющихся взаимодействий, — задача нетривиальная и трудоёмкая. Нам нужны автоматические механизмы.
Решить проблему можно сбором метрик. В Ozon огромное количество различных метрик и решений, связанных с observability. У нас есть метрики нашего сервиса и сервисов, в которые ходит наш API, — но нет метрик, связывающих их воедино. Мы не видим общей картины.
Решение
Мы поняли, что нам необходимо придумать своё решение. Что у нас для этого есть? Первое, что приходит в голову, — это Prometheus. Можно завести новую метрику, например типа counter (счетчик) с лейблами: api_method, target_service, target_service_method, которая будет хранить факт вызова нижележащего сервиса за последние N секунд. После экспорта данной метрики можно будет доставать данные с помощью PromQL и отрисовывать их в Grafana. Звучит неплохо!
Но получалось, что таким образом мы начали бы плодить новые метрики, которых и так много.
Ну что ж, мы пошли думать дальше. А что, если те же лейблы превратить в поля таблицы? У нашего сервиса есть своя база данных Postgres, где мы сами решаем, что и как хранить. Звучит приемлемо.
Приступим к решению. Для начала нам нужно создать таблицу:
create table endpoint_map( id bigint generated by default as identity primary key, api_method text not null, target_service text not null, target_service_method text not null, created_at timestamp with time zone default CURRENT_TIMESTAMP not null );
Прежде чем что-то писать в таблицу, необходимо определить, как мы будем понимать, какой метод API обращается к какому методу сервиса. Как это можно сделать?
В Ozon для внешних API, доступных по HTTP, мы используем gRPC-Gateway. Если кратко, это плагин для gRPC, позволяющий описывать gRPC и RESTful API в одном proto-файле. Документация: https://grpc-ecosystem.github.io/grpc-gateway/.
Из proto-файла с помощью плагина gRPC-Gateway генерируется код, в котором описываются HTTP-хендлеры. Из того же файла генерируется код gRPC-сервера. gRPC-Gateway выступает в роли reverse proxy (обратного прокси) и преобразует HTTP-трафик в gRPC-трафик.
Какой путь проходит запрос к публичному API?
Упрощённо его можно представить так:
-
Запрос от API-клиента приходит в gRPC-Gateway.
-
gRPC-Gateway преобразует HTTP-запрос в gRPC-запрос.
-
Прежде чем запрос отправится дальше, он попадает в middleware (промежуточное ПО), которое может выполнить необходимые действия с ним, такие как логирование, проверка токена или, как в нашем случае, обогащение контекста запроса нужным именем метода API.
-
Затем запрос отправляется в сервисный слой — ту логику, которая непосредственно его обрабатывает.
-
После обработки запроса действия выполняются в обратном порядке.
На стороне сервисного слоя, где располагается логика обработки запроса, могут находиться gRPC-клиенты конечных сервисов, в которые Seller API проксирует запросы данных. В каждый такой клиент можно добавить middleware (в парадигме gRPC их называют interceptors), которое, зная об исходном контексте запроса, может достать имя нужного нам метода API и выполнить с ним необходимые действия.
Попробуем закодить вышеописанное.
HTTP middleware
func NewHTTPMiddleware(ctx context.Context) func(handler http.Handler) http.Handler { return func(handler http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { // ... // положим в контекст имя метода API apiMethod = req.Method + " " + req.RequestURI req = req.WithContext(context_helper.HTTPEndpointToContext(req.Context(), apiMethod)) // ... } } }
gRPC client middleware
func NewEndpointMapUnaryInterceptor() grpc.UnaryClientInterceptor { return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { apiMethod := utcontext_helperils.HTTPEndpointFromContext(ctx) targetService := cc.Target() // insert(ctx, apiMethod, targetService, method) return invoker(ctx, method, req, reply, cc, opts...) } }
Очевидно, что нам не нужно записывать в базу каждый запрос, прилетевший в API. В middleware gRPC-клиента есть кеш, в который мы кладём отметку о том, что связка apiMethod + targetService + targetMethod уже была добавлена в таблицу. Нам нужно знать о факте вызова запросов в рамках одного дня, а не их количества.
Дело за малым — отобразить всё это в Grafana. Для этого добавляем (если ещё не добавлен) новый источник данных в виде нашей БД. Разумеется, с правами только на чтение. Создаём новый график и, выбрав источник данных, пишем запрос:
select api_method "ручка Seller API", target_service "нижележащий сервис", target_method "ручка нижележащего сервиса" from endpoint_map where to_char(created_at, 'yyyy-MM-DD') = '$date' group by api_method, target_service, target_method
Здесь $date — это переменная, которую можно указать в разделе Variables в настройках дашборда. Сделано это для удобства, чтобы можно было выбрать дату из выпадающего списка и увидеть все вызовы API в этот день.
Получилась такая красота:
Первые два столбца — активные. То есть, нажав на какое-то значение, мы попадём на страницу либо с сервисом, либо со Swagger-документацией.
Выводы
Таким недорогим и не требующим большого количества ресурсов способом мы значительно упростили жизнь как минимум трём командам: разработки, продукта и техподдержки.
Мы создали единую точку входа, где собраны ответы на вопросы, упомянутые в начале статьи. Теперь каждый сотрудник может самостоятельно зайти в Grafana и увидеть таблицу с нужными данными — без обращения к нам или к коду. Это решение уменьшило количество запросов от техподдержки и аналитиков к команде разработки и ускорило процесс поиска и устранения проблем для разработчиков.
ссылка на оригинал статьи https://habr.com/ru/articles/867630/
Добавить комментарий