为什么现代微服务更喜欢 gRPC 而不是 REST
在整体架构中,组件通过内存中的方法调用进行通信,这是即时且高度可靠的。然而,当迁移到微服务架构时,这些组件被网络边界分隔开。通信变成进程外网络调用(进程间通信,或 IPC)。
多年来,通过 HTTP/1.1 和 JSON 有效负载的 REST(表述性状态传输) 一直是构建 Web API 的默认标准。虽然 REST 非常适合面向公众的 Web 服务和客户端到服务器交互,但当用于高频、低延迟、内部服务到服务通信时,它会带来严重的瓶颈。
这就是现代微服务迅速转向**gRPC(Google 远程过程调用)**的原因。在本文中,我们将分析 REST 的局限性,剖析 gRPC 的架构支柱,并逐步完成在 Java 中 gRPC 服务的完整实现。
1. 微服务中 REST 的瓶颈
REST 为网络提供了二十多年的动力。然而,其底层技术并未针对内部分布式架构进行优化:
A. 纯文本 JSON 的开销
JSON 是人类可读的,这使得调试变得容易,但对于机器到机器的通信来说效率极低:
- 序列化/反序列化成本:解析文本字符串标记并将其转换为对象会消耗大量 CPU 周期。
- 大有效负载大小:JSON 键在每个请求中都会重复(例如
{"transactionId": "123", "amount": 99.99})。对于每秒处理数百万个请求的系统来说,这种冗余元数据浪费了巨大的网络带宽。
B. HTTP/1.1 连接限制
REST 通常在 HTTP/1.1 上运行,它表现出一些结构上的低效率:
- 队头 (HoL) 阻塞:在单个 TCP 连接上,客户端必须等待当前请求的响应,然后才能发送下一个请求。
- 连接池耗尽:为了处理并发请求,客户端必须打开多个 TCP 连接。当连接不断打开和关闭时,这会导致显着的操作系统资源开销、套接字耗尽以及慢启动性能损失。
C. 弱 API 合约
REST API 缺乏内置的编译时合约。虽然 OpenAPI/Swagger 有助于记录 API,但它们与代码库是分开的。后端开发人员很容易更改 JSON 字段名称并意外破坏下游服务,而不会出现编译时警告。
2. gRPC 的架构支柱
gRPC由Google于2015年推出,是专为云原生应用程序设计的开源、高性能RPC框架。它使用三个关键技术解决了 REST 的瓶颈:
A. 协议缓冲区 (Protobuf)
gRPC 使用 协议缓冲区 作为其接口定义语言 (IDL) 和消息序列化格式,而不是 JSON。
- Protobuf是一种高度优化的二进制序列化机制。
- 字段被序列化为小数字标签而不是文本名称。
- 有效负载比 JSON 小70-80%,序列化速度快10 倍**,显着减少 CPU 和带宽使用。
B. HTTP/2 多路复用
gRPC 使用 HTTP/2 作为其传输协议,带来了多项性能优化:
- 真正的多路复用:多个请求和响应可以通过单个长期 TCP 连接同时交织,完全消除队头阻塞。
- 标头压缩 (HPACK):HTTP 标头被压缩,进一步减小请求大小。
- 流媒体支持:HTTP/2 本身支持单向客户端流媒体、服务器流媒体和完全双向流媒体。
C. 代码生成和严格契约
通过在 .proto 文件中定义 API 结构,gRPC 生成多种语言(Java、Go、Python、C++ 等)的客户端存根和服务器基类。该合约是编译时安全的;如果服务器更新其接口,如果客户端构建与新合约不匹配,则客户端构建会立即失败。
3. Java 实现:构建 gRPC 服务
让我们构建一个实用的 Java gRPC 应用程序。我们将实施高吞吐量的支付处理服务,客户在其中提交支付请求并接收交易确认。
第 1 步:定义 API 合约 (payment.proto)
我们首先在 Protocol Buffer 文件中定义服务接口和消息结构。
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;
}
步骤 2:实现服务器端逻辑
编译 .proto 文件(通常由 Maven 或 Gradle gRPC 插件自动处理)后,该工具会生成 PaymentServiceImplBase 类。我们扩展这个类来实现业务逻辑。
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();
}
}
步骤 3:引导 gRPC 服务器
接下来,我们创建一个服务器类来在特定端口上启动 gRPC 服务器并注册我们的服务实现。
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();
}
}
步骤 4:实现客户端(阻塞和异步存根)
gRPC 的一个主要好处是它为客户端生成两种类型的存根:
- BlockingStub:同步、阻塞执行(类似于标准 REST 调用)。
- Stub:使用回调观察者的异步、非阻塞执行。
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. 架构比较:gRPC 与 REST
下表概述了两种范式的核心特征:
| 建筑特色 | REST(代表性状态转移) | gRPC(谷歌远程过程调用) |
|---|---|---|
| 协议 | HTTP/1.1(标准)、HTTP/2(可选) | HTTP/2(严格要求) |
| 有效负载格式 | 纯文本 JSON、XML、HTML | 二进制协议缓冲区 (Protobuf) |
| API 范式 | 资源(URI 路径 + GET/POST/PUT/DELETE) | 远程过程(方法/函数调用) |
| 合同质量 | 松散(OpenAPI/Swagger 是可选的/外部的) | 严格(编译时在 .proto 文件中定义) |
| 复用 | 否(HTTP/1.1 中的队头阻塞) | 是(单个 TCP 连接上的多个流) |
| 流媒体类型 | 仅单向服务器发送事件 (SSE) | 客户端、服务器和双向流 |
| 代码生成 | 需要外部工具(例如 Swagger Codegen) | 通过 Protobuf 编译器内置 (protoc) |
| 浏览器支持 | 通用(直接网络客户端访问) | 有限(需要 grpc-web 代理翻译) |
5. 何时选择 gRPC 而不是 REST?
尽管 gRPC 具有巨大的性能优势,但它并不是灵丹妙药。协议的选择取决于系统架构中的上下文:
- 将 gRPC 用于内部微服务网格:对于大容量服务间通信,gRPC 的低延迟、二进制紧凑性和流功能使其成为最佳选择。它减少了内部服务器 CPU 开销并加快了整体响应时间。
- 将 REST 用于边缘/公共 API:由于 Web 浏览器在没有转换层的情况下无法本机进行 gRPC 调用,因此 REST 仍然是公共 API、与第三方 Webhook 集成以及从客户端浏览器到架构边缘的直接通信的最佳选择。
通过将两者结合起来(在 API 网关级别公开 REST 并利用 gRPC 进行内部微服务通信),架构师可以构建高度可互操作且速度极快的系统。