之前写过很多es了,接触es也是比较早,很多项目也是用了,但是一直不成系统,零零散散的看着有很大的进步空间,所以这篇他来啦~
🎭 第一幕:ES 是谁?
首先咱先上一碟小菜:es是数据库吗,es和mysql的区别是什么?
""当你在电商站搜'红烧肉',MySQL 会用 LIKE '%红烧肉%' 扫描百万商品描述------用户等到泡面凉了还没结果。而 ES 用倒排索引 ,0.01 秒告诉你:'有 327 条结果,第 1 条是李佳琦推荐的秘制酱料'。------不是 ES 更快,而是它根本不用'找',它早就把答案按关键词排好了。
- MySQL / PostgreSQL :强一致性、事务、结构化查询 → "记账型数据库"
- Elasticsearch :近实时、全文检索、模糊匹配、聚合分析 → "情报型搜索引擎"
好了,这第一幕 倾其所有 基本上已经道破天机了,寥寥数语 浓缩了这么多黄金屋,功力已经用了0.1成,呼~ 稍作休息、正餐马上开始啦;
🏙️ 第二幕:架构揭秘
ES = 分布式 Lucene 集群 + 自动容灾 + RESTful API + 让你爱上搜索的魔法;
"单机 Lucene 最多扛几亿文档,再大就慢如蜗牛。ES 把索引拆成多个 Shard(分片) ,每个 Shard 是独立的 Lucene 实例------查询时,10 个分片并行干活,速度提升近 10 倍 ;写入时,数据自动路由到不同分片,避免单点瓶颈。Replica(副本) 更是救命稻草:主分片挂了?副本秒升主,服务不中断!"
📌 核心事实:
- 真正的索引/搜索引擎是 Lucene(Apache 顶级项目)
- ES 不存储原始数据 ,只存储 倒排索引 + 列存(Doc Values) + 源文档(_source)
- ES 的所有"魔法",都建立在 Lucene Segment 不可变模型 之上
✅ 所以,不懂 Lucene,就永远看不懂 ES 的性能边界。
Lucene 底层模型 ------ "不可变 Segment 的艺术"
1. Segment:一切性能的基石
- 每次
refresh(默认 1s),Memory Buffer → 新 Immutable Segment - Segment 一旦生成,永不修改(Immutable)以此可不加锁 长缓存
- 查询时,并行查所有 Segment,再 merge 结果
上面说的有点简约,下面咱们解释一下:
Elasticsearch 对写入的数据实行"一夫一妻制":Segment 一旦生成就永不更改,多线程读它连锁都懒得加 ,连 OS 的 Page Cache 都敢把它当长期饭票;可若所有数据挤在一个文件里,迟早胖成磁盘的"前任阴影",于是新来的先住小 Segment 单间,等后台 Merge 策略哪天心情好,就把几个小单间打通成大平层,顺手把那些"已失宠但还占编制"的幽灵文档扫地出门。
⚠️ 生产陷阱 :
如果写入太快(如日志场景),Segment 爆炸 → 查询变慢
对策 :调大 refresh_interval(如 30s),或用 Time-based Indexing(每天一个索引)
| 角色 | 职责 | 比喻 |
|---|---|---|
| Index(索引) | 逻辑数据库 | "科幻小说区"、"美食菜谱馆" |
| Shard(分片) | 数据分片 | 把"科幻小说区"拆成 5 个子馆(shard=5) |
| Replica(副本) | 容灾备份 | 每个子馆配 1 个影子馆(replica=1) |
| Node(节点) | 物理服务器 | 图书馆分馆(北京馆、上海馆) |
| Cluster(集群) | 多节点协同 | 全国图书馆联盟 |
🔥 关键设计:
- 写入时,数据自动路由到某个 Primary Shard
- 同时同步到 Replica Shard(保证高可用)
- 查询时,并行查所有分片,再合并结果 → 速度起飞!
💡 为什么不能无限加分片 ?
因为每个分片 = 一个 Lucene 实例 = 占内存 + 文件句柄;分片太多 → 集群变"老年机"(官方建议单分片 < 50GB)
🔍 第三幕:倒排索引
你搜 "红烧肉怎么做",ES 为什么能 0.02 秒返回结果?不是因为它聪明,而是因为它早就把答案按关键词排得明明白白 ------这就是 倒排索引(Inverted Index) 。 当你输入 "红烧肉",ES 直接查表,O(1) 定位到相关文档 ID 列表,根本不用扫描全文。
| 词语 | 出现在哪些文档? |
|---|---|
| 红烧肉 | [doc123, doc456, doc789] |
| 土豆 | [doc200, doc300] |
| 糖色 | [doc123, doc789] |
🧠 台下十年功附赠的是:
- 自动分词("红烧肉" → "红烧" + "肉"?No!用中文分词器 like IK)
- 相关性打分(TF-IDF / BM25)→ "红烧肉食谱" 比 "红烧肉新闻" 排更前
- 模糊匹配(
"roast pork"~2→ 容忍2个词错位)
但问题来了:如果词典有 10 亿个词,内存岂不爆炸?
🔍 关键优化技术:
(1) FST(Finite State Transducer)有限状态转换器
- 普通 Trie 树存 "application" 和 "apply" 需要完整路径
- 词典用 FST 压缩, 让它们共享前缀 "appl",比 Trie 树省 50%+ 内存
- FST 支持**前缀自动补全,**如搜 "app" 能快速列出所有以 app 开头的词
(2) Doc ID 列表:Roaring Bitmap + Skip List跳表
- 文档 IDDoc ID 列表有序存储(如 [1, 5, 9, 12, ...])
- ES 用 差值编码(Delta Encoding) 存储相邻 ID 的间隔;
- 再用 Roaring Bitmap 压缩稀疏段,10 亿 ID 列表内存 < 100MB
- 查询时靠 Skip List跳表快速跳跃---比如找 doc_10000,不用遍历前 9999 个,避免全扫描
(3) 分词不是乱切,是可控的艺术
- 中文默认不分词(会搜不到),需配 IK 分词器;
- "红烧肉" 可被切为
["红烧肉"](精准匹配)或["红烧", "肉"](泛化匹配); - 甚至支持同义词扩展:"番茄" → "西红柿"。
所以,ES 的快,从来不是玄学------是 FST 省下的每 1KB 内存,是 Skip List 跳过的每一个无关 ID,是 Roaring Bitmap 压缩的每一比特空间,共同堆出了"秒搜十亿文档"的奇迹。
⚡ 第四幕:读写与近实时搜索
"ES 默认写入后 1 秒才可搜(refresh_interval=1s),这不是 bug,是吞吐与延迟的权衡:
- 刷新越频繁,Segment 越多,查询越慢;
- 刷新越稀疏,数据可见越晚。
若你强行?refresh=true,每写一条就刷一次,集群会像被踩了尾巴的猫,瞬间炸毛(CPU 飙升,IO 爆满)
"至于强一致?ES 默认只要 1 个副本 ACK 就返回成功(wait_for_active_shards=1)。
- 想要
all副本确认?可以,但是你要容忍**写入吞吐直接腰斩,**这能忍?"士可杀不可忍"------ES 的哲学:宁可短暂不一致,也不牺牲高可用。"
🔁 写入流程(带容错):
- 协调节点(Coordinating Node) 接收请求
- 根据
_id或routing计算 目标 Primary Shard - 转发请求到 Primary Shard 所在节点
- Primary Shard :
- 写入 Memory Buffer + Translog
- 执行
refresh(可选) - 并发转发请求到 所有 Replica Shards
- Replica Shards :
- 同样写入 Buffer + Translog
- 返回 ACK
- Primary 收到 quorum 副本 ACK (默认
wait_for_active_shards=1) - Primary 返回成功给协调节点
- 协调节点返回客户端
- 后台异步 flush(Translog → 持久化 Segment)
- 每 30 分钟 或 Translog 满,触发 flush → 写入磁盘 + 清空 Translog
⚠️ 一致性保证:
- 默认 最终一致(写入后可能短暂不可见)
- 可通过
?consistency=quorum+wait_for_active_shards=all强一致(但性能差)
💡 所以:
- 默认 1 秒延迟 可见(可通过
?refresh=true强制立即可见,但别滥用!)- 即使宕机,也能从 Translog 恢复未持久化数据
查询流程:
- 协调节点广播查询到 所有相关 Shard
- 每个 Shard 本地执行查询(Lucene 层)
- 返回 Top-K 文档 ID + Score(非完整文档!)
- 协调节点 merge + re-rank(全局排序)
- 再去各 Shard fetch 完整文档 (如果需要
_source)
💡 这就是为什么
size越大越慢:
- 第一阶段:每个 Shard 返回
size条- 第二阶段:协调节点要 merge
(shard_count * size)条再取 topsize
🚫 深分页(from=100000)为何被禁止?
- 协调节点需持有 100000 + size 条结果
- 内存爆炸!官方限制
index.max_result_window = 10000
✅ 替代方案:
- Search After(基于上一页最后一条的 sort 值)
- Scroll API(用于导出,非实时)
- Point in Time (PIT)(7.x+,快照式分页)
🧩 第五幕:聚合分析
咱们上面提到的倒排索引:"text 字段会被分词(如 '红烧肉' → ['红烧', '肉']),而聚合需要的是完整值 (如 '红烧肉食谱' vs '红烧肉新闻')。所以 ES 为 keyword 类型构建 Doc Values(列存) :按文档顺序存储原始值,聚合时直接扫描列,比倒排索引快 10 倍 !若你偏要用 text 聚合?ES 会默默加载 Field Data 到堆内存 ------等着 OOM 吧,少年。"
列式存储(Doc Values) + 高效聚合算法(不像 MySQL 那样全行扫描,ES 只读需要的字段)
- 写入时,为每个字段额外构建列式存储
- 结构:
field → [value1, value2, ..., valueN](按 doc_id 顺序) - 存储在磁盘,但 OS 会缓存(mmap)
为什么不用倒排索引做聚合?
- 倒排是 term → docs ,聚合需要 doc → value
- 反向查找效率极低(尤其高基数字段)
💡 所以:
text字段默认 不开启 Doc Values(因为分词后无意义)- 聚合必须用
keyword/numeric/date等类型- 可通过
"doc_values": false关闭(节省空间,但不能聚合
🛡️ 第六幕:集群容灾与扩展
1. 脑裂(Split Brain)
- 网络分区 → 多个 Master 同时存在 → 数据写坏
- 对策 :
discovery.zen.minimum_master_nodes = (master_eligible_nodes / 2) + 1(7.x 前)- 7.x+ 用 Raft,自动防脑裂
场景:某台服务器宕机
- 如果挂的是 Data Node :
- Primary Shard 挂了?→ 自动提升 Replica 为 Primary
- 集群状态变 Yellow(副本缺失),但服务不中断!
- 如果挂的是 Master Node :
- 其他 Master-eligible 节点自动选举新老大(基于 Zen Discovery 或新版 Raft)
如何扩容?
- 加机器 → 自动加入集群
- 索引分片会自动 rebalance(数据迁移)
- 查询自动路由到新节点 → 无缝扩展
💡 最佳实践:
- 至少 3 个 Master 节点(防脑裂)
- Data 节点按角色分离(hot-warm-cold 架构)
第七幕:调优与问题
| 瓶颈 | 优化手段 |
|---|---|
| Refresh 太频繁 | refresh_interval: 30s |
| Translog 同步太勤 | translog.durability: async |
| 副本太多 | 写入时设 replicas: 0,写完再加 |
| Bulk 太小 | 单次 Bulk 5--15MB(约 1000--5000 docs) |
| Mapping 动态膨胀 | 关闭 dynamic: strict |
1. 慢查询雪崩
- 一个复杂聚合占满 CPU → 其他查询排队 → 集群 hang
- 对策 :
search.default_search_timeout(超时熔断)indices.breaker.*(内存熔断)- 监控
thread_pool.search.rejected
2. Field Data 爆炸
text字段做聚合 → 加载到堆内存(Field Data)- 对策 :永远不要对 text 做聚合!用 keyword
3.极限写入架构:
Logstash/Filebeat → Kafka → Spark/Flink → ES Bulk Write
- 中间加消息队列削峰
- Flink 做窗口聚合,减少 ES 写入量
🚫 ES 对 JVM 的特殊要求:
- 堆内存 ≤ 32GB(否则指针压缩失效,内存翻倍)
- 堆内存 ≤ 物理内存 50%(留一半给 OS Page Cache)
- 禁用 Swap (
bootstrap.memory_lock: true)
| 区域 | 用途 | 调优建议 |
|---|---|---|
| Heap | 存储查询上下文、聚合中间结果 | ≤ 31GB,G1GC |
| Page Cache | 缓存 Segment 文件 | 越大越好(靠 OS 管理) |
| Translog | 事务日志 | SSD 必备 |
| 场景 | 为什么不适合 | 正确姿势 |
|---|---|---|
| 强事务 | ES 不支持 ACID | 用 MySQL,ES 只做搜索同步 |
| 频繁更新 | 更新 = 删除+重建,性能差 | 少量更新 or 用 _update 脚本 |
| 大宽表 JOIN | 不支持 JOIN | 用 Nested / Parent-Child(慎用)或应用层关联 |
| 精确计数(10亿级) | total: 10000+ 是估算 |
用 track_total_hits=true(性能代价大) |
✅ 记住 :
ES 是"搜索加速器",不是"主数据库"!
🎯 终极总结
| 原则 | 实现 | 目的 |
|---|---|---|
| 不可变性 | Immutable Segments | 高并发、缓存友好 |
| 近实时 | Refresh + Translog | 写入吞吐 vs 可见性平衡 |
| 列存聚合 | Doc Values | 高效 BI 分析 |
| 分片自治 | Shard = Lucene Index | 水平扩展 |
| 协调解耦 | Coordinating Node | 无状态,易扩展 |
ES 不是银弹,而是一套精密的权衡系统:
- 用 空间换时间(Doc Values + 副本)
- 用 延迟换吞吐(Refresh 间隔)
- 用 复杂度换能力(分布式协调)
真正的大神,不是会用 ES,而是知道什么时候不该用 ES。
| 超能力 | 技术实现 | 效果 |
|---|---|---|
| 闪电搜索 | 倒排索引 + 分词 | 毫秒级全文检索 |
| 横向扩展 | 分片 + 集群 | PB 级数据轻松扛 |
| 高可用 | 副本 + 自动故障转移 | 节点挂了照常工作 |
| 智能分析 | 聚合 + Doc Values | 实时 BI 报表 |
| 近实时 | Refresh + Translog | 写入1秒可见,不丢数据 |
"如果你还在用 LIKE %keyword% 做搜索,那你不是在查数据,你是在给用户表演'系统正在思考人生'。"