VictoriaMetrics的高可用
VictoriaMetrics的高可用
前言
VictoriaMetrics是一个快速、高效和可扩展的时序数据库,可作为Prometheus的长期存储。查询promsql,使用grafana看图时,可以直接用VictoriaMetrics源替换掉prometheus源。
架构
这里介绍集群版本的架构:
主要有3个模块:
- vmstorage: 数据存储节点,负责存储时序数据;
- vmselect: 数据查询节点,负责接收用户查询请求,向vmstorage查询时序数据;
- vminsert: 数据插入节点,负责接收用户插入请求,向vmstorage写入时序数据;
数据复制
为了保证集群的高可用,说白了就是保证数据的高可用,常用的方法无外乎两种,第一种是写入时同时写入多个节点,第二种是存储节点之间自动进行数据的同步,比如etcd使用raft协议来进行数据同步,mysql使用binlog来进行数据同步。
VictoriaMetrics采用第一种方式,vminsert
模块支持参数-replicationFactor=N
,其中N表示的是同步的节点数,写入时的核心代码如下:
// https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster/app/vminsert/netstorage/netstorage.go
func sendBufToReplicasNonblocking(snb *storageNodesBucket, br *bufRows, snIdx, replicas int) bool {
usedStorageNodes := make(map[*storageNode]struct{}, replicas)
sns := snb.sns
for i := 0; i < replicas; i++ {
idx := snIdx + i
attempts := 0
for {
attempts++
if attempts > len(sns) {
if i == 0 {
// The data wasn't replicated at all.
cannotReplicateLogger.Warnf("cannot push %d bytes with %d rows to storage nodes, since all the nodes are temporarily unavailable; "+
"re-trying to send the data soon", len(br.buf), br.rows)
return false
}
// The data is partially replicated, so just emit a warning and return true.
// We could retry sending the data again, but this may result in uncontrolled duplicate data.
// So it is better returning true.
rowsIncompletelyReplicatedTotal.Add(br.rows)
return true
}
if idx >= len(sns) {
idx %= len(sns)
}
sn := sns[idx]
idx++
if _, ok := usedStorageNodes[sn]; ok {
// The br has been already replicated to sn. Skip it.
continue
}
if !sn.sendBufRowsNonblocking(br) {
// Cannot send data to sn. Go to the next sn.
continue
}
// Successfully sent data to sn.
usedStorageNodes[sn] = struct{}{}
break
}
}
return true
}
通读上面的代码,它要做的其实就是有多少个副本,就同步多少次,每次会遍历所有的节点,直到有一个节点发送成功则继续下一次的同步,最坏的情况下所有的节点都不可用时,数据就会丢失(写入失败),哪怕只有一个节点可用,数据都不会丢(写入成功)。
Q:那node个数
定为多少个合适呢?
官方给出的建议是至少有2*N-1
个,参考文档,这样能保证数据分散的更均匀,且不会有太多的资源消耗。
查询
查询期间可能有些节点会发生故障,因此vmselect
会根据查询结果来判断数据的一致性,代码如下:
// https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster/app/vmselect/netstorage/netstorage.go
func (snr *storageNodesRequest) collectResults(partialResultsCounter *metrics.Counter, f func(result interface{}) error) (bool, error) {
var errors []error
resultsCollected := 0
for i := 0; i < len(storageNodes); i++ {
result := <-snr.resultsCh //返回结果
if err := f(result); err != nil {
errors = append(errors, err)
continue
}
resultsCollected++
// 判定为数据完整且没有错误,直接返回
if resultsCollected > len(storageNodes)-*replicationFactor {
return false, nil
}
}
isPartial := false
if len(errors) > 0 {
// 所有结果都出错了,返回第一个错误
if len(errors) == len(storageNodes) {
return false, errors[0]
}
isPartial = true
}
return isPartial, nil
}
最终结果的判断原则:
-
最完美的情况:
所有节点都正常返回且没有错误,则认为结果是完整且没有错误的: -
若正常返回的结果 > len(storageNode) - replicaFactor,即至少有
node个数-N+1
个节点可用:
则被判定为数据完整且没有错误,直接返回,因为只有这样才能保证数据是完整的; -
若所有节点都出错了:
则认为结果是错误的,并返回第1个错误 -
若部分节点返回错误:
则认为结果是不完整的,返回的结果如下:{ "status": "success", "isPartial": true, "data": { "resultType": "vector", "result": [{ "metric": { "__name__": "up", "instance": "localhost:7000", "job": "prometheus" }, "value": [1705456168, "1"] }, { "metric": { "__name__": "up", "instance": "localhost:7100", "job": "prometheus" }, "value": [1705456168, "0"] }] } }
需要注意的是这种情况下虽然数据是不完整的(isPartial=true),但不会返回错误(status=success)