Pull to refresh

Распределенные сервисы с применением gRPC

Reading time12 min
Views28K
Original author: Fabian Zankl

Часто бывает так, что эффективная коммуникация – один из основных движущих факторов в современных программных системах, даже в мире, живущем по законам микросервисной архитектуры. Технология gRPC может справляться с этими требованиями. В этой статье будут рассмотрены некоторые основы gRPC, а еще мы реализуем первое клиент-серверное приложение с применением .NET. Кроме того, клиент на основе Python демонстрирует, насколько эффективной может быть коммуникация между различными сервисами.

Что такое gRPC?

gRPC – это протокол, обеспечивающий коммуникацию между различными конечными точками в распределенной системе. Он использует HTTP/2 и буферы протоколов (protobuf). Как и любая система вызовов удаленных процедур, gRPC состоит из сервера, определяющего методы и отклики, которые может вызывать клиент. Далее клиенты могут реализовать специфическую заглушку сервера и потреблять предоставляемые методы, как показано на следующей иллюстрации для .NET и Python.

gRPC может использовать protobuf в качестве языка для определения интерфейсов и в качестве формата для обмена передаваемыми сообщениями. Он поддерживает многие языки программирования, в частности, Java для Android, C#, C++, Dart, Go, Java, Kotlin, Node.js, Objective-C, PHP, Python и Ruby. Инструментарий для разработки программ (SDK) часто поддерживает реализации, специфичные для конкретного языка. Таким образом, на основе gRPC можно быстро реализовывать распределенные системы или микросервисы, написанные на разных языках – в зависимости от предпочтений разработчиков.

Буферы протоколов

"Буферы протоколов – это разработанный Google расширяемый механизм, не зависящий от языка и платформы и предназначенный для сериализации структурированных данных"(https://developers.google.com/protocol-buffers). По умолчанию gRPC использует буферы протоколов (protobuf) в качестве языка для обмена данными и определения интерфейса. Буферы протоколов – это механизм с открытым исходным кодом. В этом разделе мы рассмотрим, как можно использовать protobuf. Начнем с простого файла foo.proto, где определяется, как структурировать данные, предназначенные для сериализации.

message Foo {
  int32 id = 1;
  string description = 2;
}

В предыдущем примере показано, как protobuf использует сообщения для структурирования данных. В каждом буфере содержится набор пар вида «имя-значение», и такая пара называется «поле». Следовательно, сообщение, записанное в protobuf, можно воспринимать как своеобразный класс или структуру из языков для объектно-ориентированного программирования.

Теперь давайте скомпилируем определение сообщения из protobuf в соответствующие типы, поддерживаемые в вашем любимом языке программирования. Для этой цели можно скачать на релизной странице GitHub компилятор protoc для работы с protobuf. (https://github.com/protocolbuffers/protobuf/releases/)

Следующая команда для языка C# сгенерирует классы, которые затем можно будет использовать через API буфера протоколов для отправки и получения сообщений.

protoc \
  -I=$src-directory \
  --csharp_out=$destination-directory \
  $src-directory/foo.proto

В нашем примере полученный в результате код C# состоит из:

  • Класса FooReflection, содержащего метаданные о сообщении protobuf

  • Класса Foo, реализующего интерфейс IMessage<Foo> и предоставляющего свойства Id и Description.

Кроме того, класс Foo предоставляет методы, связанные с сериализацией и десериализацией. Чтобы отправить и получить сообщение, используйте в коде на C# методы, показанные в следующем примере. В данном случае применяется поток памяти, но на его месте может быть и любой другой поток. Обратите внимание на пакет Google.Protobuf из NuGet: он нужен для считывания из потоков и записи в них.

using System;
using System.IO;
using Google.Protobuf;

namespace GrpcSamples
{
  public class Program
  {
    public static void Main(string[] args)
    {
      var foo = new Foo
      {
        Id = 1,
        Description = "FooMessage"
      };

      // Запись в любой поток
      var stream = new MemoryStream();
      foo.WriteTo(stream);

      stream.Seek(0, SeekOrigin.Begin);

      // Создание объекта Foo из любого потока
      var parsedFoo = Foo.Parser.ParseFrom(stream);

      Console.WriteLine($"Foo: Id {parsedFoo.Id} - {parsedFoo.Description}");
    }
  }
}

Подробнее о том, как protoc генерирует код C#, рассказано здесь: https://developers.google.com/protocol-buffers/docs/reference/csharp-generated

По спецификации gRPC, protobuf также поддерживает определение интерфейсов.  protoc поставляется с дополнительным плагином для генерации клиентского и серверного кода. В следующем примере показан небольшой образец определения интерфейса в gRPC.

syntax = "proto3";
option csharp_namespace = "GrpcSamples";

service FooService {
  rpc GetFoo (FooRequest) returns (FooResponse);
}

message FooRequest {
  string message = 1;
}

message FooResponse {
  string message = 1;
}

В предыдущем примере определяется сервис gRPC под названием FooService. Этот сервис предоставляет унарный метод GetFoo. Как видите, объекты запроса и отклика определяются как сообщения protobuf. Опять же, protoc может использоваться для генерации кода на вашем любимом языке программирования.

protoc \
  -I=$src-directory \
  --plugin=protoc-gen-grpc=grpc_csharp_plugin \
  --csharp_out=$destination-directory \
  --grpc_out=$destination-directory \
  --grpc_opt=lite_client,no_server \
  $src-directory/foo.proto

В случае C# лучше всего использовать здесь пакет Grpc.Tools из NuGet. Ниже в этой статье мы взглянем на интеграционный инструмент, предназначенный для работы с C#. Если для определения интерфейса применяется gRPC, а в качестве языка программирования - C#, то результирующий код состоит из:

  • В общем случае:

    • Класс 'FooReflection', содержащий данные о запросе, содержащемся в protobuf, и о сообщении, получаемом в качестве отклика.

    • Класс FooRequest, реализующий интерфейс IMessage<FooRequest> и предоставляющий свойства для gRPC-запроса от метода GetFoo.

    • Класс FooResponse, реализующий интерфейс IMessage<FooResponse> и предоставляющий поля для gRPC-отклика от метода GetFoo.

  • В случае генерации серверного кода:

    • Класс FooServiceBase, действующий в качестве базового класса для конкретной реализации сервиса.

  • В случае генерации клиентского кода:

    • Класс FooServiceClient, содержащий заглушки для взаимодействия с серверной частью через gRPC-канал.

Паттерны коммуникации

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

  • Унарные RPC

Клиент отправляет запрос на сервер и получает в ответ единственный отклик

rpc GetFoo (FooRequest) returns (FooResponse);
  • Сервер выдает поток вызовов удаленных процедур (RPC)

Клиент отправляет запрос на сервер и получает поток для считывания последовательности сообщений. В рамках определения интерфейса такой порядок объявляется так: до отклика ставится дополнительное ключевое слово stream.

rpc GetFoos(FooRequest) returns (stream FooResponse);
  • Клиент выдает поток вызовов удаленных процедур (RPC)

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

rpc SendFoos(stream FooRequest) returns (FooResponse);
  • Двунаправленные потоковые вызовы удаленных процедур

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

rpc SendAndGetFoos(stream FooRequest) returns (stream FooResponse);

При использовании gRPC связь может быть разорвана клиентом, разорвана сервером, либо разорвана на основе задержек. Клиент и сервер независимо и локально определяются с тем, в каком состоянии вызов. Таким образом, потоковый вызов может удачно пройти на сервере, но неудачно – на клиенте, из-за задержки. В случае отмены любые изменения, внесенные клиентом или сервером, автоматически не откатываются. Соединение разрывается немедленно. Детали поведения, касающиеся обработки соединения и метаданных, зачастую зависят от языка.

Первое серверное и клиентское приложение на C#

Итак, давайте приступим к реализации наших первых полнофункциональных приложений (клиентского и серверного) на .NET с использованием gRPC. В Visual Studio легко создать оба приложения. При помощи мастера проектов поищите по запросу gRPC и следуйте шагам, которые показаны на следующем рисунке. Как вариант, можно включить поддержку Docker, но в данной статье мы хотим заострить внимание не на этом.

Затем Visual Studio создает проект gRPC-сервера, и мы готовы приступить к делу. Изменив определение интерфейса, основанное на protobuf, и реализовав собственную бизнес-логику внутри сгенерированных файлов, можно завершить работу над нашим gRPC-сервером. Но что при этом происходит за кулисами?

Отметим, что Visual Studio создает для реализации сервера файл protobuf вместе с классом. Но и это еще не все. Файл *.csproj явно ссылается на наш protobuf при помощи тега.

<ItemGroup>
  <Protobuf Include="Protos\foo.proto"  GrpcServices="Server" />
</ItemGroup>

Эта запись приказывает Visual Studio трактовать этот файл как ввод для генерации клиента и сервера gRPC. Свойство GrpcServices указывает, какие части должна сгенерировать Visual Studio.

Вторая важная ссылка внутри файла *csproj-file указывает на пакет Grpc.AspNetCore из NuGet.

<ItemGroup>
  <PackageReference Include="Grpc.AspNetCore" Version="2.27.0" />
</ItemGroup>

Благодаря этому пакету, Grpc.Tools также будет доступен в нашем решении. Grpc.Tools обеспечивает поддержку, необходимую для обработки файлов protobuf при помощи компилятора protoc. При такой простейшей настройке связанный файл protobuf компилируется при каждой сборке, либо, когда это инициируется через запись «Run Custom Tool» (Запустить собственный инструмент) из контекстного меню.

Поскольку серверные приложения gRPC основаны на ASP.NET Core, в нашем проекте содержится файл Startup.cs. Внутри этого файла легко активировать поддержку gRPC при помощи AddGrpc и MapGrpcService<T>, где T – реализация нашего сервиса gRPC.

public void ConfigureServices(IServiceCollection services)
{
  services.AddGrpc();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
  app.UseRouting();

  app.UseEndpoints(endpoints =>
  {
    endpoints.MapGrpcService<DefaultFooService>();
  });
}

gRPC использует HTTP/2 в качестве протокола передачи данных. Соответственно, нужно сконфигурировать Kestrel (веб-сервер, используемый в ASP.NET Core) при помощи appsettings.json или в Program.cs В следующем примере мы сообщаем всем конечным точкам, что в качестве протокола передачи данных нужно использовать HTTP/2.

"Kestrel": {
  "EndpointDefaults": {
    "Protocols": "Http2"
  }
}

В качестве альтернативы можно указать множество конечных точек и для каждой задать свою специфическую конфигурацию. Например, такую настройку можно использовать, когда в одном проекте располагаются REST API и gRPC API, и вы при этом хотите, чтобы REST API использовал для передачи данных протокол HTTP/1.

"Kestrel": {
  "Endpoints": {
    "WebApi": {
      "Url": "http://*:5002",
      "Protocols": "Http1"
    },
    "gRPC": {
      "Url": "https://*:5001",
      "Protocols": "Http2"
    }
  }
}

В дальнейшем конечные точки gRPC должны быть защищены по технологии TLS (протокол защиты транспортного уровня). В ходе разработки автоматически генерируется конечная точка, защищенная TLS. Обычно система предлагает вам довериться соответствующему сертификату, но вы сами можете инициировать это действие через командную строку: dotnet dev-certs https --trust. В производстве TLS необходимо сконфигурировать при помощи appsettings.json или Program.cs, указав параметры именно вашего сертификата. При помощи свойства Certificate все требуемые параметры можно предоставить Kestrel.

{
  "Kestrel": {
    "Endpoints": {
      "gRPC": {
        "Url": "https://*:5001",
        "Protocols": "Http2",
        "Certificate": {
          "Path": "<path to .pfx file>",
          "Password": "<certificate password>"
        }
      }
    }
  }
}

Теперь время построить наше первое клиентское приложение. Просто создайте новое консольное приложение для .NET Core и установите из NuGet пакеты Google.ProtobufGrpc.Net.Client и Grpc.Tools. После этого останется добавить ссылку на файл protobuf, которым вы уже пользовались при создании серверного приложения. Тот файл *.csproj, который у вас получится, может выглядеть примерно так:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Protobuf Include="..\..\protos\foo.proto" GrpcServices="Client">
      <Link>Protos\foo.proto</Link>
    </Protobuf>
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Google.Protobuf" Version="3.13.0" />
    <PackageReference Include="Grpc.Net.Client" Version="2.33.1" />
    <PackageReference Include="Grpc.Tools" Version="2.33.1">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
</Project>

Обратите внимание: для свойства GrpcServices тега <Protobuf/> устанавливается значение Client. Так мы приказываем компилятору protoc создать клиентские заглушки, которые будут стоять на месте соответствующих серверных частей.

Приступаем к созданию клиента. Сначала необходимо создать GrpcChannel при помощи URI нашего серверного gRPC-приложения. На втором шаге можно будет через этот канал организовать начальную настройку нашего gRPC-клиента. При помощи этого клиента мы сможем вызвать все методы, которые определим. В следующем образце показан унарный RPC-вызов. Для использования gRPC-клиента без поддержки TLS в .NET Core 3.x необходимо сделать еще один шаг. Переключатель System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport в контексте нашего приложения должен быть установлен в true. Приложениям, использующим фреймворк .NET 5, дополнительная конфигурация не требуется, но, чтобы вызывать незащищенные  gRPC-сервисы, они должны использовать Grpc.Net.Client, версия 2.32.0 или выше.

// .NET Core 3.x
// Для использования gRPC без TLS необходимо установить следующий переключатель, и только потом создавать GrpcChannel/HttpClient.
// AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);

var options = new GrpcChannelOptions();
var channel = GrpcChannel.ForAddress("https://localhost:5001", options);
var client = new FooService.FooServiceClient(channel);

// Асинхронный унарный вызов удаленной процедуры
var foosRequest = new FooRequest { Message = "Sample request" };
var fooResponse = await client.GetFooAsync(foosRequest);

Console.WriteLine($"\t{fooResponse.Message}");

При использовании приложения ASP.NET Core, реализация gRPC-клиента будет еще проще, если использовать интеграцию с HttpClientFactory. Поэтому просто установите пакет Grpc.Net.ClientFactory и зарегистрируйте сервис gRPC при запуске:

public void ConfigureServices(IServiceCollection services)
{
  services.AddGrpcClient<FooService.FooServiceClient>(options =>
  {
    options.Address = new Uri("https://localhost:5001");
  });
}

Для gRPC-клиента регистрируется как переходный в рамках механизма внедрения зависимостей, действующего в ASP.NET Core. Таким образом, клиент легко поддается внедрению и может потребляться в типах, создаваемых методом внедрения зависимостей – например, в контроллерах.

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

Встреча Python и .NET

Поскольку технология gRPC доступна на разных языках программирования, именно она обеспечивает коммуникацию между ними. Представьте себе множество микросервисов. Один разработан на C#, другой на Node.js, третий на Python. Конечно же, все они интегрируются. В этом разделе мы сосредоточимся на коммуникации между .NET и Python. (Обратите внимание: чтобы следующие примеры работали, в соответствующей системе требуется установить Python и pip).

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

# Установить зависимость grpcio и инструменты grpcio-tools, при помощи которых генерируются заглушки 
python -m pip install grpcio grpcio-tools

# Сгенерировать заглушки из файла protobuf 
python -m grpc_tools.protoc \
  -I../../protos \
  --python_out=. \
  --grpc_python_out=. ../../protos/foo.proto

В результате этого процесса получится два файла:

  • foo_pb2.py, в котором содержатся сгенерированные классы запроса и отклика

  • foo_pb2_grpc.py, в котором содержатся сгенерированные клиентские и серверные классы.

Наш gRPC-сервер работает на основе ASP.NET Core и использует TLS для защищенной коммуникации, поэтому первым делом нам потребуется экспортировать сертификат разработки .NET. Для этого нужно открыть менеджер сертификатов Windows (Windows Certificate Manager) командой certmgr. Оказавшись в менеджере сертификатов, перейдите в Current user > Personal > Certificates и экспортируйте сертификат, для которого задано понятное имя ASP.NET Core HTTPS development certificate. Пожалуйста, используйте следующие опции экспорта: Without key, DER-coded X.509 (.cer). Python по умолчанию поддерживает сертификаты в формате PEM. Поэтому самый легкий путь – преобразовать наш сертификат в такой формат. Это можно сделать командой openssl x509 -inform der -in localhost.cer -out localhost.pem.

Наконец, давайте рассмотрим примерную реализацию gRPC-клиента на Python. Мы используем сгенерированные gRPC-заглушки и наш сертификат, чтобы создать простейший тестовый клиент, как показано в следующем листинге:

import grpc

# Импортировать зависимости, созданные при помощи инструментов gRPC 
import foo_pb2
import foo_pb2_grpc

def get_foo(stub):
    foo = stub.GetFoo(foo_pb2.FooRequest(message = "Sample request"))
    print("GetFoo called %s" % (foo.message))

def run():
    # Защищенный канал, использующий сертификат разработки под dotnet 
    credential = grpc.ssl_channel_credentials(open('localhost.pem', 'rb').read())

    with grpc.secure_channel('localhost:5001', credential) as channel:
        stub = foo_pb2_grpc.FooServiceStub(channel)

        print("-------------- GetFoo --------------")
        get_foo(stub)


if __name__ == '__main__':
    run()

Теперь можно запустить приложение Python при помощи python client.py. Этот скрипт вызывает наш gRPC-сервер, основанный на ASP.NET Core. Работа ведется по защищенному каналу связи, а отклик сервера выводится в консоль.

Заключение

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

Подробнее эта тема раскрыта в следующих источниках:

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

Tags:
Hubs:
+7
Comments5

Articles

Information

Website
piter.com
Registered
Founded
Employees
201–500 employees
Location
Россия