ElasticSearch架构和写入、更新、删除、查询的底层逻辑
ES插入增产改查流程
简单说,ES 架构基于 “分布式集群 + 分片” 实现高可用与横向扩展,而读写删查的底层逻辑则围绕 “倒排索引、分片协作、版本控制” 展开,确保性能与数据一致性。
一、Elasticsearch 核心架构
ES 是分布式搜索引擎,核心架构设计的目标是 “分片存储、集群协作”,先明确关键组件及其作用:
1. 集群与节点(Cluster & Node)
- 集群(Cluster):由多个节点组成的整体,对外提供统一服务,通过
cluster.name标识(默认elasticsearch)。 - 节点(Node):单个 ES 进程,按功能分为 3 类核心节点:
- 主节点(Master Node):管理集群元数据(如分片分配、节点加入 / 退出),默认每个节点都可参选,通过
node.master: true配置。 - 数据节点(Data Node):存储分片数据,负责读写删查操作,通过
node.data: true配置(性能敏感,需足够 CPU / 内存 / 磁盘)。 - 协调节点(Coordinating Node):接收客户端请求,转发到对应节点并汇总结果,默认所有节点都是协调节点(可通过
node.master: false和node.data: false配置纯协调节点)。
- 主节点(Master Node):管理集群元数据(如分片分配、节点加入 / 退出),默认每个节点都可参选,通过
2. 索引与分片(Index & Shard)
- 索引(Index):类似关系型数据库的 “表”,是逻辑上的数据集(如
user_index存储用户数据)。 - 分片(Shard):索引的物理拆分,解决 “单节点存储海量数据” 的问题,分为两类:
- 主分片(Primary Shard):数据写入的第一站,每个索引默认 1 个(可在创建时指定,创建后不可修改),负责数据的索引构建与存储。
- 副本分片(Replica Shard):主分片的副本,用于 “高可用” 和 “分担查询压力”,数量可动态调整(默认 1 个),仅能从主分片同步数据,不直接接收写入。
例:若
user_index配置 3 个主分片、2 个副本,则总分片数 = 3(主) + 3×2(副本)= 9 个,数据会均匀分布到 3 个主分片中。
二、写入(Index)底层逻辑
ES 写入数据的核心是 “先写主分片,再同步副本,最后构建倒排索引”,确保数据可靠与查询可用,步骤如下:
- 请求路由:客户端发送写入请求到协调节点,协调节点通过公式
shard = hash(routing) % 主分片数计算数据应写入的主分片(routing默认是文档 ID,可自定义)。 - 主分片写入:协调节点将请求转发到主分片所在的数据节点,该节点执行以下操作:
- 验证请求(如字段类型、权限);
- 将数据写入 内存缓冲区(In-Memory Buffer),同时记录 事务日志(Translog)(防止内存数据丢失);
- 当内存缓冲区满(默认 100MB)或达到刷新间隔(默认 1 秒),触发 刷新(Refresh):将内存数据生成不可变的 段(Segment) 写入磁盘(但不刷盘,仍在操作系统缓存),此时数据可被查询;
- 副本同步:主分片写入成功后,将数据同步到所有副本分片所在节点,副本执行与主分片相同的写入逻辑(内存 + Translog+Segment)。
- 最终刷盘:当 Translog 满(默认 512MB)或达到刷盘间隔(默认 30 分钟),触发 ** flush**:将所有 Segment 刷到磁盘,清空 Translog,完成数据持久化。
三、更新(Update)底层逻辑
ES 没有 “原地更新”,本质是 “删除旧文档 + 写入新文档”,步骤如下:
- 查询旧文档:协调节点先根据文档 ID 找到主分片,查询旧文档的版本号(ES 用版本控制避免并发冲突)。
- 标记删除:将旧文档标记为 已删除(Tombstone),不直接物理删除(物理删除会在后续 “段合并” 时执行)。
- 写入新文档:按 “写入逻辑” 将更新后的新文档写入主分片,同时版本号 +1。
- 同步副本:主分片将 “删除旧文档 + 写入新文档” 的操作同步到副本,确保集群数据一致。
注意:若更新时未指定文档 ID(如
_update_by_query),会先查询符合条件的文档,再逐个执行 “删旧写新”,性能较低,建议尽量按 ID 更新。
四、删除(Delete)底层逻辑
ES 同样不支持 “物理删除”,而是 “标记删除 + 延迟清理”,步骤如下:
- 请求路由:协调节点根据文档 ID 找到主分片,发送删除请求。
- 标记删除:主分片将目标文档标记为 Tombstone(墓碑),此时文档不可被查询,但仍占用磁盘空间。
- 同步副本:主分片将删除标记同步到所有副本,确保副本也不可查询该文档。
- 物理清理:当 ES 后台执行 段合并(Segment Merge) 时(将多个小 Segment 合并为大 Segment),会过滤掉标记为 Tombstone 的文档,释放磁盘空间。
五、查询(Search)底层逻辑
ES 查询分为 “查询(Query)” 和 “取回(Fetch)” 两阶段,支持 “近实时查询”,步骤如下:
1. 查询阶段(Query Phase):找到匹配文档的 ID 和得分
- 请求分发:协调节点将查询请求广播到索引的所有主分片和副本分片(默认随机选一个,分担压力)。
- 分片查询:每个分片在本地的 Segment 中执行查询(基于倒排索引快速匹配),返回 “匹配的文档 ID + 相关性得分(Score)”,并按得分排序,取前 N 条(如分页查询的
size数量)。 - 结果汇总:协调节点收集所有分片返回的结果,再次按得分排序,筛选出最终的前 N 条文档 ID。
2. 取回阶段(Fetch Phase):获取完整文档数据
- 请求文档:协调节点根据最终的文档 ID,向对应的分片(主或副本)发送 “获取文档” 请求,获取完整的文档字段(默认返回所有字段,可通过
_source筛选)。 - 返回结果:协调节点汇总所有完整文档,返回给客户端。
优化点:若查询仅需部分字段(如
name、age),可在查询时指定_source: ["name", "age"],减少数据传输量,提升性能。
六、关键底层技术支撑
- 倒排索引(Inverted Index):查询的核心,将 “字段值” 映射到 “文档 ID”(如 “关键词:Elasticsearch” 对应文档 ID 1、3、5),实现快速全文检索。
- 段(Segment):ES 存储数据的最小单元,是不可变的 Lucene 索引文件,不可变特性保证了查询性能(无需锁竞争),但也导致更新 / 删除需 “标记 + 重建”。
- 版本控制:通过
_version字段实现,更新 / 删除时需验证版本号,防止并发操作导致数据覆盖(默认乐观锁,也支持悲观锁)。
七、Elasticsearch 读写删查核心步骤对比表
该表汇总了写入(Index)、更新(Update)、删除(Delete)、查询(Search)四大操作的核心流程、关键技术点及核心目标,方便快速对比和记忆。
| 操作类型 | 核心步骤(按执行顺序) | 关键技术 / 组件 | 核心目标 |
|---|---|---|---|
| 写入(Index) | 1. 协调节点按 hash(routing)%主分片数 路由到主分片2. 主分片验证请求,写入内存缓冲区 + Translog3. 触发 Refresh:内存数据生成 Segment(可查询)4. 主分片同步数据到所有副本分片5. 触发 Flush:Segment 刷盘,清空 Translog | routing 路由、内存缓冲区、Translog、Segment、Refresh/Flush | 数据可靠存储(持久化)、近实时可查 |
| 更新(Update) | 1. 协调节点路由到主分片,查询旧文档并获取版本号2. 主分片标记旧文档为 Tombstone(逻辑删除)3. 按写入流程将更新后的数据写入主分片(版本号 + 1)4. 同步 “删旧写新” 操作到所有副本5. 旧文档待 Segment Merge 时物理删除 | 版本控制、Tombstone、Segment Merge | 保证数据更新一致性,避免并发冲突 |
| 删除(Delete) | 1. 协调节点路由到主分片,验证文档存在性2. 主分片标记目标文档为 Tombstone3. 同步删除标记到所有副本分片(副本同步标记后,文档不可查)4. 待 Segment Merge 时,过滤 Tombstone 文档,释放磁盘空间 | routing 路由、Tombstone、Segment Merge | 快速标记删除(不阻塞操作),延迟清理释放空间 |
| 查询(Search) | 1. 查询阶段:- 协调节点广播请求到所有相关分片(主 / 副本)- 分片执行查询(基于倒排索引),返回 “文档 ID + 得分” 并排序- 协调节点汇总结果,再次排序筛选出前 N 个 ID2. 取回阶段:- 协调节点向对应分片请求完整文档数据- 汇总文档,返回给客户端 | 倒排索引、协调节点汇总、相关性得分(Score)、_source 筛选 | 快速匹配目标文档,返回准确且排序合理 的结果 |
Translog 刷盘机制 和 Segment Merge 触发条件
拆解 Translog 刷盘机制 和 Segment Merge 触发条件 的核心细节,帮你搞懂 ES 数据持久化与空间优化的底层逻辑。
一、Translog 刷盘机制:确保写入不丢数据
Translog 是 ES 用于 “防止内存数据丢失” 的关键组件,本质是一个追加写入的日志文件,其刷盘机制直接决定数据的持久化可靠性,核心逻辑可从 “触发时机” 和 “刷盘流程” 两方面看。
1. 核心作用
- 写入数据时,先写内存缓冲区(In-Memory Buffer),同时同步写 Translog(顺序写,性能高)。
- 若节点突然宕机,内存缓冲区数据会丢失,但 Translog 已落盘,重启后可通过 Translog 恢复数据,避免丢失。
2. 刷盘触发时机(3 种核心场景)
Translog 并非实时刷盘,而是通过 “定时 + 定量” 机制平衡性能与可靠性,具体触发条件如下:
| 触发类型 | 具体条件 | 配置参数(默认值) | 核心目的 |
|---|---|---|---|
| 定量触发 | Translog 文件大小达到阈值 | index.translog.flush_threshold_size: 512MB | 避免 Translog 文件过大,导致恢复时耗时过长 |
| 定时触发 | 距离上次刷盘时间达到间隔 | index.translog.flush_threshold_period: 30m | 即使数据量小,也能定期持久化,降低丢失风险 |
| 手动触发 | 调用 API 强制刷盘 | 执行 POST /索引名/_flush(或 _flushall 全局刷盘) | 运维场景(如节点重启前),确保所有数据落盘 |
3. 刷盘核心流程
- 触发刷盘后,ES 会先关闭当前 Translog 文件,新建一个新的 Translog 文件接收后续写入(避免刷盘阻塞新请求)。
- 将关闭的 Translog 文件强制刷到磁盘(调用操作系统
fsync命令,确保数据真正写入磁盘,而非停留在操作系统缓存)。 - 刷盘成功后,清空内存缓冲区中已写入 Segment 的数据(未触发 Refresh 的数据仍在内存),完成一次刷盘周期。
二、Segment Merge 触发条件:优化查询性能 + 释放空间
Segment 是 ES 存储数据的最小单元(不可变),写入时会生成大量小 Segment,过多小 Segment 会导致 “查询时需遍历所有小 Segment,性能下降”,且删除 / 更新标记的文档无法释放空间。Segment Merge 就是将 “多个小 Segment 合并为大 Segment” 的后台任务,触发条件分为 “自动触发” 和 “手动触发”。
1. 核心作用
- 提升查询性能:合并后 Segment 数量减少,查询时只需遍历少量大 Segment,减少 IO 开销。
- 释放磁盘空间:合并过程中,会过滤掉标记为 Tombstone(删除 / 更新旧文档)的记录,真正释放磁盘空间。
- 减少元数据开销:每个 Segment 都有独立的元数据,合并后元数据总量减少,降低内存占用。
2. 自动触发条件(4 种核心场景)
ES 会通过后台线程(MergeScheduler)自动检测并触发 Merge,核心判断逻辑基于 “Segment 数量 + 大小”:
| 触发场景 | 具体条件 | 配置参数(默认值) | 说明 |
|---|---|---|---|
| Segment 数量超标 | 同一分片下,大小小于 merge.policy.floor_segment 的小 Segment 数量超过阈值 | merge.policy.max_merge_at_once: 10(一次最多合并 10 个小 Segment)merge.policy.floor_segment: 2MB(小于 2MB 的视为 “小 Segment”) | 最常见的触发场景,避免小 Segment 堆积 |
| Segment 大小比例超标 | 当一个 Segment 的大小,是其相邻更大 Segment 大小的一定比例以上 | merge.policy.max_merged_segment: 5GB(合并后的大 Segment 最大不超过 5GB)merge.policy.segments_per_tier: 10(每层 Segment 数量不超过 10 个,超过则合并) | 防止出现 “大小差异过大的 Segment”,平衡查询效率 |
| 写入触发 | 每次 Refresh 生成新 Segment 后,会检查当前 Segment 数量,若超标则触发 Merge | 无单独参数,依赖上述 “数量 / 大小” 阈值 | 写入频繁时,小 Segment 生成快,Merge 也会更频繁 |
| 定时触发 | 后台 Merge 线程会定期(默认 1 秒)检查是否需要合并 | indices.memory.index_buffer_size: 10%(间接影响,内存缓冲满触发 Refresh 后,进而可能触发 Merge) | 兜底机制,确保即使写入少,也能清理过期数据 |
3. 手动触发条件(运维场景)
当自动 Merge 不及时(如大量删除后需快速释放空间),可通过 API 手动触发 Merge:
- 合并指定索引:
POST /索引名/_forcemerge - 合并所有索引:
POST /_forcemerge - 关键参数:
max_num_segments=1(可选,强制将分片的 Segment 合并为 1 个大 Segment,适合只读索引场景,但会占用大量 IO,需避开业务高峰)
4. 注意事项
- Merge 是 IO 密集型任务:合并大 Segment 时会占用大量磁盘 IO 和 CPU,可能影响查询性能。ES 默认会限制 Merge 的 IO 速率(通过
indices.store.throttle.max_bytes_per_sec配置,默认 20MB/s),避免过度抢占资源。 - 只读索引建议手动 Merge:对于日志等只读索引(写入后不再更新),可在写入完成后手动触发
_forcemerge max_num_segments=1,最大化查询性能。
Segment为什么分层
在 Elasticsearch 中,“每层 segment” 是指基于TieredMergePolicy(分层合并策略)下,按照段的大小划分的不同 “层级”。
Elasticsearch 使用 Lucene 作为底层索引引擎,Lucene 采用段化存储来管理索引,段是不可变的磁盘索引文件集合,包含倒排索引、正向索引等数据结构。随着数据不断写入,会不断生成新的段,而过多的小段会影响查询性能,因为每个搜索请求都需要访问多个段。为了优化查询性能和磁盘占用,Elasticsearch 会在后台定期执行合并操作,将多个小段合并成较大的段。
TieredMergePolicy会将索引中的段分成若干层,每个层次内的段符合某些大小范围。例如,较小的段可能处于较低的层次,随着不断合并,段的大小逐渐增大,会晋升到更高的层次。每个层次都有一个最大段数限制,由index.merge.policy.segments_per_tier参数控制,默认值为 10。当某个层次中的段数量超过这个阈值时,就会触发合并操作,将这些段合并为一个更大的段。
之所以引入 “层” 的概念,主要是为了更好地管理段的合并过程。通过分层,可以避免过度合并小段,同时确保合并后的段大小合理,能够有效地提升查询性能。如果没有分层的概念,可能会导致频繁地对所有段进行合并,消耗大量的系统资源,或者无法及时将小段合并成大段,影响查询性能。
