一、引言
本文罗列多数人使用Elasticsearch时可能会遇到的一些坑点,供大家参考、讨论、补充。
二、坑1:ES是准实时的?
为了验证这个坑是否是真坑,大家可以自己手动测试一下:
当更到数据到ES并且返回提示成功这一瞬间,立马通过ES查询,查看返回的数据是不是最新的。
思考:若查询到的数据是最新的,这个坑不算坑,可以填土了;而如果不是最新的数据,那么背后的原因是什么?
如果你还没有做验证,不要紧,我们一起来看下ES数据索引的整个过程,也许你从中也会找到蛛丝马迹。
|| 数据索引整个过程
数据索引整个过程涉及ES的分片,Lucene Index、Segment、Document的三者之间的关系等知识点。
Lucene 、Segment、Document的三者之间的关系图如下:
相互之间的关系:
- Lucene Index 可以存放多个Segment;
- Segment可以存放多个Document。
ES的一个分片就是一个Lucene Index,每一个Lucene Index由多个Segment构成,即Lucene Index的子集是Segment:
|| 数据的索引过程详解
- 当新的Document被创建,数据首先会存放到新的Segment中,同时旧的Document会被删除,并在原来的Segment上标记一个删除标识。当Document被更新,旧版本Document会被标识为删除 ,并将新版Document存放到新的Segment中。
- Shared收到写请求时,请求会被写入Translog中,然后Document被存放memory buffer(注意:memory buffer的数据并不能被搜索到)中,最终Translog保存所有修改记录。
- 每隔1秒(默认设置),refresh操作被执行一次,且memory buffer中的数据会被写入一个Segment并存放filesystem cache中,此时新的数据就可以被搜索到了。
- 通过上述索引过程的说明,我们可以得出结论:ES不是实时的,有1秒的延迟。
你可能会问,实际应用时我们应该如何解决延迟的问题呢?
简单的做法:提示用户查询的数据有一定的延迟,重试即可。
三、坑2:ES宕机恢复后,数据丢失
上面的第一个坑点的时候,我们有提到,每个1秒(默认配置),memory buffer中的数据会被写入Segment中,此时这部分的数据可被用户搜索到,但没有被持久化,一旦系统宕机,数据就会丢失。
上图中灰色桶内的数据可以被搜索到,但没有持久化,一旦ES宕机,这部分的数据将会丢失。
如何方式数据丢失呢?
使用Lucene中的commit操作可以轻松解决该问题。
commit具体操作:先将多个Segment合并保存到磁盘中,再将灰色桶变成绿色桶。
commit不足之处:耗IO,从而引发ES在commit之间宕机的问题。一旦系统在translog fsync之前宕机,数据也会直接丢失。
这就引出新的问题,如何保证数据数据的完整性?
采用Translog解决,因为Translog中的数据不会直接保存在磁盘中,只有fsync后才保存。Translog两种解决方案:
- 第一种:将Index.translog.durability设置成request,如果我们发现系统运行得不错,采用这种方式即可。
- 第二种:将Index.translog.durability设置成fsync,每次ES宕机启动后,先将主数据和ES数据进行比对,再将ES缺失的数据找出来.
注意,这里要强调一个知识点:Translog什么时候会fsync?
当 Index.translog.durability 设置成 request 后,每个请求都会 fsync,不过这样影响 ES 性能。这时我们可以把 Index.translog.durability 设置成 fsync,那么每隔 Index.translog.sync_interval 后每个请求才会 fsync 一次。
三、坑3:分页越深,查询效率越慢
ES分页这坑的出现,与ES的读操作请求的处理流程密切关联,为此我们有必要先深度剖析下ES的读操作请求的处理流程,入下图所示:
ES的读操作流程主要分为2个阶段:Query Phase、Fetch Phase。
|| Query Phase 查询阶段
协调的节点先把请求分发到所有分片,然后每个分片在本地查询,建立一个结果集队列,并将命令中的Document id以及搜索分数存放队列中,再返回给协调节点,最后协调节点会建一个全局队列,归并收到的所有结果集并进行全局排序。
Query Phase 需要强调:在 ES 查询过程中,如果 search 带了 from 和 size 参数,Elasticsearch 集群需要给协调节点返回 shards number * (from + size) 条数据,然后在单机上进行排序,最后给客户端返回 size 大小的数据。比如客户端请求 10 条数据(比如 3 个分片),那么每个分片则会返回 10 条数据,协调节点最后会归并 30 条数据,但最终只返回 10 条数据给客户端。
|| Fetch Phase 拉取阶段
协调节点先根据结果集里的Document id向所有分片获取完整的Document,然后所有分片返回完整的Document给协调节点,最后协调节点将结果返回给客户端。
在整个 ES 的读操作流程中,Elasticsearch 集群实际上需要给协调节点返回 shards number * (from + size) 条数据,然后在单机上进行排序,最后返回给客户端这个 size 大小的数据。
比如有 5 个分片,我们需要查询排序序号从 10000 到 10010(from=10000,size=10)的结果,每个分片到底返回多少数据给协调节点计算呢?告诉你不是 10 条,是 10010 条。也就是说,协调节点需要在内存中计算 10010*5=50050 条记录,所以在系统使用中,如果用户分页越深查询速度会越慢,也就是说并不是分页越多越好。
那如何更好地解决 ES 分页问题呢?
为了控制性能,我们主要使用 ES 中的 max_result_window 配置,这个数据默认为 10000,当 from+size > max_result_window ,ES 将返回错误。
由此可见,在系统设计时,我们一般需要控制用户翻页不能太深,而这在现实场景中用户也能接受,这也是我之前方案采用的设计方式。要是用户确实有深度翻页的需求,我们再使用 ES 中search_after 的功能也能解决,不过就是无法实现跳页了。
查询按订单总额分页,上一页最后一条order的总金额total_amount是10,那么下一页的查询示例代码如下:(search_after值就是上次查询结果排序字段的结果值)
{
"query":{
"bool":{
"must":[
{
"term":{
"user.user_name.keyword":"李大侠"
}
}
],
"must_not":[
],
"should":[
]
}
},
"from":0,
"size":2,
"search_after":[
"10"
],
"sort":[
{
"total_amount":"asc"
}
],
"aggs":{
}
}