《Nacos 2.x源码深度解析》专栏目录
一、架构通信篇:
《Nacos 2.x 源码深度解析 (一):架构整体全貌 —— 核心模块划分与版本演进》
《Nacos 2.x 源码深度解析 (二):通信协议迭代 —— HTTP长轮询到gRPC演进》
二、配置中心篇
《Nacos 2.x 源码深度解析 (三):配置中心客户端 —— 启动加载与自动装配》
《Nacos 2.x 源码深度解析 (四):配置中心服务端 —— 事件总线与数据持久化》
《Nacos 2.x 源码深度解析 (五):gRPC 推送链路 —— 配置变更下发与动态刷新》
《Nacos 2.x 源码深度解析 (六):三级缓存体系 —— 降级兜底与故障自愈机制》
三、服务注册发现篇
《Nacos 2.x 源码深度解析 (七):服务注册流程 —— 客户端上报与服务端存储》
《Nacos 2.x 源码深度解析 (八):服务订阅机制 —— 从首次订阅到gRPC双向流变更通知》
目录
一、HTTP长轮询到gRPC演进:一切为了极致的性能
1.1 HTTP 1.1:短连接模型的天然瓶颈
1.2 HTTP 2.0:长连接与多路复用的突破
1.3 gRPC:为微服务而生的通信框架
1.4 Nacos 的选择:从HTTP短连接到gRPC长连接
二、gRPC 通信层源码剖析——Nacos 2.x 的性能基石
2.1 消息体与服务定义:有什么能力
2.2 编译生成gRPC代码:根据定义生成java代码
2.3 Stub(存根):谁来调用
2.4 Channel(通道): 建立连接通道
2.5 Server(服务端): 谁来处理
2.6 序列化:怎么传
2.7 流程串联:梳理gRPC开发及执行流程
三、全文小结
在上篇文章中,我们从整体架构、源码分层和项目依赖三个维度,建立了对 Nacos 2.4.3 的全局认知。本篇将聚焦 Nacos 2.x 最核心的技术决策——通信协议的演进,深入拆解从 HTTP 1.1 短连接到 gRPC 长连接的性能跃迁过程。
如果说架构是骨架,那么通信协议就是神经系统的传导方式。Nacos 1.x 基于 HTTP 1.1 的通信模型,在面对大规模集群时逐步暴露出连接数膨胀、心跳风暴和推送延迟三重瓶颈。2.x 选择 gRPC 而非直接使用 HTTP/2,背后是对双向流通信、强类型契约和连接管理机制的深度考量。一条 gRPC 长连接承载所有心跳、查询、注册和推送,连接资源消耗从与实例数线性相关降为常量级,推送延迟从秒级压缩至毫秒级,单机可支撑的实例数量提升了一个数量级。
本文将从协议特性对比入手,结合nacos_grpc_service.proto的消息体定义、GrpcClient.connectToServer()的连接建立、BaseGrpcServer.startServer()的服务端启动、以及PayloadRegistry的类型路由机制,完整呈现 gRPC 通信层的源码实现细节。理解了这一层,就掌握了 Nacos 2.x 性能基石的构建逻辑。
一、HTTP长轮询到gRPC演进:一切为了极致的性能
在分布式系统中,通信协议的选择直接决定了系统的吞吐量天花板与实时性下限。Nacos 2.x 全面从 HTTP 转向 gRPC,本质上是一次面向规模化场景的通信模型重构。要理解这个决策,需要先回到协议本身的特性差异上来。
1.1 HTTP 1.1:短连接模型的天然瓶颈
参考文档:https://www.rfc-editor.org/rfc/rfc9112.html
HTTP 1.1 是最广泛使用的应用层协议,但它与生俱来的几个特性,使其在大规模微服务通信场景中力不从心。首先是短连接模式,每次 HTTP 请求都需要独立建立 TCP 连接,完成请求响应后再断开。TCP 三次握手和四次挥手的开销,在少量请求时微不足道,但当数千个客户端同时与服务端保持心跳通信时,连接的反复建立与销毁会迅速消耗服务端的 CPU 和文件描述符资源。其次,HTTP 1.1 的请求响应模型是单向的——客户端发请求,服务端返回响应——服务端无法主动向客户端推送数据。当配置发生变更时,服务端要么等待客户端下一次轮询,要么自行发起 HTTP 请求反向推送,前者存在延迟,后者依然逃不开短连接的开销。即便 HTTP 1.1 引入了 Keep-Alive 机制允许在单个连接上发送多次请求,但这种复用受限于请求串行处理,队头阻塞问题让并发效率大打折扣。
1.2 HTTP 2.0:长连接与多路复用的突破
参考文档:https://www.rfc-editor.org/rfc/rfc9113.html
HTTP 2.0 在保持与 HTTP 1.1 语义兼容的前提下,对传输层做了彻底重构。它引入二进制分帧层,将请求和响应拆解为独立的帧,在一个 TCP 连接上交错传输,实现了真正的多路复用。一个连接上可以同时承载多个请求和响应,彼此独立、互不阻塞,解决了 HTTP 1.1 的队头阻塞问题。长连接模型下,TCP 连接只需建立一次,后续所有通信都在这条连接上完成,连接的创建/销毁成本大幅降低。服务端推送能力也被原生支持,服务端可以主动向客户端推送资源,不再受限于请求响应的单向模型。这些特性使得 HTTP 2.0 在性能和实时性上显著优于 HTTP 1.1。
1.3 gRPC:为微服务而生的通信框架
gRPC 是一个现代开源高性能 RPC 框架,官方定义将其描述为“可以在任何环境中运行,能够高效地连接数据中心内部和跨数据中心的服务,并支持负载均衡、链路追踪、健康检查和认证等可插拔能力,同时也适用于将设备、移动应用和浏览器连接到后端服务的最后一公里”。这一定义直接点明了 gRPC 的设计目标:构建一套通用、高性能、可扩展的服务间通信体系,而这正是微服务基础设施的核心诉求。
gRPC 由 Google 推出,底层基于 HTTP/2 协议传输,但它在 HTTP/2 的多路复用和长连接能力之上,进一步构建了一套完整的微服务通信体系。gRPC 使用 Protocol Buffers 作为接口定义语言和序列化协议,通过.proto文件精确定义服务方法、请求参数和返回类型,强类型的契约定义从协议层面消除了服务提供方和消费方之间的接口歧义。同时,Protocol Buffers 的二进制序列化相较于 JSON 等文本序列化格式,在数据体积和编解码性能上都有数量级的提升,这对于高频、高并发的服务间通信场景意义显著。HTTP/2 的多路复用和长连接能力被 gRPC 充分继承,单个 TCP 连接上可以同时承载多个并发请求,连接资源得到高效复用。
gRPC 最关键的差异化能力在于其双向流通信模型。HTTP 1.1 和 HTTP/2 的请求响应模型本质上是客户端主动、服务端被动,服务端推送只是 HTTP/2 的一种辅助机制。gRPC 则从根本上重新定义了客户端与服务端的交互关系,将通信模型抽象为四种调用方式:
一元调用,客户端发送一次请求、服务端返回一次响应,对应传统的请求响应模式;
服务端流式,客户端发送一次请求,服务端持续返回多个响应;
客户端流式,客户端持续发送多个请求,服务端最后返回一个响应;
双向流,客户端和服务端同时独立地发送和接收数据流,两个方向的流完全解耦。双向流的能力从根本上改变了推送的通信范式——服务端不需要等待客户端主动询问,就可以通过已建立的长连接随时向客户端推送消息。这在微服务架构中直接对应着配置变更实时推送、服务实例上下线通知、心跳保持与租约续约等核心通信场景,也是 Nacos 2.x 选择 gRPC 作为核心通信协议的根本原因。
1.4 Nacos 的选择:从HTTP短连接到gRPC长连接
理解了协议栈的演进,Nacos 2.x 的技术决策就变得清晰了。1.x 基于 HTTP 1.1 的通信模型面对大规模集群时,连接数膨胀、心跳风暴、推送延迟等问题逐步成为性能瓶颈。2.x 选择 gRPC 而不是直接使用 HTTP 2.0,核心原因在于 gRPC 在 HTTP 2.0 的传输能力之上,提供了开箱即用的双向流、强类型契约和经过大规模验证的连接管理机制,这些正是微服务基础设施最需要的能力。Client 与 Server 之间通过一条 gRPC 长连接承载所有通信,心跳、配置查询、服务注册、变更推送全部在这条连接上多路复用,连接资源消耗从与实例数线性相关降低为常量级,推送延迟从秒级降至毫秒级。这为 Nacos 走向更大规模集群奠定了通信层的核心基础。
二、gRPC 通信层源码剖析——Nacos 2.x 的性能基石
理解了 Nacos 的整体架构以及从 HTTP 转向 gRPC 的技术决策之后,接下来我们通过源码来了解Nacos对于gRPC通信处理的相关细节。
2.1 消息体与服务定义:有什么能力
这是所有 gRPC 开发的起点。你需要定义好客户端和服务端之间"说什么语言"。
首先是需要先编写.proto文件,定义消息体和服务。Nacos 的核心协议文件是nacos_grpc_service.proto,它定义了两种核心消息体和服务:
message:
Message:通信的数据载体,类似 Java 的
POJO。字段编号:
= 2、= 3是字段在二进制中的唯一标识,不可随意修改,决定了序列化/反序列化的兼容性。Any 类型:可以装载任意 Protobuf 消息,Nacos 用这个实现多态,
metadata.type决定了body里装的是什么具体请求/响应。service:
Request / Response 流:一元 RPC,用于客户端发送请求,服务端返回响应。例如服务注册、配置查询等。
BiRequestStream 流:建立一个双向流通道,这是 Nacos 实现服务端主动推送(如配置变更通知、服务实例下线通知)的关键。
api/src/main/proto/nacos_grpc_service.proto syntax = "proto3"; // 声明使用proto3语法 import "google/protobuf/any.proto"; // 导入Google的Any类型,用于表示任意类型的消息体。在Nacos中,这是实现请求/响应体多态的关键,服务发现、配置管理等不同模块的具体数据都通过它来承载 import "google/protobuf/timestamp.proto"; // 导入Google的时间戳类型,用于表示精确的时间点 option java_multiple_files = true; // 为每个消息/服务生成一个单独的Java文件 option java_package = "com.alibaba.nacos.api.grpc.auto"; // 指定生成Java代码到该路径下 message Metadata { // 定义元数据消息,用于承载请求的上下文信息 string type = 3; // 请求类型,用于区分是配置查询(ConfigQuery)、服务注册(InstancePublish)还是其他类型的请求。这是 Nacos 服务端进行路由分发的核心依据 string clientIp = 8; // 客户端IP地址,服务端通过它来识别请求来源,实现IP白名单、灰度发布等功能 map<string, string> headers = 7; // 请求头,以键值对形式存储 } message Payload { // 定义通信的有效载荷 Metadata metadata = 2; // 请求/响应的元数据,包含此次通信的上下文信息 google.protobuf.Any body = 3; // 请求/响应的具体内容。这是一个Any类型,可以装载任何符合protobuf定义的消息。 // 例如,当 metadata.type = "ConfigQuery"时,body装载的是ConfigQueryRequest/Response } service Request { // 发送一个普通请求,并同步等待响应 rpc request (Payload) returns (Payload) { } } service BiRequestStream { // 建立一个双向流连接。客户端和服务端可以互相发送一系列 Payload 消息。 // 这个连接是 Nacos 2.x 性能的基石,由 BiRequestStreamServerImpl 负责管理。 // 它的核心作用是: // 1. 服务端可以主动推送配置变更、服务实例上下线等通知给客户端,无需客户端轮询。 // 2. 客户端可以复用此连接发送心跳,大幅减少频繁建立和销毁连接的开销。 rpc requestBiStream (stream Payload) returns (stream Payload) { } }2.2 编译生成gRPC代码:根据定义生成java代码
在执行编译生成gRPC前,需要先阅读api模块下的maven依赖及相关代码生成插件。
这里需要注意的是,通常定义好消息体和服务后,需要执行gRPC代码生成插件,在/target的目录下生成对应的java代码。
这里由于Nacos为了让开发者拿到 Nacos 源码后,能开箱即用地启动核心服务,免除开发者安装protoc编译环境,避免因protoc版本不一致等环境问题导致项目无法编译,因此直接将生成好的gRPC-java代码放到了yourPath/nacos-2.4.3/api/src/main/java/com/alibaba/nacos/api/grpc/auto目录下,这也解释了为什么在"快速开始:Idea启动Nacos 2.4.3"小节中没有单独执行maven install,代码却没有爆红。
api/pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-all</artifactId> <version>${revision}</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>nacos-api</artifactId> <packaging>jar</packaging> <name>nacos-api ${project.version}</name> <url>https://nacos.io</url> <description>Nacos api pom.xml file</description> <build> // 执行gRPC代码生成的相关插件,如果需要生成grpc代码可以放开,如果这里放开后编译还是报错,可以参考consistency模块,需要注意引入extensions标签相关内容 <plugins> <!-- reuse when you need to update grpc model --> <!--<plugin> <groupId>org.xolstice.maven.plugins</groupId> <artifactId>protobuf-maven-plugin</artifactId> <version>0.5.0</version> <configuration> <protocArtifact>com.google.protobuf:protoc:3.8.0:exe:${os.detected.classifier}</protocArtifact> <pluginId>grpc-java</pluginId> <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.14.0:exe:${os.detected.classifier}</pluginArtifact> </configuration> <executions> <execution> <goals> <goal>compile</goal> <goal>compile-custom</goal> </goals> </execution> </executions> </plugin>--> </plugins> </build> <dependencies> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <scope>test</scope> </dependency> // gRPC的网络传输层。基于Netty实现HTTP/2通信,负责客户端和服务端之间真正的字节流收发 <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-netty-shaded</artifactId> </dependency> // gRPC和Protobuf之间的桥接层。提供ProtoUtils工具类、Marshaller等,让gRPC能够把Protobuf的Message对象序列化成网络传输的字节流。 <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-protobuf</artifactId> </dependency> // gRPC的Stub(桩)基类。代码里用到的RequestBlockingStub、RequestFutureStub、AbstractStub等客户端 Stub,以及服务端的BindableService接口,都来自这个包。它是*Grpc.java生成代码的父类来源。 <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-stub</artifactId> </dependency> // gRPC 的工具类库。包含一些辅助功能,如超时控制Deadline、并发工具等。 <dependency> <groupId>io.grpc</groupId> <artifactId>grpc-util</artifactId> </dependency> <dependency> <groupId>com.google.api.grpc</groupId> <artifactId>proto-google-common-protos</artifactId> </dependency> <dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> </dependency> <dependency> <groupId>javax.annotation</groupId> <artifactId>javax.annotation-api</artifactId> </dependency> </dependencies> </project> 下图展示Nacos将生成好的gRPC-java代码放到了yourPath/nacos-2.4.3/api/src/main/java/com/alibaba/nacos/api/grpc/auto目录下。
如果读者想自己动手生成gRPC-java代码,可以根据下图设置api模块下的pom文件(将consistency模块的extensions和plugins标签拷贝覆盖到该pom文件),然后在maven控制面板找到api模块下的protobuf插件,编译即可生成,也可以通过编译整个api模块来生成代码。
2.3 Stub(存根):谁来调用
Stub 是 gRPC 编译器根据.proto文件自动生成的客户端代理类。对开发者来说,调用 Stub 的方法就跟调用本地方法一样——传入请求对象,拿到响应对象,完全不需要关心底层的网络传输、序列化、HTTP2 帧解析等细节。
com.alibaba.nacos.api.grpc.auto.RequestGrpc // 创建一个支持该服务所有调用类型的异步 Stub public static RequestStub newStub(io.grpc.Channel channel) { // 定义 Stub 工厂,匿名内部类实现了 StubFactory 接口,用于创建 RequestStub 实例 io.grpc.stub.AbstractStub.StubFactory<RequestStub> factory = new io.grpc.stub.AbstractStub.StubFactory<RequestStub>() { @Override public RequestStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { // 每次需要新 Stub 时(比如设置超时、添加拦截器),都会调用这个方法 // Channel:底层传输通道(TCP/HTTP2 连接) // CallOptions:调用选项(截止时间、认证信息、元数据等) return new RequestStub(channel, callOptions); } }; // 通过工厂创建 Stub 实例 return RequestStub.newStub(factory, channel); } // 创建一个阻塞式 Stub,支持一元 RPC 和服务端流式调用 public static RequestBlockingStub newBlockingStub( io.grpc.Channel channel) { // 定义阻塞式 Stub 工厂 io.grpc.stub.AbstractStub.StubFactory<RequestBlockingStub> factory = new io.grpc.stub.AbstractStub.StubFactory<RequestBlockingStub>() { @Override public RequestBlockingStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { return new RequestBlockingStub(channel, callOptions); } }; return RequestBlockingStub.newStub(factory, channel); }2.4 Channel(通道): 建立连接通道
Channel 是客户端与服务端之间的虚拟连接,底层基于 TCP/HTTP2 协议。过程遵循"先握手、后建流"的原则:
客户端根据服务端 IP 和 gRPC 端口**(主端口 + 1000,如 8848 → 9848)创建
ManagedChannel,建立底层 TCP 连接。基于这个 Channel 创建一元 RPC 的
FutureStub,发起ServerCheck健康检查请求,获取服务端分配的connectionId。健康检查通过后,在同一个 Channel 上再创建双向流的
BiRequestStreamStub,调用requestBiStream()建立长连接流,并通过ConnectionSetupRequest完成客户端版本号、能力表、租户信息的上报。一旦双向流建立成功,后续客户端与服务端之间的所有通信都复用这个Channel,服务注册、配置查询、心跳保活、服务端推送等操作全部走同一条 TCP 连接,避免了频繁创建和销毁连接的开销。这也是 Nacos 2.x 相比于 1.x HTTP 短连接模式性能大幅提升的根本原因。
com.alibaba.nacos.common.remote.client.grpc.GrpcClient#connectToServer @Override public Connection connectToServer(ServerInfo serverInfo) { // 待分配的最新连接 ID,由服务端在握手时返回 String connectionId = ""; // 建立gRPC Channel,计算gRPC端口:服务端主端口 + 偏移量(默认 +1000,即 8848 → 9848) int port = serverInfo.getServerPort() + rpcPortOffset(); // 创建一个ManagedChannel,底层建立 TCP/HTTP2 连接,可配置TLS或明文传输(默认为明文) ManagedChannel managedChannel = createNewManagedChannel(serverInfo.getServerIp(), port); // 在一元RPC通道上创建异步Stub,用于后续的serverCheck RequestGrpc.RequestFutureStub newChannelStubTemp = createNewChannelStub(managedChannel); // 服务端健康检查(握手),向服务端发送ServerCheckRequest,验证服务端是否存活且兼容 Response response = serverCheck(serverInfo.getServerIp(), port, newChannelStubTemp); // 获取服务端返回的响应,其中包含服务端分配的唯一连接ID ServerCheckResponse serverCheckResponse = (ServerCheckResponse) response; connectionId = serverCheckResponse.getConnectionId(); // 创建双向流 Stub,复用上面已经建立的ManagedChannel,创建双向流的异步Stub,双向流模式下,客户端和服务端可以同时独立地发送和接收消息 BiRequestStreamGrpc.BiRequestStreamStub biRequestStreamStub = BiRequestStreamGrpc.newStub( newChannelStubTemp.getChannel()); // 封装连接对象, 创建一个GrpcConnection,grpcExecutor处理双向流消息线程池 GrpcConnection grpcConn = new GrpcConnection(serverInfo, grpcExecutor); // 设置服务端返回的连接 ID,后续所有通信都需要带上这个 ID 来标识身份 grpcConn.setConnectionId(connectionId); // 绑定双向流(重要),内部实现如下: // 1. 调用 biRequestStreamStub.requestBiStream(observer),发起双向流 RPC // 2. 返回一个 StreamObserver<Payload>,客户端通过它向服务端发送消息 // 3. 传入的 grpcConn 实现了 StreamObserver 接口,用于接收服务端推送的消息 StreamObserver<Payload> payloadStreamObserver = bindRequestStream(biRequestStreamStub, grpcConn); // 完善连接对象,将payloadStreamObserver设置到连接中,后续通过它发送请求 grpcConn.setPayloadStreamObserver(payloadStreamObserver); // 保留一元RPC的异步 Stub,用于后续的配置查询、服务注册等一次性请求 grpcConn.setGrpcFutureServiceStub(newChannelStubTemp); // 保留Channel引用,用于关闭连接 grpcConn.setChannel(managedChannel); // 发送连接建立请求,构造ConnectionSetupRequest,告诉服务端自己的身份和能力 ConnectionSetupRequest conSetupRequest = new ConnectionSetupRequest(); // 客户端版本号,服务端可以据此做兼容性处理 conSetupRequest.setClientVersion(getClientVersion()); // 客户端自定义标签,用于灰度发布、机房标识等场景 conSetupRequest.setLabels(super.getLabels()); // 设置客户端的能力表:声明自己支持哪些功能,比如是否支持批量注册、是否支持模糊查询等 conSetupRequest.setAbilityTable( NacosAbilityManagerHolder.getInstance().getCurrentNodeAbilities(abilityMode())); // 租户信息(命名空间 ID),用于多租户隔离 conSetupRequest.setTenant(super.getTenant()); // 通过双向流通道发送连接建立请求,完成最后的握手和注册 grpcConn.sendRequest(conSetupRequest); // 返回封装好的连接对象,上层通过它来与服务端通信 return grpcConn; }2.5 Server(服务端): 谁来处理
请求到了服务端之后,由 gRPC Server 负责接收和分发。服务端基于Netty构建,在BaseGrpcServer.startServer()中完成了服务注册、TLS协议、数据压缩/解压、心跳保活等相关处理,服务端启动后,Netty开始监听gRPC端口,接收客户端连接,并根据请求类型将消息分发到对应的Service实现类中处理。
com.alibaba.nacos.core.remote.grpc.BaseGrpcServer#startServer @Override public void startServer() throws Exception { // 创建可变的服务处理器注册表 final MutableHandlerRegistry handlerRegistry = new MutableHandlerRegistry(); // 注册gRPC服务实现 addServices(handlerRegistry, getSeverInterceptors().toArray(new ServerInterceptor[0])); // 创建Netty服务端构建器,监听端口,默认为主端口 + 1000(即 8848 → 9848) NettyServerBuilder builder = NettyServerBuilder.forPort(getServicePort()).executor(getRpcExecutor()); // 配置TLS协议协商器,如果配置了TLS证书,加入到builder中,启用 TLS 加密传输,否则使用使用明文传输(HTTP/2 的 h2c 模式) Optional<InternalProtocolNegotiator.ProtocolNegotiator> negotiator = newProtocolNegotiator(); if (negotiator.isPresent()) { InternalProtocolNegotiator.ProtocolNegotiator actual = negotiator.get(); Loggers.REMOTE.info("Add protocol negotiator {}", actual.getClass().getCanonicalName()); builder.protocolNegotiator(actual); } // 添加传输层过滤器,用于连接和日志管理 for (ServerTransportFilter each : getServerTransportFilters()) { builder.addTransportFilter(each); } // 配置并构建 Server server = builder .maxInboundMessageSize(getMaxInboundMessageSize()) // 可通过环境变量配置,默认10MB .fallbackHandlerRegistry(handlerRegistry) // 请求的服务方法找不到时的回调兜底 .compressorRegistry(CompressorRegistry.getDefaultInstance()) // 压缩响应数据 .decompressorRegistry(DecompressorRegistry.getDefaultInstance()) // 如果客户端压缩了请求体,这里进行解压 .keepAliveTime(getKeepAliveTime(), TimeUnit.MILLISECONDS) // 服务端向客户端发送心跳的间隔时间 .keepAliveTimeout(getKeepAliveTimeout(), TimeUnit.MILLISECONDS) // 心跳超时时间 .permitKeepAliveTime(getPermitKeepAliveTime(), TimeUnit.MILLISECONDS) // 没有活跃的Stream,也允许发送心跳的最小间隔 .build(); // 启动 server.start(); }2.6 序列化:怎么传
gRPC 默认使用protobuf作为序列化协议。相比传统的JSON,protobuf编码后的数据体积更小、解析速度更快,特别适合微服务场景下的高频通信。在"消息体服务与定义"小节里,通信载体Payload的body字段使用了google.protobuf.Any类型,可以装载任意 Protobuf 消息。服务端收到请求后,通过PayloadRegistry(类型注册表)根据metadata.type查找到对应的具体消息类型,再将Any反序列化为ConfigQueryRequest、ConfigPublishRequest等具体对象——这种"类型路由 + 动态反序列化"的机制,让 Nacos 所有业务模块得以复用同一条 gRPC 通道。
com.alibaba.nacos.common.remote.client.grpc.GrpcUtils#parse // 将Payload解析为具体的请求/响应模型对象 public static Object parse(Payload payload) { // 根据type获取对应的Java类型,PayloadRegistry 是Nacos内部的类型注册表,维护了 type → Class<?> 的映射关系 Class classType = PayloadRegistry.getClassByType(payload.getMetadata().getType()); // 提取 body 中的二进制数据 // payload.getBody() 返回的是 google.protobuf.Any 类型,Any.getValue() 返回 ByteString, // 即未被解析的原始字节序列,ByteString 是 Protobuf 的二进制封装类,比普通 byte[] 更高效 ByteString byteString = payload.getBody().getValue(); // 将ByteString转换为只读的ByteBuffer,只读模式可以避免意外修改数据,同时保证线程安全 ByteBuffer byteBuffer = byteString.asReadOnlyByteBuffer(); // 反序列化为具体对象 // 1. 将ByteBuffer包装为InputStream,使其能被Jackson读取,为什么用Jackson而不是Protobuf的反序列化? // 因为body中存储的是JSON格式的数据,Protobuf的Any只是把它当作一个字节块来传输 // 2. 将JSON字符串反序列化为classType指定的java对象,底层使用Jackson的ObjectMapper // 3. 反序列化的结果就是具体类型的对象,如ConfigQueryRequest实例 Object obj = JacksonUtils.toObj(new ByteBufferBackedInputStream(byteBuffer), classType); // 补充请求头信息,如果解析出来的对象是 Request 类型,需要将 Metadata 中的 headers 也注入进去 // Request接口定义了 putAllHeader() 方法,用于携带客户端传来的附加信息 // headers 中包含的内容可能有:Client-Id、Client-Versio、用户自定义的标签、认证信息等信息 if (obj instanceof Request) { ((Request) obj).putAllHeader(payload.getMetadata().getHeadersMap()); } // 返回解析好的业务对象 return obj; }2.7 流程串联:梳理gRPC开发及执行流程
理解了gRPC的设计理念之后,我们通过一张流程图来直观地梳理gRPC的开发步骤与请求执行的全链路。下图从.proto协议定义文件开始,一直到一次完整的RPC调用结束,覆盖了开发阶段和运行时阶段两个维度。
一切从Proto File开始。开发者需要编写.proto文件来定义gRPC服务的接口契约,其中包括消息体和服务结构。这一步是gRPC开发的起点,也是服务提供方和消费方之间唯一的“协议共识”。以Nacos的配置查询为例,需要在.proto中声明service服务和message消息,消息中的每个字段都需要指定类型和字段编号。这份.proto文件就是后续所有代码生成的唯一输入源。有了协议定义之后,protoc编译器介入,根据.proto文件自动生成对应语言的代码。产出的代码分为两个分支:一边是客户端的Client Stub,它封装了远程调用的序列化与网络传输逻辑,让本地调用看起来就像调用一个本地方法;另一边是服务端的Server Service Interface,它是一个抽象接口,开发者只需要实现这个接口来编写具体的业务逻辑,而网络接收、请求分发、序列化与反序列化全部由gRPC框架自动完成。
进入运行时阶段,客户端通过Stub发起远程调用,Stub将请求消息序列化为二进制数据,通过Channel发送出去。Channel底层基于TCP连接,使用HTTP/2协议与Server通信,多路复用让一条连接上可以同时承载多个并发请求。服务端收到数据后,gRPC框架自动完成反序列化,将二进制数据还原为服务端代码可以直接处理的内存对象,然后根据请求中的方法名分发到对应的Service实现上。业务逻辑处理完毕后,服务端将响应消息序列化,沿原路通过HTTP/2连接返回给客户端。客户端Channel接收到响应数据后,Stub完成反序列化,将结果以方法返回值的正常形式交还给调用方。整个过程中,开发者只需要关注三步:定义.proto文件、实现服务端接口、通过Stub发起调用,其余的网络传输、序列化、流控、错误处理全部由gRPC框架承担,这就是RPC框架的核心价值所在。
书籍参考:
《gRPC: Up and Running: Building Cloud Native Applications with Go and Java for Docker and Kubernetes》
——Kasun Indrasiri、Danesh Kuruppu
三、全文小结
本文聚焦通信协议的演进历程,从 HTTP 1.1 的短连接瓶颈到 gRPC 双向流的性能突破,详细分析了 Nacos 2.x 通信层的技术决策与源码实现。
在协议层面,HTTP 1.1 的短连接模式与单向请求响应模型,在大规模集群中导致连接数膨胀、心跳风暴和推送延迟三重瓶颈。HTTP/2 引入二进制分帧与多路复用解决了队头阻塞问题,而 gRPC 在 HTTP/2 的传输能力之上,进一步提供了双向流通信、Protocol Buffers 强类型契约和开箱即用的连接管理,这正是 Nacos 2.x 选择 gRPC 而非直接使用 HTTP/2 的根本原因。一条 gRPC 长连接承载所有通信,连接资源消耗降为常量级,推送延迟压缩至毫秒级,单机可支撑的实例数量提升了一个数量级。
在源码层面,本文逐一拆解了 gRPC 通信层的六个核心环节:nacos_grpc_service.proto定义消息体与服务契约,protoc 编译器自动生成 Stub 与 Service Interface,GrpcClient.connectToServer()完成 TCP 建连、健康检查握手与双向流绑定,BaseGrpcServer.startServer()基于 Netty 完成服务注册与心跳保活,PayloadRegistry通过类型路由实现多业务模块复用同一条 gRPC 通道,以及整个开发与运行时的全流程串联。理解这一层,就掌握了 Nacos 2.x 性能基石的构建逻辑。
建立通信层的全局认知后,下篇将进入配置中心客户端的源码实战,从 Spring Boot 自动装配机制入手,深入拆解NacosConfigDataLoader远程配置拉取、NacosContextRefresher监听器注册与 gRPC 长连接建立的完整初始化链路。
原创不易,如果本文对您有帮助,带来了些许灵感或启发,烦请动动小手点赞、关注、转发、收藏。这是作者持续更新的动力源泉,衷心感谢您的支持。我会尽量在工作之余,为大家带来更高质量的内容,努力保持周更。