#2.2 route-thrift

55 阅读24分钟

Thrift 路由学习笔记

📚 目录


1. 核心概念

1.1 什么是 Thrift 路由?

Thrift 路由 是指在使用 Thrift RPC 协议时,框架自动选择目标机房和节点的机制。

核心特点:

  • 框架层路由: 路由决策在 Thrift 框架内部完成
  • 业务无感知: 业务代码拿到的永远是同一个客户端
  • 动态路由: 每次 RPC 调用都重新决策
  • 高性能: 二进制协议 + 压缩,比 HTTP 快 2-3 倍

1.2 关键组件

┌─────────────────────────────────────────────────┐
│  CustomThriftProxy (自定义 Thrift 代理)          │
│  - 继承 ThriftClientProxy                       │
│  - 注入 KrsRouteService (路由服务)              │
│  - 注入 KrsOCTOAgentCluster (集群管理器)        │
└─────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────┐
│  KrsRouteService (路由决策服务)                  │
│  - 从配置中心读取路由配置                        │
│  - 支持按比例分流 (RouteTable)                  │
│  - 支持动态切换 (配置热更新)                     │
└─────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────┐
│  KrsOCTOAgentCluster (集群管理器)                │
│  - 获取路由目标 IDC                              │
│  - 按 IDC 过滤节点                               │
│  - 返回可用节点列表                              │
└─────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────┐
│  MTThriftMethodInterceptor (CGLIB 拦截器)       │
│  - 拦截所有方法调用                              │
│  - 调用 cluster.getServerConnList()             │
│  - 负载均衡选择节点                              │
│  - 发送 Thrift RPC 请求                         │
└─────────────────────────────────────────────────┘

2. HTTP vs Thrift 路由对比

2.1 架构对比

HTTP 模式架构
┌─────────────────────────────────────────────────┐
│         KrsSearchManager (多集群管理器)          │
│                                                 │
│  clients = {                                    │
│    "appkey.mt"EsClient1 (MT机房),           │
│    "appkey.zf"EsClient2 (ZF机房),           │
│    "appkey.sh"EsClient3 (SH机房)            │
│  }                                              │
│                                                 │
│  ⭐ getClient() {                               │
│    // 应用层路由决策                            │String idc = routeTable.getRouteIdc();      │
│    return clients.get("appkey." + idc);        │
│  }                                              │
└─────────────────────────────────────────────────┘
Thrift 模式架构
┌─────────────────────────────────────────────────┐
│         KrsSearchManager (多集群管理器)          │
│                                                 │
│  thriftWrapperClient = EsClient(thriftProxy)   │
│                                                 │
│  getClient() {                                  │
│    return thriftWrapperClient;  // 直接返回    │
│  }                                              │
└─────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────┐
│              CustomThriftProxy                  │
│  - krsRouteService (路由服务)                   │
│  - cluster = KrsOCTOAgentCluster                │
│  - serviceProxy = CGLIB 动态代理                │
└─────────────────────────────────────────────────┘

2.2 核心差异

维度HTTP 模式Thrift 模式
路由决策位置应用层 (getClient())框架层 (getServerConnList())
客户端数量多个 (每个机房一个)单个 (包装 Thrift 代理)
路由时机获取客户端时每次 RPC 调用时
业务感知✅ 能感知不同客户端❌ 完全无感知
拦截机制CGLIB 动态代理
通信协议HTTP/REST (JSON)Thrift RPC (Binary)
端口84129888
性能基准 (1x)快 2-3 倍

3. Thrift 路由完整流程

3.1 调用链路图

【业务代码】
client = searchManagerProxy.getClient();
// 返回: thriftWrapperClient (固定的)

client.search(request, callback);
    ↓
【CGLIB 拦截】
MTThriftMethodInterceptor.intercept()
    ↓
【框架层路由决策】
cluster.getServerConnList() {
    // 1. 获取路由目标 IDC
    String idc = krsRouteService.getReadRouteIdcName();
    // ⭐ 调用 routeTable.getRouteIdc()
    // 生成随机数: random = 30 → "mt"
    //            random = 70 → "zf"

    // 2. 按 IDC 过滤节点
    return filterByIdc(idc);
}
    ↓
【返回过滤后的节点列表】
[ServerConn(10.1.1.1:9888, az="mt"), ServerConn(10.1.1.2:9888, az="mt")]
    ↓
【负载均衡】
loadBalancer.select(serverConnList)
选择: 10.1.1.1:9888
    ↓
【发送 Thrift RPC 请求】
thriftClient.send(10.1.1.1:9888, request)
    ↓
【返回结果】

3.2 关键代码位置

KrsRouteService.java (路由决策)
public String getReadRouteIdcName() {
    if (RouteUtil.newRouteEnable(this.remoteAppKey)) {
        return routeTable.getRouteIdc();  // ⭐ 按比例随机
    }
    return readRouteIdcName;
}
KrsOCTOAgentCluster.java (节点过滤)
private List<ServerConn> filterConnsByDegradedAndIdc(List<ServerConn> connList) {
    // 1. 获取路由目标 IDC
    String routeIdcName = this.krsRouteService.getReadRouteIdcName();

    // 2. 按 IDC 过滤节点
    List<ServerConn> validConnList = new ArrayList<>();
    for (ServerConn conn : serverConnList) {
        if (routeIdcName.equalsIgnoreCase(conn.getServer().getAz())) {
            validConnList.add(conn);
        }
    }
    return validConnList;
}

4. 框架层路由实现原理

4.1 三层拦截架构

┌─────────────────────────────────────────────────┐
│           第1层: CGLIB 动态代理                  │
│  业务代码调用 thriftClient.search() 时被拦截    │
│  ↓                                              │
│  MTThriftMethodInterceptor.intercept()         │
└─────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────┐
│           第2层: 集群管理器 (Cluster)            │
│  KrsOCTOAgentCluster.getServerConnList()       │
│  ↓                                              │
│  1. 调用 krsRouteService.getReadRouteIdcName() │
│  2. 调用 filterConnsByDegradedAndIdc()         │
└─────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────┐
│           第3层: 负载均衡器 (LoadBalancer)       │
│  从过滤后的节点列表中选择一个具体节点            │
│  ↓                                              │
│  loadBalancer.select(serverConnList)           │
└─────────────────────────────────────────────────┘

4.2 CGLIB 动态代理

代理创建 (ThriftClientProxy.java)
private void loadServiceProxy() {
    // ⭐ 创建拦截器,传入 cluster
    MTThriftMethodInterceptor clientInterceptor =
        new MTThriftMethodInterceptor(this, cluster, getLoadBalancer());
    //                                      ↑
    //                          这里的 cluster 是子类注入的 KrsOCTOAgentCluster!

    // ⭐ 创建 CGLIB 代理
    ProxyFactory pf = new ProxyFactory(interfaceClazz, clientInterceptor);
    serviceProxy = pf.getProxy();
}
方法拦截 (MTThriftMethodInterceptor.java - 真实代码)

🔍 为什么调用 client.search() 会被拦截?

因为 client 是 CGLIB 创建的代理对象,不是真实对象!

// ThriftClientProxy.java:679-684
ProxyFactory pf = new ProxyFactory(interfaceClazz, clientInterceptor);
//                                                   ↑
//                                    这里传入了 MTThriftMethodInterceptor
serviceProxy = pf.getProxy();
//             ↑
//    返回的是 CGLIB 代理对象,不是真实的 KrsSearchThriftService!

业务代码拿到的 client 实际上是这样的:

// 业务代码
EsClient client = searchManagerProxy.getClient();
// client 的真实类型是: KrsSearchThriftService$AsyncIface$$EnhancerByCGLIB$$12345678

// 当调用 search() 方法时
client.search(request, callback);
// ↓
// CGLIB 会拦截这个调用,转发给 MTThriftMethodInterceptor.intercept()

MTThriftMethodInterceptor 的真实代码:

// MTThriftMethodInterceptor.java:81-89
public class MTThriftMethodInterceptor implements MethodInterceptor {
    private ThriftClientProxy clientProxy;
    private ICluster cluster;  // KrsOCTOAgentCluster
    private ILoadBalancer loadBalancer;

    // ⭐ CGLIB 会调用这个方法拦截所有方法调用
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
            throws Throwable {
        // 构建 RPC 调用上下文
        RpcInvocation invocation = buildRpcInvocation(method, args);

        // 执行过滤器链(包含路由决策)
        RpcResult result = filterChain.invoke(invocation);

        return result.getValue();
    }
}

doInvoke 方法(真实的路由逻辑):

// MTThriftMethodInterceptor.java:257-319
private Object doInvoke(RpcInvocation invocation) throws Throwable {
    MethodInvocation methodInvocation = invocation.getMethodInvocation();
    String methodName = methodInvocation.getMethod().getName();

    // ⭐ 第1步: 获取可用节点列表(触发路由决策!)
    // MTThriftMethodInterceptor.java:285
    List<ServerConn> connList = cluster.getServerConnList(routerMetaData);
    //                          ↑
    //                    这里会调用 KrsOCTOAgentCluster.getServerConnList()
    //                    触发路由决策: 获取目标 IDC → 按 IDC 过滤节点

    if (null == connList || connList.isEmpty()) {
        throw new TException("connection list is empty!");
    }

    // 第2步: 创建调用上下文
    InvokerContext invokerContext = new InvokerContext(
        methodInvocation, connList, timeline, methodName, invocation, timeout, ...);

    // 第3步: 执行重试逻辑(包含负载均衡和发送请求)
    return doInvokeWithRetry(invokerContext);
}

doInvokeWithRetry 方法(负载均衡和发送请求):

// MTThriftMethodInterceptor.java:332-428
private Object doInvokeWithRetry(InvokerContext invokerContext) throws Throwable {
    List<ServerConn> connList = invokerContext.getConnList();

    // 重试循环
    for (int i = 0; connList.size() > 0 && i < this.retryTimes; i++) {

        // ⭐ 第1步: 负载均衡选择一个节点
        // MTThriftMethodInterceptor.java:348
        ServerConn serverConn = loadBalancer.select(connList, methodInvocation);
        //                      ↑
        //            从过滤后的节点列表中选择一个
        //            例如: 从 [10.1.1.1:9888, 10.1.1.2:9888] 中选择 10.1.1.1:9888

        // ⭐ 第2步: 获取连接
        Object socket = getConnection(invokerContext, serverConn);

        if (socket == null) {
            // 连接获取失败,移除这个节点,重试下一个
            connList = removeConn(invokerContext, connList, serverConn);
            continue;
        }

        // ⭐ 第3步: 发送 Thrift RPC 请求
        Entry<Object, Throwable> rpcResult = getRpcResult(invokerContext, serverConn, socket);

        if (rpcResult != null && rpcResult.getT1() != null) {
            return rpcResult.getT1();  // 成功返回
        }

        // 如果是网络异常,重试下一个节点
        if (shouldRetry(rpcResult)) {
            connList = removeConn(invokerContext, connList, serverConn);
            continue;
        }
    }

    throw toThrow;
}

完整的拦截流程:

【业务代码】
client.search(request, callback);
    ↓
【CGLIB 检测到方法调用】
发现 client 是代理对象,不是真实对象
    ↓
【CGLIB 调用拦截器】
MTThriftMethodInterceptor.intercept(obj, search方法, [request, callback], proxy)
    ↓
【拦截器内部】
1. filterChain.invoke(invocation)
    ↓
2. doInvoke(invocation)
    ↓
3. cluster.getServerConnList(routerMetaData)  // ⭐ 触发路由决策
    ↓
    KrsOCTOAgentCluster.getServerConnList()
        ↓
        krsRouteService.getReadRouteIdcName()  // 获取目标 IDC: "mt"filterConnsByDegradedAndIdc(connList)  // 按 IDC 过滤节点
        ↓
        返回: [ServerConn(10.1.1.1:9888, az="mt"), ServerConn(10.1.1.2:9888, az="mt")]
    ↓
4. doInvokeWithRetry(invokerContext)
    ↓
5. loadBalancer.select(connList)  // 负载均衡选择: 10.1.1.1:98886. getConnection(serverConn)  // 获取连接
    ↓
7. getRpcResult(serverConn, socket)  // 发送 Thrift RPC 请求
    ↓
【返回结果】

4.3 为什么说是"框架层路由"?

对比维度HTTP 模式(应用层)Thrift 模式(框架层)
路由决策位置KrsSearchManager.getClient()MTThriftMethodInterceptor.intercept()
触发时机业务代码调用 getClient()业务代码调用 RPC 方法时
业务感知✅ 能感知不同客户端❌ 完全无感知
拦截机制无拦截,直接返回CGLIB 动态代理拦截

5. CustomThriftProxy 设计解析

5.1 为什么要自定义?

问题: MTThrift 框架的 ThriftClientProxy 不支持自定义路由逻辑

解决方案: 继承 ThriftClientProxy,通过反射注入自定义组件

5.2 继承关系

public class CustomThriftProxy extends ThriftClientProxy {
    private KrsRouteService krsRouteService;  // 自定义路由服务

    @Override
    public void afterPropertiesSet() throws Exception {
        // 1. 初始化自定义路由服务
        this.krsRouteService = new KrsRouteService(this);

        // 2. 配置自定义集群管理器
        configClusterManager();

        // 3. 调用父类的 loadServiceProxy()
        this.execSuperMethodByReflect("loadServiceProxy", null, null);
    }

    private void configClusterManager() throws Exception {
        // ⭐ 通过反射替换父类的 cluster 字段
        this.updateSuperFieldByReflect("cluster", new KrsOCTOAgentCluster(this));
    }
}

5.3 核心设计思想

1. 继承父类,复用功能
✅ 不需要重写 loadServiceProxy() (100+ 行)
✅ 不需要重写 initTrace() (50+ 行)
✅ 不需要重写 configureAuth() (80+ 行)
✅ 不需要重写其他 20+ 个方法
✅ 总共复用父类 1000+ 行代码
2. 反射注入,替换关键组件
// 通过反射修改父类的 cluster 字段
this.updateSuperFieldByReflect("cluster", new KrsOCTOAgentCluster(this));
3. 父类自动使用自定义组件
// 父类的 loadServiceProxy() 中
MTThriftMethodInterceptor clientInterceptor =
    new MTThriftMethodInterceptor(this, cluster, ...);
//                                      ↑
//                          这里的 cluster 是子类注入的 KrsOCTOAgentCluster!

5.4 完整调用链路

【应用启动】
    ↓
CustomThriftProxy.afterPropertiesSet()
    ↓
【第1步】初始化自定义路由服务
this.krsRouteService = new KrsRouteService(this);
    ↓
【第2步】配置自定义集群管理器
configClusterManager() {
    this.updateSuperFieldByReflect("cluster", new KrsOCTOAgentCluster(this));
}
    ↓
【第3步】调用父类的 loadServiceProxy()
this.execSuperMethodByReflect("loadServiceProxy", null, null);
    ↓
【父类方法】ThriftClientProxy.loadServiceProxy()
    ↓
创建拦截器:
MTThriftMethodInterceptor clientInterceptor =
    new MTThriftMethodInterceptor(this, cluster, ...);
    //                                  ↑
    //                    这里的 cluster 是 KrsOCTOAgentCluster!
    ↓
创建 CGLIB 代理:
serviceProxy = pf.getProxy();
    ↓
【业务调用】
client.search(request, callback);
    ↓
【CGLIB 拦截】→【自定义路由】→【发送请求】

6. 初始化流程详解

6.1 完整初始化流程

@Override
public void afterPropertiesSet() throws Exception {
    // ========== 第1阶段: 基础环境初始化 ==========
    MTthriftDependencyVersionChecker.checkMtthriftDependencyVersion();
    ContextInitializer.init();  // 初始化配置中心连接

    // ========== 第2阶段: Thrift 客户端基础配置 ==========
    this.execSuperMethodByReflect("portPreCheck", null, null);
    this.execSuperMethodByReflect("loadAppkey", null, null);
    this.execSuperMethodByReflect("loadServiceName", null, null);
    this.execSuperMethodByReflect("loadClassLoader", null, null);
    this.execSuperMethodByReflect("initFunctionProtocol", null, null);
    this.execSuperMethodByReflect("configAnnotation", null, null);
    this.execSuperMethodByReflect("loadLoadBalancer", ...);

    // ========== 第3阶段: 网络和异步配置 ==========
    this.updateSuperFieldByReflect("localEndpoint", ...);
    if (super.isAsync() && !super.isNettyIO()) {
        this.execSuperMethodByReflect("initAsyncThriftResourceIfNeeded", ...);
    }

    // ========== 第4阶段: 路由和服务发现(核心!) ==========
    this.krsRouteService = new KrsRouteService(this);  // ⭐ 路由服务
    configClusterManager();  // ⭐ 集群管理器

    // ========== 第5阶段: 高级功能配置 ==========
    this.execSuperMethodByReflect("initMccConfig", null, null);
    this.execSuperMethodByReflect("loadServiceProxy", null, null);  // ⭐ CGLIB 代理
    this.execSuperMethodByReflect("initTrace", null, null);
    this.execSuperMethodByReflect("configureAuth", null, null);
    this.execSuperMethodByReflect("initHttpServer", null, null);
    this.execSuperMethodByReflect("initNettyChannel", null, null);
    this.execSuperMethodByReflect("loadClientInfo", null, null);
    this.execSuperMethodByReflect("loadMeshConfig", null, null);
    this.execSuperMethodByReflect("loadServiceInterface", null, null);

    CellConfigManager.getInstance();
    TlsUtil.initLocalTlsAppkeys();
    this.updateSuperFieldByReflect("started", true);
}

6.2 关键步骤说明

步骤方法作用
1checkMtthriftDependencyVersion()检查 MTThrift 框架版本
2ContextInitializer.init()初始化配置中心连接
3portPreCheck()检查端口配置
4loadAppkey()加载应用 AppKey
5loadServiceName()加载服务名称
6initFunctionProtocol()初始化 Thrift 协议
7loadLoadBalancer()加载负载均衡器
8new KrsRouteService()⭐ 初始化路由服务
9configClusterManager()⭐ 创建集群管理器
10loadServiceProxy()⭐ 创建 CGLIB 代理
11initTrace()初始化链路追踪
12configureAuth()配置 TLS 鉴权
13initNettyChannel()初始化 Netty 连接池

6.3 为什么用反射?

// 父类的方法是 private 的
private void loadAppkey() { ... }
private void loadServiceName() { ... }

// 子类想自定义初始化顺序,但又要复用父类逻辑
// 所以用反射强制调用
private void execSuperMethodByReflect(String methodName, ...) throws Exception {
    Method declaredMethod = SUPER_CLAZZ.getDeclaredMethod(methodName, ...);
    declaredMethod.setAccessible(true);  // 绕过访问权限检查
    declaredMethod.invoke(this, args);
}

7. 连接池初始化详解

核心问题: KrsOCTOAgentCluster.getServerConnList() 是如何获取到所有连接的?

7.1 核心答案

简短回答:

KrsOCTOAgentCluster.getServerConnList() 获取连接的过程:

1. 应用启动时,从 MNS (服务发现) 获取服务节点列表
2. 为每个节点创建 ServerConn (包含连接池)
3. 存储到 serverConns 列表中
4. 定时更新服务列表 (每 3 秒)
5. getServerConnList() 直接返回 serverConns 列表

关键数据结构:

// OCTOAgentBaseCluster.java
public class OCTOAgentBaseCluster extends BaseCluster {
    // ⭐ 核心数据结构1: 存储所有可用的服务连接
    protected volatile List<ServerConn> serverConns = new ArrayList<ServerConn>();

    // ⭐ 核心数据结构2: IP:Port → ServerConn 的映射
    protected volatile Map<String, ServerConn> ipPortServerConnsMap =
        new ConcurrentHashMap<>();

    // ⭐ 核心数据结构3: Cell → Swimlane → Region → ServerConn 的索引
    private volatile Table<String, String, Map<String, List<ServerConn>>>
        serverConnListIndexTable;
}

7.2 完整初始化流程

【应用启动】
    ↓
【CustomThriftProxy.afterPropertiesSet()】
    ↓
【ThriftClientProxy.configClusterManager()】
    ↓
【创建 KrsOCTOAgentCluster】
new KrsOCTOAgentCluster(clientProxy)
    ↓
【调用父类构造函数】
super(clientProxy)  // OctoAgentCluster
    ↓
【调用父类构造函数】
super(clientProxy)  // OCTOAgentBaseCluster
    ↓
【第1步: 初始化定时任务】
scheduExec.scheduleWithFixedDelay(
    () -> getServerListByAgent(),  // 每3秒更新一次
    1, 3, TimeUnit.SECONDS
)
    ↓
【第2步: 从 MNS 获取服务列表】
getServerListByAgent() {
    List<SGService> sgServiceList = MnsInvoker.getServiceList(protocolRequest);
    //                              ↑
    //                    调用 MNS (服务发现) 获取服务节点
    initServerList(sgServiceList);
}
    ↓
【第3步: 初始化服务列表】
initServerList(sgServiceList) {
    for (SGService sgService : sgServiceList) {
        Server server = sgService2Server(sgService);
        addServer(server);  // 为每个节点创建连接
    }
    updateValidConns();  // 更新 serverConns 列表
}
    ↓
【第4步: 创建 ServerConn】
addServer(server) {
    ServerConn serverConn = new ServerConn();
    serverConn.setServer(server);

    // 创建连接池
    if (isNettyIO) {
        serverConn.setChannelPool(new NettyChannelPool(...));
    } else {
        serverConn.setObjectPool(createPool(...));
    }

    // 存储到 Map
    ipPortServerConnsMap.put(server.getIp() + ":" + server.getPort(), serverConn);
}
    ↓
【第5步: 更新可用连接列表】
updateValidConns() {
    List<ServerConn> _serverConns = new ArrayList<>();
    for (ServerConn conn : ipPortServerConnsMap.values()) {
        if (ALIVE == conn.getServer().getStatus()) {
            _serverConns.add(conn);
        }
    }
    serverConns = _serverConns;  // ⭐ 更新全局列表
}
    ↓
【完成初始化】
serverConns 列表已包含所有可用连接

7.3 服务发现机制 (MNS)

MNS 是企业内部的服务发现系统,类似于 Consul、Eureka。

┌─────────────────────────────────────────────────┐
│              MNS (服务注册中心)                  │
│                                                 │
│  服务列表:                                       │
│  - com.sankuai.krs.search                      │
│    - 10.1.1.1:9888 (MT机房, ALIVE)            │
│    - 10.1.1.2:9888 (MT机房, ALIVE)            │
│    - 10.2.1.1:9888 (ZF机房, ALIVE)            │
│    - 10.2.1.2:9888 (ZF机房, ALIVE)            │
│    - 10.3.1.1:9888 (SH机房, ALIVE)            │
└─────────────────────────────────────────────────┘
                    ↑
                    │ getServiceList(protocolRequest)
                    │
┌─────────────────────────────────────────────────┐
│         OCTOAgentBaseCluster (客户端)           │
│                                                 │
│  定时任务 (每3秒):                               │
│  1. 调用 MnsInvoker.getServiceList()           │
│  2. 获取最新的服务节点列表                       │
│  3. 更新本地 serverConns 列表                   │
└─────────────────────────────────────────────────┘

ProtocolRequest (请求参数):

ProtocolRequest protocolRequest = new ProtocolRequest();
protocolRequest.setRemoteAppkey("com.sankuai.krs.search");  // 远程服务的 AppKey
protocolRequest.setServiceName("KrsSearchThriftService");   // 服务接口名
protocolRequest.setProtocol("thrift");                      // 协议类型
protocolRequest.setLocalAppkey("com.sankuai.krs.client");  // 本地 AppKey

SGService (服务节点信息):

// MNS 返回的服务节点信息
public class SGService {
    private String ip;           // "10.1.1.1"
    private int port;            // 9888
    private int status;          // 2 (ALIVE)
    private double weight;       // 100.0
    private String cell;         // "default_cell"
    private String swimlane;     // "default_swimlane"
    private String az;           // "mt" (机房标识)
    private Map<String, ServiceDetail> serviceInfo;  // 服务详情
}

7.4 关键代码解析

7.4.1 从 MNS 获取服务列表
// OCTOAgentBaseCluster.java:331-351
private void getServerListByAgent(ProtocolRequest protocolRequest, boolean isInit) {
    List<SGService> sgServiceList = null;
    try {
        // ⭐ 调用 MNS (服务发现) 获取服务节点列表
        sgServiceList = MnsInvoker.getServiceList(protocolRequest);
        //                          ↑
        //    protocolRequest 包含:
        //    - remoteAppkey: "com.sankuai.krs.search"
        //    - serviceName: "KrsSearchThriftService"
        //    - protocol: "thrift"

        valid = validateServiceList(sgServiceList);
    } catch (Exception ex) {
        LOG.warn("getServerList by Agent Exception", ex);
        valid = false;
    }

    if (valid) {
        if (isInit) {
            initServerList(sgServiceList);  // 初始化
        } else {
            updateServerList(sgServiceList);  // 更新
        }
    }
}
7.4.2 初始化服务列表
// OCTOAgentBaseCluster.java:414-448
private void initServerList(List<SGService> sgServiceList) {
    Map<Server, Boolean> nettyServers = new HashMap<>();

    // ⭐ 遍历所有服务节点
    for (SGService sgService : sgServiceList) {
        // 过滤条件1: 端口匹配
        boolean filterPortNotMatch = remoteServerPort > 0 &&
                                     sgService.getPort() != remoteServerPort;

        // 过滤条件2: 服务名匹配
        Map<String, ServiceDetail> serviceDetailMap = sgService.getServiceInfo();
        boolean filterServiceNotMatch = filterByServiceName &&
                                        (serviceDetailMap == null ||
                                         !serviceDetailMap.containsKey(serviceName));

        // 过滤条件3: 状态必须是 ALIVE (2)
        if (ALIVE != sgService.getStatus() ||
            filterPortNotMatch ||
            filterServiceNotMatch) {
            continue;
        }

        // ⭐ 将 SGService 转换为 Server 对象
        Server server = sgService2Server(sgService, serviceName);

        // ⭐ 为每个节点创建连接
        if (isNettyIO && server.isNettyIOSupported()) {
            nettyServers.put(server, lazyInit);
        } else {
            addServer(server, true, lazyInit);
        }
    }

    // 批量初始化 Netty 连接
    for (Server server : nettyServers.keySet()) {
        addServer(server, true, nettyServers.get(server));
    }

    // ⭐ 更新可用连接列表
    updateValidConns();
    updateServerConnListIndexTable();
}
7.4.3 创建 ServerConn
// OCTOAgentBaseCluster.java:641-685
protected boolean addServer(Server server, boolean asyncInit, boolean lazyInit) {
    LOG.info("addServer:{}", server);

    // ⭐ 第1步: 创建 ServerConn 对象
    ServerConn serverConn = new ServerConn();
    serverConn.setCluster(this);
    serverConn.setServer(server);
    serverConn.setSwimlane(server.getSwimlane());
    serverConn.setCell(server.getCell());
    serverConn.setServerConnStatus(ServerConn.ServerConnStatus.AVAILABLE);
    serverConn.setConnPoolConf(poolConfig);

    // ⭐ 第2步: 创建连接池
    if (lazyInit) {
        // 延迟初始化
        serverConn.setLazyInitPool(true);
    } else {
        if (isNettyIO && server.isNettyIOSupported()) {
            // Netty 连接池
            serverConn.setChannelPool(
                new NettyChannelPool(serverConn, poolConfig, clientProxy, asyncInit)
            );
        } else {
            // Thrift 连接池
            serverConn.setObjectPool(
                createPool(server.getIp(), server.getPort(), timeOut, poolConfig, connTimeout)
            );
        }
    }

    // ⭐ 第3步: 存储到 Map
    String serverIpPort = server.getIp() + ":" + server.getPort();
    ServerConn oldServerConn = ipPortServerConnsMap.put(serverIpPort, serverConn);
    //                         ↑
    //    存储到全局 Map: "10.1.1.1:9888" → ServerConn

    if (oldServerConn != null) {
        destroyServerConn(oldServerConn, true);
    }

    return true;
}
7.4.4 更新可用连接列表
// OCTOAgentBaseCluster.java:575-588
protected void updateValidConns() {
    List<ServerConn> _serverConns = new ArrayList<ServerConn>();

    // ⭐ 从 Map 中筛选出所有可用的连接
    for (ServerConn conn : ipPortServerConnsMap.values()) {
        if (ALIVE == conn.getServer().getStatus()) {
            _serverConns.add(conn);
        }
    }

    // ⭐ 更新全局列表 (volatile 保证可见性)
    serverConns = _serverConns;
    //  ↑
    // 这就是 getServerConnList() 返回的列表!
}

7.5 动态更新机制

定时更新流程:

【定时任务 (每3秒)】
    ↓
getServerListByAgent(protocolRequest, false)
    ↓
【从 MNS 获取最新服务列表】
List<SGService> sgServiceList = MnsInvoker.getServiceList(protocolRequest);
    ↓
【更新服务列表】
updateServerList(sgServiceList) {
    // 1. 处理下线节点
    handleServerRemoved(ipHostSGServiceMap);

    // 2. 处理新增节点
    handleServerAdded(ipHostSGServiceMap);

    // 3. 处理节点属性变更
    handleServerAttributesChanged(ipHostSGServiceMap);

    // 4. 更新可用连接列表
    updateValidConns();
}

处理下线节点:

// OCTOAgentBaseCluster.java:477-495
private boolean handleServerRemoved(Map<String, SGService> ipHostSGServiceMap) {
    boolean changed = false;
    Set<String> ipHostSet = ipHostSGServiceMap.keySet();

    // ⭐ 遍历本地所有连接
    for (Iterator<String> it = ipPortServerConnsMap.keySet().iterator(); it.hasNext(); ) {
        String serverIpPort = it.next();

        // ⭐ 如果 MNS 返回的列表中没有这个节点,说明已下线
        if (!ipHostSet.contains(serverIpPort)) {
            ServerConn serverConn = ipPortServerConnsMap.get(serverIpPort);
            it.remove();  // 从 Map 中移除
            changed = true;

            if (serverConn != null) {
                LOG.info("removeServer:{}", serverIpPort);
                destroyServerConn(serverConn, true);  // 销毁连接池
            }
        }
    }
    return changed;
}

7.6 getServerConnList() 的实现

// KrsOCTOAgentCluster.java:71-80
@Override
public List<ServerConn> getServerConnList(RouterMetaData routerMetaData) {
    if (getServersWithoutRegion) {
        // ⭐ 模式1: 不考虑 Region,直接过滤
        List<ServerConn> serversWithoutRegion =
            filterConnsByDegradedAndIdc(serverConns);
        //                              ↑
        //                    直接使用 serverConns 列表!
        //                    这个列表是在应用启动时从 MNS 获取并初始化的

        if (CollectionUtil.isEmpty(serversWithoutRegion)) {
            serversWithoutRegion.addAll(serverConns);
        }
        return serversWithoutRegion;
    }

    // ⭐ 模式2: 考虑 Region,使用索引表
    return getServerConnListByCellRoute(routerMetaData);
}

7.7 完整调用链

【业务代码】
client.search(request, callback);
    ↓
【CGLIB 拦截】
MTThriftMethodInterceptor.intercept()
    ↓
【获取所有连接】
List<ServerConn> connList = cluster.getServerConnList(routerMetaData);
//                          ↑
//                    返回 serverConns 列表 (从 MNS 获取并缓存)
//                    serverConns = [
//                      ServerConn(10.1.1.1:9888, az="mt"),
//                      ServerConn(10.1.1.2:9888, az="mt"),
//                      ServerConn(10.2.1.1:9888, az="zf"),
//                      ServerConn(10.2.1.2:9888, az="zf")
//                    ]
    ↓
【按 IDC 过滤】
List<ServerConn> filteredConns = filterConnsByDegradedAndIdc(connList);
//                                ↑
//                    根据 krsRouteService.getReadRouteIdcName() 过滤
//                    假设返回 "mt", 则过滤后:
//                    filteredConns = [
//                      ServerConn(10.1.1.1:9888, az="mt"),
//                      ServerConn(10.1.1.2:9888, az="mt")
//                    ]
    ↓
【负载均衡】
ServerConn selected = loadBalancer.select(filteredConns);
//                    ↑
//                    选择: ServerConn(10.1.1.1:9888, az="mt")
    ↓
【获取连接】
Object socket = getConnection(selected);
//              ↑
//        从连接池中获取一个可用连接
    ↓
【发送请求】
getRpcResult(selected, socket);

7.8 核心要点总结

  1. serverConns 是全局共享的

    • 所有 RPC 调用都使用同一个 serverConns 列表
    • 通过 volatile 保证可见性
  2. 连接池是预先创建的

    • 应用启动时就创建好所有连接池
    • 不是每次调用时才创建
  3. 动态更新机制

    • 定时任务每3秒从 MNS 获取最新服务列表
    • 自动处理节点上下线和属性变更
  4. 路由过滤在 getServerConnList() 之后

    • getServerConnList() 返回所有可用连接
    • filterConnsByDegradedAndIdc() 按 IDC 过滤
    • loadBalancer.select() 负载均衡选择一个

8. 实战案例

8.1 灰度发布新集群

配置示例
// 配置中心: appkey.route.beijing
{
  "mt_old": 90,   // 老集群 90% 流量
  "mt_new": 10    // 新集群 10% 流量(灰度)
}
效果
100 个请求:
- 约 90 个 → MT_OLD 集群
- 约 10 个 → MT_NEW 集群
灰度步骤
// 第1步: 灰度 10%
{"mt_old": 90, "mt_new": 10}

// 第2步: 灰度 30%
{"mt_old": 70, "mt_new": 30}

// 第3步: 灰度 50%
{"mt_old": 50, "mt_new": 50}

// 第4步: 全量切换
{"mt_old": 0, "mt_new": 100}

8.2 集群故障切换

配置示例
# 正常情况
appkey.read.mt = mt

# MT 集群故障,切换到 ZF 集群
appkey.read.mt = zf

# 故障恢复,切回 MT 集群
appkey.read.mt = mt
效果
  • ✅ 配置热更新,无需重启应用
  • ✅ 秒级切换,快速止损
  • ✅ 自动恢复,无需人工干预

9. 常见问题

Q1: 为什么 Thrift 性能更好?

A: 主要有以下几个原因:

1. 二进制协议 vs JSON 文本协议

HTTP/JSON 方式 (文本协议):

// 发送一个搜索请求
{
  "query": "美食",
  "from": 0,
  "size": 10,
  "filters": {
    "city": "北京",
    "price": 50
  }
}
  • 数据大小: 约 100 字节
  • 需要解析: 字符串 → JSON 对象
  • 可读性: ✅ 人类可读
  • 效率: ❌ 占用空间大,解析慢

Thrift/Binary 方式 (二进制协议):

// 同样的数据,二进制表示 (用十六进制显示)
[0x01, 0x00, 0x01, 0x0B, 0x00, 0x00, 0x00, 0x06,
 0xE7, 0xBE, 0x8E, 0xE9, 0xA3, 0x9F, 0x08, 0x00, ...]
  • 数据大小: 约 40 字节 (减少 60%)
  • 需要解析: 二进制 → 对象 (直接映射)
  • 可读性: ❌ 人类不可读
  • 效率: ✅ 占用空间小,解析快

🔍 你说得对!这里用的是十六进制表示法:

  • 0x 前缀表示这是十六进制数
  • 每个十六进制数代表 4 位二进制
  • 两个十六进制数 = 1 字节 = 8 位二进制

详细转换过程见下方 👇

类比:

  • JSON 就像写信,要写"我今年25岁",需要7个汉字
  • Binary 就像发电报,只需要发"25",2个数字就够了

🔢 补充: 二进制、十六进制、数据转换详解
1. 为什么用十六进制显示?

二进制太长,不方便阅读:

二进制: 11100111 10111110 10001110  (太长!)
十六进制: E7       BE       8E        (简洁!)

十六进制是二进制的简写:

  • 1个十六进制位 = 4个二进制位
  • 2个十六进制位 = 1个字节 = 8个二进制位
2. 进制转换对照表
十进制二进制十六进制说明
000000
100011
200102
300113
401004
501015
601106
701117
810008
910019
101010A十六进制用A表示10
111011B
121100C
131101D
141110E
151111F十六进制用F表示15
3. 实际转换示例: "美食" 两个字

第1步: 文本 → UTF-8 编码

"美" → UTF-8: E7 BE 8E (3个字节)
"食" → UTF-8: E9 A3 9F (3个字节)

第2步: 十六进制 → 二进制

E7 = 1110 0111
BE = 1011 1110
8E = 1000 1110

E9 = 1110 1001
A3 = 1010 0011
9F = 1001 1111

第3步: 在网络上传输的实际数据

实际传输的是二进制:
11100111 10111110 10001110 11101001 10100011 10011111

但我们用十六进制显示:
E7 BE 8E E9 A3 9F

因为十六进制更简洁易读!
4. 完整的 Thrift 数据包结构

假设我们要发送: {"query": "美食", "size": 10}

JSON 方式 (文本):

{"query":"美食","size":10}
  • 实际字节: 7B 22 71 75 65 72 79 22 3A 22 E7 BE 8E E9 A3 9F 22 2C 22 73 69 7A 65 22 3A 31 30 7D
  • 字节数: 29 字节
  • 说明: 每个字符都要编码,包括 {":

Thrift Binary 方式 (二进制):

字段1: query (字符串类型)
  0x0B          → 字段类型: String (1字节)
  0x00 0x01     → 字段ID: 1 (2字节)
  0x00 0x06     → 字符串长度: 6字节 (2字节)
  0xE7 0xBE 0x8E 0xE9 0xA3 0x9F  → "美食" (6字节)

字段2: size (整数类型)
  0x08          → 字段类型: i32 (1字节)
  0x00 0x02     → 字段ID: 2 (2字节)
  0x00 0x00 0x00 0x0A  → 数值: 10 (4字节)

结束标记:
  0x00          → 字段结束 (1字节)
  • 总字节数: 1+2+2+6 + 1+2+4 + 1 = 19 字节
  • 节省: (29-19)/29 = 34.5%
5. 为什么 Thrift 更小?

JSON 的开销:

{"query":"美食","size":10}
 ↑      ↑     ↑      ↑
 这些符号都要占用字节!
  • { } : , " 这些符号都要传输
  • 字段名 "query"、"size" 也要传输
  • 数字 10 要转成字符串 "10"

Thrift 的优化:

0x0B 0x00 0x01 0x00 0x06 ...
 ↑    ↑    ↑    ↑    ↑
类型  字段ID  长度  数据
  • 不需要传输字段名 (用字段ID代替)
  • 不需要符号 (用类型标记代替)
  • 数字直接用二进制表示 (不转字符串)
6. 实际代码示例

Java 中如何处理二进制数据:

// ========== JSON 方式 ==========
String json = "{"query":"美食","size":10}";
byte[] jsonBytes = json.getBytes("UTF-8");
System.out.println("JSON 字节数: " + jsonBytes.length);  // 29

// 查看十六进制
for (byte b : jsonBytes) {
    System.out.printf("%02X ", b);
}
// 输出: 7B 22 71 75 65 72 79 22 3A 22 E7 BE 8E E9 A3 9F 22 2C 22 73 69 7A 65 22 3A 31 30 7D

// ========== Thrift Binary 方式 ==========
// 手动构造 Thrift 二进制数据
ByteBuffer buffer = ByteBuffer.allocate(100);

// 字段1: query (String)
buffer.put((byte) 0x0B);           // 类型: String
buffer.putShort((short) 1);        // 字段ID: 1
buffer.putShort((short) 6);        // 长度: 6
buffer.put("美食".getBytes("UTF-8")); // 数据

// 字段2: size (i32)
buffer.put((byte) 0x08);           // 类型: i32
buffer.putShort((short) 2);        // 字段ID: 2
buffer.putInt(10);                 // 数值: 10

// 结束标记
buffer.put((byte) 0x00);

byte[] thriftBytes = new byte[buffer.position()];
buffer.flip();
buffer.get(thriftBytes);

System.out.println("Thrift 字节数: " + thriftBytes.length);  // 19

// 查看十六进制
for (byte b : thriftBytes) {
    System.out.printf("%02X ", b);
}
// 输出: 0B 00 01 00 06 E7 BE 8E E9 A3 9F 08 00 02 00 00 00 0A 00
7. 十六进制 vs 二进制 vs 十进制对比

以数字 255 为例:

进制表示说明
十进制255人类习惯的表示
二进制11111111计算机实际存储 (8位)
十六进制0xFF简洁的表示 (2位)

为什么程序员喜欢用十六进制?

  • ✅ 比二进制短 (8位 → 2位)
  • ✅ 和二进制转换简单 (每4位对应1位)
  • ✅ 方便查看内存数据
8. 形象类比

十进制 (Decimal):

  • 就像我们平时数数: 1, 2, 3, ..., 9, 10, 11, ...
  • 逢十进一

二进制 (Binary):

  • 就像开关: 开(1) 或 关(0)
  • 逢二进一
  • 计算机只认识这个!

十六进制 (Hexadecimal):

  • 就像二进制的"简写"
  • 0-9 用数字, 10-15 用 A-F
  • 逢十六进一
  • 程序员用来"翻译"二进制

类比:

二进制 = 摩斯密码 (点点划划,太长)
十六进制 = 简写版摩斯密码 (更短,更易读)
十进制 = 普通文字 (人类习惯)
9. 总结
特性JSON (文本)Thrift (二进制)
实际存储文本字符二进制数据
显示方式直接显示用十六进制显示
字段名完整传输用ID代替
符号需要传输不需要
数字转成字符串直接二进制
大小小 (节省30-60%)

关键点:

  • 💡 二进制是计算机实际存储的方式
  • 💡 十六进制是人类查看二进制的简便方式
  • 💡 0x 前缀表示这是十六进制数
  • 💡 Thrift 传输的是二进制,但我们用十六进制显示

2. 数据压缩: Snappy/Gzip

未压缩的 JSON:

{
  "results": [
    {"id": 1, "name": "火锅店", "score": 4.5},
    {"id": 2, "name": "烧烤店", "score": 4.3},
    {"id": 3, "name": "川菜馆", "score": 4.8},
    ... // 重复 100 次
  ]
}
  • 数据大小: 约 5000 字节

Snappy 压缩后:

[压缩后的二进制数据]
  • 数据大小: 约 1000 字节 (减少 80%)
  • 压缩时间: < 1ms (非常快)
  • 解压时间: < 1ms

类比:

  • 未压缩就像把100个苹果一个个装箱
  • 压缩就像把苹果榨成汁,体积小了很多

3. 长连接 vs 短连接

HTTP 短连接 (每次请求都要建立连接):

第1次请求:
1. TCP 三次握手 (1.5ms)
2. 发送请求 (2ms)
3. 接收响应 (2ms)
4. TCP 四次挥手 (1.5ms)
总耗时: 7ms

第2次请求:
1. TCP 三次握手 (1.5ms)  ← 又要握手!
2. 发送请求 (2ms)
3. 接收响应 (2ms)
4. TCP 四次挥手 (1.5ms)
总耗时: 7ms

100次请求总耗时: 700ms

Thrift 长连接 (连接复用):

第1次请求:
1. TCP 三次握手 (1.5ms)  ← 只握手一次!
2. 发送请求 (2ms)
3. 接收响应 (2ms)
总耗时: 5.5ms

第2次请求:
1. 发送请求 (2ms)  ← 直接发送,不用握手!
2. 接收响应 (2ms)
总耗时: 4ms

第3-100次请求:
每次只需要 4ms

100次请求总耗时: 5.5 + 99*4 = 401.5ms

节省时间: 700ms - 401.5ms = 298.5ms (节省 43%)

类比:

  • HTTP 短连接就像每次打电话都要重新拨号
  • Thrift 长连接就像保持通话,一直聊下去

4. 解析速度对比

JSON 解析 (文本 → 对象):

// HTTP/JSON 方式
String json = "{"name":"张三","age":25}";

// 第1步: 词法分析 (找到 {、"、:、} 等符号)
// 第2步: 语法分析 (构建 JSON 树)
// 第3步: 类型转换 ("25" → 25)
// 第4步: 创建对象

User user = JSON.parseObject(json, User.class);
// 耗时: 约 100 微秒

Binary 解析 (二进制 → 对象):

// Thrift/Binary 方式
byte[] bytes = [0x01, 0x00, 0x01, 0x0B, ...];

// 第1步: 直接读取字段类型 (0x01 = String)
// 第2步: 直接读取字段值 (0x06 = 长度6, 后面6字节是数据)
// 第3步: 直接映射到对象

User user = ThriftCodec.decode(bytes, User.class);
// 耗时: 约 20 微秒

速度提升: 100 / 20 = 5 倍

类比:

  • JSON 解析就像读一篇文章,要理解每个字的意思
  • Binary 解析就像看图表,一眼就能看懂

5. 完整性能对比实例

假设我们要搜索 100 次,每次返回 10 条结果:

HTTP/JSON 方式:

单次请求:
1. 建立连接: 1.5ms (TCP 握手)
2. 发送请求: 2ms (JSON 100字节)
3. 接收响应: 5ms (JSON 5000字节)
4. 解析响应: 0.1ms (JSON 解析)
5. 关闭连接: 1.5ms (TCP 挥手)
单次总耗时: 10.1ms

100次请求总耗时: 10.1 * 100 = 1010ms

Thrift/Binary 方式:

第1次请求:
1. 建立连接: 1.5ms (TCP 握手,只一次)
2. 发送请求: 0.5ms (Binary 40字节)
3. 接收响应: 1ms (Binary 1000字节,Snappy压缩)
4. 解压响应: 0.5ms (Snappy 解压)
5. 解析响应: 0.02ms (Binary 解析)
第1次总耗时: 3.52ms

第2-100次请求 (复用连接):
1. 发送请求: 0.5ms
2. 接收响应: 1ms
3. 解压响应: 0.5ms
4. 解析响应: 0.02ms
每次总耗时: 2.02ms

100次请求总耗时: 3.52 + 99*2.02 = 203.5ms

性能对比:

  • HTTP/JSON: 1010ms
  • Thrift/Binary: 203.5ms
  • 速度提升: 1010 / 203.5 = 4.96 倍5 倍!

6. 真实场景举例

场景: 电商搜索,每秒 10000 次请求

HTTP/JSON 方式:

每次请求耗时: 10ms
需要的服务器数量: 10000 * 10ms / 1000ms = 100 台

每月服务器成本: 100台 * 5000元/台 = 50万元

Thrift/Binary 方式:

每次请求耗时: 2ms
需要的服务器数量: 10000 * 2ms / 1000ms = 20 台

每月服务器成本: 20台 * 5000元/台 = 10万元

节省成本: 50万 - 10万 = 40万元/月 = 480万元/年!


7. 为什么不都用 Thrift?

Thrift 的缺点:

  • ❌ 不易调试 (二进制数据人类不可读)
  • ❌ 需要定义 IDL 文件 (接口定义语言)
  • ❌ 跨语言支持有限 (需要生成代码)
  • ❌ 学习成本高

HTTP/JSON 的优点:

  • ✅ 易于调试 (可以直接看懂)
  • ✅ 通用性强 (浏览器、Postman 都能用)
  • ✅ 跨语言简单 (任何语言都支持 JSON)
  • ✅ 学习成本低

选择建议:

使用 HTTP/JSON:
- 对外 API (给第三方调用)
- 管理后台 (调用量小)
- 快速开发 (原型验证)

使用 Thrift/Binary:
- 内部服务 (微服务之间)
- 高并发场景 (QPS > 10000)
- 性能敏感 (延迟 < 10ms)

8. 总结对比表
维度HTTP/JSONThrift/Binary提升倍数
数据大小100 字节40 字节2.5x
压缩后100 字节10 字节10x
解析速度100 微秒20 微秒5x
连接开销每次 3ms首次 3msN倍
单次请求10ms2ms5x
100次请求1010ms203.5ms5x
服务器成本100台20台5x

综合性能提升: 2-5 倍 (取决于场景)


9. 形象类比总结

HTTP/JSON 就像:

  • 📝 写信: 要写很多字,邮递员要看懂
  • 🚗 开车: 每次都要从家里出发
  • 📚 读文章: 要理解每个字的意思

Thrift/Binary 就像:

  • 📟 发电报: 只发关键信息,用代码表示
  • 🚄 高铁: 一次出发,中途不停
  • 📊 看图表: 一眼就能看懂数据

结论: Thrift 就像是给数据"瘦身"+"加速",让它跑得更快!

Q2: 业务代码如何使用?

A: 业务代码无需关心路由逻辑,正常调用即可:

@Service
public class SearchService {
    @Autowired
    private SearchManagerProxy searchManagerProxy;

    public SearchResponse search(String keyword) {
        // ⭐ 获取客户端(返回固定的 thriftWrapperClient)
        EsClient client = searchManagerProxy.getClient();

        // 调用搜索(框架层自动路由)
        return client.search(request, callback);
    }
}

Q3: 如何监控路由是否生效?

A: 可以通过以下方式监控:

// 1. 查看日志
LOGGER.info("THRIFT NEW ROUTE TABLE CHANGED, appKey: {}, routeTable {}",
            remoteAppKey, routeTable);

// 2. 添加监控埋点
Cat.logEvent("ES.Route", idc);  // 记录每次路由到哪个 IDC

// 3. 查看 CAT 监控
// 在 CAT 上查看 ES.Route 事件的分布
// 应该看到 50% mt, 50% zf

Q4: 路由配置错误会怎样?

A: 会抛出异常,应用启动失败:

// 错误配置1: 权重总和不是 100
{"mt": 50, "zf": 40}  // 总和 90
// 异常: Invalid route info.

// 错误配置2: IDC 不在 multi 中
{"mt": 50, "unknown": 50}
// 异常: invalid routeInfo config, not in multi

// 错误配置3: IDC 不在同一个 Region
{"mt": 50, "sh": 50}  // mt 在 beijing, sh 在 shanghai
// 异常: invalid routeInfo config, not in this region

10. KrsDirectlyCluster 直连模式详解

本章节详细解析 KrsDirectlyClusterKrsOCTOAgentCluster 两种集群管理器在整个 RPC 调用链路中的作用。

10.1 两种 Cluster 的对比

维度KrsOCTOAgentClusterKrsDirectlyCluster
服务发现方式MNS (服务发现)配置中心 (Lion)
节点列表来源动态从 MNS 获取从配置中心读取 IP 列表
更新频率每 3 秒每 240 秒 (4分钟)
适用场景标准服务调用直连特定节点
配置复杂度低 (自动发现)高 (需手动配置 IP)
灵活性高 (自动感知节点变化)低 (需手动更新配置)

10.2 完整的 RPC 调用链路

阶段1: 应用启动阶段
【Spring 容器启动】
    ↓
@Bean
public CustomThriftProxy customThriftProxy() {
    return new CustomThriftProxy();
}
    ↓
【Spring 调用 afterPropertiesSet()】
CustomThriftProxy.afterPropertiesSet()
阶段2: 初始化阶段 (Cluster 的核心作用)

2.1 创建 Cluster

// CustomThriftProxy.java
@Override
public void afterPropertiesSet() throws Exception {
    // ... 省略前面的步骤 ...

    // ⭐ 第4阶段: 创建集群管理器
    configClusterManager();  // 这里创建 Cluster!
}

private void configClusterManager() throws Exception {
    if (直连模式) {
        // ⭐ 创建 KrsDirectlyCluster
        this.updateSuperFieldByReflect("cluster",
            new KrsDirectlyCluster(this));
    } else {
        // ⭐ 创建 KrsOCTOAgentCluster
        this.updateSuperFieldByReflect("cluster",
            new KrsOCTOAgentCluster(this));
    }
}

2.2 KrsDirectlyCluster 初始化

// KrsDirectlyCluster.java:42-64
public KrsDirectlyCluster(CustomThriftProxy clientProxy) throws Exception {
    // ⭐ 环节1: 服务发现
    List<String> sameRegionIpList = getSameRegionIpList();
    // 从配置中心获取 IP 列表
    // 返回: ["10.1.1.1", "10.1.1.2", "10.2.1.1"]

    // ⭐ 环节2: 建立连接
    this.serverConns = buildServerConnList(sameRegionIpList);
    // 为每个 IP 创建连接池
    // serverConns = [
    //   ServerConn(10.1.1.1:9888, pool=NettyChannelPool),
    //   ServerConn(10.1.1.2:9888, pool=NettyChannelPool),
    //   ServerConn(10.2.1.1:9888, pool=NettyChannelPool)
    // ]

    // ⭐ 环节3: 初始化路由服务
    this.krsRouteService = clientProxy.getKrsRouteService();

    // ⭐ 环节4: 启动定时任务
    startScheduledTask();
    // 每 240 秒更新一次节点列表
}

2.3 KrsOCTOAgentCluster 初始化

// KrsOCTOAgentCluster.java:20-24
public KrsOCTOAgentCluster(CustomThriftProxy clientProxy) throws Exception {
    // ⭐ 调用父类构造函数
    super(clientProxy);  // OctoAgentCluster

    // ⭐ 初始化路由服务
    this.krsRouteService = clientProxy.getKrsRouteService();
}

// 父类 OCTOAgentBaseCluster 的构造函数
public OCTOAgentBaseCluster(ThriftClientProxy clientProxy) {
    // ⭐ 环节1: 启动定时任务
    scheduExec.scheduleWithFixedDelay(
        () -> getServerListByAgent(),  // 每 3 秒更新一次
        1, 3, TimeUnit.SECONDS
    );

    // ⭐ 环节2: 首次获取服务列表
    getServerListByAgent(protocolRequest, true);
    // 从 MNS 获取节点列表

    // ⭐ 环节3: 初始化服务列表
    initServerList(sgServiceList);
    // 为每个节点创建连接池
}
阶段3: 运行时阶段 (Cluster 的核心作用)

3.1 业务代码发起 RPC 调用

// 业务代码
EsClient client = searchManagerProxy.getClient();
SearchResponse response = client.search(request, callback);

3.2 完整调用链路

【第1步: CGLIB 拦截】
MTThriftMethodInterceptor.intercept()
    ↓
【第2步: 获取可用节点列表】⭐ Cluster 的第1个核心作用
List<ServerConn> connList = cluster.getServerConnList(routerMetaData);
    ↓
    ┌─────────────────────────────────────────┐
    │ KrsDirectlyCluster.getServerConnList()  │
    │ 或                                       │
    │ KrsOCTOAgentCluster.getServerConnList() │
    └─────────────────────────────────────────┘
    ↓
【第3步: 路由过滤】⭐ Cluster 的第2个核心作用
filterConnsByDegradedAndIdc(connList)
    ↓
    // 1. 过滤降级节点
    // 2. 按 IDC 过滤节点
    // 3. 返回过滤后的节点列表
    ↓
【第4步: 负载均衡】
ServerConn selected = loadBalancer.select(connList);
    ↓
【第5步: 获取连接】⭐ Cluster 的第3个核心作用
Object socket = getConnection(selected);
    ↓
    // 从连接池中获取一个可用连接
    // 如果是 Netty: channelPool.acquire()
    // 如果是 Thrift: objectPool.borrowObject()
    ↓
【第6步: 发送 RPC 请求】
getRpcResult(selected, socket);
    ↓
【第7步: 返回结果】

10.3 Cluster 在各环节的详细作用

环节1: 获取可用节点列表

KrsDirectlyCluster

// KrsDirectlyCluster.java:140-164
@Override
public List<ServerConn> getServerConnList() {
    // ⭐ 作用1: 过滤降级节点
    List<ServerConn> serverConnExcludeDegradeNodeList =
        super.filterServerConnListByDegradedWeight(this.serverConns);

    // ⭐ 作用2: 按 IDC 路由过滤
    String _routeIdcName = this.krsRouteService.getReadRouteIdcName();
    // 从路由配置中获取目标 IDC: "mt" 或 "zf"

    List<ServerConn> validConnList = new ArrayList<>();
    for (ServerConn serverConn : serverConnExcludeDegradeNodeList) {
        if (_routeIdcName.equalsIgnoreCase(serverConn.getServer().getAz())) {
            validConnList.add(serverConn);
        }
    }

    return validConnList;
}

输入:

  • this.serverConns: 所有节点 (3个)
  • _routeIdcName: "mt"

输出:

[
  ServerConn(10.1.1.1:9888, az="mt"),
  ServerConn(10.1.1.2:9888, az="mt")
]

KrsOCTOAgentCluster

// KrsOCTOAgentCluster.java:71-80
@Override
public List<ServerConn> getServerConnList(RouterMetaData routerMetaData) {
    if (getServersWithoutRegion) {
        // ⭐ 作用1: 过滤降级节点
        // ⭐ 作用2: 按 IDC 路由过滤
        List<ServerConn> serversWithoutRegion =
            filterConnsByDegradedAndIdc(serverConns);

        return serversWithoutRegion;
    }

    // ⭐ 作用3: 按 Region 路由过滤
    return getServerConnListByCellRoute(routerMetaData);
}
环节2: 路由过滤
// KrsOCTOAgentCluster.java:26-43
private List<ServerConn> filterConnsByDegradedAndIdc(List<ServerConn> connList) {
    // ⭐ 第1步: 过滤降级节点
    List<ServerConn> serverConnList =
        super.filterServerConnListByDegradedWeight(connList);

    // ⭐ 第2步: 获取路由目标 IDC
    String routeIdcName = this.krsRouteService.getReadRouteIdcName();
    // 调用 routeTable.getRouteIdc()
    // 按比例随机: 50% → "mt", 50% → "zf"

    // ⭐ 第3步: 按 IDC 过滤节点
    List<ServerConn> validConnList = new ArrayList<ServerConn>();
    for (ServerConn conn : serverConnList) {
        if (routeIdcName.equalsIgnoreCase(conn.getServer().getAz())) {
            validConnList.add(conn);
        }
    }

    return validConnList;
}

作用:

  • 从 5 个节点 → 过滤到 2 个节点
  • 实现了机房级别的路由
环节3: 提供连接池
// MTThriftMethodInterceptor.java
private Object doInvokeWithRetry(InvokerContext invokerContext) {
    // ⭐ 第1步: 负载均衡选择一个节点
    ServerConn serverConn = loadBalancer.select(connList);

    // ⭐ 第2步: 从连接池获取连接
    Object socket = getConnection(invokerContext, serverConn);
    //              ↑
    //        这里会调用 serverConn.getChannelPool().acquire()
    //        或 serverConn.getObjectPool().borrowObject()

    // ⭐ 第3步: 发送 RPC 请求
    Entry<Object, Throwable> rpcResult =
        getRpcResult(invokerContext, serverConn, socket);

    return rpcResult.getT1();
}

Cluster 的作用:

  • 在初始化时创建了连接池
  • 在运行时提供连接池的引用

10.4 后台定时任务 (Cluster 的持续作用)

KrsDirectlyCluster 的定时任务
// KrsDirectlyCluster.java:202-213
private void startScheduledTask() {
    serverListPollingTask = scheduExec.scheduleAtFixedRate(
        () -> directUpdate(),  // ⭐ 每 240 秒执行一次
        60, 240, TimeUnit.SECONDS
    );
}

private void directUpdate() throws Exception {
    // ⭐ 环节1: 从配置中心获取最新 IP 列表
    List<String> clusterLocalRegionIps = getSameRegionIpList();

    // ⭐ 环节2: 检测下线节点
    Set<String> needRemoved = handleServerRemoved(clusterLocalRegionIps);

    // ⭐ 环节3: 检测新增节点
    Set<String> needAdded = handleServerAdded(clusterLocalRegionIps);

    // ⭐ 环节4: 更新 serverConns 列表
    // 移除下线节点,添加新增节点

    // ⭐ 环节5: 销毁下线节点的连接池
    for (ServerConn serverConn : needDestroy) {
        destroyServerConn(serverConn, true);
    }
}
KrsOCTOAgentCluster 的定时任务
// OCTOAgentBaseCluster.java
scheduExec.scheduleWithFixedDelay(
    () -> getServerListByAgent(),  // ⭐ 每 3 秒执行一次
    1, 3, TimeUnit.SECONDS
);

private void getServerListByAgent() {
    // ⭐ 环节1: 从 MNS 获取最新服务列表
    List<SGService> sgServiceList = MnsInvoker.getServiceList(protocolRequest);

    // ⭐ 环节2: 更新服务列表
    updateServerList(sgServiceList);

    // ⭐ 环节3: 处理节点上下线
    handleServerRemoved(ipHostSGServiceMap);
    handleServerAdded(ipHostSGServiceMap);

    // ⭐ 环节4: 更新 serverConns 列表
    updateValidConns();
}

10.5 销毁阶段

// KrsDirectlyCluster.java:167-185
@Override
public void destroy() {
    // ⭐ 环节1: 取消定时任务
    if (null != serverListPollingTask) {
        serverListPollingTask.cancel(true);
    }

    // ⭐ 环节2: 销毁所有连接池
    for (ServerConn serverConn : serverConns) {
        destroyServerConn(serverConn, false);
    }

    // ⭐ 环节3: 清空列表
    serverConns.clear();
}

10.6 完整流程图

┌─────────────────────────────────────────────────────────────┐
│                    【应用启动阶段】                          │
│                                                             │
│  Spring 容器启动 → CustomThriftProxy.afterPropertiesSet()  │
│                           ↓                                 │
│                  configClusterManager()                     │
│                           ↓                                 │
│         ┌─────────────────┴─────────────────┐              │
│         ↓                                   ↓              │
│  KrsDirectlyCluster              KrsOCTOAgentCluster       │
│  - 从配置中心获取 IP              - 从 MNS 获取节点         │
│  - 创建连接池                     - 创建连接池              │
│  - 启动定时任务 (240秒)           - 启动定时任务 (3秒)      │
└─────────────────────────────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────────────┐
│                    【运行时阶段】                            │
│                                                             │
│  业务代码: client.search(request, callback)                 │
│                           ↓                                 │
│  【CGLIB 拦截】MTThriftMethodInterceptor.intercept()        │
│                           ↓                                 │
│  【获取节点】cluster.getServerConnList() ⭐ Cluster 作用1    │
│                           ↓                                 │
│  【路由过滤】filterConnsByDegradedAndIdc() ⭐ Cluster 作用2  │
│                           ↓                                 │
│  【负载均衡】loadBalancer.select(connList)                  │
│                           ↓                                 │
│  【获取连接】getConnection(serverConn) ⭐ Cluster 作用3      │
│                           ↓                                 │
│  【发送请求】getRpcResult(serverConn, socket)               │
│                           ↓                                 │
│  【返回结果】                                                │
└─────────────────────────────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────────────┐
│                  【后台定时任务】                            │
│                                                             │
│  KrsDirectlyCluster (每 240 秒):                            │
│  - 从配置中心获取最新 IP 列表                                │
│  - 检测节点上下线                                            │
│  - 更新 serverConns 列表 ⭐ Cluster 作用4                    │
│  - 销毁下线节点的连接池                                      │
│                                                             │
│  KrsOCTOAgentCluster (每 3 秒):                             │
│  - 从 MNS 获取最新服务列表                                   │
│  - 检测节点上下线                                            │
│  - 更新 serverConns 列表 ⭐ Cluster 作用4                    │
│  - 销毁下线节点的连接池                                      │
└─────────────────────────────────────────────────────────────┘
                           ↓
┌─────────────────────────────────────────────────────────────┐
│                    【销毁阶段】                              │
│                                                             │
│  Spring 容器关闭 → cluster.destroy()                        │
│  - 取消定时任务                                              │
│  - 销毁所有连接池 ⭐ Cluster 作用5                           │
│  - 清空列表                                                  │
└─────────────────────────────────────────────────────────────┘

10.7 Cluster 的核心作用总结

阶段环节Cluster 的作用重要性
初始化服务发现获取节点列表 (配置中心/MNS)⭐⭐⭐⭐⭐
初始化建立连接为每个节点创建连接池⭐⭐⭐⭐⭐
运行时获取节点返回可用节点列表⭐⭐⭐⭐⭐
运行时路由过滤按 IDC/Region 过滤节点⭐⭐⭐⭐⭐
运行时提供连接提供连接池引用⭐⭐⭐⭐⭐
后台动态更新定时更新节点列表⭐⭐⭐⭐
后台节点管理处理节点上下线⭐⭐⭐⭐
销毁资源释放销毁连接池,释放资源⭐⭐⭐

结论: Cluster 不是"边缘环节",而是整个 Thrift RPC 框架的核心组件,贯穿了应用的整个生命周期! 🎯

10.8 实战案例: 直连模式配置

配置示例
# Lion 配置中心
# 配置 key: com.sankuai.krs.search.cluster.mt
# 配置 value: 10.1.1.1,10.1.1.2,10.1.1.3

# 应用配置
krs.search.direct.mode=true
krs.search.remote.port=9888
使用场景
  1. 测试环境: 直连特定测试节点
  2. 灰度验证: 直连新版本节点进行验证
  3. 故障排查: 直连特定节点进行问题定位
  4. 性能测试: 直连高性能节点进行压测

📝 总结

🔑 关键概念速查表

概念定义核心作用
Thrift 路由框架层自动选择目标机房和节点的机制实现多机房流量分配
CustomThriftProxy自定义 Thrift 代理类注入路由服务和集群管理器
KrsRouteService路由决策服务从配置中心读取路由规则,按比例分流
KrsOCTOAgentCluster集群管理器 (MNS 模式)从 MNS 获取节点,按 IDC 过滤
KrsDirectlyCluster集群管理器 (直连模式)从配置中心获取 IP,直连特定节点
MTThriftMethodInterceptorCGLIB 拦截器拦截 RPC 调用,触发路由决策
ServerConn服务连接对象封装节点信息和连接池
RouteTable路由表存储 IDC 权重配置,按比例随机

核心要点

  1. Thrift 路由的本质:

    • 业务代码拿到的永远是同一个客户端
    • 路由决策在 Thrift 框架内部完成
    • 通过 CGLIB 动态代理拦截方法调用
  2. 三层架构:

    • 第1层: CGLIB 动态代理 (拦截方法调用)
    • 第2层: 集群管理器 (路由决策 + 节点过滤)
    • 第3层: 负载均衡器 (选择具体节点)
  3. CustomThriftProxy 的设计:

    • 继承 ThriftClientProxy,复用父类功能
    • 通过反射注入自定义组件 (KrsRouteService, KrsOCTOAgentCluster)
    • 父类的 loadServiceProxy() 自动使用自定义组件
  4. 关键优势:

    • ✅ 业务代码无感知
    • ✅ 高性能 (比 HTTP 快 2-3 倍)
    • ✅ 支持灰度发布
    • ✅ 支持动态切换
    • ✅ 配置热更新

学习路径

1. 理解 HTTP 路由 (应用层)
   ↓
2. 对比 Thrift 路由 (框架层)
   ↓
3. 学习 CGLIB 动态代理原理
   ↓
4. 分析 CustomThriftProxy 设计
   ↓
5. 掌握初始化流程
   ↓
6. 实战案例演练