Skip to content

Commit bd332b3

Browse files
committed
raft: add (*RawNode).WithProgress
Calls to Status can be frequent and currently incur three heap allocations, but often the caller has no intention to hold on to the returned status. Add StatusWithoutProgress and WithProgress to allow avoiding heap allocations altogether. StatusWithoutProgress does what's on the tin and additionally returns a value (instead of a pointer) to avoid the associated heap allocation. By not returning a Progress map, it avoids all other allocations that Status incurs. To still introspect the Progress map, add WithProgress, which uses a simple visitor pattern. Add benchmarks to verify that this is indeed allocation free. ``` BenchmarkStatusProgress/members=1/Status-8 5000000 353 ns/op 784 B/op 3 allocs/op BenchmarkStatusProgress/members=1/Status-example-8 5000000 372 ns/op 784 B/op 3 allocs/op BenchmarkStatusProgress/members=1/StatusWithoutProgress-8 100000000 17.6 ns/op 0 B/op 0 allocs/op BenchmarkStatusProgress/members=1/WithProgress-8 30000000 48.6 ns/op 0 B/op 0 allocs/op BenchmarkStatusProgress/members=1/WithProgress-example-8 30000000 42.9 ns/op 0 B/op 0 allocs/op BenchmarkStatusProgress/members=3/Status-8 5000000 395 ns/op 784 B/op 3 allocs/op BenchmarkStatusProgress/members=3/Status-example-8 3000000 449 ns/op 784 B/op 3 allocs/op BenchmarkStatusProgress/members=3/StatusWithoutProgress-8 100000000 18.7 ns/op 0 B/op 0 allocs/op BenchmarkStatusProgress/members=3/WithProgress-8 20000000 78.1 ns/op 0 B/op 0 allocs/op BenchmarkStatusProgress/members=3/WithProgress-example-8 20000000 70.7 ns/op 0 B/op 0 allocs/op BenchmarkStatusProgress/members=5/Status-8 3000000 470 ns/op 784 B/op 3 allocs/op BenchmarkStatusProgress/members=5/Status-example-8 3000000 544 ns/op 784 B/op 3 allocs/op BenchmarkStatusProgress/members=5/StatusWithoutProgress-8 100000000 19.7 ns/op 0 B/op 0 allocs/op BenchmarkStatusProgress/members=5/WithProgress-8 20000000 105 ns/op 0 B/op 0 allocs/op BenchmarkStatusProgress/members=5/WithProgress-example-8 20000000 94.0 ns/op 0 B/op 0 allocs/op BenchmarkStatusProgress/members=100/Status-8 100000 11903 ns/op 22663 B/op 12 allocs/op BenchmarkStatusProgress/members=100/Status-example-8 100000 13330 ns/op 22669 B/op 12 allocs/op BenchmarkStatusProgress/members=100/StatusWithoutProgress-8 50000000 20.9 ns/op 0 B/op 0 allocs/op BenchmarkStatusProgress/members=100/WithProgress-8 1000000 1731 ns/op 0 B/op 0 allocs/op BenchmarkStatusProgress/members=100/WithProgress-example-8 1000000 1571 ns/op 0 B/op 0 allocs/op ```
1 parent 510ae3d commit bd332b3

File tree

3 files changed

+123
-13
lines changed

3 files changed

+123
-13
lines changed

raft/rawnode.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,39 @@ func (rn *RawNode) Status() *Status {
236236
return &status
237237
}
238238

239+
// StatusWithoutProgress returns a Status without populating the Progress field
240+
// (and returns the Status as a value to avoid forcing it onto the heap). This
241+
// is more performant if the Progress is not required. See WithProgress for an
242+
// allocation-free way to introspect the Progress.
243+
func (rn *RawNode) StatusWithoutProgress() Status {
244+
return getStatusWithoutProgress(rn.raft)
245+
}
246+
247+
// ProgressType indicates the type of replica a Progress corresponds to.
248+
type ProgressType byte
249+
250+
const (
251+
// ProgressTypePeer accompanies a Progress for a regular peer replica.
252+
ProgressTypePeer ProgressType = iota
253+
// ProgressTypeLearner accompanies a Progress for a learner replica.
254+
ProgressTypeLearner
255+
)
256+
257+
// WithProgress is a helper to introspect the Progress for this node and its
258+
// peers.
259+
func (rn *RawNode) WithProgress(visitor func(id uint64, typ ProgressType, pr Progress)) {
260+
for id, pr := range rn.raft.prs {
261+
pr := *pr
262+
pr.ins = nil
263+
visitor(id, ProgressTypePeer, pr)
264+
}
265+
for id, pr := range rn.raft.learnerPrs {
266+
pr := *pr
267+
pr.ins = nil
268+
visitor(id, ProgressTypeLearner, pr)
269+
}
270+
}
271+
239272
// ReportUnreachable reports the given node is not reachable for the last send.
240273
func (rn *RawNode) ReportUnreachable(id uint64) {
241274
_ = rn.raft.Step(pb.Message{Type: pb.MsgUnreachable, From: id})

raft/rawnode_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package raft
1616

1717
import (
1818
"bytes"
19+
"fmt"
1920
"reflect"
2021
"testing"
2122

@@ -545,3 +546,73 @@ func TestRawNodeBoundedLogGrowthWithPartition(t *testing.T) {
545546
rawNode.Advance(rd)
546547
checkUncommitted(0)
547548
}
549+
550+
func BenchmarkStatusProgress(b *testing.B) {
551+
setup := func(members int) *RawNode {
552+
peers := make([]uint64, members)
553+
for i := range peers {
554+
peers[i] = uint64(i + 1)
555+
}
556+
cfg := newTestConfig(1, peers, 3, 1, NewMemoryStorage())
557+
cfg.Logger = discardLogger
558+
r := newRaft(cfg)
559+
r.becomeFollower(1, 1)
560+
r.becomeCandidate()
561+
r.becomeLeader()
562+
return &RawNode{raft: r}
563+
}
564+
565+
for _, members := range []int{1, 3, 5, 100} {
566+
b.Run(fmt.Sprintf("members=%d", members), func(b *testing.B) {
567+
// NB: call getStatus through rn.Status because that incurs an additional
568+
// allocation.
569+
rn := setup(members)
570+
571+
b.Run("Status", func(b *testing.B) {
572+
b.ReportAllocs()
573+
for i := 0; i < b.N; i++ {
574+
_ = rn.Status()
575+
}
576+
})
577+
578+
b.Run("Status-example", func(b *testing.B) {
579+
b.ReportAllocs()
580+
for i := 0; i < b.N; i++ {
581+
s := rn.Status()
582+
var n uint64
583+
for _, pr := range s.Progress {
584+
n += pr.Match
585+
}
586+
_ = n
587+
}
588+
})
589+
590+
b.Run("StatusWithoutProgress", func(b *testing.B) {
591+
b.ReportAllocs()
592+
for i := 0; i < b.N; i++ {
593+
_ = rn.StatusWithoutProgress()
594+
}
595+
})
596+
597+
b.Run("WithProgress", func(b *testing.B) {
598+
b.ReportAllocs()
599+
visit := func(uint64, ProgressType, Progress) {}
600+
601+
for i := 0; i < b.N; i++ {
602+
rn.WithProgress(visit)
603+
}
604+
})
605+
b.Run("WithProgress-example", func(b *testing.B) {
606+
b.ReportAllocs()
607+
for i := 0; i < b.N; i++ {
608+
var n uint64
609+
visit := func(_ uint64, _ ProgressType, pr Progress) {
610+
n += pr.Match
611+
}
612+
rn.WithProgress(visit)
613+
_ = n
614+
}
615+
})
616+
})
617+
}
618+
}

raft/status.go

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,29 +32,35 @@ type Status struct {
3232
LeadTransferee uint64
3333
}
3434

35-
// getStatus gets a copy of the current raft status.
36-
func getStatus(r *raft) Status {
35+
func getProgressCopy(r *raft) map[uint64]Progress {
36+
prs := make(map[uint64]Progress)
37+
for id, p := range r.prs {
38+
prs[id] = *p
39+
}
40+
41+
for id, p := range r.learnerPrs {
42+
prs[id] = *p
43+
}
44+
return prs
45+
}
46+
47+
func getStatusWithoutProgress(r *raft) Status {
3748
s := Status{
3849
ID: r.id,
3950
LeadTransferee: r.leadTransferee,
4051
}
41-
4252
s.HardState = r.hardState()
4353
s.SoftState = *r.softState()
44-
4554
s.Applied = r.raftLog.applied
55+
return s
56+
}
4657

58+
// getStatus gets a copy of the current raft status.
59+
func getStatus(r *raft) Status {
60+
s := getStatusWithoutProgress(r)
4761
if s.RaftState == StateLeader {
48-
s.Progress = make(map[uint64]Progress)
49-
for id, p := range r.prs {
50-
s.Progress[id] = *p
51-
}
52-
53-
for id, p := range r.learnerPrs {
54-
s.Progress[id] = *p
55-
}
62+
s.Progress = getProgressCopy(r)
5663
}
57-
5864
return s
5965
}
6066

0 commit comments

Comments
 (0)