?開篇
最近使用Elasticsearch實(shí)現(xiàn)畫像系統(tǒng),實(shí)現(xiàn)的dmp的數(shù)據(jù)中臺能力。同時(shí)調(diào)研了競品的架構(gòu)選型。以及重溫了redis原理等。特此做一次es的總結(jié)和回顧。網(wǎng)上沒看到有人用Elasticsearch來完成畫像的。我來做第一次嘗試。
背景說完,我們先思考一件事,使用內(nèi)存系統(tǒng)做數(shù)據(jù)庫。他的優(yōu)點(diǎn)是什么?他的痛點(diǎn)是什么?
?一、原理
這里不在闡述全貌。只聊聊通訊、內(nèi)存、持久化三部分。
通訊
es集群最小單元是三個(gè)節(jié)點(diǎn)。兩個(gè)從節(jié)點(diǎn)搭配保證其高可用也是集群化的基礎(chǔ)。那么節(jié)點(diǎn)之間RPC通訊用的是什么?必然是netty,es基于netty實(shí)現(xiàn)了Netty4Transport的通訊包。初始化Transport后建立Bootstrap,通過MessageChannelHandler完成接收和轉(zhuǎn)發(fā)。es里區(qū)分server和client,如圖1。序列化使用的json。es在rpc設(shè)計(jì)上偏向于易用、通用、易理解。而不是單追求性能。
??
圖1
有了netty的保駕護(hù)航使得es放心是使用json序列化。
內(nèi)存
??
圖2
es內(nèi)存分為兩部分【on heap】和【off heap】。on heap這部分由es的jvm管理。off heap則是由lucene管理。on heap 被分為兩部分,一部分可以回收,一部分不能回收。
能回收的部分index buffer存儲新的索引文檔。當(dāng)被填滿時(shí),緩沖區(qū)的文檔會(huì)被寫入到磁盤segment上。node上共享所有shards。
不能被回收的有node query cache、shard request cache、file data cache、segments cache
node query cache是node級緩存,過濾后保存在每個(gè)node上,被所有shards共享,使用bitset數(shù)據(jù)結(jié)構(gòu)(布隆優(yōu)化版)關(guān)掉了評分。使用的LRU淘汰策略。GC無法回收。
shard request cache是shard級緩存,每個(gè)shard都有。默認(rèn)情況下該緩存只存儲request結(jié)果size等于0的查詢。所以該緩存不會(huì)被hits,但卻緩存hits.total,aggregations,suggestions??梢酝ㄟ^clear cache api清除。使用的LRU淘汰策略。GC無法回收。
file data cache 是把聚合、排序后的data緩存起來。初期es是沒有doc values的,所以聚合、排序后需要有一個(gè)file data來緩存,避免磁盤IO。如果沒有足夠內(nèi)存存儲file data,es會(huì)不斷地從磁盤加載數(shù)據(jù)到內(nèi)存,并刪除舊的數(shù)據(jù)。這些會(huì)造成磁盤IO和引發(fā)GC。所以2.x之后版本引入doc values特性,把文檔構(gòu)建在indextime上,存儲到磁盤,通過memory mapped file方式訪問。甚至如果只關(guān)心hits.total,只返回doc id,關(guān)掉doc values。doc values支持keyword和數(shù)值類型。text類型還是會(huì)創(chuàng)建file data。
segments cache是為了加速查詢,F(xiàn)ST永駐堆內(nèi)內(nèi)存。FST可以理解為前綴樹,加速查詢。but??!es 7.3版本開始把FST交給了堆外內(nèi)存,可以讓節(jié)點(diǎn)支持更多的數(shù)據(jù)。FST在磁盤上也有對應(yīng)的持久化文件。
off heap 即Segments Memory,堆外內(nèi)存是給Lucene使用的。 所以建議至少留一半的內(nèi)存給lucene。
es 7.3版本開始把tip(terms index)通過mmp方式加載,交由系統(tǒng)的pagecache管理。除了tip,nvd(norms),dvd(doc values), tim(term dictionary),cfs(compound)類型的文件都是由mmp方式加載傳輸,其余都是nio方式。tip off heap后的效果jvm占用量下降了78%左右??梢允褂胈cat/segments API 查看 segments.memory內(nèi)存占用量。
由于對外內(nèi)存是由操作系統(tǒng)pagecache管理內(nèi)存的。如果發(fā)生回收時(shí),F(xiàn)ST的查詢會(huì)牽扯到磁盤IO上,對查詢效率影響比較大??梢詤⒖?a href="http://www.wenjunhu.com/v/tag/538/" target="_blank">linux pagecache的回收策略使用雙鏈策略。
持久化
es的持久化分為兩部分,一部分類似快照,把文件緩存中的segments 刷新(fsync)磁盤。另一部分是translog日志,它每秒都會(huì)追加操作日志,默認(rèn)30分鐘刷到磁盤上。es持久化和redis的RDB+AOF模式很像。如下圖
??
圖3
上圖是一個(gè)完整寫入流程。磁盤也是分segment記錄數(shù)據(jù)。這里濡染跟redis很像。但是內(nèi)部機(jī)制沒有采用COW(copy-on-write)。這也是查詢和寫入并行時(shí)load被打滿的原因所在。
??
圖4
如果刪除操作,并不是馬上物理清除被刪除的文檔,而是標(biāo)記為delete狀態(tài);更新操作,標(biāo)記原有的文檔為delete狀態(tài),再插入一條新的文檔。( 如圖4)
系統(tǒng)中會(huì)產(chǎn)生很多的Segment file文件。所以定期要執(zhí)行合并(merge)操作,將多個(gè)Segment file文件合并為一個(gè)。在合并的過程中,會(huì)將標(biāo)記刪除的文件進(jìn)行物理刪除操作。
ES記錄每個(gè)Segment file文件的提交點(diǎn)(commit point),用于管理所有的Segment file文件。
小結(jié)
es內(nèi)存和磁盤的設(shè)計(jì)上非常巧妙。零拷貝上采用mmap方式,磁盤數(shù)據(jù)映射到off heap,也就是lucene。為了加速數(shù)據(jù)的訪問,es每個(gè)segment都有會(huì)一些索引數(shù)據(jù)駐留在off heap里;因此segment越多,瓜分掉的off heap也越多,這部分是無法被GC回收!
結(jié)合以上兩點(diǎn)可以清楚知道為什么es非常吃內(nèi)存了。
二、應(yīng)用
用戶畫像系統(tǒng)中有以下難點(diǎn)需要解決。
1.人群預(yù)估:根據(jù)標(biāo)簽選出一類人群,如20-25歲的喜歡電商社交的男性。20-25歲∩電商社交∩男性。通過與或非的運(yùn)算選出符合特征的clientId的個(gè)數(shù)。這是一組。
我們組與組之前也是可以在做交并差的運(yùn)算。如既是20-25歲的喜歡電商社交的男性,又是北京市喜歡擼鐵的男性。(20-25歲∩電商社交∩男性)∩(20-25歲∩擼鐵∩男性)。對于這樣的遞歸要求在17億多的畫像庫中,秒級返回預(yù)估人數(shù)。
2.人群包圈選:上述圈選出的人群包。 要求分鐘級構(gòu)建。
3.人包判定:判斷一個(gè)clientId是否存在若干個(gè)人群包中。要求10毫秒返回結(jié)果。
我們先嘗試用es來解決以上所有問題。
人群預(yù)估,最容易想到方案是在服務(wù)端的內(nèi)存中做邏輯運(yùn)算。但是圈選出千萬級的人群包人數(shù)秒級返回的話在服務(wù)端做代價(jià)非常大。這時(shí)候可以吧計(jì)算壓力拋給es存儲端,像查詢數(shù)據(jù)庫一樣。使用一條語句查出我們想要的數(shù)據(jù)來。
例如mysql
select a.age from a where a.tel in (select b.age from b);
對應(yīng)的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的高檢索性能來滿足業(yè)務(wù)需求。無論所少組,組內(nèi)多少的標(biāo)簽。都打成一條dsl語句。來保證秒級返回結(jié)果。
使用官方推薦的RestHighLevelClient,實(shí)現(xiàn)方式有三種,一種是拼json字符串,第二種調(diào)用api去拼字符串。我使用第三種方式BoolQueryBuilder來實(shí)現(xiàn),比較優(yōu)雅。它提供了filter、must、should和mustNot方法。如
/** * Adds a query that must not 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 must not appear in the matching documents. */ public List mustNot() { return this.mustNotClauses; }
使用api的可以大大的show下編代碼的能力。
構(gòu)建人群包。目前我們?nèi)Τ鲎畲蟮陌?千多萬的clientId。想要分鐘級別構(gòu)建完(7千萬數(shù)據(jù)在條件限制下35分鐘構(gòu)建完)需要注意兩個(gè)地方,一個(gè)是es深度查詢,另一個(gè)是批量寫入。
es分頁有三種方式,深度分頁有兩種,后兩種都是利用游標(biāo)(scroll和search_after)滾動(dòng)的方式檢索。
scroll需要維護(hù)游標(biāo)狀態(tài),每一個(gè)線程都會(huì)創(chuàng)建一個(gè)32位唯一scroll id,每次查詢都要帶上唯一的scroll id。如果多個(gè)線程就要維護(hù)多個(gè)游標(biāo)狀態(tài)。search_after與scroll方式相似。但是它的參數(shù)是無狀態(tài)的,始終會(huì)針對對新版本的搜索器進(jìn)行解析。它的排序順序會(huì)在滾動(dòng)中更改。scroll原理是將doc id結(jié)果集保留在協(xié)調(diào)節(jié)點(diǎn)的上下文里,每次滾動(dòng)分批獲取。只需要根據(jù)size在每個(gè)shard內(nèi)部按照順序取回結(jié)果即可。
寫入時(shí)使用線程池來做,注意使用的阻塞隊(duì)列的大小,還要選擇適的拒絕策略(這里不需要拋異常的策略)。批量如果還是寫到es中(比如做了讀寫分離)寫入時(shí)除了要多線程外,還有優(yōu)化寫入時(shí)的refresh policy。
人包判定接口,由于整條業(yè)務(wù)鏈路非常長,這塊檢索,上游服務(wù)設(shè)置的熔斷時(shí)間是10ms。所以優(yōu)化要優(yōu)化es的查詢(也可以redis)畢竟沒負(fù)責(zé)邏輯處理。使用線程池解決IO密集型優(yōu)化后可以達(dá)到1ms。tp99高峰在4ms。
?三、優(yōu)化、瓶頸與解決方案
以上是針對業(yè)務(wù)需求使用es的解題方式。還需要做響應(yīng)的優(yōu)化。同時(shí)也遇到es的瓶頸。
1.首先是mapping的優(yōu)化。畫像的mapping中fields中的type是keyword,index要關(guān)掉。人包中的fields中的doc value關(guān)掉。畫像是要精確匹配;人包判定只需要結(jié)果而不需要取值。es api上人包計(jì)算使用filter去掉評分,filter內(nèi)部使用bitset的布隆數(shù)據(jù)結(jié)構(gòu),但是需要對數(shù)據(jù)預(yù)熱。寫入時(shí)線程不易過多,和核心數(shù)相同即可;調(diào)整refresh policy等級。手動(dòng)刷盤,構(gòu)建時(shí)index.refresh_interval 調(diào)整-1,需要注意的是停止刷盤會(huì)加大堆內(nèi)存,需要結(jié)合業(yè)務(wù)調(diào)整刷盤頻率。構(gòu)建大的人群包可以將index拆分成若干個(gè)。分散存儲可以提高響應(yīng)。目前幾十個(gè)人群包還是能支撐。如果日后成長到幾百個(gè)的時(shí)候。就需要使用bitmap來構(gòu)建存儲人群包。es對檢索性能很卓越。但是如遇到寫操作和查操作并行時(shí),就不是他擅長的。比如人群包的數(shù)據(jù)是每天都在變化的。這個(gè)時(shí)候es的內(nèi)存和磁盤io會(huì)非常高。上百個(gè)包時(shí)我們可以用redis來存。也可以選擇使用MongoDB來存人包數(shù)據(jù)。
四、總結(jié)
以上是我們使用Elasticsearch來解決業(yè)務(wù)上的難點(diǎn)。同時(shí)發(fā)現(xiàn)他的持久化沒有使用COW(copy-on-write)方式。導(dǎo)致在實(shí)時(shí)寫的時(shí)候檢索性能降低。
使用內(nèi)存系統(tǒng)做數(shù)據(jù)源有點(diǎn)非常明顯,就是檢索塊!尤其再實(shí)時(shí)場景下堪稱利器。同時(shí)痛點(diǎn)也很明顯,實(shí)時(shí)寫會(huì)拉低檢索性能。當(dāng)然我們可以做讀寫分離,拆分index等方案。
除了Elasticsearch,我們還可以選用ClickHouse,ck也是支持bitmap數(shù)據(jù)結(jié)構(gòu)。甚至可以上Pilosa,pilosa本就是BitMap Database。
?
?
參考
?貝殼DMP平臺建設(shè)實(shí)踐?
?Mapping parameters | Elasticsearch Reference [7.10] | Elastic?
?Elasticsearch 7.3 的 offheap 原理?
審核編輯 黃宇
-
數(shù)據(jù)庫
+關(guān)注
關(guān)注
7文章
3845瀏覽量
64616 -
引擎
+關(guān)注
關(guān)注
1文章
361瀏覽量
22624 -
Elasticsearch
+關(guān)注
關(guān)注
0文章
30瀏覽量
2849
發(fā)布評論請先 登錄
相關(guān)推薦
評論