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)

posted @ 2023-12-18 15:55  独揽风月  阅读(415)  评论(0编辑  收藏  举报