Как, имея более 200 методов HTTP, смэтчить их с тем, в какие сервисы они ходят

от автора

Привет, Хабр! На связи команда 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/.

gRPC-Gateway

gRPC-Gateway

Из proto-файла с помощью плагина gRPC-Gateway генерируется код, в котором описываются HTTP-хендлеры. Из того же файла генерируется код gRPC-сервера. gRPC-Gateway выступает в роли reverse proxy (обратного прокси) и преобразует HTTP-трафик в gRPC-трафик. 

Какой путь проходит запрос к публичному API? 

Упрощённо его можно представить так:

  1. Запрос от API-клиента приходит в gRPC-Gateway.

  2. gRPC-Gateway преобразует HTTP-запрос в gRPC-запрос.

  3. Прежде чем запрос отправится дальше, он попадает в middleware (промежуточное ПО),  которое может выполнить необходимые действия с ним, такие как логирование, проверка токена или, как в нашем случае, обогащение контекста запроса нужным именем метода API.

  4. Затем запрос отправляется в сервисный слой — ту логику, которая непосредственно его обрабатывает.

  5. После обработки запроса действия выполняются в обратном порядке.

На стороне сервисного слоя, где располагается логика обработки запроса, могут находиться 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *