VictorialMetrics存储原理之索引

运维 数据库运维
我们来分析下当 vmstorage 接收到数据后是如何保存监控指标的。

前文我们介绍了 VictorialMetrics 中是如何接收和传输数据的​​,接下来我们来分析下当 vmstorage 接收到数据后是如何保存监控指标的。

现在我们使用 csv 来导入一行指标数据,直接使用下面的请求即可:

curl -d "GOOG,1.23,4.56,NYSE" 'http://127.0.0.1:8480/insert/0/prometheus/api/v1/import/csv?format=2:metric:ask,3:metric:bid,1:label:ticker,4:label:market'

执行上面的请求后,在 vmstorage 组件下面会收到如下所示的一些日志信息:

图片

同时在数据目录 vmstorage-data 下面也多了一个 cache 目录,而且 data 下面的 small 目录和 indexdb 目录下面也生成了一些文件,这些文件就是用来存储指标数据的。

图片

接下来我们就来仔细分析下这些文件是干什么的,以及这些文件的存储格式是怎样的。

要想弄明白 vmstorage 是如何去存储数据的,首先我们要先弄明白几个概念。

存储格式

下图是 VictoriaMetrics 支持的 Prometheus 协议的一个写入示例。

图片

VM 在收到写入请求时,会对请求中包含的时序数据做转换处理。首先根据包含 metric 和 labels 的 MetricName 生成一个唯一标识 TSID,然后 metric(指标名称__name__) + labels + TSID 作为索引 index,TSID + timestamp + value 作为数据 data,最后索引 index 和数据 data 分别进行存储和检索。

图片

因此 VM 的数据整体上分成索引和数据两个部分,因此文件格式整体上会有两个部分,其中索引部分主要是用于支持按照 label 或者 tag 进行多维检索,数据存储时,先将数据按 TSID 进行分组,然后每个 TSID 包含的数据点各自使用列式压缩存储。

TSID

VictoriaMetrics 的 MetricName 的结构如下所示,包含 MetricGroup(指标名称 __name__) 和 Tag 数组,其中,Tags 是可选的,每个 Tag 由 Key 和 Value 等字节数组构成。

图片

为了规范,Tags 必须按标签 Key 排序,使用 sortTags 方法。

图片

VictoriaMetrics 的 TSID 的结构如下所示,包含 MetricGroupID、JobID、InstanceID、MetricID 等几个字段,其中除了 MetricID 外,其他字段都是可选的。这个几个 ID 的生成方法如下:

  • MetricGroupID​ 是根据MetricName​ 中的MetricGroup​ 使用xxhash 的 sum64 算法生成。
  • JobID​ 和InstanceID​ 分别由MetricName​ 中的第一个 tag 和第二个 tag 使用xxhash 的 sum64 算法生成。为什么使用第一个 tag 和第二个 tag?这是因为 VictoriaMetrics 在写入时,将写入请求中的 JobID 和 InstanceID 放在了 Tag 数组的第一个和第二个位置。
  • MetricID,使用 VictoriaMetrics 进程启动时的系统纳秒时间戳自增生成。
// lib/storage/tsid.go
// TSID ID
//
// TSID
//
// MetricID
//
type TSID struct {
AccountID uint32
ProjectID uint32 //

// MetricGroupIDID(AccountID, ProjectID)
//
// Metric Group memory_usagehttp_requests
// memory_usage :
//
// memory_usage{datacenter="foo1", job="bar1", instance="baz1:1234"}
// memory_usage{datacenter="foo1", job="bar1", instance="baz2:1234"}
// memory_usage{datacenter="foo1", job="bar2", instance="baz1:1234"}
// memory_usage{datacenter="foo2", job="bar1", instance="baz2:1234"}
MetricGroupID uint64
// JobID ID
//
// JobID (AccountID, ProjectID)
//
// Job
// See https://prometheus.io/docs/concepts/jobs_instances/ for details.
JobID uint32
// InstanceID ID(AccountID, ProjectID)
InstanceID uint32
// MetricID ID
//
// TSID MetricID
MetricID uint64
}

因为 TSID 中除了 MetricID 外,其他字段都是可选的,因此 TSID 中可以始终作为有效信息的只有 MetricID,因此 VictoriaMetrics 的在构建 tag 到 TSID 的字典过程中,是直接存储的 tag 到 MetricID 的字典。

以写入 http_requests_total{status="200", method="GET"} 为例,则 MetricName 为 http_requests_total{status="200", method="GET"},假设生成的 TSID 为 {metricGroupID=0, jobID=0, instanceID=0, metricID=51106185174286},则 VictoriaMetrics 在写入时就构建了如下几种类型的索引 item,其他类型的索引 item 是在后台或者查询时构建的。

  • metricName -> TSID​, 即http_requests_total{status="200", method="GET"} -> {metricGroupID=0, jobID=0, instanceID=0, metricID=51106185174286}。
  • metricID -> metricName​,即51106185174286 -> http_requests_total{status="200", method="GET"}。
  • metricID -> TSID​,即51106185174286 -> {metricGroupID=0, jobID=0, instanceID=0, metricID=51106185174286}。
  • tag -> metricID​,即status="200" -> 51106185174286​、method="GET" -> 51106185174286​、"__name__" = http_requests_total -> 51106185174286(其实还有一个联合索引)。

有了这些索引的 item 后,就可以支持基于 tag 的多维检索了,在当给定查询条件 http_requests_total{status="200"} 时,VictoriaMetrics 先根据给定的 tag 条件,找出每个 tag 的 metricID 列表,然后计算所有 tag 的 metricID 列表的交集,然后根据交集中的 metricID,再到索引文件中检索出 TSID,根据 TSID 就可以到数据文件中查询数据了,在返回结果之前,再根据 TSID 中的 metricID,到索引文件中检索出对应的写入时的原始 MetircName。

但是由于 VictoriaMetrics 的 tag 到 metricID 的字典,没有将相同 tag 的所有 metricID 放在一起存储,在检索时,一个 tag 可能需要查询多次才能得到完整的 metricID 列表。另外查询出 metricID 后,还要再到索引文件中去检索 TSID 才能去数据文件查询数据,又增加了一次 IO 开销。这样来看的话,VictoriaMetrics 的索引文件在检索时,如果命中的时间线比较多的情况下,其 IO 开销会比较大,查询延迟也会比较高。

这里我们了解了 TSID 这个非常重要的概念,还有几个结构体需要我们了解下,比如 rawRow 表示一个原始的时间序列行,MetricRow 表示插入到存储中的指标数据:

// lib/storage/raw_row.go
// rawRow
type rawRow struct {
TSID TSID // ID
Timestamp int64 //
Value float64 //
// PrecisionBits [1..64]
// 1 . 50% error, 2 - 25%, 3 - 12.5%, 64 , i.e.
//
PrecisionBits uint8
}
// libe/storage/storage.go
// MetricRow
type MetricRow struct {
// MetricNameRaw 使 metricne.UnmarshalRaw
MetricNameRaw []byte
Timestamp int64
Value float64
}

插入指标

有了上面几个概念的认识,现在我们回过头再去看下 vmstorage 中对 vminsert 请求的处理:

// app/vmstorage/transport/server.go
func (s *Server) processVMInsertConn(bc *handshake.BufferedConn) error {
return clusternative.ParseStream(bc, func(rows []storage.MetricRow) error {
vminsertMetricsRead.Add(len(rows))
return s.storage.AddRows(rows, uint8(*precisionBits))
}, s.storage.IsReadOnly)
}

当 vmstorage 节点接收到数据后,最后会通过回调执行 s.storage.AddRows(rows, uint8(*precisionBits)),该函数将数据添加到底层存储去:

// lib/storage/storage.go
// AddRows mrs s
func (s *Storage) AddRows(mrs []MetricRow, precisionBits uint8) error {
if len(mrs) == 0 {
return nil
}
// goroutine
// goroutine AddRows CPU
select {
// channel CPU
// default case
case addRowsConcurrencyCh <- struct{}{}:
default: // channel insert select
atomic.AddUint64(&s.addRowsConcurrencyLimitReached, 1)
t := timerpool.Get(addRowsTimeout) // 30stimer

//

// pacelimiter insert
// insert IncSearch
storagepacelimiter.Search.Inc()

select { //
// 30s channel
case addRowsConcurrencyCh <- struct{}{}:
timerpool.Put(t) // timer GC
// channel insert Dec
storagepacelimiter.Search.Dec()
// 0 cond.Broadcast() select
case <-t.C: // 30s
// timer GC timerpool.Put(t)
// insert
storagepacelimiter.Search.Dec()
atomic.AddUint64(&s.addRowsConcurrencyLimitTimeout, 1) //
atomic.AddUint64(&s.addRowsConcurrencyDroppedRows, uint64(len(mrs))) // mr
// 30CPU
return fmt.Errorf("cannot add %d rows to storage in %s, since it is overloaded with %d concurrent writers; add more CPUs or reduce load",
len(mrs), addRowsTimeout, cap(addRowsConcurrencyCh))
}
}

//
//
var firstErr error
ic := getMetricRowsInsertCtx()
maxBlockLen := len(ic.rrs)
for len(mrs) > 0 {
mrsBlock := mrs
// mrs
if len(mrs) > maxBlockLen {
// mrs mrsBlock = mrs[:maxBlockLen]
// mrs
mrs = mrs[maxBlockLen:]
} else {
mrs = nil
}
// add
if err := s.add(ic.rrs, ic.tmpMrs, mrsBlock, precisionBits); err != nil {
if firstErr == nil {
firstErr = err
}
continue
}
// mrs
atomic.AddUint64(&rowsAddedTotal, uint64(len(mrsBlock)))
}
//
putMetricRowsInsertCtx(ic)

<-addRowsConcurrencyCh // insert

return firstErr
}

该函数的实现非常经典,会限制可能向存储添加数据的并发 goroutine 数量,当太多的 goroutine 调用 AddRows 时,可以防止内存不足错误和 CPU 抖动。这里实现了插入比查询更高的优先级,当资源不足时,查询操作会挂起让出资源给到插入操作使用。

获取 TSID

真正实现添加数据是下面的 add 函数,其中 rawRow 是原始的时序数据行,MetricRow 是要插入到存储中的行数据,该函数的核心就是要生成指标序列的 TSID 数据,如下所示:

// lib/storage/storage.go
func (s *Storage) add(rows []rawRow, dstMrs []*MetricRow, mrs []MetricRow, precisionBits uint8) error {
// 使
idb := s.idb()
j := 0
var (
// metricName
prevTSID TSID
prevMetricNameRaw []byte
)
var pmrs *pendingMetricRows
//
minTimestamp, maxTimestamp := s.tb.getMinMaxTimestamps()

// TSID
var genTSID generationTSID

//
var firstWarn error
// rawRow TSID
for i := range mrs {
mr := &mrs[i]
if math.IsNaN(mr.Value) { // NaN
if !decimal.IsStaleNaN(mr.Value) {
// Prometheus staleness NaN
// 使
continue
}
}
//
//
if mr.Timestamp < minTimestamp {
......
continue
}
//
if mr.Timestamp > maxTimestamp {
......
continue
}
dstMrs[j] = mr
r := &rows[j]
j++
r.Timestamp = mr.Timestamp
r.Value = mr.Value
r.PrecisionBits = precisionBits
// - mr mr TSID
if string(mr.MetricNameRaw) == string(prevMetricNameRaw) {
// MetricNameRaw
r.TSID = prevTSID
continue
}
// TSID
if s.getTSIDFromCache(&genTSID, mr.MetricNameRaw) {
r.TSID = genTSID.TSID
//
if s.isSeriesCardinalityExceeded(r.TSID.MetricID, mr.MetricNameRaw) {
j--
continue
}
// - MetricNameRaw TSID
// r.TSID.MetricID tsidCache MetricName -> TSID Storage.DeleteMetrics
prevTSID = r.TSID // TSID
prevMetricNameRaw = mr.MetricNameRaw // MetricNameRaw

// TSID
if genTSID.generation != idb.generation {
// 使 TSID
// https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1401
created, err := idb.maybeCreateIndexes(&genTSID.TSID, mr.MetricNameRaw)
if err != nil {
return fmt.Errorf("cannot create indexes in the current indexdb: %w", err)
}
if created {
// TSID
genTSID.generation = idb.generation
// TSID -> MetricNameRaw 便
s.putTSIDToCache(&genTSID, mr.MetricNameRaw)
}
}
continue
}

// - TSID
//
j--
if pmrs == nil {
// pendingMetricRows
pmrs = getPendingMetricRows()
}
// mr pendingMetricRows
if err := pmrs.addRow(mr); err != nil {
// -
//
if firstWarn == nil {
firstWarn = err
}
continue
}
}
// TSID
if pmrs != nil {
// pendingMetricRows 便 is
pendingMetricRows := pmrs.pmrs
sort.Slice(pendingMetricRows, func(i, j int) bool {
return string(pendingMetricRows[i].MetricName) < string(pendingMetricRows[j].MetricName)
})
//
is := idb.getIndexSearch(0, 0, noDeadline)
prevMetricNameRaw = nil // MetricNameRaw
var slowInsertsCount uint64
for i := range pendingMetricRows {
pmr := &pendingMetricRows[i]
mr := pmr.mr // MetricRaw
dstMrs[j] = mr
r := &rows[j]
j++
r.Timestamp = mr.Timestamp
r.Value = mr.Value
r.PrecisionBits = precisionBits
// - mr mr TSID
if string(mr.MetricNameRaw) == string(prevMetricNameRaw) {
// MetricNameRaw
r.TSID = prevTSID
if s.isSeriesCardinalityExceeded(r.TSID.MetricID, mr.MetricNameRaw) {
//
j--
continue
}
continue
}
//
slowInsertsCount++ //
// MetricName TSID
if err := is.GetOrCreateTSIDByName(&r.TSID, pmr.MetricName); err != nil {
if firstWarn == nil {
firstWarn = fmt.Errorf("cannot obtain or create TSID for MetricName %q: %w", pmr.MetricName, err)
}
j--
continue
}
// genTSID TSID
genTSID.generation = idb.generation
genTSID.TSID = r.TSID
//
s.putTSIDToCache(&genTSID, mr.MetricNameRaw)
// TSID MetricNameRaw便
prevTSID = r.TSID
prevMetricNameRaw = mr.MetricNameRaw
if s.isSeriesCardinalityExceeded(r.TSID.MetricID, mr.MetricNameRaw) {
//
j--
continue
}
}
//
idb.putIndexSearch(is)
putPendingMetricRows(pmrs)
atomic.AddUint64(&s.slowRowInserts, slowInsertsCount)
}
//
if firstWarn != nil {
logger.WithThrottler("storageAddRows", 5*time.Second).Warnf("warn occurred during rows addition: %s", firstWarn)
}
dstMrs = dstMrs[:j]
rows = rows[:j]

// TSID
var firstError error
if err := s.tb.AddRows(rows); err != nil {
firstError = fmt.Errorf("cannot add rows to table: %w", err)
}
if err := s.updatePerDateData(rows, dstMrs); err != nil && firstError == nil {
firstError = fmt.Errorf("cannot update per-date data: %w", err)
}
if firstError != nil {
return fmt.Errorf("error occurred during rows addition: %w", firstError)
}
return nil
}

首先循环数据,把时间戳过小或过大的都过滤掉,然后就是想办法尽可能快地获取到指标的 TSID:

  • 快速路径- 当前 MetricRow 包含与前一 MetricRow 相同的指标名称,因此它们具有相同的 TSID,所以直接将当前对象的 TSID 设置成前一个 TSID,这是最快的方式。
  • 如果和前一个指标名称不一样,则去查看 genTSID 是否在缓存中(命中缓存)。
  • 如果命中缓存则 genTSID 中的 TSID 就是我们需要的,同时也将其设置为前一个 prevTSID。如果该 TSID 不是当代的索引(来自上一代缓存下来的索引),则需要尝试使用该 TSID 重新填充当代的索引数据,这和索引轮换有关,后面会详细说明。
  • 如果没有命中缓存,则属于慢速路径,将当前数据添加到pendingMetricRows 中去待处理。
  • 循环了所有指标数据后,接下来需要处理pendingMetricRows 中的数据,也就是缓存中没有对应的 TSID,此时就需要我们去生成对应的 TSID 数据。
  • 快速路径- 同样是当前 MetricRow 与前一个 MetricRow 的指标名称相同,因此它包含相同的 TSID,直接设置成前一个 TSID 即可。
  • 慢速路径- 走到这个分支则只能去创建 TSID 了,通过 MetricName 去获取(没有就创建)TSID 数据,也就是上面GetOrCreateTSIDByName 函数。获取后记得放到缓存中去。

上面费了很大的功夫就是为了获取时间序列对应的 TSID 数据的,这也是插入数据过程中最可能出现慢插入的地方,因为该过程涉及到索引,比较耗时间,如果你插入的数据出现大量的高基数序列(比如包含一些随机生成的 ID 作为标签),则会大大降低 vmstorage 的插入性能。

我们可以去查看下 GetOrCreateTSIDByName 函数的实现。

// lib/storage/index_db.go
// GetOrCreateTSIDByName 使 metricName TSID dst
func (is *indexSearch) GetOrCreateTSIDByName(dst *TSID, metricName []byte) error {
// hack TSID
//
if is.tsidByNameMisses < 100 {
err := is.getTSIDByMetricName(dst, metricName)
if err == nil {
is.tsidByNameMisses = 0
return nil
}
if err != io.EOF {
return fmt.Errorf("cannot search TSID by MetricName %q: %w", metricName, err)
}
is.tsidByNameMisses++
} else {
is.tsidByNameSkips++
if is.tsidByNameSkips > 10000 {
is.tsidByNameSkips = 0
is.tsidByNameMisses = 0
}
}
// TSID
// mn TSID goroutines
// TableSearch mn
if err := is.db.createTSIDByName(dst, metricName); err != nil {
return fmt.Errorf("cannot create TSID by MetricName %q: %w", metricName, err)
}
return nil
}
// metricName TSID
func (is *indexSearch) getTSIDByMetricName(dst *TSID, metricName []byte) error {
dmis := is.db.s.getDeletedMetricIDs()
ts := &is.ts // TableSearch
kb := &is.kb
kb.B = append(kb.B[:0], nsPrefixMetricNameToTSID) // MetricName -> TSID
kb.B = append(kb.B, metricName...)
kb.B = append(kb.B, kvSeparatorChar)
ts.Seek(kb.B) // Seek ts k
for ts.NextItem() { //
if !bytes.HasPrefix(ts.Item, kb.B) { // ts.Item kb.B
//
return io.EOF
}
v := ts.Item[len(kb.B):] //
tail, err := dst.Unmarshal(v) // dst
if err != nil {
return fmt.Errorf("cannot unmarshal TSID: %w", err)
}
if len(tail) > 0 { //
return fmt.Errorf("unexpected non-empty tail left after unmarshaling TSID: %X", tail)
}
if dmis.Len() > 0 { // MetricID
// dst
if dmis.Has(dst.MetricID) {
// dst
continue
}
}
// dst
return nil
}
if err := ts.Error(); err != nil {
return fmt.Errorf("error when searching TSID by metricName; searchPrefix %q: %w", kb.B, err)
}
//
return io.EOF
}

该函数会获取 metricName 对应的 TSID,但是可能会出现多次连续未命中的情况,为了提高性能,这里做了一点 hack,如果连续未查询到 TSID 100 次则跳过搜索,就只能去创建 TSID 了,如果跳过了 10000 次则又重置可以重新去搜索。

搜索 TSID 是通过下面的 getTSIDByMetricName 函数来实现的,创建 TSID 是通过 createTSIDByName 函数实现的。

TSID 的生成方法如下所示:

// lib/storage/index_db.go
// metricName TSID
func (db *indexDB) createTSIDByName(dst *TSID, metricName []byte) error {
mn := GetMetricName()
defer PutMetricName(mn)
if err := mn.Unmarshal(metricName); err != nil {
return fmt.Errorf("cannot unmarshal metricName %q: %w", metricName, err)
}
// TSID
created, err := db.getOrCreateTSID(dst, metricName, mn)
if err != nil {
return fmt.Errorf("cannot generate TSID: %w", err)
}
// TSID
if err := db.createIndexes(dst, mn); err != nil {
return fmt.Errorf("cannot create indexes: %w", err)
}
// 使 tag db tb OpenTable invalidateTagFiltersCache flushCallback
if created {
// indexDB tsid newTimeseriesCreated
atomic.AddUint64(&db.newTimeseriesCreated, 1)
if logNewSeries {
logger.Infof("new series created: %s", mn.String())
}
}
return nil
}
// getOrCreateTSID db.extDB metricName TSID
// TSID
//
// TSID true TSID extDB false
func (db *indexDB) getOrCreateTSID(dst *TSID, metricName []byte, mn *MetricName) (bool, error) {
// TSID
// db
var err error
// db TSID
if db.doExtDB(func(extDB *indexDB) {
err = extDB.getTSIDByNameNoCreate(dst, metricName)
}) {
if err == nil {
// TSID
return false, nil
}
if err != io.EOF {
return false, fmt.Errorf("external search failed: %w", err)
}
}
// TSID
generateTSID(dst, mn)
return true, nil
}
// TSID
func generateTSID(dst *TSID, mn *MetricName) {
dst.AccountID = mn.AccountID
dst.ProjectID = mn.ProjectID
// MetricName MetricGroup 使 xxhash sum64
dst.MetricGroupID = xxhash.Sum64(mn.MetricGroup)
// job-like metric mn.Tags[0] instance-like metric mn.Tags[1]
// mn.Tags generateTSID() 使 mn.sortTags()
// jobinstance
// job / instance IO
// `process_resident_memory_bytes{job="vmstorage"}`
if len(mn.Tags) > 0 {
dst.JobID = uint32(xxhash.Sum64(mn.Tags[0].Value)) // Tag JobID
}
if len(mn.Tags) > 1 {
dst.InstanceID = uint32(xxhash.Sum64(mn.Tags[1].Value)) // Tag InstanceID
}
dst.MetricID = generateUniqueMetricID() // ID
}

MetricID 通过 generateUniqueMetricID() 生成, 在重启时, nextUniqueMetricID 被赋值为当时的时间戳, 随后每次新的 TSID 的创建都会在此基础之上+1。

// lib/storage/index_db.go
// MetricID
func generateUniqueMetricID() uint64 {
// metricID
// metric_ids uint64set.Set
return atomic.AddUint64(&nextUniqueMetricID, 1)
}
// 退 metricID
// VictoriaMetrics
var nextUniqueMetricID = uint64(time.Now().UnixNano())

但是我们可能在这里看不懂 TSID 是如何去搜索或者创建的,这就需要我们去了解下 VM 中的倒排索引了。

倒排索引

当创建完 TSID 后, 需要建立一系列的索引供查找时使用。在 VM 中不同类型的索引都是通过 KV 关系来描述,在代码中称为 Item , Item 的结构如下:

图片

在 VM 中 Item 的整体上是一个 KV 结构的字节数组,共计有 7 种类型,每种类型的 Item 通过固定前缀来区分,前缀类型如下图所示。

图片

在 storage/index_db.go: createIndexes 函数中,去分别建立了各个索引,生成 Items,代码如下所示:

// lib/storage/index_db.go
//
func (db *indexDB) createIndexes(tsid *TSID, mn *MetricName) error {
// items
ii := getIndexItems()
defer putIndexItems(ii)
// MetricName -> TSID
ii.B = append(ii.B, nsPrefixMetricNameToTSID) //
ii.B = mn.Marshal(ii.B)
ii.B = append(ii.B, kvSeparatorChar) //
ii.B = tsid.Marshal(ii.B)
ii.Next()
// MetricID -> MetricName
ii.B = marshalCommonPrefix(ii.B, nsPrefixMetricIDToMetricName, mn.AccountID, mn.ProjectID)
ii.B = encoding.MarshalUint64(ii.B, tsid.MetricID)
ii.B = mn.Marshal(ii.B)
ii.Next()
// MetricID -> TSID
ii.B = marshalCommonPrefix(ii.B, nsPrefixMetricIDToTSID, mn.AccountID, mn.ProjectID)
ii.B = encoding.MarshalUint64(ii.B, tsid.MetricID)
ii.B = tsid.Marshal(ii.B)
ii.Next()
// Tag -> MetricID
prefix := kbPool.Get()
prefix.B = marshalCommonPrefix(prefix.B[:0], nsPrefixTagToMetricIDs, mn.AccountID, mn.ProjectID)
ii.registerTagIndexes(prefix.B, mn, tsid.MetricID)
kbPool.Put(prefix)
// Items Table
return db.tb.AddItems(ii.Items)
}

对于 ask{market="NYSE",ticker="GOOG"} 1.23 的时序指标,对应的 MetricName 为 AccountID=0, ProjectID=0, ask{market="NYSE",ticker="GOOG"},假设生成的 TSID 为:

{
AccountID: 0
ProjectID: 0
MetricGroupID: 6661248876682682060
JobID: 3817370224
InstanceID: 4166188337
MetricID: 1654132102944898001
}

则生成的索引 Item 逻辑结构如下图所示:

图片

上图为构建的 MetricName -> TSID 的索引,前缀为 nsPrefixMetricNameToTSID=0,整个索引项就是一个 key: value 的形式,key 为 MetricName 编码后的值,value 为 TSID 编码后的值,中间通过一个 kvSeparator 的分隔符进行连接,当然这些值真正的存储形式都是 []byte。除了上图的这个索引之外还有几个其他的索引:MetricID -> MetricName、MetricID -> TSID、Tag -> MetricID,方式都是一样的,只是要注意每种索引的前缀是不一样的。最后得到的索引就是上面构建的几种索引的集合数组。

索引构建完成后又是如何去持久化数据的呢?保存的数据又是怎样的格式呢?

责任编辑:姜华 来源: k8s技术圈
相关推荐

2022-06-08 07:34:02

持久化数据存储原理索引存储格式

2022-05-30 07:36:54

vmstoragevmselect

2019-06-03 15:15:09

MySQL索引数据库

2018-07-27 10:39:13

对象存储Git

2015-10-30 15:55:43

MySQL

2021-04-26 09:05:55

高并发索引MySQL

2018-11-13 09:49:11

存储云存储云备份

2021-01-12 14:46:34

Kubernetes开发存储

2021-09-13 14:06:03

SQL 数据库多元索引

2021-09-09 14:11:58

表格存储 SQL

2017-08-07 08:15:31

搜索引擎倒排

2020-03-20 10:14:49

搜索引擎倒排索引

2021-11-01 23:57:03

数据库哈希索引

2023-10-21 21:13:00

索引SQL工具

2010-04-21 16:07:04

Oracle逻辑存储结

2021-08-26 11:52:32

FeignWeb服务

2011-03-21 15:00:13

LAMPMySQL

2010-07-14 15:04:53

SQL Sever索引

2021-11-30 21:10:19

数据库B树索引

2022-10-27 16:07:24

littlefs存储结构
点赞
收藏

51CTO技术栈公众号