唯品會 Dragonfly 日誌系統的 Elasticsearch 實踐
開篇-唯品會日誌系統初探

唯品會日誌系統,承接了公司上千個應用的日誌,提供了日誌快速查詢、統計、告警等基礎服務,是保障公司技術體系正常執行必不可缺的重要系統之一。日均接入應用日誌600億條,壓縮後大小約40TB,大促時日誌峰值流量達到每分鐘3億條。
唯品會日誌系統,取名Dragonfly,寓意像蜻蜓複眼一樣,可以依據應用日誌既準確又快速的觀察到系統的執行細節、並發現系統的任何異動。最初,Dragonfly是圍繞開源的ELK(Elasticsearch/Logstash/Kibana)技術棧打造的,後來架構不斷演進,增加新的功能元件。目前的系統架構如下圖所示。
但無論怎麼演進,快速、穩定的日誌查詢始終是日誌系統的核心服務所在,因此Elasticsearch可謂是Dragonfly系統中的核心元件。本文將重點介紹唯品會日誌系統有效使用Elasticsearch的各種實踐經驗。
Elasticsearch簡介

朋友圈裡這個月最為熱議的技術新聞之一,就是Elastic公司2018.10.6在納斯達克上市,當天開盤後股價就上漲了一倍。將一項技術通過開源不斷做大做強,成為無可爭議的垂直領域領導者,最終實現上市獲得更大發展——Elastic公司實現了無數軟體創業公司的夢想,並提供了一個完美的創業成功典範。而Elasticsearch正是這家公司的招牌產品。
Elasticsearch(下面簡稱ES)是基於Apache Lucene打造的分散式文件儲存+文字查詢引擎。它通過倒排索引(inverted-index)技術提供極快速的文字查詢和聚合統計功能,通過合理的索引和分片設計,又可以支援海量的文字資訊。因此非常適合用於搭建日誌平臺。
儘管如此,將這樣一個開源軟體,既要能貼合公司內部的實際使用場景,又要做到高吞吐、高容量、高可靠,還是需要做不少細緻的工作,下面會一一道來。
Dragonfly系統從2015年開始搭建,最初使用Elasticsearch 1.x,後來升級到5.6版本。
硬體配置

Dragonfly系統在不同機房搭建了多個ES叢集(配置了跨叢集查詢),共上百臺機器,最大的2個叢集均有接近50臺伺服器。這些伺服器包含了兩種硬體配置型別:
●一種使用多個SSD磁碟,並擁有效能強勁的CPU。 SSD伺服器作為叢集中的熱資料伺服器使用,負責新日誌的寫入,並提供使用頻率最高的最近幾天的日誌查詢服務。
●另一種使用多個大容量的HDD/SATA磁碟,搭配較弱的CPU。 HDD伺服器作為叢集中的冷資料伺服器使用,負責儲存較早前的日誌。由於早前的日誌訪問頻率較低,因此不需要很好的效能,而看重磁碟容量。
兩種型別伺服器的磁碟均使用RAID0陣列,記憶體大小為64GB或128GB。
通過冷熱分離,可以保證ES叢集的寫入和實時查詢效能,又能在成本較低的情況下提供較長的日誌儲存時間,下面還會詳細介紹ES叢集的冷熱分離是怎麼實現的。
日誌索引管理

ES中的文件資料是儲存在索引(index)之中的。索引可以劃分多個分片(shard)分佈在不同的節點上,並通過配置一定的備份數實現高可用。
對於日誌資料,通常把每個應用、每天的日誌儲存到不同的索引中(這是以下很多討論的前提)。 要承接公司上千個應用的海量日誌,又要應對每個應用每天不同的資料量,如何有效管理這些日誌索引,成為Dragonfly能否提供穩定和快速服務的關鍵。 下面將分多個方面為你介紹。你不會在官方文件中直接找到這些知識,它們來源於我們多年的實戰經驗。
1. 索引預建立
當有一個寫入請求,而請求中指定的索引不存在時,ES會自動建立一個5分片的新索引。因為要完成尋找合適的節點、建立主分片、建立備份分片、廣播新的叢集資訊、更新欄位mapping等一系列的操作,建立一個新索引是耗時的。根據叢集規模、新索引的分片數量而不同,在我們的叢集中建立一個新索引需要幾秒到幾十秒的時間。如果要在新的一天開始的時刻,大量新一天的日誌同時到來時觸發建立上千個索引,必然會造成叢集的長時間服務中斷。因此,每天的索引必須提前建立。
此外,還有一個必須提前建立索引的關鍵原因,就是ES索引的分片數在建立時就必須給定,不能再動態增加(早期版本還不能減少,ES 5.x開始提供了shrink操作)。在兩種情況下會造成問題:
●如果某個應用的日誌量特別大,如果我們使用過少的分片數(如預設的5個分片),就會造成嚴重的熱點問題,也就是分配到這個大索引分片的節點,將要處理更多的寫入和查詢資料量,成為叢集的瓶頸。
●大部分的應用日誌量非常小,如果分配了過多的分片數,就會造成叢集需要維護的活躍分片數目很大,在節點間定時同步的叢集資訊資料也會很大,在叢集繁忙的時候更容易出現請求超時,在叢集出現故障的時候恢復時間也更長。
出於以上考慮, 必須提前建立索引,併為不同應用的索引指定合適的分片數量。 分片的數量可以由以下公式計算:
n_shards=avg_index_size/ size_per_shard * magnified_factor
其中:
●avg_index_size為前N天這個應用索引大小的指數移動平均值(日期越靠近、權重越大)
●size_per_shard為期望每個分片的大小。分片過大容易造成上述熱點問題,同時segment merge操作的開銷更大(下面詳細介紹),在需要移動分片時耗時也會較久;而過小又會造成叢集分片總數過多。因此要選取一個均衡的大小,我們使用的是20GB。
●magnified_factor為擴大系數,是我們根據公司特有的促銷場景引入的。在促銷日,業務請求數會數倍甚至數十倍於平時,日誌量也會相應增長,因此我們需要在促銷日為索引增加更多分片數。
索引預建立的操作我們放在前一天的清早(後面會看到,開銷很大的操作我們都放在夜間和凌晨)進行。即今天凌晨會根據之前索引大小的EMA及上述公式計算出分片數,建立明天的索引。
2. 替補索引
這是很有意思的一個概念,你不會在任何其他ES相關的文章找到它(如果有,請注意百分百是抄襲:)
它解決的是這樣的問題:即使我們已經提前估算出一個應用的日誌索引的分片數,但是仍然會有異常情況。有時你會發現某個應用某一天的日誌忽然增加了很多,可能是開發打開了DEBUG級別開關查問題,也可能在做壓測,或者可能是bug死迴圈的列印了無數stacktrace。有時是一個新接入的應用,由於沒有歷史索引,所以並不能估算出需要的分片數。以上情況都有可能引起分片數不足的問題。
記得上面提到過索引在建立後就不能再動態調整分片數了,怎麼破?
讓我們來新建立一個替補索引(substitude index)吧,這一天接下來的所有日誌都會寫到這裡。替補索引的命名,我們會在原索引名的基礎上加上-subst字尾,因此使用者在查詢今天的日誌時,查詢介面仍可以通過一定的規則指定同時使用今天的“正選”索引和替補索引。
我們定時掃描當天索引的平均分片大小,一旦發現某個索引的分片大小過大,就及時建立替補索引。注意,建立替補索引時仍需指定分片數,這是根據當天已過去的小時數比上還剩下的小時數,以及當時已經寫入的資料量估算得出的。
替補索引還有一種使用場景。有時促銷是在晚上進行的,一到促銷開始,請求量和日誌量會出現一個非常大的尖峰。而由於已經寫入了大半天的日誌,分片已經比較大,這時任何的寫入都可能引起更大規模的segment merge(熟悉ES的同學會知道,每次refresh會生成新的segment,小的segment會不斷merge變成更大的segment,越大的segment merge時對磁碟IO和CPU的開銷佔用越大。這相當於HBase的compaction行為,也同樣會有寫放大問題),會使寫入速率受到限制。而如果這時切換到新的索引寫入(場上球員請全部給我下來~),就可以減輕merge的影響,對保障流量尖峰時ES叢集的寫入速率非常有幫助。
這種促銷日的替補索引是提前建立的。分片數的計算、以及還有可能使用到“替補的替補”,就不再贅述了。
3. Force Merge
當資料寫入ES時先產生小的segment。segment會佔用堆記憶體,數量過多對查詢時間也有影響。因此ES會按一定的策略進行自動合併segment的動作,這前文已經提過。此外,ES還提供force merge的方法(在早期版本稱為optimize),通過呼叫此方法可以進一步強化merge的效果。
具體的,我們在凌晨執行定時任務,對前一天的索引,按照分片大小以及每個segment 500MB的期望值,計算出需要合併到的segment數量,然後執行force merge。
通過這個操作,我們可以節省很多堆記憶體。具體每個節點segment的記憶體佔用,可以在_nodes//stats api中檢視到。當segment memory一項佔用heap大小2/3以上,很容易造成各種gc問題。
另外由於force merge操作會大量讀寫磁碟,要保證只在SSD伺服器上執行。
4. 冷熱分離
最初搭建ES叢集時,為了保證讀寫速率,我們使用的都是SSD型別的伺服器。但由於SSD磁碟成本高,容量小,隨著接入應用越來越多,不得不縮短日誌儲存天數,一度只能提供7天的日誌,使用者開始產生抱怨。
我們自然而然產生了冷熱分離的想法。當時在網上並不能找到ES冷熱分離部署的案例,但出於曾經翻閱過官方文件多遍後對ES各種api的熟悉,我們認為是可行的。
●首先,在節點的配置檔案中通過node.tag屬性使用不同的標籤可以區分不同型別的伺服器(如硬體型別,機櫃等);
●通過index.routing.allocation.include/exclude的索引設定可以指定索引分片只能分配在某種tag的伺服器上;
●利用以上方法,我們在建立的新的索引時,指定只分配在熱資料伺服器(SSD伺服器)上。使擁有強勁磁碟IO和CPU效能的熱資料伺服器承接了新日誌的寫入和查詢;
●設定夜間定時任務,將N天前的索引修改為只能分配在冷資料伺服器(HDD伺服器)上。使訪問頻率降低的索引relocate到大磁碟容量的伺服器中儲存。
通過以上冷熱分離的部署方法,我們只增加了少量大磁碟容量的伺服器(成本比SSD型別的伺服器還低很多),就將日誌儲存時長增加到了30天,部分應用可以按需儲存更長時間。
5. 日誌歸檔
從上面可以看出,通過冷熱分離,熱資料伺服器只存放最近3天的日誌索引,其餘27天的索引都存放在冷資料伺服器上。這樣的話,冷資料伺服器的segment memory將非常大,遠超過官方文件建議的不超過32GB的heap配置。如果要增加heap到32GB以上,會對效能有一定影響。
考慮到越久遠的日誌,查詢的可能性越低,我們將7天以前的日誌進行了歸檔——也就是close索引的操作。close的索引是不佔用記憶體的,也就解決了冷資料伺服器的heap使用問題。
Dragonfly前端放開放了歸檔日誌管理的頁面,使用者如需查詢7天前的日誌,可以自助開啟已經歸檔的日誌——也就是重新open已經close掉的索引。
日誌寫入降級策略

前面已經提到過,我們公司促銷高峰時的日誌量會達到平日的數十倍。我們不可能配備足夠支撐促銷高峰期日誌量的伺服器規模,因為那樣的話在全年大部分時間內很多伺服器資源是浪費的。而在伺服器規模有限的情況下,ES叢集的寫入能力是有限的,如果不採取任何措施,在促銷高峰期、以及萬一叢集有故障發生時,接入的日誌將發生堆積,從而造成大面積的寫入延遲,使用者將完全無法查詢最新的日誌。此時如果有業務故障發生但無法通過日誌來分析的話,問題會非常致命。
因此就需要有合理的降級措施和預案,以保證當日志流入速率大於日誌寫入ES叢集的速率時,Dragonfly仍能提供最大限度的服務。我們首先需要做出取捨,制訂降級服務的目標,最終我們採用的是:保證各個應用都有部分伺服器的日誌可以實時查詢。
唯品會的日誌,是從應用伺服器先採集到Kafka做快取的,因此可以在讀取Kafka資料寫入ES的過程中做一些處理。嚴格來說如何實現這一目標並不屬於Elasticsearch的範疇。
要達到降級服務目標,我們需要設計一套組合拳:
●日誌上傳到Kafka時,使用主機名作為partitioning hash key,即同一臺應用伺服器的日誌會採集到同一個Kafka topic的partition中;
●在消費Kafka topic時,使用同一個consumer group的多個例項,每個consumer例項設定不同的處理優先順序;
●對Kafka consumer按照優先順序不同,配置不同的寫ES的執行緒數。優先順序越高,寫入執行緒數越多,寫入速率越快;優先順序越低,寫入執行緒數越少,甚至暫停寫入;
●這樣就能保證高優先順序的consumer能夠實時消費某些partition並寫入到ES中,通過hashing採集到那些被實時消費的partition的主機日誌,也將在ES中可以被實時查詢;
●隨著度過日誌流量高峰,當ES叢集支援高優先順序consumer的日誌寫入沒有壓力時,可以逐步增加次高優先順序consumer的寫入執行緒數。直到對應partition的堆積日誌被消費完,又可以對更低優先順序的consumer進行同樣的調整;
●最終所有堆積的日誌都被消費完,降級結束。
結語

除了核心元件Elasticsearch,Dragonfly日誌系統還在日誌格式規範、日誌採集客戶端、查詢前端、規則告警、錯誤日誌異常檢測等方面做了很多的工作,有機會再跟大家繼續分享。
推薦閱讀
唯品會Noah雲平臺實現內幕披露
基於Kafka1.0訊息可靠性設計之消費重試策略
唯品會高吞吐量Access Log儲存的實現
“唯技術”一檔專為唯品技術人發聲的公眾號
歡迎投稿!!
只要是 技術相關的文章 儘管砸過來!