记一次 VictoriaMetrics 代理性能优化问题
本文转自 cnblog,原文:https://www.cnblogs.com/charlieroro/p/16054058.html,版权归原作者所有。欢迎投稿,投稿请添加微信好友:cloud-native-yang
最近有做一个 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[1],即出现大量 active time series[2]导致缓存 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
,造成老数据丢失。
至此,结束。
引用链接
[1]slow insert: https://docs.victoriametrics.com/FAQ.html#what-is-a-slow-insert
[2]active time series: https://docs.victoriametrics.com/FAQ.html#what-is-an-active-time-series
你可能还喜欢
点击下方图片即可阅读
云原生是一种信仰 🤘
关注公众号
后台回复◉k8s◉获取史上最方便快捷的 Kubernetes 高可用部署工具,只需一条命令,连 ssh 都不需要!
点击 "阅读原文" 获取更好的阅读体验!
发现朋友圈变“安静”了吗?