#2 route

32 阅读8分钟

多集群路由架构对比

HTTP 模式:

请求 → 获取应用所在的IDC → 根据路由表得到server IDC → 返回对应的EsClient → 发送请求

关键点:

  • ⭐ 一次路由,一次决策
  • 路由发生在 getClient() 时
  • 返回的是不同的 EsClient 对象

Thrift 模式:

请求 → 获取应用所在的IDC → 返回固定的 thriftWrapperClient 
     → 调用 search() 方法 
     → CGLIB 拦截 
     → 根据路由表得到 server IDC 
     → 过滤节点 
     → 发送请求

关键点:

  • ⭐ 延迟路由,每次决策
  • 路由发生在每次 RPC 调用
  • 返回的是同一个 thriftWrapperClient 对象
  • 路由决策在 CGLIB 拦截器中完成

📚 目录


1. 架构对比

1.1 HTTP 模式架构

┌─────────────────────────────────────────────────┐
│              KrsSearchManager (多集群管理器)              │
│                                                         │
│  clients = {                                            │
│    "appkey.mt" → EsClient1 (MT机房),                   │
│    "appkey.zf" → EsClient2 (ZF机房),                   │
│    "appkey.sh" → EsClient3 (SH机房)                    │
│  }                                                      │
│                                                         │
│  routeTable = RouteTable({"mt":50, "zf":50})           │
│                                                         │
│  ⭐ getClient() {                                       │
│    // 应用层路由决策                                     │
│    String idc = routeTable.getRouteIdc();  // 按比例随机    │
│    return clients.get("appkey." + idc);                │
│  }                                                      │
└─────────────────────────────────────────────────┘
                        ↓
        ┌───────────────┼───────────────┐
        ↓               ↓               ↓
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│  EsClient1   │ │  EsClient2   │ │  EsClient3   │
│  (MT 机房)   │ │  (ZF 机房)   │ │  (SH 机房)   │
│              │ │              │ │              │
│ RestHighLevel│ │ RestHighLevel│ │ RestHighLevel│
│   Client     │ │   Client     │ │   Client     │
│      +       │ │      +       │ │      +       │
│  RestClient  │ │  RestClient  │ │  RestClient  │
│      +       │ │      +       │ │      +       │
│   Sniffer    │ │   Sniffer    │ │   Sniffer    │
└──────────────┘ └──────────────┘ └──────────────┘
        ↓               ↓               ↓
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ ES MT 集群   │ │ ES ZF 集群   │ │ ES SH 集群   │
│ HTTP:8412    │ │ HTTP:8412    │ │ HTTP:8412    │
│ 10.1.1.1     │ │ 10.2.1.1     │ │ 10.3.1.1     │
│ 10.1.1.2     │ │ 10.2.1.2     │ │ 10.3.1.2     │
└──────────────┘ └──────────────┘ └──────────────┘

特点:

  • 应用层路由: 在 KrsSearchManager.getClient() 中决策
  • 多个 EsClient: 每个机房一个独立的客户端
  • HTTP 协议: 使用 RestClient 通信
  • 端口: 8412

1.2 Thrift 模式架构

┌─────────────────────────────────────────────────┐
│              KrsSearchManager (多集群管理器)              │
│                                                         │
│  thriftWrapperClient = EsClient(thriftProxy)  // 只有一个!      │
│                                                         │
│  getClient() {                                          │
│    return thriftWrapperClient;  // 直接返回,不做路由决策        │
│  }                                                      │
└─────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────┐
│                      EsClient                           │
│  thriftClient = CustomThriftProxy.getObject()          │
│  // 包装 Thrift 客户端                                   │
└─────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────┐
│                  CustomThriftProxy                      │
│                 (Thrift 客户端代理)                      │
│                                                         │
│  krsRouteService = new KrsRouteService(this)           │
│  cluster = new KrsOCTOAgentCluster(this)                       │
│                                                         │
│  serviceProxy = CGLIB 动态代理(KrsSearchThriftService.class)    │
└─────────────────────────────────────────────────┘
                        ↓
        ┌───────────────┴───────────────┐
        ↓                               ↓
┌──────────────────────┐    ┌──────────────────────────────┐
│  KrsRouteService     │    │   KrsOCTOAgentCluster        │
│  (路由决策服务)       │    │   (集群管理 + 节点过滤)       │
│                      │    │                              │
│  routeTable =        │    │  ⭐ getServerConnList() {    │
│    RouteTable(...)   │    │    // 1. 获取路由目标 IDC    │
│                      │    │    String idc =              │
│  ⭐ getRouteIdc() {  │    │      krsRouteService         │
│    return routeTable │    │        .getReadRouteIdc();   │
│      .getRouteIdc(); │    │                              │
│  }                   │    │    // 2. 按 IDC 过滤节点     │
│                      │    │    return filterByIdc(idc);  │
│  }                   │    │  }                           │
└──────────────────────┘    │  └──────────────────────────────┘
                                        ↓
                        ┌───────────────┼───────────────┐
                        ↓               ↓               ↓
                ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
                │ ES MT 集群   │ │ ES ZF 集群   │ │ ES SH 集群   │
                │ Thrift:9888  │ │ Thrift:9888  │ │ Thrift:9888  │
                │ 10.1.1.1     │ │ 10.2.1.1     │ │ 10.3.1.1     │
                │ 10.1.1.2     │ │ 10.2.1.2     │ │ 10.3.1.2     │
                └──────────────┘ └──────────────┘ └──────────────┘

特点:

  • 框架层路由: 在 Thrift 框架内部决策
  • 单个 EsClient: 只有一个客户端,包装 Thrift 代理
  • Thrift 协议: 使用 Thrift RPC 通信
  • 端口: 9888
  • CGLIB 拦截: 所有方法调用都被拦截

2. 核心差异

维度HTTP 模式Thrift 模式
路由决策位置应用层 (KrsSearchManager.getClient())框架层 (KrsOCTOAgentCluster.getServerConnList())
EsClient 数量多个 (每个机房一个)单个 (包装 Thrift 代理)
路由时机获取客户端时每次 RPC 调用时
通信协议HTTP/RESTThrift RPC
端口84129888
数据格式JSON (文本)Binary (二进制)
压缩支持❌ 不支持✅ Snappy/Gzip
连接管理RestClient 连接池Thrift 连接池 (Netty)
节点发现Sniffer 自动发现OCTO Agent 服务发现
拦截机制CGLIB 动态代理
业务感知✅ 能感知不同客户端❌ 完全无感知
性能基准 (1x)快 1.5-3 倍

3. HTTP 模式详解

3.1 调用流程

业务代码
    ↓
searchManagerProxy.getClient()
    ↓
【应用层路由决策】
KrsSearchManager.getClient() {
    if (启用新路由 && routeTable != null) {
        String idc = routeTable.getRouteIdc();  // ⭐ 按比例随机
        // random = 30 → "mt"
        // random = 70 → "zf"
        return clients.get("appkey." + idc);
    }
    return clients.get(selectedKey);
}
    ↓
返回 EsClient (例如: MT 机房的客户端)
    ↓
SearchBuilder.action()
    ↓
RestHighLevelClient.search()
    ↓
RestClient.performRequest()
    ↓
【节点级负载均衡】
轮询选择节点: 10.1.1.1:8412
    ↓
发送 HTTP 请求
POST http://10.1.1.1:8412/_search
    ↓
返回结果

3.2 关键代码

// KrsSearchManager.java:237
public EsClient getClient() {
    // ⭐ 应用层路由决策
    if (RouteUtil.newRouteEnable(appkey) && null != routeTable) {
        String idc = routeTable.getRouteIdc();  // 按比例随机
        String clusterKey = NameService.getClusterKey(appkey, idc);
        return clients.get(clusterKey);
    }
    return clients.get(selectedKey);
}

4. Thrift 模式详解

4.1 调用流程

业务代码
    ↓
searchManagerProxy.getClient()
    ↓
【应用层】
KrsSearchManager.getClient() {
    return thriftWrapperClient;  // 直接返回,不做路由决策
}
    ↓
返回 thriftWrapperClient (固定的)
    ↓
thriftClient.search(request, callback)
    ↓
【CGLIB 拦截】
MTThriftMethodInterceptor.intercept()
    ↓
【框架层路由决策】
cluster.getServerConnList() {
    // 1. 获取路由目标 IDC
    String idc = krsRouteService.getReadRouteIdcName();
    // ⭐ 按比例随机: 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 请求
    ↓
返回结果

4.2 关键代码

// KrsOCTOAgentCluster.java:26
private List<ServerConn> filterConnsByDegradedAndIdc(List<ServerConn> connList) {
    // ⭐ 框架层路由决策
    String routeIdcName = this.krsRouteService.getReadRouteIdcName();
    // routeIdcName = "mt" 或 "zf" (按比例随机选择)

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

5. 路由流程对比

5.1 HTTP 模式路由流程

业务代码
    ↓
searchManagerProxy.getClient()
    ↓
【应用层路由决策】
KrsSearchManager.getClient() {
    if (启用新路由 && routeTable != null) {
        String idc = routeTable.getRouteIdc();  // ⭐ 按比例随机
        return clients.get("appkey." + idc);
    }
}
    ↓
返回 EsClient (例如: MT 机房的客户端)
    ↓
SearchBuilder.action()
    ↓
RestHighLevelClient.search()
    ↓
RestClient.performRequest()
    ↓
【节点级负载均衡】
轮询选择节点: 10.1.1.1:8412
    ↓
发送 HTTP 请求
POST http://10.1.1.1:8412/_search
    ↓
返回结果

5.2 Thrift 模式路由流程

业务代码
    ↓
searchManagerProxy.getClient()
    ↓
【应用层】
KrsSearchManager.getClient() {
    return thriftWrapperClient;  // 直接返回,不做路由决策
}
    ↓
返回 thriftWrapperClient (固定的)
    ↓
thriftClient.search(request, callback)
    ↓
【CGLIB 拦截】
MTThriftMethodInterceptor.intercept()
    ↓
【框架层路由决策】
cluster.getServerConnList() {
    // 1. 获取路由目标 IDC
    String idc = krsRouteService.getReadRouteIdcName();
    // ⭐ 按比例随机: 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 请求
    ↓
返回结果

6. 实战案例

6.1 灰度发布新集群

HTTP 模式灰度发布
// 初始配置: 100% 流量到老集群
{
  "mt_old": 100,
  "mt_new": 0
}

// 灰度 10%
{
  "mt_old": 90,
  "mt_new": 10
}

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

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

效果: 100 个请求中,老集群从 100 个逐渐减少到 0 个,新集群从 0 个逐渐增加到 100 个。

Thrift 模式灰度发布
// 配置中心: appkey.route.beijing
{
  "mt_old": 90,   // 老集群 90% 流量
  "mt_new": 10    // 新集群 10% 流量(灰度)
}

效果: 同样支持灰度发布,但路由决策在框架层完成。

6.2 集群故障切换

HTTP 模式故障切换
# 正常情况
appkey.read.mt = appkey.mt

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

# 故障恢复,切回 MT 集群
appkey.read.mt = appkey.mt
Thrift 模式故障切换
# 正常情况
appkey.read.mt = mt

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

# 故障恢复,切回 MT 集群
appkey.read.mt = mt

效果: 两种模式都支持配置热更新,无需重启应用。


7. 最佳实践

7.1 HTTP 模式最佳实践

@Service
public class SearchService {

    @Autowired
    private SearchManagerProxy searchManagerProxy;

    public SearchResponse search(String keyword) {
        // ⭐ 每次都调用 getClient()
        EsClient client = searchManagerProxy.getClient();

        SearchBuilder builder = new SearchBuilder(client)
            .addIndices("index_alias")
            .setQuery(QueryBuilders.matchQuery("keyword", keyword))
            .from(0).size(10);

        return builder.action();
    }
}

// 100 个请求:
// - 约 50 个 → IDC1 集群
// - 约 50 个 → IDC2 集群
// ⭐ 按比例分流生效!

7.2 Thrift 模式最佳实践

@Service
public class SearchService {

    @Autowired
    private SearchManagerProxy searchManagerProxy;

    public SearchResponse search(String keyword) {
        // ⭐ 每次都调用 getClient() (虽然返回同一个,但这是规范)
        EsClient client = searchManagerProxy.getClient();

        // 调用 search 方法
        return client.search(request, callback);
        // ⭐ Thrift 框架会自动拦截,完成路由决策
    }
}

// 100 个请求:
// - 约 50 个 → MT 集群
// - 约 50 个 → ZF 集群
// ⭐ 按比例分流仍然生效! (因为路由在框架层)

8. 常见问题

Q1: 为什么 Thrift 模式性能更好?

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

  1. 二进制协议: Thrift 使用二进制序列化,比 JSON 更紧凑
  2. 压缩支持: 支持 Snappy/Gzip 压缩,减少网络传输
  3. 长连接: Netty 长连接,减少连接开销
  4. 更少的解析: 二进制解析比 JSON 解析更快
数据大小对比:
JSON: {"name":"张三","age":25}  → 30 字节
Binary: [0x01, 0x06, 0xE5...]    → 10 字节
Snappy: [0x1F, 0x8B, ...]        → 5 字节

性能提升: 2-3

Q2: 如何选择 HTTP 还是 Thrift?

A: 根据以下因素选择:

因素选择 HTTP选择 Thrift
QPS< 10000> 10000
延迟要求不敏感敏感 (< 10ms)
集群数量< 5 个> 5 个
调试需求需要方便调试性能优先
跨语言需要不需要
已有框架已有 OCTO

Q3: 路由表配置错误会怎样?

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

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

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

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

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

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

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

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

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

📝 总结

HTTP 模式:

应用层路由: 在 getClient() 时按比例随机选择一个机房的 EsClient,然后用这个客户端发送 HTTP 请求。就像去餐厅点餐,先选择哪个窗口(机房),然后在这个窗口点餐(发请求)。

特点:

  • ✅ 路由在应用层,业务代码能感知
  • ✅ 多个 EsClient,每个管理一个机房
  • ✅ HTTP 协议,JSON 格式,易于调试
  • ✅ 适合通用场景,QPS < 10000

Thrift 模式:

框架层路由: getClient() 总是返回同一个客户端,但在 Thrift 框架内部,每次 RPC 调用都会被 CGLIB 拦截,然后按比例随机选择一个机房的节点发送请求。就像有个智能服务员,你只管点餐,他会自动帮你选择最合适的窗口。

特点:

  • ✅ 路由在框架层,业务代码无感知
  • ✅ 单个 thriftWrapperClient,包装 Thrift 代理
  • ✅ Thrift 协议,二进制格式,高性能
  • ✅ 适合高性能场景,QPS > 10000

核心差异

维度HTTP 模式Thrift 模式
路由层应用层框架层
客户端多个单个
协议HTTP/RESTThrift RPC
性能基准 (1x)快 1.5-3 倍
复杂度简单复杂

选择建议

选择 HTTP 模式,如果:
✅ 需要简单易懂的架构
✅ 需要方便调试
✅ QPS < 10000
✅ 对性能要求不高

选择 Thrift 模式,如果:
✅ 需要高性能 (QPS > 10000)
✅ 需要数据压缩
✅ 已有 OCTO 框架
✅ 对延迟敏感