作者:京东保险 管顺利
开篇
最近应用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 原理