唯品會 Dragonfly 日誌系統的 Elasticsearch 實踐

唯品會日誌系統,承接了公司上千個應用的日誌,提供了日誌快速查詢、統計、告警等基礎服務,是保障公司技術體系正常運行必不可缺的重要系統之一。日均接入應用日誌 600 億條,壓縮後大小約 40TB,大促時日誌峰值流量達到每分鐘 3 億條。

唯品會日誌系統,取名 Dragonfly,寓意像蜻蜓複眼一樣,可以依據應用日誌既準確又快速的觀察到系統的運行細節、並發現系統的任何異動。最初,Dragonfly 是圍繞開源的 ELK(Elasticsearch/Logstash/Kibana)技術棧打造的,後來架構不斷演進,增加新的功能組件。目前的系統架構如下圖所示。

唯品會 Dragonfly 日誌系統的 Elasticsearch 實踐


但無論怎麼演進,快速、穩定的日誌查詢始終是日誌系統的核心服務所在,因此 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 到大磁盤容量的服務器中保存。


唯品會 Dragonfly 日誌系統的 Elasticsearch 實踐


通過以上冷熱分離的部署方法,我們只增加了少量大磁盤容量的服務器(成本比 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 進行同樣的調整;
  • 最終所有堆積的日誌都被消費完,降級結束。


唯品會 Dragonfly 日誌系統的 Elasticsearch 實踐


結語

除了核心組件 Elasticsearch,Dragonfly 日誌系統還在日誌格式規範、日誌採集客戶端、查詢前端、規則告警、錯誤日誌異常檢測等方面做了很多的工作,有機會再跟大家繼續分享。


分享到:


相關文章: