Elasticsearch 要存储海量数据,应该如何设计路由(Routing)策略?
在 Elasticsearch (ES) 中存储海量数据时,路由(Routing)策略的设计直接决定了查询性能、写入吞吐量以及集群的稳定性。
默认情况下,ES 使用文档 _id 进行哈希路由,这能保证数据均匀分布,但会导致查询时的“广播模式”(Scatter-Gather),即查询需要扫描所有分片。对于海量数据,这通常是不可接受的。
以下是针对海量数据场景的路由设计策略指南,分为核心原则、常见策略、以及应对数据倾斜的方案。
一、 核心原理
ES 确定文档所在分片的公式为:
- 默认情况:
routing=_id。 - 自定义路由:
routing=user_id/city/tenant_id等业务字段。
二、 路由策略设计方案
1. 默认路由(不推荐用于海量数据的精确查询)
- 机制:使用自动生成的
_id或业务主键作为路由。 - 优点:数据在分片间分布非常均匀,写入性能好,无热点分片问题。
- 缺点:查询性能差。每次查询都需要访问所有分片,然后聚合结果。随着数据量和分片数增加,Coordinator 节点的压力剧增,延迟变高。
- 适用场景:数据量中等,或者查询模式非常随机(无法通过特定键聚合)。
2. 自定义路由(Custom Routing)—— 推荐方案
- 机制:指定业务强相关的字段(如
user_id,order_id,region)作为路由键。bashPUT /my_index/_doc/1?routing=user_123 { ... } - 优点:查询性能极高。查询时带上
routing参数,ES 只需要去这 1 个分片查找,避免了广播风暴。bashGET /my_index/_search?routing=user_123 { ... } - 缺点:数据倾斜(Data Skew)。如果某个
user_123的数据量远超其他用户,会导致该分片过大,成为性能瓶颈(木桶效应)。 - 适用场景:多租户系统(SaaS)、用户维度的订单/日志查询。
3. 路由分区(Routing Partition Size)—— 折中方案
- 机制:ES 允许将同一个 routing key 的数据路由到一组分片中,而不是一个分片。
- 配置:设置
index.routing_partition_size(例如设置为 3)。 - 效果:
user_123的数据会分布在 3 个分片上,而不是 1 个。 - 优点:缓解了单分片过大的数据倾斜问题,同时查询时只需要查这 3 个分片,比查所有分片快,比查 1 个分片慢。
- 适用场景:单个路由键的数据量较大,单个分片存不下,但又希望获得一定的查询剪枝优化。
三、 海量数据下的架构模式
针对海量数据,通常需要结合 时间维度 和 业务维度 混合设计。
模式 A:基于时间的滚动索引 (Time-Based + ILM)
- 场景:日志、监控指标、流水记录。
- 策略:
- 不使用自定义路由,或者仅在特定场景使用。
- 利用 Data Streams 或 Index Lifecycle Management (ILM)。
- 数据按天/周/月生成新索引。
- 查询优化:查询时带上时间范围,ES 会自动跳过不在时间范围内的索引(利用
min/max字段优化)。
模式 B:自定义路由 + 别名隔离 (Routing + Alias)
- 场景:SaaS 平台,每个租户数据量差异巨大。
- 策略:
- 小租户:共用一个大索引,使用
routing=tenant_id。 - 大租户:单独建立索引(独立索引),不使用路由或使用默认路由。
- 应用层封装:应用层维护一个映射表(Tenant -> Index),查询时根据租户 ID 决定查哪个索引。
- 小租户:共用一个大索引,使用
模式 C:冷热分离 + 路由
- 场景:海量历史数据。
- 策略:
- 热数据(近期):使用自定义路由,保证写入和高频查询性能。
- 冷数据(历史):通过 ILM 迁移到冷节点,甚至在 Force Merge 后取消路由限制(如果查询模式发生变化),或者继续保持路由以节省 IO。
四、 如何解决“数据倾斜”问题?
这是自定义路由最大的风险。如果你的业务中存在“超级大V”用户,必须处理。
业务层拆分(加盐):
- 如果
user_id=888数据量太大,应用层写入时人为将其拆分为888_0,888_1,888_2。 - 路由时使用
routing=888_0等。 - 查询时使用
routing=888_0,888_1,888_2(ES 支持指定多个 routing)。
- 如果
独立索引(Isolated Index):
- 检测到某个 Routing Key 数据量超过阈值(如 50GB),将其迁移到独立索引。
- 普通用户继续在共享索引中使用 Routing。
使用
routing_partition_size:- 如前所述,让数据分散到多个分片,而非一个。注意:开启此功能后,
_id的唯一性仅在分片内保证,而非索引内保证(通常不是问题)。
- 如前所述,让数据分散到多个分片,而非一个。注意:开启此功能后,
五、 最佳实践清单
- 强制带 Routing 查询:如果设计了路由,应用代码必须在查询时带上
routing参数,否则查询结果可能不完整(如果开启了_routing必填配置)或性能极差。 - 设置
_routing.required: true:在 Mapping 中强制要求写入和查询必须带路由,防止开发人员误操作导致数据分散或全表扫描。 - 基数评估:选择的 Routing Key 基数(Cardinality)要够大。不要使用
gender(性别)这种只有几个值的字段做路由,会导致分片极度不均。 - 分片大小控制:配合路由策略,单个分片建议控制在 30GB - 50GB 之间。
- ID 生成策略:如果使用了自定义路由,尽量不要让 ES 自动生成
_id,最好由业务生成并保证全局唯一,因为 ES 在不同分片上的 ID 查重开销不同。
总结建议
- 如果是日志/流水数据:优先使用 Data Streams + ILM,路由策略次之(除非有明确的按用户/设备ID追踪需求)。
- 如果是业务实体数据(订单、用户内容):
- 数据分布均匀:使用 自定义路由(
user_id)。 - 存在超级大户:使用
routing_partition_size或 应用层分索引。
- 数据分布均匀:使用 自定义路由(