UnitBean Блог

Как мы ускорили клиент-серверное взаимодействие в несколько раз

Мы просто делали приложение КУРЬЕРА

В проекте ChistoApp у нас есть приложение курьера. Приложение каждую секунду отсылает запрос о своем местоположении, чтобы обновлять его для клиента химчистки на карте да еще и в админке оператора. Водитель так же должен делать фото каждой вещи, которую он отдает клиенту, и сумку с вещами, когда он ее принимает и пломбирует. Фоток получается много. Да и вообще, в том же общепите, фоток товаров может много лететь, что трафик может не справляться, а показать их надо. Ведь одно фото нам стоит 1000+ рублей, а так же фото товара напрямую влияет на конверсию и продажу. И вот мы задались вопросом, как уже решить эту проблему? Как ускорить взаимодействие клиент сервера?

Данная статья ставит целью познакомить читателя с технологией gRPC и не может служить рабочей документацией. Ссылки на официальную документацию с исчерпывающим описанием приведен в конце статьи в блоке дополнительные материалы

Что такое gRPC?

gRPC - это система удаленного вызова процедур (Remote Procedure Call), разработанная Google в 2015 году

В основе данной технологии лежит следующая концепция: локальный код осуществляет обращение к процедуре, происходящей на удаленной машине, т.е. в рамках кода нет чёткого разделения на обращение к локальным функциям или к ресурсам удаленной машины

Выделяют 4 основных вида взаимодействия в рамках gRPC:

  • одинарный вызов удаленной процедуры (unary RPC) - единичный запрос с получением ответа, полный аналог функционала REST API (POST, GET, etc. запросов). Возможно наличие аргументов запроса и контента в теле ответа
  • клиентский поток - открытие канала передачи данных со стороны клиента до сервера, куда клиент может в любое время передать сообщение. По окончанию взаимодействия требует явного события закрытия
  • серверный поток - аналогичное открытие канала передачи данных, но со стороны сервера, откуда клиент может также получать поток данных
  • двунаправленный поток - совмещение клиентского и серверного потоков, когда обе стороны являются и источниками, и потребителями сообщений

Технология реализует поддержку metadata - аналога HTTP Headers - используемую для дополнительной передачи информации в формате "ключ-значение", однако для этого есть ряд ограничений:

  • контент ключей и значений (поддерживаются символы только из ASCII-таблицы)
  • возможность конфликта с общей семантикой gRPC, в рамках которой существует подход "значений по-умолчанию". В случае с metadata существует вероятность того, что значения по искомому ключу не окажется, и будет выброшено исключение обращения к несуществующей позиции
  • В этой связи использование metadata в качестве регулярного способа передачи ценной информации не рекомендуется

Процедуры и сервисы описываются в специальных .proto-файлах по стандарту proto3. Существует возможность выделить список этих файлов в отдельный репозиторий и подключать его как подмодуль в существующие репозитории всех зависимых конечных модулей

Отличие от REST API

Первым основным отличием является базирование на протоколе HTTP/2.0, который дает ряд преимуществ по сравнению с разнообразием протоколов, находящихся под REST API

Вторым основным отличием является использование protocol buffers в качестве протокола передачи данных. Это дает более строгую спецификацию по сравнению с JSON/XML, а также сокращает накладные издержки для обработки и передачи каждого сообщения

Также gRPC создаёт единый протокол клиент-серверного взаимодействия на различных платформах (web, backend (Spring Boot), android, ios, etc.). В случае REST API каждая из платформ решает проблему интеграции самостоятельно, с помощью различных библиотек

Поддержка потоков (streams) - серверных, клиентских, двунаправленных. Их наличие даёт широкие возможности по реализации нестандартных задач, например, построение чата внутри системы, или эффективная отправка файлов

Пример запроса на gRPC

На стороне клиента используется код (приведены выдержки), полный код доступен по ссылке на репозиторий:
// stub - это сервис-заглушка, которая принимает запросы
private final GreeterGrpc.GreeterBlockingStub blockingStub;
...
// создание канала связи. target - это адрес сервера
ManagedChannel channel = ManagedChannelBuilder.forTarget(target)
// каналы поддерживают защищенное соединение (через SSL/TLS). для упрощения примера будем использовать незащищенное соединение
  .usePlaintext()
  .build();
// создание сервиса-заглушки
blockingStub = GreeterGrpc.newBlockingStub(channel);
...
// создание запроса с указанием имени. если не указывать явно, то отправится значение по-умолчанию
HelloRequest request = HelloRequest.newBuilder().setName(name).build();
// отправка запроса в процедуру sayHello
final HelloReply response = blockingStub.sayHello(request);
Также на стороне сервера, полный код доступен в репозитории:
syntax = "proto3";

package helloworld;

// Объявление сервиса приветствия
service Greeter {
  // Процедура приветствия
  rpc SayHello (HelloRequest) returns (HelloReply)
}

// Сообщение запроса, содержащее имя пользователя
message HelloRequest {
  string name = 1;
}

// Сообщение ответа, содержащее фразу-приветствие
message HelloReply {
  string message = 1;
}
Как генерируем код на backend и Android

Генерация кода осуществляется автоматически с помощью подключаемого плагина компилятора. Для Backend (Spring Boot) это делается с помощью плагина для maven, а для Android используется плагин для gradle. Поддерживается также ручная генерация с помощью команды:

protoc [OPTION] PROTO_FILES

Сгенерированный плагинами код располагается по следующим папкам:

  • Backend путь: /target/classes
  • Android путь: /build/generated/source/proto/

Конфигурация nginx

В процессе адаптации имеющихся технологий к gRPC была получена следующая конфигурация nginx-сервера:
server {
  server_name courier.unitbeandev.com;
  location / {
    grpc_pass grpc://localhost:50051; # ключевой момент, необходимо вместо proxy_pass http://localhost:{port}. Указание одного и того же порта важно на сервер и на клиенте
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Port $server_port;
    client_max_body_size 50m;
  }

  listen 443 ssl http2; # также важный момент конфигурации, поддержка HTTP/2.0
  ssl_certificate /etc/letsencrypt/live/courier.unitbeandev.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/courier.unitbeandev.com/privkey.pem;
  include /etc/letsencrypt/options-ssl-nginx.conf;
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

server {
  if ($host = courier.unitbeandev.com) {
    return 301 https://$host$request_uri;
  }
  server_name courier.unitbeandev.com;
  listen 80;
  return 404;
}

Обратная совместимость процедур

В процессе работы над системой неизбежны изменения контракта клиент-серверного взаимодействия. Для этих целей в стандарте proto предусмотрены целочисленные индексы для каждого из полей сообщения. Индексы полей должны быть уникальны для каждого поля в рамках отдельного сообщения. При добавлении поля ему должен присваиваться новый индекс. При удалении ненужного поля его индекс должен удаляться или вноситься в специальный список зарезервированных индексов, чтобы было невозможно его переиспользовать в будущем, получив явное указание на конфликт полей.

Таким образом, каждое из полей сообщения является уникальным и для клиентов разных версий работа будет происходить только с теми полями, про которые знает приложение. На стороне сервера работа будет происходить сразу со всеми полями в рамках тех стандартов версионирования кода, которое будет принято в системе

ЧТО ПО СКОРОСТИ?

В рамках исследования было произведено несколько замеров разницы в производительности с открытым исходным кодом, результаты приведены в следующим ссылкам:


В среднем gRPC имеет преимущество в быстродействии над REST в 7-10 раз

Разница с REST

Также было проведено исследование замера скорости исполнения аналогичных запросов на имеющихся проектах:

ОперацияgRPC (ms)REST (ms)получение данных (GET)55-15070-350отправка файла (4мб)5001900-2300отправка данных (POST)55-15070-550

По результатам сравнения наблюдается ощутимая разница между технологиями, особенно при отправке файлов. Также наблюдается небольшая разница и в регулярных запросах, причем среднее время исполнения запроса (~75мс в большинстве случаев) позволяет создавать системы, в которых задержка взаимодействия с сервером визуально почти не заметна (не фиксируется в пределах до 200мс)

Также стоит отметить, что преимущество в скорости передачи данных накладывает определенные требования на качество работы серверного приложения: требуется корректная настройка количества памяти, использование эффективных алгоритмов работы внутри кода и сведение к минимуму возможных задержек, что обеспечит высокую производительность. Только в данном случае применение gRPC будет наиболее эффективным и обладать заметной полезностью

Существующий механизм полей по-умолчанию предусматривает следующую логику: если значение поля в сообщении является равным значению по-умолчанию, то передача данного поля не осуществляется, что также положительно сказывается на скорости и количестве передаваемых данных

Выводы и области применения

Подводя черту, можно сказать, что gRPC - это достойная альтернатива |золотому стандарту" в виде REST и может быть полезен в применении на тех проектах, где требуется широкий спектр клиент-серверного взаимодействия, либо в проектах, в которых требуется максимальная скорость общения частей системы между собой

Стоит отметить, что не рассмотренным остался широкий спектр задач, решаемых gRPC в рамках микросервисной архитектуры. Для большого количества отдельных сервисов общение между собой по gRPC может дать кумулятивный эффект улучшения скорости работы системы в целом

Также весьма эффективно будет применение данной технологии в клиент-серверных системах, в которых передаются большие массивы данных, например, в приложениях доставки еды, каталогах, сервисах отслеживания курьера, приложениях для работы с передачей файлов по сети, приложениях с возможностями чата

Стоит отметить некоторый входной порог в gRPC - базовые знания технологии необходимы всем разработчикам, работающим с данной технологией. Однако, в целом технология не сложна, и весьма хорошо описана как разработчиками, так и сообществом пользователей

Прикладной инструментарий


Дополнительные материалы