snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
This commit is contained in:
905
weed/shell/command_fs_distribute_chunks.go
Normal file
905
weed/shell/command_fs_distribute_chunks.go
Normal file
@@ -0,0 +1,905 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"math"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||
"github.com/seaweedfs/seaweedfs/weed/operation"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/security"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
util_http "github.com/seaweedfs/seaweedfs/weed/util/http"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Commands = append(Commands, &commandFsDistributeChunks{})
|
||||
}
|
||||
|
||||
type commandFsDistributeChunks struct{}
|
||||
|
||||
func (c *commandFsDistributeChunks) Name() string {
|
||||
return "fs.distributeChunks"
|
||||
}
|
||||
|
||||
func (c *commandFsDistributeChunks) Help() string {
|
||||
return `redistribute file chunks evenly across volume server nodes
|
||||
|
||||
Modes:
|
||||
primary (default) balance chunk count per node using topology-based ownership.
|
||||
Note: with replication, ownership is derived from volume ID hashing
|
||||
(vid %% node count), which may not reflect the actual Assign target.
|
||||
Results can be approximate in replicated environments.
|
||||
|
||||
replica balance both ownership and replica copies across nodes.
|
||||
Step 1: owner-based balancing (same as primary mode).
|
||||
Step 2: additional moves to balance total copies (including replicas).
|
||||
Same ownership limitation as primary mode applies.
|
||||
|
||||
round-robin assign chunks to nodes by file offset order (chunk[0]->A, chunk[1]->B,
|
||||
chunk[2]->C, ...). Best mode for sequential read performance — ensures
|
||||
consecutive chunks are on different nodes for I/O pipelining.
|
||||
Recommended for replication environments as it does not depend on
|
||||
ownership calculation.
|
||||
|
||||
Files using chunk manifests (very large files) are handled by resolving manifests
|
||||
to the underlying data chunks, redistributing those, then re-manifestizing before
|
||||
updating the filer. Old manifest + data chunks are garbage-collected by the filer.
|
||||
|
||||
# analyze current distribution (dry-run, primary mode)
|
||||
fs.distributeChunks -path=/buckets/my-bucket/large-file.dat
|
||||
|
||||
# apply redistribution
|
||||
fs.distributeChunks -path=/buckets/my-bucket/large-file.dat -apply
|
||||
|
||||
# distribute across 5 nodes (instead of all)
|
||||
fs.distributeChunks -path=/buckets/my-bucket/large-file.dat -nodes=5 -apply
|
||||
|
||||
# balance including replica copies
|
||||
fs.distributeChunks -path=/buckets/my-bucket/large-file.dat -mode=replica -apply
|
||||
|
||||
# round-robin for sequential read performance (recommended with replication)
|
||||
fs.distributeChunks -path=/buckets/my-bucket/large-file.dat -mode=round-robin -apply
|
||||
`
|
||||
}
|
||||
|
||||
func (c *commandFsDistributeChunks) HasTag(CommandTag) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type chunkMove struct {
|
||||
chunkIndex int
|
||||
chunk *filer_pb.FileChunk
|
||||
fromNode string
|
||||
toNode string
|
||||
}
|
||||
|
||||
func (c *commandFsDistributeChunks) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) {
|
||||
|
||||
fsDistributeCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
|
||||
filePath := fsDistributeCommand.String("path", "", "file path to redistribute chunks")
|
||||
apply := fsDistributeCommand.Bool("apply", false, "apply the redistribution")
|
||||
targetNodeCount := fsDistributeCommand.Int("nodes", 0, "number of nodes to distribute across (0 = all nodes)")
|
||||
mode := fsDistributeCommand.String("mode", "primary", "distribution mode: primary, replica, round-robin")
|
||||
if err = fsDistributeCommand.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *filePath == "" {
|
||||
return fmt.Errorf("-path is required")
|
||||
}
|
||||
if *mode != "primary" && *mode != "replica" && *mode != "round-robin" {
|
||||
return fmt.Errorf("-mode must be one of: primary, replica, round-robin")
|
||||
}
|
||||
|
||||
infoAboutSimulationMode(writer, *apply, "-apply")
|
||||
|
||||
path, err := commandEnv.parseUrl(*filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir, name := util.FullPath(path).DirAndName()
|
||||
|
||||
entry, err := lookupFileEntry(commandEnv, dir, name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("lookup %s: %v", path, err)
|
||||
}
|
||||
|
||||
chunks, hadManifest, err := resolveEntryDataChunks(commandEnv, entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(chunks) == 0 {
|
||||
fmt.Fprintf(writer, "File has no chunks (possibly stored inline)\n")
|
||||
return nil
|
||||
}
|
||||
if hadManifest {
|
||||
fmt.Fprintf(writer, "Resolved chunk manifests: %d top-level chunk(s) -> %d data chunks.\n", len(entry.GetChunks()), len(chunks))
|
||||
}
|
||||
|
||||
volumeInfoMap, volumeNodesList, volumeToOwner, nodeList, err := collectVolumeTopology(commandEnv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(nodeList) < 2 {
|
||||
return fmt.Errorf("need at least 2 volume nodes, found %d", len(nodeList))
|
||||
}
|
||||
if *targetNodeCount < 0 {
|
||||
return fmt.Errorf("-nodes must be >= 0 (0 = all nodes)")
|
||||
}
|
||||
if *targetNodeCount > len(nodeList) {
|
||||
return fmt.Errorf("-nodes=%d exceeds available nodes (%d)", *targetNodeCount, len(nodeList))
|
||||
}
|
||||
|
||||
chunkToNode, ownerCount, copiesCount, err := buildDistributionCounts(chunks, volumeToOwner, volumeNodesList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
activeNodeList, activeSet := selectActiveNodes(nodeList, ownerCount, *targetNodeCount)
|
||||
|
||||
totalChunks := len(chunks)
|
||||
totalNodes := len(activeNodeList)
|
||||
totalCopies := 0
|
||||
for _, cnt := range copiesCount {
|
||||
totalCopies += cnt
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, "\nFile: %s\n", path)
|
||||
fmt.Fprintf(writer, "Total chunks: %d, Total nodes: %d, Mode: %s\n", totalChunks, totalNodes, *mode)
|
||||
printCurrentDistribution(writer, nodeList, ownerCount, copiesCount, activeSet, *targetNodeCount, *mode, totalChunks, totalNodes, totalCopies)
|
||||
|
||||
moves, ownerTarget := planDistribution(*mode, writer, activeNodeList, nodeList, activeSet, ownerCount, copiesCount, chunkToNode, chunks, volumeNodesList, totalChunks, totalNodes, totalCopies)
|
||||
|
||||
if len(moves) == 0 {
|
||||
fmt.Fprintf(writer, "\nAlready well distributed. No moves needed.\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
allRelevantNodes := relevantNodes(nodeList, activeSet, ownerCount)
|
||||
printRedistributionPlan(writer, allRelevantNodes, ownerCount, ownerTarget, len(moves))
|
||||
|
||||
if !*apply {
|
||||
fmt.Fprintf(writer, "\nTo apply, add -apply flag\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, "\nExecuting redistribution...\n")
|
||||
defer util_http.GetGlobalHttpClient().CloseIdleConnections()
|
||||
|
||||
movedCount, err := executeChunkMoves(commandEnv, writer, moves, volumeInfoMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if movedCount == 0 {
|
||||
fmt.Fprintf(writer, "\nNo chunks were moved successfully.\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
finalChunks := chunks
|
||||
if hadManifest {
|
||||
// no chunk deleter here: a failed fold reports the blobs it saved
|
||||
remanifested, manErr := filer.MaybeManifestize(newShellSaveAsChunk(commandEnv), nil, chunks)
|
||||
if manErr != nil {
|
||||
fmt.Fprintf(writer, "WARNING: re-manifestize failed: %v. Writing flat chunk list.\n", manErr)
|
||||
} else {
|
||||
finalChunks = remanifested
|
||||
fmt.Fprintf(writer, "Re-manifestized %d data chunks into %d top-level chunk(s).\n", len(chunks), len(finalChunks))
|
||||
}
|
||||
}
|
||||
entry.Chunks = finalChunks
|
||||
|
||||
fmt.Fprintf(writer, "\nUpdating filer metadata (%d chunks moved)...\n", movedCount)
|
||||
err = commandEnv.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
return filer_pb.UpdateEntry(context.Background(), client, &filer_pb.UpdateEntryRequest{
|
||||
Directory: dir,
|
||||
Entry: entry,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Fprintf(writer, "FAILED to update filer metadata: %v\n", err)
|
||||
fmt.Fprintf(writer, "WARNING: New chunks were uploaded but metadata was not updated.\n")
|
||||
fmt.Fprintf(writer, "The original file is still intact. New chunks are orphaned.\n")
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(writer, "Filer metadata updated successfully.\n")
|
||||
|
||||
fmt.Fprintf(writer, "\nRedistribution complete. %d chunks moved.\n", movedCount)
|
||||
return nil
|
||||
}
|
||||
|
||||
// lookupFileEntry fetches the filer entry for the given directory and name.
|
||||
func lookupFileEntry(commandEnv *CommandEnv, dir, name string) (*filer_pb.Entry, error) {
|
||||
var entry *filer_pb.Entry
|
||||
err := commandEnv.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
resp, err := filer_pb.LookupEntry(context.Background(), client, &filer_pb.LookupDirectoryEntryRequest{
|
||||
Directory: dir,
|
||||
Name: name,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entry = resp.Entry
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entry == nil {
|
||||
return nil, fmt.Errorf("lookup for %s/%s returned no entry", dir, name)
|
||||
}
|
||||
if entry.IsDirectory {
|
||||
return nil, fmt.Errorf("%s/%s is a directory", dir, name)
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// resolveEntryDataChunks returns the flattened data chunks of an entry. If the
|
||||
// entry contains manifest chunks, their contents are resolved. hadManifest
|
||||
// signals whether any manifest chunks were resolved, so the caller knows to
|
||||
// re-manifestize after redistribution.
|
||||
func resolveEntryDataChunks(commandEnv *CommandEnv, entry *filer_pb.Entry) (dataChunks []*filer_pb.FileChunk, hadManifest bool, err error) {
|
||||
chunks := entry.GetChunks()
|
||||
if !filer.HasChunkManifest(chunks) {
|
||||
return chunks, false, nil
|
||||
}
|
||||
dataChunks, _, err = filer.ResolveChunkManifest(context.Background(), filer.LookupFn(commandEnv), chunks, 0, math.MaxInt64, nil)
|
||||
if err != nil {
|
||||
return nil, true, fmt.Errorf("resolve chunk manifest: %v", err)
|
||||
}
|
||||
return dataChunks, true, nil
|
||||
}
|
||||
|
||||
// collectVolumeTopology queries the master for cluster topology and returns per-volume
|
||||
// metadata, per-volume node lists, per-volume ownership, and the sorted node list.
|
||||
func collectVolumeTopology(commandEnv *CommandEnv) (
|
||||
volumeInfoMap map[uint32]*master_pb.VolumeInformationMessage,
|
||||
volumeNodesList map[uint32][]string,
|
||||
volumeToOwner map[uint32]string,
|
||||
nodeList []string,
|
||||
err error,
|
||||
) {
|
||||
topoInfo, _, err := collectTopologyInfo(commandEnv, 0)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("collect topology: %v", err)
|
||||
}
|
||||
|
||||
volumeInfoMap = make(map[uint32]*master_pb.VolumeInformationMessage)
|
||||
volumeNodesList = make(map[uint32][]string)
|
||||
allNodes := make(map[string]bool)
|
||||
|
||||
for _, dc := range topoInfo.GetDataCenterInfos() {
|
||||
for _, rack := range dc.GetRackInfos() {
|
||||
for _, dn := range rack.GetDataNodeInfos() {
|
||||
nodeId := dn.GetId()
|
||||
allNodes[nodeId] = true
|
||||
for _, disk := range dn.GetDiskInfos() {
|
||||
for _, vol := range disk.GetVolumeInfos() {
|
||||
volumeNodesList[vol.GetId()] = append(volumeNodesList[vol.GetId()], nodeId)
|
||||
if _, exists := volumeInfoMap[vol.GetId()]; !exists {
|
||||
volumeInfoMap[vol.GetId()] = vol
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
volumeToOwner = make(map[uint32]string)
|
||||
for vid, nodes := range volumeNodesList {
|
||||
sort.Strings(nodes)
|
||||
volumeToOwner[vid] = nodes[int(vid)%len(nodes)]
|
||||
}
|
||||
|
||||
nodeList = make([]string, 0, len(allNodes))
|
||||
for node := range allNodes {
|
||||
nodeList = append(nodeList, node)
|
||||
}
|
||||
sort.Strings(nodeList)
|
||||
|
||||
return volumeInfoMap, volumeNodesList, volumeToOwner, nodeList, nil
|
||||
}
|
||||
|
||||
// buildDistributionCounts maps each chunk to its owning node and tallies owner/copy counts.
|
||||
func buildDistributionCounts(
|
||||
chunks []*filer_pb.FileChunk,
|
||||
volumeToOwner map[uint32]string,
|
||||
volumeNodesList map[uint32][]string,
|
||||
) (chunkToNode map[int]string, ownerCount map[string]int, copiesCount map[string]int, err error) {
|
||||
chunkToNode = make(map[int]string)
|
||||
ownerCount = make(map[string]int)
|
||||
copiesCount = make(map[string]int)
|
||||
|
||||
for i, chunk := range chunks {
|
||||
var vid uint32
|
||||
if chunk.Fid == nil {
|
||||
fileId, parseErr := needle.ParseFileIdFromString(chunk.GetFileIdString())
|
||||
if parseErr != nil {
|
||||
return nil, nil, nil, fmt.Errorf("failed to parse file id for chunk %d: %v", i, parseErr)
|
||||
}
|
||||
vid = uint32(fileId.VolumeId)
|
||||
} else {
|
||||
vid = chunk.Fid.GetVolumeId()
|
||||
}
|
||||
owner, ok := volumeToOwner[vid]
|
||||
if !ok {
|
||||
return nil, nil, nil, fmt.Errorf("volume %d not found in topology", vid)
|
||||
}
|
||||
chunkToNode[i] = owner
|
||||
ownerCount[owner]++
|
||||
for _, node := range volumeNodesList[vid] {
|
||||
copiesCount[node]++
|
||||
}
|
||||
}
|
||||
return chunkToNode, ownerCount, copiesCount, nil
|
||||
}
|
||||
|
||||
// selectActiveNodes picks up to targetNodeCount nodes to participate in redistribution.
|
||||
// When targetNodeCount is 0 all nodes are active.
|
||||
func selectActiveNodes(nodeList []string, ownerCount map[string]int, targetNodeCount int) (activeNodeList []string, activeSet map[string]bool) {
|
||||
activeNodeList = nodeList
|
||||
if targetNodeCount > 0 && targetNodeCount < len(nodeList) {
|
||||
var withChunks, withoutChunks []string
|
||||
for _, node := range nodeList {
|
||||
if ownerCount[node] > 0 {
|
||||
withChunks = append(withChunks, node)
|
||||
} else {
|
||||
withoutChunks = append(withoutChunks, node)
|
||||
}
|
||||
}
|
||||
n := targetNodeCount
|
||||
if len(withChunks) >= n {
|
||||
sort.Slice(withChunks, func(i, j int) bool {
|
||||
return ownerCount[withChunks[i]] > ownerCount[withChunks[j]]
|
||||
})
|
||||
activeNodeList = withChunks[:n]
|
||||
} else {
|
||||
activeNodeList = append(withChunks, withoutChunks[:n-len(withChunks)]...)
|
||||
}
|
||||
sort.Strings(activeNodeList)
|
||||
}
|
||||
|
||||
activeSet = make(map[string]bool, len(activeNodeList))
|
||||
for _, node := range activeNodeList {
|
||||
activeSet[node] = true
|
||||
}
|
||||
return activeNodeList, activeSet
|
||||
}
|
||||
|
||||
// printCurrentDistribution writes the per-node chunk/copy table to writer.
|
||||
func printCurrentDistribution(
|
||||
writer io.Writer,
|
||||
nodeList []string,
|
||||
ownerCount, copiesCount map[string]int,
|
||||
activeSet map[string]bool,
|
||||
targetNodeCount int,
|
||||
mode string,
|
||||
totalChunks, totalNodes, totalCopies int,
|
||||
) {
|
||||
maxOwner := 0
|
||||
for _, cnt := range ownerCount {
|
||||
if cnt > maxOwner {
|
||||
maxOwner = cnt
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, "\nCurrent distribution:\n")
|
||||
for _, node := range nodeList {
|
||||
oc := ownerCount[node]
|
||||
cc := copiesCount[node]
|
||||
bar := strings.Repeat("#", int(math.Ceil(float64(oc)*30/math.Max(float64(maxOwner), 1))))
|
||||
marker := " "
|
||||
if targetNodeCount > 0 && !activeSet[node] {
|
||||
marker = "-"
|
||||
}
|
||||
if mode == "replica" {
|
||||
fmt.Fprintf(writer, " %s %-20s %4d chunks %4d copies %-30s\n", marker, shortName(node), oc, cc, bar)
|
||||
} else {
|
||||
fmt.Fprintf(writer, " %s %-20s %4d chunks %-30s\n", marker, shortName(node), oc, bar)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(writer, " Ideal: ~%.0f chunks per node", math.Ceil(float64(totalChunks)/float64(totalNodes)))
|
||||
if mode == "replica" {
|
||||
fmt.Fprintf(writer, ", ~%.0f copies per node", math.Ceil(float64(totalCopies)/float64(totalNodes)))
|
||||
}
|
||||
fmt.Fprintln(writer)
|
||||
}
|
||||
|
||||
// planDistribution computes the set of chunk moves required for the chosen mode.
|
||||
func planDistribution(
|
||||
mode string,
|
||||
writer io.Writer,
|
||||
activeNodeList, nodeList []string,
|
||||
activeSet map[string]bool,
|
||||
ownerCount, copiesCount map[string]int,
|
||||
chunkToNode map[int]string,
|
||||
chunks []*filer_pb.FileChunk,
|
||||
volumeNodesList map[uint32][]string,
|
||||
totalChunks, totalNodes, totalCopies int,
|
||||
) (moves []chunkMove, ownerTarget map[string]int) {
|
||||
type nodeExcess struct {
|
||||
node string
|
||||
excess int
|
||||
}
|
||||
|
||||
allRelevantNodes := relevantNodes(nodeList, activeSet, ownerCount)
|
||||
ownerTarget = make(map[string]int)
|
||||
|
||||
switch mode {
|
||||
case "primary":
|
||||
ownerTarget = computeOwnerTarget(activeNodeList, nodeList, activeSet, totalChunks, totalNodes)
|
||||
moves = planOwnerMoves(ownerCount, ownerTarget, chunkToNode, chunks, allRelevantNodes)
|
||||
|
||||
case "replica":
|
||||
ownerTarget = computeOwnerTarget(activeNodeList, nodeList, activeSet, totalChunks, totalNodes)
|
||||
moves = planOwnerMoves(ownerCount, ownerTarget, chunkToNode, chunks, allRelevantNodes)
|
||||
|
||||
copiesMin := totalCopies / totalNodes
|
||||
copiesRemainder := totalCopies % totalNodes
|
||||
copiesTarget := make(map[string]int)
|
||||
for i, node := range activeNodeList {
|
||||
if i < copiesRemainder {
|
||||
copiesTarget[node] = copiesMin + 1
|
||||
} else {
|
||||
copiesTarget[node] = copiesMin
|
||||
}
|
||||
}
|
||||
|
||||
alreadyMoving := make(map[int]bool)
|
||||
for _, mv := range moves {
|
||||
alreadyMoving[mv.chunkIndex] = true
|
||||
}
|
||||
|
||||
// local copy so the planner does not mutate the caller's copiesCount
|
||||
localCopiesCount := maps.Clone(copiesCount)
|
||||
|
||||
var copiesOver, copiesUnder []nodeExcess
|
||||
for _, node := range activeNodeList {
|
||||
diff := localCopiesCount[node] - copiesTarget[node]
|
||||
if diff > 0 {
|
||||
copiesOver = append(copiesOver, nodeExcess{node, diff})
|
||||
} else if diff < 0 {
|
||||
copiesUnder = append(copiesUnder, nodeExcess{node, -diff})
|
||||
}
|
||||
}
|
||||
sort.Slice(copiesOver, func(i, j int) bool {
|
||||
return copiesOver[i].excess > copiesOver[j].excess
|
||||
})
|
||||
|
||||
for oi := range copiesOver {
|
||||
over := &copiesOver[oi]
|
||||
if over.excess <= 0 {
|
||||
continue
|
||||
}
|
||||
for idx, chunk := range chunks {
|
||||
if over.excess <= 0 {
|
||||
break
|
||||
}
|
||||
if alreadyMoving[idx] {
|
||||
continue
|
||||
}
|
||||
vid, vidErr := chunkVolumeId(chunk)
|
||||
if vidErr != nil {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(volumeNodesList[vid], over.node) {
|
||||
continue
|
||||
}
|
||||
sort.Slice(copiesUnder, func(i, j int) bool {
|
||||
return copiesUnder[i].excess > copiesUnder[j].excess
|
||||
})
|
||||
for j := range copiesUnder {
|
||||
if copiesUnder[j].excess > 0 {
|
||||
moves = append(moves, chunkMove{
|
||||
chunkIndex: idx,
|
||||
chunk: chunk,
|
||||
fromNode: chunkToNode[idx],
|
||||
toNode: copiesUnder[j].node,
|
||||
})
|
||||
alreadyMoving[idx] = true
|
||||
for _, srcNode := range volumeNodesList[vid] {
|
||||
localCopiesCount[srcNode]--
|
||||
for k := range copiesOver {
|
||||
if copiesOver[k].node == srcNode {
|
||||
copiesOver[k].excess--
|
||||
}
|
||||
}
|
||||
}
|
||||
copiesUnder[j].excess--
|
||||
localCopiesCount[copiesUnder[j].node]++
|
||||
over.excess--
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case "round-robin":
|
||||
type chunkWithIndex struct {
|
||||
index int
|
||||
offset int64
|
||||
}
|
||||
sortedChunks := make([]chunkWithIndex, totalChunks)
|
||||
for i, chunk := range chunks {
|
||||
sortedChunks[i] = chunkWithIndex{index: i, offset: chunk.GetOffset()}
|
||||
}
|
||||
sort.Slice(sortedChunks, func(i, j int) bool {
|
||||
return sortedChunks[i].offset < sortedChunks[j].offset
|
||||
})
|
||||
|
||||
for i, sc := range sortedChunks {
|
||||
targetNode := activeNodeList[i%totalNodes]
|
||||
currentOwner := chunkToNode[sc.index]
|
||||
ownerTarget[targetNode]++
|
||||
if currentOwner != targetNode {
|
||||
moves = append(moves, chunkMove{
|
||||
chunkIndex: sc.index,
|
||||
chunk: chunks[sc.index],
|
||||
fromNode: currentOwner,
|
||||
toNode: targetNode,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, "\nRound-robin assignment (sequential read optimized):\n")
|
||||
fmt.Fprintf(writer, " First assignments: ")
|
||||
for pos := range sortedChunks {
|
||||
if pos >= 12 {
|
||||
fmt.Fprintf(writer, "... ")
|
||||
break
|
||||
}
|
||||
fmt.Fprintf(writer, "%s ", shortName(activeNodeList[pos%totalNodes]))
|
||||
}
|
||||
fmt.Fprintf(writer, "\n Pattern: ")
|
||||
for i := 0; i < totalNodes && i < 6; i++ {
|
||||
fmt.Fprintf(writer, "%s ", shortName(activeNodeList[i]))
|
||||
}
|
||||
if totalNodes > 6 {
|
||||
fmt.Fprintf(writer, "...")
|
||||
}
|
||||
fmt.Fprintf(writer, "(repeating)\n")
|
||||
}
|
||||
|
||||
return moves, ownerTarget
|
||||
}
|
||||
|
||||
// printRedistributionPlan writes the before/after chunk count table for each node.
|
||||
func printRedistributionPlan(writer io.Writer, allRelevantNodes []string, ownerCount, ownerTarget map[string]int, numMoves int) {
|
||||
fmt.Fprintf(writer, "\nRedistribution plan: %d chunks to move\n", numMoves)
|
||||
for _, node := range allRelevantNodes {
|
||||
current := ownerCount[node]
|
||||
target := ownerTarget[node]
|
||||
diff := target - current
|
||||
sign := ""
|
||||
if diff > 0 {
|
||||
sign = "+"
|
||||
}
|
||||
fmt.Fprintf(writer, " %-25s %4d -> %4d (%s%d)\n", shortName(node), current, target, sign, diff)
|
||||
}
|
||||
}
|
||||
|
||||
// relevantNodes returns nodes that are either active or currently hold chunks.
|
||||
func relevantNodes(nodeList []string, activeSet map[string]bool, ownerCount map[string]int) []string {
|
||||
relevant := make([]string, 0, len(nodeList))
|
||||
for _, node := range nodeList {
|
||||
if activeSet[node] || ownerCount[node] > 0 {
|
||||
relevant = append(relevant, node)
|
||||
}
|
||||
}
|
||||
return relevant
|
||||
}
|
||||
|
||||
// chunkVolumeId extracts the volume ID from a chunk, falling back to parsing
|
||||
// the legacy FileId string when Fid is nil.
|
||||
func chunkVolumeId(chunk *filer_pb.FileChunk) (uint32, error) {
|
||||
if chunk.Fid != nil {
|
||||
return chunk.Fid.GetVolumeId(), nil
|
||||
}
|
||||
fileId, err := needle.ParseFileIdFromString(chunk.GetFileIdString())
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse file id %q: %v", chunk.GetFileIdString(), err)
|
||||
}
|
||||
return uint32(fileId.VolumeId), nil
|
||||
}
|
||||
|
||||
// shortName returns the hostname portion (before the first ".") of a node ID.
|
||||
func shortName(nodeID string) string {
|
||||
return strings.Split(nodeID, ".")[0]
|
||||
}
|
||||
|
||||
// computeOwnerTarget returns the target chunk count per node for owner-based balancing.
|
||||
func computeOwnerTarget(activeNodeList, nodeList []string, activeSet map[string]bool, totalChunks, totalNodes int) map[string]int {
|
||||
target := make(map[string]int)
|
||||
perNode := totalChunks / totalNodes
|
||||
remainder := totalChunks % totalNodes
|
||||
for i, node := range activeNodeList {
|
||||
if i < remainder {
|
||||
target[node] = perNode + 1
|
||||
} else {
|
||||
target[node] = perNode
|
||||
}
|
||||
}
|
||||
for _, node := range nodeList {
|
||||
if !activeSet[node] {
|
||||
target[node] = 0
|
||||
}
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
// executeChunkMoves runs all moves concurrently and returns the number of successfully moved chunks.
|
||||
// Old chunk cleanup is handled automatically by the filer: filer.UpdateEntry calls
|
||||
// deleteChunksIfNotNew which removes chunks present in the old entry but not the new one.
|
||||
func executeChunkMoves(
|
||||
commandEnv *CommandEnv,
|
||||
writer io.Writer,
|
||||
moves []chunkMove,
|
||||
volumeInfoMap map[uint32]*master_pb.VolumeInformationMessage,
|
||||
) (int, error) {
|
||||
uploader, err := operation.NewUploader()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("create uploader: %v", err)
|
||||
}
|
||||
|
||||
var mu sync.Mutex
|
||||
movedCount := 0
|
||||
ewg := NewErrorWaitGroup(DefaultMaxParallelization)
|
||||
|
||||
for i, mv := range moves {
|
||||
ewg.Add(func() error {
|
||||
chunk := mv.chunk
|
||||
oldFidStr := chunk.GetFileIdString()
|
||||
prefix := fmt.Sprintf(" [%d/%d] Moving %s from %s to %s",
|
||||
i+1, len(moves), oldFidStr,
|
||||
shortName(mv.fromNode), shortName(mv.toNode),
|
||||
)
|
||||
|
||||
fail := func(msg string) {
|
||||
mu.Lock()
|
||||
fmt.Fprintf(writer, "%s ... FAILED (%s)\n", prefix, msg)
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
oldFid, parseErr := needle.ParseFileIdFromString(oldFidStr)
|
||||
if parseErr != nil {
|
||||
fail(fmt.Sprintf("parse fid: %v", parseErr))
|
||||
return nil
|
||||
}
|
||||
|
||||
downloadURLs, lookupErr := commandEnv.MasterClient.LookupVolumeServerUrl(oldFid.VolumeId.String())
|
||||
if lookupErr != nil || len(downloadURLs) == 0 {
|
||||
fail(fmt.Sprintf("lookup source: %v", lookupErr))
|
||||
return nil
|
||||
}
|
||||
|
||||
// dlCancel must be called after reader.Close() — the response body is
|
||||
// streamed into the uploader, so cancelling the context before the
|
||||
// body is fully consumed causes "context canceled" upload failures.
|
||||
dlCtx, dlCancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
var resp *http.Response
|
||||
var reader io.ReadCloser
|
||||
var readErr error
|
||||
readJwt := filer.JwtForVolumeServer(oldFidStr)
|
||||
for _, serverURL := range downloadURLs {
|
||||
var dlReq *http.Request
|
||||
dlReq, readErr = http.NewRequestWithContext(dlCtx, http.MethodGet, fmt.Sprintf("http://%s/%s", serverURL, oldFidStr), nil)
|
||||
if readErr != nil {
|
||||
continue
|
||||
}
|
||||
dlReq.Header.Add("Accept-Encoding", "gzip")
|
||||
if readJwt != "" {
|
||||
dlReq.Header.Set("Authorization", security.BearerPrefix+readJwt)
|
||||
}
|
||||
resp, readErr = util_http.GetGlobalHttpClient().Do(dlReq)
|
||||
if readErr == nil && resp.StatusCode >= 400 {
|
||||
util_http.CloseResponse(resp)
|
||||
readErr = fmt.Errorf("download %s: %s", serverURL, resp.Status)
|
||||
}
|
||||
if readErr == nil {
|
||||
reader = resp.Body
|
||||
break
|
||||
}
|
||||
}
|
||||
if readErr != nil {
|
||||
dlCancel()
|
||||
fail(fmt.Sprintf("download: %v", readErr))
|
||||
return nil
|
||||
}
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
isCompressed := resp.Header.Get("Content-Encoding") == "gzip"
|
||||
md5Hash := resp.Header.Get("Content-MD5")
|
||||
var filename string
|
||||
if cd := resp.Header.Get("Content-Disposition"); len(cd) > 0 {
|
||||
if _, after, found := strings.Cut(cd, "filename="); found {
|
||||
filename = strings.Trim(after, "\"")
|
||||
}
|
||||
}
|
||||
|
||||
// Buffer the full response body so the download context can be
|
||||
// cancelled before the upload starts — otherwise the 5-minute
|
||||
// dlCtx timeout would also cancel the (potentially slow) upload.
|
||||
bodyBytes, readBodyErr := io.ReadAll(reader)
|
||||
reader.Close()
|
||||
util_http.CloseResponse(resp)
|
||||
dlCancel()
|
||||
if readBodyErr != nil {
|
||||
fail(fmt.Sprintf("read body: %v", readBodyErr))
|
||||
return nil
|
||||
}
|
||||
|
||||
var replication, collection string
|
||||
if vInfo, ok := volumeInfoMap[uint32(oldFid.VolumeId)]; ok {
|
||||
replication = fmt.Sprintf("%03d", vInfo.GetReplicaPlacement())
|
||||
collection = vInfo.GetCollection()
|
||||
}
|
||||
|
||||
assignResult, assignErr := operation.Assign(context.Background(), commandEnv.MasterClient.GetMaster, commandEnv.option.GrpcDialOption,
|
||||
&operation.VolumeAssignRequest{
|
||||
Count: 1,
|
||||
Replication: replication,
|
||||
Collection: collection,
|
||||
DataNode: mv.toNode,
|
||||
})
|
||||
if assignErr != nil {
|
||||
fail(fmt.Sprintf("assign: %v", assignErr))
|
||||
return nil
|
||||
}
|
||||
|
||||
uploadURL := fmt.Sprintf("http://%s/%s", assignResult.Url, assignResult.Fid)
|
||||
|
||||
var jwt security.EncodedJwt
|
||||
if assignResult.Auth != "" {
|
||||
jwt = assignResult.Auth
|
||||
} else {
|
||||
v := util.GetViper()
|
||||
signingKey := v.GetString("jwt.signing.key")
|
||||
if signingKey != "" {
|
||||
expiresAfterSec := v.GetInt("jwt.signing.expires_after_seconds")
|
||||
jwt = security.GenJwtForVolumeServer(security.SigningKey(signingKey), expiresAfterSec, assignResult.Fid)
|
||||
}
|
||||
}
|
||||
|
||||
_, uploadErr, _ := uploader.Upload(context.Background(), bytes.NewReader(bodyBytes), &operation.UploadOption{
|
||||
UploadUrl: uploadURL,
|
||||
Filename: filename,
|
||||
IsInputCompressed: isCompressed,
|
||||
Cipher: false,
|
||||
MimeType: contentType,
|
||||
Md5: md5Hash,
|
||||
Jwt: jwt,
|
||||
})
|
||||
|
||||
if uploadErr != nil {
|
||||
fail(fmt.Sprintf("upload: %v", uploadErr))
|
||||
return nil
|
||||
}
|
||||
|
||||
newFid, fidParseErr := needle.ParseFileIdFromString(assignResult.Fid)
|
||||
if fidParseErr != nil {
|
||||
fail(fmt.Sprintf("parse new fid: %v", fidParseErr))
|
||||
return nil
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
movedCount++
|
||||
chunk.Fid = &filer_pb.FileId{
|
||||
VolumeId: uint32(newFid.VolumeId),
|
||||
FileKey: uint64(newFid.Key),
|
||||
Cookie: uint32(newFid.Cookie),
|
||||
}
|
||||
chunk.FileId = ""
|
||||
fmt.Fprintf(writer, "%s ... OK -> %s\n", prefix, assignResult.Fid)
|
||||
mu.Unlock()
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
_ = ewg.Wait()
|
||||
return movedCount, nil
|
||||
}
|
||||
|
||||
// newShellSaveAsChunk returns a SaveDataAsChunk function for use with
|
||||
// filer.MaybeManifestize from shell context. The master selects placement —
|
||||
// manifest chunks are metadata and don't need fan-out distribution.
|
||||
func newShellSaveAsChunk(commandEnv *CommandEnv) filer.SaveDataAsChunkFunctionType {
|
||||
return filer.SaveDataAsChunkFunctionType(func(reader io.Reader, name string, offset int64, tsNs int64, _ uint64) (*filer_pb.FileChunk, error) {
|
||||
uploader, err := operation.NewUploader()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create uploader: %v", err)
|
||||
}
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read manifest data: %v", err)
|
||||
}
|
||||
assignResult, err := operation.Assign(context.Background(), commandEnv.MasterClient.GetMaster, commandEnv.option.GrpcDialOption,
|
||||
&operation.VolumeAssignRequest{Count: 1})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("assign manifest chunk: %v", err)
|
||||
}
|
||||
var jwt security.EncodedJwt
|
||||
if assignResult.Auth != "" {
|
||||
jwt = assignResult.Auth
|
||||
} else {
|
||||
v := util.GetViper()
|
||||
signingKey := v.GetString("jwt.signing.key")
|
||||
if signingKey != "" {
|
||||
expiresAfterSec := v.GetInt("jwt.signing.expires_after_seconds")
|
||||
jwt = security.GenJwtForVolumeServer(security.SigningKey(signingKey), expiresAfterSec, assignResult.Fid)
|
||||
}
|
||||
}
|
||||
uploadResult, uploadErr, _ := uploader.Upload(context.Background(), bytes.NewReader(data), &operation.UploadOption{
|
||||
UploadUrl: fmt.Sprintf("http://%s/%s", assignResult.Url, assignResult.Fid),
|
||||
Filename: name,
|
||||
Jwt: jwt,
|
||||
})
|
||||
if uploadErr != nil {
|
||||
return nil, fmt.Errorf("upload manifest chunk: %v", uploadErr)
|
||||
}
|
||||
if uploadResult.Error != "" {
|
||||
return nil, fmt.Errorf("upload manifest chunk: %s", uploadResult.Error)
|
||||
}
|
||||
return uploadResult.ToPbFileChunk(assignResult.Fid, offset, tsNs), nil
|
||||
})
|
||||
}
|
||||
|
||||
// planOwnerMoves generates moves to balance owner-based chunk distribution.
|
||||
func planOwnerMoves(ownerCount, ownerTarget map[string]int, chunkToNode map[int]string, chunks []*filer_pb.FileChunk, allRelevantNodes []string) []chunkMove {
|
||||
type nodeExcess struct {
|
||||
node string
|
||||
excess int
|
||||
}
|
||||
var overNodes, underNodes []nodeExcess
|
||||
for _, node := range allRelevantNodes {
|
||||
diff := ownerCount[node] - ownerTarget[node]
|
||||
if diff > 0 {
|
||||
overNodes = append(overNodes, nodeExcess{node, diff})
|
||||
} else if diff < 0 {
|
||||
underNodes = append(underNodes, nodeExcess{node, -diff})
|
||||
}
|
||||
}
|
||||
|
||||
var moves []chunkMove
|
||||
for _, over := range overNodes {
|
||||
toMove := over.excess
|
||||
var nodeChunks []int
|
||||
for i, node := range chunkToNode {
|
||||
if node == over.node {
|
||||
nodeChunks = append(nodeChunks, i)
|
||||
}
|
||||
}
|
||||
moved := 0
|
||||
for _, idx := range nodeChunks {
|
||||
if moved >= toMove {
|
||||
break
|
||||
}
|
||||
for j := range underNodes {
|
||||
if underNodes[j].excess > 0 {
|
||||
moves = append(moves, chunkMove{
|
||||
chunkIndex: idx,
|
||||
chunk: chunks[idx],
|
||||
fromNode: over.node,
|
||||
toNode: underNodes[j].node,
|
||||
})
|
||||
underNodes[j].excess--
|
||||
moved++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return moves
|
||||
}
|
||||
Reference in New Issue
Block a user