目录
一、ES的部署方案概述
在ES中,实现高可用与性能优化,需要从集群部署、分片机制、分片策略、数据存储和索引划分5个维度综合考量:
- 高可用部署:多主节点+多数据节点+跨机架/跨可用区,开启最少3个主节点以保证选举正常;
- 分片机制:主分片+副本分片,主分片数量按 数据量/单片目标容量 计算,副本数按查询吞吐与可用性设定;
- 分片策略:新索引按业务场景(时间、业务模块)划分主分片数量,不盲目用默认5,单片目标保持在 20~50GB;
- 数据存储策略:冷热/冷归档分层存储(Hot / Warm / Cold),结合 SSD/HDD,不同节点类型承担不同角色;
- 索引划分策略:常见的时序索引(如每日/每月)与实体索引(业务对象分表)结合,根据查询模式灵活分组。
原理分析
- 分片与副本
- 主分片:存储原始数据,负责写入操作;
- 副本分片:主分片的完整镜像,负责查询分摊与故障切换。
- 为什么要分片?
- 水平扩展:将数据与负载分布到多台机器;
- 并行处理:查询可并发的在多个分片上执行;
- 容错:副本分片可在节点故障时接管服务。
- 分片过多 vs 过少
- 管理开销
- 分片过多:元数据、文件句柄增多,恢复/重平衡耗时长
- 分片过少:单节点压力大,写入瓶颈,恢复速度慢
- 查询效率
- 分片过多:遍历大量小分片,搜索协调开销高
- 分片过少:单分片数据多,检索耗时高
- 资源利用率
- 分片过多:频繁的小I/O,缓存利用率低
- 分片过少:大I/O,可能超出单机资源
- 管理开销
实践中的分片设置建议
- 单分片目标大小:建议20~50GB,既能充分利用磁盘与内存,又能保证恢复、重平衡速度可控;
- 主分片数量:预计总数据量➗单分片目标大小,根据业务增长预留一定冗余,避免临近上限才扩容导致索引不可改分片数;
- 副本数:根据查询并发需求加副本,最少一份保证高可用,多副本可提升并发查询吞吐,但写入延迟略增;
- 节点数:分片总数➗节点数
整数,保证分片均衡分布,避免某些节点过载;
- 滚动策略/ILM:使用Index Lifecycle Management和Rollover API,按时间或数据量滚动索引,可动态调整分片数量,避免冷数据占用过多资源。
示例
- 场景A:预计年增长数据量2TB,3台数据节点
- 单片目标40GB ➡️ 主分片数
(2048GB➗40GB)= 52
- 每索引(按月划分)
52 ➗ 12
5个主分片;
- 副本一份 ➡️ 总分片10个;
- 分布到3节点 ➡️ 大约每节点 3-4 个分片,负载均衡。
- 单片目标40GB ➡️ 主分片数
- 场景B:日志索引,日数据量100GB,5台节点
- 单日索引可设置分片3个(约33GB/片);
- 副本1分 ➡️ 索引共6个分片;
- 分布到5个节点 ➡️ 某些节点不带此索引,平滑扩容。
二、ILM生命周期策略详解
尤其是在ELK实践中,通过ILM(Index Lifecycle Management)与分层存储策略,可以在不同阶段自动化的调整索引副本、分片数、硬件部署和归档方式,实现成本与性能的最佳平衡。
1、策略详解
Elasticsearch从 Hot ➡️ Warm ➡️ Cold ➡️ Delete 四个阶段,对索引执行不同操作。
| 阶段 | 触发条件 | 典型操作 | 目标 |
|---|---|---|---|
| Hot | 索引创建后立即进入 | -默认1副本,适合的主分片数 -刷新间隔短(1s) -合并策略 aggressive | 支撑高并发写入与查询 |
| Warm | 数据超过N天或体量达到XGB | -减少副本到0/1 -减少主分片数量 -关闭刷新 | 降低资源占用,减少merge I/O |
| Gold | 再过M天 | -增加副本以保证出问题可读 -设置index.priority低 -关闭副本分配到Hot节点 | 支撑偶尔查询,节省高性能存储 |
| Delete | 冷数据超过保留天数 | -删除索引或将数据快照到远程仓库 | 控制存储成本,合规 |
示例:
PUT _ilm/policy/logs-policy
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_size": "50gb",
"max_age": "1d"
}
}
},
"warm": {
"min_age": "1d",
"actions": {
"forcemerge": { "max_num_segments": 1 },
"shrink": { "number_of_shards": 1 },
"set_priority": { "priority": 50 }
}
},
"cold": {
"min_age": "7d",
"actions": {
"allocate": {
"include": { "data": "cold" }
},
"set_priority": { "priority": 25 }
}
},
"delete": {
"min_age": "30d",
"actions": {
"delete": {}
}
}
}
}
}
2、分层存储硬件部署
将集群节点按存储与性能需要划分为不同角色,并部署在适配的硬件上:
| 节点角色 | 存储介质 | 规格示例 | 特性/用途 |
|---|---|---|---|
| hot | NVMe SSD | 64 C CPU, 256 GB RAM, 4 TB NVMe | 高频写入、低延迟查询;默认承载索引创建、写入与新数据搜索 |
| warm | SATA SSD | 32 C CPU, 128 GB RAM, 8 TB SSD | 降低刷新与合并开销,承载 Warm 数据的查询,写入较少 |
| cold | HDD | 16 C CPU, 64 GB RAM, 20 TB HDD | 存放冷数据,偶尔查询,配合 allocate API 定向分配;优先使用滚动快照或 Snapshot 检索 |
| frozen | 低成本 HDD+网络 | 8 C CPU, 32 GB RAM, 50 TB HDD | 只读归档索引,利用 searchable snapshots |
策略要点:
- 不同节点角色:通过 node.attr.data:hot|warm|cold标签与allocate动作结合,自动将索引迁移到对应节点;
- 自动化运维:基于ILM,索引生命周期阶段切换时执行 allocate 与 shrink,减少人工干预;
- 快照归档:对 cold或frozen数据,结合snapshot存储到S3/OSS,节省本地存储。
3、每个阶段的自动化动作
| 阶段 | 触发条件 | ILM动作 |
|---|---|---|
| hot | max_age/max_size达成 | rollover➡️生成新索引(保留别名) |
| warm | 新索引创建后min_age达到 | 1、forcemerge(max_num_segments=1) 2、shrink(number_of_shards=1) 3、allocate到warm节点 |
| cold | warm后min_age达到 | 1、set_priority(低) 2、allocate到cold节点 |
| delete | cold后min_age达到 | delete或者searchable_snapshot |
- Rollover:在hot阶段结束时,用别名(如logs-write)指向新索引,客户端无需感知底层变化
- Forcemerge:合并段文件,减少 Lucene segment 数量,降低后续查询 I/O
- Shrink:把多分片索引压缩为更少分片,降低元数据与管理开销
- Allocate:通过 allocation rules,把索引的所有分片迁移到带有对应 data 标签的节点上。
4、ILM的数据管理
ILM是ES自身提供的内置功能,由master节点自动执行各阶段动作(rollover、forcemerge、shrink、allocate、delete等)
- 当ILM将索引分片从hot ➡️ warm ➡️ cold 时,背后调用了 Cluster Reroute API 对分片做重定位,新节点会接管分片数据,旧节点再删除本地副本;
- 查询时,无论分片在何处,ES都会通过最新的集群状态(Cluster State)中的路由表(Routing Table)自动定位到当前承载该分片的节点,客户端无需感知分片迁移。
1、自动化执行原理
- 定时触发:ILM background job按照policy中配置的 min_age 定期执行;
- 动作下发:master节点在phase切换时,调用对应rest api(
_rollover、_forcemerge、_shrink、/_cluster/reroute); 执行搬迁:Cluster Reroute 根据分配规则,将分片从源节点复制到目标节点,复制完毕后再删除源分片;更新元数据:结束后更新Cluster State,新的路由表包含最新分片位置。
2、查询如何定位到新的分片
集群状态:master在每次分片变更后,都会发布新的cluster state到所有节点,其中包含最新的routing table;Routing Table:指明每个索引的每个分片(主分片和副本)当前所在节点;客户端请求流程:写入/读别名:客户端通过索引或alias发起请求;协调节点:接受请求后,读取本地cluster state;分片路由:根据routing table,将请求并行转发到承载目标分片的节点汇总结果:集合各分片响应后返回给客户端。
整个过程中,只要 Cluster State 更新完成,所有后续请求都会自动走新的分片位置,无需额外配置或干预。
三、ES的数据一致性
1、为什么会产生ES与数据库之间的数据不一致?
1.1、异步写入机制(最终一致性架构)
ES通常是通过异步方式(MQ、Binlog、任务同步)接收数据库变更:
DB → MQ(或 Canal)→ ES → Flush to segment
中间链路非事务,写入过程存在延迟和失败的可能性。
1.2、ES本身的刷新机制
ES写入数据后,并不是立即可查询:
| 步骤 | 说明 | 延迟风险 |
| -------------------- | -------------- | ----------- |
| 写入 buffer + translog | 内存中完成,数据安全但不可见 | ❌ 不可查询 |
| 每秒自动 refresh(或手动)| 刷新后 segment 可见 | ⏱ 默认延迟约 1 秒 |
| 写入失败、MQ 丢消息 | 不可控错误 | ❌ 直接导致丢数据 |
1.3、写入链路复杂
例如 Dubbo + DB + RocketMQ + ES,链路上任何一环节失败或延迟,都会引起不一致:
- MQ延迟
- 消费失败
- ES写入冲突
- Mapping错误导致drop文档
2、单条数据查询不一致的应对方案
场景:如查询某订单明细,状态显示不对(如ES是未支付,但当前DB/业务流程已经流转到已支付)
2.1、主策略:状态判断+回源兜底查询
在读取ES时,如果数据状态不符合预期,则兜底回源数据库:
Order order = es.query(orderId);
if (!"PAID".equals(order.getStatus())) {
order = db.query(orderId); // 回源兜底
}
2.2、辅助策略:强同步写ES(在变更端兜底)
在关键数据变更(如订单状态更新)时:
db.update(orderId, "PAID");
es.update(orderId, "PAID"); // 同步写入 ES,失败则重试或报警
扩展
(1)restHighLevelClient.index() 和 bulkProcessor.add() 写入后,ES是否立马可查?
默认都不会立马可查。ES的写入流程 = 写内存buffer+translog,需要等 refresh 才可查询。默认情况下,refresh是每1秒触发一次(由refresh_interval决定),所以这两种方式都会存在 ~1s 的“不可见窗口”。
- client.index(request):同步发送单条请求,写成功后即可确认写入成功,不一定可查。
- bulkProcessor.add(request):异步批量写入,由内部线程池批量提交,延迟更不可控。
(2)为什么写入成功 不等于 查询可见?
原因是ES的底层 Lucene 引擎:
- 必须执行 refresh 才能将内存 buffer 的倒排索引段刷新为 segment 文件
- 只有 segment 中的内容才能被查询(searchable)
所以,不管你是同步写一条,还是批量写,都要等 refresh 后才能被正常查询。
(3)那么如何实现“强制同步写入+可立刻查询”?
a、使用参数 refresh=wait_for 或手动 refresh
设置 refresh=wait_for (推荐)
在单条 index 请求时加参数:
IndexRequest request = new IndexRequest(indexName)
.id("123")
.source(...);
request.setRefreshPolicy(WriteRequest.RefreshPolicy.WAIT_UNTIL); // 等待刷新完成
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
效果:写入成功后,会等待一次 refresh(最多等1s),写入数据可立刻查询到。
注意:性能开销大,不建议批量数据使用。用于关键路径(如订单状态变更、操作后立即查询)非常合适。
b、手动刷新index(低频适合)
RefreshRequest refreshRequest = new RefreshRequest(indexName);
client.indices().refresh(refreshRequest, RequestOptions.DEFAULT);
写入后的数据立即可查,但:是手动刷新整个索引,成本更高;不推荐频繁调用,但可以用于任务完成后(如定时数据处理后刷新一次)。
c、设置 index 的 refresh_interval = -1(写入期不刷新)
适合批量导入阶段,避免频繁刷新:
PUT /my_index/_settings
{
"index" : {
"refresh_interval" : "-1"
}
}
然后 bulk 导入;
导入完成后,手动执行一次刷新;
再恢复默认的1s自动刷新;
POST /my_index/_refresh
PUT /my_index/_settings
{
"index" : {
"refresh_interval" : "1s"
}
}

2.3、延迟跳转/用户感知控制
对于操作行为链路(如刚支付完跳转订单页):延迟跳转 300~500ms 给ES留时间。
2.4、离线修复机制
定期对比DB与ES数据:
- 用定时任务对账(如每小时)
- 发现差异则重写ES数据
2.5、核心业务不查ES
ES主要解决大量数据的搜索、排序,尤其是我们对数据做了分库分表之后,这些数据的查询和排序在数据库层面就不好做了,这些依赖ES去处理。对于单个数据的业务处理,比如订单在业务系统之间的流转,都可以通过订单ID精准的去查询数据库,这些核心业务都查询数据库不走ES。ES只提供搜索数据给用户等不需要强一致要求的地方。
3、统计、聚合类数据不一致的应对方案
场景:如首页订单汇总、商品销售榜、转化率统计等聚合类指标因ES延迟而不准。
3.1、延迟聚合策略(强烈建议)
聚合时避开当前时间段,延迟5分钟统计:
当前时间 = 10:00 → 聚合时间 = [00:00 ~ 09:55]
避免使用尚未同步完成的数据,提升一致性稳定性。
3.2、聚合走DB/离线数仓
不直接依赖ES做统计,而用数据库 Hive/ClickHouse 等进行聚合:
SELECT SUM(paid_amount) FROM orders WHERE status = 'PAID'
精度要求高时,统计查询不要走ES
3.3、增量式聚合系统(Redis / Kafka Sink)
数据变更时发增量时间,异步消费并更新聚合结果:
- 实时性好
- 与ES分离,结构清晰
- 支持高并发统计
总结:




4248

被折叠的 条评论
为什么被折叠?



