多集群路由架构对比
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/REST | Thrift RPC |
| 端口 | 8412 | 9888 |
| 数据格式 | 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: 主要有以下几个原因:
- 二进制协议: Thrift 使用二进制序列化,比 JSON 更紧凑
- 压缩支持: 支持 Snappy/Gzip 压缩,减少网络传输
- 长连接: Netty 长连接,减少连接开销
- 更少的解析: 二进制解析比 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/REST | Thrift RPC |
| 性能 | 基准 (1x) | 快 1.5-3 倍 |
| 复杂度 | 简单 | 复杂 |
选择建议
选择 HTTP 模式,如果:
✅ 需要简单易懂的架构
✅ 需要方便调试
✅ QPS < 10000
✅ 对性能要求不高
选择 Thrift 模式,如果:
✅ 需要高性能 (QPS > 10000)
✅ 需要数据压缩
✅ 已有 OCTO 框架
✅ 对延迟敏感