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 }