ElasticSearch 集群原理与分片管理深度解析

ElasticSearch 集群原理与分片管理深度解析

ElasticSearch 是全文检索领域的王者。本文深入剖析 ES 的分布式架构、倒排索引原理、分片分配机制、副本同步、写入流程,以及性能优化与运维实践。

一、ES 集群架构

1.1 ES 集群组件

ES 集群
节点 3
Node
分片 2
副本 1
节点 2
Node
分片 1
副本 0
节点 1 (Master Eligible)
Node
分片 0
副本 1

1.2 节点角色

节点类型
Master Node
管理集群元数据
创建/删除索引
分片分配
Data Node
存储数据
执行查询
CRUD 操作
Coordinating Node
接收请求
转发到 Data Node
聚合结果
Ingest Node
数据预处理
Pipeline
数据转换

1.3 分片结构

分片类型
Primary Shard
主分片,处理写入
数量创建时固定
可读可写
Replica Shard
副本分片,数据同步
提供高可用
只读


二、倒排索引

2.1 倒排索引原理

倒排索引
关键词1 -> [文档1, 文档2]
关键词2 -> [文档1]
关键词3 -> [文档1]
关键词4 -> [文档2]
正排索引
文档 1 -> [关键词1, 关键词2, 关键词3]
文档 2 -> [关键词1, 关键词4]

2.2 倒排索引结构

存储格式
FST: Finite State Transducer
前缀压缩
快速定位
索引结构
Term Dictionary
Terms Index (FST)
Posting List
Positions

2.3 分词器

分词器类型
Standard Analyzer
英文分词
IK Analyzer
中文分词
自定义分词
组合使用
分词流程
I love Beijing
Character Filter
Tokenizer
Token Filter

I, love, beijing


三、写入流程

3.1 写入流程详解

Replica Primary Node Client Replica Primary Node Client PUT /index/doc/1 写入请求 写入内存 Buffer 写入 Translog 同步到副本 ACK ACK 成功响应

3.2 刷新机制

刷新时机
默认每秒 Refresh
可配置 refresh_interval
立即刷新: ?refresh=true
刷新流程
内存 Buffer
Refresh
生成新 Segment
可被搜索


四、分片分配

4.1 分片分配策略

分配因素
节点负载
磁盘使用率
分片数量均衡
故障检测
自动故障转移
副本自动提升为主分片

4.2 分片重分配

重分配场景
节点加入/离开
分片重新平衡
负载均衡


五、性能优化

5.1 写入优化

写入优化
批量写入
使用 Bulk API
控制批量大小
副本优化
写入时副本数设为 0
数据写入完成后开启副本
分片优化
合理设置分片数
避免小分片

5.2 查询优化

查询优化
使用 Filter
Filter 不计算相关性
Filter 可缓存
避免通配符
*test* 性能差
前缀查询更好
分页优化
使用 Search After
避免深度分页


六、面试高频问题

6.1 ES 写入原理?

复制代码
写入流程:
1. 请求发送到协调节点
2. 协调节点路由到主分片
3. 主分片写入内存 Buffer 和 Translog
4. 主分片同步到副本
5. 返回成功响应

刷新:
- 内存 Buffer 每秒 Refresh 生成 Segment
- Segment 可被搜索

持久化:
- Translog 确保数据不丢失
- Segment 定期 flush 到磁盘

6.2 分片和副本的关系?

复制代码
分片:
- 主分片数量创建时固定
- 负责读写操作
- 数量影响并行度和数据分布

副本:
- 主分片的副本
- 提供高可用
- 处理读请求
- 数量可动态调整

6.3 倒排索引原理?

复制代码
结构:
1. Term Dictionary: 所有词项
2. Terms Index: 前缀树 (FST)
3. Posting List: 文档列表
4. Positions: 词项位置

查询流程:
1. 解析查询词
2. 在 Term Dictionary 查找
3. 定位 Term Index
4. 获取 Posting List
5. 合并结果

八、Segment 内部结构与 Lucene 底层原理

8.1 Lucene Segment 架构

ElasticSearch 基于 Lucene 构建,每个分片包含多个 Segment(段),每个 Segment 是一个完整的倒排索引。
Index 元数据
分片 Shard
Segments 目录
_0.fnm

字段定义
_0.tip

Terms Index
_0.tim

Terms Dictionary
_0.doc

Posting List
_0.pos

Positions
_0.nrm

Norms
_0.cfs

复合文件
segments_N

段信息
*.si

段元信息
write.lock

写锁

8.2 Segment 文件详解

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        Segment 文件结构                          │
├─────────────────────────────────────────────────────────────────┤
│  文件后缀      │  内容说明                                       │
├─────────────────────────────────────────────────────────────────┤
│  _0.fnm       │  Fields 信息,包含字段名、字段类型、存储选项      │
│  _0.tip       │  Terms Index,FST 前缀树,用于快速定位 Terms      │
│  _0.tim       │  Terms Dictionary,BKD 树或跳表,存储所有词项     │
│  _0.doc       │  DocID -> DocValues 的映射                       │
│  _0.pos       │  词项位置信息,用于 phrase 查询                   │
│  _0.pay       │  Payload 和 offset 信息                          │
│  _0.nrm       │  Norms 向量,字段长度归一化因子                   │
│  _0.dvd/_0.dvm│  DocValues,列式存储用于排序和聚合                │
│  segments_N   │  段元数据,包含所有段的 commit 信息               │
└─────────────────────────────────────────────────────────────────┘

8.3 FST(Finite State Transducer)原理

FST 是 Lucene 4.0+ 引入的紧凑前缀树结构,用于高效存储和查询 Terms:
FST特性
共享前缀: map, man, men
最小化有向无环图
空间效率: 约50%压缩
O(len(term)) 查询复杂度
FST结构
m
a
p
-t
r
e
e

java 复制代码
// FST 在 Lucene 中的简化结构
public class FST<T> {
    // 节点结构
    static class FSTNode {
        long nodeId;           // 节点编号
        byte[] bytes;          // 弧上的标签
        T output;             // 输出值(可权重)
        int nextArc;           // 下一条弧的偏移
    }
    
    FSTNode[] nodes;           // 所有节点
    BytesStore bytes;          // 弧的标签数据
    
    // 前缀共享示例: "map", "man", "men" -> [m]->[a]->[p]
    //                                   ->[n]->[e]->[n]
}

8.4 DocValues 列式存储

DocValues 是 ES 用于排序、聚合、脚本的列式存储结构:
压缩算法
Roaring Bitmap
GCD 压缩
差分编码
DocValues类型
NUMERIC

单值数值
BINARY

字节数据
SORTED

单值字符串
SORTED_SET

多值字符串
GEO_POINT

地理坐标
DocValues写入
Doc_values写入
按docId排序
按列分组
使用Block compression

java 复制代码
// DocValues 读取流程
public class DocValuesReader {
    
    public NumericDocValues getNumeric(LeafReader reader, String field) {
        // 1. 获取字段的 DocValues 元数据
        DocValuesMetaData meta = reader.getDocValuesMeta(field);
        
        // 2. 打开对应的 .dvd 文件
        long ramBytesUsed = meta.ramBytesUsed();
        
        // 3. 创建迭代器
        NumericDocValues values = meta.getNumeric(values);
        
        // 4. 按 docId 随机访问
        for (int docId = 0; docId < maxDoc; docId++) {
            long value = values.get(docId);
        }
        return values;
    }
}

8.5 联合索引查询与 BitSet

当多个条件组合查询时,Lucene 使用 BitSet 进行高效的位运算:
AND运算
BitSet
查询条件
status:active
type:order
status:active

1,0,1,1,0,1,0,1

type:order

1,1,0,0,0,1,1,0

AND 结果

1,0,0,0,0,1,0,0

java 复制代码
// Lucene 中 BitSet 的实现
public class BitSetIterator implements DocIdSetIterator {
    
    // Roaring Bitmap 结构
    static class RoaringBitmap {
        RoaringArray highLowContainer;  // 高16位 -> Container
        
        // Container 类型
        abstract class Container {
            // 3种容器:Bitmap(65536位)、Array(稀疏)、Run(行程)
        }
        
        // 按位与运算
        public RoaringBitmap and(RoaringBitmap other) {
            RoaringBitmap result = new RoaringBitmap();
            for (int i = 0; i < size; i++) {
                Container c1 = getContainer(i);
                Container c2 = other.getContainer(i);
                result.setContainer(i, c1.and(c2));
            }
            return result;
        }
    }
}

九、Translog 详细机制与数据恢复

9.1 Translog 结构

Translog 是保证数据持久性的关键机制,记录所有未刷新的操作:
Translog文件
translog-1.dat
translog-2.dat
translog-3.dat
Translog生命周期
写入 Translog
内存 Buffer
Refresh
Segment 持久化
清空 Translog

9.2 Translog 格式详解

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    Translog Block 结构 (512 bytes)               │
├─────────────────────────────────────────────────────────────────┤
│  Header (8 bytes)                                                │
│  ├─ 1 bytes: 操作类型 (1=Index, 2=Delete, 3=Update)           │
│  ├─ 3 bytes: Sequence Number 高位                               │
│  └─ 4 bytes: Data Length                                        │
├─────────────────────────────────────────────────────────────────┤
│  Payload (可变长度)                                              │
│  ├─ Primary Term (8 bytes)                                       │
│  ├─ Sequence Number (8 bytes)                                     │
│  ├─ 2 bytes: Version Type                                        │
│  └─ 文档数据 (JSON bytes)                                        │
├─────────────────────────────────────────────────────────────────┤
│  Footer (2 bytes): CheckSum                                      │
└─────────────────────────────────────────────────────────────────┘

9.3 两阶段提交流程

Translog Replica Primary Node Client Translog Replica Primary Node Client Phase 1: 写内存 Phase 2: 提交 (fsync) PUT /index/doc/1 Index Request 写入 Buffer 写入 Translog (不 fsync) 复制到副本 ACK (不 fsync) ACK 成功 (NRT) Translog fsync 创建 commit point Commit ACK

9.4 数据恢复流程

恢复类型
Peer Recovery
主副分片间同步
基于 sequence number
Snapshot Recovery
从备份恢复
如快照库
恢复流程
主分片故障
新主分片选举
读取 Global Checkpoint
从 Translog 重放

java 复制代码
// Translog 恢复核心逻辑
public class TranslogRecoveryPerformer {
    
    public void recoverFromTranslog(
            TranslogRecoveryHandler handler,
            long minSeqNo,
            long maxSeqNo) {
        
        // 1. 打开 Translog
        try (TranslogReader reader = translog.openReader()) {
            
            // 2. 按 sequence number 遍历
            Translog.Operation operation;
            while ((operation = reader.next()) != null) {
                
                // 3. 检查是否在恢复范围内
                if (operation.seqNo >= minSeqNo 
                    && operation.seqNo <= maxSeqNo) {
                    
                    // 4. 重放操作
                    switch (operation.opType) {
                        case INDEX:
                            handler.indexOperation(operation);
                            break;
                        case DELETE:
                            handler.deleteOperation(operation);
                            break;
                    }
                }
            }
        }
    }
}

十、集群选主与故障检测机制

10.1 ES 选主机制(Zen Discovery)

ElasticSearch 使用自定义的 Zen Discovery 协议进行选主:
投票机制
每个节点一票
多数派 (N/2+1)
节点 ID 最小的胜出
选主流程
Yes
No
节点启动
发送 Ping 请求
等待 master_ping_timeout
收到多数回复?
成为 Master
重新 Ping

java 复制代码
// Zen Discovery 选主简化逻辑
public class ZenMaster选举 {
    
    // 节点配置
    int minimumMasterNodes = (clusterNodes.size() / 2) + 1;
    
    public MasterCandidate electMaster() {
        List<ZenPing.PingResponse> responses = 
            zenPing.pingAndWait(timeValue);
        
        // 过滤有效的 master 节点候选
        List<MasterCandidate> masterCandidates = responses.stream()
            .filter(r -> r.hasMasterNode())
            .sorted(Comparator.comparing(r -> r.nodeId))
            .collect(toList());
        
        // 达到法定人数
        if (masterCandidates.size() >= minimumMasterNodes) {
            return masterCandidates.get(0);  // 选择最小的节点ID
        }
        
        return null;  // 无法选主
    }
}

10.2 故障检测(Fault Detection)

节点检测 Master
Node -> Master
Master Fault Detection
无法连接
触发重新选主
节点间检测
3次
Master -> Node
Nodes Fault Detection
Schedule Ping
超时?
标记为离线
触发分片重分配

10.3 分片分配策略

分配过滤
filter: 属性过滤
include/exclude
自定义分配策略
分配决策树
Primary
Replica
No
Yes
新写入请求
有可用分片?
分配给节点
同节点优先?
选择负载最低节点
分配到不同节点

java 复制代码
// 分片分配决策
public class ShardAllocator {
    
    public ShardRouting allocateShard(
            ShardId shardId,
            IndexMetadata indexMetadata,
            java.util.List<MutableShardRouting> candidates) {
        
        // 1. 应用分配过滤器
        candidates = candidates.stream()
            .filter(r -> allocationFilter.match(r.currentNodeId()))
            .collect(toList());
        
        // 2. 负载均衡评分
        List<AllocateAndScore> scored = candidates.stream()
            .map(node -> new AllocateAndScore(
                node,
                calculateScore(node, shardId)
            ))
            .sorted(Comparator.comparing(s -> -s.score))
            .collect(toList());
        
        // 3. 返回最优节点
        return scored.get(0).routing;
    }
    
    // 评分因子
    private double calculateScore(AllocationNode node, ShardId shard) {
        double diskUsage = 1.0 - (node.diskUsage / node.threshold);
        double shardCount = 1.0 / (node.shardCount + 1);
        double load = 1.0 / (node.load + 1);
        return diskUsage * 0.4 + shardCount * 0.3 + load * 0.3;
    }
}

十一、段合并策略与优化

11.1 段合并流程

合并策略
TieredMergePolicy
默认策略
归并大小相近的段
LogByteSizeMergePolicy
按字节大小归并
LogDocMergePolicy
按文档数归并
合并执行
读取多个段
按 DocID 排序
写入新 Segment
更新 Segment 元数据
删除旧段
合并触发
Yes
Refresh 生成新 Segment
检查合并策略
满足合并条件?
选择待合并段

11.2 TieredMergePolicy 详解

java 复制代码
// TieredMergePolicy 核心逻辑
public class TieredMergePolicy extends MergePolicy {
    
    // 配置参数
    double mergeFactor = 10;          // 归并因子
    long maxMergedSegmentBytes = 5GB;  // 最大合并段大小
    double segmentsPerTier = 10;       // 每层段数
    
    public MergePolicy.MergeSpecification findForcedMerges(
            SegmentInfos infos) {
        
        // 1. 按大小分层
        Map<Integer, List<SegmentCommitInfo>> tiers = 
            groupBySize(infos);
        
        // 2. 选择需要合并的段
        List<SegmentCommitInfo> toMerge = new ArrayList<>();
        for (int tier = 0; tier < tiers.size(); tier++) {
            List<SegmentCommitInfo> segments = tiers.get(tier);
            
            // 3. 达到合并条件时触发
            while (segments.size() > segmentsPerTier) {
                // 选择最小的一组段进行合并
                toMerge.addAll(
                    segments.subList(0, (int)mergeFactor)
                );
                segments = segments.subList((int)mergeFactor, 
                                            segments.size());
            }
        }
        
        // 4. 返回合并规格
        return new MergeSpecification(toMerge);
    }
}

11.3 合并策略配置

json 复制代码
// 索引级别配置
{
  "settings": {
    "index.merge.policy.type": "tiered",
    "index.merge.policy.max_merged_segment": "5gb",
    "index.merge.policy.segments_per_tier": 10,
    "index.merge.policy.expunge_deletes_allowed": true,
    
    "index.store.preload": ["nrm", "dvd", "tim"]
  }
}

11.4 合并对性能的影响

优化建议
降低 refresh_interval
减少段数量
使用 bulk 批量写入
按时间滚动索引
合并资源消耗
CPU: 读取/压缩/写入
磁盘 IO: 顺序写
内存: 缓冲区


12.1 深度分页问题

计算复杂度
分片数: P = 5
每分片返回: from+size = 10010
总排序: 10010 * 5 = 50050
取中间丢弃: 50040 条
传统分页问题
from=10000, size=10
每个分片返回 10010 条
协调节点排序 10010*N 条
只取第 10000-10010 条
资源浪费严重

Shard Coordinator Client Shard Coordinator Client 第一页 第二页 (使用 search_after) GET /search?size=10&sort=date:asc,id:asc 查询前 11 条 返回 11 条数据 返回 10 条 + sort_values GET /search?size=10&search_after=[2024-01-15,id_123] 查找 sort > [2024-01-15, id_123] 返回 11 条 返回 10 条 + 新 sort_values

json 复制代码
// Search After 实现
{
  "query": { "match": { "title": "elasticsearch" } },
  "sort": [
    { "date": "desc" },
    { "_id": "asc" }
  ],
  "size": 10,
  
  // 第一页: 不传 search_after
  // 第二页及以后: 传入上一页最后一条的 sort 值
  "search_after": ["2024-01-15T10:30:00.000Z", "doc_123"]
}

12.3 PointInTime(PIT)

语义保证
全局搜索视图
不包含新数据
分片不变化
PIT生命周期
创建 PIT
PIT ID
跨搜索使用
定时清理

bash 复制代码
# PIT 使用示例
# 1. 创建 PIT
curl -X POST "/my_index/_pit?keep_alive=1m"

# 响应: {"id": "PIT_ID_xxx"}

# 2. 使用 PIT 搜索
curl -X POST "/_search" -d '{
  "size": 1000,
  "query": { "match": { "content": "elasticsearch" } },
  "pit": {
    "id": "PIT_ID_xxx",
    "keep_alive": "1m"
  },
  "sort": [{ "date": "asc" }, { "_id": "asc" }]
}'

# 3. 使用 search_after 翻页
curl -X POST "/_search" -d '{
  "size": 1000,
  "query": { "match": { "content": "elasticsearch" } },
  "pit": { "id": "PIT_ID_xxx", "keep_alive": "1m" },
  "search_after": ["2024-01-15", "doc_123"],
  "sort": [{ "date": "asc" }, { "_id": "asc" }]
}'

七、总结

7.1 核心要点

复制代码
ES 核心要点:

1. 分布式架构
   - 分片副本机制
   - 自动故障转移

2. 倒排索引
   - Term Dictionary
   - Posting List

3. 写入流程
   - Buffer + Translog
   - Refresh 生成 Segment

4. 分片管理
   - 自动分配
   - 负载均衡

7.2 最佳实践

复制代码
最佳实践:

1. 合理分片
   - 避免过多小分片
   - 分片大小 20-50GB

2. 副本策略
   - 高可用至少 1 副本
   - 写入性能敏感可先关闭副本

3. 映射设计
   - 选择合适的字段类型
   - 适当使用分词器
相关推荐
数说故事1 小时前
数说故事消费者洞察:全域大数据解析电解质饮料日常水替新趋势
大数据·数说故事·消费者洞察
TDengine (老段)1 小时前
TDengine 整体架构全景 — 深度解析
大数据·数据库·物联网·架构·时序数据库·tdengine·涛思数据
m0_716255001 小时前
二、Hadoop 面试必背 | 三、Hive 面试必背
大数据·hadoop·面试
zz0723201 小时前
Elasticsearch
大数据·elasticsearch·搜索引擎
薪火铺子1 小时前
ElasticSearch 聚合查询与性能优化实战
大数据·elasticsearch·性能优化
CableTech_SQH2 小时前
F5G全光网络二层扁平架构技术拆解:OLT+ODN+ONU全链路原理详解
大数据·网络·5g·信息与通信
2601_949936962 小时前
2026年职业资格证书趋势分析:专业化与数字化融合视角
大数据
团象科技2 小时前
当出海合规压力持续上升时,多云服务容易忽略哪些细节
大数据·微服务·架构
AC赳赳老秦3 小时前
故障自愈实战:用 OpenClaw 实现服务器日志自动化分析、根因定位、解决方案自动生成
大数据·运维·服务器·自动化·github·deepseek·openclaw