記一次victoriaMetrics代理性能優化問題


最近有做一個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驗證。

后續

喜聞樂見的后續來了。。。

由於公司有兩個大的線上集群,暫稱為more集群和less集群,很不幸,性能達標的就是less集群的,其指標數據相比more集群來說非常less,大概是前者的十分之一。上到more集群之后服務內存直接達到50G,多個副本一起吃內存,直接將節點搞掛了。

迫不得已(又是那句話,感覺對了的點往往不對),重新做了pprof壓力測試,發現內存黑洞就是下面這個函數(來自Prometheus),即便在辦公電腦下進行壓測,其內存使用仍然達到好幾百M。該函數主要是讀取vmagent傳來的請求,首先進行snappy.Decode解碼,然后unmarshal到臨時變量wr中。低流量下完全沒有問題,但高流量下完全無法應對:

func DecodeWriteRequest(r io.Reader) (*ReqData, error) {
	compressed, err := ioutil.ReadAll(r)
	if err != nil {
		return nil, err
	}
	reqBuf, err := snappy.Decode(nil, compressed)
	if err != nil {
		return nil, err
	}
	var wr prompb.WriteRequest
	if err := proto.Unmarshal(reqBuf, &wr); err != nil {
		return nil, err
	}
	return &ReqData{
		reqBuf: reqBuf,
		wr:     &wr,
	}, nil
}

解決辦法就是拿出sync.pool大殺器,下面方式參考了victoriaMetrics的byteutil庫(代碼路徑lib/byteutil),有興趣的可以去看下,經過壓測,相同測試情況下內存降到了不足100M。

func DecodeWriteRequest(r io.Reader, callback func(*prompb.WriteRequest)) error {
	ctx := getPushCtx(r)
	defer putPushCtx(ctx)
	if err := ctx.Read(); err != nil {
		return err
	}

	bb := bodyBufferPool.Get()
	defer bodyBufferPool.Put(bb)

	var err error
	bb.B, err = snappy.Decode(bb.B[:cap(bb.B)], ctx.reqBuf.B)
	if err != nil {
		return err
	}

	wr := getWriteRequest()
	defer putWriteRequest(wr)
	if err := wr.Unmarshal(bb.B); err != nil {
		return err
	}

	callback(wr)
	return nil
}

這樣一來性能完全達標,10core下單pod每秒可以處理250w個指標!

重新發布線上,自然又出問題了,這次prom-proxy服務一切正常,但導致后端vmstorage(victoriametrics的存儲服務)內存爆滿。經過初步定位,是由於出現了slow insert,即出現大量 active time series導致緩存miss,進而導致內存暴增(prom-proxy服務會在原始指標中增加標簽,並創建其他新的指標,這兩類指標數目非常龐大,都屬於active time series)。

最終的解決方式是將修改的指標作分類,並支持配置化啟用,即如果修改的指標類型有:A、B、C、D四類。首先上線A,然后上線B,以此類推,讓vmstorage逐步處理active time series,以此減少對后端存儲的瞬時壓力。

vmstorage有一個參數:--storage.maxDailySeries,它可以限制active time series的數目。但環境中正常情況下就有大量active time serials,如果設置了這個參數,新增的active time serials極有可能會擠掉老的active time serials,造成老數據丟失。

至此,結束。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM