Files
seaweedfs/weed/server/volume_grpc_client_to_master.go
T
Chris Lu ce7d388639 heartbeat: send only the volumes that changed (#10640)
* pb: let a heartbeat carry only the volumes that changed

A partial list cannot travel in volumes: a master that did not understand it
would read the absences as deletions. So changes get their own field, used only
once the master has said it compares digests and can tell when it has fallen
behind.

* master: apply the volumes a heartbeat reports as changed

Only the named volumes are touched. A full report says the server holds exactly
these; a changed report says nothing about the ones it leaves out, so absence
must not read as removal.

Also advertises that the master compares digests, which is what lets a server
stop sending its whole list. Advertising it once per connection means a server
reconnecting to a master that does not is back to full lists straight away.

* volume: send only the volumes that changed once the master accepts them

The whole list goes on every heartbeat until the master says it compares
digests, and again whenever it asks, so a master that cannot tell when it has
fallen behind never has to.

has_no_volumes stays derived from a full list alone. Deriving it from what a
heartbeat happens to carry would make a quiet one read as a server that had
lost every volume, and the master would drop them all.

The digest still covers every volume held rather than the ones sent, which is
what lets the master confirm that applying the changes left it current.

Reporting state is per-connection: a server that reconnects, or reaches a
different master, starts again from the full list.

* volume: let the zero reporting state stand for having told no master anything

A Store built as a literal, which tests do, left the reporting state nil and
panicked on the first heartbeat. As a value its zero form already means nothing
has been reported to anyone, which is exactly the state that sends the whole
list.

* rust: send only the volumes that changed once the master accepts them

Mirrors the Go volume server, with one hazard the Go side does not have: mount
and unmount deltas here are derived by diffing successive heartbeats, so a
heartbeat that carries a partial list would report every volume it left out as
unmounted. Collecting now returns the full set alongside the message, and every
site that diffs uses that rather than what went on the wire.

* volume: do not let a full-list request be lost to the heartbeat it raced

The request arrived while a heartbeat was already being built as a delta, and
committing that heartbeat cleared it, so the master waited for another digest
mismatch before asking again. Count the requests and clear only the one the
heartbeat answered.

* rust: stop marking volumes reported by a heartbeat that is thrown away

The state-notify path collected a heartbeat only to diff its volume list, then
sent a delta message of its own and dropped the one it had collected. Once
collecting recorded what the master had been told, every mount or unmount
silently marked the changed volumes as sent, and the master learned of them
only after a digest mismatch.

Snapshotting no longer records anything, and no longer expires ec volumes
whose deletion that path was already discarding.

* master: announce only the volumes a change actually brought

Every changed volume was broadcast as a new location. Volumes grow constantly
and growth moves no location, so on a busy cluster that told every connected
client about volumes it could already reach, filling bounded broadcast queues
and pushing out the topology updates that matter.

* master: ask for the full list when only one can repair the master

Delta heartbeats stop the full report, and with it the only thing that
re-registers a volume the lookup index lost. The volume server cannot see that
divergence and its digest cannot show it, so the master now checks its own two
indexes agree and asks for the list when they do not.

A node reporting one volume id twice is kept on full lists for the same reason
rather than merely skipped: its digest can never be verified, so nothing else
would tell the master what it had stopped holding.

* master: keep the volume options on every heartbeat response

A volume server takes them from whatever response arrives, and preallocate is a
bare bool with no way to tell off from unmentioned. A response sent to ask for
the volume list therefore turned preallocation off until the server reconnected.

Responses sent mid-stream now start from the configured options rather than
being built field by field.

* master: announce a volume the lookup index had lost

Repairing the index makes the volume servable again, but clients were told it
went when the node dropped out and nothing told them otherwise: the disk map
still held it, so it did not count as an arrival.

Reaching the lookup index is what makes a volume servable, so recovering an
entry there is an arrival as far as clients are concerned, on both the full
report and the changed-volume path.
2026-08-07 23:36:28 -07:00

395 lines
14 KiB
Go

package weed_server
import (
"context"
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/seaweedfs/seaweedfs/weed/operation"
"github.com/seaweedfs/seaweedfs/weed/stats"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/security"
"github.com/seaweedfs/seaweedfs/weed/storage/backend"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/util"
)
func (vs *VolumeServer) GetMaster(ctx context.Context) pb.ServerAddress {
return vs.getCurrentMaster()
}
// lookupRaftLeaderMaster resolves the raft leader for topology mutations via
// GetMasterConfiguration on configured peers. It does not update currentMaster;
// the heartbeat loop owns that field and reconnects when the stream reports a
// different leader than the connected peer.
func (vs *VolumeServer) lookupRaftLeaderMaster(ctx context.Context) (pb.ServerAddress, error) {
if ctx == nil {
ctx = context.Background()
}
leader, err := operation.LookupRaftLeaderMaster(ctx, vs.SeedMasterNodes, vs.grpcDialOption)
if err != nil {
if isContextDoneErr(err) {
glog.V(1).Infof("volume server %s:%d: raft leader lookup: %v", vs.store.Ip, vs.store.Port, err)
} else {
glog.V(0).Infof("volume server %s:%d: raft leader lookup: %v", vs.store.Ip, vs.store.Port, err)
}
return "", err
}
current := vs.getCurrentMaster()
if !leader.Equals(current) {
glog.V(1).Infof("volume server %s:%d: raft leader %v (heartbeat peer %v)", vs.store.Ip, vs.store.Port, leader, current)
}
return leader, nil
}
func isContextDoneErr(err error) bool {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return true
}
if st, ok := status.FromError(err); ok {
return st.Code() == codes.Canceled || st.Code() == codes.DeadlineExceeded
}
return false
}
// getCurrentMaster returns vs.currentMaster under a read lock so callers
// (e.g. Ping admission) do not race with the heartbeat goroutine that
// rewrites it on leader changes.
func (vs *VolumeServer) getCurrentMaster() pb.ServerAddress {
vs.currentMasterLock.RLock()
defer vs.currentMasterLock.RUnlock()
return vs.currentMaster
}
// setCurrentMaster updates vs.currentMaster under a write lock. The
// heartbeat goroutine calls this whenever it (re)connects to a master.
func (vs *VolumeServer) setCurrentMaster(master pb.ServerAddress) {
vs.currentMasterLock.Lock()
vs.currentMaster = master
vs.currentMasterLock.Unlock()
}
func (vs *VolumeServer) checkWithMaster() (err error) {
for {
for _, master := range vs.SeedMasterNodes {
err = operation.WithMasterServerClient(context.Background(), false, master, vs.grpcDialOption, func(masterClient master_pb.SeaweedClient) error {
resp, err := masterClient.GetMasterConfiguration(context.Background(), &master_pb.GetMasterConfigurationRequest{})
if err != nil {
return fmt.Errorf("get master %s configuration: %v", master, err)
}
vs.metricsAddress, vs.metricsIntervalSec = resp.MetricsAddress, int(resp.MetricsIntervalSeconds)
backend.LoadFromPbStorageBackends(resp.StorageBackends)
return nil
})
if err == nil {
return
} else {
glog.V(0).Infof("checkWithMaster %s: %v", master, err)
}
}
time.Sleep(1790 * time.Millisecond)
}
}
func (vs *VolumeServer) heartbeat() {
glog.V(0).Infof("Volume server start with seed master nodes: %v", vs.SeedMasterNodes)
vs.store.SetDataCenter(vs.dataCenter)
vs.store.SetRack(vs.rack)
grpcDialOption := security.LoadClientTLS(util.GetViper(), "grpc.volume")
var err error
var newLeader pb.ServerAddress
duplicateRetryCount := 0
for vs.isHeartbeating {
for _, master := range vs.SeedMasterNodes {
if newLeader != "" {
// the new leader may actually is the same master
// need to wait a bit before adding itself
time.Sleep(3 * time.Second)
master = newLeader
}
vs.store.MasterAddress = master
newLeader, err = vs.doHeartbeatWithRetry(master, grpcDialOption, vs.pulsePeriod, duplicateRetryCount)
if err != nil {
glog.V(0).Infof("heartbeat to %s error: %v", master, err)
// Check if this is a duplicate UUID retry error
if strings.Contains(err.Error(), "duplicate UUIDs detected, retrying connection") {
duplicateRetryCount++
retryDelay := time.Duration(1<<(duplicateRetryCount-1)) * 2 * time.Second // exponential backoff: 2s, 4s, 8s
glog.V(0).Infof("Waiting %v before retrying due to duplicate UUID detection...", retryDelay)
time.Sleep(retryDelay)
} else {
// Regular error, reset duplicate retry count
duplicateRetryCount = 0
time.Sleep(vs.pulsePeriod)
}
stats.VolumeServerMasterDisconnections.WithLabelValues(master.String()).Inc()
newLeader = ""
vs.store.MasterAddress = ""
} else {
// Successful connection, reset retry count
duplicateRetryCount = 0
}
if !vs.isHeartbeating {
break
}
}
}
}
func (vs *VolumeServer) StopHeartbeat() (isAlreadyStopping bool) {
if !vs.isHeartbeating {
return true
}
vs.isHeartbeating = false
close(vs.stopChan)
return false
}
func (vs *VolumeServer) doHeartbeatWithRetry(masterAddress pb.ServerAddress, grpcDialOption grpc.DialOption, sleepInterval time.Duration, duplicateRetryCount int) (newLeader pb.ServerAddress, err error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
grpcConnection, err := pb.GrpcDial(ctx, masterAddress.ToGrpcAddress(), false, grpcDialOption)
if err != nil {
return "", fmt.Errorf("fail to dial %s : %v", masterAddress, err)
}
defer grpcConnection.Close()
client := master_pb.NewSeaweedClient(grpcConnection)
stream, err := client.SendHeartbeat(ctx)
if err != nil {
glog.V(0).Infof("SendHeartbeat to %s: %v", masterAddress, err)
return "", err
}
glog.V(0).Infof("Heartbeat to: %v", masterAddress)
vs.setCurrentMaster(masterAddress)
doneChan := make(chan error, 1)
go func() {
for {
in, err := stream.Recv()
if err != nil {
doneChan <- err
return
}
if len(in.DuplicatedUuids) > 0 {
var duplicateDir []string
for _, loc := range vs.store.Locations {
for _, uuid := range in.DuplicatedUuids {
if uuid == loc.DirectoryUuid {
duplicateDir = append(duplicateDir, loc.Directory)
}
}
}
// Implement retry logic for potential race conditions
const maxRetries = 3
if duplicateRetryCount < maxRetries {
retryDelay := time.Duration(1<<duplicateRetryCount) * 2 * time.Second // exponential backoff: 2s, 4s, 8s
glog.Errorf("Master reported duplicate volume directories: %v (retry %d/%d)", duplicateDir, duplicateRetryCount+1, maxRetries)
glog.Errorf("This might be due to a race condition during reconnection. Waiting %v before retrying...", retryDelay)
// Return error to trigger retry with increased count
doneChan <- fmt.Errorf("duplicate UUIDs detected, retrying connection (attempt %d/%d)", duplicateRetryCount+1, maxRetries)
return
} else {
// After max retries, this is likely a real duplicate
glog.Errorf("Shut down Volume Server due to persistent duplicate volume directories after %d retries: %v", maxRetries, duplicateDir)
glog.Errorf("Please check if another volume server is using the same directory")
os.Exit(1)
}
}
volumeOptsChanged := false
if vs.store.GetPreallocate() != in.GetPreallocate() {
vs.store.SetPreallocate(in.GetPreallocate())
volumeOptsChanged = true
}
if in.GetVolumeSizeLimit() != 0 && vs.store.GetVolumeSizeLimit() != in.GetVolumeSizeLimit() {
vs.store.SetVolumeSizeLimit(in.GetVolumeSizeLimit())
volumeOptsChanged = true
}
if volumeOptsChanged {
if vs.store.MaybeAdjustVolumeMax() {
if err = stream.Send(vs.store.CollectHeartbeat()); err != nil {
glog.V(0).Infof("Volume Server Failed to talk with master %s: %v", vs.getCurrentMaster(), err)
return
}
}
}
if in.GetVolumeDigestSupported() {
vs.store.AcceptVolumeChanges()
}
if in.GetResendFullVolumeList() {
glog.V(0).Infof("master %s asked for the full volume list", masterAddress)
vs.store.RequestFullVolumeList()
}
if in.GetLeader() != "" {
current := vs.getCurrentMaster()
if !current.Equals(pb.ServerAddress(in.GetLeader())) {
glog.V(0).Infof("Volume Server found a new master newLeader: %v instead of %v", in.GetLeader(), current)
newLeader = pb.ServerAddress(in.GetLeader())
doneChan <- nil
return
}
}
}
}()
// This master may know nothing about this server, and has not yet said
// whether it understands digests, so start from the whole list.
vs.store.ResetVolumeReporting()
if err = stream.Send(vs.store.CollectHeartbeat()); err != nil {
glog.V(0).Infof("Volume Server Failed to talk with master %s: %v", masterAddress, err)
return "", err
}
if err = stream.Send(vs.store.CollectErasureCodingHeartbeat()); err != nil {
glog.V(0).Infof("Volume Server Failed to talk with master %s: %v", masterAddress, err)
return "", err
}
volumeTickChan := time.NewTicker(sleepInterval)
defer volumeTickChan.Stop()
ecShardTickChan := time.NewTicker(17 * sleepInterval)
defer ecShardTickChan.Stop()
dataCenter := vs.store.GetDataCenter()
rack := vs.store.GetRack()
ip := vs.store.Ip
port := uint32(vs.store.Port)
for {
select {
case stateMessage := <-vs.store.StateUpdateChan:
stateBeat := &master_pb.Heartbeat{
Ip: ip,
Port: port,
DataCenter: dataCenter,
Rack: rack,
State: stateMessage,
}
glog.V(0).Infof("volume server %s:%d updates state to %v", vs.store.Ip, vs.store.Port, stateMessage)
if err = stream.Send(stateBeat); err != nil {
glog.V(0).Infof("Volume Server Failed to update state to master %s: %v", masterAddress, err)
return "", err
}
case first := <-vs.store.NewVolumesChan:
volumes := util.DrainChannel(vs.store.NewVolumesChan, first)
deltaBeat := &master_pb.Heartbeat{
Ip: ip,
Port: port,
DataCenter: dataCenter,
Rack: rack,
NewVolumes: volumes,
}
for _, v := range volumes {
glog.V(0).Infof("volume server %s:%d adds volume %d", vs.store.Ip, vs.store.Port, v.Id)
}
if err = stream.Send(deltaBeat); err != nil {
glog.V(0).Infof("Volume Server Failed to update to master %s: %v", masterAddress, err)
return "", err
}
case first := <-vs.store.NewEcShardsChan:
shards := util.DrainChannel(vs.store.NewEcShardsChan, first)
deltaBeat := &master_pb.Heartbeat{
Ip: ip,
Port: port,
DataCenter: dataCenter,
Rack: rack,
NewEcShards: shards,
}
for _, s := range shards {
si := erasure_coding.ShardsInfoFromVolumeEcShardInformationMessage(s)
glog.V(0).Infof("volume server %s:%d adds ec shards to %d [%s]", vs.store.Ip, vs.store.Port, s.Id, si.String())
}
if err = stream.Send(deltaBeat); err != nil {
glog.V(0).Infof("Volume Server Failed to update to master %s: %v", masterAddress, err)
return "", err
}
case first := <-vs.store.DeletedVolumesChan:
volumes := util.DrainChannel(vs.store.DeletedVolumesChan, first)
deltaBeat := &master_pb.Heartbeat{
Ip: ip,
Port: port,
DataCenter: dataCenter,
Rack: rack,
DeletedVolumes: volumes,
}
for _, v := range volumes {
glog.V(0).Infof("volume server %s:%d deletes volume %d", vs.store.Ip, vs.store.Port, v.Id)
}
if err = stream.Send(deltaBeat); err != nil {
glog.V(0).Infof("Volume Server Failed to update to master %s: %v", masterAddress, err)
return "", err
}
case first := <-vs.store.DeletedEcShardsChan:
shards := util.DrainChannel(vs.store.DeletedEcShardsChan, first)
deltaBeat := &master_pb.Heartbeat{
Ip: ip,
Port: port,
DataCenter: dataCenter,
Rack: rack,
DeletedEcShards: shards,
}
for _, s := range shards {
glog.V(0).Infof("volume server %s:%d deletes ec shard %d:%s", vs.store.Ip, vs.store.Port, s.Id,
erasure_coding.ShardsInfoFromVolumeEcShardInformationMessage(s).String())
}
if err = stream.Send(deltaBeat); err != nil {
glog.V(0).Infof("Volume Server Failed to update to master %s: %v", masterAddress, err)
return "", err
}
case <-volumeTickChan.C:
glog.V(4).Infof("volume server %s:%d heartbeat", vs.store.Ip, vs.store.Port)
vs.store.MaybeAdjustVolumeMax()
if err = stream.Send(vs.store.CollectHeartbeat()); err != nil {
glog.V(0).Infof("Volume Server Failed to talk with master %s: %v", masterAddress, err)
return "", err
}
case <-ecShardTickChan.C:
glog.V(4).Infof("volume server %s:%d ec heartbeat", vs.store.Ip, vs.store.Port)
if err = stream.Send(vs.store.CollectErasureCodingHeartbeat()); err != nil {
glog.V(0).Infof("Volume Server Failed to talk with master %s: %v", masterAddress, err)
return "", err
}
case err = <-doneChan:
return
case <-vs.stopChan:
var volumeMessages []*master_pb.VolumeInformationMessage
emptyBeat := &master_pb.Heartbeat{
Ip: ip,
Port: port,
PublicUrl: vs.store.PublicUrl,
MaxFileKey: uint64(0),
DataCenter: dataCenter,
Rack: rack,
Volumes: volumeMessages,
HasNoVolumes: len(volumeMessages) == 0,
}
glog.V(1).Infof("volume server %s:%d stops and deletes all volumes", vs.store.Ip, vs.store.Port)
if err = stream.Send(emptyBeat); err != nil {
glog.V(0).Infof("Volume Server Failed to update to master %s: %v", masterAddress, err)
return "", err
}
return
}
}
}