参考:
5、重点引用:github.com/doocs/advan…
一、写数据底层原理
1、数据先写入内存 buffer,
2、然后每隔 1s,将数据 refresh 到 os cache,到了 os cache 数据就能被搜索到(所以我们才说 es 从写入到能被搜索到,中间有 1s 的延迟)。
如果 buffer 快满了,或者到一定时间,就会将内存 buffer 数据 refresh 到一个新的 segment file 中,
但是此时数据不是直接进入 segment file 磁盘文件,而是先进入 os cache 。这个过程就是 refresh 。
操作系统里面,磁盘文件其实都有一个东西,叫做 os cache ,即操作系统缓存,
就是说数据写入磁盘文件之前,会先进入 os cache ,先进入操作系统级别的一个内存缓存中去。
只要 buffer 中的数据被 refresh 操作刷入 os cache 中,这个数据就可以被搜索到了。
可以通过 es 的 restful api 或者 java api ,手动执行一次 refresh 操作,
就是手动将 buffer 中的数据刷入 os cache 中,让数据立马就可以被搜索到。
3、每隔 5s,将数据写入 translog 文件(这样如果机器宕机,内存数据全没,最多会有 5s 的数据丢失),
4、translog 大到一定程度,或者默认每隔 30mins,会触发 commit 操作,将缓冲区的数据都 flush 到 segment file 磁盘文件中。
数据写入 segment file 之后,同时就建立好了倒排索引。
重复上面的步骤,新的数据不断进入 buffer 和 translog,
不断将 buffer 数据写入一个又一个新的 segment file 中去,每次 refresh 完 buffer 清空,
translog 保留。随着这个过程推进,translog 会变得越来越大。
当 translog 达到一定长度的时候,就会触发 commit 操作。这个 commit 操作叫做 flush。
flush操作就对应着commit的全过程,我们可以通过es api,手动执行flush操作,手动将os cache中的数据fsync 强刷到磁盘上去。
其中:
translog 日志文件的作用是什么?你执行 commit 操作之前,数据要么是停留在 buffer 中,要么是停留在 os cache 中,无论是 buffer 还是 os cache 都是内存,一旦这台机器死了,内存中的数据就全丢了。所以需要将数据对应的操作写入一个专门的日志文件 translog 中,一旦此时机器宕机,再次重启的时候,es 会自动读取 translog 日志文件中的数据,恢复到内存 buffer 和 os cache 中去。
translog 其实也是先写入 os cache 的,默认每隔 5 秒刷一次fsync到磁盘中去,
所以默认情况下,可能有 5 秒的数据会仅仅停留在 buffer 或者 translog 文件的 os cache 中,
如果此时机器挂了,会丢失 5 秒钟的数据。但是这样性能比较好,最多丢 5 秒的数据。
也可以将 translog 设置成每次写操作必须是直接 fsync 到磁盘,但是性能会差很多。
index.translog.sync_interval 控制 translog 多久 fsync 到磁盘,最小为 100ms; index.translog.durability translog是每 5 秒钟刷新一次还是每次请求都 fsync, 这个参数有 2 个取值: request(每次请求都执行 fsync,es 要等 translog fsync 到磁盘后才会返回成功) async(默认值,translog 每隔 5 秒钟 fsync 一次)。
[
2、删除/更新数据底层原理
如果是删除操作,commit 的时候会生成一个 .del 文件,里面将某个 doc 标识为 deleted 状态,那么搜索的时候根据 .del 文件就知道这个 doc 是否被删除了。
如果是更新操作,就是将原来的 doc 标识为 deleted 状态,然后新写入一条数据。
buffer 每 refresh 一次,就会产生一个 segment file ,所以默认情况下是 1 秒钟一个 segment file ,这样下来 segment file 会越来越多,此时会定期执行 merge。每次 merge 的时候,会将多个 segment file 合并成一个,同时这里会将标识为 deleted 的 doc 给物理删除掉,然后将新的 segment file 写入磁盘,这里会写一个 commit point ,标识所有新的 segment file ,然后打开 segment file 供搜索使用,同时删除旧的 segment file 。
3、底层 lucene
[
简单来说,lucene 就是一个 jar 包,里面包含了封装好的各种建立倒排索引的算法代码。我们用 Java 开发的时候,引入 lucene jar,然后基于 lucene 的 api 去开发就可以了。
通过 lucene,我们可以将已有的数据建立索引,lucene 会在本地磁盘上面,给我们组织索引的数据结构。
4、es查询性能优化
性能优化的杀手锏——filesystem cache

[
往 es 里写的数据,实际上都写到磁盘文件里去了,查询的时候,操作系统会将磁盘文件里的数据自动缓存到 filesystem cache 里面去。
es 的搜索引擎严重依赖于底层的 filesystem cache ,你如果给 filesystem cache 更多的内存,尽量让内存可以容纳所有的 idx segment file 索引数据文件,那么你搜索的时候就基本都是走内存的,性能会非常高。如果内存不够走到磁盘,性能就会比较差。
归根结底,你要让 es 性能要好,最佳的情况下,就是你的机器的内存,至少可以容纳你的总数据量的一半。
优化手段
1、es + hbase架构
在 es 中存要用来检索的少数几个字段就可以了,比如说就写入 es id,name,age 三个字段,然后你可以把其他的字段数据存在 mysql/hbase 里,我们一般是建议用 es + hbase 这么一个架构。从 es 中根据 name 和 age 去搜索,拿到的结果可能就 20 个 doc id ,然后根据 doc id 到 hbase 里去查询每个 doc id 对应的完整的数据,给查出来再返回给前端。
2、数据预热
对于那些你觉得比较热的、经常会有人访问的数据,最好做一个专门的缓存预热子系统,就是对热数据每隔一段时间,就提前访问一下,让数据进入 filesystem cache 里面去。这样下次别人访问的时候,性能一定会好很多。
3、冷热分离
最好是将冷数据写入一个索引中,然后热数据写入另外一个索引中,这样可以确保热数据在被预热之后,尽量都让他们留在 filesystem os cache 里,别让冷数据给冲刷掉。
5、分页性能优化
有什么解决方案吗?
1、from/size 浅分页;不允许深度分页(默认深度分页性能很差)
es 的分页是较坑的,为啥呢?举个例子吧,假如你每页是 10 条数据,你现在要查询第 100 页,实际上是会把每个 shard 上存储的前 1000 条数据都查到一个协调节点上,如果你有个 5 个 shard,那么就有 5000 条数据,接着协调节点对这 5000 条数据进行一些合并、处理,再获取到最终第 100 页的 10 条数据。你翻页的时候翻的越深,每个 shard 返回的数据就越多,而且协调节点处理的时间越长,非常坑爹。所以用 es 做分页的时候,你会发现越翻到后面,就越是慢。
[
GET /index/_search
{
"from": 100,
"size": 10,
"query": { ... }
}
性能差:ES 需要在每个分片上收集 from+size 条结果,然后在协调节点排序合并
**2、scroll 深分页,可以一页一页刷出来,**用 **scroll api**
scroll 会一次性生成所有数据的一个快照,然后每次滑动向后翻页就是通过游标 **scroll_id** 移动,获取下一页下一页这样子,性能会比上面说的那种分页性能要高很多很多,基本上都是毫秒级的。但是,缺点是不能随意跳到任何一页的场景。
初始化时必须指定 **scroll** 参数,告诉 es 要保存此次搜索的上下文多长时间。你需要确保用户不会持续不断翻页翻几个小时,否则可能因为超时而失败。
# 初始化scroll(保持1分钟)
GET /index/_search?scroll=1m
{
"size": 100,
"query": { ... }
}
# 后续获取
GET /_search/scroll
{
"scroll": "1m",
"scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVY..."
}
结果非实时是基于初始快照。
3. search_after 高效深分页
可以用 search_after 来做, search_after 的思想是使用前一页的结果来帮助检索下一页的数据,显然这种方式也不允许你随意翻页,你只能一页页往后翻。初始化时,需要使用一个唯一值的字段作为 sort 字段。
# 第一次查询
GET /index/_search
{
"size": 100,
"query": { ... },
"sort": [
{"timestamp": "asc"},
{"_id": "asc"} # 确保排序唯一性
]
}
# 后续查询(使用上一页最后结果的sort值)
GET /index/_search
{
"size": 100,
"query": { ... },
"search_after": [1463538857, "654323"],
"sort": [
{"timestamp": "asc"},
{"_id": "asc"}
]
}
工作原理:使用上一页最后结果的排序值作为下一页的起点
优点: 无深度分页性能问题 /结果实时 /不需要维护上下文
缺点: 必须指定稳定唯一的排序字段 /不支持随机跳页 /需要客户端维护状态
写入流程: