灵异的「闪现」订单
mapping 理顺后,老王的搜索总算能用了。新的怪事又来了:运营上架新品,商品列表接口(走 MySQL)立刻能看到,搜索接口(走 ES)却搜不到;过个一两秒再搜,商品又凭空出现了。运营以为自己眼花,老王以为集群抽风,翻完文档才知道:这是 ES 的设计,不是故障——Elasticsearch 是近实时(Near Real Time)的,写入到可搜索之间,默认隔一秒。
文档写入的完整链路
先抛结论:文档写进来,不是直接进倒排索引的。链路是这样:
写入请求
-> 写内存 buffer(还没法被搜索)
-> 写 translog(事务日志,防丢)
-> 默认每隔 1 秒执行 refresh
-> buffer 里的文档生成一个新的 segment(段)
-> segment 打开,文档此刻才可被搜索关键在 segment:Lucene 的倒排索引是一段一段的,每个段一旦生成就是不可变的。不可变意味着没有锁、不用更新、对缓存和压缩极其友好——这是 ES 快的根基。但代价是:想搜新文档,必须等它进了段。refresh 干的就是这件事,默认一秒一次,这就是「近实时」里那个「近」的来源。对比 MySQL 写完立刻可查的实时性,ES 用一秒的延迟换了写入吞吐,这笔账大多数搜索场景都划算。
数据不丢的底气:translog
有个问题冒出来了:buffer 在内存里,refresh 前断电了,这一秒的数据不就丢了?这就是 translog 存在的意义。每条写请求在进 buffer 的同时,也追加写进 translog,默认策略是每个请求都落盘(fsync)成功才返回。
节点重启时,ES 重放 translog,把还没生成段的数据补回来——是不是有点眼熟?对,和 MySQL 的 redo log 一个思路:内存快,但易失;日志落盘,用顺序写换来不丢。而真正把内存段固化到磁盘的动作叫 flush:Lucene 做一次 commit,把段永久落盘,然后清空 translog。refresh 每秒一次很便宜,flush 则重得多,默认 translog 太大或每 30 分钟才触发一次。
删除和更新的真相
段不可变还带来两个反直觉的真相。删除文档并不真删:只是在删除文件(.del)里给对应文档打了个墓碑标记,搜索时会过滤掉,磁盘上那条数据还躺着。更新文档也不真更新:等于「标记删除旧版本 + 写入新版本」,同一个文档的多个版本可能散落在不同段里。
那墓碑和旧版本谁来清?段合并(merge)。ES 后台会把一堆小段合并成大段,合并时被标记删除的文档被真正丢弃。这也是为什么频繁删除更新的索引,磁盘占用会先涨后落——涨的部分是墓碑和新版本,落下来靠合并。老王的商品表天天改价格,磁盘曲线跟心电图似的,看到这就释然了。
一秒太久怎么办:refresh 调优
反过来,一秒的延迟在有的场景嫌久,在有的场景又嫌贵。refresh 是可以在索引级别调的:
PUT /product/_settings
{
"refresh_interval": "30s"
}
批量导入前临时关刷新,导完手动刷一次再恢复
PUT /product/_settings
{ "refresh_interval": "-1" }
POST /product/_refresh
PUT /product/_settings
{ "refresh_interval": "30s" }写多读少的日志类索引,把 refresh_interval 拉长到 30 秒甚至更久,写入吞吐立竿见影;同步进 ES 的搜索场景,一秒默认值够用;大批量灌数据时临时关刷新,是第 12 篇写入调优的常规操作。还有一个 API 值得记住:GET /product/_doc/1?realtime=true,get 接口可以绕过 refresh 直接读(必要时从 translog 里捞),所以按 ID 取文档是实时的,「搜不到」只发生在 search 这条走倒排的链路上。老王那件灵异事件,本质就是列表查 MySQL、搜索走倒排,两条链路可见时间不一致。
小结
写入链路一句话:进 buffer 加日志,refresh 出段才可搜,flush 落盘清日志,删除打墓碑,合并来打扫。段不可变是快之本,translog 是不丢之本,refresh_interval 是吞吐与实时的调节旋钮。
文档能搜到了,怎么搜又是门学问。下一篇进查询的世界:match、term、filter 与 bool 的分工——以及为什么对 text 字段用 term 查询是新手第一坑。
评论 (0)