Loki index实现探究

本文深入探讨了Grafana Labs的Loki项目中用于日志聚合的Loki Index结构、写入与查询流程。Loki利用标签而非日志内容创建索引,重点介绍了基于boltdb-shipper的存储模型,包括KV存储和前缀查询。文章详细分析了Index的写入过程,包括GetCacheKeysAndLabelWriteEntries和GetChunkWriteEntries函数,并解析了Index查询过程,涉及lookupSeriesByMetricNameMatchers和lookupChunksBySeries等关键步骤。
摘要由CSDN通过智能技术生成

导言

Loki 是 Grafana Labs 团队最新的开源项目,是一个水平可扩展,高可用性,多租户的日志聚合系统。它的设计非常经济高效且易于操作,因为它不会为日志内容编制索引,而是为每个日志流编制一组标签。本文档对其中标签的实现index进行探究。

Index结构

Index是Loki查询最重要的数据结构,为了理解整体查询的原理,我们必须理解index的实现。

这里以boltdb-shipper作为index的存储类型,这里注意两点即可:KV存储+前缀查询。存储模型如下图索引:
在这里插入图片描述

存储结构说明:

字段解释 index查询 boltdb使用
- seriesID:日志流ID
- shard:分片, shard = seriesID % 分片数(可配置)
- userID:租户ID
- lableName:标签名
- labelVaue: 标签值
- labelValueHash:标签值hash
- chunkID:chunk的ID(cos中key)
- bucketID: 分桶,timestamp / secondsInDay(按天分割)
- **chunkThrough:**chunk里最后一条数据的时间
- metricName:固定为logs
图中四种颜色表示的索引类型从上到下分别为:


- 数据类型1: 用于根据用户ID搜索查询所有日志流的ID
- 数据类型2: 用户根据日志流ID搜索对应的所有标签名
- 数据类型3: 用于根据用户ID和标签查询日志流的ID
- 数据类型4: 用于根据用户ID日志流ID查询底层存储ChunkID


其中数据类型1和数据类型2用于查询Label;
数据类型3和数据类型4用户查询实际数据,这个是我们经常用的
boltdb:https://github.com/boltdb/bolt
Bolt stores its keys in byte-sorted order within a bucket。


- key:HashValue + “\000” + RangeValue
- value: Value


serialID相关信息放到key里面,是可以有效利用boltdb的key是按照 byte-sorted order的特性
但是value无法排序,所以在key上面做文章;使用前缀匹配查询
key分为HashValue和RangeValue,是为了设计不同版本的schema。

streamID/seriersID/ChunkID的生成规则如下代码所示

// streamID或者Fingerprint获得
func (i *instance) getHashForLabels(ls labels.Labels) model.Fingerprint {
   
	var fp uint64
	fp, i.buf = ls.HashWithoutLabels(i.buf, []string(nil)...)
	return i.mapper.mapFP(model.Fingerprint(fp), ls)
}

// seriersID如何获得, 对应stream
seriersID := labelsSeriesID(labels)
func labelsSeriesID(ls labels.Labels) []byte {
   
	h := sha256.Sum256([]byte(labelsString(ls)))
	return encodeBase64Bytes(h[:])
}

labelsString(ls) --> logs{
   ls[0].name=ls[0]=value, ls[1].name=ls[1].value, ..., ls[n].name=ls[n].value}

// ChunkID生成方法
// ExternalKey returns the key you can use to fetch this chunk from external
// storage. For newer chunks, this key includes a checksum.
func (c *Chunk) ExternalKey() string {
   
	// Some chunks have a checksum stored in dynamodb, some do not.  We must
	// generate keys appropriately.
	if c.ChecksumSet {
   
		// This is the inverse of parseNewExternalKey.
		return fmt.Sprintf("%s/%x:%x:%x:%x", c.UserID, uint64(c.Fingerprint), int64(c.From), int64(c.Through), c.Checksum)
	}
	// This is the inverse of parseLegacyExternalKey, with "<user id>/" prepended.
	// Legacy chunks had the user ID prefix on s3/memcache, but not in DynamoDB.
	// See comment on parseExternalKey.
	return fmt.Sprintf("%s/%d:%d:%d", c.UserID, uint64(c.Fingerprint), int64(c.From), int64(c.Through))
}

Index写入

Index写入概要

Index的写入数据流如下图所示:

在这里插入图片描述

其中ingester内存的处理流程如下图所示:

没刷新一个chunk,需要写入的index如下

data = jsoniter.ConfigFastest.Marshal(labelNames)
entries := []IndexEntry{
   
		// Entry for metricName -> seriesID (数据类型1)
		{
   
			TableName:  bucket.tableName,
			HashValue:  fmt.Sprintf("%02d:%s:%s", shard, bucket.hashKey, metricName),
			RangeValue: encodeRangeKey(seriesRangeKeyV1, seriesID, nil, nil),
			Value:      empty,
		},

		// Entry for seriesID -> label names (数据类型2)
		{
   
			TableName:  bucket.tableName,
			HashValue:  string(seriesID),
			RangeValue: encodeRangeKey(labelNamesRangeKeyV1, nil, nil, nil),
			Value:      data,
		},
	}

// Entries for metricName:labelName -> hash(value):seriesID (数据类型3)
for _, v := range labels {
   
		if v.Name == model.MetricNameLabel {
   
			continue
		}
		valueHash := sha256bytes(v.Value)
		entries = append(entries, IndexEntry{
   
			TableName:  bucket.tableName,
			HashValue:  fmt.Sprintf("%02d:%s:%s:%s", shard, bucket.hashKey, metricName, v.Name),
			RangeValue: encodeRangeKey(labelSeriesRangeKeyV1, valueHash, seriesID, nil),
			Value:      []byte(v.Value),
		})
	}

// Entry for seriesID -> chunkID (数据类型4)
entries := []IndexEntry{
   
		// Entry for seriesID -> chunkID
		{
   
			TableName:  bucket.tableName,
			HashValue:  bucket.hashKey + ":" + string(seriesID),
			RangeValue: encodeRangeKey(chunkTimeRangeKeyV3, encodedThroughBytes, nil, []byte(chunkID)),
			Value:      empty,
		},
	}

上述代码中RangeValue的生成使用了encodeRangeKey,其具体实现如下

// Encode a complete key including type marker (which goes at the end)
func encodeRangeKey(keyType byte, ss ...[]byte) []byte {
   
	output := buildRangeValue(2, ss...)
	output[len(output)-2] = keyType
	return output
}

// Build an index key, encoded as multiple parts separated by a 0 byte, with extra space at the end.
func buildRangeValue(extra int, ss ...[]byte) []byte {
   
	length := extra
	for _, s := range ss {
   
		length += len(s) + 1
	}
	output, i := make([]byte, length), 0
	for _, s := range ss {
   
		i += copy(output[i:], s) + 1
	}
	return output
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值