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 }