Files
seaweedfs/weed/ec/ec_move_runner.go
T
dc03c3b1f7 shell: parallel moves and a per-run limit for volumeServer.evacuate (#12561)
* shell: parallel moves and a per-run limit for volumeServer.evacuate

-maxParallelization runs up to N EC shard moves at once (default 1), with the
ec.balance scheduler: never two shards of one volume at a time, free workers
go to the least busy targets. The moves are planned first with the same target
choice as before, then applied through volume_move.Mover.MoveEcShards, which
verifies the target registered a shard before deleting it on the source.

-movesPerExec limits how many volumes and EC shards one run moves (default 0
for unlimited), like -volumesPerExec in volume.balance, so a big server can be
emptied in several runs.

With -skipNonMoveable a shard that fails to move (a read error on a failing
disk, for example) no longer drops the other shards of its volume; they are
moved and the failures are listed at the end. Without it the run stops at the
first failure as before. Losing the admin lock stops the run in either mode.

Refs #12501

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shell: plan evacuation again after failed moves; fail on a lost lock

A failed shard move kept the target slot it was planned on, so a readable
shard that found no destination in the plan stayed on the server although
the room was never used. Failed moves now give their targets back to the
model and the shards without a destination are planned again, up to
maxEcEvacuationRounds times; the failed shards are not tried again.

With -skipNonMoveable a lost admin lock was printed as a skipped failure and
the run returned success. ec.ErrLockLost is now exported and always fails
the run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shell: volumeServer.evacuate reports -movesPerExec after an applied EC run

The round loop only checked the limit after a failed move, so an applied run
that stopped at -movesPerExec without failures never said to run it again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shell: volumeServer.evacuate plans again for as long as failed moves leave room

The ten-round cap left readable shards on the server once more than ten
rounds of unreadable shards had taken the free room first, and a rerun
fails the same shards first again. Every round moves or fails each shard it
plans and a failed shard is not planned again, so the rounds end without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shell: volumeServer.evacuate gives a failed move back every slot it took

When the target already held the shard in another disk type, adding it to
the planned disk type still took a slot, but the undo gave back only the
explicit one. Record the slots the plan took and restore exactly those.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ec: an evacuation move that landed on its target keeps the target slot

A move can fail after the target registered the shard, when only the source
unmount or delete failed. Evacuate then gave the target's slot back and could
plan another shard onto a full server. The runner now checks the target after
a failed move and leaves a shard the target has out of FailedMoves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shell: volumeServer.evacuate gives back the slot of a failed move onto a shard the target had

A target that held the shard before the plan reports it after any failed
move, so the runner took such a move for one that landed and evacuate kept
the slot the plan had taken. The runner now lists those moves in
LandedMoves, and evacuate undoes the ones whose target had the shard, then
plans again with the room they gave back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
2026-11-11 10:18:51 -08:00

142 lines
5.2 KiB
Go

package ec
import (
"context"
"errors"
"fmt"
"io"
"sync"
"sync/atomic"
"github.com/seaweedfs/seaweedfs/weed/operation/volume_move"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding/ecbalancer"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
)
// ErrLockLost stops every move that has not started, whatever stopOnError says,
// and fails the whole run.
var ErrLockLost = errors.New("lock is lost")
// errShardOnTarget marks a failed move whose target registered the shard: only
// the source cleanup failed, so the shard takes a slot on the target.
var errShardOnTarget = errors.New("the target has the shard")
// EcShardMovesResult counts what RunEcShardMoves did with the planned moves.
type EcShardMovesResult struct {
Moved int
Failed int
NotStarted int
// FailedMoves are the failed moves whose target did not register the shard;
// their shards are still on the source.
FailedMoves []ecbalancer.Move
// LandedMoves are the failed moves whose target has the shard.
LandedMoves []ecbalancer.Move
}
// RunEcShardMoves applies planned shard moves, up to maxParallelization at once,
// scheduled like ec.balance moves (runMovesSpreadingTargets): never two moves of
// one volume at a time, and the free workers go to the least busy targets. Each
// move copies the shard, verifies the target registered it and only then deletes
// it on the source (volume_move.Mover.MoveEcShards). The in-memory model is not
// touched, so the caller updates it while planning.
//
// With stopOnError no new move starts after the first failure (moves already
// running finish). Otherwise every other move still runs, including the other
// shards of the failed move's volume: unlike ec.balance moves, evacuation moves
// all leave one server for other ones, so no move waits for a slot another frees.
// Losing the admin lock stops the moves that have not started in either case.
// The errors of the failed moves are joined.
func RunEcShardMoves(env *Env, nodes []*EcNode, moves []ecbalancer.Move, maxParallelization int, ioBytePerSecond int64, stopOnError bool, writer io.Writer) (EcShardMovesResult, error) {
byID := make(map[string]*EcNode, len(nodes))
for _, en := range nodes {
byID[en.Info.Id] = en
}
out := &lockedWriter{w: writer}
mover := volume_move.NewMover(env.GrpcDialOption)
return runEcShardMoves(moves, maxParallelization, stopOnError, func(m ecbalancer.Move) error {
if !env.isLocked() {
return ErrLockLost
}
src, dst := byID[m.SourceNode], byID[m.TargetNode]
if src == nil || dst == nil {
return fmt.Errorf("move ec shard %d.%d %s => %s: unknown volume server", m.VolumeID, m.ShardID, m.SourceNode, m.TargetNode)
}
move := volume_move.EcShardMove{
VolumeId: needle.VolumeId(m.VolumeID),
Collection: m.Collection,
ShardIds: []erasure_coding.ShardId{erasure_coding.ShardId(m.ShardID)},
Source: pb.NewServerAddressFromDataNode(src.Info),
Target: pb.NewServerAddressFromDataNode(dst.Info),
TargetDisk: m.TargetDisk,
}
err := mover.MoveEcShards(context.Background(), move, volume_move.EcMoveOptions{IoBytePerSecond: ioBytePerSecond, Writer: out})
if err != nil {
if mover.VerifyEcShards(context.Background(), move.VolumeId, move.Target, move.ShardIds) == nil {
err = fmt.Errorf("%w: %w", errShardOnTarget, err)
}
return fmt.Errorf("move ec shard %d.%d %s => %s: %w", m.VolumeID, m.ShardID, m.SourceNode, m.TargetNode, err)
}
fmt.Fprintf(out, "moved ec shard %d.%d %s => %s\n", m.VolumeID, m.ShardID, m.SourceNode, m.TargetNode)
return nil
})
}
// runEcShardMoves is RunEcShardMoves with the move itself passed in.
func runEcShardMoves(moves []ecbalancer.Move, maxParallelization int, stopOnError bool, move func(ecbalancer.Move) error) (EcShardMovesResult, error) {
var stopped atomic.Bool
var moved, failed atomic.Int64
var mu sync.Mutex
var errs []error
var failedMoves, landedMoves []ecbalancer.Move
err := runMovesSpreadingTargets(maxParallelization, moves, func(m ecbalancer.Move) error {
if stopped.Load() {
return nil
}
if err := move(m); err != nil {
failed.Add(1)
mu.Lock()
if errors.Is(err, errShardOnTarget) {
landedMoves = append(landedMoves, m)
} else {
failedMoves = append(failedMoves, m)
}
mu.Unlock()
if stopOnError || errors.Is(err, ErrLockLost) {
stopped.Store(true)
return err
}
// Not returned, so the runner keeps the rest of this volume's moves.
mu.Lock()
errs = append(errs, err)
mu.Unlock()
return nil
}
moved.Add(1)
return nil
})
result := EcShardMovesResult{
Moved: int(moved.Load()),
Failed: int(failed.Load()),
FailedMoves: failedMoves,
LandedMoves: landedMoves,
}
// Moves skipped after a stop, and moves dropped together with a failed move
// of their volume, did not run.
result.NotStarted = len(moves) - result.Moved - result.Failed
return result, errors.Join(append(errs, err)...)
}
// lockedWriter serializes the progress lines of concurrent moves.
type lockedWriter struct {
mu sync.Mutex
w io.Writer
}
func (lw *lockedWriter) Write(p []byte) (int, error) {
lw.mu.Lock()
defer lw.mu.Unlock()
return lw.w.Write(p)
}