ElasticSearch的高可用

目录

一、ES的部署方案概述

二、ILM生命周期策略详解

三、ES的数据一致性

1、为什么会产生ES与数据库之间的数据不一致?

2、单条数据查询不一致的应对方案

3、统计、聚合类数据不一致的应对方案


一、ES的部署方案概述

在ES中,实现高可用与性能优化,需要从集群部署、分片机制、分片策略、数据存储和索引划分5个维度综合考量:

  1. 高可用部署:多主节点+多数据节点+跨机架/跨可用区,开启最少3个主节点以保证选举正常;
  2. 分片机制:主分片+副本分片,主分片数量按 数据量/单片目标容量 计算,副本数按查询吞吐与可用性设定;
  3. 分片策略:新索引按业务场景(时间、业务模块)划分主分片数量,不盲目用默认5,单片目标保持在 20~50GB;
  4. 数据存储策略:冷热/冷归档分层存储(Hot / Warm / Cold),结合 SSD/HDD,不同节点类型承担不同角色;
  5. 索引划分策略:常见的时序索引(如每日/每月)与实体索引(业务对象分表)结合,根据查询模式灵活分组。

原理分析

  1. 分片与副本
    1. 主分片:存储原始数据,负责写入操作;
    2. 副本分片:主分片的完整镜像,负责查询分摊与故障切换。
  2. 为什么要分片?
    1. 水平扩展:将数据与负载分布到多台机器;
    2. 并行处理:查询可并发的在多个分片上执行;
    3. 容错:副本分片可在节点故障时接管服务。
  3. 分片过多 vs 过少
    1. 管理开销
      1. 分片过多:元数据、文件句柄增多,恢复/重平衡耗时长
      2. 分片过少:单节点压力大,写入瓶颈,恢复速度慢
    2. 查询效率
      1. 分片过多:遍历大量小分片,搜索协调开销高
      2. 分片过少:单分片数据多,检索耗时高
    3. 资源利用率
      1. 分片过多:频繁的小I/O,缓存利用率低
      2. 分片过少:大I/O,可能超出单机资源

实践中的分片设置建议

  1. 单分片目标大小:建议20~50GB,既能充分利用磁盘与内存,又能保证恢复、重平衡速度可控;
  2. 主分片数量:预计总数据量➗单分片目标大小,根据业务增长预留一定冗余,避免临近上限才扩容导致索引不可改分片数;
  3. 副本数:根据查询并发需求加副本,最少一份保证高可用,多副本可提升并发查询吞吐,但写入延迟略增;
  4. 节点数:分片总数➗节点数 \approx 整数,保证分片均衡分布,避免某些节点过载;
  5. 滚动策略/ILM:使用Index Lifecycle Management和Rollover API,按时间或数据量滚动索引,可动态调整分片数量,避免冷数据占用过多资源。

示例

  1. 场景A:预计年增长数据量2TB,3台数据节点
    1. 单片目标40GB ➡️ 主分片数 \approx (2048GB➗40GB)= 52
    2. 每索引(按月划分)\approx 52 ➗ 12 \approx 5个主分片;
    3. 副本一份 ➡️ 总分片10个;
    4. 分布到3节点 ➡️ 大约每节点 3-4 个分片,负载均衡。
  2. 场景B:日志索引,日数据量100GB,5台节点
    1. 单日索引可设置分片3个(约33GB/片);
    2. 副本1分 ➡️ 索引共6个分片;
    3. 分布到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、分层存储硬件部署

将集群节点按存储与性能需要划分为不同角色,并部署在适配的硬件上:

节点角色存储介质规格示例特性/用途
hotNVMe SSD64 C CPU, 256 GB RAM, 4 TB NVMe高频写入、低延迟查询;默认承载索引创建、写入与新数据搜索
warmSATA SSD32 C CPU, 128 GB RAM, 8 TB SSD降低刷新与合并开销,承载 Warm 数据的查询,写入较少
coldHDD16 C CPU, 64 GB RAM, 20 TB HDD存放冷数据,偶尔查询,配合 allocate API 定向分配;优先使用滚动快照或 Snapshot 检索
frozen低成本 HDD+网络8 C CPU, 32 GB RAM, 50 TB HDD只读归档索引,利用 searchable snapshots

策略要点:

  1. 不同节点角色:通过 node.attr.data:hot|warm|cold标签与allocate动作结合,自动将索引迁移到对应节点;
  2. 自动化运维:基于ILM,索引生命周期阶段切换时执行 allocate 与 shrink,减少人工干预;
  3. 快照归档:对 cold或frozen数据,结合snapshot存储到S3/OSS,节省本地存储。

3、每个阶段的自动化动作

阶段触发条件ILM动作
hotmax_age/max_size达成rollover➡️生成新索引(保留别名)
warm新索引创建后min_age达到

1、forcemerge(max_num_segments=1)

2、shrink(number_of_shards=1)

3、allocate到warm节点

coldwarm后min_age达到

1、set_priority(低)

2、allocate到cold节点

deletecold后min_age达到delete或者searchable_snapshot

  1. Rollover:在hot阶段结束时,用别名(如logs-write)指向新索引,客户端无需感知底层变化
  2. Forcemerge:合并段文件,减少 Lucene segment 数量,降低后续查询 I/O
  3. Shrink:把多分片索引压缩为更少分片,降低元数据与管理开销
  4. Allocate:通过 allocation rules,把索引的所有分片迁移到带有对应 data 标签的节点上。

4、ILM的数据管理

ILM是ES自身提供的内置功能,由master节点自动执行各阶段动作(rollover、forcemerge、shrink、allocate、delete等)

  1. 当ILM将索引分片从hot ➡️ warm ➡️ cold 时,背后调用了 Cluster Reroute API 对分片做重定位,新节点会接管分片数据,旧节点再删除本地副本;
  2. 查询时,无论分片在何处,ES都会通过最新的集群状态(Cluster State)中的路由表(Routing Table)自动定位到当前承载该分片的节点,客户端无需感知分片迁移。

1、自动化执行原理

  1. 定时触发:ILM background job按照policy中配置的 min_age 定期执行;
  2. 动作下发:master节点在phase切换时,调用对应rest api(_rollover_forcemerge_shrink/_cluster/reroute);
  3. 执行搬迁:Cluster Reroute 根据分配规则,将分片从源节点复制到目标节点,复制完毕后再删除源分片;
  4. 更新元数据:结束后更新Cluster State,新的路由表包含最新分片位置。

2、查询如何定位到新的分片

  1. 集群状态:master在每次分片变更后,都会发布新的cluster state到所有节点,其中包含最新的routing table;
  2. Routing Table:指明每个索引的每个分片(主分片和副本)当前所在节点;
  3. 客户端请求流程:
    1. 写入/读别名:客户端通过索引或alias发起请求;
    2. 协调节点:接受请求后,读取本地cluster state;
    3. 分片路由:根据routing table,将请求并行转发到承载目标分片的节点
    4. 汇总结果:集合各分片响应后返回给客户端。

整个过程中,只要 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,链路上任何一环节失败或延迟,都会引起不一致:

  1. MQ延迟
  2. 消费失败
  3. ES写入冲突
  4. 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 的“不可见窗口”。

  1. client.index(request):同步发送单条请求,写成功后即可确认写入成功,不一定可查。
  2. bulkProcessor.add(request):异步批量写入,由内部线程池批量提交,延迟更不可控。
(2)为什么写入成功 不等于 查询可见?

原因是ES的底层 Lucene 引擎:

  1. 必须执行 refresh 才能将内存 buffer 的倒排索引段刷新为 segment 文件
  2. 只有 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数据:

  1. 用定时任务对账(如每小时)
  2. 发现差异则重写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)

数据变更时发增量时间,异步消费并更新聚合结果:

  1. 实时性好
  2. 与ES分离,结构清晰
  3. 支持高并发统计

总结:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值