vm-insert到vm-storage链路上的配置说明
作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢!
本文是为了解决Victoria-Metrics中的过载问题而得出的较好实践。也是为了回答这样一些问题:
- vm-storage的单核写入性能的极限在哪里?(请见之前的三篇文章)
- 怎么样才能感知到,vm-insert到vm-storage这条写入链路上已经发生了过载?
- 如果写入链路发生过载,是否会丢数据?如果丢数据,究竟在哪个环节丢数据?过载了如何做保护,如何避免雪崩?
下面开始回答以下问题:
1.vm-storage的单核写入性能的极限在哪里?
- 全是新metric的情况:6000/核/s
- 全是旧的metric的情况:43万/核/s
- 新metric占整体1%的情况:24.6万/核/s
此外:写入性能是否会随着时间推移索引数增多后,导致写入性能下降?
会的!原因在于内存占用量升高,tsid cache命中率降低导致。
只要活跃的metric都在tsid cache中,写入性能就不会受到影响。
2.写入链路过载感知
2.1 vm-storage中,几乎不会发生丢数据的情况
统计drop相关的metric,可以看出vm-storage上丢弃的数据量:
sum by () (rate(vm_hourly_series_limit_rows_dropped_total{tenant=~"$tenant",namespace=~"$namespace",env_name=~"$env_name",pod_name=~"${cluster}.+"})) +
sum by () (rate(vm_daily_series_limit_rows_dropped_total{tenant=~"$tenant",namespace=~"$namespace",env_name=~"$env_name",pod_name=~"${cluster}.+"})) +
sum by () (rate(vm_concurrent_addrows_dropped_rows_total{tenant=~"$tenant",namespace=~"$namespace",env_name=~"$env_name",pod_name=~"${cluster}.+"}))
或者,把接收到的行数为分母,把最终写入存储的行数为分子,可以计算出vm-storage上的写入成功率:
sum by () (rate(vm_rows_added_to_storage_total{tenant=~"$tenant",namespace=~"$namespace",env_name=~"$env_name",pod_name=~"${cluster}.+"}[1m])) /
sum by () (rate(vm_vminsert_metrics_read_total{tenant=~"$tenant",namespace=~"$namespace",env_name=~"$env_name",pod_name=~"${cluster}.+"}[1m]))
在没有刻意限制每小时和每天的新metric数量的情况下,实测过程中没有发现任何vm-storage上的数据丢弃。
2.2 vm-insert中的数据丢弃逻辑:
通过配置 -dropSamplesOnOverload
来丢弃数据
see: app/vminsert/netstorage/netstorage.go:53
func (sn *storageNode) push(buf []byte, rows int) error {
if len(buf) > maxBufSizePerStorageNode {
logger.Panicf("BUG: len(buf)=%d cannot exceed %d", len(buf), maxBufSizePerStorageNode)
}
sn.rowsPushed.Add(rows)
if sn.trySendBuf(buf, rows) {
// Fast path - the buffer is successfully sent to sn.
return nil
} // 只要缓冲区写满,且配置了丢弃数据,就会直接丢弃数据。丢弃数据能够避免vm-insert OOM崩溃
if *dropSamplesOnOverload && atomic.LoadUint32(&sn.isReadOnly) == 0 {
sn.rowsDroppedOnOverload.Add(rows)
dropSamplesOnOverloadLogger.Warnf("some rows dropped, because -dropSamplesOnOverload is set and vmstorage %s cannot accept new rows now. "+
"See vm_rpc_rows_dropped_on_overload_total metric at /metrics page", sn.dialer.Addr())
return nil
}
// Slow path - sn cannot accept buf now, so re-route it to other vmstorage nodes.
if err := sn.rerouteBufToOtherStorageNodes(buf, rows); err != nil {
return fmt.Errorf("error when re-routing rows from %s: %w", sn.dialer.Addr(), err)
}
return nil
}
由源码可知:当vm-storage达到瓶颈,vm-insert与vm-storage之间的传输必然变慢,变慢后导致netstorage客户端的缓冲区无法及时释放。这个时候如果配置了-dropSamplesOnOverload
选项,就会直接丢弃数据。
丢弃数据后,通过metric vm_rpc_rows_dropped_on_overload_total
会体现出丢弃的量。
虽然这个配置项能够避免vm-insert发生OOM(Out of memory)崩溃,但是这个层面上的数据丢弃无法补救。
建议vm-insert不要配置-dropSamplesOnOverload选项
vm-insert的http remote write协议层面
作为http服务器,可以看看vm-insert在http协议上如何处理过载:
see: lib/writeconcurrencylimiter/concurrencylimiter.go:32
// Do calls f with the limited concurrency.
func Do(f func() error) error {
// Limit the number of conurrent f calls in order to prevent from excess
// memory usage and CPU thrashing.
select {
case ch <- struct{}{}: // 放入channel成功代表允许的并发数足够
err := f() // 执行具体的协议回调函数
<-ch
return err
default:
}
// All the workers are busy.
// Sleep for up to *maxQueueDuration.
concurrencyLimitReached.Inc()
t := timerpool.Get(*maxQueueDuration) // maxQueueDuration 默认60秒,也就是无法处理的请求最长要等待60秒
select {
case ch <- struct{}{}:
timerpool.Put(t)
err := f()
<-ch
return err
case <-t.C:
timerpool.Put(t)
concurrencyLimitTimeout.Inc()
return &httpserver.ErrorWithStatusCode{
Err: fmt.Errorf("cannot handle more than %d concurrent inserts during %s; possible solutions: "+
"increase `-insert.maxQueueDuration`, increase `-maxConcurrentInserts`, increase server capacity", *maxConcurrentInserts, *maxQueueDuration),
StatusCode: http.StatusServiceUnavailable, // 等待60秒后仍然没有资源,则向调用端返回http 503错误
}
}
}
由源码可知:后端过载后,发送数据变慢,变慢导致用于处理并发的协程长时间阻塞。新来的请求没有协程去处理,就进入了等待。等待足够长的时间仍然没有资源,就向调用端返回http 503错误码。
把无法处理的请求通过错误码返回,这样调用方就能够感知到后端的过载了。
通过metric可以查询到vm-insert上拒绝的请求数:vm_http_request_errors_total{protocol="promremotewrite"}
到这里,怎么样才能感知到,vm-insert到vm-storage这条写入链路上已经发生了过载?
这个问题已经有了答案:
1.关闭-dropSamplesOnOverload
选项,不要在vm-insert这个层面上丢包;
2.通过http remote write的状态码 http 503 来感知后端是否已经过载;
vm-insert仍然有其他细节可能导致丢包,比如配置了
-maxInsertRequestSize
/-maxLabelValueLen
/-maxLabelsPerTimeseries
等选项的情况下。
可以通过下面的表达式来查询vm-insert这个环节的转发成功率:
sum by () (rate(vm_rpc_rows_sent_total{tenant=~"$tenant",namespace=~"$namespace",env_name=~"$env_name",pod_name=~"${cluster}.*"})) /
sum by () (rate(vm_rows_inserted_total{tenant=~"$tenant",namespace=~"$namespace",env_name=~"$env_name",pod_name=~"${cluster}.*",type="promremotewrite"}))
如何合理配置vm-insert的参数
首先,分析一下vm-insert写数据到vm-storage的延迟是多少:
max by () (rate(vm_rpc_send_duration_seconds_total{tenant=~"$tenant",namespace=~"$namespace",env_name=~"$env_name",pod_name=~"${cluster}.*"}[1m]))
我实验群集查询出来的数值是 0.996~1.01秒。
在 -maxConcurrentInserts
参数不配置的情况下,每核的默认并发为4.
因此,每协程的延迟大约是: 1000ms / 4核 / 4 协程 = 62.5ms
vm-insert与vm-storage之间的延迟如此之短,所以 -insert.maxQueueDuration=60s
这个时间实在太长了。我在压测中发现,-insert.maxQueueDuration=2s
能够带来很好的吞吐量,并使vm-insert的内存压力很小。
-insert.maxQueueDuration=2s
后,remote write客户端的请求延迟要略大于2s。这样的话,vm写入链路上如果发生过载,绝大多数的过载请求都会通过 http 503来体现。
总结一下:
- 通过metric
max by () (rate(vm_rpc_send_duration_seconds_total{tenant=~"$tenant",namespace=~"$namespace",env_name=~"$env_name",pod_name=~"${cluster}.*"}[1m]))
来计算实例的最大延迟。然后除以总的协程数,得到每个协程的延迟。 - 队列中的等待时间
insert.maxQueueDuration
要与上面得到的延迟值相对等同。否则,过载情况下会积累非常多远超于延迟时间的请求,容易导致vm-insert发生OOM - vm-insert的参数
-maxConcurrentInserts
建议不配置,使用默认的每核4协程的并发量。- 协程数增加对于vm-insert的吞吐量没有明显提升,反而导致增加协程调度的消耗。
- 实际压测中发现每核4协程的吞吐量很好,瓶颈还是在vm-storage一侧。
- 关闭
-dropSamplesOnOverload
,把过载的信息传给上游。然后由上游来选择丢弃还是重试。
3.过载的一系列问题
如果写入链路发生过载,是否会丢数据?如果丢数据,究竟在哪个环节丢数据?过载了如何做保护,如何避免雪崩?
综合上面的信息:
- 如果不在vm-insert上配置
-dropSamplesOnOverload
选项,几乎不会发生丢数据- 如果需要知道在某些数据合法性检查的环节是否有丢数据,可以通过vm中的某些metric计算出来具体丢了多少;(压测过程中一次也没发生过)
- 丢数据的最大环节仍然在vm-insert上,导致丢弃的原因是netstorage客户端的buffer写满。
- 过载保护最好的位置是在remote write的发送端,通过http 503错误码来感知后端是否过载。
- vm-storage过载后,表现为CPU耗满,插入时没有对应的可调度协程。最终导致vm-insert端连接超时。也就是说,vm-storage自身不会因为写入太多而发生崩溃。
- vm-insert的过载一般由于vm-storage变慢导致。过载后,等待处理的缓冲区的数据变多,没有可用的insert协程,最终触发超时,返回http 503错误。只要
insert.maxQueueDuration
参数设置合理,一般也不会发生vm-insert上的崩溃。
总结
- 把vm-insert和vm-storage看成一个整体,在这条路径上可以配置为不丢弃数据;
- 把过载的检查和过载后的等待/重试等任务交给remote write的客户端;
- vm-storage的单核性能极为强悍,且能够做到水平扩容,可以认为其数据写入能力是没有上限的。
posted on 2022-07-13 20:07 ahfuzhang 阅读(1216) 评论(0) 编辑 收藏 举报