貸前系統負責從進件到放款前全部業務流程的實現,其中涉及一些數據量較大、條件多樣且複雜的綜合查詢,引入ElasticSearch主要是爲了提升查詢效率,並但願基於ElasticSearch快速實現一個簡易的數據倉庫,提供一些OLAP相關功能。本文將介紹貸前系統ElasticSearch的實踐經驗。html
描述:爲快速定位數據而設計的某種數據結構。java
索引比如是一本書前面的目錄,能加快數據庫的查詢速度。瞭解索引的構造及使用,對理解ES的工做模式有很是大的幫助。linux
經常使用索引:git
位圖索引github
哈希索引算法
BTREE索引sql
倒排索引數據庫
位圖索引適用於字段值爲可枚舉的有限個數值的狀況。json
位圖索引使用二進制的數字串(bitMap)標識數據是否存在,1標識當前位置(序號)存在數據,0則表示當前位置沒有數據。數組
下圖1 爲用戶表,存儲了性別和婚姻情況兩個字段;
圖2中分別爲性別和婚姻狀態創建了兩個位圖索引。
例如:性別->男 對應索引爲:101110011,表示第一、三、四、五、八、9個用戶爲男性。其餘屬性以此類推。
使用位圖索引查詢:
男性 而且已婚 的記錄 = 101110011 & 11010010 = 100100010,即第一、四、8個用戶爲已婚男性。
女性 或者未婚的記錄 = 010001100 | 001010100 = 011011100, 即第二、三、五、六、7個用戶爲女性或者未婚。
顧名思義,是指使用某種哈希函數實現key->value 映射的索引結構。
哈希索引適用於等值檢索,經過一次哈希計算便可定位數據的位置。
下圖3 展現了哈希索引的結構,與JAVA中HashMap的實現相似,是用衝突表的方式解決哈希衝突的。
BTREE索引是關係型數據庫最經常使用的索引結構,方便了數據的查詢操做。
BTREE: 有序平衡N階樹, 每一個節點有N個鍵值和N+1個指針, 指向N+1個子節點。
一棵BTREE的簡單結構以下圖4所示,爲一棵2層的3叉樹,有7條數據:
以Mysql最經常使用的InnoDB引擎爲例,描述下BTREE索引的應用。
Innodb下的表都是以索引組織表形式存儲的,也就是整個數據表的存儲都是B+tree結構的,如圖5所示。
主鍵索引爲圖5的左半部分(若是沒有顯式定義自主主鍵,就用不爲空的惟一索引來作聚簇索引,若是也沒有惟一索引,則innodb內部會自動生成6字節的隱藏主鍵來作聚簇索引),葉子節點存儲了完整的數據行信息(以主鍵 + row_data形式存儲)。
二級索引也是以B+tree的形式進行存儲,圖5右半部分,與主鍵不一樣的是二級索引的葉子節點存儲的不是行數據,而是索引鍵值和對應的主鍵值,由此能夠推斷出,二級索引查詢多了一步查找數據主鍵的過程。
維護一顆有序平衡N叉樹,比較複雜的就是當插入節點時節點位置的調整,尤爲是插入的節點是隨機無序的狀況;而插入有序的節點,節點的調整隻發生了整個樹的局部,影響範圍較小,效率較高。
能夠參考紅黑樹的節點的插入算法:
https://en.wikipedia.org/wiki/Red%E2%80%93black_tree
所以若是innodb表有自增主鍵,則數據寫入是有序寫入的,效率會很高;若是innodb表沒有自增的主鍵,插入隨機的主鍵值,將致使B+tree的大量的變更操做,效率較低。這也是爲何會建議innodb表要有無業務意義的自增主鍵,能夠大大提升數據插入效率。
注:
Mysql Innodb使用自增主鍵的插入效率高。
使用相似Snowflake的ID生成算法,生成的ID是趨勢遞增的,插入效率也比較高。
倒排索引也叫反向索引,能夠相對於正向索引進行比較理解。
正向索引反映了一篇文檔與文檔中關鍵詞之間的對應關係;給定文檔標識,能夠獲取當前文檔的關鍵詞、詞頻以及該詞在文檔中出現的位置信息,如圖6 所示,左側是文檔,右側是索引。
反向索引則是指某關鍵詞和該詞所在的文檔之間的對應關係;給定了關鍵詞標識,能夠獲取關鍵詞所在的全部文檔列表,同時包含詞頻、位置等信息,如圖7所示。
反向索引(倒排索引)的單詞的集合和文檔的集合就組成了如圖8所示的」單詞-文檔矩陣「,打鉤的單元格表示存在該單詞和文檔的映射關係。
倒排索引的存儲結構能夠參考圖9。其中詞典是存放的內存裏的,詞典就是整個文檔集合中解析出的全部單詞的列表集合;每一個單詞又指向了其對應的倒排列表,倒排列表的集合組成了倒排文件,倒排文件存放在磁盤上,其中的倒排列表內記錄了對應單詞在文檔中信息,即前面提到的詞頻、位置等信息。
下面以一個具體的例子來描述下,如何從一個文檔集合中生成倒排索引。
如圖10,共存在5個文檔,第一列爲文檔編號,第二列爲文檔的文本內容。
將上述文檔集合進行分詞解析,其中發現的10個單詞爲:[谷歌,地圖,之父,跳槽,Facebook,加盟,創始人,拉斯,離開,與],以第一個單詞」谷歌「爲例:首先爲其賦予一個惟一標識 」單詞ID「, 值爲1,統計出文檔頻率爲5,即5個文檔都有出現,除了在第3個文檔中出現2次外,其他文檔都出現一次,因而就有了圖11所示的倒排索引。
對於一個規模很大的文檔集合來講,可能包含幾十萬甚至上百萬的不一樣單詞,可否快速定位某個單詞,這直接影響搜索時的響應速度,其中的優化方案就是爲單詞詞典創建索引,有如下幾種方案可供參考:
Hash索引簡單直接,查詢某個單詞,經過計算哈希函數,若是哈希表命中則表示存在該數據,不然直接返回空就能夠;適合於徹底匹配,等值查詢。如圖12,相同hash值的單詞會放在一個衝突表中。
相似於Innodb的二級索引,將單詞按照必定的規則排序,生成一個BTree索引,數據節點爲指向倒排索引的指針。
一樣將單詞按照必定的規則排序,創建一個有序單詞數組,在查找時使用二分查找法;二分查找法能夠映射爲一個有序平衡二叉樹,如圖14這樣的結構。
FST爲一種有限狀態轉移機,FST有兩個優勢:1)空間佔用小。經過對詞典中單詞前綴和後綴的重複利用,壓縮了存儲空間;2)查詢速度快。O(len(str))的查詢時間複雜度。
以插入「cat」、 「deep」、 「do」、 「dog」 、「dogs」這5個單詞爲例構建FST(注:必須已排序)。
如圖15 最終咱們獲得瞭如上一個有向無環圖。利用該結構能夠很方便的進行查詢,如給定一個詞 「dog」,咱們能夠經過上述結構很方便的查詢存不存在,甚至咱們在構建過程當中能夠將單詞與某一數字、單詞進行關聯,從而實現key-value的映射。
固然還有其餘的優化方式,如使用Skip List、Trie、Double Array Trie等結構進行優化,再也不一一贅述。
下面結合貸前系統具體的使用案例,介紹ES的一些心得總結。
目前使用的ES版本:5.6
官網地址:https://www.elastic.co/products/elasticsearch
ES一句話介紹:The Heart of the Elastic Stack(摘自官網)
ES的一些關鍵信息:
2010年2月首次發佈
Elasticsearch Store, Search, and Analyze
豐富的Restful接口
ES的索引,也就是Index,和前面提到的索引並非一個概念,這裏是指全部文檔的集合,能夠類比爲RDB中的一個數據庫。
即寫入ES的一條記錄,通常是JSON形式的。
文檔數據結構的元數據描述,通常是JSON schema形式,可動態生成或提早預約義。
因爲理解和使用上的錯誤,type已不推薦使用,目前咱們使用的ES中一個索引只創建了一個默認type。
一個ES的服務實例,稱爲一個服務節點。爲了實現數據的安全可靠,而且提升數據的查詢性能,ES通常採用集羣模式進行部署。
多個ES節點相互通訊,共同分擔數據的存儲及查詢,這樣就構成了一個集羣。
分片主要是爲解決大量數據的存儲,將數據分割爲若干部分,分片通常是均勻分佈在各ES節點上的。須要注意:分片數量沒法修改。
分片數據的一份徹底的複製,通常一個分片會有一個副本,副本能夠提供數據查詢,集羣環境下能夠提升查詢性能。
JDK版本: JDK1.8
安裝過程比較簡單,可參考官網:下載安裝包 -> 解壓 -> 運行
安裝過程遇到的坑:
ES啓動佔用的系統資源比較多,須要調整諸如文件句柄數、線程數、內存等系統參數,可參考下面的文檔。
http://www.cnblogs.com/sloveling/p/elasticsearch.html
下面以一些具體的操做介紹ES的使用:
初始化索引,主要是在ES中新建一個索引並初始化一些參數,包括索引名、文檔映射(Mapping)、索引別名、分片數(默認:5)、副本數(默認:1)等,其中分片數和副本數在數據量不大的狀況下直接使用默認值便可,無需配置。
下面舉兩個初始化索引的方式,一個使用基於Dynamic Template(動態模板) 的Dynamic Mapping(動態映射),一個使用顯式預約義映射。
1) 動態模板 (Dynamic Template)
<p style="line-height: 2em;"><span style="font-size: 14px;">curl -X PUT http://ip:9200/loan_idx -H 'content-type: application/json' <br> -d '{"mappings":{ "order_info":{ "dynamic_date_formats":["yyyy-MM-dd HH:mm:ss||yyyy-MM-dd],<br> "dynamic_templates":[<br> {"orderId2":{<br> "match_mapping_type":"string",<br> "match_pattern":"regex",<br> "match":"^orderId$",<br> "mapping":{<br> "type":"long"<br> }<br> }<br> },<br> {"strings_as_keywords":{<br> "match_mapping_type":"string",<br> "mapping":{<br> "type":"keyword",<br> "norms":false<br> }<br> }<br> }<br> ]<br> }<br>},<br>"aliases":{<br> "loan_alias":{}<br>}}'<br></span></p>
上面的JSON串就是咱們用到的動態模板,其中定義了日期格式:dynamic_date_formats 字段;定義了規則orderId2:凡是遇到orderId這個字段,則將其轉換爲long型;定義了規則strings_as_keywords:凡是遇到string類型的字段都映射爲keyword類型,norms屬性爲false;關於keyword類型和norms關鍵字,將在下面的數據類型小節介紹。
2)預約義映射
預約義映射和上面的區別就是預先把全部已知的字段類型描述寫到mapping裏,下圖截取了一部分做爲示例:
圖16中JSON結構的上半部分與動態模板相同,紅框中內容內容爲預先定義的屬性:apply.applyInfo.appSubmissionTime, apply.applyInfo.applyId, apply.applyInfo.applyInputSource等字段,type代表了該字段的類型,映射定義完成後,再插入的數據必須符合字段定義,不然ES將返回異常。
經常使用的數據類型有text, keyword, date, long, double, boolean, ip
實際使用中,將字符串類型定義爲keyword而不是text,主要緣由是text類型的數據會被當作文本進行語法分析,作一些分詞、過濾等操做,而keyword類型則是當作一個完整數據存儲起來,省去了多餘的操做,提升索引性能。
配合keyword使用的還有一個關鍵詞norm,置爲false表示當前字段不參與評分;所謂評分是指根據單詞的TF/IDF或其餘一些規則,對查詢出的結果賦予一個分值,供展現搜索結果時進行排序, 而通常的業務場景並不須要這樣的排序操做(都有明確的排序字段),從而進一步優化查詢效率。
初始化一個索引,都要在URL中明確指定一個索引名,一旦指定則沒法修改,因此通常創建索引都要指定一個默認的別名(alias):
<p style="line-height: 2em;"><span style="font-size: 14px;">"aliases":{ "loan_alias":{ }<br> }<br></span></p>
別名和索引名是多對多的關係,也就是一個索引能夠有多個別名,一個別名也能夠映射多個索引;在一對一這種模式下,全部用到索引名的地方均可以用別名進行替換;別名的好處就是能夠隨時的變更,很是靈活。
若是一個字段已經初始化完畢(動態映射經過插入數據,預約義經過設置字段類型),那就肯定了該字段的類型,插入不兼容的數據則會報錯,好比定義了一個long類型字段,若是寫入一個非數字類型的數據,ES則會返回數據類型錯誤的提示。
這種狀況下可能就須要重建索引,上面講到的別名就派上了用場;通常分3步完成:
上述步驟適合於離線遷移,若是要實現不停機實時遷移步驟會稍微複雜些。
基本的操做就是增刪改查,能夠參考ES的官方文檔:
https://www.elastic.co/guide/en/elasticsearch/reference/current/docs.html
一些比較複雜的操做須要用到ES Script,通常使用類Groovy的painless script,這種腳本支持一些經常使用的JAVA API(ES安裝使用的是JDK8,因此支持一些JDK8的API),還支持Joda time等。
舉個比較複雜的更新的例子,說明painless script如何使用:
需求描述
appSubmissionTime表示進件時間,lenssonStartDate表示開課時間,expectLoanDate表示放款時間。要求2018年9月10日的進件,若是進件時間 與 開課時間的日期差小於2天,則將放款時間設置爲進件時間。
Painless Script以下:
<p style="line-height: 2em;"><span style="font-size: 14px;">POST loan_idx/_update_by_query<br> { "script":{ "source":"long getDayDiff(def dateStr1, def dateStr2){ <br> LocalDateTime date1= toLocalDate(dateStr1); LocalDateTime date2= toLocalDate(dateStr2); ChronoUnit.DAYS.between(date1, date2);<br> }<br> LocalDateTime toLocalDate(def dateStr)<br> { <br> DateTimeFormatter formatter = DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss\"); LocalDateTime.parse(dateStr, formatter);<br> }<br> if(getDayDiff(ctx._source.appSubmissionTime, ctx._source.lenssonStartDate) < 2)<br> { <br> ctx._source.expectLoanDate=ctx._source.appSubmissionTime<br> }", "lang":"painless"<br> }<br> , "query":<br> { "bool":{ "filter":[<br> { "bool":{ "must":[<br> { "range":{ <br> "appSubmissionTime":<br> {<br> "from":"2018-09-10 00:00:00", "to":"2018-09-10 23:59:59", "include_lower":true, "include_upper":true<br> }<br> }<br> }<br> ]<br> }<br> }<br> ]<br> }<br> }<br>}<br></span></p>
解釋:整個文本分兩部分,下半部分query關鍵字表示一個按範圍時間查詢(2018年9月10號),上半部分script表示對匹配到的記錄進行的操做,是一段類Groovy代碼(有Java基礎很容易讀懂),格式化後以下, 其中定義了兩個方法getDayDiff()和toLocalDate(),if語句裏包含了具體的操做:
<p style="line-height: 2em;"><span style="font-size: 14px;">long getDayDiff(def dateStr1, def dateStr2){<br> LocalDateTime date1= toLocalDate(dateStr1);<br> LocalDateTime date2= toLocalDate(dateStr2);<br> ChronoUnit.DAYS.between(date1, date2);<br>}<br>LocalDateTime toLocalDate(def dateStr){<br> DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");<br> LocalDateTime.parse(dateStr, formatter);<br>}if(getDayDiff(ctx._source.appSubmissionTime, ctx._source.lenssonStartDate) < 2){<br> ctx._source.expectLoanDate=ctx._source.appSubmissionTime<br>}<br></span></p>
而後提交該POST請求,完成數據修改。
這裏重點推薦一個ES的插件ES-SQL:
https://github.com/NLPchina/elasticsearch-sql/wiki/Basic-Queries-And-Conditions
這個插件提供了比較豐富的SQL查詢語法,讓咱們可使用熟悉的SQL語句進行數據查詢。其中,有幾個須要注意的點:
ES-SQL使用Http GET方式發送狀況,因此SQL的長度是受限制的(4kb),能夠經過如下參數進行修改:http.max_initial_line_length: "8k"
計算總和、平均值這些數字操做,若是字段被設置爲非數值類型,直接使用ESQL會報錯,可改用painless腳本。
使用Select as語法查詢出的結果和通常的查詢結果,數據的位置結構是不一樣的,須要單獨處理。
NRT(Near Real Time):準實時
向ES中插入一條記錄,而後再查詢出來,通常都能查出最新的記錄,ES給人的感受就是一個實時的搜索引擎,這也是咱們所指望的,然而實際狀況卻並不是老是如此,這跟ES的寫入機制有關,作個簡單介紹:
寫入ES的數據,首先是寫入到Lucene索引段中的,而後才寫入ES的索引中,在寫入ES索引前查到的都是舊數據。
索引段中的數據會以原子寫的方式寫入到ES索引中,因此提交到ES的一條記錄,可以保證徹底寫入成功,而不用擔憂只寫入了一部分,而另外一部分寫入失敗。
索引段提交後還有最後一個步驟:refresh,這步完成後才能保證新索引的數據能被搜索到。
出於性能考慮,Lucene推遲了耗時的刷新,所以它不會在每次新增一個文檔的時候刷新,默認每秒刷新一次。這種刷新已經很是頻繁了,然而有不少應用卻須要更快的刷新頻率。若是碰到這種情況,要麼使用其餘技術,要麼審視需求是否合理。
不過,ES給咱們提供了方便的實時查詢接口,使用該接口查詢出的數據老是最新的,調用方式描述以下:
GET http://IP:PORT/index_name/type_name/id
上述接口使用了HTTP GET方法,基於數據主鍵(id)進行查詢,這種查詢方式會同時查找ES索引和Lucene索引段中的數據,並進行合併,因此最終結果老是最新的。但有個反作用:每次執行完這個操做,ES就會強制執行refresh操做,致使一次IO,若是使用頻繁,對ES性能也會有影響。
數組的處理比較特殊,拿出來單獨講一下。
1)表示方式就是普通的JSON數組格式,如:
[1, 2, 3]、 [「a」, 「b」]、 [ { "first" : "John", "last" : "Smith" },{"first" : "Alice", "last" : "White"} ]
2)須要注意ES中並不存在數組類型,最終會被轉換爲object,keyword等類型。
3)普通數組對象查詢的問題。
普通數組對象的存儲,會把數據打平後將字段單獨存儲,如:
<p style="line-height: 2em;"><span style="font-size: 14px;">{ "user":[<br> { "first":"John", "last":"Smith"<br> },<br> { "first":"Alice", "last":"White"<br> }<br> ]<br>}<br></span></p>
會轉化爲下面的文本
<p style="line-height: 2em;"><span style="font-size: 14px;">{ "user.first":[ "John", "Alice"<br> ], "user.last":[ "Smith", "White"<br> ]<br>}<br></span></p>
將原來文本之間的關聯打破了,圖17展現了這條數據從進入索引到查詢出來的簡略過程:
組裝數據,一個JSONArray結構的文本。
寫入ES後,默認類型置爲object。
查詢user.first爲Alice而且user.last爲Smith的文檔(實際並不存在同時知足這兩個條件的)。
返回了和預期不符的結果。
4)嵌套(Nested)數組對象查詢
嵌套數組對象能夠解決上面查詢不符的問題,ES的解決方案就是爲數組中的每一個對象單獨創建一個文檔,獨立於原始文檔。如圖18所示,將數據聲明爲nested後,再進行相同的查詢,返回的是空,由於確實不存在user.first爲Alice而且user.last爲Smith的文檔。
5)通常對數組的修改是全量的,若是須要單獨修改某個字段,須要藉助painless script,參考:https://www.elastic.co/guide/en/elasticsearch/reference/5.6/docs-update.html
數據安全是相當重要的環節,主要經過如下三點提供數據的訪問安全控制:
XPACK提供了Security插件,能夠提供基於用戶名密碼的訪問控制,能夠提供一個月的免費試用期,事後收取必定的費用換取一個license。
是指在ES服務器開啓防火牆,配置只有內網中若干服務器能夠直接鏈接本服務。
通常不容許業務系統直連ES服務進行查詢,須要對ES接口作一層包裝,這個工做就須要代理去完成;而且代理服務器能夠作一些安全認證工做,即便不適用XPACK也能夠實現安全控制。
ElasticSearch服務器默認須要開通9200、9300 這兩個端口。
下面主要介紹一個和網絡相關的錯誤,若是你們遇到相似的錯誤,能夠作個借鑑。
引出異常前,先介紹一個網絡相關的關鍵詞,keepalive :
Http keep-alive和Tcp keepalive。
HTTP1.1中默認啓用"Connection: Keep-Alive",表示這個HTTP鏈接能夠複用,下次的HTTP請求就能夠直接使用當前鏈接,從而提升性能,通常HTTP鏈接池實現都用到keep-alive;
TCP的keepalive的做用和HTTP中的不一樣,TPC中主要用來實現鏈接保活,相關配置主要是net.ipv4.tcp_keepalive_time這個參數,表示若是通過多長時間(默認2小時)一個TCP鏈接沒有交換數據,就發送一個心跳包,探測下當前連接是否有效,正常狀況下會收到對方的ack包,表示這個鏈接可用。
下面介紹具體異常信息,描述以下:
兩臺業務服務器,用restClient(基於HTTPClient,實現了長鏈接)鏈接的ES集羣(集羣有三臺機器),與ES服務器分別部署在不一樣的網段,有個異常會有規律的出現:
天天9點左右會發生異常Connection reset by peer. 並且是連續有三個Connection reset by peer
<p style="line-height: 2em;"><span style="font-size: 14px;">Caused by: java.io.IOException: Connection reset by peer <br> at sun.nio.ch.FileDispatcherImpl.read0(Native Method) <br> at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:39) <br> at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:223) <br> at sun.nio.ch.IOUtil.read(IOUtil.java:197)<br></span></p>
爲了解決這個問題,咱們嘗試了多種方案,查官方文檔、比對代碼、抓包。。。通過若干天的努力,最終發現這個異常是和上面提到keepalive關鍵詞相關(多虧運維組的同事幫忙)。
實際線上環境,業務服務器和ES集羣之間有一道防火牆,而防火牆策略定義空閒鏈接超時時間爲例如爲1小時,與上面提到的linux服務器默認的例如爲2小時不一致。因爲咱們當前系統晚上訪問量較少,致使某些鏈接超過2小時沒有使用,在其中1小時後防火牆自動就終止了當前鏈接,到了2小時後服務器嘗試發送心跳保活鏈接,直接被防火牆攔截,若干次嘗試後服務端發送RST中斷了連接,而此時的客戶端並不知情;當次日早上使用這個失效的連接請求時,服務端直接返回RST,客戶端報錯Connection reset by peer,嘗試了集羣中的三臺服務器都返回一樣錯誤,因此連續報了3個相同的異常。解決方案也比較簡單,修改服務端keepalive超時配置,小於防火牆的1小時便可。
《深刻理解ElasticSearch》
http://www.cnblogs.com/Creator/p/3722408.html
https://yq.aliyun.com/articles/108048
http://www.cnblogs.com/LBSer/p/4119841.html
http://www.cnblogs.com/yjf512/p/5354055.html
做者:綜合信貸雷鵬
來源:宜信技術學院