一、NaviRPC 整体架构概览
NaviRPC 的设计目标非常明确:在超高并发、大规模集群环境下提供稳定的 RPC 通信能力。与 Dubbo、gRPC 等开源框架相比,NaviRPC 更侧重于百度内部的特定场景——多数据中心部署、跨机房调用、服务实例数量级达到万级。
框架的核心分层如下:
- API 层:面向开发者的编程接口,通过 Java Interface + Annotation 定义服务契约。
- Registry 层:服务注册与发现,对接百度内部的 Naming 服务。
- Cluster 层:集群容错策略,包含负载均衡、路由规则、熔断降级。
- Transport 层:网络通信层,负责连接管理、序列化与反序列化、IO 多路复用。
- Protocol 层:协议编解码,定义请求/响应的报文格式。
// NaviRPC 服务定义示例
@NaviService(version = "1.0.0", group = "billing")
public interface BillingService {
@NaviMethod(timeout = 3000, retries = 2)
BillingResult settle(BillingRequest request);
@NaviMethod(timeout = 1000, retries = 0)
AccountBalance queryBalance(String accountId);
}
// 消费端引用
@NaviReference(version = "1.0.0", group = "billing",
loadBalance = "weighted_round_robin")
private BillingService billingService;
这种基于接口的编程模型对开发者非常友好,屏蔽了底层网络通信的复杂性。但从源码层面看,从 @NaviReference 注解到一次完整的远程调用,中间经历了服务发现、负载均衡选址、连接获取、序列化、网络传输、反序列化、结果回调等多个环节。
二、服务发现与动态路由
NaviRPC 的服务发现基于百度内部的 Naming 系统(类似 Consul/Zookeeper 但针对大规模场景做了深度优化)。每个服务提供者在启动时向 Naming 注册自己的地址信息,消费者通过订阅获取服务列表。
与常规的服务发现不同,NaviRPC 引入了服务分片路由的概念。在广告结算这类场景中,同一个服务集群往往按照商户 ID 做数据分片,消费者需要根据请求参数将调用路由到正确的分片节点,否则会导致跨分片访问和额外的网络开销。
// 路由规则配置示例
public class ShardRouter implements Router {
@Override
public List<Invoker> route(List<Invoker> invokers, Invocation invocation) {
String merchantId = invocation.getAttachment("merchantId");
int shardCount = invokers.stream()
.map(inv -> inv.getUrl().getParameter("shard"))
.distinct()
.size();
int shardIndex = Math.abs(merchantId.hashCode()) % shardCount;
return invokers.stream()
.filter(inv -> shardIndex == Integer.parseInt(
inv.getUrl().getParameter("shard")))
.collect(Collectors.toList());
}
}
Naming 的高可用设计也值得关注。NaviRPC 在客户端本地维护了一份服务地址缓存,当 Naming 中心不可用时,消费者仍能使用缓存中的地址进行调用。同时,NaviRPC 实现了 长轮询 + 推拉结合 的地址变更通知机制:客户端定期向 Naming 发送长轮询请求,如果服务列表没有变化,Naming 会 hold 住请求直到超时;一旦有变化,Naming 立即返回变更内容,实现准实时的地址更新。
三、连接池与网络传输优化
RPC 框架的性能瓶颈通常在网络 IO 层。NaviRPC 在连接管理和网络传输方面做了大量优化。
连接池设计:NaviRPC 采用了分组连接池策略。同一个 Consumer 对同一个 Provider 会建立多条长连接(默认 5 条),通过轮询分配请求到不同的连接上,避免单连接成为瓶颈。连接池的 key 是 Consumer IP + Provider IP + 端口,每个连接池独立管理自己的连接生命周期。
// 连接池核心逻辑(简化)
public class NaviConnectionPool {
private final Map<Address, ConnectionGroup> pools =
new ConcurrentHashMap<>();
public Connection get(Address address) {
return pools.computeIfAbsent(address, addr -> {
ConnectionGroup group = new ConnectionGroup(
addr, maxConnections);
group.init();
return group;
}).getIdleConnection();
}
public void release(Connection conn) {
ConnectionGroup group = pools.get(conn.getAddress());
if (group != null) {
group.releaseConnection(conn);
}
}
}
class ConnectionGroup {
private final Queue<Connection> idleConnections =
new ConcurrentLinkedQueue<>();
private final AtomicInteger activeCount = new AtomicInteger(0);
public synchronized Connection getIdleConnection() {
Connection conn = idleConnections.poll();
if (conn == null && activeCount.get() < maxConnections) {
conn = createNewConnection();
activeCount.incrementAndGet();
}
if (conn != null && !conn.isActive()) {
activeCount.decrementAndGet();
return getIdleConnection(); // 递归获取新连接
}
return conn;
}
}
网络传输:NaviRPC 默认使用 Netty 作为网络框架,采用非阻塞 IO + Reactor 多线程模型。在协议层面,NaviRPC 定义了自定义的二进制协议,报文结构为:[Magic Number(2B)] [Version(1B)] [RequestId(8B)] [Codec(1B)] [MessageType(1B)] [Body Length(4B)] [Body(NB)]。相比 HTTP/JSON 协议,二进制协议在序列化开销和网络传输体积上都有显著优势。
在序列化方面,NaviRPC 支持 Hessian、Protobuf 和自定义的高性能序列化器。对于内部高频调用的服务,推荐使用 Protobuf,其序列化/反序列化速度比 Hessian 快 3-5 倍,序列化后体积减少 40-60%。
超时与重试:NaviRPC 的超时机制非常精细,支持三个级别的超时配置:
- 全局超时:Consumer 端的全局默认超时时间。
- 接口级超时:通过
@NaviMethod(timeout = 3000)在方法级别指定。 - 动态超时:根据历史调用的 P99 延迟自动调整超时阈值。
重试策略同样分层设计:可重试异常(网络超时、连接重置等)会自动重试,不可重试异常(业务异常、参数校验失败等)直接返回错误。重试时会自动避开上次调用的节点,避免在故障节点上反复尝试。
// 重试策略核心逻辑
public class FailoverClusterInvoker<T> implements Invoker<T> {
@Override
public Result invoke(Invocation invocation) throws RpcException {
int retries = getRetries(invocation);
List<Invoker<T>> selected = loadBalance.select(invokers, invocation);
Address lastFailed = null;
for (int i = 0; i <= retries; i++) {
try {
Invoker<T> invoker = selectInvoker(selected, lastFailed);
return invoker.invoke(invocation);
} catch (RpcException e) {
lastFailed = e.getFailedAddress();
if (!e.isRetryable() || i == retries) {
throw e;
}
// 重试前更新服务列表,过滤掉故障节点
selected = loadBalance.select(
filterHealthy(invokers), invocation);
}
}
throw new RpcException("All retries failed");
}
}
作为 NaviRPC 的源码贡献者,我参与了连接池管理和负载均衡模块的优化工作。最大的感受是:一个成熟的 RPC 框架不是某个单点的极致优化,而是每个环节的精细打磨——连接复用、连接预热、慢连接检测、优雅关闭、Keep-Alive 心跳、自适应超时……这些细节共同构成了一个可靠的高性能通信基础设施。