Когда всё, что делает компания — от продуктов, услуг и до обслуживания — направлено на то, чтобы клиент был доволен и возвращался снова, все команды объединяются вокруг единой цели — смотреть на задачи и искать решения с точки зрения проблем и нужд конечного клиента.
Я Александр Шакмаев — технический лидер в Cloud.ru. Поделюсь опытом нашей команды: расскажу, как с помощью gRPC-интерцепторов и рефлексии команда Go-разработчиков может изменить продукт и улучшить пользовательский опыт.
Сразу отмечу, что большинство наших API-сервисов мы пишем на основе gRPC, а магию с REST и обработкой HTTP-запросов нам помогает осуществить Sidecar в виде Envoy в контексте Istio.
Клиентоцентричность со стороны разработчика: как это работает у нас
Клиентоцентричность в ее классическом понимании — это постоянное улучшение взаимоотношений с клиентом. Как мы в команде разработки можем поучаствовать в этом процессе или на него повлиять? Как минимум, не возвращать пользователю ошибки лишний раз, когда в запрос случайно проскочил пробел, символ табуляции или переноса строки.
Вот пример такой ошибки:
Здесь пользователь ввел JWT-токен в заголовок авторизации и случайно скопировал символы переноса в строке. Сервер тут же отвечает, что в JWT-токен пришел какой-то неопознанный символ. Получается, пользователь вынужден самостоятельно искать проблему и исправлять ее.
Есть и другие примеры — случайно проскочившие лишние пробелы или нетипичные для параметра символы. Здесь, например, пользователю вернули ошибку, так как он случайно передал в UUID пробел:
Давайте разберемся, как обрабатывать такие случаи в Go корректно — исключать лишние символы из каждого gRPC-запроса и не беспокоить пользователя лишний раз. При этом наша фича будет работать для всех новых методов, которые добавят разработчики в будущем.
Есть множество исследований в области микровзаимодействия с пользователем на уровне дизайна и frontend-части. А что по поводу backend? Известный факт — внимание к небольшим деталям может дать мощный результат.
Любая мелочь может повлиять на отношение пользователя к продукту, сервису и всей компании. В наших силах не портить впечатление на уровне микровзаимодействия с клиентом.
Модифицируем запросы пользователей
Итак, нам нужно вмешаться в запрос пользователя и исправить незначительную ошибку. Что же делать и какие вообще есть варианты? В Go есть несколько способов, как самостоятельно нормализовать нетипичные данные в gRPC-запросе.
Первый способ — вырезаем лишнее на frontend
Первое, что приходит на ум — попытаться резать лишнее на фронте. Казалось бы, идея плохая, но если подумать…
…действительно, плохая 😀! Поэтому подумаем еще.
И в голову приходит новая идея — валидировать и модифицировать запросы внутри всех контроллеров, которые обеспечивают API взаимодействие с пользователем.
Второй способ — правим код внутри всех контроллеров
Что если поставить какой-нибудь модификатор во все контроллеры, который просто будет проверять, есть ли во входящем запросе ненужные символы в выбранных полях, а затем обрабатывать регулярками или тримить данные (например, Trim).
Первая и очевидная проблема — повторяемость кода. Нам заранее известно, что наш код точно будет повторяться, ведь мы пытаемся в разных контроллерах запускать одну и ту же операцию. Конечно, можно сделать какую-то общую функцию, которая бы делала одну и ту же операцию. Наш любимый dry, single responsibility. Мы все это любим, но…
… даже при таком варианте придется, хоть и немного, но поменять код всех контроллеров. Звучит так себе, согласитесь? И если говорить про будущий технический долг, то получается, что разработчики должны всегда помнить, что для нового контроллера обязательно нужен метод для обработки запросов. А это очень неудобно.
Поэтому мы в команде продолжили искать варианты, как изменить запрос максимально сократив задачу разработки. И в конце концов придумали 🙂.
Оптимальный способ — используем интерцепторы gRPC
Нам на помощь пришли gRPC-интерцепторы, которые можно поставить между клиентом и сервером и проксируя запрос попытаться поменять данные запроса внутри хендлера.
Но тут на сцену выходит знакомая проблема — строгая типизация в языке Go. В любой общей функции придется делать свитчеры для обхода и разбора типов полей входящих структур. Поэтому добавим немного магии рефлексии и вот он — идеальный вариант модификации запроса в едином месте.
Что мы сделали
func UnaryServerInterceptor(opts ...Option) grpc.UnaryServerInterceptor { return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (_ any, err error) { value := reflect.ValueOf(req) handleFields(&value) return handler(ctx, req) } }
Сначала мы просто встраивались и перехватывали запрос, затем в этот запрос в функции handleFields
мы отправляли уже целевой запрос — посмотреть, что в нем не так, и если надо, то модифицировать:
func handleField(field *reflect.Value) { if field.IsValid() && field.CanSet() { switch field.Kind() { case reflect.String: trimValue := strings.Trim(field.String(), " \r\n\t") field.SetString(trimValue) case reflect.Struct, reflect.Pointer: // рекурсивно обрабатываем поля, если это структуры // Pointer - т.к. в grpc все вложенные структуры - указатели handleFields(field) default: return } } }
Затем пошли чуть дальше, и разработали механизм, который позволяет задавать конкретные имена полей для модификации значений только в этих полях:
func handleFields(value *reflect.Value, fieldNames ...string) { elem := value.Elem() if elem.Kind() == reflect.Struct { if len(fieldNames) > 0 { for _, name := range fieldNames { field := elem.FieldByName(name) handleField(&field) } } else { for i := 0; i < elem.NumField(); i++ { field := elem.Field(i) handleField(&field) } } } }
В коде видно, что функция распознает значение по типу. И если это строка — то мы смело можем триммить по краям лишний пробел, переносы строки, перенос каретки или табуляцию, а затем возвращать обратно.
В ходе тестирования мы обнаружили, что не обрабатываем случаи, когда в запросе лежат структуры, а в структурах — структуры, и так далее рекурсивно. Тут нам на помощь и пришла рефлексия. В итоге немного доработали схему и теперь, если к нам в запросе прилетает структура, которая бесконечно разбирает структуры до тех пор, пока их не разберет до конца, мы обходим ее рекурсивно.
Сравниваем производительность
Мы решили померить, какой из двух способов работает быстрее всего — модификация на уровне контроллера или на уровне интерцепторов с применением рефлексии. Для этого использовали инструмент нагрузочного тестирования gRPC — ghz.
С помощью ghz можно:
-
делать параллельную отправку запросов;
-
поддерживать различные методы аутентификации, включая токены;
-
задавать данные запроса через файлы;
-
генерировать отчеты в форматах HTML, CSV и JSON, для последующего анализа результатов.
Запустили стандартный бенчмарк для двух основных вариантов. Сначала реализовали такую модификацию:
ghz -c 100 -n 1000000 --insecure \ --proto api/proto/hello.proto \ --call hello.HelloService.HelloCloudRu \ -d '{"name":"Александр ", "uuid":" 51478fd5-8fad-4efc-9c14-54219b2d400d"}'
И вот такие результаты получили:
Существенной разницы между подходами замечено не было, но однозначно можно сказать одно — модификация запросов через интерцепторы происходит немного быстрее:
Если интересны подробности — добро пожаловать в репозиторий с бенчмарками.
Заключение
Кажется, создавать приложения с учетом клиентоцентричного подхода — не очень сложная задача. Рефлексия в интерцепторах помогает динамически извлекать и изменять информацию о данных запроса в реальном времени, что способствует большей гибкости в обработке пользовательских запросов.
Использование рефлексии и grpc-интерцепторов помогает в создании более адаптивных и ориентированных на клиента решений, обеспечивая конкурентное преимущество и улучшая пользовательский опыт.
Ну правда, зачем клиентам возвращать ошибку при валидном JWT-токене, если туда случайно попал пробел или перенос строки?
Кроме того, разработчикам не нужно заранее предугадывать все возможные варианты данных в запросах, что в свою очередь позволяет более оперативно реагировать на обратную связь от клиентов и внедрять необходимые изменения с минимальными усилиями.
Другие публикации в блоге:
ссылка на оригинал статьи https://habr.com/ru/articles/861668/
Добавить комментарий