一、引言

本文罗列多数人使用Elasticsearch时可能会遇到的一些坑点,供大家参考、讨论、补充。

二、坑1:ES是准实时的?

为了验证这个坑是否是真坑,大家可以自己手动测试一下:
当更到数据到ES并且返回提示成功这一瞬间,立马通过ES查询,查看返回的数据是不是最新的。

思考:若查询到的数据是最新的,这个坑不算坑,可以填土了;而如果不是最新的数据,那么背后的原因是什么?

如果你还没有做验证,不要紧,我们一起来看下ES数据索引的整个过程,也许你从中也会找到蛛丝马迹。

|| 数据索引整个过程
数据索引整个过程涉及ES的分片,Lucene Index、Segment、Document的三者之间的关系等知识点。

Lucene 、Segment、Document的三者之间的关系图如下:

es 0 1两种状态数据查询很慢 es查询结果不对_分页


相互之间的关系:

  • Lucene Index 可以存放多个Segment;
  • Segment可以存放多个Document。

ES的一个分片就是一个Lucene Index,每一个Lucene Index由多个Segment构成,即Lucene Index的子集是Segment:

es 0 1两种状态数据查询很慢 es查询结果不对_数据_02

|| 数据的索引过程详解

  1. 当新的Document被创建,数据首先会存放到新的Segment中,同时旧的Document会被删除,并在原来的Segment上标记一个删除标识。当Document被更新,旧版本Document会被标识为删除 ,并将新版Document存放到新的Segment中。
  2. Shared收到写请求时,请求会被写入Translog中,然后Document被存放memory buffer(注意:memory buffer的数据并不能被搜索到)中,最终Translog保存所有修改记录。
  3. es 0 1两种状态数据查询很慢 es查询结果不对_es 0 1两种状态数据查询很慢_03

  4. 每隔1秒(默认设置),refresh操作被执行一次,且memory buffer中的数据会被写入一个Segment并存放filesystem cache中,此时新的数据就可以被搜索到了。
  5. es 0 1两种状态数据查询很慢 es查询结果不对_java_04

  6. 通过上述索引过程的说明,我们可以得出结论:ES不是实时的,有1秒的延迟。

你可能会问,实际应用时我们应该如何解决延迟的问题呢?
简单的做法:提示用户查询的数据有一定的延迟,重试即可。

三、坑2:ES宕机恢复后,数据丢失

上面的第一个坑点的时候,我们有提到,每个1秒(默认配置),memory buffer中的数据会被写入Segment中,此时这部分的数据可被用户搜索到,但没有被持久化,一旦系统宕机,数据就会丢失。

es 0 1两种状态数据查询很慢 es查询结果不对_数据_05


上图中灰色桶内的数据可以被搜索到,但没有持久化,一旦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 0 1两种状态数据查询很慢 es查询结果不对_数据_06

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 的功能也能解决,不过就是无法实现跳页了。

es 0 1两种状态数据查询很慢 es查询结果不对_数据_07


查询按订单总额分页,上一页最后一条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":{

    }
}