github.com/m3db/m3@v1.5.0/src/cmd/tools/split_shards/main/main.go (about)

     1  // Copyright (c) 2021 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package main
    22  
    23  import (
    24  	"bytes"
    25  	"errors"
    26  	"fmt"
    27  	"io"
    28  	iofs "io/fs"
    29  	"log"
    30  	"os"
    31  	"path/filepath"
    32  	"regexp"
    33  	"strconv"
    34  	"strings"
    35  	"time"
    36  
    37  	"github.com/pborman/getopt"
    38  	"go.uber.org/zap"
    39  
    40  	"github.com/m3db/m3/src/dbnode/persist"
    41  	"github.com/m3db/m3/src/dbnode/persist/fs"
    42  	"github.com/m3db/m3/src/dbnode/sharding"
    43  	xerrors "github.com/m3db/m3/src/x/errors"
    44  	"github.com/m3db/m3/src/x/ident"
    45  	xtime "github.com/m3db/m3/src/x/time"
    46  )
    47  
    48  var checkpointPattern = regexp.MustCompile(`/data/(\w+)/([0-9]+)/fileset-([0-9]+)-([0-9]+)-checkpoint.db$`)
    49  
    50  func main() {
    51  	var (
    52  		optSrcPath       = getopt.StringLong("src-path", 's', "", "Source path [e.g. /temp/lib/m3db/data]")
    53  		optDstPathPrefix = getopt.StringLong("dst-path", 'd', "", "Destination path prefix [e.g. /var/lib/m3db/data]")
    54  		optBlockUntil    = getopt.Int64Long("block-until", 'b', 0, "Block Until Time, exclusive [in nsec]")
    55  		optShards        = getopt.Uint32Long("src-shards", 'h', 0, "Original (source) number of shards")
    56  		optFactor        = getopt.IntLong("factor", 'f', 0, "Integer factor to increase the number of shards by")
    57  	)
    58  	getopt.Parse()
    59  
    60  	rawLogger, err := zap.NewDevelopment()
    61  	if err != nil {
    62  		log.Fatalf("unable to create logger: %+v", err)
    63  	}
    64  	logger := rawLogger.Sugar()
    65  
    66  	if *optSrcPath == "" ||
    67  		*optDstPathPrefix == "" ||
    68  		*optSrcPath == *optDstPathPrefix ||
    69  		*optBlockUntil <= 0 ||
    70  		*optShards == 0 ||
    71  		*optFactor <= 0 {
    72  		getopt.Usage()
    73  		os.Exit(1)
    74  	}
    75  
    76  	var (
    77  		srcFilesetLocation = dropDataSuffix(*optSrcPath)
    78  		dstFilesetLocation = dropDataSuffix(*optDstPathPrefix)
    79  
    80  		srcFsOpts = fs.NewOptions().SetFilePathPrefix(srcFilesetLocation)
    81  		dstFsOpts = fs.NewOptions().SetFilePathPrefix(dstFilesetLocation)
    82  	)
    83  
    84  	// Not using bytes pool with streaming reads/writes to avoid the fixed memory overhead.
    85  	srcReader, err := fs.NewReader(nil, srcFsOpts)
    86  	if err != nil {
    87  		logger.Fatalf("could not create srcReader: %+v", err)
    88  	}
    89  
    90  	dstReaders := make([]fs.DataFileSetReader, *optFactor)
    91  	dstWriters := make([]fs.StreamingWriter, *optFactor)
    92  	for i := range dstWriters {
    93  		dstReaders[i], err = fs.NewReader(nil, dstFsOpts)
    94  		if err != nil {
    95  			logger.Fatalf("could not create dstReader, %+v", err)
    96  		}
    97  		dstWriters[i], err = fs.NewStreamingWriter(dstFsOpts)
    98  		if err != nil {
    99  			logger.Fatalf("could not create writer, %+v", err)
   100  		}
   101  	}
   102  
   103  	hashFn := sharding.DefaultHashFn(int(*optShards) * (*optFactor))
   104  
   105  	start := time.Now()
   106  
   107  	if err := filepath.WalkDir(*optSrcPath, func(path string, d iofs.DirEntry, err error) error {
   108  		if err != nil || d.IsDir() || !strings.HasSuffix(d.Name(), "-checkpoint.db") {
   109  			return err
   110  		}
   111  		fmt.Printf("%s - %s\n", time.Now().Local(), path) // nolint: forbidigo
   112  		pathParts := checkpointPattern.FindStringSubmatch(path)
   113  		if len(pathParts) != 5 {
   114  			return fmt.Errorf("failed to parse path %s", path)
   115  		}
   116  
   117  		var (
   118  			namespace        = pathParts[1]
   119  			shard, err2      = strconv.Atoi(pathParts[2])
   120  			blockStart, err3 = strconv.Atoi(pathParts[3])
   121  			volume, err4     = strconv.Atoi(pathParts[4])
   122  		)
   123  		if err = xerrors.FirstError(err2, err3, err4); err != nil {
   124  			return err
   125  		}
   126  
   127  		if blockStart >= int(*optBlockUntil) {
   128  			fmt.Println(" - skip (too recent)") // nolint: forbidigo
   129  			return nil
   130  		}
   131  
   132  		if err = splitFileSet(
   133  			srcReader, dstWriters, hashFn, *optShards, *optFactor, namespace, uint32(shard),
   134  			xtime.UnixNano(blockStart), volume); err != nil {
   135  			if strings.Contains(err.Error(), "no such file or directory") {
   136  				fmt.Println(" - skip (incomplete fileset)") // nolint: forbidigo
   137  				return nil
   138  			}
   139  			return err
   140  		}
   141  
   142  		err = verifySplitShards(
   143  			srcReader, dstReaders, hashFn, *optShards, namespace, uint32(shard),
   144  			xtime.UnixNano(blockStart), volume)
   145  		if err != nil && strings.Contains(err.Error(), "no such file or directory") {
   146  			return nil
   147  		}
   148  		return err
   149  	}); err != nil {
   150  		logger.Fatalf("unable to walk the source dir: %+v", err)
   151  	}
   152  
   153  	runTime := time.Since(start)
   154  	fmt.Printf("Running time: %s\n", runTime) // nolint: forbidigo
   155  }
   156  
   157  func splitFileSet(
   158  	srcReader fs.DataFileSetReader,
   159  	dstWriters []fs.StreamingWriter,
   160  	hashFn sharding.HashFn,
   161  	srcNumShards uint32,
   162  	factor int,
   163  	namespace string,
   164  	srcShard uint32,
   165  	blockStart xtime.UnixNano,
   166  	volume int,
   167  ) error {
   168  	if srcShard >= srcNumShards {
   169  		return fmt.Errorf("unexpected source shard ID %d (must be under %d)", srcShard, srcNumShards)
   170  	}
   171  
   172  	readOpts := fs.DataReaderOpenOptions{
   173  		Identifier: fs.FileSetFileIdentifier{
   174  			Namespace:   ident.StringID(namespace),
   175  			Shard:       srcShard,
   176  			BlockStart:  blockStart,
   177  			VolumeIndex: volume,
   178  		},
   179  		FileSetType:      persist.FileSetFlushType,
   180  		StreamingEnabled: true,
   181  	}
   182  
   183  	err := srcReader.Open(readOpts)
   184  	if err != nil {
   185  		return fmt.Errorf("unable to open srcReader: %w", err)
   186  	}
   187  
   188  	plannedRecordsCount := uint(srcReader.Entries() / factor)
   189  	if plannedRecordsCount == 0 {
   190  		plannedRecordsCount = 1
   191  	}
   192  
   193  	for i := range dstWriters {
   194  		writeOpts := fs.StreamingWriterOpenOptions{
   195  			NamespaceID:         ident.StringID(namespace),
   196  			ShardID:             mapToDstShard(srcNumShards, i, srcShard),
   197  			BlockStart:          blockStart,
   198  			BlockSize:           srcReader.Status().BlockSize,
   199  			VolumeIndex:         volume + 1,
   200  			PlannedRecordsCount: plannedRecordsCount,
   201  		}
   202  		if err := dstWriters[i].Open(writeOpts); err != nil {
   203  			return fmt.Errorf("unable to open dstWriters[%d]: %w", i, err)
   204  		}
   205  	}
   206  
   207  	dataHolder := make([][]byte, 1)
   208  	for {
   209  		entry, err := srcReader.StreamingRead()
   210  		if errors.Is(err, io.EOF) {
   211  			break
   212  		}
   213  		if err != nil {
   214  			return fmt.Errorf("read error: %w", err)
   215  		}
   216  
   217  		newShardID := hashFn(entry.ID)
   218  		if newShardID%srcNumShards != srcShard {
   219  			return fmt.Errorf("mismatched shards, %d to %d", srcShard, newShardID)
   220  		}
   221  		writer := dstWriters[newShardID/srcNumShards]
   222  
   223  		dataHolder[0] = entry.Data
   224  		if err := writer.WriteAll(entry.ID, entry.EncodedTags, dataHolder, entry.DataChecksum); err != nil {
   225  			return err
   226  		}
   227  	}
   228  
   229  	for i := range dstWriters {
   230  		if err := dstWriters[i].Close(); err != nil {
   231  			return err
   232  		}
   233  	}
   234  
   235  	return srcReader.Close()
   236  }
   237  
   238  func verifySplitShards(
   239  	srcReader fs.DataFileSetReader,
   240  	dstReaders []fs.DataFileSetReader,
   241  	hashFn sharding.HashFn,
   242  	srcNumShards uint32,
   243  	namespace string,
   244  	srcShard uint32,
   245  	blockStart xtime.UnixNano,
   246  	volume int,
   247  ) error {
   248  	if srcShard >= srcNumShards {
   249  		return fmt.Errorf("unexpected source shard ID %d (must be under %d)", srcShard, srcNumShards)
   250  	}
   251  
   252  	srcReadOpts := fs.DataReaderOpenOptions{
   253  		Identifier: fs.FileSetFileIdentifier{
   254  			Namespace:   ident.StringID(namespace),
   255  			Shard:       srcShard,
   256  			BlockStart:  blockStart,
   257  			VolumeIndex: volume,
   258  		},
   259  		FileSetType:      persist.FileSetFlushType,
   260  		StreamingEnabled: true,
   261  	}
   262  
   263  	err := srcReader.Open(srcReadOpts)
   264  	if err != nil {
   265  		return fmt.Errorf("unable to open srcReader: %w", err)
   266  	}
   267  
   268  	dstEntries := 0
   269  	for i := range dstReaders {
   270  		dstReadOpts := fs.DataReaderOpenOptions{
   271  			Identifier: fs.FileSetFileIdentifier{
   272  				Namespace:   ident.StringID(namespace),
   273  				Shard:       mapToDstShard(srcNumShards, i, srcShard),
   274  				BlockStart:  blockStart,
   275  				VolumeIndex: volume + 1,
   276  			},
   277  			FileSetType:      persist.FileSetFlushType,
   278  			StreamingEnabled: true,
   279  		}
   280  		if err := dstReaders[i].Open(dstReadOpts); err != nil {
   281  			return fmt.Errorf("unable to open dstReaders[%d]: %w", i, err)
   282  		}
   283  		dstEntries += dstReaders[i].Entries()
   284  	}
   285  
   286  	if srcReader.Entries() != dstEntries {
   287  		return fmt.Errorf("entry count mismatch: src %d != dst %d", srcReader.Entries(), dstEntries)
   288  	}
   289  
   290  	for {
   291  		srcEntry, err := srcReader.StreamingReadMetadata()
   292  		if errors.Is(err, io.EOF) {
   293  			break
   294  		}
   295  		if err != nil {
   296  			return fmt.Errorf("src read error: %w", err)
   297  		}
   298  
   299  		newShardID := hashFn(srcEntry.ID)
   300  		if newShardID%srcNumShards != srcShard {
   301  			return fmt.Errorf("mismatched shards, %d to %d", srcShard, newShardID)
   302  		}
   303  		dstReader := dstReaders[newShardID/srcNumShards]
   304  
   305  		// Using StreamingRead() on destination filesets here because it also verifies data checksums.
   306  		dstEntry, err := dstReader.StreamingRead()
   307  		if err != nil {
   308  			return fmt.Errorf("dst read error: %w", err)
   309  		}
   310  
   311  		if !bytes.Equal(srcEntry.ID, dstEntry.ID) {
   312  			return fmt.Errorf("ID mismatch: %s != %s", srcEntry.ID, dstEntry.ID)
   313  		}
   314  		if !bytes.Equal(srcEntry.EncodedTags, dstEntry.EncodedTags) {
   315  			return fmt.Errorf("EncodedTags mismatch: %s != %s", srcEntry.EncodedTags, dstEntry.EncodedTags)
   316  		}
   317  		if srcEntry.DataChecksum != dstEntry.DataChecksum {
   318  			return fmt.Errorf("data checksum mismatch: %d != %d, id=%s",
   319  				srcEntry.DataChecksum, dstEntry.DataChecksum, srcEntry.ID)
   320  		}
   321  	}
   322  
   323  	for i := range dstReaders {
   324  		dstReader := dstReaders[i]
   325  		if _, err := dstReader.StreamingReadMetadata(); !errors.Is(err, io.EOF) {
   326  			return fmt.Errorf("expected EOF on split shard %d, but got %w",
   327  				dstReader.Status().Shard, err)
   328  		}
   329  		if err := dstReader.Close(); err != nil {
   330  			return err
   331  		}
   332  	}
   333  
   334  	return srcReader.Close()
   335  }
   336  
   337  func mapToDstShard(srcNumShards uint32, i int, srcShard uint32) uint32 {
   338  	return srcNumShards*uint32(i) + srcShard
   339  }
   340  
   341  func dropDataSuffix(path string) string {
   342  	dataIdx := strings.LastIndex(path, "/data")
   343  	if dataIdx < 0 {
   344  		return path
   345  	}
   346  	return path[:dataIdx]
   347  }