Influxdb源码分析-TSM Engine WAL

前言

这是一个influxdb源码分析系列的文章,上一章分析了Shard结构,主要是Engine。并且着重分析了Engine的Cache实现,结尾部分看到了CacheLoader使用WAL来恢复内存中的Cache。那么本篇文章就来分析一下Engine的WAL部分

WAL 的定位

WAL是(Write Ahead Log)的缩写,翻译过来叫做预写日志,是一种保证数据可靠性的方案。一般写入数据时,会先写入WAL,然后再更新本地的数据。当数据被持久化到真正用于存储它的文件上(SSTable),WAL就会被删除。

基本结构

TSM Engine的WAL位于tsdb/tsm1/wal.go。结构如下(有简化):

// WAL represents the write-ahead log used for writing TSM files.
type WAL struct {
    syncCount   uint64
    syncWaiters chan chan error

    mu            sync.RWMutex
    lastWriteTime time.Time

    path string

    currentSegmentID     int
    currentSegmentWriter *WALSegmentWriter

    once    sync.Once
    closing chan struct{}

    syncDelay time.Duration
  
    SegmentSize int

    stats   *WALStatistics
    limiter limiter.Fixed
}

从这个结构可以看出以下信息:

  • 基本信息,path等
  • currentSegmentID和currentSegmentWriter等对应的文件信息
  • 一些限制信息

其中currentSegmentID是wal 文件名字的一部分,是一个递增的id。用来区分wal的新旧程度。这里是很重要的,等会会解释。currentSegmentWriter是一个WALSegmentWriter,用来写文件的。

WAL Open

看一下核心的函数,首先是Open函数。在看这个函数之前,先看另外一个:查找当前目录下所有的wal文件


// segmentFileNames will return all files that are WAL segment files in sorted order by ascending ID.
func segmentFileNames(dir string) ([]string, error) {
	names, err := filepath.Glob(filepath.Join(dir, fmt.Sprintf("%s*.%s", WALFilePrefix, WALFileExtension)))
	if err != nil {
		return nil, err
	}
	sort.Strings(names)
	return names, nil
}

这里会返回一个数组,代表所有的wal 文件。格式为:_xxxx.wal,xxxx是id。注意这里返回的时候是做了排序的,字典序小的在前面,大的在后面。这里就是上面说的区分新旧程度。为什么这样做呢?这里要和时间线保持一致,比如现在对于同一个series有写入和删除,在wal里面就会存在两个entry,第一个是insert,第二个是带有delete标记的insert这样在读取wal的时候,最终的结果是后面的覆盖了前面的,数据被标记位delete

​ 然后是Open函数:

   func (l *WAL) Open() error {
    segments, err := segmentFileNames(l.path)
    if len(segments) > 0 {
        lastSegment := segments[len(segments)-1]
        id, err := idFromFileName(lastSegment)
        if err != nil {
            return err
        }

        l.currentSegmentID = id
        stat, err := os.Stat(lastSegment)
        if err != nil {
            return err
        }

        if stat.Size() == 0 {
            os.Remove(lastSegment)
            segments = segments[:len(segments)-1]
        } else {
            fd, err := os.OpenFile(lastSegment, os.O_RDWR, 0666)
            if err != nil {
                return err
            }
            if _, err := fd.Seek(0, io.SeekEnd); err != nil {
                return err
            }
            l.currentSegmentWriter = NewWALSegmentWriter(fd)

            // Set the correct size on the segment writer
            atomic.StoreInt64(&l.stats.CurrentBytes, stat.Size())
            l.currentSegmentWriter.size = int(stat.Size())
        }
    }
    return nil
}

这里会选取最后一个文件,因为最后一个文件是最新的文件,也就是active的,其他WAL是达到了写入限制,不能被写入的。

WALEntry

和其他的WAL文件一样(series,index) WAL的写入也是以entry粒度的。WAL Entry有不同类型,代表不同的操作,主要有三种:

  • WriteWALEntry
  • DeleteWALEntry
  • DeleteRangeWALEntry

这些结构有一个顶层的抽象:

type WALEntry interface {
	Type() WalEntryType
	Encode(dst []byte) ([]byte, error)
	MarshalBinary() ([]byte, error)
	UnmarshalBinary(b []byte) error
	MarshalSize() int
}

不同的Type代表:

const (
	// WriteWALEntryType indicates a write entry.
	WriteWALEntryType WalEntryType = 0x01

	// DeleteWALEntryType indicates a delete entry.
	DeleteWALEntryType WalEntryType = 0x02

	// DeleteRangeWALEntryType indicates a delete range entry.
	DeleteRangeWALEntryType WalEntryType = 0x03
)

例如WriteWALEntry

type WriteWALEntry struct {
	Values map[string][]Value
	sz     int
}

核心结构是一个Values的map,key是Measurement+tags+field ,value是这个series key对应的Value集合。

WAL在写入到文件时,会做Encode,变成成一个byte数组,这段逻辑由具体的实现结构自己完成。如WriteWALEntry的Encode逻辑:

func (w *WriteWALEntry) Encode(dst []byte) ([]byte, error) {
	// The entries values are encode as follows:
	//
	// For each key and slice of values, first a 1 byte type for the []Values
	// slice is written.  Following the type, the length and key bytes are written.
	// Following the key, a 4 byte count followed by each value as a 8 byte time
	// and N byte value.  The value is dependent on the type being encoded.  float64,
	// int64, use 8 bytes, boolean uses 1 byte, and string is similar to the key encoding,
	// except that string values have a 4-byte length, and keys only use 2 bytes.
	//
	// This structure is then repeated for each key an value slices.
	//
	// ┌────────────────────────────────────────────────────────────────────┐
	// │                           WriteWALEntry                            │
	// ├──────┬─────────┬────────┬───────┬─────────┬─────────┬───┬──────┬───┤
	// │ Type │ Key Len │   Key  │ Count │  Time   │  Value  │...│ Type │...│
	// │1 byte│ 2 bytes │ N bytes│4 bytes│ 8 bytes │ N bytes │   │1 byte│   │
	// └──────┴─────────┴────────┴───────┴─────────┴─────────┴───┴──────┴───┘

	encLen := w.MarshalSize() // Type (1), Key Length (2), and Count (4) for each key

	// allocate or re-slice to correct size
	if len(dst) < encLen {
		dst = make([]byte, encLen)
	} else {
		dst = dst[:encLen]
	}

	// Finally, encode the entry
	var n int
	var curType byte

	for k, v := range w.Values {
		switch v[0].(type) {
		case FloatValue:
			curType = float64EntryType
		case IntegerValue:
			curType = integerEntryType
		case UnsignedValue:
			curType = unsignedEntryType
		case BooleanValue:
			curType = booleanEntryType
		case StringValue:
			curType = stringEntryType
		default:
			return nil, fmt.Errorf("unsupported value type: %T", v[0])
		}
		dst[n] = curType
		n++

		binary.BigEndian.PutUint16(dst[n:n+2], uint16(len(k)))
		n += 2
		n += copy(dst[n:], k)

		binary.BigEndian.PutUint32(dst[n:n+4], uint32(len(v)))
		n += 4

		for _, vv := range v {
			binary.BigEndian.PutUint64(dst[n:n+8], uint64(vv.UnixNano()))
			n += 8

			switch vv := vv.(type) {
			case FloatValue:
				if curType != float64EntryType {
					return nil, fmt.Errorf("incorrect value found in %T slice: %T", v[0].Value(), vv)
				}
				binary.BigEndian.PutUint64(dst[n:n+8], math.Float64bits(vv.value))
				n += 8
			case IntegerValue:
				if curType != integerEntryType {
					return nil, fmt.Errorf("incorrect value found in %T slice: %T", v[0].Value(), vv)
				}
				binary.BigEndian.PutUint64(dst[n:n+8], uint64(vv.value))
				n += 8
			case UnsignedValue:
				if curType != unsignedEntryType {
					return nil, fmt.Errorf("incorrect value found in %T slice: %T", v[0].Value(), vv)
				}
				binary.BigEndian.PutUint64(dst[n:n+8], uint64(vv.value))
				n += 8
			case BooleanValue:
				if curType != booleanEntryType {
					return nil, fmt.Errorf("incorrect value found in %T slice: %T", v[0].Value(), vv)
				}
				if vv.value {
					dst[n] = 1
				} else {
					dst[n] = 0
				}
				n++
			case StringValue:
				if curType != stringEntryType {
					return nil, fmt.Errorf("incorrect value found in %T slice: %T", v[0].Value(), vv)
				}
				binary.BigEndian.PutUint32(dst[n:n+4], uint32(len(vv.value)))
				n += 4
				n += copy(dst[n:], vv.value)
			default:
				return nil, fmt.Errorf("unsupported value found in %T slice: %T", v[0].Value(), vv)
			}
		}
	}

	return dst[:n], nil
}

格式已经给出来了。按照这个格式做编码即可。这里会区分一下field的类型,不同value的类型有点区别。但是总得逻辑是一样的。既然有encode,那么就会有decode。这个就不再细说了,是逆操作。

WAL WriteMulti和writeToLog

有了上面的基础,看一下写入WAL的逻辑。

首先是writeToLog:

func (l *WAL) writeToLog(entry WALEntry) (int, error) {

    // 获取bytebuffer
    bytes := bytesPool.Get(entry.MarshalSize())
    // 编码
    b, err := entry.Encode(bytes)
    if err != nil {
        bytesPool.Put(bytes)
        return -1, err
    }

    encBuf := bytesPool.Get(snappy.MaxEncodedLen(len(b)))
    // 做snappy 压缩
    compressed := snappy.Encode(encBuf, b)
    bytesPool.Put(bytes)

    syncErr := make(chan error)

    segID, err := func() (int, error) {
        l.mu.Lock()
        defer l.mu.Unlock()

        // Make sure the log has not been closed
        select {
        case <-l.closing:
            return -1, ErrWALClosed
        default:
        }

        // 尝试选取下一个segment,这里是因为file size达到限制。
        // roll the segment file if needed
        if err := l.rollSegment(); err != nil {
            return -1, fmt.Errorf("error rolling WAL segment: %v", err)
        }

        // write and sync
        if err := l.currentSegmentWriter.Write(entry.Type(), compressed); err != nil {
            return -1, fmt.Errorf("error writing WAL entry: %v", err)
        }

        select {
        case l.syncWaiters <- syncErr:
        default:
            return -1, fmt.Errorf("error syncing wal")
        }
        
        l.scheduleSync()
        // Update stats for current segment size
        atomic.StoreInt64(&l.stats.CurrentBytes, int64(l.currentSegmentWriter.size))

        l.lastWriteTime = time.Now().UTC()

        return l.currentSegmentID, nil

    }()

    bytesPool.Put(encBuf)

    if err != nil {
        return segID, err
    }

    // schedule an fsync and wait for it to complete
    return segID, <-syncErr
}

代码有点长,没做简化,因为每一步骤都很重要。简单解释一下。

  • 首先是encode,这里会对数据做压缩,所以做了个byte pool复用 byte数组
  • 得到压缩之后的数据,开始写入到wal中,开始会尝试做一下文件滚动,这里的逻辑就是尝试新建下一个wal文件,会check一些条件,主要是wal的大小。
  • roll之后,开始写入文件。这里是写入到linux page cache,没有调用sync 方法。
  • 出发sync,强制刷盘
  • 归还byte buffer

WriteMulti

func (l *WAL) WriteMulti(values map[string][]Value) (int, error) {
	// 这个map 是key-> values 的map
	entry := &WriteWALEntry{
		Values: values,
	}

	id, err := l.writeToLog(entry)
	if err != nil {
		atomic.AddInt64(&l.stats.WriteErr, 1)
		return -1, err
	}
	atomic.AddInt64(&l.stats.WriteOK, 1)

	return id, nil
}

看懂了writeToLog再看WriteMulti就很简单了,把数据构建为entry,然后调用writeToLog。这里多提一下,是不是很眼熟?在上一章Cache部分和数据写入部分,都有这个函数的身影。WriteMulti是datapoint 写入最后几步操作。可以去回顾一下。

WAL scheduleSync

在write时,调用的bufferWrite的write方法,这部分数据可能存在buffer,或者存在page cache里,没有被真正sync到磁盘上。所以会定时刷盘。把数据刷到磁盘上。

func (l *WAL) scheduleSync() {
	// If we're not the first to sync, then another goroutine is fsyncing the wal for us.
	if !atomic.CompareAndSwapUint64(&l.syncCount, 0, 1) {
		return
	}
	// Fsync the wal and notify all pending waiters
	go func() {
		var timerCh <-chan time.Time

		// time.NewTicker requires a > 0 delay, since 0 indicates no delay, use a closed
		// channel which will always be ready to read from.
		if l.syncDelay == 0 {
			// Create a RW chan and close it
			timerChrw := make(chan time.Time)
			close(timerChrw)
			// Convert it to a read-only
			timerCh = timerChrw
		} else {
			t := time.NewTicker(l.syncDelay)
			defer t.Stop()
			timerCh = t.C
		}
		for {
			select {
			case <-timerCh:
				l.mu.Lock()
				if len(l.syncWaiters) == 0 {
					atomic.StoreUint64(&l.syncCount, 0)
					l.mu.Unlock()
					return
				}

				l.sync()
				l.mu.Unlock()
			case <-l.closing:
				atomic.StoreUint64(&l.syncCount, 0)
				return
			}
		}
	}()
}

WAL Remove

上面看到了WAL的写入,那么这个文件什么时候删除呢?首先删除方法是有的:Remove。

func (l *WAL) Remove(files []string) error {
	l.mu.Lock()
	defer l.mu.Unlock()
	for _, fn := range files {
		l.traceLogger.Info("Removing WAL file", zap.String("path", fn))
		os.RemoveAll(fn)
	}

	// Refresh the on-disk size stats
	segments, err := segmentFileNames(l.path)
	if err != nil {
		return err
	}

	var totalOldDiskSize int64
	for _, seg := range segments {
		stat, err := os.Stat(seg)
		if err != nil {
			return err
		}

		totalOldDiskSize += stat.Size()
	}
	atomic.StoreInt64(&l.stats.OldBytes, totalOldDiskSize)

	return nil
}

提供文件全路径,调用os.RemoveAll,来完成文件删除。怎么删除的已经知道了,那么什么时候删除呢?

WAL的删除

这部分是一个单独的逻辑,和其他模块是联动的。这里先看一个主要的部分。先说结论,Cache被compact时,会删除对应的WAL来看一下这部分怎么实现的。

Engine compactCache

首先是对cache做compact:

// compactCache continually checks if the WAL cache should be written to disk.
func (e *Engine) compactCache() {
	t := time.NewTicker(time.Second)
	defer t.Stop()
	for {
		e.mu.RLock()
		quit := e.snapDone
		e.mu.RUnlock()

		select {
		case <-quit:
			return

		case <-t.C:
			e.Cache.UpdateAge()
			if e.ShouldCompactCache(time.Now()) {
				start := time.Now()
				e.traceLogger.Info("Compacting cache", zap.String("path", e.path))
				err := e.WriteSnapshot()
				if err != nil && err != errCompactionsDisabled {
					e.logger.Info("Error writing snapshot", zap.Error(err))
					atomic.AddInt64(&e.stats.CacheCompactionErrors, 1)
				} else {
					atomic.AddInt64(&e.stats.CacheCompactions, 1)
				}
				atomic.AddInt64(&e.stats.CacheCompactionDuration, time.Since(start).Nanoseconds())
			}
		}
	}
}

这是个定时任务,会定时检查,然后调用WriteSnapshot方法(方法有删减)

func (e *Engine) WriteSnapshot() (err error) {
    started := time.Now()
    closedFiles, snapshot, err := func() (segments []string, snapshot *Cache, err error) {
        e.mu.Lock()
        defer e.mu.Unlock()

        if e.WALEnabled {
            if err = e.WAL.CloseSegment(); err != nil {
                return
            }
            segments, err = e.WAL.ClosedSegments()
          
        }
        snapshot, err = e.Cache.Snapshot()
        return
    }()
    return e.writeSnapshotAndCommit(log, closedFiles, snapshot)
}

WriteSnapshot的核心逻辑是两个:

  • 关闭当前所有的segment,也就是wal file
  • 对Cache做snapshot

拿到这两份数据之后,调用writeSnapshotAndCommit,来把数据写到文件里。

func (e *Engine) writeSnapshotAndCommit(log *zap.Logger, closedFiles []string, snapshot *Cache) (err error) {
    newFiles, err := e.Compactor.WriteSnapshot(snapshot)
    if err != nil {
        log.Info("Error writing snapshot from compactor", zap.Error(err))
        return err
    }

    e.mu.RLock()
    defer e.mu.RUnlock()
    if err := e.FileStore.Replace(nil, newFiles); err != nil {
        log.Info("Error adding new TSM files from snapshot. Removing temp files.", zap.Error(err))

        // Remove the new snapshot files. We will try again.
        for _, file := range newFiles {
            if err := os.Remove(file); err != nil {
                log.Info("Unable to remove file", zap.String("path", file), zap.Error(err))
            }
        }
        return err
    }

    // clear the snapshot from the in-memory cache, then the old WAL files
    e.Cache.ClearSnapshot(true)

    if e.WALEnabled {
        if err := e.WAL.Remove(closedFiles); err != nil {
            log.Info("Error removing closed WAL segments", zap.Error(err))
        }
    }

    return nil
}

这里面就能看到WAL什么时候被移除了,在Cache被WriteSnapShot之后,会清除掉当前的Cache。所以得到的结论是:

Cache每次在Commit时,会清除当前系统的WAL

总结

本篇文章分析一下WAL 的工作原理,什么时候创建,怎么写入,具体的用处,以及什么时候删除。分析完这个之后,接下来就是用来存储数据的TSM File,或者是我们理解的SSTable。这部分刚才其实提到了一写,最后Cache在WriteSnapshot时,为啥可以删除WAL了?因为这个WriteSnapshot就是把数据写到TSM File里,已经持久化到磁盘了,所以这里WAL的作用就不再存在了。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值