## 學以致用
<div style="text-indent:2em;">
<p>隨著第4章的慢慢接近尾聲,我們需要獲取一些接近我們日常工作的知識。因此,我們決定把一個真實的案例分成兩個章節的內容。在本章節中,你將學到如何結合所學的知識,基于一些假設,構建一個容錯的、可擴展的集群。由于本章主要講配置相關的內容,我們也將聚焦集群的配置。也許結構和數據有所不同,但是對面同樣的數據量集群處理檢索需求的解決方案也許對你有用。</p>
<h3>假設</h3>
<p>在進入到紛繁的配置細節之前,我們來做一些假設,我們將基于這些假設來配置我們的ElasticSearch集群。</p>
<h4>數據規模和檢索性能需求</h4>
<p>假設我們有一個在線圖書館,目前線上銷售100,000種各種語言的書籍。我們希望查詢請求的平均響應時間不高于200毫秒,這樣就能避免用戶在使用搜索服務時等待太長的時間,也能避免瀏覽器渲染頁面時等待太長時間。所以,現在來實現期望負載。我們做了一些性能測試(內容超出本書的范圍),而且我們測到如下方案性能最好:給集群分配4個節點,數據切分到兩個分片,而且每個分片掛載一個副本。</p>
<!--note structure -->
<div style="height:220px;width:650px;text-indent:0em;">
<div style="float:left;width:13px;height:100%; background:black;">
<img src="../lm.png" height="210px" width="13px" style="margin-top:5px;"/>
</div>
<div style="float:left;width:50px;height:100%;position:relative;">
<img src="../note.png" style="position:absolute; top:30%; "/>
</div>
<div style="float:left; width:550px;height:100%;">
<p style="font-size:13px;margin-top:5px;">讀者也許想自己做一些性能測試。如果自己做,可以選擇一些開源工具來模擬用戶發送查詢命令到集群中。比如,Apache JMeter(http://jmeter.apache.org/) 或者ActionGenerator(https://github.com/sematext/ActionGenerator) 。除此之外,還可以通過ElasticSearch提供的一些插件來查看統計記錄,比如paramedic(https://github.com/karmi/elasticsearch-paramedic) ,或者BigDesk(https://github.com/lukas-vlcek/bigdesk) ,或者直接使用功能完善的監測和報警解決方案,比如Sematext公司開發,用于ElasticSearch的SPM系統(http://sematext.com/spm/elasticsearch-performancemonitoring/index.html) 。所有的這些工具都會提供性能測試的圖示,幫助用戶找到系統的瓶頸。除了上面提到的工具,讀者可能還需要監控JVM垃圾收集器的工作以及操作系統的行為(上面提到的工具中有部分工具提供了相應的功能)。</p>
</div>
<div style="float:left;width:13px;height:100%;background:black;">
<img src="../rm.png" height="210px" width="13px" style="margin-top:5px;"/>
</div>
</div> <!-- end of note structure -->
<p>因此,我們希望我們的集群與下圖類似:</p>
<center><img src="../46-cluster.png"/></center>
<p>當然,分片及分片副本真實的放置位置可能有所不同,但是背后的邏輯是一致的:即我們希望一節點一分片。</p>
<h4>集群完整配置</h4>
<p>接下來我們為集群創建配置信息,并詳細討論為什么要在集群中使用如下的屬性:</p>
<blockquote style="text-indent:0;">cluster.name: books<br/>
\# node configuration<br/>
node.master: true<br/>
node.data: true<br/>
node.max\_local\_storage\_nodes: 1<br/>
\# indices configuration<br/>
index.number\_of\_shards: 2<br/>
index.number\_of\_replicas: 1<br/>
index.routing.allocation.total\_shards\_per\_node: 1<br/>
\# instance paths<br/>
path.conf: /usr/share/elasticsearch/conf<br/>
path.plugins: /usr/share/elasticsearch/plugins<br/>
path.data: /mnt/data/elasticsearch<br/>
path.work: /usr/share/elasticsearch/work<br/>
path.logs: /var/log/elasticsearch<br/>
\# swapping<br/>
bootstrap.mlockall: true<br/>
\#gateway<br/>
gateway.type: local<br/>
gateway.recover\_after\_nodes: 3<br/>
gateway.recover\_after\_time: 30s<br/>
gateway.expected\_nodes: 4<br/>
\# recovery<br/>
cluster.routing.allocation.node\_initial\_primaries\_recoveries: 1<br/>
cluster.routing.allocation.node\_concurrent\_recoveries: 1<br/>
indices.recovery.concurrent\_streams: 8<br/>
\# discovery<br/>
discovery.zen.minimum\_master\_nodes: 3<br/>
\# search and fetch logging<br/>
index.search.slowlog.threshold.query.info: 500ms<br/>
index.search.slowlog.threshold.query.debug: 100ms<br/>
index.search.slowlog.threshold.fetch.info: 1s<br/>
index.search.slowlog.threshold.fetch.debug: 200ms<br/>
\# JVM gargabe collection work logging<br/>
monitor.jvm.gc.ParNew.info: 700ms<br/>
monitor.jvm.gc.ParNew.debug: 400ms<br/>
monitor.jvm.gc.ConcurrentMarkSweep.info: 5s<br/>
monitor.jvm.gc.ConcurrentMarkSweep.debug: 2s
</blockquote>
<p>接下來了解各個屬性值的意義。</p>
<h4>節點層面的配置</h4>
<p>在節點層面的配置中,我們指定了一個集群名字(使用cluster.name屬性)來標識我們的集群。如果在同一個網段中配置了多個集群,名字相同的節點會守護甜心連接成一個集群。接下來,這個特殊的節點會被選舉成主節點(用node.master:true屬性),而且該節點可以容納索引數據(node.data:true)。此外,通過設置node.max\_local\_storeage\_nodes屬性值為1,可以限制一個節點上最多能夠運行1個ElasticSearch實例。</p>
<h4>索引的配置</h4>
<p>由于我們只有一個索引,而且暫時也不打算添加更多的索引,我們決定設置分片的默認數量為2(用index.number\_of\_shards屬性),設置分片副本的默認數量為1(用index.number\_of\_replicas屬性)。此外,我們還設置了index.routing.allocation.total\_shards\_per\_node屬性值為1,這意味著對于每個索引,ElasticSearch只會在單個節點上分配一個分片。這應用到我們的4-節點集群的例子中就是每個節點會平均分配所有的分片。</p>
<h4>各種目錄的規劃</h4>
<p>我們已經把ElasticSearch安裝到了/usr/share/elasticsearch目錄,基于此,conf目錄、plugins目錄和工作目錄都在該目錄下。由于這個原因,我們把數據單獨指定到硬盤的一個地方,這個地方就是/mnt/data/elasticsearch掛載點。最后,我們把日志文件安置到/var/log/elasticsearch目錄。基于這樣的目錄規劃,我們在做配置的更新操作時,只需要關注/usr/share/elasticsearch目錄即可,無需接觸其它的目錄。</p>
<h4>Gateway的配置</h4>
<p>正如讀者所了解的,gateway是負責存儲索引和元數據的模塊。在本例中,我們選擇推薦的,也是唯一沒有廢棄的gateway類型,即local(gateway.type屬性)。我們說我們希望當集群只有三個節點時,恢復進程就啟動(gateway.recover\_after\_nodes屬性),同時至少3個節點相互連接30秒后開始恢復任務(用gateway.recover\_after\_time屬性)。此外,我們還可以通過設置gateway.expected\_nodes屬性值為4,用來通知ElasticSearch,我們的集群將由4個節點組成。</p>
<h4>集群恢復機制</h4>
<p>對于ElasticSearch來說,最核心的一種配置就是集群恢復配置。盡管它不是每天都會用到,正如你不會每天都重啟ElasticSearch,也不希望集群經常失效一樣。但是防范于未然是必須的。因此我們來討論一下用到的相關屬性。我們已經設置了 cluster.routing.allocation.node\_initial\_
primaries\_recoveries屬性為1,這意味著我們只允許每個節點同時恢復一個主分片。這沒有問題,因為每個服務器上只有一個節點。然而請記住這個操作基于gateway的local類型時會非常快,因此如果一個節點上有多個主分片時,不妨把這個值設置得大一點。 我們也設置了cluster.
routing.allocation.node\_concurrent\_recoveries屬性值為1,再一次限制每個節點同時恢復的分片數量(我們的集群中每個節點只有一個分片,不會觸發這條屬性的紅線,但是如果每個節點不止一個分片,而且系統I/O允許時,我們可以把這個值設置得稍微大一點)。此外,我們也設置了indices.recovery.concurrent\_streams屬性值為8,這是因為在最初測試recovery過程時,我們了解到我們的網絡 和服務器在從對等的分片中恢復一個分片時能夠輕松地使用8個并發流,這也意味著我們可以同時讀取8個索引文件。 </p>
<h4>節點發現機制</h4>
<p>在集群的discovery模塊配置上,我們只需要設置一個屬性:設置discovery.zen.minimum\_master\_nodes屬性值為3。它指定了組成集群所需要的最少主節點候選節點數。這個值至少要設置成節點數的50%+1,在本例中就是3。它用來防止集群出現如下的狀況:由于某些節點的失效,部分節點的網絡連接會斷開,并形成一個與原集群一樣名字的集群(這種情況也稱為“集群腦裂”狀況)。這個問題非常危險,因為兩個新形成的集群會同時索引和修改集群的數據。 </p>
<h4>記錄慢查詢日志</h4>
<p>使用ElasticSearch時有件事情可能會很有用,那就是記錄查詢命令執行過程中一段時間或者更長的日志。記住這種日志并非記錄命令的整個執行時間,而是單個分片上的執行時間,即命令的部分執行時間。在本例中,我們用INFO級別的日志來記錄執行時間長于500毫秒的查詢命令以及執行時間長于1秒的real time get請求。在調試時,我們把這些值分別設置為100毫秒和200毫秒。如下的配置片段用于上述需求:
<blockquote style="text-indent:0;">
index.search.slowlog.threshold.query.info: 500ms
index.search.slowlog.threshold.query.debug: 100ms
index.search.slowlog.threshold.fetch.info: 1s
index.search.slowlog.threshold.fetch.debug: 200ms
</blockquote>
</p>
<h4>記錄垃圾回收器的工作日志</h4>
<p>最后,由于我們的集群沒有監控解決方案(至少剛開始沒有),我們想看到垃圾收集器的工作狀態。說得更清楚一點,我們希望看到垃圾回收器是否花了太多的時間,如果是,是在哪個時間段。為了實現這一需求,我們在elasticsearch.yml文件中添加下面的信息:
<blockquote style="text-indent:0;">
monitor.jvm.gc.ParNew.info: 700ms
monitor.jvm.gc.ParNew.debug: 400ms
monitor.jvm.gc.ConcurrentMarkSweep.info: 5s
monitor.jvm.gc.ConcurrentMarkSweep.debug: 2s
</blockquote>
在INFO級別的日志中,ElasticSearch會把運行時間太長的垃圾回收過程的相關信息記錄下來,按照設置,閾值為 concurrent mark sweep收集器收集過程超過5秒,新生垃圾收集超過700毫秒。我們也添加了DEBUG級別的日志來應對debug需求和問題的修復。
</p>
<!--note structure -->
<div style="height:50px;width:650px;text-indent:0em;">
<div style="float:left;width:13px;height:100%; background:black;">
<img src="../lm.png" height="40px" width="13px" style="margin-top:5px;"/>
</div>
<div style="float:left;width:50px;height:100%;position:relative;">
<img src="../note.png" style="position:absolute; top:30%; "/>
</div>
<div style="float:left; width:550px;height:100%;">
<p style="font-size:13px;margin-top:5px;">如果不清楚什么是新生代垃圾回收,或者不清楚什么是concurrent mark sweep,請參考Oracle的Java文檔:http://www.oracle.com/technetwork/java/javase/
gc-tuning-6-140523.html. </p>
</div>
<div style="float:left;width:13px;height:100%;background:black;">
<img src="../rm.png" height="40px" width="13px" style="margin-top:5px;"/>
</div>
</div> <!-- end of note structure -->
<h4>內存設置</h4>
<p>直到現在我們都沒有提到RAM內存的設置,所以本節來學習這一知識點。假設每個節點都有16GB RAM。通常不推薦將JVM 堆內存設置高于可用內存的50%,本例也是如此。我們設置Java的Xms屬性值為8g,對于我們的應用來說應該夠用了。由于我們的索引數據量不大,而且由于不需要facet較高基于的域,所以就沒有parent-child關系型數據。在前面顯示的配置信息中,我們在ElasticSearch中也設置了垃圾回收器的相關參數,但是對于長期監測,最好使用專業的監控工具,比如SPM(http://sematext.com/spm/index.html )或者Munin(http://munin-monitoring.org/ )。</p>
<!--note structure -->
<div style="height:160px;width:650px;text-indent:0em;">
<div style="float:left;width:13px;height:100%; background:black;">
<img src="../lm.png" height="150px" width="13px" style="margin-top:5px;"/>
</div>
<div style="float:left;width:50px;height:100%;position:relative;">
<img src="../note.png" style="position:absolute; top:30%; "/>
</div>
<div style="float:left; width:550px;height:100%;">
<p style="font-size:13px;margin-top:5px;">我們已經提到通用的規則,即50%的物理內存用于JVM,余下的內存用于操作系統。就像其它絕大部分規則一樣,這條規則也適用于絕大部分的場景。但是我讓設想一下,我們的索引數據會占到30GB的硬盤空間,我們有128GB的RAM內存,但是考慮到parent-child關系型的數據量和高基數的域中進行faceting操作,如果分配到JVM的堆內存是64G就會有出現out-of-memory異常的風險。在這樣的安全中,是否依然只分配50%的可用內存空間呢?在我們看來,答案是NO,但這只適用于特殊的案例,前面提到從128G內存中JVM分配64G內存后,單個索引的數據量遠遠小于JVM中可用內存的大小,所以我們可以適當增加。但是一定要記住給操作系統留下足夠的內存以避免swapping的出現。 </p>
</div>
<div style="float:left;width:13px;height:100%;background:black;">
<img src="../rm.png" height="150px" width="13px" style="margin-top:5px;"/>
</div>
</div> <!-- end of note structure -->
<h4>遺失的美好</h4>
<p>還有一點沒有提到,就是bootstrap.mlockall屬性。該屬性能夠讓ElasticSearch將堆內存鎖住,并確保該塊內存不會被操作系統替換成虛擬內存。如果把bootstrap.mlockall設置為true,推薦用戶把ES\_MIN\_ME和ES\_MAX\_ME兩個屬性設置成相同的值。這樣做可以確保服務器有足夠的物理內存來啟動ElasticSearch,并且保留足夠的內存給操作系統讓系統流暢運行。我們將在第6章<i>應對突發事件</i> 的 <i>避免Unix-like操作系統的swapping操作</i>一節中了解更多的相關知識。</p>
<h4>量變引起質變</h4>
<p>假定現在我們的服務做得很成功。訪問的流量也逐步增長。而且,一家大公司希望跟我們合作。這家大的供應商不是賣自己的書,只是供貨給零售商。預計該公司大概會上線200萬種圖書,所以系統需要處理的數據量將是現在的20倍(只估算索引文檔的數量)。我們必須為這些變化作準備,也就是說要更改我們的ElasticSearch集群,使我們的用戶體驗能夠得到保持甚至提升現。我們需要做什么呢?先解決容易的事情。我們可以更改(增加或者減少)分片副本的數量,這無需做其它的工作。這樣做系統就可以同時執行更多的查詢命令,當然也會相應地增加集群的壓力。這樣做的缺點就是增加了額外的硬盤空間開銷。我們同時也要確保額外的分片副本可以分配到集群的節點上(參考<i>選擇恰當的分片數量和分片副本數量</i>一節中的那個公式)。還要記住性能測試的結論:作為結果的吞吐量指標永遠依賴于多個無法用數學公式刻畫的因素。 </p>
<p>添加主分片怎么樣?前面已經提到,我們無法在線修改主分片的數量。如果我們事多分配分片,就為預期的數據增長預留了空間。但是在本例中,集群有2個主分片,應對100,000的數據足夠了。但是在短時間里對于2,100,000(已經處理的數據和將要添加進來的數據)的數據量來說太少。誰會預想到會這么成功呢?因此,必須設想一個可以處理數據增長的解決方案,但是又必須盡可能減少停服的時間,畢竟停服就意味著金錢的損失。</p>
<h4>重新索引</h4>
<p>第一個選擇就是刪除舊的索引,然后創建有更多分片的索引。這是最簡單解決辦法,但是在重新索引期間服務不可用。在本例中,準備用于添加到索引數據是一個耗時的過程,而且從數據庫中導入數據用的時間也很長。公司的經營者說在整個重新索引數據期間停止服務是不可行的。第二個想法是創建第二個索引,并且添加數據,然后把應用接口調轉到新的索引。方案可行,但是有個小問題,創建新的索引需要額外的空間開銷。當然,我們將擁有新的存儲空間更大的機器(我們需要索引新的“大數據”),但是在得到機器前,我們要解決耗時的任務。我們決定尋找其它的更簡單的解決方案。 </p>
<h4>路由</h4>
<p>也許我們的例子中用routing解決會很方便?顯而易見的收獲就是通過routing可以用查詢命令只返回我們數據集中的書籍,或者只返回屬于合作伙伴的書籍(因為routing允許我們只查詢部分索引)。然而,我們需要應用恰當的filter,routing不保證來自兩個數據源的數據不在同一個分片上出現。不幸的是,我們的例子中還有另一個死胡同,引入routing需要進行重新索引數據。因此,我們只得把這個解決方案扔到桌子邊的垃圾桶里。</p>
<h4>多索引結構</h4>
<p>讓我們從基本的問題開始,為什么我們只需要一個索引?為什么我們要改變當前的系統。答案是我們想要搜索所有的文檔,確定它們是來自于原始數據還是和作伙伴的數據。請注意ElasticSearch允許我們直接搜索多個索引。我們可以通過API端點使用多個索引,比如,/book,partner1/。我們還有一個靈巧的方法簡單快速添加另一個合作伙伴,無需改變現有集群,也無需停止服務。我們可以用過別名(aliases)創建虛擬索引,這樣就無需修改應用的源代碼。</p>
<p>經過頭腦風暴,我們決定選擇最后一個解決方案,通過一些額外的改善使得ElasticSearch在索引數據時壓力不大。我們所做的就是禁止集群的刷新率,然后刪除分片副本。
<blockquote>curl -XPUT localhost:9200/books/\_settings -d '{
"index" : {
"refresh\_interval" : -1,
"number\_of\_replicas" : 0
}
}'</blockquote>
當然,索引數據后我們變回它原來的值,唯一的一個問題就是ElasticSearch不允許在線改變索引的名字,這導致在配置文件中修改索引名稱時,會使用服務短時間停止一下。
</p>
</div>
- 前言
- 第1章 認識Elasticsearch
- 認識Apache Lucene
- 熟悉Lucene
- 總體架構
- 分析你的文本
- Lucene查詢語言
- 認識 ElasticSearch
- 基本概念
- ElasticSearch背后的核心理念
- ElasticSearch的工作原理
- 本章小結
- 第2章 強大的用戶查詢語言DSL
- Lucene默認打分算法
- 查詢重寫機制
- 重排序
- 批處理
- 查詢結果的排序
- Update API
- 使用filters優化查詢
- filters和scope在ElasticSearch Faceting模塊的應用
- 本章小結
- 第3章 索引底層控制
- 第4章 探究分布式索引架構
- 選擇恰當的分片數量和分片副本數量
- 路由功能淺談
- 調整集群的分片分配
- 改變分片的默認分配方式
- 查詢的execution preference
- 學以致用
- 本章小結
- 第5章 管理Elasticsearch
- 選擇正確的directory實現類——存儲模塊
- Discovery模塊的配置
- 索引段數據統計
- 理解ElasticSearch的緩存
- 本章小結
- 第6章 應對突發事件
- 第7章 優化用戶體驗
- 第8章 ElasticSearch Java API
- 第9章 開發ElasticSearch插件