ES 查询性能优化

面试官心理分析

面试官心想:

“这小子简历上写熟悉 ES,但我得考考他。几十亿数据量下去,不做优化肯定慢得像蜗牛。如果他只知道调调 refresh_interval 这种皮毛,说明没干过大项目。我要听的是架构设计物理内存原理模型设计。”


面试题:ES 数据量数十亿,如何提高查询效率?

核心回答逻辑(总分总)

一句话总结:
ES 的性能优化没有银弹,不能指望改一个参数就飞升。核心秘诀就一句话:只要内存装得下,ES 就是神;走磁盘,就是坑。 所以一切优化手段,都是为了让热点数据留在内存(Filesystem Cache)里。

以下是 5 个实战中的杀手锏:

1. 架构层优化:把 ES 当搜索引擎,别当数据库(最重要的点!)

  • 原理(Filesystem Cache):
    ES 里的数据是存在磁盘上的,但查询时,操作系统会把磁盘文件缓存到内存(Filesystem Cache)里。
    • 如果内存够大,容纳了所有索引文件,查询就是毫秒级(走内存)。
    • 如果内存不够,数据要去磁盘读,查询就是秒级甚至更慢(走磁盘 IO)。
  • 实战痛点:
    很多初学者把一整行数据(几十个字段,包括大段的 HTML 内容)都塞进 ES。结果就是:单条数据太大,内存存不下几条数据,导致频繁的磁盘 IO,性能极差。
  • 解决方案(ES + MySQL/HBase):
    • ES 只存索引字段: 比如 id, name, title, category 这些用来搜索、过滤、排序的字段。
    • 数据库存全量数据: 比如 content, detail 等大字段存在 MySQL 或 HBase 里。
    • 流程: 先在 ES 里通过 name 查出 id(速度极快),拿着 id 去 HBase/MySQL 查详情(根据主键查也极快)。
    • 效果: 这样能把 ES 索引体积压缩到原来的 1/10 甚至更小,让有限的内存能装下所有的热点索引数据。

2. 硬件与预热:数据预热(Warm Up)

  • 场景: 就算用了上面的方案,数据量还是太大,内存装不下所有数据怎么办?
  • 策略: 二八原则。80% 的查询集中在 20% 的热点数据上(比如微博热搜、电商爆款)。
  • 手段:
    写一个后台定时任务,每隔一分钟,模拟用户去搜索那些“热点词”或者“最新数据”。
    • 目的: 强制让操作系统把这部分数据加载到 Filesystem Cache 里。
    • 效果: 用户真来查的时候,数据已经在内存里了,直接起飞。

3. 存储层优化:冷热分离(配合 ES 8.x 的 ILM)

  • 场景: 日志类或时间序列数据,半年前的数据几乎没人看,但它们占着宝贵的内存,把今天的热数据挤出去了。
  • 手段:
    利用 ES 的 ILM (Index Lifecycle Management) 索引生命周期管理。
    • 热节点(Hot Node): 部署高性能 CPU + 大内存 + NVMe SSD。存放最近 7 天的数据,负责读写。
    • 冷节点(Cold/Frozen Node): 部署大容量机械硬盘,内存可以小点。存放 7 天以前的数据。
    • 效果: 确保好钢(内存)用在刀刃(热数据)上,不让冷数据污染 Filesystem Cache。

4. 模型设计优化:拒绝关联查询(Join)

  • 误区: 很多人把 ES 当 MySQL 用,想搞 Parent-Child(父子文档)或者 Nested(嵌套文档),甚至想在查询时做 Join。
  • 现实: ES 是分布式的,做关联查询性能极差(可能慢 10 倍以上)。
  • 手段(宽表设计):
    • Denormalization(反范式化): 在写入 ES 之前,在 Java 代码里就把数据拼好。
    • 比如:订单表和用户表,写入 ES 时直接存成一个大文档:{order_id: 1, user_name: "张三", ...}
    • 原则: 空间换时间。宁可数据冗余,也要保证查询时是单表扫描。

5. 分页性能优化:千万别用深度分页

  • 坑: from: 10000, size: 10
    ES 要去每个 Shard 查前 10010 条数据,汇总到协调节点,排序,扔掉前 10000 条,取最后 10 条。越往后翻,内存和 CPU 消耗是指数级增长的。
  • 解决方案(ES 8.x 更新):
    • 方案 A(产品经理妥协): 不允许跳页。只准点“下一页”。
    • 方案 B(技术实现 - Search After):
      • 旧方法: Scroll API。注意!在 ES 新版本中,Scroll 不再推荐用于实时搜索,它主要是用来做数据导出(迁移)的,因为它维护上下文很耗资源。
      • 新标准: 使用 search_after 配合 PIT (Point In Time)
      • 原理: 拿着上一页最后一条数据的 sort value(排序值)去查下一页。ES 就知道从哪里开始找,不需要把前面的数据都过一遍。性能是恒定的,翻到第 100 万页也很快。

面试官追问预演

Q: 你刚才说 ES 8.x 的 search_after,它和 Scroll 有什么本质区别?
A:

  • Scroll: 像是给数据拍了个“快照”,数据更新了你看不到,适合做数据迁移,用完必须手动清除上下文,否则内存爆炸。
  • Search After: 是无状态的(或者结合轻量级 PIT),它基于索引的实时(或由于 PIT 固定的)排序值定位。它更轻量,更适合 C 端用户的“无限下拉”场景。

Q: 你们 ES 集群大概多少台机器?内存怎么分配的?
A: (举个例子,根据你实际情况编,但要合理)
我们有 5 台 32核 64G 的机器。
JVM Heap 分配了 31G(注意:不要超过 32G,因为超过 32G 也就是 32768MB 后,JVM 的指针压缩 Compressed Oops 会失效,内存利用率反而下降)。
剩下的 33G 全部留给操作系统做 Filesystem Cache。我们严格控制存入 ES 的字段,保证热点索引数据量不超过集群总 Filesystem Cache 的大小(比如控制在 150G 以内),这样才能保证查询几乎全走内存。