背景
衆所周知,Metrics名額資料是可觀測重要的基石之一,在2021年底的時候,B站基于Promtheus+Thanos 方案,完成了統一監控平台的落地。但随着B站業務迅猛發展,名額資料級也迎來了爆炸式增長,不僅給現有監控系統的穩定(可用性, 雲上監控資料品質等)帶來沖擊,也無法滿足公司層面可觀測穩定性建設(1-5-10)目标。目前架構面臨的痛點總結如下:
- 穩定性差:由于當時告警計算是基于Promtheus本地計算的,target監控執行個體在排程時,一個應用的所有執行個體必須要被同一個Promtheus采集,每當一些大應用釋出時,pod name 等一些中繼資料名額label就會發生變化,重新建構timeseries 索引時,會占用大量記憶體,導緻Promtheus oom。
- 使用者查詢體驗差:頻發的oom告警,經常導緻資料斷點,再加上Promtheus+Thanos 查詢性能性能有限,經常出現面闆查詢慢/逾時,open api 查詢失敗,誤告警等。
- 雲上監控資料品質差:由于我們使用了許多不同廠商的雲主機,即使同一廠商也有多個賬号和不同地域,有自建可用區和雲上可用區打通的,也有不打通的,有專線的,也有非專線的,網絡拓撲複雜,經常出現因網絡不通導緻無監控資料。同時每個賬号下都是一套獨立的Promtheus采集,是以存在多個雲監控資料源,使用者很難選擇需要的雲資料源。
2.0 架構設計
設計思路
- 采集存儲分離:由于Promtheus是采集存儲一體的,導緻我們在target監控執行個體排程分發時,很難快速彈性擴縮,同時也不太容易排程到不同的采集節點。是以要實作采集存儲分離的架構,target執行個體可以動态排程,采集器可以彈性擴縮。
- 存算分離:由于Promtheus也是存算一體的,但随着業務發展,名額資料量級和計算需求往往不是線性關系的,比如在存儲資源增加不到一倍的情況,計算資源(查詢需求)需要兩倍以上,是以,如果按照存算一體的方式采購伺服器資源,勢必造成一些資源浪費。是以要實作存算分離的架構,在寫入,存儲和查詢都可以分别進行彈性擴縮。
- 時序資料庫選型:我們了解到VictoriaMetrics(以下簡稱VM)已經被越來越多的公司做為時序資料庫,同時經過我們的調研,在寫入&查詢性能,分布式架構,VM運維效率都滿足我們的需求。(關于VM一些選型優勢,本文不在讨論,下文會有介紹對VM的一些查詢優化)
- 單元化容災:由于我們是大多數場景(95%)基于pull模式的,每個target監控執行個體需要按照一定的排程規則,配置設定到不同的采集器。但是之前沒有一個标準,有的場景按照cluster次元排程,有的場景按照idc排程,有的按照instance name次元,導緻名額資料無法做到從采集->傳輸->存儲→查詢 整個鍊路單元内閉環。是以,我們制定了一個新标準,全部按照zone次元排程,可實作同zone下,全鍊路單元内容災。
基于以上核心的設計思路,設計了監控2.0架構。首先先看下整體的功能架構:
功能架構概覽
通過上面的功能架構圖可以看出,Metrics 名額資料,不僅應用在大盤、告警等基礎場景,一些業務場景、釋出平台都會依賴名額資料,做一些流程決策。是以2.0架構的落地,存在以下幾個方面的挑戰:監控系統自身穩定性,資料可用性,查詢性能,故障爆炸半徑等。
整體架構
如上是整體的技術架構,下面重點從資料來源,資料采集,資料存儲,和資料查詢方面介紹:
資料來源
從大的分類來看,監控覆寫場景主要分為paas層(應用監控,在/離線元件和一些中間件監控)和iaas層(自建機房伺服器/雲主機,容器監控和網絡監控)。由于之前是基于pull的方式發現target監控執行個體,主要存在兩方面問題:
- 發現target監控執行個體存在延遲:因為pull的方式有一定的同步周期,比如30s, 當一個新的監控執行個體出現,需要30s才能發現,是以看監控名額要比預期晚30s。
- 運維成本較大:當pull 業務方提供的接口有問題時,業務方一般無法第一時間感覺問題,需要我們去主動協同處理。
針對上面問題,我們将pull 方式改成push方式,讓業務方主動push 需要被監控的target執行個體。這樣可以實時的push,解決因pull 方式存在的30s延遲,同時,為了讓各個業務方更方面的管理target執行個體,在名額平台提供按內建任務申請監控接入,并且實時顯示target執行個體采集狀态,采集數量和采集耗時,進一步提升監控接入狀态的可見性。
資料采集
排程層
排程層主要負責采集job配置的生成和target監控執行個體的分發。為了實作單元内鍊路全閉環,排程層分為一級排程(所有采集配置)和二級排程(本機房内采集配置)
一級排程(Master)
- 從資料庫中,拿到全量的采集job配置和target監控執行個體。根據采集排程配置(zone次元排程),在記憶體中建構各個二級排程所需的采集配置。
- 為了保證Master資料高可用,當通路依賴資料庫異常,記憶體快照資料不更新。同時在其他異常的場景下,我們也做了一些記憶體快照資料保護政策:如,當一次性删除>5k targets的job, 一次更新diff 量減少> 5k targets時,平台會攔截保護,做doubble check, 防止使用者誤操作。
- 目前通過一些手段,異步,記憶體cache, 多協程等方式,Master 全量排程時間從50s降低到10s内。
二級排程(Contractor)
- 根據采集叢集名稱+版本号定時去Master拿本機房的采集配置。設計多套二級排程,為了防止故障爆炸半徑。
- 拿到本機房采集配置後,根據Collector 心跳,拿到目前health 采集節點(Collector),根據執行個體和容量次元進行排程,将采集配置分給對應的采集節點。
- 當Master采集配置或采集節點有更新時,會觸發新一輪的排程。為了保證target不随機排程,在實作排程算法時,已經配置設定給某個采集節點的配置,下次排程時,還是會優先拿到該配置。
-
當Contractor應用發版,重新開機等場景時,怎麼保證名額資料無斷點或抖動呢?
在Contractor 重新開機時, 記憶體會維護一個全局state變量,等待Collector全部上報targets完成後,才開始進行targets排程,當排程完成後,state設定為ready,Contractor接口才會對Collector提供通路。
采集器
- 采集器Collector 是基于vmagent封裝了一層。主要有兩個功能,一個是定時上報心跳給Contractor, 二是拿到相關采集配置,call reload api,觸發vmagent開始采集。
- 當我們灰階了一些量後,發現vmagent占用的記憶體較高,通過heap pprof發現, 在每次pull 抓取上報的名額消耗記憶體較多,後面開啟流式采集 promscrape.streamParse=true 後,記憶體降低20%左右。
- vmagent 自身會有随機(采集間隔時間)平滑load機制。比如我們采集間隔配置了30s,當vmagent拿到配置時,一個target最慢要30s才會有名額資料。是以當Collector擴/縮容時,target會漂移到其他Collector采集時,會出現名額斷點。是以我們設計了一個機制,Collector 監聽到退出信号後,不立即退出,隻是停止上報心跳,同時繼續采集一個周期後,才會退出,這樣可以保證名額無斷點。
資料存儲
我們通過 vmstorage 進行名額的存儲。由于vmstorage涉及到存儲細節比較多,這裡主要介紹索引結構的存儲。首先認識下 vmstorage 當中幾個核心類型。
- MetricName 存儲 label kv 的變量,寫入的時候會由 vminsert 将實際名額資料當中的 label 序列化成 MetricName 進行發起寫入請求,同時在 vmselect 查詢的時候,拿到的結果集當中的 label 資訊也是由 MetricName 進行序列化存儲,在實際的存儲當中,MetricName 更類似 tv 的内容元資訊。MetricName 在 vmstorage 落盤的序列化結構如下:
4 byte account id | 4 byte projectid | metricname(__name__) | 1 | tag1 k | 1 | tag1 v | 1 | .... | tagn k | 1 | tagn v | 1 | 2 |
- MetricId 納秒時間戳,一簇 ts 的唯一鍵,在一簇 ts 第一次在 vmstorage 入庫的時候生成。8 byte。
- TSID 也是一簇 ts 的唯一鍵。由名額的租戶,名額名id,job instance labelvalue id 和 MetricId 組成。TSID 在 vmstorage 落盤的序列化結構如下
4 byte accountid | 4 byte projectid | 8 byte metricname(__name__) id | 4 byte job id | 4 byte instance id | 8 byte metricid
TSID 與 MetricId 都是一簇 ts 的唯一鍵,差別在與 MetricId 隻是 8byte 的時間戳,而 TSID 的序列化資訊在包含 MetricId 之外還包含其他非常多的辨別資訊,當 TSID 通過字典序排序之後,同租戶相同名字的名額将會在分布上連續在一起,綜合名額查詢的特點,查詢的結果總是集中在同一個租戶下的同名名額,并且都會有相似的 label 條件。是以 TSID 實際上是 vmstorage 當中 data 目錄下的 key, 在索引部分的查詢核心是根據查詢條件得到最後的 TSID。
在上述三個類型下,就能形成如下幾個抽象索引結構:
MetricName -> TSID:0 | MetricName | TSID
MetricId -> MetricName:3 | 4 byte accountid | 4 byte projectid | MetricId | MetricName
MetricId -> TSID:2 | 4 byte accountid | 4 byte projectid | MetricId | TSID
同時核心的反向索引結構抽象如下:
1 | 4 byte accountid | 4 byte projectid | metricname(__name__) | 1 | MetricId
1 | 4 byte accountid | 4 byte projectid | tag k | 1 | tag v | 1 | MetricId
結合反向索引和索引3就可以得到這麼一條流程:
根據查詢攜帶的租戶資訊和名額名稱與 label 資訊,就可以組成所需要的反向索引字首,在字典序排序的索引下,就可以通過二分查找查得所需要的 MetricId 集合。将多個條件從反向索引中查詢到的 MetricId 求交集,就能得到符合 label 查詢條件的 MetricId 集合,就能通過索引3組裝成響應的索引字首查詢到最後符合條件的 TSID。在 vmstorage 中執行查詢時,索引部分的核心目标就是根據條件求得最後所需要的 TSID 集合。
結合以上的抽象索引,vm 通過基于字首的壓縮進行存儲來達到減少磁盤占用的目的。是以相比于Prometheus,磁盤存儲成本大概有40%的降幅。
在正常的 vmstorage 使用中,vmstroage 提供了十分優秀的性能,以某一個叢集為例子,單台 48c 256g 的 vm stroage 日常足夠支撐每秒 40w 點的寫入, 2w qps 的查詢。在這一過程中,通過調整應用的 gogc 來平衡 cpu 占比來優化 vm 的資源使用。預設情況下,vmstorage 的 gogc 的值給到了 30,可能會存在比較頻繁的 gc,在查詢寫入飽和的情況下,如果與名額點的 merge 發生在一起,那麼查詢将會在短時間記憶體在比較大的握手延遲而導緻失敗。在記憶體夠用的情況下,調大 vmstorage 的 gogc ,可以以一定的記憶體為代價換來更穩定 vmstorage 運作。
資料查詢
promql 自動替換增強
在我們通過 grafana 通路 victoriametrics 進行日常名額查詢的過程中,經常會遇到某些 panel 傳回資料過慢或者是直接傳回了查詢覆寫的資料量太大而直接失敗。這部分的 panel 常常在盡可能優化了查詢條件後,仍舊無法通過正常手段查詢。
在這裡以一個 promql 語句作為例子:
histogram_quantile(0.99, sum(rate(grpc_server_requests_duration_ms_bucket{app="$app",env=~"$env"}[2m])) by (le, method))
這條語句用來查詢某個env 下的某個 app 下的所有 grpc 接口調用 p99 耗時。在這條語句中,假設一個存在 app A,其下包含 2000 個執行個體,20 個接口, 50 個調用方,5 個 le 桶,以 30s 為間隔進行一次上報,單個時間上的點符合條件的點就有 1000 w,這條語句還包括一個 2m 的時間視窗統計,那麼本次查詢總體上符合點的數量就為 4000w。這樣的查詢語句,即使能夠避開查詢的容量限制,也會影響整體的查詢資料源的穩定性。在正常的 grafana 使用習慣上,我們将會對這條語句的整體進行一次預聚合,但是在這個語句的場景下,b 站處于活躍狀态的 app 數量為大幾千,而這條語句中隻有兩位數的應用存在查詢性能問題,如果直接對這條語句進行預聚合的話,就會存在比較大的資源浪費。
同時,在這條語句中,如果将第一個參數分别修改為 0.5 和 0.9,那麼可以直接查詢得到當相應的 p50 和 p90 名額,對着這條的優化并不能幫助到 p50 和 p90的查詢,單次聚合的産出也不理想。是以,在這思考一個方式,能不能通過僅聚合存在性能問題的部分來解決包含這個語句的 panel 的查詢問題。
首先,從 vmselect 執行這個語句的流程入手,這條語句在 vmselect 執行的過程中,将會被解析成如下的一顆執行樹:
首先來描述一下該語句的執行流程,在解析得到這棵執行樹之後,将會通過深度優先的方式依次來執行各個節點。具體流程如下
- 名額資料查詢:在葉子節點處,根據名額名稱和 label 的篩選條件從 vmstorage 當中進行分布式查詢得到需要的原始名額資料
- rate 函數執行:從第1步中查詢到的名額資料根據 label 相同的名額分為一個名額簇,對相同的名額簇内的2m時間視窗中的最後一個點和第一個點相減,并除以他們之間的距離,得到 rate 函數的結果
- sum 聚合函數執行:最後根據 sum 的group by 分區鍵将第2步中的資料根據 le + method 的分區鍵進行分區,之後對分到同一個區内的名額進行 sum 操作
- histogram_quantile 函數執行:最後周遊第3步中得到的每個分區的資料進行 p99 的計算,得到最後需要的耗時分布的結果
我們在這裡繼續用前文提到的 app 舉例子,其下包含 2000 個執行個體,20 個接口, 50 個調用方,5 個 le 桶,以 30s 為間隔進行一次上報,在第一步中查詢需要得到的名額點數量就達到了 4000w。該 4000w 的資料在第二步會在時間 rate 函數的時間視窗計算當中被減少到 1000w。到第三步的時候,這 1000w 的資料會根據 le + method 的分區鍵被分成 100 個分區,最後輸出的結果相應的被縮減到 100 個。第 4 步的結果和輸入的數量相同。
是以我們可以看到,在整顆樹的執行過程中,第 1 步往往會成為真正執行過程中的性能瓶頸,第 2 步雖然在輸出上大大減少了數量,但是仍會存在巨大的名額輸出,并沒有根本解決性能問題。而如果能針對第 3 步的輸出結果去進行預聚合并将預聚合的結果用到查詢中,該查詢将會直接變成一個查詢 20 個名額點的簡單查詢。同時,再考慮一點,無論是 p50 p90 p99 的計算,到第 3 步的名額輸出為止,底層的計算邏輯是完全一緻的,那麼如果我們将第 3 步的結果進行預聚合并持久化下來,完全可以複用到任何參數的 histogram_quantile 計算分位耗時計算當中,一次預聚合的成本效益達到了相當大的價值。
那麼,我們假設已經通過實時或者定時的方式将 app A 的這條 promql 進行預聚合并将上述這個表達式的結果轉化為了名額 test_metric_app_A。
sum(rate(grpc_server_requests_duration_ms_bucket{app="A"}[2m])) by (le, method)
那麼我們實際執行查詢 app A 的 p99 分位耗時的時候,希望真正執行的查詢語句如下
histogram_quantile(0.99, test_metric_app_A)
但是,在 grafana 的 panel 配置當中,一個 panel 對應原始查詢語句和多個這樣的優化查詢的話,實際查詢時體驗将會變得非常奇怪,甚至對整體體驗會達到負優化的效果,那麼回到一開始原始語句的查詢執行樹:
如果我們把黃色的部分的子樹,直接替換成 test_metric_app_A 的查詢樹就可以無縫完成查詢的優化了。那麼我們需要在 vmselect 上面在查詢之前就建構如下的一個映射關系:
在正式執行查詢的時候,我們在執行樹解析完成的時候,從執行樹的根節點就可以開始依次檢查收否存在完全滿足映射條件的 key 的執行樹的子樹,隻要符合,就可以将原始查詢語句的子樹替換成映射的 value 樹。
這樣看似是一個非常合适的方案,但是實際的查詢過程中的替換還是存在問題。首先我們的原始語句當中,用到了 env 作為查詢條件,但是我們預聚合的結果采用的分區鍵是 le + method,在我們預聚合的結果名額當中并沒有 env,直接那這裡的預聚合名額進行替換,會直接丢失掉查詢 env 的能力,是以此處的替換是不符合實際查詢的要求的。那麼我們的預聚合語句應該修改為:
sum(rate(grpc_server_requests_duration_ms_bucket{app="A"}[2m])) by (le, method, code)
這樣的預聚合,當我們想重新将查詢語句的執行樹進行替換的時候,顯然就不能單純的将替換關系直接套在查詢的執行樹上,反之,在聚合函數外層應該加上一層新的聚合函數節點。
重新思考目前的場景,當我們存在如上的預聚合之後,我們需要的是在預聚合的基礎上自動增加一層聚合函數,就能夠達到我們需要的目的。
我們新的映射關系變成下面的樣子:
當我們得到這個映射關系之後,我們重新開始從根節點開始進行周遊的時候,會發現在 sum 這一層的分區鍵上存在不比對的關系,但是,查詢的分區鍵集合(le, method)是映射的分區鍵(le, method, env)的一部分,直接使用也對語句的執行沒有任何影響。在這個基礎上,可以直接在替換子樹的時候把新的子樹挂在這一層的聚合函數下面。替換結果如下:
達成如上的執行樹替換之後,就可以在語義無損的前提下完成 promql 的自動替換。同時,在這個轉換的支援下,對于預聚合的名額我們可以在不影響預聚合輸出數量的前提下盡可能的增加一些聚合的次元,這樣預聚合的結果可以作為子查詢用在更多的場景下。而這裡的查詢轉換,對于使用者層面在使用 grafana 進行查詢的時候,是完全無感覺的。在查詢中對于使用了預聚合的 app,将會自動對解析後的執行樹進行替換,而對于本身就不需要進行優化的查詢條件的時候,将會直接根據原始查詢語句去進行查詢,在盡可能節省預聚合所消耗的資源的前提下,對存在性能瓶頸的查詢語句進行了優化。再者,即使使用者修改了查詢語句,隻要語句中包含預聚合了的子查詢,都能夠起到優化的效果,達到了預聚合資源消耗的最大回報。
ps:(avg 聚合函數會存在一定的語義損耗,但是在大部分場景下的誤差可以忽略不計)
基于 promql 的flink 名額預聚合
對于前文中提到的預聚合,B站原本主要的預聚合都是通過定時的批量查詢來進行預聚合。這樣的查詢聚合對存儲側 vmstroage 和查詢側 vmselect 都有比較大的壓力,進而可能會影響到正常的名額查詢需求。在預聚合的角度來看,執行原生的 promql 聚合存在一定的記憶體浪費,以上文提到的一條 promql 為例子:
sum(rate(grpc_server_requests_duration_ms_bucket{app="A"}[2m])) by (le, method, code)
在這個預聚合語句當中,vmselect 将會從 vmstorage 當中全量撈出符合 grpc_server_requests_duration_ms_bucket{app="A"} 的名額并在記憶體當中保留全部的标簽,直到全部的分布式查詢完成後,進行計算,在最後的 sum 部分才會根據 group by 的 le,method,code 進行 label 收斂再得到最後的結果。這個過程中,vmselect 的記憶體常常在執行 sum 之前,就遇到了瓶頸,尤其是這裡的 rate 函數,設定了 2m 的時間視窗計算,這部分進一步擴大了 vmselect 的壓力。同時,這樣的查詢往往會成為慢查詢,這部分資料的聚合也會出現明顯的延遲。同理,定時的大批量查詢也會增加 stroage 的壓力。
為了避免查詢請求對 vm 叢集的壓力,同時盡可能保證預聚合的實效性,我們嘗試采用 flink 來進行基于 promql 的名額預聚合。
前文提到了 promql 在 vmselect 當中的執行方式。類似 sum(rate(grpc_server_requests_duration_ms_bucket{app="A"}[2m])) by (le, method, code) 這樣的表達式,将會在 vmselect 當中解析成如下的執行樹。
我們在 flink 預聚合的配置側就可以類似的對這個 promql 進行相應的解析,解析成如上的執行樹。在 flink 那一側就不需要關心原始 promql 語句的構成,隻需要得到這顆執行樹的 json 即可,同時在配置側需要關心的是在解析期間得到的其中的時間視窗資訊作為中繼資料儲存在一起,flink 的 job 将會各自配置的時間視窗配置從配置中選取符合條件的預聚合配置執行。
對于這顆執行樹,我們站在 flink 的角度去進行設計執行流程。
資料過濾階段
這個階段類比我們進行 vm 查詢名額,和實際vm查詢執行一樣,用到的都是執行的葉子節點,在配置時我們就根據每棵樹的葉子節點的名額名稱 label 建構 key,初步篩選實時 kafka 流中的名額資料,并在初步篩選完畢之後,進一步根據剩餘的名額 label 過濾條件進行過濾,在第一階段隻需要 check 執行樹的葉子結點就可以從實時名額流中得到所有符合 promql 查詢的名額原始資料
資料分區階段
sum(rate(grpc_server_requests_duration_ms_bucket{app="A"}[2m])) by (le, method, code) 的時候,我們常常在執行這個 2m 的時間視窗聚合的時候需要在記憶體中持有 2m 内的全量原始資料而存在壓力,通常情況下,一個名額點的label集合就将達到1-2k,對于預聚合是非常大的記憶體壓力。優化執行聚合過程中,記憶體中時間視窗中持有的名額點的記憶體是最核心的優化方向。
在我們通過 flink 進行分區時間視窗聚合的時候,我們正好需要依賴的就是執行樹中的 sum 節點, sum 的 group by 鍵正是 flink 當中需要的分區鍵,我們完全可以複用這裡的邏輯,将名額中對應的label 提取作為分區鍵,将資料點作為 value 進行緩存在 flink 當中的時間視窗當中進行聚合。但是,此處就存在了與 vmselect 執行語句的一樣的記憶體瓶頸。我們回顧這條語句,grpc_server_requests_duration_ms_bucket 這個名額,其實除了分區鍵當中涉及的le method code之外的label,在執行過程中實際都不是我們需要考慮的label,我們完全可以在此處對剩餘的label全部丢棄掉,隻保留一個 label 集合統一的 uuid 用來在 rate 的時候能夠 hash 到一個分桶裡進行聚合即可。
同時對于分區鍵的組合,我們甚至可以直接丢棄掉label key的部分,因為我們的分區鍵全部都是按照順序排列的,我們的分區鍵隻需要保留這兩個label 的具體value, 而對 label 的 key 直接進行丢棄,在執行的時候按照 promql 聲明的順序順序取即可,具體為promql id 和 label value,而我們的具體名額資料也隻需要保留點的label 集合uuid 和時間數值即可。此處的優化可以直接解決聚合當中的記憶體壓力。
以這條語句所涉及的一個名額為例子,一個名額所生成的分區鍵為"promqlid(四位元組 需要在時間視窗中定位具體的執行樹) + le value + method value + code value",需要緩存在時間視窗的名額點的内容為 "label set uuid (四位元組) + time(八位元組) + value(八位元組)"。
最後一個點在記憶體中固定的記憶體占用為 20 位元組,而相同分區鍵下的點之間共享的分區鍵大小也隻有4位元組 + 必須要的value串,達到了最小的記憶體消耗。
時間視窗執行階段
當我們完成資料的篩選和分區之後進入時間視窗的算子階段,我們隻需要在這個階段對這顆執行樹進行一次深度優先的調用即可。
這個階段中,由于 flink 的時間視窗限制,在具體的函數實作上會和 vm 存在一些細微的差別。
最明顯的例子為 increase 函數,在 vm 對于該參數的實作中,是通過目前時間視窗的最後一個點和前一個時間視窗的最後一個點進行相減得到的結果,在flink中,由于已經定死了緩存2分鐘的實時資料,這個實作将會造成額外的記憶體消耗,同時,在别的函數的實作上也會需要考慮額外的存儲所帶來的實作成本,是以我們選擇了 prometheus 的 increace 實作來達到我們的目的。在 prometheus 的 increase 實作中,通過同一視窗内最後一個點和最後一個點在時間坐标軸的一次函數的k值,來根據時間視窗的長度來預估這個期間的增量。
類似如此的場景,由于 vm 相比 prometheus 在很多涉及時間視窗的函數中引入了前一個時間視窗的資料來參與計算讓資料更加精準,這樣的優化在 flink 将會導緻額外的資源,所有類似的函數 flink 将會參考prometheus 的設計來實作。
同時,由于我們在上一個階段中對絕大部分的label進行了丢棄,在這裡類似 topk 這樣需要前後保留原始label 資訊的函數也存在不支援的場景。
資料上報階段
在前一個階段得到的預聚合名額資料就是我們所需要的結果,可以直接 flink 的 sink 當中通過 remotewrite 協定寫到 vminsert上。
經過以上的設計,我們基于 promql 的 flink 預聚合可以快速根據實際存在查詢瓶頸的 promql 進行快速配置生效,在實際場景下 100c 400g的 flink job 配置即可滿足每2分鐘3億個名額點的時間視窗緩存和計算需求,相比定時預聚合,大大減少了預聚合的資源需求,這裡的實作也滿足我們在透明查詢當中對資源利用最大化的期望。
查詢優化收益
查詢的自動優化加上 flink 的專項預聚合,可以針對實際情況下的查詢情況,對特定的 promql 的集中進行治理優化,在非常小的資源消耗下,日均使 20s 以上的慢查詢減少了百分之90,查詢資料源資源減少百分之50,以非常小的代價帶來非常高的收益。
資料可視化
目前我們主要使用grafana建構監控大盤,由于曆史原因,grafana使用的一直是比較老的版本(v6.7.x),但是新版grafana有非常多的功能疊代和性能優化,是以我們決定更新grafana版本到v9.2.x。但版本更新存在如下挑戰:
- v6.7.x更新到v9.2.x存在多個break change(除了官方描述的一些break change,同時也包括一些自定義資料源/pannel 插件存在break change)。
- 老版本grafana部署成本高:之前是實體機部署,且部署方式相對黑盒,同時我們增加nginx+grafana auth服務做auth proxy 認證,是以部署運維成本進一步提高。
面對版本更新存在的挑戰和問題,我們做了以下事項:
- 在更新前,我們做了大量的測試和驗證,通過一些腳本fix因為break change造成的一些資料不相容,通過opensearch資料源插件替換新版本再支援的es插件。
- 在部署方式上,首先用git倉庫管理整個grafana部署編譯腳本,讓每次版本變更不再黑盒,同時将nginx+grafana auth+grafana建構成all-in-one鏡像, 實作容器化部署,進一步降低部署成本。
- 新版本的grafana, 如果prometheus資料源使用版本>v2.37.x,變量的擷取方式預設從series api改成 label values api, 查詢性能會提升10倍左右(2s->200ms)。
更新完成後,面闆加載性能和整體使用者查詢體驗大幅提升。
整體收益
- 從Prometheus全部切到VM架構後, p90查詢耗時降低10倍以上
- 目前支援了170w+ 采集對象,全部按照zone次元排程, 采集, 實作單元内容災
- 僅增加磁盤資源情況下, 應用監控全量采集間隔從60s調整到30s,實作1-5-10中的1分鐘發現
- 名額斷流、oom告警等異常,降低90%以上
- 目前寫入吞吐44M/s,查詢吞吐48k/s, 通過查詢優化, 查詢再加速,p90查詢耗時降低到ms級(300ms)
雲監控方案
目前雲上監控存在以下痛點:
- 因有各個雲廠商,或者同一個雲廠商,因地域不同造成網絡環境不通,導緻采集失敗,出現無監控資料的情況。
- 每個賬号下都是一套獨立的Promtheus采集,是以存在多個雲監控資料源,使用者很難選擇需要的雲資料源。
雲監控方案整體和idc采集方案類似,為了友善使用者統一資料源查詢和統一告警計算,我們将Prometheus采集的雲上資料,通過remote write 回源到 idc 存儲叢集。架構如下:
比較idc采集方案,雲上監控有以下幾點不同:
1. Contractor支援從公網pull本zone所需的采集配置。
2. 為什麼使用Prometheus而不是vmagent采集?
- 改造成本低:曆史上雲上資料,都是Prometheus采集的,同時目前Collector實作層面,采集器是可插拔的,使用Prometheus采集覆寫成本低。
- Prometheus本地資料:目前雲上采集資料,一份通過remote write 回源到idc存儲叢集,同時本地會存1d 資料,當remote write 出現故障的時候,可查詢本地資料。
3. 為什麼要引入vm-auth 元件?
因為雲上資料回源到idc, 走的是公網,直接回源remote write 到vm-insert,會存在安全風險,是以我們加了vm-auth,對回源流量做租戶認證和流量排程。vm-auth配置如下:
收益
- 雲上監控全部按照zone排程後,雲上資料品質大幅提升,雲上監控無資料等oncall 降低90%以上。
- 統一資料源查詢,20+ 雲上資料源收斂到1個。
未來規劃
- 支援更長時間Metrics名額資料存儲: 目前資料預設存儲時間是15d, 希望給業務方提供更長時間的資料, 用作分析,複盤,資源預估等場景。
- 支援更細粒度的名額埋點:目前應用監控預設是30s一個點, 少部分場景支援了5s,希望後面提供更細粒度的埋點,覆寫更多的場景,為業務可觀測、排障助力。
- 自監控能力增強:目前有一套自監控鍊路,跟運維平台關聯,提供了基礎的監控自身系統的監控和告警,希望後面可以覆寫全所有的自監控場景&運維SOP。
- 名額平台疊代:目前名額平台主要提供了監控接入,采集配置管理和監控對象查詢等基礎能力,未來規劃增加寫入/查詢封禁、白名單等能力,同時希望後面可以借助大模型的能力,通過名額元資訊增強,實作text2promql(自然語言翻譯成PromQL,以及自動生成PromQL注釋)
作者:鮑森樂、剁椒
來源-微信公衆号:哔哩哔哩技術
出處:https://mp.weixin.qq.com/s/gTB_hEXJQ2gz_oP7VN3-dg