Warum moderne Microservices gRPC gegenüber REST bevorzugen
In einer monolithischen Architektur kommunizieren Komponenten über speicherinterne Methodenaufrufe, die sofort erfolgen und äußerst zuverlässig sind. Bei der Umstellung auf eine Microservices-Architektur sind diese Komponenten jedoch durch Netzwerkgrenzen getrennt. Die Kommunikation wird zu einem Out-of-Process-Netzwerkaufruf (Inter-Process Communication oder IPC).
Seit Jahren ist REST (Representational State Transfer) über HTTP/1.1 mit JSON-Nutzlasten der Standardstandard für die Erstellung von Web-APIs. Während sich REST hervorragend für öffentlich zugängliche Webdienste und Client-zu-Server-Interaktionen eignet, führt es zu erheblichen Engpässen, wenn es für hochfrequente, interne Service-zu-Service-Kommunikation mit geringer Latenz verwendet wird.
Aus diesem Grund verlagern sich moderne Mikrodienste schnell in Richtung gRPC (Google Remote Procedure Call). In diesem Artikel analysieren wir die Einschränkungen von REST, analysieren die architektonischen Säulen von gRPC und gehen eine vollständige Implementierung eines gRPC-Dienstes in Java durch.
1. Die Engpässe von REST in Microservices
REST treibt das Web seit über zwei Jahrzehnten an. Die zugrunde liegenden Technologien sind jedoch nicht für interne verteilte Architekturen optimiert:
A. Der Overhead von Klartext-JSON
JSON ist für Menschen lesbar, was das Debuggen erleichtert, ist jedoch für die Maschine-zu-Maschine-Kommunikation äußerst ineffizient:
- Serialisierungs-/Deserialisierungskosten: Das Parsen von Textzeichenfolgen-Tokens und deren Konvertierung in Objekte nimmt erhebliche CPU-Zyklen in Anspruch.
- Große Nutzlastgröße: JSON-Schlüssel werden in jeder einzelnen Anfrage wiederholt (z. B.
{"transactionId": "123", "amount": 99.99}). Bei Systemen, die Millionen von Anfragen pro Sekunde verarbeiten, verschwenden diese redundanten Metadaten enorme Netzwerkbandbreite.
B. HTTP/1.1-Verbindungsbeschränkungen
REST läuft normalerweise auf HTTP/1.1, was mehrere strukturelle Ineffizienzen aufweist:
- Head-of-Line (HoL)-Blockierung: Bei einer einzelnen TCP-Verbindung muss ein Client auf die Antwort der aktuellen Anfrage warten, bevor er die nächste sendet.
- Erschöpfung des Verbindungspools: Um gleichzeitige Anforderungen zu verarbeiten, müssen Clients mehrere TCP-Verbindungen öffnen. Dies führt zu einem erheblichen Overhead der Betriebssystemressourcen, einer Socket-Erschöpfung und Leistungseinbußen beim langsamen Start, da Verbindungen ständig geöffnet und geschlossen werden.
C. Schwache API-Verträge
REST-APIs verfügen nicht über einen integrierten Vertrag zur Kompilierungszeit. Während OpenAPI/Swagger bei der Dokumentation von APIs hilft, sind sie von der Codebasis getrennt. Für einen Backend-Entwickler ist es leicht, einen JSON-Feldnamen zu ändern und versehentlich nachgelagerte Dienste ohne Warnungen zur Kompilierungszeit zu unterbrechen.
2. Die architektonischen Säulen von gRPC
gRPC wurde 2015 von Google eingeführt und ist ein Open-Source-Hochleistungs-RPC-Framework, das speziell für Cloud-native Anwendungen entwickelt wurde. Es löst die Engpässe von REST mithilfe von drei Schlüsseltechnologien:
A. Protokollpuffer (Protobuf)
Anstelle von JSON verwendet gRPC Protokollpuffer als Interface Definition Language (IDL) und Nachrichtenserialisierungsformat.
- Protobuf ist ein hochoptimierter binärer Serialisierungsmechanismus.
- Felder werden als kleine numerische Tags und nicht als Textnamen serialisiert.
- Die Nutzdaten sind bis zu 70-80 % kleiner als bei JSON und die Serialisierung ist bis zu 10x schneller, was die CPU- und Bandbreitenauslastung erheblich reduziert.
B. HTTP/2-Multiplexing
gRPC nutzt HTTP/2 als Transportprotokoll und bringt mehrere Leistungsoptimierungen mit sich:
- Echtes Multiplexing: Mehrere Anfragen und Antworten können gleichzeitig über eine einzige langlebige TCP-Verbindung verschachtelt werden, wodurch Head-of-Line-Blockierung vollständig entfällt.
- Header-Komprimierung (HPACK): HTTP-Header werden komprimiert, wodurch die Anforderungsgröße weiter reduziert wird.
- Streaming-Unterstützung: HTTP/2 unterstützt nativ unidirektionales Client-Streaming, Server-Streaming und vollständiges bidirektionales Streaming.
C. Codegenerierung und strenge Verträge
Durch die Definition der API-Struktur in einer .proto-Datei generiert gRPC Client-Stubs und Server-Basisklassen in Dutzenden von Sprachen (Java, Go, Python, C++ usw.). Der Vertrag ist zur Kompilierzeit sicher; Wenn der Server seine Schnittstelle aktualisiert, schlagen Client-Builds sofort fehl, wenn sie nicht mit dem neuen Vertrag übereinstimmen.
3. Java-Implementierung: Erstellen eines gRPC-Dienstes
Lassen Sie uns eine praktische Java-gRPC-Anwendung erstellen. Wir werden einen Zahlungsabwicklungsdienst mit hohem Durchsatz implementieren, bei dem ein Kunde eine Zahlungsanforderung einreicht und eine Transaktionsbestätigung erhält.
Schritt 1: Definieren Sie den API-Vertrag (payment.proto)
Wir beginnen mit der Definition unserer Serviceschnittstelle und Nachrichtenstrukturen in einer Protokollpufferdatei.
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.ghaznix.grpc.payment";
option java_outer_classname = "PaymentProto";
package payment;
// The Payment Service definition
service PaymentService {
// Unary RPC: Sends a single payment request and receives a confirmation
rpc ProcessPayment (PaymentRequest) returns (PaymentResponse);
}
// The Payment Request message
message PaymentRequest {
string transaction_id = 1;
string customer_id = 2;
double amount = 3;
string currency = 4;
}
// The Payment Response message
message PaymentResponse {
string transaction_id = 1;
string status = 2;
string message = 3;
string timestamp = 4;
}
Schritt 2: Implementieren Sie die serverseitige Logik
Nach dem Kompilieren der Datei .proto (normalerweise automatisch von Maven- oder Gradle-gRPC-Plugins verarbeitet) generiert das Tool die Klasse PaymentServiceImplBase. Wir erweitern diese Klasse, um die Geschäftslogik zu implementieren.
package com.ghaznix.grpc.payment;
import io.grpc.stub.StreamObserver;
import java.time.Instant;
public class PaymentServiceImpl extends PaymentServiceGrpc.PaymentServiceImplBase {
@Override
public void processPayment(PaymentRequest request, StreamObserver<PaymentResponse> responseObserver) {
System.out.println("Received payment request for customer: " + request.getCustomerId()
+ " with amount: " + request.getAmount() + " " + request.getCurrency());
// Process payment logic (simulation)
String status = request.getAmount() > 0 ? "SUCCESS" : "DECLINED";
String message = status.equals("SUCCESS") ? "Payment processed successfully." : "Invalid payment amount.";
PaymentResponse response = PaymentResponse.newBuilder()
.setTransactionId(request.getTransactionId())
.setStatus(status)
.setMessage(message)
.setTimestamp(Instant.now().toString())
.build();
// Send the response to the client
responseObserver.onNext(response);
// Signal that the RPC execution is complete
responseObserver.onCompleted();
}
}
Schritt 3: Bootstrap des gRPC-Servers
Als Nächstes erstellen wir eine Serverklasse, um den gRPC-Server an einem bestimmten Port zu starten und unsere Dienstimplementierung zu registrieren.
package com.ghaznix.grpc.payment;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class PaymentServer {
private Server server;
public void start() throws IOException {
int port = 50051;
server = ServerBuilder.forPort(port)
.addService(new PaymentServiceImpl())
.build()
.start();
System.out.println("gRPC Server started, listening on port " + port);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.err.println("*** Shutting down gRPC server since JVM is shutting down");
try {
PaymentServer.this.stop();
} catch (InterruptedException e) {
e.printStackTrace(System.err);
}
System.err.println("*** Server shut down");
}));
}
public void stop() throws InterruptedException {
if (server != null) {
server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
}
}
public void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
public static void main(String[] args) throws IOException, InterruptedException {
final PaymentServer server = new PaymentServer();
server.start();
server.blockUntilShutdown();
}
}
Schritt 4: Implementieren Sie den Client (Blockierung und Async-Stubs)
Ein großer Vorteil von gRPC besteht darin, dass es zwei Arten von Stubs für den Client generiert:
- BlockingStub: Synchrone, blockierende Ausführung (ähnlich wie Standard-REST-Aufrufe).
- Stub: Asynchrone, nicht blockierende Ausführung mithilfe von Callback-Beobachtern.
package com.ghaznix.grpc.payment;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import java.util.concurrent.TimeUnit;
public class PaymentClient {
private final ManagedChannel channel;
private final PaymentServiceGrpc.PaymentServiceBlockingStub blockingStub;
public PaymentClient(String host, int port) {
// Create a communication channel to the server
this.channel = ManagedChannelBuilder.forAddress(host, port)
.usePlaintext() // Disables TLS for local development
.build();
// Create the synchronous blocking stub
this.blockingStub = PaymentServiceGrpc.newBlockingStub(channel);
}
public void shutdown() throws InterruptedException {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
}
public void executePayment(String txId, String customerId, double amount) {
PaymentRequest request = PaymentRequest.newBuilder()
.setTransactionId(txId)
.setCustomerId(customerId)
.setAmount(amount)
.setCurrency("USD")
.build();
System.out.println("Sending payment request for: " + txId);
// Execute unary synchronous call
PaymentResponse response = blockingStub.processPayment(request);
System.out.println("Server Response: " + response.getStatus() + " | " + response.getMessage());
}
public static void main(String[] args) throws InterruptedException {
PaymentClient client = new PaymentClient("localhost", 50051);
try {
client.executePayment("TX-99081", "customer-abc-123", 250.75);
} finally {
client.shutdown();
}
}
}
4. Architekturvergleich: gRPC vs. REST
In der folgenden Tabelle sind die Kernmerkmale beider Paradigmen aufgeführt:
| Architektonisches Merkmal | REST (Representational State Transfer) | gRPC (Google Remote Procedure Call) |
|---|---|---|
| Protokoll | HTTP/1.1 (Standard), HTTP/2 (Optional) | HTTP/2 (strenge Anforderung) |
| Nutzlastformat | Klartext JSON, XML, HTML | Binäre Protokollpuffer (Protobuf) |
| API-Paradigma | Ressourcen (URI-Pfade + GET/POST/PUT/DELETE) | Remote-Prozeduren (Methoden-/Funktionsaufrufe) |
| Vertragsqualität | Loose (OpenAPI/Swagger ist optional/extern) | Streng (in .proto-Dateien bei der Kompilierung definiert) |
| Multiplexen | Nein (Head-of-Line-Blockierung in HTTP/1.1) | Ja (Mehrere Streams auf einer einzigen TCP-Verbindung) |
| Streaming-Typen | Nur unidirektionale, vom Server gesendete Ereignisse (SSE) | Client-, Server- und bidirektionales Streaming |
| Codegenerierung | Externe Tools erforderlich (z. B. Swagger Codegen) | Integriert über den Protobuf-Compiler (protoc) |
| Browser-Unterstützung | Universal (Direkter Web-Client-Zugriff) | Eingeschränkt (Erfordert grpc-web-Proxy-Übersetzung) |
5. Wann sollte man gRPC gegenüber REST wählen?
Trotz der enormen Leistungsvorteile von gRPC ist es kein Allheilmittel. Die Wahl des Protokolls hängt vom Kontext innerhalb Ihrer Systemarchitektur ab:
- Verwenden Sie gRPC für internes Microservice Mesh: Für die Service-zu-Service-Kommunikation mit hohem Volumen ist gRPC aufgrund seiner geringen Latenz, der binären Kompaktheit und der Streaming-Funktionen die beste Wahl. Es reduziert den CPU-Overhead des internen Servers und beschleunigt die Gesamtantwortzeiten.
- Verwenden Sie REST für Edge-/öffentliche APIs: Da Webbrowser ohne Übersetzungsschichten keine nativen gRPC-Aufrufe durchführen können, bleibt REST die beste Wahl für öffentliche APIs, Integrationen mit Webhooks von Drittanbietern und direkte Kommunikation von Client-Browsern zum Rand Ihrer Architektur.
Durch die Kombination der beiden – Bereitstellung von REST auf der API-Gateway-Ebene und Nutzung von gRPC für die interne Microservice-Kommunikation – können Architekten Systeme erstellen, die sowohl hochgradig interoperabel als auch blitzschnell sind.
Weitere Einblicke in Softwareentwicklung und Backend-Engineering finden Sie im Ghaznix-Blog →