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'
  • 1.

执行上面的请求后,在 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
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.

因为 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  
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

插入指标

有了上面几个概念的认识,现在我们回过头再去看下 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)  
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

当 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  
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.

该函数的实现非常经典,会限制可能向存储添加数据的并发 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  
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.

首先循环数据,把时间戳过小或过大的都过滤掉,然后就是想办法尽可能快地获取到指标的 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  
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.

该函数会获取 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
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.

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())
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

但是我们可能在这里看不懂 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)
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.

对于 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
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

则生成的索引 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开发存储

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工具

2021-09-13 14:06:03

SQL 数据库多元索引

2021-09-09 14:11:58

表格存储 SQL

2010-04-21 16:07:04

Oracle逻辑存储结

2021-08-26 11:52:32

FeignWeb服务

2011-03-21 15:00:13

LAMPMySQL

2021-11-30 21:10:19

数据库B树索引

2010-07-14 15:04:53

SQL Sever索引

2023-05-15 15:44:02

JavaScript数值存储
点赞
收藏

51CTO技术栈公众号