Skip to content

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: 100000000ES 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/writemmap
数据拷贝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 -p
json
// 索引层面:对关键文件类型做预加载(解决冷启动)
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 不是"把数据从内存写到磁盘"。它做了两件事:

  1. 将内存中的 Segment 持久化(Lucene Commit)
  2. 清空/截断 Translog(数据已安全落盘,translog 不再需要)

5.2 完整数据生命周期

写入 Index Buffer + Translog

         │ ① Refresh(默认每 1s)

       Segment in OS Cache(可搜索,未持久化)

         │ ② Flush(translog 满 512MB 时触发)

       Segment on Disk(已持久化)+ Translog 清理

5.3 Flush 的 5 种触发条件

#触发条件配置项默认值
1Translog 大小达到阈值(最主要translog.flush_threshold_size512MB
2Translog 操作数达到阈值translog.flush_threshold_ops50万(7.x 已废弃)
3长时间无写入的定期 Flushtranslog.flush_threshold_period12h
4手动触发POST /my_index/_flush
5节点关闭/重启时自动

5.4 Flush vs Refresh 彻底区分

RefreshFlush
做什么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 = 可能 OOM

6.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 场景速查表

预估索引大小推荐主分片数单分片大小场景
< 1GB1< 1GB配置索引、小字典表
1-10GB1~25-10GB小型业务索引
10-50GB2~510-25GB中型业务索引
50-200GB5~1020-40GB大型业务索引
200GB-1TB10~2025-50GB日志 / 监控索引
> 1TB20+ 或拆索引25-50GB超大规模,按时间拆分

7.3 铁律清单

  1. 分片数不可修改:提前规划,宁可稍多,不可过少。
  2. 每个分片有固定的内存开销:单节点分片数不超过 20 个/GB 堆内存(ES 官方建议)。
  3. 过多分片的代价:集群恢复慢、Master 负载重、节点变动时大量分片迁移、协调开销大。
  4. 分片数 ≥ 数据节点数:尽量让每个节点至少分配一个主分片,充分利用写入能力。

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-fieldsPUT /_mapping
查询分词器PUT /_mapping
动态 settingsPUT /_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(自动切分)= 零人工干预的索引管理。