导言
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
}