Appearance
Elasticsearch 核心机制深度学习笔记
一、索引创建后可修改与不可修改的内容
1.1 核心原则
任何会影响已有数据底层存储结构的配置,创建后均不可修改。
因为 Lucene 的 Segment 一旦写入就是不可变的(immutable),修改结构会导致新旧数据不一致。
1.2 速查总览
| 配置项 | 能否修改 | 替代方案 |
|---|---|---|
分片数 number_of_shards | ❌ 不可修改 | Reindex / Split / 别名切换 |
| 字段类型(如 text → keyword) | ❌ 不可修改 | Reindex 新索引 |
| 字段名 | ❌ 不可修改 | 新增字段 + copy_to / Reindex |
索引时分词器 analyzer | ❌ 不可修改 | Reindex(重新分词) |
副本数 number_of_replicas | ✅ 可修改 | — |
| 新增字段 | ✅ 可修改 | — |
| 添加多字段 multi-fields | ✅ 可修改 | — |
查询分词器 search_analyzer | ✅ 可修改 | — |
refresh_interval | ✅ 可修改 | — |
| Translog 配置 | ✅ 可修改 | — |
| 别名 Alias | ✅ 可修改 | — |
| 自定义分析器(新增) | ✅ 可修改 | — |
1.3 不可修改项详解
分片数(number_of_shards)
创建后不可直接修改,因为路由公式 hash(_routing) % primary_shards 依赖分片数。
替代方案有三种:
- Reindex:创建新索引(新分片数),通过
POST /_reindex迁移数据,最后用别名切换。 - Split:分片数只能扩到整数倍(如 3→6),需先锁写入。
- Shrink:减少分片数(如 6→3),目标必须是源分片的因子。
字段类型 / 字段名 / 索引时分词器
这三者决定了数据在倒排索引中的存储方式。已有 Segment 中的数据已经按旧配置分词和存储,无法原地修改。
💡 查询分词器 search_analyzer 可以修改
analyzer(索引时)不可改,但search_analyzer(查询时)可以改。前提是两种分词器的 token 有交集,否则旧数据搜不到。
1.4 Reindex 迁移示例
json
// 1. 创建新索引
PUT /my_index_v2
{
"settings": { "number_of_shards": 6 },
"mappings": { ... }
}
// 2. 迁移数据
POST /_reindex
{
"source": { "index": "my_index_v1" },
"dest": { "index": "my_index_v2" }
}
// 3. 别名无缝切换
POST /_aliases
{
"actions": [
{ "remove": { "index": "my_index_v1", "alias": "my_alias" } },
{ "add": { "index": "my_index_v2", "alias": "my_alias" } }
]
}二、索引模板(Index Template)
2.1 为什么需要索引模板
日志、监控等时序场景下,索引按天/按大小轮转,每天都要新建索引。如果没有模板,每次都要手动配置 settings、mappings,极易出错。
新索引名匹配 log-* → 自动应用模板 → settings / mappings / aliases 全部就绪2.2 两种模板类型
| 类型 | API | 说明 |
|---|---|---|
| Legacy Template(ES 7.8 之前) | PUT /_template/xxx | 单一模板包含所有配置 |
| Composable Template(ES 7.8+,推荐) | PUT /_index_template/xxx | 模块化设计,多个 Component Template 可组合复用 |
2.3 Composable Template 实战
分为两步:先创建可复用的组件模板,再创建索引模板引用它们。
json
// 组件1:通用 Settings
PUT /_component_template/base_settings
{
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "30s",
"codec": "best_compression"
}
}
}
// 组件2:日志 Mappings
PUT /_component_template/log_mappings
{
"template": {
"mappings": {
"properties": {
"timestamp": { "type": "date" },
"message": { "type": "text" },
"level": { "type": "keyword" }
}
}
}
}json
// 索引模板:引用组件
PUT /_index_template/log_template
{
"index_patterns": ["log-*"],
"priority": 200,
"composed_of": ["base_settings", "log_mappings"],
"template": {
"aliases": {
"logs-search": {}
}
}
}2.4 多模板优先级与匹配
多个模板匹配同一索引名时,priority 值越大优先级越高。相同 key 的配置,高优先级模板覆盖低优先级。
2.5 模拟测试
上线前可以用 POST /_index_template/_simulate 预览模板应用效果,避免线上事故。
三、Rollover 自动切分索引
3.1 Rollover 是什么
根据预设条件自动创建新索引,并将写入流量切换到新索引。防止单个索引无限膨胀。
log-000001 (写满 50GB)
│
│ ── rollover 触发 ──→
│
log-000001 (只读) log-000002 (新数据写入这里)
│
│ ── rollover 触发 ──→
│
log-000001 (只读) log-000002 (只读) log-000003 (新数据)3.2 三种触发条件
| 条件 | 配置 | 说明 |
|---|---|---|
| 分片大小 | max_primary_shard_size: "50gb" | 推荐,最直观 |
| 索引年龄 | max_age: "1d" | 按时间轮转 |
| 文档数量 | max_docs: 100000000 | ES 7.x 已不推荐 |
3.3 自动 Rollover 的实现机制
通过 ILM(Index Lifecycle Management)+ 别名配合实现自动 rollover。
json
// 索引模板中关联 ILM 策略和 rollover 别名
PUT /_index_template/log_template
{
"index_patterns": ["log-*"],
"template": {
"settings": {
"index.lifecycle.name": "log_policy", // ← ILM 策略
"index.lifecycle.rollover_alias": "logs-write" // ← rollover 别名
},
"aliases": {
"logs-search": {} // ← 每个新索引自动拥有查询别名
}
}
}
// 创建引导索引(必须手动创建第一个)
PUT /log-000001
{
"aliases": {
"logs-write": { "is_write_index": true },
"logs-search": {}
}
}3.4 别名的角色
应用层始终只认一个名字:logs-write
┌───────────────────┐
│ logs-write │ ← 写入目标(永远不变)
│ (is_write_index) │
└────────┬──────────┘
│
┌──────────────┼──────────────┐
↓ ↓ ↓
log-000001 log-000002 log-000003
(只读) (只读) (当前写入)
查询时用 logs-search(包含所有索引)
┌───────────────────┐
│ logs-search │ ← 查询目标(永远不变)
└────────┬──────────┘
│
┌──────────────┼──────────────┐
↓ ↓ ↓
log-000001 log-000002 log-000003
(全量可查) (全量可查) (全量可查)3.5 ⚠️ logs-search 不会自动扩展
Rollover 只自动管理
rollover_alias指定的写别名(logs-write)。查询别名(logs-search)必须在索引模板的aliases中定义,新索引才会自动拥有。如果模板中没定义,新索引不会拥有 logs-search 别名。
3.6 is_write_index 的含义
别名可以指向多个索引,但只有一个能被标记为 is_write_index: true。
logs-write 别名:
log-000001 → is_write_index: false (只读,历史数据)
log-000002 → is_write_index: false (只读,历史数据)
log-000003 → is_write_index: true (当前写入目标)
写入请求:POST /logs-write/_doc → 自动路由到 log-000003
查询请求:GET /logs-write/_search → 搜索所有三个索引Rollover 时 ES 自动做的切换:
Before rollover:
log-000001 → is_write_index: true
After rollover:
log-000001 → is_write_index: false ← 自动改为只读
log-000002 → is_write_index: true ← 自动承接写入四、mmap 机制与 ES Segment
4.1 什么是 mmap
mmap(Memory-Mapped Files)是操作系统提供的系统调用,将磁盘文件直接映射到进程的虚拟地址空间。程序像访问内存一样读写文件,OS 的虚拟内存管理器自动处理磁盘 I/O。
4.2 mmap 相比传统 read/write 的优势
传统 read/write:
磁盘 → 内核 Page Cache → 用户缓冲区 (2次拷贝,2次系统调用)
mmap:
磁盘 → 内核 Page Cache ⇄ 用户虚拟地址空间 (0次额外拷贝)
页表直接映射同一份物理内存,用户直接读| 维度 | 传统 read/write | mmap |
|---|---|---|
| 数据拷贝 | 2 次(内核→用户) | 0 次(直接映射) |
| 随机读性能 | 差(每次 seek + read) | 优(直接指针偏移) |
| 缓存管理 | 应用需自行管理 | OS 自动按 LRU 管理 |
| 内存占用 | 精确控制 | 按需加载(Demand Paging) |
| 适用场景 | 顺序读 / 小文件 | 大文件随机读 |
4.3 ES/Lucene 如何使用 mmap
Lucene 底层通过 MMapDirectory 实现 mmap 读取。写入用普通 I/O,读取用 mmap——这是 Lucene 的设计哲学。
以下 Segment 文件全部通过 mmap 读取:
.tim— 词项字典(FST 结构).tip— 词项索引(快速定位词项).doc— 倒排列表(文档 ID 列表).dvd— doc values(排序/聚合用).pos— 词项位置信息.pay— 载荷/偏移量.nvd— 归一化因子.dim— 点数据(范围查询)
4.4 mmap 为什么特别适合 ES
ES 搜索的 I/O 模式:
✅ 大文件 → 按需加载,不全量读入内存
✅ 随机读 → 直接指针偏移,无 seek/read 系统调用
✅ 只读 → Segment 不可变,不需要写回
✅ 热点集中 → Page Cache 自动将热数据驻留内存4.5 ES 中 mmap 的相关配置
bash
# 系统层面:ES 官方建议 max_map_count 至少 262144
sysctl -w vm.max_map_count=262144
# 永久生效
echo "vm.max_map_count=262144" >> /etc/sysctl.conf
sysctl -pjson
// 索引层面:对关键文件类型做预加载(解决冷启动)
PUT /my_index/_settings
{
"index.store.preload": ["nvd", "dvd", "tim", "tip"]
}4.6 mmap 的潜在风险与应对
| 风险 | 表现 | 应对方案 |
|---|---|---|
| Page Cache 被挤占 | 高查询量时物理内存紧张 | 增加机器内存;限制缓存大小 |
| Page Fault 风暴 | 冷索引首次查询延迟飙高 | 预热:store.preload 配置 |
| 32 位系统不支持 | 虚拟地址空间不足 | 必须使用 64 位系统 |
五、Flush 机制与触发条件
5.1 Flush 做了什么
Flush 不是"把数据从内存写到磁盘"。它做了两件事:
- 将内存中的 Segment 持久化(Lucene Commit)
- 清空/截断 Translog(数据已安全落盘,translog 不再需要)
5.2 完整数据生命周期
写入 Index Buffer + Translog
│
│ ① Refresh(默认每 1s)
▼
Segment in OS Cache(可搜索,未持久化)
│
│ ② Flush(translog 满 512MB 时触发)
▼
Segment on Disk(已持久化)+ Translog 清理5.3 Flush 的 5 种触发条件
| # | 触发条件 | 配置项 | 默认值 |
|---|---|---|---|
| 1 | Translog 大小达到阈值(最主要) | translog.flush_threshold_size | 512MB |
| 2 | Translog 操作数达到阈值 | translog.flush_threshold_ops | 50万(7.x 已废弃) |
| 3 | 长时间无写入的定期 Flush | translog.flush_threshold_period | 12h |
| 4 | 手动触发 | POST /my_index/_flush | — |
| 5 | 节点关闭/重启时 | 自动 | — |
5.4 Flush vs Refresh 彻底区分
| Refresh | Flush | |
|---|---|---|
| 做什么 | Buffer → 新 Segment(OS Cache) | Segment → 磁盘持久化 + 清理 translog |
| 数据可搜索? | ✅ 是 | ✅ 是(早已可以) |
| 数据持久化? | ❌ 否(仅 OS Cache) | ✅ 是 |
| Translog 变化 | 无 | 被清理/截断 |
| 触发频率 | 高(默认每 1s) | 低(translog 满时) |
| 目的 | 搜索可见性 | 数据安全性 |
5.5 Translog 写入机制
| 模式 | 行为 | 安全性 | 性能 |
|---|---|---|---|
request(默认) | 每次写请求后立即 fsync | 不丢数据 | 较慢 |
async | 每 5s fsync 一次 | 最多丢 5s 数据 | 更快 |
json
// 切换为异步模式
PUT /my_index/_settings
{
"index.translog.durability": "async",
"index.translog.sync_interval": "5s"
}六、别名查询的性能问题
6.1 问题分析
① 查询扇出开销巨大
logs-search 指向 90 个索引,每索引 3 个分片:
请求 → 协调节点 → 广播到 270 个分片
→ 每个分片独立查询、排序
→ 270 份结果返回协调节点
→ 合并排序 → 返回 top N
时间 = 最慢分片耗时 + 网络传输 + 协调节点合并耗时② 无效查询(索引剪枝缺失)
查"最近 1 小时"的数据,但 logs-search 包含 90 天的索引。其中 89 个索引扫描后返回 0 条结果,只有当天索引有数据。99% 的查询可能是浪费。
③ 堆内存压力
270 个分片 × 每个分片返回 top 100 的结果
= 协调节点需要同时处理 27000 份排序信息
聚合场景更恐怖:
270 个分片 × 每个分片返回 N 个 bucket = 可能 OOM6.2 解决方案(按优先级排序)
方案一:查询时按时间范围指定索引(最简单有效)
json
// 只查近 3 天,而非全部 90 天
GET /log-2025.05.19,log-2025.05.20,log-2025.05.21/_search
{
"query": {
"bool": {
"must": [
{ "match": { "message": "错误" } },
{ "range": { "@timestamp": { "gte": "now-3d" } } }
]
}
}
}方案二:分层别名策略
| 别名 | 覆盖范围 | 用途 |
|---|---|---|
logs-write | 当前写入索引 | 写入专用 |
logs-recent | 近 3 天(hot) | 实时查询 / 告警 |
logs-weekly | 近 7 天 | 仪表盘 / 报表 |
logs-monthly | 近 30 天 | 趋势分析 |
logs-all | 全部(慎用) | 全文检索 / 审计 |
通过 ILM 策略在不同阶段添加/移除别名,实现自动分层:
json
PUT /_ilm/policy/logs_policy
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": { "max_primary_shard_size": "50gb" }
}
},
"warm": {
"min_age": "3d",
"actions": {
"shrink": { "number_of_shards": 1 },
"forcemerge": { "max_num_segments": 1 },
"aliases": {
"add": { "alias": "logs-weekly", "is_write_index": false },
"remove": ["logs-recent"]
}
}
},
"cold": {
"min_age": "30d",
"actions": {
"freeze": {},
"aliases": {
"add": { "alias": "logs-monthly" },
"remove": ["logs-weekly"]
}
}
}
}
}
}方案三:ILM shrink + forcemerge
Warm 阶段将 3 分片 shrink 为 1 分片 + forcemerge 合并 segment,活跃分片数减少 3 倍。
方案四:freeze 冻结旧索引
冷数据冻结后不参与常规查询,需要时显式指定 ignore_throttled=false。
6.3 各方案效果对比
90 天数据,应用分层策略后:
别名 索引数 每索引分片数 活跃分片总数
logs-recent 3个 3 9 ← 最快
logs-weekly 7个 1(已shrink) 7 ← 快
logs-monthly 30个 1(已冻结) 0 ← 不参与查询
logs-all 90个 1 90 ← 慢,慎用七、索引数据量与分片规划
7.1 核心原则
单个分片大小:10GB ~ 50GB(推荐甜蜜区间)
分片数 = 数据总量 ÷ 单个分片目标大小
过小(< 5GB) 甜蜜区间 过大(> 100GB)
██████░░░░░░░ ████████████ █████████████
分片太多 10GB ~ 50GB 分片太大
调度开销大 最佳性能 查询变慢
元数据膨胀 均衡最优 恢复时间长7.2 场景速查表
| 预估索引大小 | 推荐主分片数 | 单分片大小 | 场景 |
|---|---|---|---|
| < 1GB | 1 | < 1GB | 配置索引、小字典表 |
| 1-10GB | 1~2 | 5-10GB | 小型业务索引 |
| 10-50GB | 2~5 | 10-25GB | 中型业务索引 |
| 50-200GB | 5~10 | 20-40GB | 大型业务索引 |
| 200GB-1TB | 10~20 | 25-50GB | 日志 / 监控索引 |
| > 1TB | 20+ 或拆索引 | 25-50GB | 超大规模,按时间拆分 |
7.3 铁律清单
- 分片数不可修改:提前规划,宁可稍多,不可过少。
- 每个分片有固定的内存开销:单节点分片数不超过 20 个/GB 堆内存(ES 官方建议)。
- 过多分片的代价:集群恢复慢、Master 负载重、节点变动时大量分片迁移、协调开销大。
- 分片数 ≥ 数据节点数:尽量让每个节点至少分配一个主分片,充分利用写入能力。
7.4 不同场景的推荐配置
业务搜索(商品、文章、用户)
json
{
"settings": {
"number_of_shards": 3, // 数据量 10-50GB
"number_of_replicas": 1,
"refresh_interval": "1s",
"translog.durability": "request"
}
}日志类(写多读少,按时间切分)
json
{
"settings": {
"number_of_shards": 3, // 每天 30-100GB
"number_of_replicas": 1,
"refresh_interval": "30s", // 不需要实时搜索
"translog.durability": "async",
"codec": "best_compression", // 压缩节省磁盘
"index.lifecycle.name": "logs_policy"
}
}7.5 常见误区
| 误区 | 正确理解 |
|---|---|
| 数据量大就多加主分片 | 1 亿文档 × 500B = 50GB → 3 个分片就够了。分片数取决于数据大小,不是文档数量 |
| 分片越多查询越快 | 分片过多反而更慢。查询延迟 = max(各分片耗时) + 合并时间,合并时间随分片数线性增长 |
| 主分片数必须等于节点数 | 5GB 数据在 5 个节点上设 5 个分片 → 每分片 1GB(太小)。应设 1 个分片,副本分布在其他节点 |
| 设了不能改所以设很大 | 先用 3-5 个分片。未来不够 → Reindex 到新索引 + 别名切换。不必预留过多 |
八、核心速查表
8.1 索引管理速查
| 操作 | 能否改 | 方案 |
|---|---|---|
| 分片数 | ❌ | Reindex / Split |
| 字段类型 | ❌ | Reindex |
| 字段名 | ❌ | 新增字段 + copy_to |
| 索引时分词器 | ❌ | Reindex |
| 副本数 | ✅ | PUT /_settings |
| 新增字段 | ✅ | PUT /_mapping |
| 多字段 multi-fields | ✅ | PUT /_mapping |
| 查询分词器 | ✅ | PUT /_mapping |
| 动态 settings | ✅ | PUT /_settings |
8.2 数据流与持久化速查
写入 → Index Buffer + Translog(JVM 堆内存)
│
│ Refresh(默认每 1s)
▼
Segment in OS Cache(可搜索,未持久化)
│
│ Flush(translog 满 512MB 时触发)
▼
Segment on Disk(已持久化)+ Translog 清理
Translog 写入机制:
request 模式(默认)→ 每次写请求后立即 fsync,不丢数据
async 模式 → 每 5s fsync,最多丢 5s 数据8.3 分片规划速查
核心公式:分片数 = 数据总量 ÷ 30GB(取整)
< 10GB → 1 个分片
10-50GB → 2~5 个分片
50-200GB → 5~10 个分片
> 200GB → 拆分索引,每索引 ≤ 200GB
铁律:
✅ 单分片 10-50GB
✅ 总分片数 ≤ 节点数 × 20/GB 堆内存
✅ 主分片数 ≥ 数据节点数(尽量)
✅ 副本数 = 1(默认)
✅ 配合 ILM 自动管理生命周期8.4 ILM 生命周期速查
Hot Phase(0-3天) → 3 分片,SSD,rollover
Warm Phase(3-30天) → 1 分片(shrink),HDD,forcemerge
Cold Phase(30-90天)→ freeze,不参与常规查询
Delete Phase(90天+)→ 自动删除8.5 生产环境黄金组合
索引模板(自动配置)+ ILM(自动生命周期)+ 别名(应用无感知)+ Rollover(自动切分)= 零人工干预的索引管理。