为什么现代微服务更喜欢 gRPC 而不是 REST

为什么现代微服务更喜欢 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 的瓶颈:

gRPC 的 3 个架构支柱:协议缓冲区、HTTP/2 传输和代码生成

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 的一个主要好处是它为客户端生成两种类型的存根:

  1. BlockingStub:同步、阻塞执行(类似于标准 REST 调用)。
  2. 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 进行内部微服务通信),架构师可以构建高度可互操作且速度极快的系统。


在 Ghaznix 博客上探索更多软件开发和后端工程见解 →