Pull to refresh
0

gRPC: лучше ли этот фреймворк, чем REST API?

Reading time9 min
Views32K

Привет! Я — Роман Махнык, .NET developer в NIX. Уже четвертый год я занимаюсь разработкой коммерческих проектов, а сейчас проектирую разные приложения на основе облачных решений.

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

Если вы только начинаете работу с API или хотите лучше разобраться именно в gRPC — эта статья для вас.

REST и gRPC: ключевые отличия

Чтобы лучше понять принципиальную разницу между этими фреймворками, рассмотрим, как они устроены. Для работы REST посредством самого обычного CRUD подхода обычно используется пять типов HTTP-запросов для данных какого-то ресурса:

  • GET: получение/чтение;

  • POST: добавление/запись;

  • PUT/PATCH: обновление/изменение;

  • DELETE: удаление.

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

Если поменять HTTP-запросы на RPC-коллы (вызовы удаленных процедур), то мы получаем схожую схему. Разница заключается в том, как происходят отправка и получение сервером запроса и получение ответа клиентом.

RPC — это протокол вызова удаленных процедур, который одна программа может использовать, чтобы вызвать метод, функцию или процедуру в другой, доступной в сети. При этом не нужно досконально разбираться в сети. Как видим на схеме, у клиента и сервера есть дополнительный уровень в общении — клиентская и серверная заглушки. Они созданы для того, чтобы отправлять данные по нужному адресу. Соответственно, в самом коде мы об этом вообще не переживаем. Эти заглушки генерируются автоматически. Все, что нам остается сделать — вызвать нужную процедуру.

Новая и, пожалуй, наиболее действенная и перспективная реализация этой концепции — фреймворк gRPC. По сравнению с другими реализациями RPC, у него есть множество преимуществ:

  • Идиоматические клиентские библиотеки на более чем 10 языках. Мы можем использовать библиотеки на Java, C#, JavaScript, Python и т.д. Все это выглядит нативно — не так, словно мы используем какую-то инопланетную технологию.

  • Простая структура определения сервисов. Для этого используются .proto-файлы.

  • HTTP/2. Двунаправленная потоковая передача на основе HTTP/2

  • Трассировка. Позволяет мониторить вызовы процедур, что очень полезно для отладки приложения и анализа работы сервисов для их дальнейших оптимизаций и улучшений.

  • Health check. С помощью этого механизма можно быстро проверить, работоспособен ли сервис и готов ли он обрабатывать запросы. Это помогает для балансировки нагрузки.

  • Балансировка нагрузки. Эта особенность позволяет распределять нагрузку на несколько экземпляров серверного приложения, тем самым значительно упрощая вопрос масштабирования. Балансировка может производиться как на клиентской части (например, клиентское приложение поочередно отправляет запросы на разные серверы), так и на промежуточной (специальной прокси) посредством сервис-меша.

  • Подключение аутентификации. Ее отсутствие было главным недостатком прошлых реализаций протокола. Из-за такой уязвимости RPC рекомендовали только для внутреннего общения.

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

  • Работа с Protobuf. В REST для передачи данных применяется текстовый формат JSON, который не сжимается. Protobuf — это бинарный формат. Используя его, мы избегаем передачи лишних данных и нам не надо будет десериализовать после этого полученные сообщения.

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

  • Простота определения контрактов. В REST для описания интерфейсов и документации нужно использовать сторонние инструменты и библиотеки — такие, как OpenAPI или Swagger. В gRPC происходит простое определение контрактов в .proto-файлах.

  • HTTP/2. REST зачастую использует более старую версию данного протокола — HTTP/1.1.

Чем хорош HTTP/2? Среди важных преимуществ:

  • бинарный формат передачи данных (уменьшает размер сообщений и ускоряет работу);

  • экономия трафика (усовершенствованное сжатие HTTP-сообщений, в первую очередь хедеров);

  • возможность передавать потоки данных;

  • мультиплексирование (в HTTP 1.1 для передачи трех файлов надо установить три соединения, в каждом из которых будет запрашиваться и отправляться определенный файл. В HTTP/2 можно все передать по одному соединению);

  • приоритезация потоков.

Как настроить соединение по gRPC

Последовательность создания gRPC канала включает несколько этапов:

  1. Открытие сокетов.

  2. Установка TCP-соединения.

  3. Согласование TLS.

  4. Запуск HTTP/2-соединения.

  5. Выполнение вызова gRPC-процедуры.

Мы могли бы избежать первых четырех пунктов за счет того, что один раз устанавливаем соединение и просто пользуемся им — это называется Persistent Connection. Почему бы не работать с таким решением постоянно? Однако возникает вопрос, как настроить балансировку нагрузки, когда, допустим, нам понадобится несколько экземпляров сервиса — как их распределять? Вариантов несколько:

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

  • Балансировка нагрузки через прокси. Если эти сервисы хостятся на каком-то оркестраторе (типа Kubernetes), он может решить, что инстансов слишком много, и убирает один или, наоборот, добавляет новый. В этом случае помогает балансировка нагрузки через прокси Service Mesh. Это может быть Linkerd, Istio, Consul и т.п. Клиент будет устанавливать одно постоянное соединение к Mesh, а уже он посмотрит, какие есть экземпляры сервиса, когда они появляются или исчезают и будет хендлить это. Соединение будет только к актуальным сервисам, а клиент об этом знать не будет — у него всегда один коннекшн.

Иногда gRPC сравнивают с WCF. Я не думаю, что это актуально, поскольку gRPC — это узконаправленный фреймворк, который хорошо решает одну задачу. WCF — более универсальный фреймворк, который поддерживает RPC, но также поддерживает REST, SOAP и т.п. К сожалению, WCF не является универсальным в плане поддерживаемых платформ, ведь пока он сильно привязан к .NET. В свою очередь gRPC может работать в любой среде, и писать его можно на любых языках из списка поддерживаемых.

Однако на данный момент gRPC не может полноценно функционировать в браузерах. Реализовать HTTP/2-общение в браузере невозможно, потому что нет такого контроля над каналом связи, какой может быть, допустим, в .NET-приложении. Поэтому Google предлагает альтернативу: использовать gRPC-прокси. То есть сам браузер будет отправлять запросы HTTP 1.1 на прокси, который будет мапить сообщение в вызов gRPC процедуры.

На картинке выше вы можете увидеть пример того, как огромное количество микросервисов Netflix таким образом связаны друг с другом — здесь их более 500! Это одна из тех компаний, которая выиграла от перехода на протокол gRPC и усилила производительность своих сервисов. Раньше им для каждого запроса приходилось устанавливать отдельное соединение. Это занимало миллисекунды, но в масштабах такой компании простой на сотнях коннекшенов постепенно складывается в секунды и минуты. Теперь же по одному соединению можно отправлять все запросы. Скорость передачи данных выросла в разы, ведь удается избежать тех самых первых четырех этапов установки соединения.

Как создать приложение gRPC

А теперь обещанный бонус — попробуем создать простое приложение Hello gRPC.

В первую очередь отмечу, что Visual Studio уже имеет предустановленный шаблон для создания подобных сервисов. Нам достаточно зайти в «Создание нового проекта», написать «gRPC» — и перед нами появится шаблон дефолтного gRPC-приложения. При этом поддерживается две самые актуальные версии .NET: 5 и 3.1.

У нас есть .proto-файл, в котором описан сервис и интерфейс. Мы видим, что у этого сервиса есть некая процедура SayHello, которая принимает объект Hello в реквест, описанный ниже, и возвращает в реплай, который в этом же файле и описывается. Уникальной опцией для .NET является csharp_namespace, необходимый для того, чтобы указать, куда генерировать те классы, которые будут использоваться в этом приложении.

syntax = “proto3” ;
option csharp_namespace = “Grpcservice2” ;
package greet ; 
    // The greeting service definition.
service Greeter { 
// Sends a greeting 
rpc SayHello (HelloRequest) returns (HelloReply) ;
}
  // The request message containing the user’s name. 
message HelloRequest {
string name = 1;
}
// The response message containing the greetings.
message HelloReply {
string message = 1;
} 

Под каждым свойством отправляемых и получаемых сообщений есть нумерация свойств. Это нужно для понимания, в каком порядке будут передаваться данные (так как у нас все-таки бинарный формат), а также для поддержки предыдущих версий. Если на сервере мы обновили наш .proto-файл и добавили новое свойство или убрали старое, то клиент, на котором еще не успели обновить файл, просто проигнорирует новое свойство и будет читать старое — как пустое, которое удалили. Соответственно, не будет никаких ошибок.

syntax = “proto3” ;
option  csharp_namespace = “GrpcService2”  ;
package greet;
     // The greeting service definition.
service Greeter { 
 // Sends a greeting 
rpc SayHello (HelloRequest) returns (HelloReply) ;
}
   // The request message containing the user’s name. 
message HelloRequest {
string name = 1;
}
// The response message containing the greetings.
message HelloReply {
string message = 1;
} 

Теперь рассмотрим регистрацию сервисов gRPC в .NET-приложении. Для этого понадобится одна строчка, которая добавит все необходимые сервисы к нам в проект.

using System. Collections.Generic ;
using System.Linq;
using System. Threading. Tasks ;
namespace GrpcService2
{ 
public class Startup 
{ 
// This method gets called by the runtime. Use this method to add services to the cont
// For more information on how to configure your application, visit https://go.microsoft 
public void ConfigureServices (IServiceCollection services) 
{ 
services. AddGrpc () 
}
// This method gets called by the runtime. Use this method to configure the HTTP reque
public void Configure  (IApplicationBuilder app, IWebHostEnvironment env)
{
   if (env. IsDevelopment ())
{

Чуть ниже мы видим, что gRPC-сервисы мапятся точно так же, как и обычные контроллеры. Потому есть уже готовый Gtreeter-сервис, интерфейс которого определен в .proto-файле. Если мы попытаемся посмотреть код этого GreeterService, то увидим, что дефолтная реализация уже есть. Сам сервис наследуется от некого GreeterBase — и это как раз тот файл, в который генерируется различная информация и классы, связанные с отправкой и получением сообщений. Все, что мы делаем — наследуемся от этого уже созданного объекта и реализовываем функции так, как нам надо.

// This method gets called by the runtime. Use this method to configure the HTTP reque
public void Configure (IApplicationBuilder app, IWebHostEnvironment env)
{
if (env. IsDevelopment ())
   app. UseDeveloperExceptionPage();
}
app. UseRouting ();
app. UseEndpoints (endpoints =>
{
endpoints MapGrpcService<GreeteService>(); 
endpoints. MapGet ( “ / ”, async context =>); 
{
   await context. Response. WriteAsyns (“Communication with gRPC endpoints must 
                { );
      } ); 
}
}
}

Что касается создания клиента под данный сервис, я предлагаю перейти к уже созданному. Здесь мы видим, что клиент WebApi — приложение, которое будет принимать HTTP-запросы и обращаться к gRPC сервису для выполнения на нем процедуры. Все, что поменялось в .proto-файле — namespace, в который будет генерироваться клиентский код.

syntax = “proto3”;
option csharp_namespace = “WebApi”;
package greet;
service Greeter {
   prc SayHello (HelloRequest) returns (HelloReply) ;
}
message HelloRequest {
    string name = 1;
} 
message HelloReply {
    string name = 1;
} 

В самом проекте Protobuf-файлы регистрируются отдельно. То есть не как обычный файл, а указывается, что будет использоваться конкретный Protobuf и роль данного сервиса в этом контракте. В данном случае, роль будет клиентская. При этом в серверном проекте мы указываем, что используется такой же .proto-файл, но роль — сервер.

<Project Sdk = “Microsoft.NET.Sdk.Web”>
<PropertyGroup>
   <TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
  <Protobuf Include = “Protos\greet.photo” GrpcServices=”Client” />
</ItemGroup>
</ItemGroup>
<PackageReference Include= “Grpc. AspNetCore”  Version= “2.39.0” />
<PackageReference Include= “Grpc. Net. ClientFactory” Version= “2.39.0” />
 <PackageReference Include= “Swashbuncle. AspNetCore” Version= “5.6.3” />
</ItemGroup> 
</Project>

Для того, чтобы переиспользовать одно и то же соединение, мы регистрируем фабрику gRPC клиентов. Делается это просто: приводим AddGrpcClient, указываем, какому именно сервису клиент нужен, и добавляем адрес, по которому будет хоститься уже сам сервис (не этот, а gRPC сервер; клиент будет отдельно).

public IConfiguration Configuration { get; } 
public void ConfigureServices (IServiceCollection services) 
{
services. AddGrpcClient<Greeter. GreeterClient>(0 => 
    o.Address = new Uri ( “https://localhost :500 “) ; 
} ) ;
services. AddControllers () ;
services. AddSwaggerGen(c =>
{
    c.SwaggerDoc ( “v1”, new OpenApiInfo { Title = “WebApi”, Version = “v1” } ) ;
   } ) 
}
public void  Configure (IApplicationbuilder app, IWebhostEnvironment env)

Теперь давайте попробуем использовать наш gRPC клиент. Мы можем в любой момент вытащить его в нужном нам объекте через Dependency Injection. После чего — обращаться к нему как к обычному объекту. В нашем случае я сделал SayHello эндпоинт. Это функция контроллера, поэтому принимать она будет запросы HTTP и для обработки будет обращаться уже к gRPC сервису.

{Route ( “ [controller] “ ) ]
public class GreetController : СontrollerBase 
{
       private readonly Greeter. GreeterClients _client;  
public GreetController (Greeter. GreeterClient client)
{
     client = client;
}
[HttpPost] 
public asyns Task <IActionResult> SayHello ([FromBody] HelloRequest request) 
{
      var result = await _client. SayHello (request) ;
      return Ok  ( new { MessageSum = result. Message ] );
     }
   }
}

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

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

Tags:
Hubs:
-2
Comments38

Articles

Information

Website
www.nixsolutions.com
Registered
Founded
1994
Employees
1,001–5,000 employees