influxdb源码解析-series

前言

   ~~   这是一个分析inlfuxdb源码的系列。在此之前,已经分析了数据的基本模型,以及写入流程。在上一章,分析了数据写入的一些细节,例如解析point的逻辑,元数据校验和更新等。在元数据的更新和校验时,有个方法是validateSeriesAndFields,这个方法会校验当前写入的点,并且把新的series创建出来。不过我们没有自信的分析创建的过程,因为涉及到sereis模块,只是一笔带过。这一章就专门来分析series。

提示

还是温馨提示,这部分内容其实算是元数据部分。如果不了解什么叫做series,那么回到概念详解部分,看看series的解答。series在influxdb官方的文档中被描述为measurement+tags+field的组合,其实在代码实现的过程中,series是measurement+tags,没有考虑到field。

Series的基本结构

    ~~~    series信息,被保存在了seriesFile结构,seriesFile又分为多个partition,每个partition叫做SeriesPartition。SeriesPartition是Series的一个逻辑上的分桶,用来避免全局锁。减小竞争。SeriesPartition下面分为多个SeriesSegment,每个SeriesSegment就是一个保存了Series信息的文件。使用mmap映射到内存里面。
    ~~~    这里的结构有点像kafka 的设计。具体的逻辑如下。那么接下来分别分析一下这些结构,从最简单的开始。
在这里插入图片描述

SeriesSegment

上面提到了,seriesSegment对应的就是一个文件。

// SeriesSegment represents a log of series entries.
type SeriesSegment struct {
	// 映射一个磁盘文件
	id   uint16
	path string

	data []byte        // mmap file
	file *os.File      // write file handle
	w    *bufio.Writer // bufferred file handle
	size uint32        // current file size
}

从注释里可以看出来,SeriesSegment是series entry log的集合。这些文件,路径是:
在这里插入图片描述
其中00-07代表seriesPartition,0000代表一个seriesSegment。seriesSegment保存了一条条的serie log,这个和WAL的设计类似。

CreateSeriesSegment

创建一个SeriesSegment的逻辑在influxdb/tsdb/series_segment.go

func CreateSeriesSegment(id uint16, path string) (*SeriesSegment, error) {
	f, err := os.Create(path + ".initializing")
	if err != nil {
		return nil, err
	}
	defer f.Close()
	// Write header to file and close.
	hdr := NewSeriesSegmentHeader()
	if _, err := hdr.WriteTo(f); err != nil {
		return nil, err
	} else if err := f.Truncate(int64(SeriesSegmentSize(id))); err != nil {
		return nil, err
	} else if err := f.Sync(); err != nil {
		return nil, err
	} else if err := f.Close(); err != nil {
		return nil, err
	}
	// Swap with target path.
	if err := os.Rename(f.Name(), path); err != nil {
		return nil, err
	}

	// Open segment at new location.
	segment := NewSeriesSegment(id, path)
	if err := segment.Open(); err != nil {
		return nil, err
	}
	return segment, nil
}

逻辑闭件简单,首先是创建文件,然后写入Header信息,这里有个函数Truncate,可能有些人不熟悉,这个函数的作用是把文件大小resize一下,但是不会改变文件指针的位置。这里其实是一个比较重要的逻辑:

func SeriesSegmentSize(id uint16) uint32 {
	const min = 22 // 4MB
	const max = 28 // 256MB

	shift := id + min
	if shift >= max {
		shift = max
	}
	return 1 << shift
}

这里可以看出来,series的文件大小是和id有关系的,最小是4MB,最大是28MB。在我们上面展示的series文件列表中,也可以看到,确实是4MB大小。新建之后,会调用series的Open函数。

// Open memory maps the data file at the file's path.
func (s *SeriesSegment) Open() error {
	if err := func() (err error) {
		// Memory map file data.
		if s.data, err = mmap.Map(s.path, int64(SeriesSegmentSize(s.id))); err != nil {
			return err
		}

		// Read header.
		hdr, err := ReadSeriesSegmentHeader(s.data)
		if err != nil {
			return err
		} else if hdr.Version != SeriesSegmentVersion {
			return ErrInvalidSeriesSegmentVersion
		}

		return nil
	}(); err != nil {
		s.Close()
		return err
	}

	return nil
}

这块逻辑也比较简单,其实就是把data做个mmap映射,把数据映射到内存里。

offset

本来接下来就该分析怎么写入一个series log entry,但是这里先介绍另外一个概念:offset.
在这里插入图片描述
一个seriesSegment对应的文件如上所示,每一个series entry都代表了一个series。为了寻找他们,就需要知道他们在文件中的偏移,例如series entry 2在文件的偏移等于series entry 1的大小+1。但是offset就指的是series在文件中的偏移嘛?其实不全是,因为series文件可能有多个,每个series 文件的嘛名字都是这样生成的:

	var id uint16
	if len(p.segments) > 0 {
		id = p.segments[len(p.segments)-1].ID() + 1
	}
	filename := fmt.Sprintf("%04x", id)

也就是下一个series 文件的名字是0001,接着是0002…,所以offset还要带有文件名字的信息。
为了达到这个目的,offset采用如下方式生成:

func JoinSeriesOffset(segmentID uint16, pos uint32) int64 {
	return (int64(segmentID) << 32) | int64(pos)
}

使用高32位存储文件名字,也就是segmentId,第32位存储文件偏移量。这样给定一个offset,就能很快找到这个offset对应的series entry。
    ~~~    明白了这些,那么接下来看看写入的流程。

func (s *SeriesSegment) WriteLogEntry(data []byte) (offset int64, err error) {
	if !s.CanWrite(data) {
		return 0, ErrSeriesSegmentNotWritable
	}
	offset = JoinSeriesOffset(s.id, s.size)
	if _, err := s.w.Write(data); err != nil {
		return 0, err
	}
	s.size += uint32(len(data))

	return offset, nil
}

这里的写入就很清晰了,计算offset,然后写入到文件中。

两个flag

在series_segment开始部分,有一些常量,其中有两个Flag标记。

	SeriesEntryInsertFlag    = 0x01
	SeriesEntryTombstoneFlag = 0x02

第一个叫做InsertFlag,第二个是TombstoneFlag,这两个flag干啥用的呢?
    ~~~    众所周知,influxdb是支持删除一个series的,但是在LSM tree base的存储中,真实删除都是在compact的过程总完成,前期只是插入一个删除标记。influxdb 对series删除的支持,也是这样做的。当要删除一个series时,并不是直接立刻删除,而是插入删除标记,等到做compact再删除。这两个flag就是标记当前series是不是被删除。

compactToPath

这个函数从注释上就能看出来,是为了remove掉被删除的series。核心逻辑在forEach里面,如果被删除了,那么久跳过,否则重新append到里面。

// CompactToPath rewrites the segment to a new file and removes tombstoned entries.
func (s *SeriesSegment) CompactToPath(path string, index *SeriesIndex) error {
	dst, err := CreateSeriesSegment(s.id, path)
	if err != nil {
		return err
	}
	defer dst.Close()

	if err = dst.InitForWrite(); err != nil {
		return err
	}

	// Iterate through the segment and write any entries to a new segment
	// that exist in the index.
	var buf []byte
	if err = s.ForEachEntry(func(flag uint8, id uint64, _ int64, key []byte) error {
		if index.IsDeleted(id) {
			return nil // series id has been deleted from index
		} else if flag == SeriesEntryTombstoneFlag {
			return fmt.Errorf("[series id %d]: tombstone entry but exists in index", id)
		}

		// copy entry over to new segment
		buf = AppendSeriesEntry(buf[:0], flag, id, key)
		if _, err := dst.WriteLogEntry(buf); err != nil {
			return err
		}
		return err
	}); err != nil {
		return err
	}

	// Close the segment and truncate it to its maximum size.
	size := dst.size
	if err := dst.Close(); err != nil {
		return err
	} else if err := os.Truncate(dst.path, int64(size)); err != nil {
		return err
	}
	return nil
}

这段代码出现了一个新的东西,seriesIndex,是用来索引series内部的相关信息的。这里要搞清楚,因为后面还有个index,是用来索引其他模块和series信息的。compact之后,才会真正删除掉被标记位tomb的series。

ReadSeriesEntry和AppendSeriesEntry

这两个函数可以理解为读取series和写入series,但是这里却不是seriesSegment提供的能力。这两个函数属于是一层包装函数,因为series log是有格式的。但是seriesSegment不感知这series entry log的格式,它只关心最后的数据,也就是一个byte 数组。所以这两个函数的作用就相当于是序列化和反序列化操作
    ~~~    既然是序列化和反序列化,那么肯定就要有协议。也就是serie log entry的格式。这个格式从AppendSeriesEntry也能窥得一二。

func AppendSeriesEntry(dst []byte, flag uint8, id uint64, key []byte) []byte {
	buf := make([]byte, 8)
	binary.BigEndian.PutUint64(buf, id)

	dst = append(dst, flag)
	dst = append(dst, buf...)

	switch flag {
	case SeriesEntryInsertFlag:
		dst = append(dst, key...)
	case SeriesEntryTombstoneFlag:
	default:
		panic(fmt.Sprintf("unreachable: invalid flag: %d", flag))
	}
	return dst
}

注意看dst的append逻辑,dst首先append了flag,这是个uint8,然后是append了buf,最后是append了key。所以series log entry的格式就是:
在这里插入图片描述
key的格式就不再讲了,key=measurement+tags这些信息在key里面是依次平铺的。在之前的代码中也能看到。Read和Append的相反的操作,就不再赘述。
    ~~~    到这里seriesSegment的基本逻辑算是结束了,接下来是seriesPartition。

SeriesPartition

SeriesPartition代表了一组serieSegment的集合。具体结构如下:

// SeriesPartition represents a subset of series file data.
type SeriesPartition struct {
	mu   sync.RWMutex
	wg   sync.WaitGroup
	id   int
	path string

	closed  bool
	closing chan struct{}
	once    sync.Once

	segments []*SeriesSegment
	index    *SeriesIndex
	seq      uint64 // series id sequence

	compacting          bool
	compactionLimiter   limiter.Fixed
	compactionsDisabled int

	CompactThreshold int

	Logger *zap.Logger
}

从这个结构中大致可以看到三个重要信息:

  1. SeriesPartition包含了一组SeriesSegment
  2. SeriesPartition使用了SeriesIndex,来索引这个partition下面的所有seriesSegment信息。
  3. SeriesPartition会控制SeriesIndex的compact

除此之外,SeriesPartition还有一个重要的成员:seq。这个字段是用来生成当前SeriesPartition下面所有Series的SeriesId的。至于怎么生成的,后面会分析。

New和Open

首先还是老规矩,看看SeriesPartition的NewSeriesPartition和Open函数。New很简洁,就是一些基本字段的赋值。

func NewSeriesPartition(id int, path string, compactionLimiter limiter.Fixed) *SeriesPartition {
	return &SeriesPartition{
		id:                id,
		path:              path,
		closing:           make(chan struct{}),
		compactionLimiter: compactionLimiter,
		CompactThreshold:  DefaultSeriesPartitionCompactThreshold,
		Logger:            zap.NewNop(),
		seq:               uint64(id) + 1,
	}
}

这里有个细节就是,上面提到了seq,可以发现,在初始化SeriesPartition是,seq=SeriesPartitionId+1.然后是Open函数

// Open memory maps the data file at the partition's path.
func (p *SeriesPartition) Open() error {
	if p.closed {
		return errors.New("tsdb: cannot reopen series partition")
	}

	// Create path if it doesn't exist.
	if err := os.MkdirAll(filepath.Join(p.path), 0777); err != nil {
		return err
	}

	// Open components.
	if err := func() (err error) {
		if err := p.openSegments(); err != nil {
			return err
		}

		// Init last segment for writes.
		if err := p.activeSegment().InitForWrite(); err != nil {
			return err
		}

		p.index = NewSeriesIndex(p.IndexPath())
		// index主要是keyid和idoffset
		if err := p.index.Open(); err != nil {
			return err
			// rebuild
		} else if p.index.Recover(p.segments); err != nil {
			return err
		}

		return nil
	}(); err != nil {
		p.Close()
		return err
	}
	return nil
}

Open函数主要干了三件事情:

  1. open当钱SeriesPartition下所有的SeriesSegment
  2. 选中当前Active的SeriesSegment,并且初始化
  3. 恢复Index.
openSegments

这是SeriesPartitionOpen做的第一件事情,之所以拿出来说,是因为这里面也有一个特殊的逻辑;

func (p *SeriesPartition) openSegments() error {
     //........ 开启所有Segment
	// Find max series id by searching segments in reverse order.
	for i := len(p.segments) - 1; i >= 0; i-- {
		if seq := p.segments[i].MaxSeriesID(); seq >= p.seq {
			// Reset our sequence num to the next one to assign
			p.seq = seq + SeriesFilePartitionN
			break
		}
	}
	// ....... 省略

这里前后省略了,只看中间部分。这里从后往前遍历所有的segment,然后更新seq。这里有个注释:Find max series id by searching segments in reverse order. 说明了series id的生成规则:seriesId=seq+SeriesFilePartitionN。举个例子,
在这里插入图片描述
因为SeriesFilePartitionN 默认数量是8个,所以每个partition中对seriesid的生成都是隔离的,这样避免了锁竞争。同时给定一个seriesId,也能很快的找出来他位于那个partition

    ~~~    open结束之后,SeriesPartition的信息基本都被建立起来了。为了逻辑更加的清晰,接下来会把目光转到对SeriesIndex的介绍。因为后面的操作都是和这个有一定的关系。

SeriesIndex

seriesIndex在上面简单的提了一下,是SeriesPartition为了索引SeriesSegment所保存的信息。看一下具体的结构:

// SeriesIndex represents an index of key-to-id & id-to-offset mappings.
type SeriesIndex struct {
	path string

	count    uint64
	capacity int64
	mask     int64

	maxSeriesID uint64
	maxOffset   int64

	data         []byte // mmap data
	keyIDData    []byte // key/id mmap data
	idOffsetData []byte // id/offset mmap data

	// In-memory data since rebuild.
	keyIDMap    *rhh.HashMap
	idOffsetMap map[uint64]int64
	tombstones  map[uint64]struct{}
}

这里有三个重要的结构:

  1. KeyIdMap
  2. IdOffsetMap
  3. tombstones

这三个结构是最重要的数据。KeyIdMap保存了seriesKey-> seriesId的映射。seriesId是怎么生成的,上一面已经提到。IdOffsetMap保存了seriesId-> offset的映射。offset具体是啥,上面也已经提到。最后是tombstones,保存了所有被删除的series信息。除此之外,Index还保存了KeyId,idOffset等信息。

New和Open

也是先看看这个结构的New和Open函数。

func NewSeriesIndex(path string) *SeriesIndex {
	return &SeriesIndex{
		path: path,
	}
}

// Open memory-maps the index file.
func (idx *SeriesIndex) Open() (err error) {
	// Map data file, if it exists.
	if err := func() error {
		if _, err := os.Stat(idx.path); err != nil && !os.IsNotExist(err) {
			return err
		} else if err == nil {
			if idx.data, err = mmap.Map(idx.path, 0); err != nil {
				return err
			}

			hdr, err := ReadSeriesIndexHeader(idx.data)
			if err != nil {
				return err
			}
			idx.count, idx.capacity, idx.mask = hdr.Count, hdr.Capacity, hdr.Capacity-1
			idx.maxSeriesID, idx.maxOffset = hdr.MaxSeriesID, hdr.MaxOffset

			idx.keyIDData = idx.data[hdr.KeyIDMap.Offset : hdr.KeyIDMap.Offset+hdr.KeyIDMap.Size]
			idx.idOffsetData = idx.data[hdr.IDOffsetMap.Offset : hdr.IDOffsetMap.Offset+hdr.IDOffsetMap.Size]
		}
		return nil
	}(); err != nil {
		idx.Close()
		return err
	}

	idx.keyIDMap = rhh.NewHashMap(rhh.DefaultOptions)
	idx.idOffsetMap = make(map[uint64]int64)
	idx.tombstones = make(map[uint64]struct{})
	return nil
}

New基本没有任何逻辑,只对index路径赋值。Open尝试读取文件中的数据,并且保存到对应的字段中。这里没啥特别之处。

Recover

接下来是从segment中恢复数据。

// Recover rebuilds the in-memory index for all new entries.
func (idx *SeriesIndex) Recover(segments []*SeriesSegment) error {
	// Allocate new in-memory maps.
	idx.keyIDMap = rhh.NewHashMap(rhh.DefaultOptions)
	idx.idOffsetMap = make(map[uint64]int64)
	idx.tombstones = make(map[uint64]struct{})

	// Process all entries since the maximum offset in the on-disk index.
	minSegmentID, _ := SplitSeriesOffset(idx.maxOffset)
	for _, segment := range segments {
		if segment.ID() < minSegmentID {
			continue
		}

		if err := segment.ForEachEntry(func(flag uint8, id uint64, offset int64, key []byte) error {
			if offset <= idx.maxOffset {
				return nil
			}
			idx.execEntry(flag, id, offset, key)
			return nil
		}); err != nil {
			return err
		}
	}
	return nil
}

这里的逻辑有点意思,只恢复了部分segment数据,也就是maxOffset对应的segment以及之后的数据,才会在recover里面被重建索引。重建的过程就是遍历所有series,然后插入到index中。

func (idx *SeriesIndex) execEntry(flag uint8, id uint64, offset int64, key []byte) {
	switch flag {
	case SeriesEntryInsertFlag:
		idx.keyIDMap.Put(key, id)
		idx.idOffsetMap[id] = offset

		if id > idx.maxSeriesID {
			idx.maxSeriesID = id
		}
		if offset > idx.maxOffset {
			idx.maxOffset = offset
		}

	case SeriesEntryTombstoneFlag:
		idx.tombstones[id] = struct{}{}

	default:
		panic("unreachable")
	}
}

这里同时也会动态的更新maxOffset和maxSeriesID。这里恢复部分索引应该是基于内存的考虑。重建所有索引内存消耗挺大。
    ~~~    SeriesIndex提供了一些查找函数:

  • FindIDByNameTags:这个函数通过给定的measurement+tags查询SeriesId
  • FindIDListByNameTagsFindIDByNameTags传数组版本
  • FindOffsetByID 根据seriesId寻找offset

上述提到了,index里面只有部分数据保存到了内存里面,但是还有部分其实没有在内存。如果cache miss,就会计算offset,到文件中寻找。

SeriesFile

上面中断了一下分析了一下seriesIndex结构,seriesIndex结构总结一下就是,对当前SeriesPartition 比较活跃的几个SeriesSegment做了一下内存索引,并且保存了当前所有SeriesSegment的SeriesId信息和Offset信息,这部分是被保存在磁盘上的。那么接下来分析最顶层的结构:SeriesFile

// SeriesFile represents the section of the index that holds series data.
type SeriesFile struct {
	path       string
	partitions []*SeriesPartition

	maxSnapshotConcurrency int

	refs sync.RWMutex // RWMutex to track references to the SeriesFile that are in use.

	Logger *zap.Logger
}

从注释里面可以看出来,**seriesfile代表了index hold的series相关的data。**注意这里的index指的是influxdb的index模块,并不是SeriesIndex。从这个结构可以看到两个基本信息:

  1. SeriesFile有多个SeriesPartition
  2. 支持snapshot,并且控制snapshot的并发度。

New和Open

也是一下New和Open函数。

// NewSeriesFile returns a new instance of SeriesFile.
func NewSeriesFile(path string) *SeriesFile {
	maxSnapshotConcurrency := runtime.GOMAXPROCS(0)
	if maxSnapshotConcurrency < 1 {
		maxSnapshotConcurrency = 1
	}

	return &SeriesFile{
		path:                   path,
		maxSnapshotConcurrency: maxSnapshotConcurrency,
		Logger:                 zap.NewNop(),
	}
}
// Open memory maps the data file at the file's path.
func (f *SeriesFile) Open() error {
	// Wait for all references to be released and prevent new ones from being acquired.
	f.refs.Lock()
	defer f.refs.Unlock()

	// Create path if it doesn't exist.
	if err := os.MkdirAll(filepath.Join(f.path), 0777); err != nil {
		return err
	}

	// Limit concurrent series file compactions
	compactionLimiter := limiter.NewFixed(f.maxSnapshotConcurrency)

	// Open partitions.
	f.partitions = make([]*SeriesPartition, 0, SeriesFilePartitionN)
	for i := 0; i < SeriesFilePartitionN; i++ {
		p := NewSeriesPartition(i, f.SeriesPartitionPath(i), compactionLimiter)
		p.Logger = f.Logger.With(zap.Int("partition", p.ID()))
		if err := p.Open(); err != nil {
			f.Logger.Error("Unable to open series file",
				zap.String("path", f.path),
				zap.Int("partition", p.ID()),
				zap.Error(err))
			f.close()
			return err
		}
		f.partitions = append(f.partitions, p)
	}

	return nil
}

这里的逻辑比较简单,并发度是和当前机器环境相关的,Open就是Open所有的seriesPartition。不再赘述。接下来看一下非常重要的函数:CreateSeriesListIfNotExists

CreateSeriesListIfNotExists

这个函数做所以被单独拿出来,读过上一章的朋友们可能知道,这是我们留下的一个疑问,其实这个函数也是贯穿上述结构的根本所在。
首先看这个函数在SeriesFile中的实现:

func (f *SeriesFile) CreateSeriesListIfNotExists(names [][]byte, tagsSlice []models.Tags) ([]uint64, error) {
	keys := GenerateSeriesKeys(names, tagsSlice)
	keyPartitionIDs := f.SeriesKeysPartitionIDs(keys)
	ids := make([]uint64, len(keys))

	var g errgroup.Group
	for i := range f.partitions {
		p := f.partitions[i]
		g.Go(func() error {
			return p.CreateSeriesListIfNotExists(keys, keyPartitionIDs, ids)
		})
	}
	if err := g.Wait(); err != nil {
		return nil, err
	}
	return ids, nil
}

比较简介,分为三步:

  1. 生成seriesKey
  2. 生成seriesKey-> seriesPartition的映射
  3. 调用SeriesPartition的CreateSeriesListIfNotExists函数

第一步就跳过了,比较简单。我们来看第二步:

// SeriesKeysPartitionIDs 选择具体的partition
func (f *SeriesFile) SeriesKeysPartitionIDs(keys [][]byte) []int {
	partitionIDs := make([]int, len(keys))
	for i := range keys {
		partitionIDs[i] = f.SeriesKeyPartitionID(keys[i])
	}
	return partitionIDs
}

func (f *SeriesFile) SeriesKeyPartitionID(key []byte) int {
	// 给每个series选取partition的逻辑
	return int(xxhash.Sum64(key) % SeriesFilePartitionN)
}

func (f *SeriesFile) SeriesKeyPartition(key []byte) *SeriesPartition {
	partitionID := f.SeriesKeyPartitionID(key)
	if partitionID >= len(f.partitions) {
		return nil
	}
	return f.partitions[partitionID]
}

第二步有三个函数,总结一下就是通过key,使用xxhash生成hash值,对SeriesFilePartitionN取模得到seriesPartition的下标。 最后返回一个数组,partitionIDs[i]表示第i个key应该写到哪个SeriesPartition。

有了第二步的信息,就开始调用SeriesPartition的CreateSeriesListIfNotExists进行写入。

func (p *SeriesPartition) CreateSeriesListIfNotExists(keys [][]byte, keyPartitionIDs []int, ids []uint64) error {
}

这个函数的签名如上所示,key就是生成的key数组,keyPartitionIDs是生成的partition分组信息,ids是需要返回的参数,ids[i]表示第i个key的seriesId。函数开始部分,首先从index里面检索,把已经存在的series key 的id找出来并且赋值:

	var writeRequired bool
	p.mu.RLock()
	if p.closed {
		p.mu.RUnlock()
		return ErrSeriesPartitionClosed
	}
	for i := range keys {
		if keyPartitionIDs[i] != p.id {
			continue
		}
		id := p.index.FindIDBySeriesKey(p.segments, keys[i])
		if id == 0 {
			writeRequired = true
			continue
		}
		ids[i] = id
	}
	p.mu.RUnlock()

接下来遍历所有的key,对于没有ids[i]没有被赋值的位置,尝试写入series:

	// Track offsets of duplicate series.
	newIDs := make(map[string]uint64, len(ids))

	for i := range keys {
		// Skip series that don't belong to the partition or have already been created.
		if keyPartitionIDs[i] != p.id || ids[i] != 0 {
			continue
		}

		// Re-attempt lookup under write lock.
		key := keys[i]
		if ids[i] = newIDs[string(key)]; ids[i] != 0 {
			continue
		} else if ids[i] = p.index.FindIDBySeriesKey(p.segments, key); ids[i] != 0 {
			continue
		}

		// Write to series log and save offset.
		id, offset, err := p.insert(key)
		if err != nil {
			return err
		}
		// Append new key to be added to hash map after flush.
		ids[i] = id
		newIDs[string(key)] = id
		newKeyRanges = append(newKeyRanges, keyRange{id, offset})
	}

并且保存seirsId和offset。这里深入一下,看一下insert逻辑。

func (p *SeriesPartition) insert(key []byte) (id uint64, offset int64, err error) {
	id = p.seq
	offset, err = p.writeLogEntry(AppendSeriesEntry(nil, SeriesEntryInsertFlag, id, key))
	if err != nil {
		return 0, 0, err
	}

	p.seq += SeriesFilePartitionN
	return id, offset, nil
}

这里可以看出来id生成规则:seq+=SeriesFilePartitionN,这个上面其实已经提到过。这里写入或写到active的segment。最后是刷盘和index的更新,保证信息不丢

// Flush active segment writes so we can access data in mmap.
	if segment := p.activeSegment(); segment != nil {
		if err := segment.Flush(); err != nil {
			return err
		}
	}

	// Add keys to hash map(s).
	for _, keyRange := range newKeyRanges {
		p.index.Insert(p.seriesKeyByOffset(keyRange.offset), keyRange.id, keyRange.offset)
	}

到这里series算是新建完成。其实后面还有一部分逻辑,就是出发index的compact:

	if p.compactionsEnabled() && !p.compacting &&
		p.CompactThreshold != 0 && p.index.InMemCount() >= uint64(p.CompactThreshold) &&
		p.compactionLimiter.TryTake() {
		p.compacting = true
		log, logEnd := logger.NewOperation(p.Logger, "Series partition compaction", "series_partition_compaction", zap.String("path", p.path))

		p.wg.Add(1)
		go func() {
			defer p.wg.Done()
			defer p.compactionLimiter.Release()

			compactor := NewSeriesPartitionCompactor()
			compactor.cancel = p.closing
			if err := compactor.Compact(p); err != nil {
				log.Error("series partition compaction failed", zap.Error(err))
			}

			logEnd()

			// Clear compaction flag.
			p.mu.Lock()
			p.compacting = false
			p.mu.Unlock()
		}()
	}

可以看到compact的逻辑,注意,这里的compact是SeriesIndex的compact,这个compact的作用是把内存中的数据刷一部分到磁盘上去,因为内存中的index数量是有上限的。上面也提到了SeriesSegment的compact,和这里的index的compact不是一个。不要混淆。
    ~~~    介绍完CreateSeriesListIfNotExists 其实series基本就差不多了,这篇文章也挺长了,行文至此,准备结束。

总结

本篇文章介绍了influxdb series模块的存储和结构。可以看到里面还是有大量的值得学习和借鉴的细节。还有部分比较细节的内容留在下一章介绍,下一章准备series Index compact过程,以及index模块中,是怎么使用series模块的。

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值