在微服务架构里,服务间通信方式直接决定了系统的吞吐与延迟。gRPC 微服务通信凭借 HTTP/2 多路复用、Protocol Buffers 二进制序列化与强类型契约,正在成为高性能内部 RPC 的首选。相比 JSON over REST,它在跨服务高频调用场景下能显著降低带宽与序列化开销,同时用 .proto 文件把接口契约固化下来,前后端、多语言团队不再为字段含义扯皮。本文从 .proto 契约定义讲起,一步步带你用 Java 落地一套可上生产的 gRPC 服务,并给出模式选型、生产加固与常见排障建议。
一、为什么选 gRPC:与 REST 的硬核对比
很多团队已经用 Spring Cloud 做服务治理(可参考Spring Cloud 微服务治理),但治理解决的是“服务怎么发现、怎么熔断、怎么做负载均衡”,并不解决“调用本身快不快、序列化重不重”。gRPC 补的正是这一层:它把传输、编码、调用语义一次性标准化。下面这张表把两者在五个关键维度上摆开对比,帮你在设计初期就做对取舍。
| 维度 | REST / JSON | gRPC |
|---|---|---|
| 传输协议 | HTTP/1.1 | HTTP/2(多路复用) |
| 序列化 | 文本 JSON | Protobuf 二进制 |
| 接口契约 | 弱(靠文档) | 强(.proto 即契约) |
| 调用模式 | 仅请求-响应 | unary / 服务端流 / 客户端流 / 双向流 |
| 典型延迟 | 较高 | 低(二进制+多路复用) |
结论很清晰:对外暴露给前端或第三方的开放 API 仍用 REST,可读性高、调试方便;而服务内部那些高频、强契约、对延迟敏感的调用,优先考虑 gRPC。
二、定义服务契约:Protocol Buffers
所有 gRPC 方法都由 .proto 文件描述。这里用的是 proto3 语法:每个字段后面的 = 1、= 2 不是默认值,而是该字段在二进制编码中的“标签号(tag)”,一旦上线就不能改,否则旧客户端会解析错乱。下面的示例定义一个用户查询服务,包含一元调用(Unary)和服务端流式调用(Server streaming),覆盖最常见的两种形态。
syntax = "proto3";
package demo.user;
option java_multiple_files = true;
option java_package = "com.fsdata.grpc.user";
service UserService {
// 一元调用:查单个用户
rpc GetUser (UserRequest) returns (UserResponse);
// 服务端流式:批量推送用户
rpc StreamUsers (UserFilter) returns (stream UserResponse);
}
message UserRequest { int64 id = 1; }
message UserFilter { string role = 1; }
message UserResponse {
int64 id = 1;
string name = 2;
string email = 3;
}
三、生成 Java 代码:protoc 与 gradle 插件
手写 Stub 既枯燥又易错,所以官方提供 protoc 插件自动生成 Java 类。推荐在 Gradle 里集成 com.google.protobuf 插件,build 时自动产出 Message 与 Grpc Stub。注意 protoc 与 protoc-gen-grpc-java 的版本要匹配,否则会报“插件未找到”。配置如下:
plugins {
id 'java'
id 'com.google.protobuf' version '0.9.4'
}
protobuf {
protoc { artifact = "com.google.protobuf:protoc:4.26.1" }
plugins { grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.62.2" } }
generateProtoTasks {
all()*.plugins { grpc {} }
}
}
dependencies {
implementation 'io.grpc:grpc-netty-shaded:1.62.2'
implementation 'io.grpc:grpc-protobuf:1.62.2'
implementation 'io.grpc:grpc-stub:1.62.2'
}
四、实现服务端:Server 与 ServiceImpl
继承自动生成的 UserServiceGrpc.UserServiceImplBase,重写方法即可对外提供能力。关键点是:一元调用用 resp.onNext(...) 推送结果后必须调用 resp.onCompleted() 结束;如果漏掉 onCompleted(),客户端会一直阻塞等待。启动服务器时用 Grpc.newServerBuilderForPort 指定端口并注册实现类:
public class UserServer {
public static void main(String[] args) throws IOException, InterruptedException {
Server server = Grpc.newServerBuilderForPort(9090, InsecureServerCredentials.create())
.addService(new UserServiceImpl())
.build()
.start();
System.out.println("gRPC server started on :9090");
server.awaitTermination();
}
static class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {
@Override
public void getUser(UserRequest req, StreamObserver<UserResponse> resp) {
UserResponse r = UserResponse.newBuilder()
.setId(req.getId())
.setName("alice").setEmail("alice@fsdata.site")
.build();
resp.onNext(r);
resp.onCompleted();
}
}
}
五、实现客户端:Channel、Stub 与四种调用模式
客户端通过 ManagedChannel 建连,再生成 Stub。阻塞 Stub(BlockingStub)同步等待结果,写法最接近普通方法调用;异步 Stub(Stub)则用 StreamObserver 回调,适合不想阻塞业务线程的高并发场景(线程模型思路可参考Spring Boot 异步线程池)。流式调用里,服务端每产生一条数据就通过 onNext 推给客户端,客户端在 onNext 里实时消费:
ManagedChannel channel = Grpc.newChannelBuilderForAddress("localhost", 9090, InsecureChannelCredentials.create()).build();
UserServiceGrpc.UserServiceBlockingStub blocking = UserServiceGrpc.newBlockingStub(channel);
// 一元调用
UserResponse r = blocking.getUser(UserRequest.newBuilder().setId(1).build());
System.out.println(r.getName());
// 服务端流式调用
UserServiceGrpc.UserServiceStub async = UserServiceGrpc.newStub(channel);
async.streamUsers(UserFilter.newBuilder().setRole("dev").build(), new StreamObserver<>() {
public void onNext(UserResponse v) { System.out.println("stream: " + v.getName()); }
public void onError(Throwable t) { t.printStackTrace(); }
public void onCompleted() { System.out.println("done"); }
});
六、四种调用模式怎么选
gRPC 标准支持四种方法类型,选型时记住一句话:数据从哪边持续产生,就让哪边“流”。一元最简单,两端都不流式;其余三种按数据流向选择,能天然表达推送、批量上传和实时双向交互。
| 模式 | 数据流 | 典型场景 |
|---|---|---|
| Unary | 一问一答 | 普通 CRUD 查询 |
| Server streaming | 服务端→客户端 | 行情、日志、监控推送 |
| Client streaming | 客户端→服务端 | 大文件分片上传、批量导入 |
| Bidirectional | 双向并发 | 聊天、实时协作、游戏同步 |
七、生产实践:超时、重试与 TLS
上面的例子为了跑通用了 Insecure 明文,只能联调。生产环境三件事不能省:超时、TLS、拦截器。Deadline 是 gRPC 的一大特色——超时不是客户端的“本地闹钟”,而是随请求头传播到服务端,服务端能读到剩余时间并主动中止,避免级联雪崩。证书则用 Netty 的 useTransportSecurity 加载:
// 服务端启用 TLS
Server server = NettyServerBuilder.forPort(9090)
.useTransportSecurity(certChain, privateKey)
.addService(new UserServiceImpl())
.build();
// 客户端设超时(Deadline 随调用传播,服务端可读)
UserResponse r = blocking.withDeadlineAfter(2, TimeUnit.SECONDS)
.getUser(UserRequest.newBuilder().setId(1).build());
此外,建议用一个 ServerInterceptor 统一做鉴权与链路追踪(把 traceId 写进 Metadata),所有方法复用同一套逻辑,不必在每个实现里重复。重试则交给客户端拦截器或更上层的弹性库,避免把重试逻辑写进业务代码。
八、常见坑与排障
落地时最容易踩的几个坑:① 忘记 onCompleted() 导致客户端永久阻塞;② proto 字段 tag 上线后擅自修改,造成新老版本不兼容;③ 在异步 Stub 的回调里直接操作非线程安全的 Spring Bean,引发并发问题;④ 没设 Deadline,下游慢查询把整条调用链拖垮。排障时可以先开 gRPC 的二进制日志(grpc-binlog),或用 grpcurl 直接打服务端验证方法是否通,再逐步定位是网络、TLS 还是业务逻辑问题。
九、与 Spring Cloud 微服务协同
gRPC 不替代服务注册发现。常见做法是沿用 Spring Cloud 微服务治理 做注册中心与熔断,内部 RPC 走 gRPC;对外网关仍暴露 REST。高并发场景(如参考 Java 21 虚拟线程 的异步模型)配合 gRPC 异步 Stub,能进一步压榨单机吞吐。事务一致性可复用 Spring 事务传播机制 的经验。在 Kubernetes 环境里,gRPC 还能直接接入 Service Mesh(如 Istio)做流量治理与 mTLS,把安全与可观测性下沉到边车,业务代码保持纯净。
十、总结
gRPC 微服务通信的核心是“契约先行 + 二进制高效传输 + HTTP/2 多路复用”。本文覆盖了 .proto 定义、Java 代码生成、服务端与客户端实现、四种调用模式,以及生产级的超时、TLS、拦截器配置与常见排障。落地时记住一条原则:对外 REST、对内 gRPC、治理交给 Spring Cloud。下一步可以接入拦截器做链路追踪与鉴权,把内部 RPC 真正变成可观测、可治理的服务网格。




