Skip to content

Commit 64b6c2b

Browse files
authored
Reuse write request from distributor to Ingesters (#5193)
* wip Signed-off-by: Alan Protasio <[email protected]> * branchmark Signed-off-by: Alan Protasio <[email protected]> * fix some linting / test Signed-off-by: Alan Protasio <[email protected]> * No allocation Signed-off-by: Alan Protasio <[email protected]> * min pool size Signed-off-by: Alan Protasio <[email protected]> * min pool size Signed-off-by: Alan Protasio <[email protected]> * fuzzy test Signed-off-by: Alan Protasio <[email protected]> * changelog Signed-off-by: Alan Protasio <[email protected]> * more benchmark Signed-off-by: Alan Protasio <[email protected]> * fix bug on the reuse Signed-off-by: Alan Protasio <[email protected]> --------- Signed-off-by: Alan Protasio <[email protected]>
1 parent 713542c commit 64b6c2b

File tree

8 files changed

+220
-9
lines changed

8 files changed

+220
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
* [ENHANCEMENT] Upgraded Docker base images to `alpine:3.17`. #5132
2222
* [ENHANCEMENT] Add retry logic to S3 bucket client. #5135
2323
* [ENHANCEMENT] Update Go version to 1.20.1. #5159
24+
* [ENHANCEMENT] Distributor: Reuse byte slices when serializing requests from distributors to ingesters. #5193
2425
* [FEATURE] Querier/Query Frontend: support Prometheus /api/v1/status/buildinfo API. #4978
2526
* [FEATURE] Ingester: Add active series to all_user_stats page. #4972
2627
* [FEATURE] Ingester: Added `-blocks-storage.tsdb.head-chunks-write-queue-size` allowing to configure the size of the in-memory queue used before flushing chunks to the disk . #5000

pkg/cortexpb/slicesPool.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package cortexpb
2+
3+
import (
4+
"math"
5+
"sync"
6+
)
7+
8+
const (
9+
minPoolSizePower = 5
10+
)
11+
12+
type byteSlicePools struct {
13+
pools []sync.Pool
14+
}
15+
16+
func newSlicePool(pools int) *byteSlicePools {
17+
sp := byteSlicePools{}
18+
sp.init(pools)
19+
return &sp
20+
}
21+
22+
func (sp *byteSlicePools) init(pools int) {
23+
sp.pools = make([]sync.Pool, pools)
24+
for i := 0; i < pools; i++ {
25+
size := int(math.Pow(2, float64(i+minPoolSizePower)))
26+
sp.pools[i] = sync.Pool{
27+
New: func() interface{} {
28+
buf := make([]byte, 0, size)
29+
return &buf
30+
},
31+
}
32+
}
33+
}
34+
35+
func (sp *byteSlicePools) getSlice(size int) *[]byte {
36+
index := int(math.Ceil(math.Log2(float64(size)))) - minPoolSizePower
37+
38+
if index >= len(sp.pools) {
39+
buf := make([]byte, size)
40+
return &buf
41+
}
42+
43+
// if the size is < than the minPoolSizePower we return an array from the first pool
44+
if index < 0 {
45+
index = 0
46+
}
47+
48+
s := sp.pools[index].Get().(*[]byte)
49+
*s = (*s)[:size]
50+
return s
51+
}
52+
53+
func (sp *byteSlicePools) reuseSlice(s *[]byte) {
54+
index := int(math.Floor(math.Log2(float64(cap(*s))))) - minPoolSizePower
55+
56+
if index >= len(sp.pools) || index < 0 {
57+
return
58+
}
59+
60+
sp.pools[index].Put(s)
61+
}

pkg/cortexpb/slicesPool_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package cortexpb
2+
3+
import (
4+
"math"
5+
"math/rand"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestFuzzyByteSlicePools(t *testing.T) {
12+
sut := newSlicePool(20)
13+
maxByteSize := int(math.Pow(2, 20+minPoolSizePower-1))
14+
15+
for i := 0; i < 1000; i++ {
16+
size := rand.Int() % maxByteSize
17+
s := sut.getSlice(size)
18+
assert.Equal(t, len(*s), size)
19+
sut.reuseSlice(s)
20+
}
21+
}
22+
23+
func TestReturnSliceSmallerThanMin(t *testing.T) {
24+
sut := newSlicePool(20)
25+
size := 3
26+
buff := make([]byte, 0, size)
27+
sut.reuseSlice(&buff)
28+
buff2 := sut.getSlice(size * 2)
29+
assert.Equal(t, len(*buff2), size*2)
30+
}

pkg/cortexpb/timeseries.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ var (
3737
}
3838
},
3939
}
40+
41+
writeRequestPool = sync.Pool{
42+
New: func() interface{} {
43+
return &PreallocWriteRequest{
44+
WriteRequest: WriteRequest{},
45+
}
46+
},
47+
}
48+
bytePool = newSlicePool(20)
4049
)
4150

4251
// PreallocConfig configures how structures will be preallocated to optimise
@@ -53,6 +62,7 @@ func (PreallocConfig) RegisterFlags(f *flag.FlagSet) {
5362
// PreallocWriteRequest is a WriteRequest which preallocs slices on Unmarshal.
5463
type PreallocWriteRequest struct {
5564
WriteRequest
65+
data *[]byte
5666
}
5767

5868
// Unmarshal implements proto.Message.
@@ -72,6 +82,32 @@ func (p *PreallocTimeseries) Unmarshal(dAtA []byte) error {
7282
return p.TimeSeries.Unmarshal(dAtA)
7383
}
7484

85+
func (p *PreallocWriteRequest) Marshal() (dAtA []byte, err error) {
86+
size := p.Size()
87+
p.data = bytePool.getSlice(size)
88+
dAtA = *p.data
89+
n, err := p.MarshalToSizedBuffer(dAtA[:size])
90+
if err != nil {
91+
return nil, err
92+
}
93+
return dAtA[:n], nil
94+
}
95+
96+
func ReuseWriteRequest(req *PreallocWriteRequest) {
97+
if req.data != nil {
98+
bytePool.reuseSlice(req.data)
99+
req.data = nil
100+
}
101+
req.Source = 0
102+
req.Metadata = nil
103+
req.Timeseries = nil
104+
writeRequestPool.Put(req)
105+
}
106+
107+
func PreallocWriteRequestFromPool() *PreallocWriteRequest {
108+
return writeRequestPool.Get().(*PreallocWriteRequest)
109+
}
110+
75111
// LabelAdapter is a labels.Label that can be marshalled to/from protos.
76112
type LabelAdapter labels.Label
77113

pkg/cortexpb/timeseries_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package cortexpb
22

33
import (
4+
"fmt"
45
"testing"
56

7+
"github.com/gogo/protobuf/proto"
68
"github.com/stretchr/testify/assert"
79
"github.com/stretchr/testify/require"
810
)
@@ -64,3 +66,67 @@ func TestTimeseriesFromPool(t *testing.T) {
6466
assert.Len(t, reused.Samples, 0)
6567
})
6668
}
69+
70+
func BenchmarkMarshallWriteRequest(b *testing.B) {
71+
ts := PreallocTimeseriesSliceFromPool()
72+
73+
for i := 0; i < 100; i++ {
74+
ts = append(ts, PreallocTimeseries{TimeSeries: TimeseriesFromPool()})
75+
ts[i].Labels = []LabelAdapter{
76+
{Name: "foo", Value: "bar"},
77+
{Name: "very long label name", Value: "very long label value"},
78+
{Name: "very long label name 2", Value: "very long label value 2"},
79+
{Name: "very long label name 3", Value: "very long label value 3"},
80+
{Name: "int", Value: fmt.Sprint(i)},
81+
}
82+
ts[i].Samples = []Sample{{Value: 1, TimestampMs: 2}}
83+
}
84+
85+
tests := []struct {
86+
name string
87+
writeRequestFactory func() proto.Marshaler
88+
clean func(in interface{})
89+
}{
90+
{
91+
name: "no-pool",
92+
writeRequestFactory: func() proto.Marshaler {
93+
return &WriteRequest{Timeseries: ts}
94+
},
95+
clean: func(in interface{}) {},
96+
},
97+
{
98+
name: "byte pool",
99+
writeRequestFactory: func() proto.Marshaler {
100+
w := &PreallocWriteRequest{}
101+
w.Timeseries = ts
102+
return w
103+
},
104+
clean: func(in interface{}) {
105+
ReuseWriteRequest(in.(*PreallocWriteRequest))
106+
},
107+
},
108+
{
109+
name: "byte and write pool",
110+
writeRequestFactory: func() proto.Marshaler {
111+
w := PreallocWriteRequestFromPool()
112+
w.Timeseries = ts
113+
return w
114+
},
115+
clean: func(in interface{}) {
116+
ReuseWriteRequest(in.(*PreallocWriteRequest))
117+
},
118+
},
119+
}
120+
121+
for _, tc := range tests {
122+
b.Run(tc.name, func(b *testing.B) {
123+
for i := 0; i < b.N; i++ {
124+
w := tc.writeRequestFactory()
125+
_, err := w.Marshal()
126+
require.NoError(b, err)
127+
tc.clean(w)
128+
}
129+
b.ReportAllocs()
130+
})
131+
}
132+
}

pkg/distributor/distributor.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -835,14 +835,15 @@ func (d *Distributor) send(ctx context.Context, ingester ring.InstanceDesc, time
835835
if err != nil {
836836
return err
837837
}
838-
c := h.(ingester_client.IngesterClient)
838+
c := h.(ingester_client.HealthAndIngesterClient)
839839

840-
req := cortexpb.WriteRequest{
841-
Timeseries: timeseries,
842-
Metadata: metadata,
843-
Source: source,
844-
}
845-
_, err = c.Push(ctx, &req)
840+
req := cortexpb.PreallocWriteRequestFromPool()
841+
req.Timeseries = timeseries
842+
req.Metadata = metadata
843+
req.Source = source
844+
845+
_, err = c.PushPreAlloc(ctx, req)
846+
cortexpb.ReuseWriteRequest(req)
846847

847848
if len(metadata) > 0 {
848849
d.ingesterAppends.WithLabelValues(ingester.Addr, typeMetadata).Inc()

pkg/distributor/distributor_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2688,6 +2688,10 @@ func (i *mockIngester) Close() error {
26882688
return nil
26892689
}
26902690

2691+
func (i *mockIngester) PushPreAlloc(ctx context.Context, in *cortexpb.PreallocWriteRequest, opts ...grpc.CallOption) (*cortexpb.WriteResponse, error) {
2692+
return i.Push(ctx, &in.WriteRequest, opts...)
2693+
}
2694+
26912695
func (i *mockIngester) Push(ctx context.Context, req *cortexpb.WriteRequest, opts ...grpc.CallOption) (*cortexpb.WriteResponse, error) {
26922696
i.Lock()
26932697
defer i.Unlock()

pkg/ingester/client/client.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
package client
22

33
import (
4+
"context"
45
"flag"
56

7+
"github.com/cortexproject/cortex/pkg/cortexpb"
8+
"github.com/cortexproject/cortex/pkg/util/grpcclient"
9+
610
"github.com/go-kit/log"
711
"github.com/prometheus/client_golang/prometheus"
812
"github.com/prometheus/client_golang/prometheus/promauto"
913
"google.golang.org/grpc"
1014
"google.golang.org/grpc/health/grpc_health_v1"
11-
12-
"github.com/cortexproject/cortex/pkg/util/grpcclient"
1315
)
1416

1517
var ingesterClientRequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
@@ -24,6 +26,7 @@ type HealthAndIngesterClient interface {
2426
IngesterClient
2527
grpc_health_v1.HealthClient
2628
Close() error
29+
PushPreAlloc(ctx context.Context, in *cortexpb.PreallocWriteRequest, opts ...grpc.CallOption) (*cortexpb.WriteResponse, error)
2730
}
2831

2932
type closableHealthAndIngesterClient struct {
@@ -32,6 +35,15 @@ type closableHealthAndIngesterClient struct {
3235
conn *grpc.ClientConn
3336
}
3437

38+
func (c *closableHealthAndIngesterClient) PushPreAlloc(ctx context.Context, in *cortexpb.PreallocWriteRequest, opts ...grpc.CallOption) (*cortexpb.WriteResponse, error) {
39+
out := new(cortexpb.WriteResponse)
40+
err := c.conn.Invoke(ctx, "/cortex.Ingester/Push", in, out, opts...)
41+
if err != nil {
42+
return nil, err
43+
}
44+
return out, nil
45+
}
46+
3547
// MakeIngesterClient makes a new IngesterClient
3648
func MakeIngesterClient(addr string, cfg Config) (HealthAndIngesterClient, error) {
3749
dialOpts, err := cfg.GRPCClientConfig.DialOption(grpcclient.Instrument(ingesterClientRequestDuration))

0 commit comments

Comments
 (0)