最近有做一个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验证。