一、NaviRPC 整体架构概览

NaviRPC 的设计目标非常明确:在超高并发、大规模集群环境下提供稳定的 RPC 通信能力。与 Dubbo、gRPC 等开源框架相比,NaviRPC 更侧重于百度内部的特定场景——多数据中心部署、跨机房调用、服务实例数量级达到万级。

框架的核心分层如下:

// 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 的超时机制非常精细,支持三个级别的超时配置:

重试策略同样分层设计:可重试异常(网络超时、连接重置等)会自动重试,不可重试异常(业务异常、参数校验失败等)直接返回错误。重试时会自动避开上次调用的节点,避免在故障节点上反复尝试。

// 重试策略核心逻辑
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 心跳、自适应超时……这些细节共同构成了一个可靠的高性能通信基础设施。