Skip to content

Commit f042e41

Browse files
feat(restore): implement native restore batching
Previously, batches restored with both rclone and native method used the same batching mechanism. Although batching to smaller chunks makes sense for rclone restore, as it reduces the risk of running out of disk space and increases resource util by having nodes which work at potentially different restore stages (rclone download / load&stream), this logic doesn't apply to native restore where sstables are not saved on disk before they are streamed to the primary replicas. In native restore case, we want to send as big batches as possible while still keeping balanced workload distribution. To achieve that, we just simply reuse the --batch-size=0 behavior making batches containing 5% of expected node workload, so that batches restored with native method aim to contain 100% of expected node workload. To do that, we need to make the decision about restore method used for given batch restoration during batch dispatch. This resulted in small code refactor - mainly populating hostInfo before accessing batching mechanism and adding restore method as a part of the batch definition. As working with batches containing 100% of expected node workload is not granular at all, we might run into problems with large workload imbalance, as giving one node one additional batch could push it towards 200% of expected workload. To fight with that, we create batches containing up to the remaining expected node bytes, or up to the 5% as in the --batch-size=0 case, so that when the node already pushes over the 100% expected workload, it will be receiving smaller batches that should result in more even distribution. Fixes #4592
1 parent 29952a4 commit f042e41

9 files changed

Lines changed: 634 additions & 119 deletions

File tree

docs/source/sctool/partials/sctool_restore.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ options:
1515
- name: batch-size
1616
default_value: "2"
1717
usage: |
18-
Number of SSTables per shard to process in one request by one node.
18+
Number of SSTables per shard to process in one request by one node when restoring with --method=rclone.
1919
Increasing the default batch size might significantly increase restore performance, as only one shard can work on restoring a single SSTable bundle.
2020
Set to 0 for best performance (batches will contain sstables of total size up to 5% of expected total node workload).
2121
- name: cluster

docs/source/sctool/partials/sctool_restore_update.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ options:
1313
- name: batch-size
1414
default_value: "2"
1515
usage: |
16-
Number of SSTables per shard to process in one request by one node.
16+
Number of SSTables per shard to process in one request by one node when restoring with --method=rclone.
1717
Increasing the default batch size might significantly increase restore performance, as only one shard can work on restoring a single SSTable bundle.
1818
Set to 0 for best performance (batches will contain sstables of total size up to 5% of expected total node workload).
1919
- name: cluster

pkg/command/restore/res.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ snapshot-tag: |
2626
Snapshot tags can be obtained from backup listing ('./sctool backup list' command - e.g. sm_20060102150405UTC).
2727
2828
batch-size: |
29-
Number of SSTables per shard to process in one request by one node.
29+
Number of SSTables per shard to process in one request by one node when restoring with --method=rclone.
3030
Increasing the default batch size might significantly increase restore performance, as only one shard can work on restoring a single SSTable bundle.
3131
Set to 0 for best performance (batches will contain sstables of total size up to 5% of expected total node workload).
3232

pkg/service/restore/batch.go

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ import (
1212
"github.com/scylladb/scylla-manager/v3/pkg/sstable"
1313
)
1414

15+
type hostBatchInfo struct {
16+
host string
17+
shardCnt uint
18+
nativeRestoreSupport bool
19+
}
20+
1521
// batchDispatcher is a tool for batching SSTables from
1622
// Workload across different hosts during restore.
1723
// It follows a few rules:
@@ -53,18 +59,21 @@ type batchDispatcher struct {
5359
// For batchSize 0, batches contain N*node_shard_cnt SSTables
5460
// of total size up to 5% of node expected workload
5561
// (expectedShardWorkload*node_shard_cnt).
62+
// For hosts and batches supporting native restore, regardless of batchSize,
63+
// batches contain N*node_shard_cnt SSTables of total
64+
// size up to 100% of node expected workload.
5665
batchSize int
5766
// Equals total_backup_size/($\sum_{node} shard_cnt(node)$)
5867
expectedShardWorkload int64
59-
// Stores host shard count
60-
hostShardCnt map[string]uint
68+
hostInfo map[string]hostBatchInfo
69+
method Method
6170
}
6271

63-
func newBatchDispatcher(workload Workload, batchSize int, hostShardCnt map[string]uint, locationInfo []LocationInfo) *batchDispatcher {
72+
func newBatchDispatcher(workload Workload, batchSize int, hostInfo map[string]hostBatchInfo, locationInfo []LocationInfo, method Method) *batchDispatcher {
6473
sortWorkload(workload)
6574
var shards uint
66-
for _, sh := range hostShardCnt {
67-
shards += sh
75+
for _, hi := range hostInfo {
76+
shards += hi.shardCnt
6877
}
6978
if shards == 0 {
7079
shards = 1
@@ -76,7 +85,8 @@ func newBatchDispatcher(workload Workload, batchSize int, hostShardCnt map[strin
7685
workloadProgress: newWorkloadProgress(workload, locationInfo),
7786
batchSize: batchSize,
7887
expectedShardWorkload: workload.TotalSize / int64(shards),
79-
hostShardCnt: hostShardCnt,
88+
hostInfo: hostInfo,
89+
method: method,
8090
}
8191
}
8292

@@ -94,6 +104,10 @@ type workloadProgress struct {
94104
// It assumes that the whole DC is backed up to a single
95105
// backup location.
96106
hostDCAccess map[string][]string
107+
// Stores the amount of bytes assigned for given host to restore.
108+
// It's used when as an additional safety net when batching sstables
109+
// eligible for native restore (expected batch size is 100 % of expected node workload).
110+
hostAssignedBytes map[string]int64
97111
// SSTables grouped by RemoteSSTableDir that are yet to
98112
// be batched. They are removed on batch dispatch, but can
99113
// be re-added when batch failed to be restored.
@@ -129,6 +143,7 @@ func newWorkloadProgress(workload Workload, locationInfo []LocationInfo) workloa
129143
dcSSTableToBeRestored: dcSSTable,
130144
hostFailedDC: make(map[string][]string),
131145
hostDCAccess: getHostDCAccess(locationInfo),
146+
hostAssignedBytes: make(map[string]int64),
132147
remoteDir: p,
133148
}
134149
}
@@ -164,6 +179,7 @@ type batch struct {
164179
*ManifestInfo
165180

166181
batchType batchType
182+
method Method // that should be used for this batch restoration
167183
RemoteSSTableDir string
168184
Size int64
169185
SSTables []RemoteSSTable
@@ -286,6 +302,7 @@ func (bd *batchDispatcher) DispatchBatch(ctx context.Context, host string) (batc
286302

287303
func (bd *batchDispatcher) dispatchBatch(host string) (batch, bool) {
288304
dirIdx := -1
305+
289306
for i := range bd.workloadProgress.remoteDir {
290307
rdw := bd.workload.RemoteDir[i]
291308
// Skip empty dir
@@ -312,9 +329,9 @@ func (bd *batchDispatcher) dispatchBatch(host string) (batch, bool) {
312329
// Returns batch from given RemoteSSTableDir and updates workloadProgress.
313330
func (bd *batchDispatcher) createBatch(dirIdx int, host string) (batch, bool) {
314331
rdp := &bd.workloadProgress.remoteDir[dirIdx]
315-
shardCnt := bd.hostShardCnt[host]
316-
if shardCnt == 0 {
317-
shardCnt = 1
332+
hi := bd.hostInfo[host]
333+
if hi.shardCnt == 0 {
334+
hi.shardCnt = 1
318335
}
319336

320337
// Choose batch type and candidate sstables
@@ -331,15 +348,36 @@ func (bd *batchDispatcher) createBatch(dirIdx int, host string) (batch, bool) {
331348
return batch{}, false
332349
}
333350

351+
// At this point, if method is explicitly set to MethodNative,
352+
// we already verified that all hosts and the entire workload
353+
// supports native restore, so we can safely fall back to rclone if needed.
354+
batchMethod := MethodRclone
355+
if slices.Contains([]Method{MethodAuto, MethodNative}, bd.method) &&
356+
hi.nativeRestoreSupport &&
357+
sstables[0].NativeRestoreSupport() == nil {
358+
batchMethod = MethodNative
359+
}
360+
334361
var i int
335362
var size int64
336-
if bd.batchSize == maxBatchSize {
363+
if bd.batchSize == maxBatchSize || batchMethod == MethodNative {
337364
// Create batch containing multiple of node shard count sstables
338-
// and size up to 5% of expected node workload.
339-
expectedNodeWorkload := bd.expectedShardWorkload * int64(shardCnt)
365+
// and size up to a percentage of expected node workload.
366+
// For rclone restore with --batch-size=0, aim for 5% of expected node workload.
367+
// For native restore, aim for 100% of expected node workload.
368+
expectedNodeWorkload := bd.expectedShardWorkload * int64(hi.shardCnt)
340369
sizeLimit := expectedNodeWorkload / 20
370+
if batchMethod == MethodNative {
371+
// For 5% rclone batches, we expect imbalance in workload distribution
372+
// to be less significant even in the more problematic scenarios.
373+
// The risk of noticeable imbalance in 100% native batches is much higher,
374+
// so we fall back to 5% batches when node already restored its expected workload.
375+
alreadyAssigned := bd.workloadProgress.hostAssignedBytes[host]
376+
remainingExpected := max(expectedNodeWorkload-alreadyAssigned, 0)
377+
sizeLimit = max(remainingExpected, sizeLimit)
378+
}
341379
for {
342-
for range shardCnt {
380+
for range hi.shardCnt {
343381
if i >= len(sstables) {
344382
break
345383
}
@@ -355,7 +393,7 @@ func (bd *batchDispatcher) createBatch(dirIdx int, host string) (batch, bool) {
355393
}
356394
} else {
357395
// Create batch containing node_shard_count*batch_size sstables.
358-
i = min(bd.batchSize*int(shardCnt), len(sstables))
396+
i = min(bd.batchSize*int(hi.shardCnt), len(sstables))
359397
for j := range i {
360398
size += sstables[j].Size
361399
}
@@ -366,19 +404,21 @@ func (bd *batchDispatcher) createBatch(dirIdx int, host string) (batch, bool) {
366404
}
367405
// Extend batch if it was to leave less than
368406
// 1 sstable per shard for the next one.
369-
if len(sstables)-i < int(shardCnt) {
407+
if len(sstables)-i < int(hi.shardCnt) {
370408
for ; i < len(sstables); i++ {
371409
size += sstables[i].Size
372410
}
373411
}
374412

375413
rdp.RemainingSSTables[batchT] = sstables[i:]
376414
rdw := bd.workload.RemoteDir[dirIdx]
415+
bd.workloadProgress.hostAssignedBytes[host] += size
377416

378417
return batch{
379418
TableName: rdw.TableName,
380419
ManifestInfo: rdw.ManifestInfo,
381420
batchType: batchT,
421+
method: batchMethod,
382422
RemoteSSTableDir: rdw.RemoteSSTableDir,
383423
Size: size,
384424
SSTables: sstables[:i],
@@ -405,6 +445,7 @@ func (bd *batchDispatcher) ReportFailure(host string, b batch) error {
405445

406446
// Mark failed DC for host
407447
bd.workloadProgress.hostFailedDC[host] = append(bd.workloadProgress.hostFailedDC[host], b.DC)
448+
bd.workloadProgress.hostAssignedBytes[host] -= b.Size
408449

409450
dirIdx := -1
410451
for i := range bd.workload.RemoteDir {

0 commit comments

Comments
 (0)