最近有做一個Prometheus metrics代理的一個小專案,暫稱為prom-proxy
,目的是為了解析特定的指標(如容器、traefik、istio等指標),然後在原始指標中加入應用ID(當然還有其他指標操作,暫且不表)。經過簡單的本地驗證,就釋出到聯調環境,跑了幾個禮拜一切正常,以為相安無事。但自以為沒事不代表真的沒事。
昨天突然老環境和新上prom-proxy
的環境都出現了資料丟失的情況,如下圖:
prom-proxy
有一個自服務指標request_total
,經觀察發現,該指標增長極慢,因而一開始懷疑是傳送端的問題(這是一個誤區,後面會講為何要增加快取功能)。
進一步排查,發現上游傳送端(使用的是victoriaMetrics的vmagent元件)出現瞭如下錯誤,說明是prom-proxy
消費的資料跟不上vmagent產生的資料:
2022-03-24T09:55:49.945Z warn VictoriaMetrics/app/vmagent/remotewrite/client.go:277 couldn't send a block with size 370113 bytes to "1:secret-url": Post "xxxx": context deadline exceeded (Client.Timeout exceeded while awaiting headers); re-sending the block in 16.000 seconds
出現這種問題,首先想到的是增加併發處理功能。當前的併發處理數為8(即後臺的goroutine數目),考慮到線上宿主機的core有30+,因此直接將併發處理數拉到30。經驗證發現毫無改善。
另外想到的一種方式是快取,如使用kafka或使用golang自帶的快取chan。但使用快取也有問題,如果下游消費能力一直跟不上,快取中將會產生大量積壓的資料,且Prometheus監控指標具有時效性,積壓過久的資料,可用性並不高又浪費儲存空間。
下面是使用了快取chan的例子,s.reqChan
的初始大小設定為5000,並使用cacheTotal
指標觀察快取的變更。這種方式下,資料接收和處理變為了非同步(但並不完全非同步)。
上面一開始有講到使用
request_total
檢視上游的請求是個誤區,是因為請求統計和請求處理是同步的,因此如果請求沒有處理完,就無法接受下一個請求,request_total
也就無法增加。
func (s *Server) injectLabels(w http.ResponseWriter, r *http.Request) {
data, _ := DecodeWriteRequest(r.Body)
s.reqChan <- data
cacheTotal.Inc()
w.WriteHeader(http.StatusNoContent)
}
func (s *Server) Start() {
go func() {
for data := range s.reqChan {
cacheTotal.Dec()
processor := s.pool.GetWorkRequest()
go func() {
processor.JobChan <- data
res := <-processor.RetChan
if 0 != len(res.errStr) {
log.Errorf("err msg:%s,err.code:%d", res.errStr, res.statusCode)
return
}
}()
}
}()
}
上線後觀察發現cacheTotal
的統計增加很快,說明之前就是因為處理能力不足導致request_total
統計慢。
至此似乎陷入了一個死衚衕。多goroutine和快取都是不可取的。
回顧一下,prom-proxy
中處理了cadvisor、kube-state-metrics、istio和traefik的指標,同時在處理的時候做了自監控,統計了各個型別的指標。例如:
prom-proxy_metrics_total{kind="container"} 1.0396728e+07
prom-proxy_metrics_total{kind="istio"} 620414
prom-proxy_metrics_total{kind="total"} 2.6840415e+07
在cacheTotal
迅猛增加的同時,發現request_total
增長極慢(表示已處理的請求),且istio
型別的指標處理速率很慢,,而container
型別的指標處理速度則非常快。這是一個疑點。
vmagent的一個請求中可能包含上千個指標,可能會混合各類指標,如容器指標、閘道器指標、中介軟體指標等等。
通過排查istio
指標處理的相關程式碼,發現有三處可以優化:
- 更精確地匹配需要處理的指標:之前是通過字首萬用字元匹配的,經過精確匹配之後,相比之前處理的指標數下降了一半。
- 程式碼中有重複寫入指標的bug:這一處IO操作耗時極大
- 將寫入指標操作放到獨立的goroutine pool中,獨立於標籤處理
經過上述優化,上線後發現快取為0,效能達標!
一開始在開發完
prom-proxy
之後也做了簡單的benchmark測試,但考慮到是在辦公網驗證的,網速本來就慢,因此註釋掉了寫入指標的程式碼,初步驗證效能還算可以就結束了,沒想到埋了一個深坑。所以所有功能都需要覆蓋驗證,未驗證的功能點都有可能是坑!
總結
- 服務中必須增加必要的自監控指標:對於高頻率請求的服務,增加請求快取機制,即便不能削峰填谷,也可以作為一個監控指標(通過Prometheus metric暴露的),用於觀察是否有請求積壓;此外由於很多線上環境並不能直接到宿主機進行操作,像獲取火焰圖之類的方式往往不可行,此時指標就可以作為一個參考模型。
- 進行多維度度、全面的benchmark:程式碼效能分為計算型和IO型。前者是演算法問題,後者則涉及的問題比較多,如網路問題、併發不足的問題、使用了阻塞IO等。在進行benchmark的時候可以將其分開驗證,即註釋掉可能耗時的IO操作,首先驗證計算型的效能,在計算型效能達標時啟用IO操作,進一步做全面的benchmark驗證。