天天看點

「資料庫」微信海量資料查詢如何從1000ms降到100ms?

作者:架構思考
微信的多元名額監控平台,具備自定義次元、名額的監控能力,主要服務于使用者自定義監控。作為架構級監控的補充,它承載着聚合前 45億/min、4萬億/天的資料量。目前,針對資料層的查詢請求也達到了峰值 40萬/min,3億/天。較大的查詢請求使得資料查詢遇到了性能瓶頸:查詢平均耗時 > 1000ms,失敗率居高不下。針對這些問題,微信團隊對資料層查詢接口進行了針對性的優化來滿足上述場景,将平均查詢速度從1000ms+優化到了100ms級别。本文為各位分享優化過程,希望對你有用!

一、背景介紹

微信多元名額監控平台(以下簡稱多元監控),是具備靈活的資料上報方式、提供次元交叉分析的實時監控平台。

在這裡,最核心的概念是“協定”、“次元”與“名額”。例如,如果想要對某個【省份】、【城市】、【營運商】的接口【錯誤碼】進行監控,監控目标是統計接口的【平均耗時】和【上報量】。在這裡,省份、城市、營運商、錯誤碼,這些描述監控目标屬性的可枚舉字段稱之為“次元”,而【上報量】、【平均耗時】等依賴“聚合計算”結果的資料值,稱之為“名額”。而承載這些名額和次元的資料表,叫做“協定”。

多元監控對外提供 2 種 API:

  • 次元枚舉查詢:用于查詢某一段時間内,一個或多個次元的排列組合以及其對應的名額值。它反映的是各次元分布“總量”的概念,可以“聚合”,也可以“展開”,或者固定次元對其它次元進行“下鑽”。資料可以直接生成柱狀圖、餅圖等。
  • 時間序列查詢:用于查詢某些次元條件在某個時間範圍的名額值序列。可以展示為一個時序曲線圖,橫坐标為時間,縱坐标為名額值。

然而,不管是使用者還是團隊自己使用多元監控平台的時候,都能感受到明顯的卡頓。主要表現在看監控圖像或者是檢視監控曲線,都會經過長時間的資料加載。

團隊意識到,這是資料量上升必然帶來的瓶頸。目前,多元監控平台已經接入了數千張協定表,每張表的特點都不同。次元組合、名額量、上報量也不同。針對大量資料的實時聚合以及 OLAP 分析,資料層的性能瓶頸越發明顯,嚴重影響了使用者體驗。于是這讓團隊人員不由得開始思考:難道要一直放任它慢下去嗎?答案當然是否定的。是以,微信團隊針對資料層的查詢進行了優化。

二、優化分析

1、使用者查詢行為分析

要優化,首先需要了解使用者的查詢習慣,這裡的使用者包含了頁面使用者和異常檢測服務。于是微信團隊盡可能多地上報使用者使用多元監控平台的習慣,包括但不限于:常用的查詢類型、每個協定表的查詢次元和查詢名額、查詢量、失敗量、耗時資料等。

在分析了使用者的查詢習慣後,有了以下發現:

  • 【時間序列】查詢占比 99% 以上

出現如此懸殊的比例可能是因為:調用一次次元枚舉,即可擷取所關心的各個次元。但是針對每個次元組合值,無論是頁面還是異常檢測都會在查詢次元對應的多條時間序列曲線中,進而出現「時間序列查詢」比例遠遠高于「次元枚舉查詢」。

  • 針對1天前的查詢占比約 90%

出現這個現象可能是因為每個頁面資料都會帶上幾天前的資料對比來展示。異常檢測子產品每次會對比大約 7 天資料的曲線,造成了對大量的非實時資料進行查詢。

2、資料層架構

分析完使用者習慣,再看下目前的資料層架構。多元監控底層的資料存儲/查詢引擎選擇了 Apache-Druid 作為資料聚合、存儲的引擎,Druid 是一個非常優秀的分布式 OLAP 資料存儲引擎,它的特點主要在于出色的預聚合能力和高效的并發查詢能力,它的大緻架構如圖:

「資料庫」微信海量資料查詢如何從1000ms降到100ms?
節點 解析
Mater節點

Overlord:實時資料攝入消費控制器

Coordinator:協調叢集上資料分片的釋出和負載均衡

實時節點

MiddleManager:實時資料寫入中間管理者,建立 Peon 節點進行資料消費任務并管理其生命周期

Peon:消費實時資料,打包并釋出實時資料分片

存儲節點

Historical:存儲資料分片

DeepStorage:分片中轉存儲,不對外查詢

MetaDataStorage:元資訊,如表結構

Zookeeper:存儲實時任務和狀态資訊

3、為什麼查詢會慢

查詢慢的核心原因,經微信團隊分析如下:

  • 協定資料分片存儲的資料片段為 2-4h 的資料,每個 Peon 節點消費回來的資料會存儲在一個獨立分片。
  • 假設異常檢測擷取 7 * 24h 的資料,協定一共有 3 個 Peon 節點負責消費,資料分片量級為 12*3*7 = 252,意味着将會産生 252次 資料分片 I/O。
  • 在時間跨度較大時、MiddleManager、Historical 處理查詢容易逾時,Broker 記憶體消耗較高。
  • 部分協定次元字段非常複雜,次元排列組合極大 (>100w),在處理此類協定的查詢時,性能就會很差。

三、優化方案設計

根據上面的分析,團隊确定了初步的優化方向:

  • 減少單 Broker 的大跨度時間查詢。
  • 減少 Druid 的 Segments I/O 次數。
  • 減少 Segments 的大小。

1、拆分子查詢請求

在這個方案中,每個查詢都會被拆解為更細粒度的“子查詢”請求。例如連續查詢 7 天的時間序列,會被自動拆解為 7 個 1天的時間序列查詢,分發到多個 Broker,此時可以利用多個 Broker 來進行并發查詢,減少單個 Broker 的查詢負載,提升整體性能。

「資料庫」微信海量資料查詢如何從1000ms降到100ms?

但是這個方案并沒有解決 Segments I/O 過多的問題,是以需要在這裡引入一層緩存。

2、拆分子查詢請求+Redis Cache

這個方案相較于 v1,增加了為每個子查詢請求維護了一個結果緩存,存儲在 Redis 中:

「資料庫」微信海量資料查詢如何從1000ms降到100ms?

假設擷取 7*24h 的資料,Peon 節點個數為 3,如果命中緩存,隻會産生 3 次 Druid 的 Segments I/O (最近的 30min)資料,相較幾百次 Segments I/O 會大幅減少。

接下來看下具體方法:

(1)時間序列子查詢設計

針對時間序列的子查詢,子查詢按照「天」來分解,整個子查詢的緩存也是按照天來聚合的。以一個查詢為例:

{
    "biz_id": 1, // 查詢協定表ID:1
    "formula": "avg_cost_time", // 查詢公式:求平均
    "keys": [
        // 查詢條件:次元xxx_id=3
        {"field": "xxx_id", "relation": "eq", "value": "3"}
    ],
    "start_time": "2020-04-15 13:23", // 查詢起始時間
    "end_time": "2020-04-17 12:00" // 查詢結束時間
}           

其中 biz_id、 formula,、keys 了每個查詢的基本條件。但每個查詢各不相同,不是這次讨論的重點。

本次優化的重點是基于查詢時間範圍的子查詢分解,而對于時間序列子查詢分解的方案則是按照「天」來分解,每個查詢都會得到當天的全部資料,由業務邏輯層來進行合并。

舉個例子,04-15 13:23 ~ 04-17 08:20 的查詢,會被分解為 04-15、04-16、04-17 三個子查詢,每個查詢都會得到當天的全部資料,在業務邏輯層找到基于使用者查詢時間的偏移量,處理結果并傳回給使用者。

每個子查詢都會先嘗試擷取緩存中的資料,此時有兩種結果:

結果 解析
緩存未命中 如果子查詢結果在緩存中不存在,即 cache miss。隻需要将調用 DruidBorker 擷取資料,異步寫入緩存中,同時該子查詢緩存的修改的時間即可。
緩存命中

在談論命中之前,首先引入一個概念「門檻值時間(threshold_time)」。它表示緩存更新前的一段時間(一般為10min)。我們預設緩存中的資料是不被信任的,因為可能因為資料積壓等情況導緻一部分資料延遲入庫。

如果子查詢命中了緩存,則存在兩種情況:「緩存部分命中」和「緩存完全命中」。其中部分命中如下圖所示。

  • 緩存部分被命中:
end_time > cache_update_time - threshold_time:這種情況說明了「緩存部分被命中」,從 cache_update_time-thresold_time 到 end_time 這段時間都不可信,這段不可信的資料需要從 DruidBroker 中查詢,并且在擷取到資料後異步回寫緩存,更新 update 時間。
「資料庫」微信海量資料查詢如何從1000ms降到100ms?
  • 緩存完全命中:
  • 而緩存完全命中則是種理想形式:end_time > cache_update_time - threshold_time。這種情況說明了緩存被完全命中,緩存中的資料都可以被相信,這種情況下直接拿出來就可以了。
「資料庫」微信海量資料查詢如何從1000ms降到100ms?

經過上述分析不難看出:對于距離現在超過一天的查詢,隻需要查詢一次,之後就無需通路 DruidBroker 了,可以直接從緩存中擷取。

而對于一些實時熱資料,其實隻是查詢了cache_update_time-threshold_time 到 end_time 這一小段的時間。在實際應用裡,這段查詢時間的跨度基本上在 20min 内,而 15min 内的資料由 Druid 實時節點提供。

(2)次元組合子查詢設計

次元枚舉查詢和時間序列查詢不一樣的是:每一分鐘,每個次元的量都不一樣。而次元枚舉拿到的是各個次元組合在任意時間的總量,是以基于上述時間序列的緩存方法無法使用。在這裡,核心思路依然是打散查詢和緩存。對此,微信團隊使用了如下方案:

緩存的設計采用了多級備援模式,即每天的資料會根據不同時間粒度:天級、4小時級、1 小時級存多份,進而适應各種粒度的查詢,也同時盡量減少和 Redis 的 IO 次數。

每個查詢都會被分解為 N 個子查詢,跨度不同時間,這個過程的粗略示意圖如下:

「資料庫」微信海量資料查詢如何從1000ms降到100ms?

舉個例子:例如 04-15 13:23 ~ 04-17 08:20 的查詢,會被分解為以下 10 個子查詢:

04-15 13:23 ~ 04-15 14:00

04-15 14:00 ~ 04-15 15:00

04-15 15:00 ~ 04-15 16:00

04-15 16:00 ~ 04-15 20:00

04-15 20:00 ~ 04-16 00:00

04-16 00:00 ~ 04-17 00:00

04-17 00:00 ~ 04-17 04:00

04-17 00:00 ~ 04-17 04:00

04-17 04:00 ~ 04-17 08:00

04-17 08:00 ~ 04-17 08:20

這裡可以發現,查詢 1 和查詢 10,絕對不可能出現在緩存中。是以這兩個查詢一定會被轉發到 Druid 去進行。2~9 查詢,則是先嘗試通路緩存。如果緩存中不存在,才會通路 DruidBroker,在完成一次通路後将資料異步回寫到 Redis 中。

次元枚舉查詢和時間序列一樣,同時也用了 update_time 作為資料可信度的保障。因為最細粒度為小時,在理想狀況下一個時間跨越很長的請求,實際上通路 Druid 的最多隻有跨越 2h 内的兩個首尾部查詢而已。

3、更進一步-子次元表

通過子查詢緩存方案,我們已經限制了 I/O 次數,并且保障 90% 的請求都來自于緩存。但是次元組合複雜的協定,即 Segments 過大的協定,仍然會消耗大量時間用于檢索資料。

是以核心問題在于:能否進一步降低 Segments 大小?

次元爆炸問題在業界都沒有很好的解決方案,大家要做的也隻能是盡可能規避它,是以這裡,團隊在查詢層實作了子次元表的拆分以盡可能解決這個問題,用空間換時間,具體做法為:

● 對于次元複雜的協定,抽離命中率高的低基數次元,建立子次元表,實時消費并入庫資料。

● 查詢層支援按照使用者請求中的查詢次元,比對最小的子次元表。

「資料庫」微信海量資料查詢如何從1000ms降到100ms?

四、優化成果

1、緩存命中率>85%

在做完所有改造後,最重要的一點便是緩存命中率。因為大部分的請求來自于1天前的曆史資料,這為緩存命中率提供了保障:

  • 子查詢緩存完全命中率(無需查詢Druid):86%
  • 子查詢緩存部分命中率(秩序查詢增量資料):98.8%

最明顯的效果就是,查詢通路 Druid 的請求,下降到了原來的 10% 左右。

2、查詢耗時優化至 100ms

在整體優化過後,查詢性能名額有了很大的提升:

平均耗時 1000+ms -> 140ms;P95:5000+ms -> 220ms。

「資料庫」微信海量資料查詢如何從1000ms降到100ms?
「資料庫」微信海量資料查詢如何從1000ms降到100ms?

結語

微信多元名額監控平台 ,是微信監控平台的重要組成部分。在分析了使用者資料查詢行為之後,我們找到了資料查詢慢的主要原因,通過減少單 Broker 的大跨度時間查詢、減少 Druid 的 Segments I/O 次數、減少 Segments 的大小。我們實作了緩存命中率>85%、查詢耗時優化至 100ms。當然,系統功能目前也或多或少尚有不足,在未來團隊會繼續探索前行,力求使其覆寫更多的場景,提供更好的服務。

文章來源:仇弈彬_騰訊雲開發者_https://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&mid=2247632041&idx=1&sn=0947b823159368aee1448785026efc01&chksm=eaa6def9ddd157efb1165880daccd3b592c55e71f9bec97f5286525fa2ad3af68027ec85d64b&scene=178&cur_album_id=2650119358723145730#rd

繼續閱讀