gRPC на стороне клиента

Большинство компонентов межсистемного взаимодействия, использующих REST, сериализуют свои данные в формате JSON. На данный момент, JSON не имеет широко используемого стандарта валидации схем: JSON Schema не получил широкого распространения. Стандартная валидация схем позволяет делегировать проверку третьей стороне и забыть о ней. Без такого стандарта мы вынуждены возвращаться к ручной валидации в коде. Хуже того, нам приходится поддерживать валидационный код в соответствии со схемой.

XML имеет встроенную валидацию схем: XML-документ может объявлять грамматику, к которой он должен соответствовать. SOAP, базируясь на XML, также извлекает выгоду из этого.

Другие альтернативы сериализации имеют возможность валидации схем: например, Avro, Kryo, и Protocol Buffers. Интересно, что gRPC использует Protobuf для обеспечения RPC между распределенными компонентами:

gRPC — это современный открытый источник высокопроизводительный фреймворк для удаленного вызова процедур (RPC), который может работать в любой среде. Он эффективно соединяет сервисы в пределах и через дата-центры с поддержкой балансировки нагрузки, отслеживания, проверки работоспособности и аутентификации. Также он применим в последней миле распределенных вычислений для соединения устройств, мобильных приложений и браузеров с сервисами на серверном уровне.

Почему gRPC?

Более того, Protocol представляет собой двоичный механизм сериализации, экономящий значительное количество пропускной способности. Таким образом, gRPC является превосходным выбором для межсистемного взаимодействия. Но что делать, если все ваши компоненты используют gRPC, как тогда простые клиенты могут обращаться к ним? В этой статье мы создадим сервис gRPC и покажем, как к нему обращаться через cURL.

A Simple gRPC Service

Документация по gRPC обширна, поэтому вот краткое изложение:

  • gRPC — это фреймворк для удаленного вызова процедур.
  • Он поддерживает широкий спектр языков.
  • Основан на Protocol Buffers:
Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.

  • Он входит в портфель CNCF и находится на стадии инкубации.

Давайте настроим наш сервис gRPC. Мы будем использовать Java, Kotlin, Spring Boot и специализированный проект интеграции gRPC с Spring Boot. Структура проекта включает два проекта: один для модели и один для кода. Начнем с проекта модели.

I didn’t want something complicated. Reusing a simple example is enough: the request sends a string, and the response prefixes it with Hello. We design this model in a dedicated Protobuf schema file:

ProtoBuf

 

syntax = "proto3";                                        //1

package ch.frankel.blog.grpc.model;                       //2

option java_multiple_files = true;                        //3
option java_package = "ch.frankel.blog.grpc.model";       //3
option java_outer_classname = "HelloProtos";              //3

service HelloService {                                    //4
    rpc SayHello (HelloRequest) returns (HelloResponse) {
    }
}

message HelloRequest {                                    //5
    string name = 1;                                      //6
}

message HelloResponse {                                   //7
    string message = 1;                                   //6
}

  1. Определение версии Protobuf
  2. Пакет
  3. Конфигурация для Java
  4. Определение сервиса
  5. Определение запроса
  6. Определение поля: сначала тип, затем имя и, наконец, порядок.
  7. Определение ответа

Мы будем использовать Maven для генерации шаблонного Java-кода:

XML

 

<project>
  <dependencies>
    <dependency>
      <groupId>io.grpc</groupId>                         <!--1-->
      <artifactId>grpc-stub</artifactId>
      <version>${grpc.version}</version>
    </dependency>
    <dependency>
      <groupId>io.grpc</groupId>                         <!--1-->
      <artifactId>grpc-protobuf</artifactId>
      <version>${grpc.version}</version>
    </dependency>
    <dependency>
      <groupId>jakarta.annotation</groupId>              <!--1-->
      <artifactId>jakarta.annotation-api</artifactId>
      <version>1.3.5</version>
      <optional>true</optional>
    </dependency>
  </dependencies>
  <build>
    <extensions>
      <extension>
        <groupId>kr.motd.maven</groupId>                 <!--2-->
        <artifactId>os-maven-plugin</artifactId>
        <version>1.7.1</version>
      </extension>
    </extensions>
    <plugins>
      <plugin>
        <groupId>org.xolstice.maven.plugins</groupId>    <!--3-->
        <artifactId>protobuf-maven-plugin</artifactId>
        <version>${protobuf-plugin.version}</version>
        <configuration>
          <protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
          <pluginId>grpc-java</pluginId>
          <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
              <goal>compile-custom</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

  1. Зависимости времени компиляции
  2. Информация о операционной системе для использования в следующем плагине
  3. Генерация Java-кода из файла proto.

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

Мы можем упаковать классы в JAR и использовать их в веб-проекте. Последний написан на Kotlin, но только потому, что это мой любимый язык JVM.

Нам нужен только конкретный стартер зависимости Spring Boot для интеграции конечных точек gRPC с Spring Boot:

XML

 

<dependency>
  <groupId>net.devh</groupId>
  <artifactId>grpc-server-spring-boot-starter</artifactId>
  <version>2.14.0.RELEASE</version>
</dependency>

Вот ключевой момент:

Kotlin

 

@GrpcService                                                        //1
class HelloService : HelloServiceImplBase() {                       //2
  override fun sayHello(
      request: HelloRequest,                                        //2
      observer: StreamObserver<HelloResponse>                       //3
  ) {
    with(observer) {
      val reply = HelloResponse.newBuilder()                        //2
                               .setMessage("Hello ${request.name}") //4
                               .build()
      onNext(reply)                                                 //5
      onCompleted()                                                 //5
    }
  }
}

  1. Стартер grpc-server-spring-boot-starter обнаруживает аннотацию и проводит свое волшебство.
  2. Ссылочные классы, сгенерированные в вышеупомянутом проекте
  3. Сигнатура метода позволяет параметру StreamObserver. Класс поставляется с grpc-stub.jar
  4. Получить запрос и добавить префикс для создания ответного сообщения.
  5. Воспроизвести события. 

Теперь мы можем запустить веб-приложение с помощью ./mvnw spring-boot:run.

Тестирование gRPC сервиса

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

Shell

 

grpcurl --plaintext localhost:9090 list   #1-2

  1. Список всех доступных gRPC сервисов без проверки TLS. 
  2. Чтобы избежать конфликтов между gRPC и другими каналами, например, REST, Spring Boot использует другой порт.
Plain Text

 

ch.frankel.blog.grpc.model.HelloService   #1
grpc.health.v1.Health                     #2
grpc.reflection.v1alpha.ServerReflection  #2

  1. Определенный нами gRPC сервис
  2. Два дополнительных сервиса, предоставленных пользовательским стартером

Мы также можем изучить структуру сервиса:

Shell

 

grpcurl --plaintext localhost:9090 describe ch.frankel.blog.grpc.model.HelloService
Java

 

service HelloService {
  rpc SayHello ( .ch.frankel.blog.grpc.model.HelloRequest ) returns ( .ch.frankel.blog.grpc.model.HelloResponse );
}

Наконец, мы можем вызвать сервис с данными:

Shell

 

grpcurl --plaintext -d '{"name": "John"}' localhost:9090 ch.frankel.blog.grpc.model.HelloService/SayHello
JSON

 

{
  "message": "Hello John"
}

Доступ к gRPC-сервису с помощью стандартных инструментов

Представьте, что у нас есть стандартное клиентское JavaScript-приложение, которое должно получать доступ к gRPC-сервису. Какие есть альтернативы?

Общая методика заключается через grpc-web:

A JavaScript implementation of gRPC for browser clients. For more information, including a quick start, see the gRPC-web documentation.

Клиенты gRPC-web соединяются с gRPC-сервисами через специальный прокси; по умолчанию gRPC-web использует Envoy.

В будущем мы ожидаем поддержки gRPC-web в языковых веб-фреймворках для таких языков, как Python, Java и Node. Подробности см. в плане.

grpc-web

Описание указывает на единственное ограничение: оно работает только для JavaScript (пока что). Однако есть и другое. Оно довольно интрузивно. Вам нужно получить файл proto, сгенерировать шаблонный код и заставить ваш код вызывать его. Вы должны делать это для каждого типа клиента. Хуже того, если файл proto изменится, вам нужно будет перегенерировать клиентский код во всех них.

Существует альтернатива, однако, если вы используете API Gateway. Я опишу, как это сделать с Apache APISIX, но, возможно, другие шлюзы могут делать то же самое. grpc-transcode — это плагин, который позволяет транскодировать REST-вызовы в gRPC и обратно.

Первым шагом является регистрация файла proto в Apache APISIX:

Shell

 

curl http://localhost:9180/apisix/admin/protos/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d "{ \"content\": \"$(sed 's/"/\\"/g' ../model/src/main/proto/model.proto)\" }"

Второй шаг – создание маршрута с помощью вышеупомянутого плагина:

Shell

 

curl http://localhost:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
  "uri": "/helloservice/sayhello",                           #1
  "plugins": {
    "grpc-transcode": {
      "proto_id": "1",                                       #2
      "service": "ch.frankel.blog.grpc.model.HelloService",  #3
      "method": "SayHello"                                   #4
    }
  },
  "upstream": {
    "scheme": "grpc",
    "nodes": {
      "server:9090": 1
    }
  }
}'

  1. Определение детального маршрута. 
  2. Ссылка на прото файл, определенный в предыдущей команде. 
  3. gRPC сервис
  4. gRPC метод

На данном этапе, любой клиент может отправить HTTP запрос к определенному конечной точке. Apache APISIX транслирует вызов в gRPC, пересылает его к определенному сервису, получает ответ и транслирует его снова.

Shell

 

curl localhost:9080/helloservice/sayhello?name=John
JSON

 

{"message":"Hello John"}

По сравнению с grpc-web, подход API Gateway позволяет делиться proto файлом с одним компонентом: сам Gateway.

Преимущества Транскрибирования

На данном этапе, мы можем использовать возможности API Gateway. Представьте, что мы хотим иметь дефолтное значение, если не передано имя, например, World. Разработчики с удовольствием установят его в коде, но любые изменения значения потребуют полного билда и развертывания. Изменения могут быть почти мгновенными, если мы поместим дефолтное значение в цепочку обработки маршрутов Gateway. Давайте изменим наш маршрут соответственно:

Shell

 

curl http://localhost:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
  "uri": "/helloservice/sayhello",
  "plugins": {
    "grpc-transcode": {
      ...
    },
    "serverless-pre-function": {                    #1
      "phase": "rewrite",                           #2
      "functions" : [
        "return function(conf, ctx)                 #3
          local core = require(\"apisix.core\")
          if not ngx.var.arg_name then
            local uri_args = core.request.get_uri_args(ctx)
            uri_args.name = \"World\"
            ngx.req.set_uri_args(uri_args)
          end
        end"
      ]
    }
  },
  "upstream": {
      ...
  }
}'

  1. Генерический универсальный плагин, когда ни один не подходит
  2. Переписать запрос.
  3. Магический Lua код, который делает трюк

Теперь, мы можем выполнить запрос с пустым аргументом и получить ожидаемый результат:

Shell

 

curl localhost:9080/helloservice/sayhello?name
JSON

 

{"message":"Hello World"}

Заключение

В этом посте мы кратко описали gRPC и то, как он улучшает межсервисную коммуникацию. Мы разработали простой сервис gRPC с использованием Spring Boot и grpc-server-spring-boot-starter. Однако это имеет свою цену: обычные клиенты не могут получить доступ к сервису. Мы были вынуждены использовать grpcurl для его тестирования. То же самое касается клиентов на JavaScript или браузерах.

Чтобы обойти это ограничение, мы можем использовать API Gateway. Я продемонстрировал, как настроить Apache APISIX с плагином grpc-transcode для достижения желаемого результата.

Полный исходный код для этого поста можно найти на GitHub.

Дальнейшее изучение

Source:
https://dzone.com/articles/grpc-on-the-client-side