計(jì)算機(jī)應(yīng)用技術(shù)專業(yè)網(wǎng)站開(kāi)發(fā)方向大數(shù)據(jù)營(yíng)銷(xiāo)案例分析
前言
QBM、MFS的試題檢索、試題查重、公式轉(zhuǎn)換映射等業(yè)務(wù)場(chǎng)景以及XOP題庫(kù)廣泛使用搜索中間件,業(yè)務(wù)場(chǎng)景有著數(shù)據(jù)量大、對(duì)內(nèi)容搜索性能要求高等特點(diǎn),其中XOP題庫(kù)數(shù)據(jù)量更是接近1億,對(duì)檢索性能以及召回率要求高。目前QBM、MFS使用的搜索中間件是Solr,后續(xù)需要升級(jí)為ES。
看的書(shū)是《ElasticSearch源碼解讀與優(yōu)化實(shí)戰(zhàn)》的前半部分(與這篇博客部分內(nèi)容重合),主要是ES的一些工程模塊,分布式集群的一些理論知識(shí)。Lucene的部分知識(shí)主要來(lái)源一些寫(xiě)的比較全面的博客,Lucene涉及的數(shù)據(jù)結(jié)構(gòu)與算法比較復(fù)雜,其中涉及的如FST前綴字典、列式存儲(chǔ)數(shù)據(jù)壓縮、ES相關(guān)的分布式Paxos算法細(xì)節(jié)等都是很復(fù)雜,還是值得思考研究下。
//TODO 該博客主要使用ES、Lucene過(guò)程一些小計(jì)以及一些原理分析,初學(xué)原理涉及的深度難免不夠,不過(guò)后續(xù)隨著學(xué)習(xí)內(nèi)容持續(xù)更新ing…
目錄
- 概述
- 實(shí)踐
- 原理分析
- 搜索引擎流程
- Lucene相關(guān)原理
- ES相關(guān)原理
一、概述
ES是什么?
非關(guān)系型、搜索引擎、近實(shí)時(shí)搜索與分析、高可用、天然分布式、橫向可擴(kuò)展。
ElasticSearch是一款非常強(qiáng)大的、基于Lucene的開(kāi)源搜索及分析引擎;它是一個(gè)實(shí)時(shí)的分布式搜索分析引擎,它能讓你以前所未有的速度和規(guī)模,去探索你的數(shù)據(jù)。屬于NoSQL文檔性DB的一種,內(nèi)容檢索性能是最大的優(yōu)勢(shì)。
實(shí)時(shí)搜索:實(shí)時(shí)搜索(Real-time Search)很好理解,對(duì)于一個(gè)數(shù)據(jù)庫(kù)系統(tǒng),執(zhí)行插入以后立刻就能搜索到剛剛插入到數(shù)據(jù)。而近實(shí)時(shí)(Near Real-time),所謂“近”也就是說(shuō)比實(shí)時(shí)要慢一點(diǎn)點(diǎn)。像常用的MySQL等關(guān)系型數(shù)據(jù)庫(kù)不能稱之為實(shí)時(shí)搜索數(shù)據(jù)庫(kù),MySQL可以配置為提供較低的延遲和更高的實(shí)時(shí)性能。但是,MySQL的實(shí)時(shí)性取決于多個(gè)因素,包括硬件性能、數(shù)據(jù)庫(kù)設(shè)計(jì)、查詢優(yōu)化和負(fù)載等因素。
全文搜索屬于最常見(jiàn)的需求,開(kāi)源的 Elasticsearch (以下簡(jiǎn)稱 Elastic)是目前全文搜索引擎的首選。
它可以快速地儲(chǔ)存、搜索和分析海量數(shù)據(jù)。維基百科、Stack Overflow、Github 都采用它。
圖源:https://db-engines.com/en/ranking
主要功能:
1)海量數(shù)據(jù)的分布式存儲(chǔ)以及集群管理,達(dá)到了服務(wù)與數(shù)據(jù)的高可用以及水平擴(kuò)展;
2)近實(shí)時(shí)搜索,性能卓越。對(duì)結(jié)構(gòu)化、全文、地理位置等類(lèi)型數(shù)據(jù)的處理;
3)海量數(shù)據(jù)的近實(shí)時(shí)分析(聚合功能)
應(yīng)用場(chǎng)景:
1)網(wǎng)站搜索、垂直搜索、代碼搜索
2)日志管理與分析、安全指標(biāo)監(jiān)控、應(yīng)用性能監(jiān)控
常用非結(jié)構(gòu)化數(shù)據(jù)存儲(chǔ)中間件區(qū)別?ES、Solr、MongoDB都屬于NoSQL的家族的一員
- ES和MongoDB區(qū)別:它們有不同的設(shè)計(jì)目標(biāo)和用例,因此在許多方面存在區(qū)別:MongoDB 在某些查詢場(chǎng)景下表現(xiàn)很好,但對(duì)于全文搜索和實(shí)時(shí)分析來(lái)說(shuō),性能通常不如ES。如果你需要處理大量文本數(shù)據(jù)并進(jìn)行實(shí)時(shí)搜索和分析,ES 可能更適合。如果你的應(yīng)用需要存儲(chǔ)和查詢半結(jié)構(gòu)化或非結(jié)構(gòu)化的文檔數(shù)據(jù),MongoDB可能更合適。有時(shí)候,這兩個(gè)數(shù)據(jù)庫(kù)也可以組合使用,以滿足不同方面的需求。
- ES和Solr的區(qū)別:都是建立在Lucene庫(kù)上的,提供RESTful的API用于CRUD以及拓展其他高級(jí)特性,語(yǔ)法類(lèi)似。 ES 通常更適合用于實(shí)時(shí)搜索、日志和指標(biāo)分析、全文搜索等需要高度動(dòng)態(tài)性和實(shí)時(shí)性能的應(yīng)用。Solr 更適合處理傳統(tǒng)的文檔檢索和結(jié)構(gòu)化數(shù)據(jù)分析,例如圖書(shū)館目錄、商品搜索等。 ES 在分布式性能方面表現(xiàn)出色,天生支持分片和復(fù)制,易于橫向擴(kuò)展。Solr 也支持分布式部署,但需要更多的手動(dòng)配置和管理。Solr需要配合Zookeeper使用,ES自身帶有分布式系統(tǒng)管理功能。
參考:
- ES和MongoDB對(duì)比:https://leriou.github.io/2019-01-09-mongodb-compareto-elasticsearch/
- ES和Solr對(duì)比:https://zhuanlan.zhihu.com/p/85362497
- ChatGPT
二、實(shí)踐
1、安裝ES和Kibana
ES下載:https://www.elastic.co/cn/downloads/elasticsearch
ES在使用容器安裝:https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html
練習(xí)測(cè)試直接使用docker安裝8.9.1 版本的ES、Kibana。(建議不要安裝這么新的,否則會(huì)有很多坑)
# 一、安裝es和kibana# 拉取es鏡像
docker pull docker.elastic.co/elasticsearch/elasticsearch:8.9.1# 創(chuàng)建一個(gè)新的Docker網(wǎng)絡(luò),通過(guò)創(chuàng)建自定義的Docker網(wǎng)絡(luò),你可以輕松地管理容器之間的通信,并根據(jù)需要隔離它們。這在多容器應(yīng)用程序和微服務(wù)架構(gòu)中特別有用。
docker network create elastic-demo# 啟動(dòng)es容器
# 使用-m標(biāo)志設(shè)置容器的內(nèi)存限制。這樣就不 需要手動(dòng)設(shè)置JVM大小。
docker run --name es01 --net elastic-demo -p 9200:9200 -it -m 1GB docker.elastic.co/elasticsearch/elasticsearch:8.9.1
# 該命令打印elastic用戶密碼和Kibana的注冊(cè)令牌。# 從新生成,為elastic用戶設(shè)置密碼elastic
docker exec -it es01 /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic -i elastic
docker exec -it es01 /usr/share/elasticsearch/bin/elasticsearch-create-enrollment-token -s kibana
# kibana的token,等會(huì)要用,eyJ2ZXIiOiI4LjkuMSIsImFkciI6WyIxNzIuMTguMC4yOjkyMDAiXSwiZmdyIjoiNTU0ZDQyY2Y3OGZmZTUwYmEzZWExZjk2ZTljOWM4YmQyOTIwYjc2OTA0ZWY4OWEwZWI5YzkwNDU4YjUzNjNmNyIsImtleSI6InRQRTFXb29CRWF5NzJYQmM1cTFqOkNUU2tOOHF0VDc2b2xLbHRwNWtLTEEifQ==# 啟動(dòng)es報(bào)錯(cuò)1:https://discuss.elastic.co/t/elasticsearch-bootstrap-checks-failing/302442
# 需要設(shè)置 vm.max_map_count 至少 262144
# 編輯vm.max_map_count內(nèi)核設(shè)置必須至少設(shè)置為262144,以供生產(chǎn)使用。# 查看
grep vm.max_map_count /etc/sysctl.conf
# 臨時(shí)設(shè)置
# 永久設(shè)置要永久更改vm.max_map_count設(shè)置的值,請(qǐng)更新 /etc/sysctl.conf的值。
sysctl -w vm.max_map_count=262144# 二、本地測(cè)試es# 我們建議將elastic密碼作為環(huán)境變量存儲(chǔ)在shell中。范例:
export ELASTIC_PASSWORD="elastic"
# 將http_ca.crt SSL證書(shū)從容器復(fù)制到本地計(jì)算機(jī)。
docker cp es01:/usr/share/elasticsearch/config/certs/http_ca.crt .
curl --cacert http_ca.crt -u elastic:$ELASTIC_PASSWORD https://localhost:9200# 輸出
[root@hecs-148865 ~]# curl --cacert http_ca.crt -u elastic:$ELASTIC_PASSWORD https://localhost:9200
{"name" : "7e48b6d68e30","cluster_name" : "docker-cluster","cluster_uuid" : "zak41dJ-Q6qb4DPckXn7fQ","version" : {"number" : "8.9.1","build_flavor" : "default","build_type" : "docker","build_hash" : "a813d015ef1826148d9d389bd1c0d781c6e349f0","build_date" : "2023-08-10T05:02:32.517455352Z","build_snapshot" : false,"lucene_version" : "9.7.0","minimum_wire_compatibility_version" : "7.17.0","minimum_index_compatibility_version" : "7.0.0"},"tagline" : "You Know, for Search"
}# 關(guān)閉es的ssl證書(shū)校驗(yàn),否則整合springboot使用需要證書(shū),很麻煩,修改elasticsearch.yml文件設(shè)置 xpack.security.enabled: false# 將docker文件復(fù)制到本地修改完再上傳docker cp es01:/usr/share/elasticsearch/config/elasticsearch.yml /home/docker/mydata/elastic-search/elasticsearch.yml
docker cp /home/docker/mydata/elastic-search/elasticsearch.yml es01:/usr/share/elasticsearch/config/elasticsearch.yml# 修改完es,發(fā)現(xiàn)kibana連接不上了,kibana.yml也需要修改
docker cp kibana:/usr/share/kibana/config/kibana.yml /home/docker/mydata/elastic-search/kibana.yml
docker cp /home/docker/mydata/elastic-search/kibana.yml kibana:/usr/share/kibana/config/kibana.yml # 三、安裝啟動(dòng)kibana
docker pull docker.elastic.co/kibana/kibana:8.9.1
docker run --name kibana --net elastic-demo -p 5601:5601 docker.elastic.co/kibana/kibana:8.9.1# 四、訪問(wèn)kibana面板
# http://0.0.0.0:5601/?code=376811
# http://120.46.82.xxx:5601/?code=537195# Kibana:http://120.46.82.xxx:5601/app/dev_tools#/console
# ES-API:https://120.46.82.xxx:9200/,(8.x版本之后開(kāi)啟了SSL校驗(yàn),需要HTTPS驗(yàn)證https://www.cnblogs.com/chaos-li/p/13667687.html,也可修改elasticsearch.yml關(guān)閉)
2、ES的數(shù)據(jù)模型
方便理解,類(lèi)比關(guān)系型數(shù)據(jù)庫(kù)的數(shù)據(jù)模型,ES的數(shù)據(jù)模型分為
- index(索引):類(lèi)比一張表,代表文檔數(shù)據(jù)的集合,文檔指的是ES中存儲(chǔ)的一條數(shù)據(jù)。
- type(文檔類(lèi)型):在新版的Elasticsearch中,已經(jīng)不使用文檔類(lèi)型了,在ES6.x版本中1個(gè)索引indices只能創(chuàng)建對(duì)應(yīng)一個(gè)types,因?yàn)椴煌瑃ypes下的字段不能沖突,刪除types也不會(huì)釋放空間,推薦需要多個(gè)types時(shí)候直接創(chuàng)建多個(gè)indices。在ES7.x版本中直接刪除掉了type的概念。在Elasticsearch老的版本中文檔類(lèi)型,代表一類(lèi)文檔的集合,index(索引)類(lèi)似mysql的數(shù)據(jù)庫(kù)、文檔類(lèi)型類(lèi)似MySQL的表。既然新的版本文檔類(lèi)型沒(méi)什么作用了,那么index(索引)就類(lèi)似mysql的表的概念,ES沒(méi)有數(shù)據(jù)庫(kù)的概念了。
- Document(文檔):類(lèi)比一行數(shù)據(jù),Elasticsearch是面向文檔的數(shù)據(jù)庫(kù),文檔是最基本的存儲(chǔ)單元,文檔類(lèi)似mysql表中的一行數(shù)據(jù)。簡(jiǎn)單的說(shuō)在ES中,文檔指的就是一條JSON數(shù)據(jù),JSON數(shù)據(jù)的字段可以是任意的,這些Documents屬于一個(gè)index。
- Field(文檔字段):類(lèi)比一個(gè)字段,文檔由多個(gè)字段(Field)組成。
- Mapping(映射):類(lèi)比元數(shù)據(jù),映射定義了文檔中每個(gè)字段的數(shù)據(jù)類(lèi)型、分析器和索引選項(xiàng)。映射是用于索引和搜索的關(guān)鍵元素,它決定了如何存儲(chǔ)和檢索文檔中的數(shù)據(jù)。
Elasticsearch 支持如下簡(jiǎn)單域類(lèi)型:
- 字符串: text(string在5.4版本被text替代)、keyword
- 當(dāng)一個(gè)字段需要用于全文搜索(會(huì)被分詞), 比如產(chǎn)品名稱、產(chǎn)品描述信息, 就應(yīng)該使用text類(lèi)型,不能排序,很少用于聚合。
- 當(dāng)一個(gè)字段需要按照精確值進(jìn)行過(guò)濾、排序、聚合等操作時(shí), 就應(yīng)該使用keyword類(lèi)型.不會(huì)被分詞。
- 整數(shù) : byte, short, integer, long
- 浮點(diǎn)數(shù): float, double
- 布爾型: boolean
- 日期: date
其他還有object、array、geo、binary
3、ES的領(lǐng)域特定查詢語(yǔ)言(Query DSL)
ES如何查詢參考
- https://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html
- ES權(quán)威指南DSL語(yǔ)法:https://www.elastic.co/guide/cn/elasticsearch/guide/current/empty-search.html
- 博客總結(jié)命令:https://www.cnblogs.com/machangwei-8/p/14979956.html#_label1
3.1、簡(jiǎn)單基本查詢
以下是 Elasticsearch 中最常用的一些命令匯總,簡(jiǎn)單列舉具體看文檔
1、Index 命令:
創(chuàng)建一個(gè)索引:PUT /<index_name>
刪除一個(gè)索引:DELETE /<index_name>
列出所有索引:GET /_cat/indices?v2、Document 命令:
添加或更新文檔:PUT /<index_name>/_doc/<document_id>
獲取文檔:GET /<index_name>/_doc/<document_id>
刪除文檔:DELETE /<index_name>/_doc/<document_id>3、檢索命令:
使用查詢字符串搜索:GET /<index_name>/_search?q=<query_string>
使用請(qǐng)求體搜索:POST /<index_name>/_search
{"query": {...}
}4、聚合命令:
執(zhí)行聚合操作:POST /<index_name>/_search
{"aggs": {...},"size": 0
}5、映射命令:
獲取索引映射定義:GET /<index_name>/_mapping
更新索引映射:PUT /<index_name>/_mapping
{"properties": {...}
}6、設(shè)置命令:
獲取集群設(shè)置:GET /_cluster/settings
修改集群設(shè)置(實(shí)時(shí)生效):PUT /_cluster/settings
{"persistent": {...},"transient": {...}
}
DSL的寫(xiě)法很多,這里列舉出練習(xí)demo的聚合查詢
# 1、統(tǒng)計(jì)每個(gè)州的state聚合查詢
GET /accounts/_search
{"size": 0,"aggs": {"group_by_state": {"terms": {"field": "state.keyword"}}}
}
GET /accounts/_search# 2、嵌套聚合查詢
# 對(duì)每個(gè)州的state分組的基礎(chǔ)上,聚合求出平均balance
GET /accounts/_search
{"size": 0,"aggs": {"sichaolong": {"terms": {"field": "state.keyword"},"aggs": {"average_balance": {"avg": {"field": "balance"}}}}}
}# 3、聚合結(jié)果排序查詢
GET /accounts/_search
{"size": 0,"aggs": {"group_by_state": {"terms": {"field": "state.keyword","order": {"average_balance": "desc"}},"aggs": {"average_balance": {"avg": {"field": "balance"}}}}}
}
3.2、復(fù)合查詢
多種條件組合的查詢,在ES中叫做復(fù)合查詢,ES提供5種復(fù)合查詢方式。
- bool query(布爾查詢)
- boosting query(提高查詢)
- constant_score(固定分?jǐn)?shù)查詢)
- dis_max(最佳匹配查詢)
- function_score(函數(shù)查詢)
具體的用法直接看官網(wǎng)文檔。
4、ES全文查詢搜索
4.1、match相關(guān)查詢和term查詢的區(qū)別
match以及相關(guān)的match_phrase、match_phrase_prefix 查詢本質(zhì)上是term查詢的組合。
match查詢和term查詢是Elasticsearch中兩種常用的查詢類(lèi)型,它們?cè)谔幚矸绞缴下杂胁煌P枰⒁獾氖?#xff0c;在Elasticsearch中,text字段通常適合使用Match查詢,而keyword字段適合使用Term查詢,這取決于你想要實(shí)現(xiàn)的具體需求和查詢場(chǎng)景。
Match查詢
- Match查詢是一種全文搜索查詢,它會(huì)將查詢字符串分析成詞項(xiàng),并根據(jù)相關(guān)性來(lái)評(píng)分。默認(rèn)情況下,它會(huì)嘗試將查詢字符串與目標(biāo)字段中的所有詞項(xiàng)進(jìn)行匹配。
- Match查詢還支持布爾操作符(AND、OR和NOT)以及短語(yǔ)搜索等高級(jí)功能。
例如,對(duì)于一個(gè)名為"title"的字段,使用Match查詢可以執(zhí)行如下查詢:
{"query": {"match": {"title": "quick brown fox"}}
}
Term查詢
- Term查詢是一種精確匹配查詢,它會(huì)將查詢字符串作為整體進(jìn)行匹配,而不會(huì)對(duì)其進(jìn)行分析或拆解為詞項(xiàng)。
- 默認(rèn)情況下,Term查詢是區(qū)分大小寫(xiě)的。
例如,對(duì)于一個(gè)名為"user.keyword"的關(guān)鍵字字段,使用Term查詢可以執(zhí)行如下查詢:
{"query": {"term": {"user.keyword": {"value": "john smith"}}}
}
match查詢的步驟
- 檢查文檔字段類(lèi)型:檢查文檔的字段類(lèi)型是否是全文檢索字段
- 分析查詢字符串:查詢字符串本身也需要分詞,如果是單個(gè)詞,執(zhí)行一個(gè)詞的term查詢。如果是多個(gè)詞,那么會(huì)被分詞,執(zhí)行多次term查詢,然后結(jié)果合并。
- 查找匹配文檔:倒排索引(后面以及往期文章會(huì)詳細(xì)介紹)找到文檔
- 為每個(gè)文檔評(píng)分:用 term 查詢計(jì)算每個(gè)文檔相關(guān)度評(píng)分 _score ,主要依據(jù)詞頻(查詢?cè)~在某文檔出現(xiàn)的頻率)、反向文檔頻率(查詢?cè)~在所有文檔中出現(xiàn)的頻率)、字段內(nèi)容的長(zhǎng)度 相結(jié)合計(jì)算得出。
下面查詢1、2兩個(gè)查詢結(jié)果是一致的,查詢3、4兩個(gè)查詢結(jié)果是一致的
# 查詢1
GET /test-dsl-match/_search
{"query": {"match": {"title": "BROWN DOG","operator": "or" # 默認(rèn)被省略了,缺省就是or}}
}# 查詢2
GET /test-dsl-match/_search
{"query": {"bool": {"should": [{"term": {"title": "brown"}},{"term": {"title": "dog"}}]}}# 查詢3
GET /test-dsl-match/_search
{"query": {"match": {"title": "BROWN DOG","operator": "and"}}
}# 查詢4
GET /test-dsl-match/_search
{"query": {"bool": {"must": [{"term": {"title": "brown"}},{"term": {"title": "dog"}}]}}另外match的匹配精度也是可以配置的,如果用戶給定 3 個(gè)查詢?cè)~,想查找至少包含其中 2 個(gè)的文檔,該如何處理?
將 operator 操作符參數(shù)設(shè)置成 and 或者 or 都是不合適的。match 查詢支持 minimum_should_match 最小匹配參數(shù),這讓我們可以指定必須匹配的詞項(xiàng)數(shù)用來(lái)表示一個(gè)文檔是否相關(guān)。
我們可以將其設(shè)置為某個(gè)具體數(shù)字,更常用的做法是將其設(shè)置為一個(gè)百分?jǐn)?shù),因?yàn)槲覀儫o(wú)法控制用戶搜索時(shí)輸入的單詞數(shù)量,查詢5、6結(jié)果也是等價(jià)的。
# 查詢5
GET /test-dsl-match/_search
{"query": {"match": {"title": {"query":"quick brown dog","minimum_should_match": "75%"}}}
}# 查詢6
GET /test-dsl-match/_search
{"query": {"bool": {"should": [{ "match": { "title": "quick" }},{ "match": { "title": "brown" }},{ "match": { "title": "dog" }}],"minimum_should_match": 2 }}
}
4.2、match_phrase查詢match_phrase_prefix的區(qū)別
match_phrase本質(zhì)上是多個(gè)有序term查詢。
前面說(shuō)match如果涉及多個(gè)詞會(huì)被拆分為多個(gè)term查詢,而且多個(gè)term是按照or查詢的。
如果想查詢某個(gè)段落,可以使用match_pharse、match_phrase_prefix。關(guān)于兩者是有差別的,match_phrase往往會(huì)被認(rèn)為是查詢字符串不被分詞,直接去文檔檢索,這是錯(cuò)誤的,其實(shí)match_phrase也是會(huì)對(duì)查詢字符串進(jìn)行分詞的,只不過(guò)相比match那種方式,分詞之后的順序是保證的。而match_phrase_prefix對(duì)應(yīng)的是上述情況。
如
match_phrase本質(zhì)是連續(xù)的term的查詢,所以f并不是一個(gè)分詞,不滿足term查詢,所以最終查不出任何內(nèi)容了。
而match_phrase_prefix可以查到。另外還有一個(gè)match_bool_prefix,本質(zhì)上可以轉(zhuǎn)化為
GET /test-dsl-match/_search
{"query": {"bool" : {"should": [{ "term": { "title": "quick" }},{ "term": { "title": "brown" }},{ "prefix": { "title": "f"}}]}}
}
4.3、其他查詢
類(lèi)似match的查詢
- multi_match:對(duì)多個(gè)字段同時(shí)查詢
- query_string:支持多內(nèi)容通過(guò)運(yùn)算符組合
- interval:查找的內(nèi)容字符串在原文檔中需要保持順序
對(duì)于term查詢 // TODO
5、使用
使用方式很多,官網(wǎng)推薦使用Java API Client,Spring Data Elasticsearch是高級(jí)封裝,隨著依賴的升級(jí)以及ES的升級(jí),可能后續(xù)不是很容易維護(hù)。
- TransportClient:TransportClient 在 Elasticsearch 7.0.0 中已被棄用,取而代之的是 Java High Level REST Client,并將在 Elasticsearch 8.0中刪除。在項(xiàng)目中不再建議使用,詳見(jiàn)官方鏈接:https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/java-api.html#java-api
- Java High Level REST Client 在 Elasticsearch 7.15.0 中已棄用,取而代之的是 Java API Client。在項(xiàng)目中不再建議使用,詳見(jiàn)官方鏈接:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/index.html
- Java API Client:官方推薦使用的方式。詳見(jiàn)官方鏈接:https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/index.html
- Spring Data Elasticsearch:關(guān)鍵功能領(lǐng)域是一個(gè)以 POJO 為中心的模型,用于與 Elastichsearch 文檔進(jìn)行交互,并輕松編寫(xiě)存儲(chǔ)庫(kù)數(shù)據(jù)訪問(wèn)層。類(lèi)似MyBatis那種方式,有著Repository、Service等抽象。springboot、spring-data-elasticsearch、elasticsearch版本對(duì)應(yīng)https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#preface.requirements
參考:
- http://masikkk.com/article/Elasticsearch/
- https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/installation.html
三、原理分析
1、搜索引擎流程
全文檢索可以分為索引、搜索返回兩個(gè)階段,過(guò)程主要分為四個(gè)部分
- 查詢分析:搜索內(nèi)容自然語(yǔ)言處理如敏感詞過(guò)濾,錯(cuò)別字糾正等。
- 分詞技術(shù):搜索內(nèi)容拆成詞條,以下是一些常用的 Lucene 分詞器
- StandardAnalyzer:標(biāo)準(zhǔn)分詞器,它基于 Unicode 文本字符邊界來(lái)劃分詞條,并去除一些停用詞,如介詞、連詞等。
- SimpleAnalyzer:簡(jiǎn)單分詞器,將文本轉(zhuǎn)換為小寫(xiě),并根據(jù)非字母字符進(jìn)行劃分
- WhitespaceAnalyzer:空格分詞器,通過(guò)空格字符進(jìn)行劃分。
- KeywordAnalyzer:關(guān)鍵字分詞器,將整個(gè)輸入當(dāng)作一個(gè)詞條,不進(jìn)行進(jìn)一步劃分。
- StopAnalyzer:停用詞分詞器,類(lèi)似于標(biāo)準(zhǔn)分詞器,但還會(huì)移除一些自定義的停用詞。此外,還有很多其他特殊用途的分詞器可供選擇,
- IKAnalyzer、SmartChineseAnalyzer 等,比如中文領(lǐng)域的。
- 關(guān)鍵詞檢索:在倒排索引庫(kù)索引,搜索找到具體的文檔數(shù)據(jù)。
- 搜索排序返回:對(duì)多個(gè)文檔進(jìn)行相關(guān)度計(jì)算,排序,返回?cái)?shù)據(jù)。
2、Lucene相關(guān)原理
Lucene概述
Apache Lucene?是一個(gè) 完全用Java編寫(xiě)的高性能、全功能搜索引擎庫(kù)。https://lucene.apache.org/core/index.html
Lucene的目的是為軟件開(kāi)發(fā)人員提供一個(gè)簡(jiǎn)單易用的工具包,You need four JARs: the Lucene JAR, the queryparser JAR, the common analysis JAR, and the Lucene demo JAR.
以下是常見(jiàn)的Lucene JAR包:
- lucene-core.jar:包含了Lucene的核心功能,如倒排索引、分詞器、查詢解析器等。
- lucene-analyzers-common.jar:包含了一系列常用的分析器,用于將文本進(jìn)行分詞和標(biāo)準(zhǔn)化處理。
- lucene-queryparser.jar:包含了用于解析用戶輸入的搜索查詢字符串并生成相應(yīng)查詢對(duì)象的工具。
- lucene-highlighter.jar:包含了實(shí)現(xiàn)搜索結(jié)果高亮顯示的組件。
- lucene-suggest.jar:包含了構(gòu)建自動(dòng)補(bǔ)全和建議功能的相關(guān)類(lèi)和接口。
- lucene-grouping.jar:包含了根據(jù)指定字段對(duì)搜索結(jié)果進(jìn)行分組的功能。
使用Lucene代碼demo
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.*;
import org.apache.lucene.search.*;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import pojo.Student;import java.io.IOException;
import java.nio.file.Paths;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;/*** @Auther: sichaolong* @Date: 2023/9/5 14:29* @Description: 簡(jiǎn)單使用Lucen工具包*/
public class LuceneDemo {// 模擬數(shù)據(jù)public static final List<Student> STUDENT_LIST = new ArrayList<Student>(Arrays.asList(new Student("1", "張三", 18, "北京市海淀區(qū)溫泉鎮(zhèn)", "法外狂徒"),new Student("2", "李四", 19, "北京市海底區(qū)東升鎮(zhèn)", "唱、跳、rap"),new Student("3", "王武", 20, "北京市海淀區(qū)上莊鎮(zhèn)", "吸煙、喝酒、燙頭"),new Student("4", "王五", 21, "北京市海淀區(qū)蘇家坨鎮(zhèn)", "點(diǎn)煙、倒酒、給別人燙頭"),new Student("5", "麻六", 18, "北京市海淀區(qū)西北旺鎮(zhèn)", "吃飯、喝酒"),new Student("6", "酸菜", 17, "統(tǒng)一老壇酸菜牛肉面", "帶著酸菜"),new Student("7", "麻辣", 10, "統(tǒng)一麻辣牛肉面", "帶著沒(méi)有牛肉的牛肉面"),new Student("8", "老母雞", 14, "康師傅老母雞湯面", "沒(méi)有老母雞的老母雞面"),new Student("9", "醬香", 15, "醬香味小龍蝦", "88元一斤"),new Student("10", "蒜蓉", 19, "蒜蓉味小龍蝦", "100元一斤")));// 數(shù)據(jù)存儲(chǔ)路徑private static final String INDEX_PATH = "./lucene-data-demo/index";public static void main(String[] args) throws IOException, ParseException {// createIndex();search();}/*** 創(chuàng)建索引功能的測(cè)試** @throws Exception*/public static void createIndex() throws IOException {// 1. 創(chuàng)建文檔對(duì)象List<Document> documents = new ArrayList<Document>();for (Student student : STUDENT_LIST) {Document document = new Document();// 2. 給文檔對(duì)象添加域// add方法: 把域添加到文檔對(duì)象中, field參數(shù): 要添加的域// TextField: 文本域, 屬性name:域的名稱, value:域的值, store:指定是否將域值保存到文檔中document.add(new TextField("id", student.getId() + "", Field.Store.YES));document.add(new TextField("name", student.getName(), Field.Store.YES));document.add(new TextField("age", student.getAge() + "", Field.Store.YES));document.add(new TextField("address", student.getAddress(), Field.Store.YES));document.add(new TextField("desc", student.getDesc(), Field.Store.YES));// 將文檔對(duì)象添加到文檔對(duì)象集合中documents.add(document);}// 3. 創(chuàng)建分析器對(duì)象(Analyzer), 用于分詞Analyzer analyzer = new StandardAnalyzer();// 4. 創(chuàng)建索引配置對(duì)象(IndexWriterConfig), 用于配置LuceneIndexWriterConfig indexConfig = new IndexWriterConfig(analyzer);// 5. 創(chuàng)建索引庫(kù)目錄位置對(duì)象(Directory), 指定索引庫(kù)的存儲(chǔ)位置,創(chuàng)建一個(gè)indexWriter對(duì)象,IndexWriter indexWriter = new IndexWriter(FSDirectory.open(Paths.get(INDEX_PATH)), indexConfig);indexWriter.addDocuments(documents);indexWriter.commit();// 6、關(guān)閉indexWriter對(duì)象。java11報(bào)錯(cuò)解決參考:http://community.jedit.org/?q=node/view/37964indexWriter.close();}/*** 搜索索引測(cè)試*/public static void search() throws IOException, ParseException {// 創(chuàng)建一個(gè)Directory對(duì)象,也就是索引庫(kù)存的位置。Directory directory = FSDirectory.open(Paths.get(INDEX_PATH));// 創(chuàng)建一個(gè)IndexReader對(duì)象,需要指定Directory對(duì)象。IndexReader indexReader = DirectoryReader.open(directory);// 創(chuàng)建一個(gè)indexSearcher對(duì)象,需要指定IndexReader對(duì)象。IndexSearcher indexSearcher = new IndexSearcher(indexReader);// 創(chuàng)建一個(gè)TermQuery對(duì)象,指定查詢的域和查詢的關(guān)鍵詞。Query query = new TermQuery(new Term("name", "張"));// 執(zhí)行查詢TopDocs topDocs = indexSearcher.search(query, 10);System.out.println("查詢結(jié)果的總條數(shù):" + topDocs.totalHits);// 返回查詢結(jié)果,遍歷查詢結(jié)果并輸出for (ScoreDoc scoreDoc : topDocs.scoreDocs) {//scoreDoc.doc屬性就是document對(duì)象的id//根據(jù)document的id找到document對(duì)象Document document = indexSearcher.doc(scoreDoc.doc);System.out.println(document.get("id"));//System.out.println(document.get("content"));System.out.println(document.get("name"));System.out.println(document.get("address"));System.out.println(document.get("desc"));System.out.println("-------------------------");}/*** 輸出:* 查詢結(jié)果的總條數(shù):1* 1* 張三* 北京市海淀區(qū)溫泉鎮(zhèn)* 法外狂徒* -------------------------*/// 關(guān)閉indexReader對(duì)象indexReader.close();}}
// TODO
如何實(shí)現(xiàn)"快"、"準(zhǔn)"搜索,Lucene很復(fù)雜, 檢索引擎最核心的部分就是索引的設(shè)計(jì)、數(shù)據(jù)的存儲(chǔ),重點(diǎn)關(guān)注索引如何設(shè)計(jì)?如何儲(chǔ)存?用什么數(shù)據(jù)結(jié)構(gòu)?數(shù)據(jù)如何組織?如何壓縮?
從數(shù)據(jù)層面分析,整個(gè)Lucene把需要處理的數(shù)據(jù)分為這么幾類(lèi), 前四種是所有檢索引擎都會(huì)保存的數(shù)據(jù),后三種是Lucene特有的
- PostingList 倒排表,也就是term->[doc1, doc3, doc5]這種倒排索引數(shù)據(jù) 。真實(shí)的倒排記錄也并非一個(gè)鏈表,而是采用了SkipList、BitSet等結(jié)構(gòu)。
- TermDict, 從term和PostingList的映射關(guān)系,這種映射一般都用FST這種數(shù)據(jù)結(jié)構(gòu)來(lái)表示,這種數(shù)據(jù)結(jié)構(gòu)其實(shí)是一種有向圖,類(lèi)似于前綴樹(shù),所以Lucene這里就叫BlockTree, 其實(shí)我更習(xí)慣叫它TermDict。
- StoredField 存進(jìn)去的原始信息 (行式組織數(shù)據(jù)存儲(chǔ)),通常用于存儲(chǔ)需要在搜索結(jié)果中返回的field,以便在檢索文檔時(shí)能夠獲取原始字段值。
- DocValue 鍵值數(shù)據(jù),通常用于存儲(chǔ)需要在搜索結(jié)果中需要聚合、排序、篩選的field,這種數(shù)據(jù)主要是用來(lái)對(duì)于高級(jí)查詢加速對(duì)字段的排序、篩選的。(列式組織數(shù)據(jù)存儲(chǔ))
- TermVector,詞向量信息,主要記一個(gè)不同term的全局出現(xiàn)頻率等信息。
- Norms,用來(lái)存儲(chǔ)Normalisation信息,分詞后的文本進(jìn)行規(guī)范化處理(小寫(xiě)轉(zhuǎn)換,刪除特殊字符)然后存儲(chǔ),以提高搜索和匹配的準(zhǔn)確性。
- PointValue 用來(lái)加速 range Query的信息,實(shí)現(xiàn)基于BKDTree。
針對(duì)不同的數(shù)據(jù)結(jié)構(gòu)采用不同的字典索引,倒排索引基于字典樹(shù)使用了FST模型壓縮索引,使用SkipList和BitSet加速多條件查詢,磁盤(pán)存儲(chǔ)組織PointValue組織形式基于BKDTree等結(jié)構(gòu)加速范圍查詢。
參考:
- Lucene源碼剖析
- 理解ElasticSearch工作原理
- 使用Java調(diào)用Lucene實(shí)現(xiàn)簡(jiǎn)單demo
- 倒排索引:ES倒排索引底層原理及FST算法的實(shí)現(xiàn)過(guò)程
- Lucene BKD樹(shù)-動(dòng)態(tài)磁盤(pán)優(yōu)化BSP樹(shù)
2.1、倒排索引之FST(Finite State Transducers)
概述:也常被稱為反向索引,是一種索引方法,被用來(lái)存儲(chǔ)在全文搜索下某個(gè)詞條在一個(gè)文檔或者一組文檔中的存儲(chǔ)位置的映射,它是文檔檢索系統(tǒng)中最常用的數(shù)據(jù)結(jié)構(gòu)。
采用映射表記錄哪些詞條出現(xiàn)在哪些文檔中,然后實(shí)現(xiàn)快速檢索。
舉例
隨著的詞條的增多,這個(gè)倒排記錄表也越來(lái)越大,倒排記錄也越來(lái)越多,每次遍歷查找的效率不高肯定不行。而且全部將倒排記錄加載進(jìn)內(nèi)存也吃不消。
因此Lucene在前面倒排記錄表前加了一層,增加一個(gè)字典結(jié)構(gòu)索引Term Index,字典結(jié)構(gòu)搜索場(chǎng)景用的比較多,實(shí)現(xiàn)方式有很多
以Trie樹(shù)實(shí)現(xiàn)的字典為例,他不存儲(chǔ)所有的單詞,只存儲(chǔ)單詞前綴,從Trie樹(shù)索引樹(shù)找詞條,最后找到詞條對(duì)應(yīng)的文檔列表。
下圖是一個(gè)簡(jiǎn)化的Trie樹(shù),真實(shí)的Trie實(shí)現(xiàn)kv結(jié)構(gòu)實(shí)現(xiàn)方式有很多種
- 樹(shù)節(jié)點(diǎn)為固定長(zhǎng)度的數(shù)組,數(shù)組的元素需要包含兩個(gè)域,一個(gè)是k,一個(gè)是next指向下一節(jié)點(diǎn)。一般一個(gè)節(jié)點(diǎn)需要固定長(zhǎng)度,如存儲(chǔ)英文字母的節(jié)點(diǎn)長(zhǎng)度為26,缺點(diǎn):對(duì)于英文做前綴還能接受,中文字符集太多,太占內(nèi)存。時(shí)間復(fù)雜度O(1),空間復(fù)雜度O(N)。
- 樹(shù)節(jié)點(diǎn)為一個(gè)鏈表,鏈表節(jié)點(diǎn)需要包含三個(gè)域,一個(gè)是k,一個(gè)是next指向下一個(gè)鏈表節(jié)點(diǎn),另外一個(gè)child指向下一個(gè)樹(shù)木節(jié)點(diǎn)。缺點(diǎn):每次都需要從鏈表頭節(jié)點(diǎn)開(kāi)始訪問(wèn)。時(shí)間復(fù)雜度O(N),空間復(fù)雜度O(1)
- 樹(shù)節(jié)點(diǎn)為哈希表。時(shí)間復(fù)雜度為上述兩種方式之間。
Finite State Transducer (FST)是一種計(jì)算模型,它基于有限狀態(tài)自動(dòng)機(jī)(FSM)并添加了輸出功能。
主要描述有限個(gè)狀態(tài)(睡覺(jué)、玩耍、吃飯、躲藏、貓砂窩)與狀態(tài)轉(zhuǎn)移動(dòng)作(提供食物、有大聲音等)之間的關(guān)系。
比如算法中的動(dòng)態(tài)規(guī)劃思想就是一種對(duì)狀態(tài)的抽象,核心的就是抽象狀態(tài)轉(zhuǎn)移方程,比如爬樓梯算法的狀態(tài)轉(zhuǎn)移方程為 f(n) = f(n-2) + f(n-1)
基于FSM實(shí)現(xiàn)的字典FST,不但能共享前綴還能共享后綴(壓縮數(shù)據(jù))。不但能判斷查找的key是否存在,還能給出響應(yīng)的輸出output。 它在時(shí)間復(fù)雜度和空間復(fù)雜度上都做了最大程度的優(yōu)化,使得Lucene能夠?qū)erm Dictionary完全加載到內(nèi)存,快速的定位Term找到響應(yīng)的output(posting倒排列表)
普通的Trie和FST對(duì)比:
Trie有重復(fù)的3個(gè)final狀態(tài)3,8,11. 而8,11都是s轉(zhuǎn)移,是可以合并的。FST可以看做是一個(gè)帶有度的有向無(wú)環(huán)圖
Lucene從4開(kāi)始大量使用的數(shù)據(jù)結(jié)構(gòu)是FST。FST有兩個(gè)優(yōu)點(diǎn):
1)空間占用小。通過(guò)對(duì)詞典中單詞前綴和后綴的重復(fù)利用,壓縮了存儲(chǔ)空間。
2)查詢速度快。O(len(str))的查詢時(shí)間復(fù)雜度。
缺點(diǎn):
1)FST通常不適合頻繁的插入和刪除操作,因?yàn)樗臉?gòu)建和修改開(kāi)銷(xiāo)較大,需要調(diào)整有向圖邊上的度,出現(xiàn)公共前綴、后綴就要調(diào)整。
我們可以將FST當(dāng)做Key-Value數(shù)據(jù)結(jié)構(gòu)來(lái)進(jìn)行使用,特別在對(duì)內(nèi)存開(kāi)銷(xiāo)要求少的應(yīng)用場(chǎng)景。FST壓縮率一般在3倍~20倍之間,相對(duì)于TreeMap/HashMap的膨脹3倍,內(nèi)存節(jié)省就有9倍到60倍!(摘自:把自動(dòng)機(jī)用作 Key-Value 存儲(chǔ))
參考:
- 字典數(shù)據(jù)結(jié)構(gòu)-FST(Finite State Transducers)
- FSM有限狀態(tài)自動(dòng)機(jī)-維基百科
- 關(guān)于Lucene的詞典FST深入剖析
2.2、倒排索引之SkipList、BitSet
倒排索引采用這兩種數(shù)據(jù)結(jié)構(gòu)主要是為了多條件查詢,SkipList用于構(gòu)建Term Dict,BitSet用于對(duì)查找到的多個(gè)倒排記錄指向的docids做交集合。
對(duì)于FST字典結(jié)構(gòu)也不是完全映射倒排記錄表的,也是做的一個(gè)前綴,因?yàn)榻M合實(shí)在太多了,實(shí)際是類(lèi)似一個(gè)目錄,通過(guò)FST找出Term Dict的起始指針、結(jié)束指針位置。
如單查詢過(guò)濾條件 name =Alice 的過(guò)程就是先利用FST結(jié)構(gòu)從Term Index找到Alice在Term Dict 的大概位置,然后再?gòu)腡erm Dict里利用SkipList精確地找到Alice這個(gè)term,然后找到指向的docids
如多條件查詢 name=Alice AND gender=女 就是把兩個(gè) posting list 做一個(gè)“與”的合并。也就是取兩個(gè)docids的交集合。
如何高效的合并呢?
- 如果查詢的filter條件緩存到了內(nèi)存中(以BitSet的形式),那么合并就是兩個(gè)BitSet的AND,舉例如[1,3,4]壓縮進(jìn)位圖中就是[1,0,1,1]。Redis中的BitMap就是這個(gè)原理,將大量數(shù)據(jù)壓縮進(jìn)位圖如應(yīng)用布隆過(guò)濾器。
- 如果查詢的filter沒(méi)有緩存,那么就用SkipList的方式去遍歷兩個(gè)on disk的posting list。找一個(gè)最短的post list先遍歷,Block Max WAND(塊最大WAND)策略是一種用于提高布爾查詢性能的優(yōu)化技術(shù),特別是對(duì)于AND操作(邏輯與)的查詢,使用最小的IO成本過(guò)濾掉那些不匹配的docid,比如name=Alice對(duì)應(yīng)的是[1,3,4],gender = 女對(duì)應(yīng)的是[5,6,8,9,29,34,54,545,54545]
// 類(lèi)似木桶原理,如果list1=[1,3,4]遍歷到完了,遍歷3次,此時(shí)list2=[5,6,8,9,29,34,54,545,54545]才讀取第一個(gè)元素。
// 雙指針寫(xiě)法def block_max_wand_intersection(list1, list2):result = []i, j = 0, 0while i < len(list1) and j < len(list2):if list1[i] == list2[j]:# 如果文檔ID匹配,將其添加到結(jié)果中result.append(list1[i])i += 1j += 1elif list1[i] < list2[j]:# 如果第一個(gè)列表中的文檔ID較小,增加其索引以找到更大的文檔IDi += 1else:# 如果第二個(gè)列表中的文檔ID較小,增加其索引以找到更大的文檔IDj += 1return result# 使用示例
name_list = [1, 3, 4]
gender_list = [5, 6, 8, 9, 29, 34, 54, 545, 54545]
result_intersection = block_max_wand_intersection(name_list, gender_list)
print(result_intersection)
2.3、Stored Field存儲(chǔ)方式
比如為什莫要區(qū)分Stored Field(行式存儲(chǔ))和 Doc Value(列式存儲(chǔ))?是否可以手動(dòng)指定?
// Lucene API
// 常規(guī)行式存儲(chǔ):document.add(new TextField("age", student.getAge() + "", Field.Store.YES));
document.add(new SortedDocValuesField("age", new BytesRef(student.getAge())));// ES API
PUT /my_index
{"mappings": {"properties": {"field1": {"type": "text","store": true},"field2": {"type": "text","store": false}}}
}
主要是有兩方面的原因:性能、存儲(chǔ)成本。
- 性能考慮:行式存儲(chǔ)document數(shù)據(jù)可以方便一次獲取全部需要查詢展示的fields數(shù)據(jù),按照列式存儲(chǔ)field可以方便排序、統(tǒng)計(jì)、篩選。
- 存儲(chǔ)考慮:Doc Values通常用于存儲(chǔ)靜態(tài)字段值,例如文檔的ID、日期、標(biāo)簽或其他結(jié)構(gòu)化數(shù)據(jù)。這些字段值是文檔的一部分,但它們不依賴于查詢條件或文檔匹配度,因此適合使用Doc Values進(jìn)行列式存儲(chǔ)。
代碼層面?
參考前面簡(jiǎn)單實(shí)用lucnen的代碼,整個(gè)過(guò)程邏輯,前四層式邏輯調(diào)用層,中間層是索引鏈?zhǔn)教幚韺?/p>
圖源:https://zhuanlan.zhihu.com/p/384486147
DefaultIndexingChain是一個(gè)非常核心的類(lèi),負(fù)責(zé)對(duì)當(dāng)前文檔個(gè)建索引的核心操作,它定義了什么時(shí)候該寫(xiě)倒排拉鏈,什么時(shí)候?qū)慏ocValue,什么時(shí)候?qū)懭隨toredField 等。 processDocument 是整個(gè)索引鏈個(gè)入口方法,它會(huì)負(fù)責(zé)將整個(gè)文檔按照Field拆開(kāi),分別調(diào)用下面的processField方法:
private int processField(IndexableField field, long fieldGen, int fieldCount) throws IOException {String fieldName = field.name();IndexableFieldType fieldType = field.fieldType();PerField fp = null;if (fieldType.indexOptions() == null) {throw new NullPointerException("IndexOptions must not be null (field: \"" + field.name() + "\")");}// Invert indexed fields:// 在該Field上面建倒排表if (fieldType.indexOptions() != IndexOptions.NONE) {fp = getOrAddField(fieldName, fieldType, true);boolean first = fp.fieldGen != fieldGen;fp.invert(field, first);if (first) {fields[fieldCount++] = fp;fp.fieldGen = fieldGen;}} else {verifyUnIndexedFieldType(fieldName, fieldType);}// Add stored fields: 存儲(chǔ)該field的storedFieldif (fieldType.stored()) {if (fp == null) {fp = getOrAddField(fieldName, fieldType, false);}if (fieldType.stored()) {String value = field.stringValue();if (value != null && value.length() > IndexWriter.MAX_STORED_STRING_LENGTH) {throw new IllegalArgumentException("stored field \"" + field.name() + "\" is too large (" + value.length() + " characters) to store");}try {storedFieldsConsumer.writeField(fp.fieldInfo, field);} catch (Throwable th) {docWriter.onAbortingException(th);throw th;}}}// 建docValueDocValuesType dvType = fieldType.docValuesType();if (dvType == null) {throw new NullPointerException("docValuesType must not be null (field: \"" + fieldName + "\")");}if (dvType != DocValuesType.NONE) {if (fp == null) {fp = getOrAddField(fieldName, fieldType, false);}indexDocValue(fp, dvType, field);}if (fieldType.pointDataDimensionCount() != 0) {if (fp == null) {fp = getOrAddField(fieldName, fieldType, false);}indexPoint(fp, field);}return fieldCount;
}
數(shù)據(jù)的落盤(pán)Lucene也是采用一些方法如vint(可變長(zhǎng))編碼方式壓縮數(shù)據(jù)存儲(chǔ)空間。
2.4、倒排索引之不可變性
Lucene倒排索引設(shè)計(jì)是不可變的,如何進(jìn)行索引與數(shù)據(jù)的維護(hù)呢?
由于倒排索引的結(jié)構(gòu)特性,在索引建立完成后對(duì)其進(jìn)行修改將會(huì)非常復(fù)雜。再加上幾層索引嵌套,更讓索引的更新變成了幾乎不可能的動(dòng)作。所以索性設(shè)計(jì)成不可改變的:倒排索引被寫(xiě)入磁盤(pán)后是不可改變的,它永遠(yuǎn)不會(huì)修改。
不變性有重要的價(jià)值:
- 并發(fā)安全:不需要鎖。如果你從來(lái)不更新索引,你就不需要擔(dān)心多進(jìn)程同時(shí)修改數(shù)據(jù)的問(wèn)題,并發(fā)鎖征用問(wèn)題也是很耗時(shí)間的。
- 緩存不用長(zhǎng)期更新:一旦索引被讀入內(nèi)核的文件系統(tǒng)緩存,便會(huì)留在哪里,由于其不變性。只要文件系統(tǒng)緩存中還有足夠的空間,那么大部分讀請(qǐng)求會(huì)直接請(qǐng)求內(nèi)存,而不會(huì)命中磁盤(pán)。這提供了很大的性能提升。其它緩存(像filter緩存),在索引的生命周期內(nèi)始終有效。它們不需要在每次數(shù)據(jù)改變時(shí)被重建,因?yàn)閿?shù)據(jù)不會(huì)變化。
- 允許數(shù)據(jù)壓縮:寫(xiě)入單個(gè)大的倒排索引允許數(shù)據(jù)壓縮,減少磁盤(pán) I/O 和 需要被緩存到內(nèi)存的索引的使用量。
缺點(diǎn):
- 主要事實(shí)是它是不可變的,你不能修改它。如果你需要讓一個(gè)新的文檔 可被搜索,你需要重建整個(gè)索引達(dá)到動(dòng)態(tài)更新索引,這要么對(duì)一個(gè)索引所能包含的數(shù)據(jù)量造成了很大的限制,要么對(duì)索引可被更新的頻率造成了很大的限制。
2.5、倒排索引之動(dòng)態(tài)更新索引
更新數(shù)據(jù)就要更新索引,Lucene采用一次寫(xiě)多次讀(write-once-read-multiple)策略來(lái)完成動(dòng)態(tài)更新索引。具體來(lái)說(shuō)就是在數(shù)據(jù)更新的過(guò)程中
- 寫(xiě)入:新增或更新的文檔首先被寫(xiě)入內(nèi)存緩沖區(qū),隨著時(shí)間的推移或達(dá)到一定大小閾值,緩沖區(qū)的內(nèi)容將被刷新到磁盤(pán)上的一個(gè)新的Segment中,同時(shí)記錄一個(gè)Commit Point文件日志,表示一次成功更新數(shù)據(jù),類(lèi)似MySQL的redolog。
- 讀取:在查詢時(shí),Lucene 首先從內(nèi)存緩沖區(qū)中查找匹配的記錄,如果未找到,則繼續(xù)在磁盤(pán)上的Segemts中進(jìn)行查找。盡量保證搜索實(shí)時(shí)性。
通過(guò)提交點(diǎn)Commit Point保證崩潰恢復(fù):在寫(xiě)入過(guò)程,每次更新數(shù)據(jù)后會(huì)進(jìn)行一次commit,每當(dāng)發(fā)生一次提交操作,就會(huì)創(chuàng)建一個(gè)新的提交點(diǎn),并將Segment cache刷盤(pán)。這樣可以應(yīng)對(duì)即使系統(tǒng)崩潰或意外關(guān)閉引起的Segment cache刷盤(pán)失敗,最近的索引更改也能夠恢復(fù)并不會(huì)丟失。另外,即使在機(jī)器故障或其他問(wèn)題導(dǎo)致索引文件出現(xiàn)損壞時(shí),Lucene也可通過(guò)檢測(cè)到損壞的提交點(diǎn)來(lái)進(jìn)行相應(yīng)的修復(fù)和恢復(fù)工作。
磁盤(pán)倒排索引合并問(wèn)題:為了優(yōu)化索引性能和空間利用率,Lucene 定期或在需要的時(shí)候會(huì)將磁盤(pán)上的多個(gè)小的Segment合并成更大的Segment。該過(guò)程可能存在的問(wèn)題:
- 索引維護(hù)開(kāi)銷(xiāo):由于頻繁的段合并和磁盤(pán)寫(xiě)入操作,索引維護(hù)需要消耗一定的時(shí)間和資源。特別是當(dāng)索引較為龐大或者更新頻率很高時(shí),索引維護(hù)可能成為性能瓶頸。在傳統(tǒng)的先寫(xiě)后讀策略中,索引段會(huì)不斷地增加,導(dǎo)致查詢時(shí)需要搜索更多的段,從而影響查詢速度。
- 空間浪費(fèi):在段合并過(guò)程中,舊的段并不會(huì)立即刪除,而是會(huì)等待所有正在讀取它們的查詢完成后再進(jìn)行刪除。因此,對(duì)于那些很少訪問(wèn)但占用大量磁盤(pán)空間的段,存在著空間浪費(fèi)的問(wèn)題。
- 讀取過(guò)期數(shù)據(jù):由于段合并操作只在某些條件滿足時(shí)才會(huì)觸發(fā),并且需要一定的時(shí)間來(lái)完成,因此,可能會(huì)出現(xiàn)在段合并過(guò)程中,讀取到已經(jīng)過(guò)期的數(shù)據(jù)的情況。
舉個(gè)例子來(lái)說(shuō)明Lucene索引合并導(dǎo)致讀取過(guò)期數(shù)據(jù)這種情況:
- 假設(shè)初始狀態(tài)下磁盤(pán)有三個(gè)段 A、B、C,其中段 B 是最新生成的。
- 在合并過(guò)程中,Lucene 將段 A 和段 B 合并成一個(gè)更大的段 AB。
- 此時(shí),如果還有查詢?nèi)蝿?wù)正在讀取段 A 中的數(shù)據(jù),但由于段合并的過(guò)程中還沒(méi)有完成,導(dǎo)致部分查詢?nèi)匀蛔x取到了段 A 中的數(shù)據(jù)。這些數(shù)據(jù)屬于已經(jīng)過(guò)期的信息,因?yàn)樗鼈円呀?jīng)被包含在了新的段 AB 中。
2.6、BKD磁盤(pán)樹(shù) // TODO
3、ES相關(guān)原理
ES就是封裝調(diào)用Lucene,提供方便的RESTful API以及高級(jí)的一些查詢,然后通過(guò)分片、副本支持分布式和高可用,也對(duì)索引等處理做了一些優(yōu)化。
3.1、倒排索引之ES索引合并優(yōu)化
在傳統(tǒng)的先寫(xiě)后讀策略中,索引段會(huì)不斷地增加,導(dǎo)致查詢時(shí)需要搜索更多的段,從而影響查詢速度。
而且索引段合并需要磁盤(pán)IO也需要考慮。針對(duì)上述問(wèn)題,ES主要做了些索引Segment的合并上的優(yōu)化
- 慢速合并策略:ES采用了一個(gè)名為“慢速合并”(slow merge)的機(jī)制來(lái)減少磁盤(pán)上的段數(shù)目和整理碎片,從而提高查詢性能。它通過(guò)將較小的段合并成更大的段來(lái)優(yōu)化索引結(jié)構(gòu)。
- 多線程執(zhí)行合并操作:ES利用多線程執(zhí)行合并操作,可以在后臺(tái)異步地進(jìn)行索引段的合并過(guò)程,不會(huì)阻塞正常的讀寫(xiě)操作。這樣可以保證合并過(guò)程對(duì)用戶的影響最小化,并且提高了合并的效率。
- 增量式索引合并(主要優(yōu)化Segment的數(shù)量):ES引入了增量式索引合并(incremental index merging)策略。當(dāng)有新的索引段生成時(shí),ES會(huì)嘗試將其與已經(jīng)存在的段進(jìn)行合并,以減少索引中的段數(shù)目。也就是防止大量索引合并堆積在一個(gè)時(shí)間。這種增量式的合并方式相比傳統(tǒng)的全局合并方式,在處理大量數(shù)據(jù)時(shí)具有更好的效率和性能。
3.2、倒排索引之ES分布式優(yōu)化
通過(guò)將索引拆分為多個(gè)分片,類(lèi)似分治的思想,每個(gè)分片可以類(lèi)比一個(gè)Lucene。通過(guò)ES集群將分片分布在不同的節(jié)點(diǎn)上。每個(gè)分片都是一個(gè)獨(dú)立的 Lucene 索引包含若干個(gè)Segment。搜索操作在每個(gè)分片上并行執(zhí)行,然后合并結(jié)果。
分片:分片是底層基礎(chǔ)的讀寫(xiě)單元,分片的目的是分割巨大索引,分片是數(shù)據(jù)的容器,文檔保存在分片內(nèi),分片又被分配到集群內(nèi)的各個(gè)節(jié)點(diǎn)里不會(huì)跨分片存儲(chǔ)。 當(dāng)你的集群規(guī)模擴(kuò)大或者縮小時(shí), Elasticsearch 會(huì)自動(dòng)的在各節(jié)點(diǎn)中遷移分片,使得數(shù)據(jù)仍然均勻分布在集群里。一個(gè)分片是一個(gè)Lucene索引,一個(gè)Lucene又分成很多Segment(每段都是一個(gè)倒排索引)。比如有100個(gè)indices(數(shù)據(jù)庫(kù)),可以拆分片到5臺(tái)機(jī)器,每臺(tái)20個(gè)indices。
- 分治減少動(dòng)態(tài)更新索引成本。
- 分片便于水平拓展。
分片副本:分片進(jìn)行副本存儲(chǔ),主分片、從分片分散分布在不同節(jié)點(diǎn),提供高可用。在索引建立的時(shí)候就已經(jīng)確定了主分片數(shù),但是副本分片數(shù)可以隨時(shí)修改。
- 系統(tǒng)高可用:索引以及數(shù)據(jù)拆分的part1、part2等等。每一份如part1也會(huì)存在副本放在其他機(jī)器上,簡(jiǎn)單說(shuō)就是part1在機(jī)器1,那么part1副本需要在其他機(jī)器上也存一份。
- 并發(fā)更新,分為主、從分片,及先寫(xiě)主分片,再寫(xiě)從分片。讀寫(xiě)請(qǐng)求會(huì)落在不同的分片上,不同的機(jī)器上,做到讀寫(xiě)分離。
關(guān)于分片數(shù)量的一些建議:分片的數(shù)量在5.x之前不能修改,在5.x-6.x之后支持一定條件的修改,可以對(duì)主分片大小拆分和縮小,分片越小,分的片就越多,應(yīng)該根據(jù)硬件和業(yè)務(wù)數(shù)據(jù)量來(lái)進(jìn)行拆分。
1、分片數(shù)量不夠時(shí),可以考慮重新建立索引,搜索1個(gè)50分片的索引和搜索50個(gè)1分片的索引效果一樣,建議是周期性創(chuàng)建新索引,如website索引index每天創(chuàng)建一個(gè)website_時(shí)間戳index,然后在website主索引進(jìn)行軟連接,這樣刪除數(shù)據(jù)時(shí)可以直接刪除某個(gè)索引,避免以id刪除文檔不會(huì)立即釋放空間,刪除的document時(shí)候只有在Lucene分段(倒排索引)合并時(shí)候才會(huì)從磁盤(pán)刪除,手動(dòng)合并會(huì)導(dǎo)致較高的I\O壓力的問(wèn)題。
2、分片數(shù)量過(guò)多:若是每天一個(gè)索引,但是某天數(shù)據(jù)量很小,可以_shrink API來(lái)減少主分片數(shù)量,減低集群管理很多分片的負(fù)載。
3.3、WAL機(jī)制優(yōu)化
WAL(Write-Ahead Log)是用來(lái)保證數(shù)據(jù)在寫(xiě)入索引之前的持久化機(jī)制,以防止數(shù)據(jù)丟失或損壞。
ES在WAL機(jī)制上做了一些升級(jí),通過(guò)Translog、異步刷新和分布式復(fù)制等措施來(lái)提高寫(xiě)入性能、降低IO延遲,并保證數(shù)據(jù)的持久性。
- ES引入了Translog(Transaction Log)作為WAL的實(shí)現(xiàn)方式。Translog是一個(gè)高效的、順序?qū)懙娜罩疚募?#xff0c;記錄了索引更新操作。當(dāng)文檔被索引時(shí),它們首先會(huì)被寫(xiě)入Translog,然后再批量刷新到內(nèi)存中的索引結(jié)構(gòu)中。通過(guò)這種方式,ES能夠提供更快的寫(xiě)入性能,而不必每次將更新操作立即寫(xiě)入磁盤(pán)。
- ES還支持異步刷新(Async Refresh)操作。當(dāng)新的文檔被索引后,在默認(rèn)情況下,ES會(huì)將這些文檔添加到內(nèi)存中的索引結(jié)構(gòu)并應(yīng)答客戶端請(qǐng)求。然后,ES會(huì)異步地將這些內(nèi)存中的變更刷新到磁盤(pán)上的Segment中,從而降低了IO延遲和硬盤(pán)寫(xiě)入壓力。
文檔數(shù)據(jù)更新的流程
- write buffer:當(dāng)更新文檔數(shù)據(jù)時(shí),它首先會(huì)將數(shù)據(jù)寫(xiě)入內(nèi)存緩沖區(qū) buffer 中,然后在新的segment buffer更新索引信息。
- write transog:同時(shí),數(shù)據(jù)還會(huì)被追加到一個(gè)稱為translog(transaction log,事務(wù)日志)的文件中。這個(gè)文件位于每個(gè)分片的本地磁盤(pán)上。只要寫(xiě)入translog成功,那么就意味著一次commit,數(shù)據(jù)這個(gè)時(shí)候就能被搜索到。
- refresh:默認(rèn)情況下,ES使用異步刷新機(jī)制定期將內(nèi)存緩沖區(qū) segemnt buffer 中的數(shù)據(jù)寫(xiě)入磁盤(pán)。刷新操作會(huì)將數(shù)據(jù)持久化到segment文件中,并清空內(nèi)存緩沖區(qū)以便接收新的寫(xiě)入請(qǐng)求。
refresh異步刷新默認(rèn)是每秒觸發(fā)一次,但也可以手動(dòng)調(diào)整該時(shí)間間隔。如果在刷新之前發(fā)生了節(jié)點(diǎn)或進(jìn)程故障,所有尚未刷入磁盤(pán)的數(shù)據(jù)都可以通過(guò)translog文件進(jìn)行恢復(fù)。
ps:操作系統(tǒng)中,磁盤(pán)文件其實(shí)都有一個(gè)操作系統(tǒng)緩存OS Cache,因此Segment file數(shù)據(jù)寫(xiě)入磁盤(pán)文件之前,會(huì)先進(jìn)入操作系統(tǒng)級(jí)別的內(nèi)存緩存OS Cache中成為 segemnt buffer,當(dāng)translog fsync之后,等待refresh,也就是segemnt buffer fsync,此時(shí)的倒排索引Segment就能被搜索到了。
這就是為什么es被稱為準(zhǔn)實(shí)時(shí)(NRT,near real-time):因?yàn)閷?xiě)入的數(shù)據(jù)默認(rèn)每隔1秒refresh一次,也就是數(shù)據(jù)每隔一秒才能被 es 搜索到,之后才能被看到,所以稱為準(zhǔn)實(shí)時(shí)。
translog日志文件的作用是什么?
在你執(zhí)行commit操作之前,數(shù)據(jù)要么是停留在buffer中,要么是停留在segment cache中,無(wú)論是buffer還是os cache都是內(nèi)存,一旦這臺(tái)機(jī)器死了,內(nèi)存中的數(shù)據(jù)就全丟了。
因此需要將數(shù)據(jù)對(duì)應(yīng)的操作寫(xiě)入一個(gè)專門(mén)的日志文件,也就是translog日志文件,一旦此時(shí)機(jī)器宕機(jī),再次重啟的時(shí)候,es會(huì)自動(dòng)讀取translog日志文件中的數(shù)據(jù),恢復(fù)到內(nèi)存buffer和segment cache中去。
ps:ES數(shù)據(jù)寫(xiě)入之后,要經(jīng)過(guò)一個(gè)refresh操作之后,才能夠創(chuàng)建索引文件到磁盤(pán),進(jìn)行查詢。但是get查詢很特殊,數(shù)據(jù)實(shí)時(shí)可查。ES5.0之前translog可以提供實(shí)時(shí)的CRUD,get查詢會(huì)首先檢查translog中有沒(méi)有最新的修改,然后再嘗試去segment中對(duì)id進(jìn)行查找。5.0之后,為了減少translog設(shè)計(jì)的復(fù)雜性以便于再其他更重要的方面對(duì)translog進(jìn)行優(yōu)化,所以取消了translog的實(shí)時(shí)查詢功能。get查詢的實(shí)時(shí)性也是一次寫(xiě)多次讀,通過(guò)每次get查詢的時(shí)候,如果發(fā)現(xiàn)該id還在內(nèi)存中沒(méi)有創(chuàng)建索引,那么首先會(huì)觸發(fā)refresh操作,來(lái)讓id可查。
文檔數(shù)據(jù)更新之文檔版本號(hào)
刪除文檔:段是不可改變的,所以既不能從把文檔從舊的段中移除,也不能修改舊的段來(lái)進(jìn)行反映文檔的更新。磁盤(pán)上的每個(gè)segment都有一個(gè).del文件與它相關(guān)聯(lián)。當(dāng)發(fā)送刪除請(qǐng)求時(shí),該文檔未被真正刪除,而是在.del文件中標(biāo)記為已刪除。此文檔可能仍然能被搜索到,但會(huì)從結(jié)果中過(guò)濾掉。當(dāng)segment合并時(shí),在.del文件中標(biāo)記為已刪除的文檔不會(huì)被包括在新的segment中,也就是說(shuō)merge的時(shí)候會(huì)真正刪除被刪除的文檔。
更新文檔:創(chuàng)建新文檔時(shí),ES將為該文檔分配一個(gè)版本號(hào)。對(duì)文檔的每次更改都會(huì)產(chǎn)生一個(gè)新的版本號(hào)。當(dāng)執(zhí)行更新時(shí),舊版本doc在.del文件中被標(biāo)記為已刪除,并且新版本doc在新的Segment中更新倒排索引。舊版本可能仍然與搜索查詢匹配,但是從結(jié)果中將其過(guò)濾掉。
使用版本號(hào)機(jī)制樂(lè)觀控制并發(fā),每個(gè)文檔都有一個(gè) _version (版本)號(hào),當(dāng)文檔被修改時(shí)版本號(hào)遞增。 Elasticsearch 使用這個(gè) _version 號(hào)來(lái)確保變更以正確順序得到執(zhí)行。如果舊版本的文檔在新版本之后到達(dá),它可以被簡(jiǎn)單的忽略。
- 使用內(nèi)部版本號(hào):刪除或者更新數(shù)據(jù)的時(shí)候,攜帶_version參數(shù),如果文檔的最新版本不是這個(gè)版本號(hào),那么操作會(huì)失敗,這個(gè)版本號(hào)是ES內(nèi)部自動(dòng)生成的,每次操作之后都會(huì)遞增一。如操作 PUT /website/blog/1?version=1 表示文檔版本不是1就會(huì)操作失敗。
- 使用外部版本號(hào):ES默認(rèn)采用遞增的整數(shù)作為版本號(hào),也可以通過(guò)外部自定義整數(shù)(long類(lèi)型)作為版本號(hào),例如時(shí)間戳。通過(guò)添加參數(shù)version_type=external,可以使用自定義版本號(hào)。內(nèi)部版本號(hào)使用的時(shí)候,更新或者刪除操作需要攜帶ES索引當(dāng)前最新的版本號(hào),匹配上了才能成功操作。外部版本號(hào)的處理方式和我們之前討論的內(nèi)部版本號(hào)的處理方式有些不同, ES 不是檢查當(dāng)前 _version 和請(qǐng)求中指定的版本號(hào)是否相同, 而是檢查當(dāng)前 _version 是否 小于 指定的版本號(hào)(版本號(hào)更大的操作才能執(zhí)行成功)。 如果請(qǐng)求成功,外部的版本號(hào)作為文檔的新 _version 進(jìn)行存儲(chǔ)。
// 當(dāng)前內(nèi)部版本號(hào)version=3,執(zhí)行PUT /accounts/_doc/1?version=1
{...
}// 執(zhí)行報(bào)錯(cuò):內(nèi)部版本號(hào)不能并發(fā)控制
{"error": {"root_cause": [{"type": "action_request_validation_exception","reason": "Validation Failed: 1: internal versioning can not be used for optimistic concurrency control. Please use `if_seq_no` and `if_primary_term` instead;"}],"type": "action_request_validation_exception","reason": "Validation Failed: 1: internal versioning can not be used for optimistic concurrency control. Please use `if_seq_no` and `if_primary_term` instead;"},"status": 400
}// 使用自己定義的外部版本號(hào),設(shè)置為5
// 如果當(dāng)前的_version < 5,那么該操作執(zhí)行成功,否者失敗PUT /accounts/_doc/1?version=5&version_type=external
{...
}
3.4、ES各模塊與啟動(dòng)流程
參考書(shū)籍《ElasticSearch源碼解讀與深度優(yōu)化》
ES的架構(gòu)設(shè)計(jì)功能實(shí)現(xiàn)主要分為8個(gè)模塊,使用Guice框架進(jìn)行模塊化管理(Guice是Google開(kāi)發(fā)的輕量級(jí)的IoC依賴注入框架)
- Cluster:主節(jié)點(diǎn)執(zhí)行集群管理封裝實(shí)現(xiàn)(在各個(gè)節(jié)點(diǎn)遷移分片,保持?jǐn)?shù)據(jù)平衡),管理集群狀態(tài)(將新生成的集群狀態(tài)發(fā)布到各個(gè)節(jié)點(diǎn)),維護(hù)集群層面的配置信息,調(diào)用Allocation模塊進(jìn)行分片分配。
- Allocation:封裝了分片分配的功能和策略,包括主分片的分配和副分片的分配,由主節(jié)點(diǎn)調(diào)用,集群完全重啟,創(chuàng)建新索引都需要分片分配的過(guò)程。
- Discovery:發(fā)現(xiàn)模塊負(fù)責(zé)發(fā)現(xiàn)集群中的節(jié)點(diǎn),以及選取主節(jié)點(diǎn),可以類(lèi)比Zookeeper,選主節(jié)點(diǎn)并管理集群拓?fù)洹?/li>
- gateway:負(fù)責(zé)收到Master廣播下來(lái)的集群狀態(tài)數(shù)據(jù)持久化存儲(chǔ),并在集群完全重啟的時(shí)候恢復(fù)它們。
- Indices:管理全局級(jí)的索引設(shè)置,不包括索引級(jí)的設(shè)置(索引設(shè)置分為全局級(jí)別、索引級(jí)別),還封裝了數(shù)據(jù)恢復(fù)的功能。
- HTTP:該模塊允許通過(guò)JSON over HTTP的方式訪問(wèn)ES的API,該模塊完全是異步的,沒(méi)有阻塞線程等待,使用異步通信進(jìn)行HTTP的好處是解決C10k問(wèn)題(10k量級(jí)的并發(fā)連接)。
- Transport:傳輸模塊負(fù)責(zé)集群各個(gè)節(jié)點(diǎn)之間的通信,從一個(gè)節(jié)點(diǎn)到另外一個(gè)節(jié)點(diǎn)的每個(gè)請(qǐng)求都是使用傳輸模塊,本質(zhì)上也是使用異步的。使用TCP通信,節(jié)點(diǎn)之間維持長(zhǎng)連接。
- Engine:封裝了對(duì)Lucene的操作以及translog的調(diào)用,他是對(duì)一個(gè)分片讀寫(xiě)操作的最終提供者。
ES單節(jié)點(diǎn)啟動(dòng)、關(guān)閉流程
分析啟動(dòng)流程中進(jìn)程如何解析配置,檢查環(huán)境、初始化內(nèi)部模塊。
1、當(dāng)執(zhí)行bin/elasticsearch啟動(dòng)ES時(shí)候,腳本通過(guò)exec加載Java程序,其中JVM的配置在config/jvm.options指定。
啟動(dòng)腳本后面可以加上參數(shù)
- E:設(shè)置某項(xiàng)配置項(xiàng),如-E “cluster.name = my_cluster”
- V:打印版本號(hào)信息
- d:后臺(tái)運(yùn)行
- p:啟動(dòng)時(shí)候在指定路徑創(chuàng)建一個(gè)pid文件,其中保存了當(dāng)前進(jìn)程的pid。之后可以通過(guò)查看這個(gè)pid文件來(lái)關(guān)閉進(jìn)程。
- q:關(guān)閉控制臺(tái)的標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤輸出。
- s:終端輸出最小信息
- v:終端輸出詳細(xì)信息
2、然后就是Java程序解析配置文件elasticsearch.yml即主要配置文件、log4j2.properties日志配置文件。
3、接著是加載安全配置(敏感信息不適合放在配置文件中的配置)、檢查內(nèi)部環(huán)境(Lucene版本防止有人替換不兼容的jar包、檢測(cè)jar沖突)、檢測(cè)外部環(huán)境(節(jié)點(diǎn)實(shí)現(xiàn)時(shí)候被封裝進(jìn)Node模塊,Node.start()就是進(jìn)行此步驟)主要包括:
- 1、堆大小檢查
- 2、文件描述符檢查
- 3、內(nèi)存鎖定檢查
- 4、最大線程數(shù)檢查
- 5、最大虛擬內(nèi)存檢查
- 6、最大文件大小檢查
- 7、虛擬內(nèi)存區(qū)域最大數(shù)量檢查
- 8、JVM Client模式檢查
- 9、串行收集器檢查
- 10、系統(tǒng)調(diào)用過(guò)濾器檢查
- 11、OnError和OOM檢查
- 12、Early-access檢查
- 13、G1GC檢查
4、檢查完畢之后就是啟動(dòng)ES的內(nèi)部子模塊(見(jiàn)上文介紹),它們啟動(dòng)方法被封裝在Node類(lèi),如discovery.start()、clusterService.start()等
5、啟動(dòng)keep-alive線程:線程本身不做具體的工作,主線程執(zhí)行完啟動(dòng)流程后會(huì)退出,keepalive線程是唯一的用戶線程,作用是保證進(jìn)程運(yùn)行,在Java程序中,至少要有一個(gè)用戶線程,否則進(jìn)程就會(huì)退出。
關(guān)閉流程中,需要按照一定的順序,綜合來(lái)看大致為
- 關(guān)閉快照和HTTP Server ,不再相應(yīng)用戶REST請(qǐng)求
- 關(guān)閉集群拓?fù)涔芾?#xff0c;不在響應(yīng)ping請(qǐng)求
- 關(guān)閉網(wǎng)絡(luò)模塊,讓節(jié)點(diǎn)離線
- 執(zhí)行各個(gè)插件的關(guān)閉流程
- 關(guān)閉IndicesService,最后關(guān)閉因?yàn)楹臅r(shí)最長(zhǎng)
3.5、ES集群與啟動(dòng)流程分析
ES是通過(guò)分片支持分布式的,分片創(chuàng)建的副本稱之為副分片,因此主、副分片可以分布在不同的機(jī)器節(jié)點(diǎn)上,共同組成ES集群。
集群主從模式:分布式系統(tǒng)的集群方式分為主從模式和無(wú)主模式,ES、HDFS、HBase使用主從模式,主從可以簡(jiǎn)化系統(tǒng)設(shè)計(jì),master作為權(quán)威節(jié)點(diǎn),負(fù)責(zé)管理元信息,缺點(diǎn)是存在單點(diǎn)故障,需要解決災(zāi)備問(wèn)題。從機(jī)器的角度看分布式系統(tǒng),每個(gè)機(jī)器可以放多個(gè)節(jié)點(diǎn),分片數(shù)據(jù)有規(guī)則的和節(jié)點(diǎn)對(duì)應(yīng)起來(lái)。
集群管理需要考慮數(shù)據(jù)路由、主副分片數(shù)據(jù)一致性等問(wèn)題,因此需要為ES所在的機(jī)器節(jié)點(diǎn)劃分角色。ES集群的機(jī)器節(jié)點(diǎn)角色
- Master節(jié)點(diǎn):設(shè)置可以作為主節(jié)點(diǎn)資格后,可以被選舉(需要各節(jié)點(diǎn)投票),主節(jié)點(diǎn)是全局唯一的,主節(jié)點(diǎn)也可以作為數(shù)據(jù)節(jié)點(diǎn),但是數(shù)據(jù)量不要太多,為了防止數(shù)據(jù)丟失,有主節(jié)點(diǎn)資格的節(jié)點(diǎn)需要知道有資格成為主節(jié)點(diǎn)的節(jié)點(diǎn)數(shù)量,默認(rèn)為1
- Data節(jié)點(diǎn):CRUD數(shù)據(jù),對(duì)cpu和內(nèi)存、I\O要求較高。
- 預(yù)處理節(jié)點(diǎn):5.0后引入的概念,允許在索引文檔之前,寫(xiě)入數(shù)據(jù)之前,通過(guò)事先定義好的一系列processors和pipeline,對(duì)數(shù)據(jù)進(jìn)行處理,富化。
- 協(xié)調(diào)節(jié)點(diǎn):處理客戶端請(qǐng)求,每個(gè)節(jié)點(diǎn)都知道任意文檔所處的位置,然后轉(zhuǎn)發(fā)這些請(qǐng)求到數(shù)據(jù)節(jié)點(diǎn),收集數(shù)據(jù)合并返回給客戶端。
- 部落節(jié)點(diǎn):5.0之前有個(gè)處理請(qǐng)求的客戶端節(jié)點(diǎn),可以理解為負(fù)載均衡,在5.0之后被協(xié)調(diào)節(jié)點(diǎn)取代。
ES集群?jiǎn)?dòng)流程:集群?jiǎn)?dòng)指的是集群首次啟動(dòng)或者是完全重啟的啟動(dòng)過(guò)程,期間要經(jīng)歷選舉ES主節(jié)點(diǎn)、主分片、數(shù)據(jù)恢復(fù)等重要階段。其過(guò)程可能會(huì)出現(xiàn)腦裂、無(wú)主、恢復(fù)慢、丟數(shù)據(jù)等問(wèn)題。
主要分為以下四個(gè)階段
- selectmaster:集群?jiǎn)?dòng),從眾多ES節(jié)點(diǎn)(ES進(jìn)程)選取一個(gè)主節(jié)點(diǎn),選舉算法是Bully算法的改進(jìn),每個(gè)節(jié)點(diǎn)都有節(jié)點(diǎn)ID,然后每個(gè)節(jié)點(diǎn)都會(huì)對(duì)當(dāng)前已知活躍排序,理論上取ID最大的為主節(jié)點(diǎn),但是會(huì)存在由于網(wǎng)絡(luò)分區(qū)或者節(jié)點(diǎn)啟動(dòng)速度相差太大的時(shí)候,會(huì)導(dǎo)致節(jié)點(diǎn)最大ID統(tǒng)計(jì)不同一,如1節(jié)點(diǎn)統(tǒng)計(jì)1,2,3,4,但是2節(jié)點(diǎn)統(tǒng)計(jì)2,3,4,5,此時(shí)就會(huì)不一致,因此此節(jié)點(diǎn)會(huì)先半數(shù)選舉一個(gè)臨時(shí)主節(jié)點(diǎn),然后半數(shù)投票才確認(rèn)最終的主節(jié)點(diǎn)。選舉完成后若有節(jié)點(diǎn)下線,需要判斷存活節(jié)點(diǎn)數(shù)是否大于當(dāng)前檢測(cè)到存活的一半節(jié)點(diǎn)數(shù),達(dá)不到就要放棄master,重新設(shè)置集群,假如5臺(tái)機(jī)器網(wǎng)絡(luò)出現(xiàn)故障分區(qū),1、2一組,3、4、5一組,產(chǎn)生分區(qū)前,master位于1或2,此時(shí)三臺(tái)一組的節(jié)點(diǎn)會(huì)重新并成功選取master,產(chǎn)生雙主,俗稱腦裂。
- gateway:確定最新的集群元信息,被選舉的master的節(jié)ES點(diǎn)存儲(chǔ)的集群元信息不一定是最新的,需要將其他節(jié)點(diǎn)元信息發(fā)過(guò)來(lái),根據(jù)版本號(hào)來(lái)確定最新的元信息。然后把這個(gè)元信息廣播,更新其他節(jié)點(diǎn),稱為集群元信息的選舉。
- allocation:分片分配至集群各節(jié)點(diǎn),構(gòu)建路由表,ES節(jié)點(diǎn)分配分片(全部index需要均分分片到對(duì)應(yīng)ES節(jié)點(diǎn)),構(gòu)建路由表,1、先要選出主分片,所有分配工作由master節(jié)點(diǎn)來(lái)做。開(kāi)始所有的分片信息都處于unassigned狀態(tài),ES中通過(guò)分配過(guò)程決定哪個(gè)分片位于哪個(gè)節(jié)點(diǎn),因此首先需要選出主分片。首先詢問(wèn)所有節(jié)點(diǎn)依次索要part1分片、part2分片…的元信息,詢問(wèn)量 = 節(jié)點(diǎn)數(shù) * 分片數(shù)(part1、2、3、4…),由此可以看出效率受分片數(shù)量影響,所以最好是控制分片數(shù)量?,F(xiàn)在拿到了所有的分片信息,5.x之前是將所有的從分片元信息匯總比較,選出版本號(hào)最大的作為主分片,但是存在分片所在機(jī)器啟動(dòng)慢問(wèn)題,5.x之后給每個(gè)分片設(shè)置一個(gè)uuid,然后再集群的元信息記錄那個(gè)shared是最新的。2、選取從分片從眾多收集的分片信息選取一個(gè)作為從分片。
- recovery:數(shù)據(jù)恢復(fù),保持主從分片數(shù)據(jù)一致,分片分配到節(jié)點(diǎn)后,開(kāi)始統(tǒng)一各主、副分片數(shù)據(jù),主分片有可能寫(xiě)的數(shù)據(jù)還沒(méi)刷盤(pán),主分片recovery不會(huì)等到副分片分配成功才開(kāi)始,但是副分片recovery需要等到主分片recovery之后(因?yàn)橹鲗?xiě)副讀,主的數(shù)據(jù)副分片有可能還沒(méi)統(tǒng)一)。一次Lucene倒排索引的提交,就會(huì)一次寫(xiě)緩沖區(qū)fsync刷盤(pán)過(guò)程。
- 主的recovery就是將最后一次提交之后的translog進(jìn)行重放。
- 副的recovery會(huì)分為兩個(gè)階段,為了不影響讀,1、全量同步:獲取主的translog鎖,保證不會(huì)受主的fsync改變translog,然后備份主分片快照,直接更新副分片。此階段完成前,通知完副分片啟動(dòng)engine,然后可以接受讀寫(xiě)請(qǐng)求了。2、增量同步:主分片在上面過(guò)程中可能寫(xiě)入新的數(shù)據(jù)和translog,因此副需要增量將translog新增的索引重放恢復(fù),增量translog數(shù)據(jù)指的是對(duì)translog從加鎖開(kāi)始到副分片復(fù)制完主分片的快照的時(shí)刻產(chǎn)生的新增數(shù)據(jù),可以對(duì)主的translog做一個(gè)快照,發(fā)送到副就能找到差異數(shù)據(jù)。
在recovery的時(shí)候主也是可以接受請(qǐng)求更新數(shù)據(jù)的,從的全量、增量同步都需要時(shí)間?從如何保證這些數(shù)據(jù)不丟失?
關(guān)于recovery階段從如何應(yīng)對(duì)全量同步階段主的更新導(dǎo)致數(shù)據(jù)丟失:前面說(shuō)從分片第二階段增量同步的translog快照包含第一階段以后所有的新的新增操作,如果在第一階段全量同步還未執(zhí)行完,主發(fā)生數(shù)據(jù) lucene commit(將文件系統(tǒng)寫(xiě)緩沖的數(shù)據(jù)刷盤(pán),并清空translog)呢?這樣是不是在第二階段就拿不到translog快照了呢?在ES2.0之前是阻止刷盤(pán)操作,這樣可能會(huì)導(dǎo)致一直往translog寫(xiě)數(shù)據(jù)而不刷盤(pán),2.0之后到6.0之前,為了防止期間出現(xiàn)過(guò)大的translog,使用translog.view來(lái)獲取后續(xù)所有操作。從6.0之后,引入TranslogDeletingPolicy的概念,他將translog做一個(gè)快照保證translog不被清理掉。
關(guān)于recovery階段從如何應(yīng)對(duì)增量量同步階段主的更新導(dǎo)致數(shù)據(jù)丟失:在ES2.0之前,副分片恢復(fù)過(guò)程其實(shí)是有三個(gè)階段的,第三階段會(huì)阻塞主的更新數(shù)據(jù)的操作,傳輸?shù)诙A段執(zhí)行期間新增的translog,這個(gè)時(shí)間很短,在2.0之后第三個(gè)階段就被刪除了,恢復(fù)期間沒(méi)有任何寫(xiě)阻塞過(guò)程,副重放translog的時(shí)候,主在第一階段和第二階段的寫(xiě)操作 與 從第二階段重放translog操作之間的時(shí)序錯(cuò)誤和沖突,通過(guò)寫(xiě)流程中進(jìn)行異常處理,對(duì)比版本號(hào)來(lái)過(guò)濾掉過(guò)期操作。遮這樣就把正對(duì)于某個(gè)doc只有最新的一次操作生效,保證了主副分片一致。
ES集群的選主流程
Discovery模塊負(fù)責(zé)發(fā)現(xiàn)集群中的節(jié)點(diǎn),以及選取主節(jié)點(diǎn),因?yàn)槭欠植际酱鎯?chǔ)系統(tǒng),自然要處理一致性問(wèn)題,一般解決方案
(1)試圖避免不一致情況發(fā)生CA
(2)發(fā)生不一致如何挽救。第二種一般對(duì)數(shù)據(jù)模型有著較高的要求CP
集群的架構(gòu)可以為主從模式、哈希表模式
- 哈希表模式:每小時(shí)可支持?jǐn)?shù)千個(gè)節(jié)點(diǎn)的加入和離開(kāi),其可以在不了解底層網(wǎng)絡(luò)拓?fù)涞那闆r下,查詢相應(yīng)很快,如Cassandra就是這種模式
- 主從模式:在網(wǎng)絡(luò)相對(duì)穩(wěn)定的情況下較為適合,當(dāng)集群沒(méi)那么多節(jié)點(diǎn)的時(shí)候,通常節(jié)點(diǎn)的數(shù)量遠(yuǎn)遠(yuǎn)小于單個(gè)節(jié)點(diǎn)能夠維持的連接數(shù),也就是連接多,并且節(jié)點(diǎn)不經(jīng)常變動(dòng),因此es選擇這種模式。
選舉算法
- Bully算法:選舉Leader的基本算法之一,假設(shè)每個(gè)節(jié)點(diǎn)都有一個(gè)唯一的ID,然后根據(jù)ID排序,任何時(shí)候選取最大ID對(duì)應(yīng)的節(jié)點(diǎn)為L(zhǎng)eader,這種方式是實(shí)現(xiàn)比較簡(jiǎn)單,不足是容易產(chǎn)生腦裂,比如A節(jié)點(diǎn)之前為L(zhǎng)eader,但是后來(lái)由于負(fù)載過(guò)重出現(xiàn)假死,這個(gè)時(shí)候排名第二的節(jié)點(diǎn)B被選為L(zhǎng)eader,然后A節(jié)點(diǎn)又突然恢復(fù)正常了,造成腦裂效應(yīng)。
- Paxos算法:選舉更靈活、簡(jiǎn)單,但是實(shí)現(xiàn)起來(lái)比較復(fù)雜。參考:https://zhuanlan.zhihu.com/p/31780743、https://www.cnblogs.com/linbingdong/p/6253479.html
詳細(xì)流程
在ES選Master過(guò)程相關(guān)的重要配置其中之一discovery.zen.minimum_master_nodes 最小主節(jié)點(diǎn)數(shù)量,值最好設(shè)置ES集群總節(jié)點(diǎn)數(shù)的半數(shù)以上,比如共三個(gè)節(jié)點(diǎn),最好設(shè)置為 3 / 2 + 1 = 2 個(gè),這是防止腦裂、數(shù)據(jù)丟失及其重要的參數(shù),作為其他幾種集群行為的判斷依據(jù)。詳細(xì)流程:
1、觸發(fā)選主:當(dāng)參選的節(jié)點(diǎn)數(shù)量大于設(shè)置的最小節(jié)點(diǎn)數(shù),才能進(jìn)行選主
2、確定Master,主要分為下面的選出臨時(shí)Master和確定最終的Master兩個(gè)步驟,原因上文也有說(shuō)。
2.1、選出臨時(shí)Master:通過(guò)配置discovry.zen.ping.unicast.hosts指定集群中的節(jié)點(diǎn)列表(包含ES進(jìn)程的ip、port),各節(jié)點(diǎn)之間投票,根據(jù)Bully選舉算法,每個(gè)節(jié)點(diǎn)計(jì)算出一個(gè)最小的已知節(jié)點(diǎn)ID(可以通過(guò)啟動(dòng)時(shí)間、網(wǎng)絡(luò)響應(yīng)時(shí)間等等確定),詳細(xì)的流程就是
- (1)每個(gè)節(jié)點(diǎn)Ping所有的集群節(jié)點(diǎn),獲取可到達(dá)的節(jié)點(diǎn)列表加入到fullPingResponses中,然后把自己也加入列表
- (2)構(gòu)建兩個(gè)列表,activeMasters列表存儲(chǔ)當(dāng)前活躍的允許被選為Master的節(jié)點(diǎn)列表,這個(gè)列表的數(shù)據(jù)來(lái)自遍歷fullPingResponses每個(gè)節(jié)點(diǎn),根據(jù)每個(gè)節(jié)點(diǎn)選出的ID最小的加入activeMasters列表(不包括自身節(jié)點(diǎn),其中配置了discovery.zen.master_election.ignore_non_master_pings為true的節(jié)點(diǎn)并且配置不具備Master也不會(huì)被加入)。另外一個(gè)是masterCandidates列表是master的候選者列表,如果activeMasters為空那么從這個(gè)列表選取。
- (3)從activeMasters列表中選取一個(gè)做為自己認(rèn)為的臨時(shí)Master,比較方法是選出一個(gè)列表中ID最小值的節(jié)點(diǎn)。
2.2、投票確定最終Master:各節(jié)點(diǎn)選出自己確定的臨時(shí)Master,需要半數(shù)該值節(jié)點(diǎn)數(shù)認(rèn)同才能成為真正的Master,否則該臨時(shí)Master就會(huì)加入集群,發(fā)送投票就是本節(jié)點(diǎn)向自己選的臨時(shí)Master發(fā)送加入集群的請(qǐng)求,獲得的票數(shù)就是該臨時(shí)Master接收到其他節(jié)點(diǎn)的加入集群的請(qǐng)求數(shù)量。投票過(guò)程中對(duì)于莫i個(gè)臨時(shí)Master會(huì)存在兩種情況:
- (1)被選上:等待其他具備Master資格的節(jié)點(diǎn)加入集群即投票達(dá)到法定人數(shù),默認(rèn)30s超時(shí)未達(dá)到法定人數(shù)則選舉失敗,選舉成功的話發(fā)布集群狀態(tài)clusterState。
- (2)其他臨時(shí)Master被選上:當(dāng)前臨時(shí)Master不在接受投票信息,向被確定為Master的節(jié)點(diǎn)發(fā)送加入請(qǐng)求,并默認(rèn)等待1min,超時(shí)會(huì)重試三次。最終確定的Master會(huì)先發(fā)布集群狀態(tài),然后在確認(rèn)加入請(qǐng)求。
2.3、Master選取元信息:像有Master資格節(jié)點(diǎn)(配置了node.master = true的節(jié)點(diǎn))發(fā)請(qǐng)求獲取元數(shù)據(jù),獲取響應(yīng)數(shù)量必須達(dá)到最小節(jié)點(diǎn)數(shù)才會(huì)選為元信息。
3、Master發(fā)布集群狀態(tài)
4、集群節(jié)點(diǎn)失效檢測(cè)
選舉完成之后集群狀態(tài)發(fā)布,后面集群需要探測(cè)到某些節(jié)點(diǎn)失效的異常情況,不執(zhí)行的話可能會(huì)造成腦裂(雙主、多主),因此需要啟動(dòng)兩種失效探測(cè)器:
- 在Master節(jié)點(diǎn):啟動(dòng)NodesFaultDetection,簡(jiǎn)稱NodesFD,定期探測(cè)加入集群的節(jié)點(diǎn)是否活躍。檢查下當(dāng)前集群存活節(jié)點(diǎn)是否達(dá)到法定節(jié)點(diǎn)數(shù)(半數(shù)以上),如果不足則會(huì)放棄Master,重新加入集群。
- 在其他節(jié)點(diǎn):啟動(dòng)MasterFaultDetection,簡(jiǎn)稱MasterFD,定期探測(cè)Master節(jié)點(diǎn)是否活躍。嘗試重新加入集群,發(fā)送加入申請(qǐng)。