github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/cdc/redo/reader/file.go (about)

     1  //  Copyright 2021 PingCAP, Inc.
     2  //  Copyright 2015 CoreOS, Inc.
     3  //
     4  //  Licensed under the Apache License, Version 2.0 (the "License");
     5  //  you may not use this file except in compliance with the License.
     6  //  You may obtain a copy of the License at
     7  //
     8  //      http://www.apache.org/licenses/LICENSE-2.0
     9  //
    10  //  Unless required by applicable law or agreed to in writing, software
    11  //  distributed under the License is distributed on an "AS IS" BASIS,
    12  //  See the License for the specific language governing permissions and
    13  //  limitations under the License.
    14  
    15  package reader
    16  
    17  import (
    18  	"bufio"
    19  	"bytes"
    20  	"container/heap"
    21  	"context"
    22  	"encoding/binary"
    23  	"io"
    24  	"math"
    25  	"net/url"
    26  	"os"
    27  	"path/filepath"
    28  	"strings"
    29  	"sync"
    30  	"time"
    31  
    32  	"github.com/pingcap/errors"
    33  	"github.com/pingcap/log"
    34  	"github.com/pingcap/tidb/br/pkg/storage"
    35  	"github.com/pingcap/tiflow/cdc/model"
    36  	"github.com/pingcap/tiflow/cdc/model/codec"
    37  	"github.com/pingcap/tiflow/cdc/redo/writer"
    38  	"github.com/pingcap/tiflow/cdc/redo/writer/file"
    39  	"github.com/pingcap/tiflow/pkg/compression"
    40  	cerror "github.com/pingcap/tiflow/pkg/errors"
    41  	"github.com/pingcap/tiflow/pkg/redo"
    42  	"go.uber.org/zap"
    43  	"golang.org/x/sync/errgroup"
    44  )
    45  
    46  const (
    47  	// frameSizeBytes is frame size in bytes, including record size and padding size.
    48  	frameSizeBytes = 8
    49  
    50  	// defaultWorkerNum is the num of workers used to sort the log file to sorted file,
    51  	// will load the file to memory first then write the sorted file to disk
    52  	// the memory used is defaultWorkerNum * defaultMaxLogSize (64 * megabyte) total
    53  	defaultWorkerNum = 16
    54  )
    55  
    56  // lz4MagicNumber is the magic number of lz4 compressed data
    57  var lz4MagicNumber = []byte{0x04, 0x22, 0x4D, 0x18}
    58  
    59  type fileReader interface {
    60  	io.Closer
    61  	// Read return the log from log file
    62  	Read() (*model.RedoLog, error)
    63  }
    64  
    65  type readerConfig struct {
    66  	startTs  uint64
    67  	endTs    uint64
    68  	dir      string
    69  	fileType string
    70  
    71  	uri                url.URL
    72  	useExternalStorage bool
    73  	workerNums         int
    74  }
    75  
    76  type reader struct {
    77  	cfg      *readerConfig
    78  	mu       sync.Mutex
    79  	br       io.Reader
    80  	fileName string
    81  	closer   io.Closer
    82  	// lastValidOff file offset following the last valid decoded record
    83  	lastValidOff int64
    84  }
    85  
    86  func newReaders(ctx context.Context, cfg *readerConfig) ([]fileReader, error) {
    87  	if cfg == nil {
    88  		return nil, cerror.WrapError(cerror.ErrRedoConfigInvalid, errors.New("readerConfig can not be nil"))
    89  	}
    90  	if !cfg.useExternalStorage {
    91  		log.Panic("external storage is not enabled, please check your configuration")
    92  	}
    93  	if cfg.workerNums == 0 {
    94  		cfg.workerNums = defaultWorkerNum
    95  	}
    96  	start := time.Now()
    97  
    98  	sortedFiles, err := downLoadAndSortFiles(ctx, cfg)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	readers := []fileReader{}
   104  	for i := range sortedFiles {
   105  		readers = append(readers,
   106  			&reader{
   107  				cfg:      cfg,
   108  				br:       bufio.NewReader(sortedFiles[i]),
   109  				fileName: sortedFiles[i].(*os.File).Name(),
   110  				closer:   sortedFiles[i],
   111  			})
   112  	}
   113  
   114  	log.Info("succeed to download and sort redo logs",
   115  		zap.String("type", cfg.fileType),
   116  		zap.Duration("duration", time.Since(start)))
   117  	return readers, nil
   118  }
   119  
   120  func downLoadAndSortFiles(ctx context.Context, cfg *readerConfig) ([]io.ReadCloser, error) {
   121  	dir := cfg.dir
   122  	// create temp dir in local storage
   123  	err := os.MkdirAll(dir, redo.DefaultDirMode)
   124  	if err != nil {
   125  		return nil, cerror.WrapError(cerror.ErrRedoFileOp, err)
   126  	}
   127  
   128  	// get all files
   129  	extStorage, err := redo.InitExternalStorage(ctx, cfg.uri)
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  	files, err := selectDownLoadFile(ctx, extStorage, cfg.fileType, cfg.startTs)
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  
   138  	limit := make(chan struct{}, cfg.workerNums)
   139  	eg, eCtx := errgroup.WithContext(ctx)
   140  	sortedFileNames := make([]string, 0, len(files))
   141  	for _, file := range files {
   142  		select {
   143  		case <-eCtx.Done():
   144  			return nil, eCtx.Err()
   145  		case limit <- struct{}{}:
   146  		}
   147  
   148  		fileName := file
   149  		if strings.HasSuffix(fileName, redo.SortLogEXT) {
   150  			log.Panic("should not download sorted log file")
   151  		}
   152  		sortedFileNames = append(sortedFileNames, getSortedFileName(fileName))
   153  		eg.Go(func() error {
   154  			defer func() { <-limit }()
   155  			return sortAndWriteFile(ctx, extStorage, fileName, cfg)
   156  		})
   157  	}
   158  	if err := eg.Wait(); err != nil {
   159  		return nil, err
   160  	}
   161  
   162  	// open all sorted files
   163  	ret := []io.ReadCloser{}
   164  	for _, sortedFileName := range sortedFileNames {
   165  		path := filepath.Join(dir, sortedFileName)
   166  		f, err := os.OpenFile(path, os.O_RDONLY, redo.DefaultFileMode)
   167  		if err != nil {
   168  			if os.IsNotExist(err) {
   169  				continue
   170  			}
   171  			return nil, cerror.WrapError(cerror.ErrRedoFileOp, err)
   172  		}
   173  		ret = append(ret, f)
   174  	}
   175  	return ret, nil
   176  }
   177  
   178  func getSortedFileName(name string) string {
   179  	return filepath.Base(name) + redo.SortLogEXT
   180  }
   181  
   182  func selectDownLoadFile(
   183  	ctx context.Context, extStorage storage.ExternalStorage,
   184  	fixedType string, startTs uint64,
   185  ) ([]string, error) {
   186  	files := []string{}
   187  	// add changefeed filter and endTs filter
   188  	err := extStorage.WalkDir(ctx, &storage.WalkOption{},
   189  		func(path string, size int64) error {
   190  			fileName := filepath.Base(path)
   191  			ret, err := shouldOpen(startTs, fileName, fixedType)
   192  			if err != nil {
   193  				log.Warn("check selected log file fail",
   194  					zap.String("logFile", fileName),
   195  					zap.Error(err))
   196  				return err
   197  			}
   198  			if ret {
   199  				files = append(files, path)
   200  			}
   201  			return nil
   202  		})
   203  	if err != nil {
   204  		return nil, cerror.WrapError(cerror.ErrExternalStorageAPI, err)
   205  	}
   206  
   207  	return files, nil
   208  }
   209  
   210  func isLZ4Compressed(data []byte) bool {
   211  	if len(data) < 4 {
   212  		return false
   213  	}
   214  	return bytes.Equal(data[:4], lz4MagicNumber)
   215  }
   216  
   217  func readAllFromBuffer(buf []byte) (logHeap, error) {
   218  	r := &reader{
   219  		br: bytes.NewReader(buf),
   220  	}
   221  	defer r.Close()
   222  
   223  	h := logHeap{}
   224  	for {
   225  		rl, err := r.Read()
   226  		if err != nil {
   227  			if err != io.EOF {
   228  				return nil, err
   229  			}
   230  			break
   231  		}
   232  		h = append(h, &logWithIdx{data: rl})
   233  	}
   234  
   235  	return h, nil
   236  }
   237  
   238  // sortAndWriteFile read file from external storage, then sort the file and write
   239  // to local storage.
   240  func sortAndWriteFile(
   241  	egCtx context.Context,
   242  	extStorage storage.ExternalStorage,
   243  	fileName string, cfg *readerConfig,
   244  ) error {
   245  	sortedName := getSortedFileName(fileName)
   246  	writerCfg := &writer.LogWriterConfig{
   247  		Dir:               cfg.dir,
   248  		MaxLogSizeInBytes: math.MaxInt32,
   249  	}
   250  	w, err := file.NewFileWriter(egCtx, writerCfg, writer.WithLogFileName(func() string {
   251  		return sortedName
   252  	}))
   253  	if err != nil {
   254  		return err
   255  	}
   256  
   257  	fileContent, err := extStorage.ReadFile(egCtx, fileName)
   258  	if err != nil {
   259  		return cerror.WrapError(cerror.ErrExternalStorageAPI, err)
   260  	}
   261  	if len(fileContent) == 0 {
   262  		log.Warn("download file is empty", zap.String("file", fileName))
   263  		return nil
   264  	}
   265  	// it's lz4 compressed, decompress it
   266  	if isLZ4Compressed(fileContent) {
   267  		if fileContent, err = compression.Decode(compression.LZ4, fileContent); err != nil {
   268  			return err
   269  		}
   270  	}
   271  
   272  	// sort data
   273  	h, err := readAllFromBuffer(fileContent)
   274  	if err != nil {
   275  		return err
   276  	}
   277  	heap.Init(&h)
   278  	for h.Len() != 0 {
   279  		item := heap.Pop(&h).(*logWithIdx).data
   280  		// This is min commitTs in log heap.
   281  		if item.GetCommitTs() > cfg.endTs {
   282  			// If the commitTs is greater than endTs, we should stop sorting
   283  			// and ignore the rest of the logs.
   284  			log.Info("ignore logs which commitTs is greater than resolvedTs",
   285  				zap.Any("filename", fileName), zap.Uint64("endTs", cfg.endTs))
   286  			break
   287  		}
   288  		if item.GetCommitTs() <= cfg.startTs {
   289  			// If the commitTs is equal or less than startTs, we should skip this log.
   290  			continue
   291  		}
   292  		data, err := codec.MarshalRedoLog(item, nil)
   293  		if err != nil {
   294  			return cerror.WrapError(cerror.ErrMarshalFailed, err)
   295  		}
   296  		_, err = w.Write(data)
   297  		if err != nil {
   298  			return err
   299  		}
   300  	}
   301  
   302  	return w.Close()
   303  }
   304  
   305  func shouldOpen(startTs uint64, name, fixedType string) (bool, error) {
   306  	// .sort.tmp will return error
   307  	commitTs, fileType, err := redo.ParseLogFileName(name)
   308  	if err != nil {
   309  		return false, err
   310  	}
   311  	if fileType != fixedType {
   312  		return false, nil
   313  	}
   314  	// always open .tmp
   315  	if filepath.Ext(name) == redo.TmpEXT {
   316  		return true, nil
   317  	}
   318  	// the commitTs=max(ts of log item in the file), if max > startTs then should open,
   319  	// filter out ts in (startTs, endTs] for consume
   320  	return commitTs > startTs, nil
   321  }
   322  
   323  // Read implement Read interface.
   324  // TODO: more general reader pair with writer in writer pkg
   325  func (r *reader) Read() (*model.RedoLog, error) {
   326  	r.mu.Lock()
   327  	defer r.mu.Unlock()
   328  
   329  	lenField, err := readInt64(r.br)
   330  	if err != nil {
   331  		if err == io.EOF {
   332  			return nil, err
   333  		}
   334  		return nil, cerror.WrapError(cerror.ErrRedoFileOp, err)
   335  	}
   336  
   337  	recBytes, padBytes := decodeFrameSize(lenField)
   338  	data := make([]byte, recBytes+padBytes)
   339  	_, err = io.ReadFull(r.br, data)
   340  	if err != nil {
   341  		if err == io.EOF || err == io.ErrUnexpectedEOF {
   342  			log.Warn("read redo log have unexpected io error",
   343  				zap.String("fileName", r.fileName),
   344  				zap.Error(err))
   345  			return nil, io.EOF
   346  		}
   347  		return nil, cerror.WrapError(cerror.ErrRedoFileOp, err)
   348  	}
   349  
   350  	redoLog, _, err := codec.UnmarshalRedoLog(data[:recBytes])
   351  	if err != nil {
   352  		if r.isTornEntry(data) {
   353  			// just return io.EOF, since if torn write it is the last redoLog entry
   354  			return nil, io.EOF
   355  		}
   356  		return nil, cerror.WrapError(cerror.ErrUnmarshalFailed, err)
   357  	}
   358  
   359  	// point last valid offset to the end of redoLog
   360  	r.lastValidOff += frameSizeBytes + recBytes + padBytes
   361  	return redoLog, nil
   362  }
   363  
   364  func readInt64(r io.Reader) (int64, error) {
   365  	var n int64
   366  	err := binary.Read(r, binary.LittleEndian, &n)
   367  	return n, err
   368  }
   369  
   370  // decodeFrameSize pair with encodeFrameSize in writer.file
   371  // the func use code from etcd wal/decoder.go
   372  func decodeFrameSize(lenField int64) (recBytes int64, padBytes int64) {
   373  	// the record size is stored in the lower 56 bits of the 64-bit length
   374  	recBytes = int64(uint64(lenField) & ^(uint64(0xff) << 56))
   375  	// non-zero padding is indicated by set MSb / a negative length
   376  	if lenField < 0 {
   377  		// padding is stored in lower 3 bits of length MSB
   378  		padBytes = int64((uint64(lenField) >> 56) & 0x7)
   379  	}
   380  	return recBytes, padBytes
   381  }
   382  
   383  // isTornEntry determines whether the last entry of the Log was partially written
   384  // and corrupted because of a torn write.
   385  // the func use code from etcd wal/decoder.go
   386  // ref: https://github.com/etcd-io/etcd/pull/5250
   387  func (r *reader) isTornEntry(data []byte) bool {
   388  	fileOff := r.lastValidOff + frameSizeBytes
   389  	curOff := 0
   390  	chunks := [][]byte{}
   391  	// split data on sector boundaries
   392  	for curOff < len(data) {
   393  		chunkLen := int(redo.MinSectorSize - (fileOff % redo.MinSectorSize))
   394  		if chunkLen > len(data)-curOff {
   395  			chunkLen = len(data) - curOff
   396  		}
   397  		chunks = append(chunks, data[curOff:curOff+chunkLen])
   398  		fileOff += int64(chunkLen)
   399  		curOff += chunkLen
   400  	}
   401  
   402  	// if any data for a sector chunk is all 0, it's a torn write
   403  	for _, sect := range chunks {
   404  		isZero := true
   405  		for _, v := range sect {
   406  			if v != 0 {
   407  				isZero = false
   408  				break
   409  			}
   410  		}
   411  		if isZero {
   412  			return true
   413  		}
   414  	}
   415  	return false
   416  }
   417  
   418  // Close implement the Close interface
   419  func (r *reader) Close() error {
   420  	if r == nil || r.closer == nil {
   421  		return nil
   422  	}
   423  
   424  	return cerror.WrapError(cerror.ErrRedoFileOp, r.closer.Close())
   425  }