Skip to content

Commit aa28258

Browse files
committed
Harden cluster registry dissemination
1 parent 58f5ac6 commit aa28258

21 files changed

Lines changed: 922 additions & 120 deletions

File tree

api/cluster/raft/config.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ type Config struct {
4949
BootstrapExpect int `json:"bootstrap_expect,omitempty"`
5050
SnapshotRetain int `json:"snapshot_retain"`
5151
MaxPool int `json:"max_pool"`
52+
// ShutdownTransferTimeout bounds the best-effort leadership transfer
53+
// attempted by Stop() when this node is leader. Zero defaults to one
54+
// election timeout. The transfer is an availability optimization, not a
55+
// safety requirement, so shutdown must continue if no eligible follower
56+
// can take leadership promptly.
57+
ShutdownTransferTimeout time.Duration `json:"shutdown_transfer_timeout"`
5258
// MaxAppendEntries caps how many log entries the leader packs into a
5359
// single AppendEntries RPC. The hashicorp/raft default is 64 which,
5460
// when a follower restarts with an empty log and needs to catch up
@@ -98,6 +104,9 @@ func (c *Config) InitDefaults() {
98104
if c.MaxPool == 0 {
99105
c.MaxPool = 3
100106
}
107+
if c.ShutdownTransferTimeout == 0 {
108+
c.ShutdownTransferTimeout = c.ElectionTimeout
109+
}
101110
if c.MaxAppendEntries == 0 {
102111
// Default below hashicorp's 64 to cap leader memory during
103112
// chaos catch-up. Followers that need 500+ entries get them

boot/components/system/cluster.example.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,4 @@ cluster:
7272
# max_append_entries: 16
7373
# reconcile_debounce: 2s
7474
# reconcile_timeout: 2s
75+
# global_dissem_tombstone_retention: 0 # 0 keeps delete fences until restart; set e.g. 24h to bound cache tombstones.

boot/components/system/constants.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,9 @@ const (
7171
ClusterRaftCommitTimeout boot.Name = "raft.commit_timeout"
7272
ClusterRaftLeaderProbeInterval boot.Name = "raft.leader_probe_interval"
7373
ClusterRaftLeaderProbeGrace boot.Name = "raft.leader_probe_grace"
74+
// ClusterRaftGlobalDissemTombstoneRetention bounds how long the AP
75+
// global-name dissemination cache retains delete tombstones as stale-gossip
76+
// fences. The Raft FSM remains authoritative; this only tunes cache memory
77+
// vs stale delete repair.
78+
ClusterRaftGlobalDissemTombstoneRetention boot.Name = "raft.global_dissem_tombstone_retention"
7479
)

boot/components/system/raft.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ func Raft() boot.Component {
6767
var logger *zap.Logger
6868
var handlerCfg sysraft.HandlerConfig
6969
var bootstrapExpect int
70+
var globalDissemTombstoneRetention time.Duration
7071

7172
return boot.New(boot.P{
7273
Name: RaftName,
@@ -137,6 +138,7 @@ func Raft() boot.Component {
137138
ReconcileDebounce: raftCfg.GetDuration(ClusterRaftReconcileDebounce, 2*time.Second),
138139
ReconcileTimeout: raftCfg.GetDuration(ClusterRaftReconcileTimeout, 2*time.Second),
139140
}
141+
globalDissemTombstoneRetention = raftCfg.GetDuration(ClusterRaftGlobalDissemTombstoneRetention, 0)
140142

141143
// Raft rides the internode mesh exclusively. Without a connection
142144
// manager (cluster transport not wired — e.g. a single-node
@@ -240,7 +242,8 @@ func Raft() boot.Component {
240242
// the cache on FSM-miss so non-members resolve names locally.
241243
if mem := clusterapi.GetMembership(ctx); mem != nil {
242244
if memSvc, ok := mem.(*membership.Service); ok {
243-
dissem := global.NewDissem(node.ID(), logger.Named("dissem"))
245+
dissem := global.NewDissem(node.ID(), logger.Named("dissem"),
246+
global.WithTombstoneRetention(globalDissemTombstoneRetention))
244247
if err := memSvc.RegisterUserDelegate(dissem); err != nil {
245248
return ctx, fmt.Errorf("raft: register globalreg dissem delegate: %w", err)
246249
}

cluster/membership/membership.go

Lines changed: 143 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package membership
44

55
import (
66
"context"
7+
"crypto/sha256"
78
"encoding/base64"
89
"encoding/json"
910
"errors"
@@ -128,6 +129,14 @@ type Config struct {
128129
VeryVerbose bool
129130
}
130131

132+
const (
133+
userBroadcastRetain = 8192
134+
userBroadcastBackpressure = userBroadcastRetain / 2
135+
// Match memberlist's conservative UDP buffer. User broadcasts above this
136+
// cannot fit once memberlist adds its own user-message/compound headers.
137+
userBroadcastMaxPacketBytes = 1400
138+
)
139+
131140
// NewService creates a new membership service.
132141
//
133142
// coll, mp, and tp wire the metrics collector and OTel providers used for
@@ -207,7 +216,7 @@ func (s *Service) Start(ctx context.Context) error {
207216
mlConfig.BindAddr = s.config.BindAddr
208217
mlConfig.BindPort = s.config.BindPort
209218
mlConfig.Events = &eventDelegate{service: s}
210-
mlConfig.Delegate = &delegate{service: s}
219+
mlConfig.Delegate = newDelegate(s, mlConfig.RetransmitMult)
211220

212221
// Use custom transport if provided (for testing)
213222
if s.transport != nil {
@@ -773,7 +782,86 @@ func (ed *eventDelegate) parseNodeMeta(meta []byte) cluster.NodeMeta {
773782

774783
// delegate handles memberlist delegate functions
775784
type delegate struct {
776-
service *Service
785+
service *Service
786+
broadcasts memberlist.TransmitLimitedQueue
787+
queuedMu sync.Mutex
788+
queued map[string]struct{}
789+
}
790+
791+
func newDelegate(service *Service, retransmitMult int) *delegate {
792+
if retransmitMult <= 0 {
793+
retransmitMult = 1
794+
}
795+
d := &delegate{
796+
service: service,
797+
queued: make(map[string]struct{}),
798+
}
799+
d.broadcasts.NumNodes = service.broadcastNodeCount
800+
d.broadcasts.RetransmitMult = retransmitMult
801+
return d
802+
}
803+
804+
func (s *Service) broadcastNodeCount() int {
805+
if s.memberlist != nil {
806+
if n := len(s.memberlist.Members()); n > 0 {
807+
return n
808+
}
809+
}
810+
s.mu.RLock()
811+
n := len(s.nodes)
812+
s.mu.RUnlock()
813+
if n > 0 {
814+
return n
815+
}
816+
return 1
817+
}
818+
819+
type muxBroadcast struct {
820+
name string
821+
msg []byte
822+
finished func(string)
823+
}
824+
825+
func (b *muxBroadcast) Invalidates(other memberlist.Broadcast) bool {
826+
o, ok := other.(*muxBroadcast)
827+
return ok && b.name == o.name
828+
}
829+
830+
func (b *muxBroadcast) Name() string { return b.name }
831+
func (b *muxBroadcast) Message() []byte {
832+
return b.msg
833+
}
834+
func (b *muxBroadcast) Finished() {
835+
if b.finished != nil {
836+
b.finished(b.name)
837+
}
838+
}
839+
840+
var _ memberlist.NamedBroadcast = (*muxBroadcast)(nil)
841+
842+
func (d *delegate) queueBroadcast(frame []byte) bool {
843+
sum := sha256.Sum256(frame)
844+
name := string(sum[:])
845+
d.queuedMu.Lock()
846+
if _, exists := d.queued[name]; exists {
847+
d.queuedMu.Unlock()
848+
return false
849+
}
850+
d.queued[name] = struct{}{}
851+
d.queuedMu.Unlock()
852+
853+
d.broadcasts.QueueBroadcast(&muxBroadcast{
854+
name: name,
855+
msg: frame,
856+
finished: d.forgetBroadcast,
857+
})
858+
return true
859+
}
860+
861+
func (d *delegate) forgetBroadcast(name string) {
862+
d.queuedMu.Lock()
863+
delete(d.queued, name)
864+
d.queuedMu.Unlock()
777865
}
778866

779867
func (d *delegate) NodeMeta(limit int) []byte {
@@ -828,6 +916,38 @@ func (d *delegate) NotifyMsg(data []byte) {
828916
}
829917

830918
func (d *delegate) GetBroadcasts(overhead, limit int) [][]byte {
919+
if d.service.tel != nil && d.service.tel.coll != nil {
920+
d.service.tel.coll.CounterInc("gossip_user_getbroadcasts_calls_total", nil)
921+
}
922+
923+
recordOut := func(out [][]byte) {
924+
if d.service.tel == nil || d.service.tel.coll == nil || len(out) == 0 {
925+
return
926+
}
927+
totalBytes := 0
928+
totalCost := 0
929+
for _, frame := range out {
930+
totalBytes += len(frame)
931+
totalCost += len(frame) + overhead
932+
}
933+
d.service.tel.coll.CounterAdd("gossip_user_getbroadcasts_frames_total", float64(len(out)), nil)
934+
d.service.tel.coll.CounterAdd("gossip_user_getbroadcasts_bytes_total", float64(totalBytes), nil)
935+
if totalCost > limit {
936+
d.service.tel.coll.CounterInc("gossip_user_getbroadcasts_overshoot_total", nil)
937+
}
938+
}
939+
940+
queued := d.broadcasts.NumQueued()
941+
if queued > userBroadcastRetain {
942+
d.broadcasts.Prune(userBroadcastRetain)
943+
queued = userBroadcastRetain
944+
}
945+
if queued >= userBroadcastBackpressure {
946+
out := d.broadcasts.GetBroadcasts(overhead, limit)
947+
recordOut(out)
948+
return out
949+
}
950+
831951
d.service.userDelegateMu.RLock()
832952
dels := make([]UserDelegate, 0, len(d.service.userDelegates))
833953
for _, ud := range d.service.userDelegates {
@@ -842,25 +962,31 @@ func (d *delegate) GetBroadcasts(overhead, limit int) [][]byte {
842962
// hogged the cycle).
843963
sort.Slice(dels, func(i, j int) bool { return dels[i].Kind() < dels[j].Kind() })
844964

845-
if d.service.tel != nil && d.service.tel.coll != nil {
846-
d.service.tel.coll.CounterInc("gossip_user_getbroadcasts_calls_total", nil)
847-
}
848-
849-
var out [][]byte
850-
totalBytes := 0
851-
totalCost := 0
965+
queuedCost := 0
852966
muxOverhead := overhead + 5
853967

854968
wrap := func(ud UserDelegate, frames [][]byte) {
855969
for _, f := range frames {
970+
if uint64(len(f)) > uint64(^uint32(0)) {
971+
if d.service.tel != nil && d.service.tel.coll != nil {
972+
d.service.tel.coll.CounterInc("gossip_user_getbroadcasts_frame_dropped_total", nil)
973+
}
974+
continue
975+
}
856976
wrapped := make([]byte, 0, len(f)+5)
857977
wrapped = append(wrapped, ud.Kind())
858978
n := uint32(len(f))
859979
wrapped = append(wrapped, byte(n), byte(n>>8), byte(n>>16), byte(n>>24))
860980
wrapped = append(wrapped, f...)
861-
totalCost += len(wrapped) + overhead
862-
totalBytes += len(wrapped)
863-
out = append(out, wrapped)
981+
if len(wrapped)+overhead > userBroadcastMaxPacketBytes {
982+
if d.service.tel != nil && d.service.tel.coll != nil {
983+
d.service.tel.coll.CounterInc("gossip_user_getbroadcasts_frame_dropped_total", nil)
984+
}
985+
continue
986+
}
987+
if d.queueBroadcast(wrapped) {
988+
queuedCost += len(wrapped) + overhead
989+
}
864990
}
865991
}
866992

@@ -883,21 +1009,19 @@ func (d *delegate) GetBroadcasts(overhead, limit int) [][]byte {
8831009
// budget contract bounds emission to what fits the remainder. This
8841010
// recovers throughput when one delegate is silent.
8851011
for _, ud := range dels {
886-
remaining := limit - totalCost
1012+
remaining := limit - queuedCost
8871013
if remaining <= muxOverhead {
8881014
break
8891015
}
8901016
frames := ud.GetBroadcasts(muxOverhead, remaining)
8911017
wrap(ud, frames)
8921018
}
8931019

894-
if d.service.tel != nil && d.service.tel.coll != nil && len(out) > 0 {
895-
d.service.tel.coll.CounterAdd("gossip_user_getbroadcasts_frames_total", float64(len(out)), nil)
896-
d.service.tel.coll.CounterAdd("gossip_user_getbroadcasts_bytes_total", float64(totalBytes), nil)
897-
if totalCost > limit {
898-
d.service.tel.coll.CounterInc("gossip_user_getbroadcasts_overshoot_total", nil)
899-
}
1020+
if d.broadcasts.NumQueued() > userBroadcastRetain {
1021+
d.broadcasts.Prune(userBroadcastRetain)
9001022
}
1023+
out := d.broadcasts.GetBroadcasts(overhead, limit)
1024+
recordOut(out)
9011025
return out
9021026
}
9031027

cluster/membership/membership_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ func TestDelegate_NodeMeta_ValidJSON(t *testing.T) {
539539
service, _, _, cancel := setupService(t)
540540
defer cancel()
541541

542-
d := &delegate{service: service}
542+
d := newDelegate(service, memberlist.DefaultLocalConfig().RetransmitMult)
543543

544544
meta := d.NodeMeta(512)
545545

@@ -565,7 +565,7 @@ func TestDelegate_NodeMeta_EmptyMeta(t *testing.T) {
565565
}
566566

567567
service := NewService(config, bus, logger, nil, nil, nil)
568-
d := &delegate{service: service}
568+
d := newDelegate(service, memberlist.DefaultLocalConfig().RetransmitMult)
569569

570570
meta := d.NodeMeta(512)
571571

@@ -589,7 +589,7 @@ func TestDelegate_NodeMeta_ExceedsLimit(t *testing.T) {
589589
}
590590

591591
service := NewService(config, bus, logger, nil, nil, nil)
592-
d := &delegate{service: service}
592+
d := newDelegate(service, memberlist.DefaultLocalConfig().RetransmitMult)
593593

594594
meta := d.NodeMeta(10)
595595

@@ -600,7 +600,7 @@ func TestDelegate_GetBroadcasts(t *testing.T) {
600600
service, _, _, cancel := setupService(t)
601601
defer cancel()
602602

603-
d := &delegate{service: service}
603+
d := newDelegate(service, memberlist.DefaultLocalConfig().RetransmitMult)
604604

605605
broadcasts := d.GetBroadcasts(10, 512)
606606

@@ -611,7 +611,7 @@ func TestDelegate_LocalState(t *testing.T) {
611611
service, _, _, cancel := setupService(t)
612612
defer cancel()
613613

614-
d := &delegate{service: service}
614+
d := newDelegate(service, memberlist.DefaultLocalConfig().RetransmitMult)
615615

616616
state := d.LocalState(false)
617617

@@ -623,7 +623,7 @@ func TestDelegate_MergeRemoteState(t *testing.T) {
623623
service, _, _, cancel := setupService(t)
624624
defer cancel()
625625

626-
d := &delegate{service: service}
626+
d := newDelegate(service, memberlist.DefaultLocalConfig().RetransmitMult)
627627

628628
d.MergeRemoteState([]byte(`{"key":"value"}`), true)
629629
}
@@ -632,7 +632,7 @@ func TestDelegate_NotifyMsg(t *testing.T) {
632632
service, _, _, cancel := setupService(t)
633633
defer cancel()
634634

635-
d := &delegate{service: service}
635+
d := newDelegate(service, memberlist.DefaultLocalConfig().RetransmitMult)
636636

637637
d.NotifyMsg([]byte("test message"))
638638
}
@@ -649,7 +649,7 @@ func TestDelegate_NotifyMsg_VeryVerbose(_ *testing.T) {
649649
}
650650

651651
service := NewService(config, bus, logger, nil, nil, nil)
652-
d := &delegate{service: service}
652+
d := newDelegate(service, memberlist.DefaultLocalConfig().RetransmitMult)
653653

654654
d.NotifyMsg([]byte("test message"))
655655
}
@@ -666,7 +666,7 @@ func TestDelegate_MergeRemoteState_VeryVerbose(_ *testing.T) {
666666
}
667667

668668
service := NewService(config, bus, logger, nil, nil, nil)
669-
d := &delegate{service: service}
669+
d := newDelegate(service, memberlist.DefaultLocalConfig().RetransmitMult)
670670

671671
d.MergeRemoteState([]byte(`{"key":"value"}`), true)
672672
}

0 commit comments

Comments
 (0)