gRPC

Contract First mit High Speed


28.09.2024

Frank Pommerening

  • Senior - Softwareentwickler
  • Consultant
  • Trainer


frank@pommerening-consulting.de

gRPC

Ãœberblick

API-History

SOAP

  • Ursprünglich die Abkürzung für Simple Object Access Protocol.
  • Service- und Datendefinitionen werden
    als XML (WSDL / XSD) bereitgestellt.
  • Datenübertragung erfolgt als XML über HTTP 1.1 und POST.
  • .NET / C# Implementierungen (legacy):
    • ASMX-Webservices (ab .NET Framework 1.0)
    • WCF mit HTTP-Binding (ab .NET Framework 3.0)

REST

  • Abkürzung für Representational State Transfer.
  • Programmierparadigma für verteilte Systeme, insbesondere Webservices (R. Fielding).
  • Ãœbertragung erfolgt per HTTP 1.1 in verschiedenen Formaten, meist JSON.
  • Prinzipien: Client-Server, Zustandslosigkeit, Operationen, Adressierbarkeit und Ressourcen.
  • L. Richardson entwickelte das Maturity Model, um den Reifegrad der API zu beschreiben - Ziel sind RESTful Services.
  • Erweiterungen wie Swagger/OpenAPI bieten API-Beschreibungen an.
Meist als RPC over HTTP umgesetzt

Keyfacts

  • gRPC = gRPC Remote Procedure Call
  • Freies, Open-Source Universal RPC-Framework von Google
  • Nutzung von HTTP/2 (für die Verbindung)
    und Protocol Buffers (für Definition / Serialisierung).
  • gRPC ist ein Bestandteil des Ökosystems der CNCF.
  • Es bietet hohe Leistung, Sprachunabhängigkeit und Effizienz.
  • Es kann um Authentifizierung, Lastenausgleich, Protokollierung und Ãœberwachung erweitert werden.

Sprachunterstützung

Die sprachneutrale Definition wird in Code für verschiedene Sprachen generiert. Aktuell werden folgende Sprachen unterstützt:
C#C++DartNode
GoRubyPHPPython
JavaKotlinObjective-C

HTTP/2

  • Von Google entwickeltes Vorläuferprotokoll SPDY
  • Im Mai 2015 von der IETF als Nachfolger
    von HTTP 1.1 freigegeben (RFC 7540)
  • Weitreichende Datenkompression
  • Schnelle TLS-Verschlüsselung (empfohlen)
  • Binär kodierte Ãœbertragung der Inhalte
  • Zusammenfassung mehrerer Anfragen (Multiplexing)
    über eine TCP-Verbindung
  • Serverinitiierte Datenübertragung (Push-Verfahren)

gRPC-Servicedefinition
mit Protobuf 3

Ãœberblick

gRPC verwendet Protocol Buffers als IDL (Interface Definition Language).
Der Style-Guide empfiehlt folgende Struktur:
Lizenzheaderwenn erforderlich
DateiübersichtBeschreibung in Textform
SyntaxVersion von Protocol Buffers (aktuell 3)
PaketPaketname in Kleinbuchstaben
ImporteSortierte Einbindung von Datenstrukturen
DateioptionenZ. T. plattformspezifische Optionen wie C# Namespace
Alles andereZ. B. Dienstdefinitionen und Datenstrukturen

Imports

Mit den Imports können, vergleichbar mit dem
using in C#, andere Strukturen eingebunden werden.
Diese können global, z. B. von Google stammen oder selbst entwickelt sein.
import "google/protobuf/struct.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";

import "elements/helpers.proto";

File options

Das Schlüsselwort option leitet teilweise plattformspezifische Optionen ein. Compiler für andere Sprachen ignorieren fremde Optionen.
option java_multiple_files = true;
option java_package = "my.java.example";
option csharp_namespace = "MyDemoService.Contract";

Servicedefinition

Die Servicedefinition beinhaltet die einzelnen Methoden mit ihren Aufruf- und Rückgabeparametern. Beide Parameter sind zwingend erforderlich.
Die Services sollten das Suffix Service enthalten.
service DemoServices {
    rpc GetDemo (GetDemoRequest)  returns (GetDemoResponse); 
    rpc SetDemo (SetDemoRequest)  returns (google.protobuf.Empty); 
}

Datenstrukturen

Die Nachrichten sind die Datenstrukturen für die Parameter
und mit Klassen in C# vergleichbar. Es werden skalare und
komplexe Datenstrukturen sowie Enums verwendet.

Indexierung

Serialisierte Nachrichten werden ohne Strukturinformationen übertragen. Ein eindeutiger, aber nicht zwingend fortlaufender, Index ist unverzichtbar.
message MyDemoMessage {
  int32 id = 1;
  bool is_valid = 2;
  Dummy dummy = 5;
}

Skalare Datentypen

Protocol Buffers liefert zahlreiche skalare Datentypen wie double, int32 oder string mit. Bei der Codeerstellung werden die Typen aus der proto-Datei in Zieltypen der Programmiersprache übersetzt. Datumswerte müssen aus dem Paket google/protobuf/timestamp.proto importiert werden.
message MyDemoMessage {
  string name = 1;
  string frist_name = 2;
  google.protobuf.Timestamp create_on = 3;
}

Enumeration

Die Werte einer Enumeration werden ebenfalls indexiert.
Der Wert 0 steht dabei für den Standardwert.
Die einzelnen Werte werden großgeschrieben benannt.
enum DemoEnum {
 NONE = 0;
 OPTION1 = 1;
 OPTION1 = 2;
}

Listen

Nachrichten können Listen von anderen Elementen enthalten.
message MyListMessage {
  repeated Dummy dummy = 5;
}

Verschachtelte Typen / Wiederverwendung

Die Bildung von untergeordneten Typen (Nachrichten)
erhöht die Übersicht und erlaubt die Wiederverwendung.
InternExtern
message MyDemoMessage {
 message Dummy {   
  int64 prop1 = 1;
  bool  prop2 = 2;
 }
 Dummy dummy1 = 1;
 Dummy dummy2 = 2;
}
message MyDemoMessage {
 Dummy dummy1 = 1;
 Dummy dummy2 = 2;
}

message Dummy {
 int64 prop1 = 1;
 bool  prop2 = 2;
}

Entwicklung mit C#

Ãœberblick

Ãœberblick

C# war seit Beginn als Zielsprache für Protobuf und gRPC verfügbar. Die erste Implementierung Grpc.Core nutzte im Kern eine C-Bibliothek. Diese ist aktuell im Status Maintenance Only und gilt ab Mai 2022 als veraltet. Die neue Bibliothek grpc-dotnet ist komplett in C# geschrieben. gRPC ist vollständig in die Tool-Chain mit Visual Studio und MSBuild integriert.

Visual Studio Integration

Um Abweichungen zwischen Client und Server zu vermeiden, sollte die Proto-Datei nur einmal vorhanden sein und relativ referenziert werden.
Das Nuget-Paket Grpc.Tools steuert die Codegenerierung bei jedem Build.

Visual Studio, Rider und die .NET CLI bringen Vorlagen für gRPC-Services mit.

Client-Integration

<Project Sdk="Microsoft.NET.Sdk">
  <ItemGroup>
    <Protobuf Include="..\Proto\my-contract.proto" GrpcServices="Client"
       Link="Protos\my-contract.proto" />
    <PackageReference Include="Google.Protobuf" Version="3.25.2" />
    <PackageReference Include="Grpc.Net.Client" Version="2.60.0" />
    <PackageReference Include="Grpc.Tools" Version="2.60.0" PrivateAssets="All" />
  </ItemGroup>
</Project>

Server-Integration

Das Nuget-Paket Grpc.AspNetCore enthält Abhängigkeiten zu Grpc.Tools.
<Project Sdk="Microsoft.NET.Sdk">
  <ItemGroup>
    <Protobuf Include="..\Proto\my-contract.proto" GrpcServices="Server"
       Link="Protos\my-contract.proto" />
    <PackageReference Include="Grpc.AspNetCore" Version="2.60.0" />
    </ItemGroup>
</Project>

Implementierung Service

gRPC-Services sind Teil der Pipeline von ASP.NET
und können durch Kestrel bereitgestellt werden.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc();
var app = builder.Build();
app.MapGrpcService<GRPC-SERVICE-CLASS>();
app.Run();

Errorhandling

Die 17 Statuswerte von gRPC sind sprachübergreifend gültig und bilden wichtige Zustände wie OK, PermissionDenied oder Unauthenticated ab.
Die Anreicherung um textuelle Zusatzinformationen ist möglich.
public override Task<GetDemoResponse>GetDemo(GetDemoRequest request, ServerCallContext context)
{
  ...
  context.Status = new Status(StatusCode.InvalidArgument, "something was wrong");
  ...
}

Verwendung Client

Workaround .NET Core 3.1

In HTTP/2 bzw. gRPC ist TLS zur Absicherung der Verbindung empfohlen, aber nicht zwingend erforderlich. Beim Einsatz von Reverse Proxies mit TLS-Termination wie Nginx und Traefik kann es zu Problemen kommen.
Bei einem Client, der mit .NET Core 3.1 implementiert ist, muss als Workaround beim Anwendungsstart eine Option aktiviert werden.
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);

Dependency Injection

Die gRPC-Clients können über die abstrakte Klasse GrpcClientFactory
per DI bereitgestellt werden. Dies ist effizient und ressourcensparend.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpcClient<Demo.DemoClient>("demo", o =>{ o.Address = ...});
...
public class DummyController : Controller
{
  private Demo.DemoClient _client;
  public DummyController(GrpcClientFactory grpcClientFactory)
  {
    _client = grpcClientFactory.CreateClient<Demo.DemoClient>("demo");
  }
}

Monitoring

Als Bestandteil von verteilten Systemen ist Monitoring wichtig.

OpenTelemetry

  • Distributed Traces
    • Ermittlung und Weitergabe des Tracing-Context im Client und Service
    • Nuget GrpcNetClient (beta) / AspNetCore
  • Metriken
    • Ermittlung Anzahl der Aufrufe (inkl. Status) und Ausführungsdauer
    • Aktuell nur Unterstützung für unterliegende HTTP-Verbindung
    • Nuget Http-Client / AspNetCore
    • Detailierte Metriken per Custom-Interceptor oder Community

prometheus-net

"Kestrel": {
 "Endpoints": {
  "Http1": {
   "Url": "http://0.0.0.0:5001",
   "Protocols": "Http1"
  },
  "Http2": {
   "Url": "http://0.0.0.0:5002",
   "Protocols": "Http2"
  }
 }
}
Mit dem Nuget-Paket
prometheus-net.AspNetCore.Grpc werden Metriken der gRPC-Services wie die Aufrufanzahl bereitgestellt. Prometheus benötigt zwingend eine HTTP 1.1 Verbindung. Dafür muss ggf. ein zusätzlicher Endpunkt im Kestrel aktiviert werden.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseGrpcMetrics();
app.UseEndpoints(endpoints =>
  { ...
    endpoints.MapMetrics();     
    ...
  }
app.Run();

gRPC

Client-UI

BloomRPC (Archived)

  • GUI Client für gRPC Services
  • Entwickelt mit HTML, CSS und React
  • LGPL-3.0 License
  • Cross-Platfrom durch Electron
  • Import von Protobuf-Definitionen
  • Unary Calls und Streaming
  • Native gRPC und gRPC Web
  • TLS und non-TLS-Verbindungen

Postman

  • Unterstützung seit 2022
  • Unary Calls und Streaming (Client, Server, Bidirectional)
  • Import Service-Definition per proto-Datei oder Server Reflection
  • Erzeugung Beispielanfragen als JSON
  • Empfang und Versand von Metadaten z.B. Header
  • Verschlüsselter und unverschlüsselter Datenverkehr
  • Variableninterpolation mithilfe von Umgebungen
  • Importieren, bearbeiten und prüfen von Protobuf-Definitionen
    mit Syntax Highlighting und Autovervollständigung
  • Authentifizierung mit Basic Auth, Bearer Token und API-Key

Entwicklung mit C#

Streaming

Ãœberblick

Das Streaming in gRPC nutzt das Multiplexing von HTTP/2, was ermöglicht, dass über eine Verbindung mehrere Anfragen und/oder Antworten gesendet werden können. Dies ist besonders nützlich für Anwendungsfälle wie den Upload oder Download größerer Dateien sowie kontinuierliche Übertragungen wie z. B. Logs. Um einen stabilen Betrieb zu gewährleisten, müssen im Client Verbindungs- und Übertragungsfehler, z. B. durch Retry, Reconnect und Healthchecks, erkannt und behandelt werden.

Arten

  • Client-Streaming
  • Server-Streaming
  • Bidirectional-Streaming

Contract

Das Schlüsselwort stream in der Service-Definition kennzeichnet, dass die Nachricht möglicherweise mehrmals übertragen wird.

Client-Streaming

service ClientStreamServices {
 rpc SendPartDemo (stream SendPartDemoRequest)  returns (SendPartDemoResponse); 
}

Server-Streaming

service ServerStreamServices {
 rpc ReceivePartDemo (ReceivePartDemoRequest)  returns (stream ReceivePartDemoResponse); 
}

Bidirectional-Streaming

service BidirectionalStreamServices {
 rpc MultipartDemo (stream MultipartC2SMessage)  returns (stream MultipartS2CMessage); 
}

Implementierung

Client-Streaming

public override async Task<SendPartDemoResponse> SendPartDemo(
    IAsyncStreamReader<SendPartDemoRequest> requestStream, ServerCallContext context)
{
  ...
  await foreach (var request in requestStream.ReadAllAsync())
  {
    ...
  }
  return new SendPartDemoResponse();
}
using (var streamingCall = client.SendPartDemo())
{
  ...
  await streamingCall.RequestStream.WriteAsync(new SendPartDemoRequest { ... })
  ...
  await streamingCall.RequestStream.CompleteAsync();
  var result = await streamingCall.ResponseAsync;
}

Server-Streaming

  public override async Task ReceivePartDemo(
      ReceivePartDemoRequest request,
      IServerStreamWriter<ReceivePartDemoResponse> responseStream, ServerCallContext context)
{
  ...
  await responseStream.WriteAsync(new ReceivePartDemoResponse { ... });
  ...
}
using (var streamingCall = client.ReceivePartDemo())
{
  ...
  await foreach (var response in streamingCall.ResponseStream.ReadAllAsync())
  {
    ...
  }
}

Bidirectional-Streaming

  public override async Task MultipartDemo(
    IAsyncStreamReader<MultipartC2SMessage> requestStream,
    IServerStreamWriter<MultipartS2CMessage> responseStream, ServerCallContext context)
{
  var taskRead = readAsync(requestStream, context);
  var taskWrite = WriteAsync(responseStream, context);
  await Task.WhenAll(taskRead,taskWrite);
}
 using (var streamingCall = client.MultipartDemo())
{
  var sendTask = SendMessage(streamingCall);
  var receiveTask = ReceiveMessage(streamingCall);
  await Task.WhenAll(sendTask, receiveTask);
}

Entwicklung mit C#

Authentifizierung / Autorisierung

Ãœberblick

Im gRPC-Framework sind keine Konzepte zur Authentifizierung bzw. Autorisierung vorgeschrieben. Erweiterungen wie Client-Zertifikate und Token (JWT / OAuth 2) rüsten die Funktionalität nach, müssen aber sowohl im Client als auch im Server implementiert werden. Die HTTP-Pipeline von ASP.NET bzw. Kestrel bietet bereits zahlreiche Optionen.

Client-Zertifikaten

Client

X509Certificate2 certificate =  LoadClientCertificate(...)
var httpClienthandler = new HttpClientHandler();
httpClienthandler.ClientCertificates.Add(certificate);
var httpClient = new HttpClient(handler); 
var options = new GrpcChannelOptions {  HttpClient = httpClient };
var channel = GrpcChannel.ForAddress(serviceUrl, opt);
var client = new AuthDemoServices.AuthDemoServicesClient(channel);

Service

[Authorize(AuthenticationSchemes = CertificateAuthenticationDefaults.AuthenticationScheme)]
public class AuthDemoServices : AuthDemoServices.AuthDemoServicesBase
{
    ...
}

Kestrel

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(opt =>  {
  X509Certificate2 certificate = LoadServerCertificate(...);
  opt.ConfigureHttpsDefaults(d =>  {
    d.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
    d.CheckCertificateRevocation = false;
    d.ServerCertificate = certificate;
    });
});

Startup

...
var app = builder.Build();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapGrpcService<GRPC-SERVICE-CLASS>();
...

JWT-Token / OAUTH

gRPC

.Net 6++

Server Reflection

  • Liefert Metadaten zum gRPC-Service ähnlich wie OpenAPI.
  • Ermöglicht Aufrufe ohne Zugriff auf den Proto-Contract.
  • Unterstützung via Nuget-Paket Grpc.AspNetCore.Server.Reflection .
  • Integration in die ASP.NET-Pipeline.
  • Client-Tools: Postman , gRPC WebUI , grpcurl
WebApplicationBuilder builder = ...
builder.Services.AddGrpc();
builder.Service.AddGrpcReflection();

IEndpointRouteBuilder app = builder.Build();
app.MapGrpcService<...>
app.MapGrpcReflectionService();

Json Transcode

  • Erweiterung für ASP.NET Core gRPC.
  • Ersatz für gRPC HTTP API (experimentell).
  • Gleichzeitige Bereitstellung von gRPC und RESTful JSON API.
  • Nuget-Paket Microsoft.AspNetCore.Grpc.JsonTranscoding
  • Unterstützung für HTTP-Verben (GET, POST, PUT ...).
  • Annotationen für Route-Parameter und Query-Parameter.

Service / Middleware

WebApplicationBuilder builder = ...
builder.Services.AddGrpc().AddJsonTranscoding();
...

Contract

Die zwei Proto-Dateien http.proto und annotations.proto
müssen aktuell manuell ins Projekt kopiert werden. Sie liefern die Option google.api.http zur Beschreibung des Endpunkts.
...
import "google/api/annotations.proto";
service OrderService {
  rpc GetOrderById (GetOrderByIdRequest) returns (GetOrderResponse){
    option (google.api.http) = { 
        get: "/v1/orders/{id}"
    };
  };
}
message GetOrderByIdRequest {  string id = 1;  }
message GetOrderResponse {  string id = 1;  ...}

Kestrel

  • RESTful-API sollte neben HTTP 2 per HTTP 1.1 erreichbar sein
  • Bereitstellung über getrennten Port oder Multiprotocol-Port
"Kestrel": {
        "Endpoints": {
          "Http1": {
            "Url": "http://0.0.0.0:5001",
            "Protocols": "Http1"
          },
          "Http2": {
            "Url": "http://0.0.0.0:5002",
            "Protocols": "Http2"
          }
        }
      }
"Kestrel": {
    "EndpointDefaults": {
      "Protocols": "Http1AndHttp2"
    }
  }

OpenAPI

Microsoft.AspNetCore.Grpc.Swagger

Performance

  • Kestrel HTTP/2 (Architektur)
    • Wechsel von Lock zu IQ (Queue).
    • Verbesserung von gleichzeitigen Operationen u.a. Streaming.
    • Reduzierung des Ressourcenbedarfs.
  • gRPC Upload Geschwindigkeit
    • Integrierter größerer Cache.
    • Bis zu 6x schnellere Ãœbertragung größerer Dateien.
  • gRPC Download Geschwindigkeit
    • HttpClient skaliert Empfangspufferfenster dynamisch

Clientseitigen Lastausgleich

  • Verbessere Leistung (keine Proxy, weniger Netzwerk-Hops /
  • Einfachere Anwendungsarchitektur (keine Konfiguration für Proxy)
  • Discover Endpunkte (DNS, Statisch, Custom)
  • Load Balance Mechanismen (Pick first, Round robin, Custom)
var channel = GrpcChannel.ForAddress("dns:///grpc.service.fqdn",
 new GrpcChannelOptions
  {
   Credentials = ChannelCredentials.Insecure,
   ServiceConfig = new ServiceConfig { LoadBalancingConfigs = { new RoundRobinConfig() } }
  });
var client = new MyDemo.DemoClient(channel);

Transiente Fehlerbehandlung

Transiente Fehler treten unregelmäßig auf und
haben i.d.R. keinen fachlichen Hintergrund.
  • Kurzfristiger Verlust der Netzwerkverbindung
  • Zeitweise Nichtverfügbarkeit
  • Timeout durch Serverlast

Standard

Standardmäßig führen transiente Fehler zum Abbruch (Exception). Der Client muss den Prozess komplett neu ausführen.

Mit Wiederholung(en)

Transiente Fehler lassen sich kaum vermeiden aber meist durch einen erneuten Versuch lösen.
var mCfg = new MethodConfig{
  Names = { MethodName.Default },
  RetryPolicy = new RetryPolicy {
    MaxAttempts = 5,
    InitialBackoff = TimeSpan.FromSeconds(1),
    MaxBackoff = TimeSpan.FromSeconds(5),
    BackoffMultiplier = 1.5,
    RetryableStatusCodes = { StatusCode.Unavailable }
  }
};
var channel = GrpcChannel.ForAddress(
  "https://grpc.service.fqdn:5001",
    new GrpcChannelOptions { ServiceConfig =
      new ServiceConfig { MethodConfigs = { mCfg } }  });

Vielen Dank für die Aufmerksamkeit