乐趣区

关于elasticsearch:解密Elasticsearch深入探究这款搜索和分析引擎-京东云技术团队

作者:京东保险 管顺利

开篇

最近应用 Elasticsearch 实现画像零碎,实现的 dmp 的数据中台能力。同时调研了竞品的架构选型。以及重温了 redis 原理等。特此做一次 es 的总结和回顾。网上没看到有人用 Elasticsearch 来实现画像的。我来做第一次尝试。

背景说完,咱们先思考一件事,应用内存零碎做数据库。他的长处是什么?他的痛点是什么?

一、原理

这里不在论述全貌。只聊聊通信、内存、长久化三局部。

通信

es 集群最小单元是三个节点。两个从节点搭配保障其高可用也是集群化的根底。那么节点之间 RPC 通信用的是什么?必然是 netty,es 基于 netty 实现了 Netty4Transport 的通信包。初始化 Transport 后建设 Bootstrap,通过 MessageChannelHandler 实现接管和转发。es 里辨别 server 和 client,如图 1。序列化应用的 json。es 在 rpc 设计上偏差于易用、通用、易了解。而不是单谋求性能。

图 1

有了 netty 的保驾护航使得 es 释怀是应用 json 序列化。

内存

图 2

es 内存分为两局部【on heap】和【off heap】。on heap 这部分由 es 的 jvm 治理。off heap 则是由 lucene 治理。on heap 被分为两局部,一部分能够回收,一部分不能回收。

能回收的局部 index buffer 存储新的索引文档。当被填满时,缓冲区的文档会被写入到磁盘 segment 上。node 上共享所有 shards。

不能被回收的有 node query cache、shard request cache、file data cache、segments cache

node query cache 是 node 级缓存,过滤后保留在每个 node 上,被所有 shards 共享,应用 bitset 数据结构(布隆优化版)关掉了评分。应用的 LRU 淘汰策略。GC 无奈回收。

shard request cache 是 shard 级缓存,每个 shard 都有。默认状况下该缓存只存储 request 后果 size 等于 0 的查问。所以该缓存不会被 hits,但却缓存 hits.total,aggregations,suggestions。能够通过 clear cache api 革除。应用的 LRU 淘汰策略。GC 无奈回收。

file data cache 是把聚合、排序后的 data 缓存起来。初期 es 是没有 doc values 的,所以聚合、排序后须要有一个 file data 来缓存,防止磁盘 IO。如果没有足够内存存储 file data,es 会一直地从磁盘加载数据到内存,并删除旧的数据。这些会造成磁盘 IO 和引发 GC。所以 2.x 之后版本引入 doc values 个性,把文档构建在 indextime 上,存储到磁盘,通过 memory mapped file 形式拜访。甚至如果只关怀 hits.total,只返回 doc id,关掉 doc values。doc values 反对 keyword 和数值类型。text 类型还是会创立 file data。

segments cache 是为了减速查问,FST 永驻堆内内存。FST 能够了解为前缀树,减速查问。but!!es 7.3 版本开始把 FST 交给了堆外内存,能够让节点反对更多的数据。FST 在磁盘上也有对应的长久化文件。

off heap 即 Segments Memory,堆外内存是给 Lucene 应用的。所以倡议至多留一半的内存给 lucene。

es 7.3 版本开始把 tip(terms index)通过 mmp 形式加载,交由零碎的 pagecache 治理。除了 tip,nvd(norms),dvd(doc values),tim(term dictionary),cfs(compound)类型的文件都是由 mmp 形式加载传输,其余都是 nio 形式。tip off heap 后的成果 jvm 占用量降落了 78% 左右。能够应用_cat/segments API 查看 segments.memory 内存占用量。

因为对外内存是由操作系统 pagecache 治理内存的。如果产生回收时,FST 的查问会牵扯到磁盘 IO 上,对查问效率影响比拟大。能够参考 linux pagecache 的回收策略应用双链策略。

长久化

es 的长久化分为两局部,一部分相似快照,把文件缓存中的 segments 刷新(fsync)磁盘。另一部分是 translog 日志,它每秒都会追加操作日志,默认 30 分钟刷到磁盘上。es 长久化和 redis 的 RDB+AOF 模式很像。如下图

图 3

上图是一个残缺写入流程。磁盘也是分 segment 记录数据。这里濡染跟 redis 很像。然而外部机制没有采纳 COW(copy-on-write)。这也是查问和写入并行时 load 被打满的起因所在。

小结

es 内存和磁盘的设计上十分奇妙。零拷贝上采纳 mmap 形式,磁盘数据映射到 off heap,也就是 lucene。为了减速数据的拜访,es 每个 segment 都有会一些索引数据驻留在 off heap 里;因而 segment 越多,瓜分掉的 off heap 也越多,这部分是无奈被 GC 回收!

联合以上两点能够分明晓得为什么 es 十分吃内存了。

二、利用

用户画像零碎中有以下难点须要解决。

1. 人群预估:依据标签选出一类人群,如 20-25 岁的喜爱电商社交的男性。20-25 岁∩电商社交∩男性。通过与或非的运算选出合乎特色的 clientId 的个数。这是一组。

咱们组与组之前也是能够在做交并差的运算。如既是 20-25 岁的喜爱电商社交的男性,又是北京市喜爱撸铁的男性。(20-25 岁∩电商社交∩男性)∩(20-25 岁∩撸铁∩男性)。对于这样的递归要求在 17 亿多的画像库中,秒级返回预估人数。

2. 人群包圈选:上述圈选出的人群包。要求分钟级构建。

3. 人包断定:判断一个 clientId 是否存在若干个人群包中。要求 10 毫秒返回后果。

咱们先尝试用 es 来解决以上所有问题。

人群预估,最容易想到计划是在服务端的内存中做逻辑运算。然而圈选出千万级的人群包人数秒级返回的话在服务端做代价十分大。这时候能够吧计算压力抛给 es 存储端,像查询数据库一样。应用一条语句查出咱们想要的数据来。

例如 mysql

select a.age from a where a.tel in (select b.age from b);

对应的 es 的 dsl 相似于

{"query":{"bool":{"must":[{"bool":{"must":[{"term":{"a9aa8uk0":{"value":"age18-24","boost":1.0}}},{"term":{"a9ajq480":{"value":"male","boost":1.0}}}],"adjust_pure_negative":true,"boost":1.0}},{"bool":{"adjust_pure_negative":true,"boost":1.0}}],"adjust_pure_negative":true,"boost":1.0}}}

这样应用 es 的高检索性能来满足业务需要。无论所少组,组内多少的标签。都打成一条 dsl 语句。来保障秒级返回后果。

应用官网举荐的 RestHighLevelClient,实现形式有三种,一种是拼 json 字符串,第二种调用 api 去拼字符串。我应用第三种形式 BoolQueryBuilder 来实现,比拟优雅。它提供了 filter、must、should 和 mustNot 办法。如

     /**
     * Adds a query that <b>must not</b> appear in the matching documents.
     * No {@code null} value allowed.
     */
    public BoolQueryBuilder mustNot(QueryBuilder queryBuilder) {if (queryBuilder == null) {throw new IllegalArgumentException("inner bool query clause cannot be null");
        }
        mustNotClauses.add(queryBuilder);
        return this;
    }

    /**
     * Gets the queries that <b>must not</b> appear in the matching documents.
     */
    public List<QueryBuilder> mustNot() {return this.mustNotClauses;}

应用 api 的能够大大的 show 下编代码的能力。

构建人群包。目前咱们圈出最大的包有 7 千多万的 clientId。想要分钟级别构建完(7 千万数据在条件限度下 35 分钟构建完)须要留神两个中央,一个是 es 深度查问,另一个是批量写入。

es 分页有三种形式,深度分页有两种,后两种都是利用游标(scroll 和 search_after)滚动的形式检索。

scroll 须要保护游标状态,每一个线程都会创立一个 32 位惟一 scroll id,每次查问都要带上惟一的 scroll id。如果多个线程就要保护多个游标状态。search_after 与 scroll 形式类似。然而它的参数是无状态的,始终会针对对新版本的搜索器进行解析。它的排序程序会在滚动中更改。scroll 原理是将 doc id 后果集保留在协调节点的上下文里,每次滚动分批获取。只须要依据 size 在每个 shard 外部依照程序取回后果即可。

写入时应用线程池来做,留神应用的阻塞队列的大小,还要抉择适的回绝策略(这里不须要抛异样的策略)。批量如果还是写到 es 中(比方做了读写拆散)写入时除了要多线程外,还有优化写入时的 refresh policy。

人包断定接口,因为整条业务链路十分长,这块检索,上游服务设置的熔断工夫是 10ms。所以优化要优化 es 的查问(也能够 redis)毕竟没负责逻辑解决。应用线程池解决 IO 密集型优化后能够达到 1ms。tp99 顶峰在 4ms。

三、优化、瓶颈与解决方案

以上是针对业务需要应用 es 的解题形式。还须要做响应的优化。同时也遇到 es 的瓶颈。

1. 首先是 mapping 的优化。画像的 mapping 中 fields 中的 type 是 keyword,index 要关掉。人包中的 fields 中的 doc value 关掉。画像是要准确匹配;人包断定只须要后果而不须要取值。es api 上人包计算应用 filter 去掉评分,filter 外部应用 bitset 的布隆数据结构,然而须要对数据预热。写入时线程不易过多,和外围数雷同即可;调整 refresh policy 等级。手动刷盘,构建时 index.refresh_interval 调整 -1,须要留神的是进行刷盘会加大堆内存,须要联合业务调整刷盘频率。构建大的人群包能够将 index 拆分成若干个。扩散存储能够进步响应。目前几十个人群包还是能撑持。如果日后成长到几百个的时候。就须要应用 bitmap 来构建存储人群包。es 对检索性能很卓越。然而如遇到写操作和查操作并行时,就不是他善于的。比方人群包的数据是每天都在变动的。这个时候 es 的内存和磁盘 io 会十分高。上百个包时咱们能够用 redis 来存。也能够抉择应用 MongoDB 来存人包数据。

四、总结

以上是咱们应用 Elasticsearch 来解决业务上的难点。同时发现他的长久化没有应用 COW(copy-on-write)形式。导致在实时写的时候检索性能升高。

应用内存零碎做数据源有点非常明显,就是检索块!尤其再实时场景下堪称利器。同时痛点也很显著,实时写会拉低检索性能。当然咱们能够做读写拆散,拆分 index 等计划。

除了 Elasticsearch,咱们还能够选用 ClickHouse,ck 也是反对 bitmap 数据结构。甚至能够上 Pilosa,pilosa 本就是 BitMap Database。

参考

贝壳 DMP 平台建设实际

Mapping parameters | Elasticsearch Reference [7.10] | Elastic

Elasticsearch 7.3 的 offheap 原理

退出移动版