一、背景
易企秀創(chuàng)意云矩陣下面運(yùn)營(yíng)了多個(gè)以內(nèi)容發(fā)布為核心的互聯(lián)網(wǎng)產(chǎn)品,每天都有大量企業(yè)與設(shè)計(jì)師在這些平臺(tái)制作并傳播他們的作品,每個(gè)產(chǎn)品又由不同部門及技術(shù)小組分管運(yùn)營(yíng),底層的技術(shù)框架、存儲(chǔ)以及數(shù)據(jù)結(jié)構(gòu)差異性都比較大,產(chǎn)品要求整合各產(chǎn)品線數(shù)據(jù)做一個(gè)一站式搜索引擎,數(shù)據(jù)要求盡可能與業(yè)務(wù)線保持實(shí)時(shí)同步,并且允許用戶對(duì)搜索結(jié)果按照實(shí)時(shí)閱讀量進(jìn)行排序。幸運(yùn)的是,elasticsearch提供了豐富的功能幫我們實(shí)現(xiàn)了大部分的操作,大幅降低了系統(tǒng)的維護(hù)成本。
二、數(shù)據(jù)處理流程
業(yè)務(wù)線用到的數(shù)據(jù)庫(kù)主要有3類,分別是MySQL、oracle、MongoDB,其中內(nèi)容數(shù)據(jù)存儲(chǔ)在MongoDB中,其它基礎(chǔ)信息分別儲(chǔ)存在MySQL與oracle中,如標(biāo)題、作者、發(fā)布時(shí)間等信息。那么對(duì)應(yīng)的數(shù)據(jù)處理流程包含:數(shù)據(jù)抽取>數(shù)據(jù)清洗>映射到es>提供query。
數(shù)據(jù)抽取
1、業(yè)務(wù)數(shù)據(jù):由于需求中要求數(shù)據(jù)近實(shí)時(shí)同步,那么按照傳統(tǒng)的方式在上億數(shù)據(jù)表中近實(shí)時(shí)做全表遷移與增量遷移的方案將是一件多么可怕的事情,即便每小時(shí)同步一次,業(yè)務(wù)方也是不能接受的;針對(duì)以上問題我們對(duì)同步方式做了優(yōu)化,采用解析MySQL binlog的方式進(jìn)行數(shù)據(jù)同步,業(yè)務(wù)方新增、修改、刪除都會(huì)產(chǎn)生對(duì)應(yīng)的binlog日志(oracle同步類似),根據(jù)不同的CRUD操作解析為對(duì)應(yīng)的es操作,并將數(shù)據(jù)實(shí)時(shí)更新到索引庫(kù);針對(duì)MongoDB中的內(nèi)容數(shù)據(jù)同步,只需每小時(shí)聚合有過修改操作的作品id,使用spark腳本根據(jù)id批量拉取MongoDB中的作品內(nèi)容同步到es(MongoDB中作品id要建索引)。
2、日志數(shù)據(jù):作品的閱讀量是基于日志計(jì)算的,前端通過sdk打點(diǎn)的,通過logserver(該項(xiàng)目會(huì)在后續(xù)實(shí)戰(zhàn)篇進(jìn)行講解,高峰期每天可處理數(shù)十億打點(diǎn)請(qǐng)求)采集日志發(fā)送到kafka。數(shù)據(jù)清洗
1、業(yè)務(wù)數(shù)據(jù):過濾標(biāo)記為刪除與發(fā)布時(shí)間為空的數(shù)據(jù),因?yàn)檫@些數(shù)據(jù)已不允許在前端進(jìn)行顯示;這樣的數(shù)據(jù)大約占全量數(shù)據(jù)的15%,濾除后可大幅提升后端存儲(chǔ)與檢索性能。
2、日志數(shù)據(jù):根據(jù)日志中業(yè)務(wù)類型與事件類型過濾出作品PV打點(diǎn)數(shù)據(jù),再對(duì)數(shù)據(jù)格式化處理后存入新的kafka topic,為后續(xù)的實(shí)時(shí)消費(fèi)使用;如果不對(duì)日志清洗,會(huì)有大量不相關(guān)的數(shù)據(jù)流入(頁(yè)面元素點(diǎn)擊事件、廣告曝光事件等),會(huì)使日志量翻好幾倍,徒增后端計(jì)算的壓力。存儲(chǔ)到ES
1、業(yè)務(wù)數(shù)據(jù):由于es中的索引庫(kù)是寬表結(jié)構(gòu),會(huì)存在根據(jù)作品id拉取業(yè)務(wù)庫(kù)多張屬性表到一個(gè)es索引庫(kù)的情況,這種情況下當(dāng)遇到有刪除操作的binlog時(shí)需要特殊處理,不能因?yàn)槠渲幸粡埍淼臄?shù)據(jù)刪除而刪除es中對(duì)應(yīng)的文檔數(shù)據(jù)。
2、日志數(shù)據(jù):通過spark streaming實(shí)時(shí)消費(fèi)kafka中的數(shù)據(jù),每分鐘形成一個(gè)batch,將計(jì)算好的作品pv數(shù)據(jù)通過es提供的spark connector存入對(duì)應(yīng)的索引庫(kù),由于活躍作品比較多,每分鐘對(duì)es的寫操作大約是6-8w。提供query
借助es提供的restful接口以及靈活的查詢模板支持,使得接口層面的開發(fā)簡(jiǎn)單許多。
三、配置與優(yōu)化
結(jié)合上述業(yè)務(wù)場(chǎng)景,如果不做任何優(yōu)化,首次檢索大約1s左右才能響應(yīng);由于es內(nèi)部會(huì)對(duì)query和segment做緩存,當(dāng)相似條件多次請(qǐng)求后,平均響應(yīng)時(shí)間會(huì)降到500ms左右;經(jīng)過對(duì)數(shù)據(jù)庫(kù)、索引配置、查詢語(yǔ)句三個(gè)層面簡(jiǎn)單配置后,首次檢索降到了200ms以內(nèi),平均響應(yīng)時(shí)間降了一個(gè)量級(jí)。
elasticsearch為滿足多種不同的應(yīng)用場(chǎng)景,底層提供多種數(shù)據(jù)結(jié)構(gòu)支持,并做了大量的默認(rèn)配置優(yōu)化,部分配置針對(duì)具體的用戶使用場(chǎng)景可能是冗余的,甚至可能造成性能的下降,需要根據(jù)實(shí)際業(yè)務(wù)場(chǎng)景做適當(dāng)取舍,我們結(jié)合自身使用場(chǎng)景做了如下優(yōu)化(文章中有疏漏或不正確的地方也歡迎點(diǎn)評(píng)指正):
數(shù)據(jù)庫(kù)
1、jvm內(nèi)存,分配給es的內(nèi)存不要超出系統(tǒng)內(nèi)存的50%,預(yù)留一半給Lucene,因?yàn)長(zhǎng)ucene會(huì)緩存segment數(shù)據(jù)提升檢索性能;內(nèi)存配置不要超過32g,如果你的服務(wù)器內(nèi)存沒有遠(yuǎn)遠(yuǎn)超過64g,那么不建議將es的jvm內(nèi)存設(shè)置為32g,因?yàn)槌^32g后每個(gè)jvm對(duì)象指針的長(zhǎng)度會(huì)翻倍,導(dǎo)致內(nèi)存與cpu的開銷增大。
2、禁用swapping,開啟服務(wù)器虛擬內(nèi)存交換功能會(huì)對(duì)es產(chǎn)生致命的打擊,如果某個(gè)segment數(shù)據(jù)被緩存到了磁盤上,會(huì)導(dǎo)致原來0.1毫秒的操作變成10毫秒,大大降低數(shù)據(jù)的檢索性能,使得es在提供查詢服務(wù)時(shí)出現(xiàn)木桶效應(yīng),拖跨整個(gè)集群的對(duì)外服務(wù)性能。
3、硬盤配置,建議使用SSD,如果你的服務(wù)器硬盤是一塊200g的機(jī)械硬盤,那么建議分成兩塊100g的盤去使用,這樣能大大提升es讀寫硬盤的能力,通過elasticsearch.yml中的path.data來實(shí)現(xiàn)。
4、其它配置
bootstrap.memory_lock: true
#設(shè)置為true鎖住內(nèi)存,當(dāng)服務(wù)混合部署了多個(gè)組件及服務(wù)時(shí),應(yīng)開啟此操作,允許es占用足夠多的內(nèi)存。
indices.breaker.request.limit: 10%
#設(shè)置單個(gè)request請(qǐng)求的內(nèi)存熔斷限制,默認(rèn)是jvm堆的60%(es7.0引入了新的內(nèi)存熔斷機(jī)制,會(huì)智能判斷,規(guī)避OOM)。
index.merge.scheduler.max_thread_count: 1
#設(shè)置segment合并時(shí)占用的線程數(shù),配置線程數(shù)越多對(duì)磁盤io消耗就越大(SSD忽略)。
indices.queries.cache.size:20%
#query請(qǐng)求可使用的jvm內(nèi)存限制,默認(rèn)是10%。
indices.requests.cache.size:2%
#查詢r(jià)equest請(qǐng)求的DSL語(yǔ)句緩存,被緩存的DSL語(yǔ)句下次請(qǐng)求時(shí)不會(huì)被二次解析,可提升檢索性能,默認(rèn)值是1%。
indices.fielddata.cache.size:30%
#設(shè)置字段緩存的最大值,默認(rèn)無限制。
索引配置
1、通過配置index_options優(yōu)化text field的索引生成方式,es默認(rèn)除生成倒排以外還會(huì)記錄文檔的詞頻與term的位置信息,如果只是用來對(duì)文檔的檢索不需要評(píng)分與高亮,可優(yōu)化。
2、禁用field評(píng)分,如果該字段不需參與相關(guān)度打分那么可禁用,通過將norms設(shè)置為false來禁用評(píng)分提升檢索性能。
3、Number類型的優(yōu)化,如果該數(shù)字類型字段存儲(chǔ)的是type信息 ,不會(huì)進(jìn)行排序與范圍查詢操作,建議存儲(chǔ)為keyword類型,keyword本身做了大量?jī)?yōu)化操作,對(duì)于檢索與過濾性能更佳。
4、禁用doc_values,es為提升keyword類型下數(shù)據(jù)的排序與聚合性能,默認(rèn)為每一個(gè)keyword field都開啟了此功能,非必要場(chǎng)景禁用此操作可節(jié)省磁盤空間。
5、開啟最優(yōu)化壓縮,通過配置"index.codec":"best_compression"開啟最優(yōu)壓縮,可節(jié)省磁盤空間,但會(huì)增大cpu負(fù)載。
6、設(shè)置合適的分片數(shù),多分片為es集群提供了分布式計(jì)算的保障,但一般不建議超過服務(wù)器的數(shù)量,過多的分片就意味著會(huì)有過多的segment產(chǎn)生,過多的segment消耗內(nèi)存與系統(tǒng)IO。
7、設(shè)置合適的副本數(shù),雖然多副本可以提升es的并發(fā)處理能力,但根據(jù)實(shí)際應(yīng)用當(dāng)中,這個(gè)值不建議配置太高,因?yàn)楦北緮?shù)據(jù)在進(jìn)行同步時(shí)會(huì)帶來過高的IO消耗。
8、降低刷新操作,過高的刷新會(huì)增大磁盤io負(fù)載,通過配置"index.refresh_interval":"-1"來控制刷新頻率,es默認(rèn)配置為1s,如果對(duì)數(shù)據(jù)時(shí)效性要求不高的話修改為30s-60s,-1表示禁用刷新。
9、通過forcemerge 強(qiáng)制合并索引段,定期清理過小的segment段,Lucene為加快檢索速度,會(huì)為每個(gè)segment構(gòu)建一層FST前綴索引,這些數(shù)據(jù)會(huì)全部加載到內(nèi)存,降低segment段的數(shù)量有助于降低內(nèi)存的消耗,過多的segment段也會(huì)增大底層os打開關(guān)閉文件的時(shí)間。
10、copy_to,使用multi_match在大數(shù)據(jù)量場(chǎng)景下進(jìn)行多字段查詢會(huì)很慢,利用copy_to將多個(gè)字段中的索引合并到一個(gè)字段,使用match查詢?cè)撟侄涡阅艽蠓嵘?/p>
查詢優(yōu)化
1、盡量避免深度翻頁(yè)場(chǎng)景,默認(rèn)最大結(jié)果集深度為10000,可根據(jù)實(shí)際使用場(chǎng)景適當(dāng)調(diào)大該值,如index.max_result_window:20000 ,該值設(shè)置過大,在高并發(fā)場(chǎng)景下會(huì)頻繁觸發(fā)es內(nèi)部gc操作,影響整體性能。
2、使用sliced scroll翻頁(yè)拉取大數(shù)據(jù)結(jié)果集,大家都知道使用scroll去做深度翻頁(yè)讀取大量數(shù)據(jù),但這種操作不支持online,原因就是它的指針是全局的無法支撐高并發(fā)操作,這時(shí)需要用到sliced scroll。
3、盡可的使用filter操作,因?yàn)閒ilter操作會(huì)使用緩存并且不會(huì)進(jìn)行評(píng)分計(jì)算,性能高于query。
4、如果查詢語(yǔ)句比較復(fù)雜且不需要翻頁(yè)操作的話可增加timeout=1s&terminate_after=10000兩個(gè)參數(shù),在高并發(fā)場(chǎng)景下性能提升3-5倍。
四、小結(jié)
5個(gè)實(shí)例(8核16g)索引了上億文檔內(nèi)容,優(yōu)化后任意檢索一條數(shù)據(jù)由原來的1s左右降低到200ms內(nèi) ,平均性能提升3-5倍。