Skip to content

Commit f8b54e7

Browse files
authored
Cherry-pick #9331 to v1.84.x (#9334)
Original PR: #9331 RELEASE NOTES: N/A
1 parent 30ce1d5 commit f8b54e7

9 files changed

Lines changed: 371 additions & 19 deletions

File tree

internal/envconfig/envconfig.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ var (
150150
// throttling limit if unforeseen issues arise, and it will be removed in a
151151
// future release.
152152
//
153-
// TODO: Remove this env var once v1.83.0 is release.
153+
// TODO: Remove this env var once v1.83.0 is released.
154154
ControlBufferThrottleLimit = uint64FromEnv("GRPC_GO_EXPERIMENTAL_CONTROL_BUFFER_THROTTLE_LIMIT", 100, 1, 10000)
155155

156156
// ALTSMaxFrameSize is the maximum frame size for ALTS in bytes.
@@ -159,6 +159,16 @@ var (
159159
// 512KiB to match altsWriteBufferMaxSize in
160160
// credentials/alts/internal/conn/record.go).
161161
ALTSMaxFrameSize = uint64FromEnv("GRPC_GO_EXPERIMENTAL_ALTS_MAX_FRAME_SIZE", 4096, 4096, 512*1024)
162+
163+
// EnableReceiveBufferCompaction enables the compaction of data buffers
164+
// to reduce the number of buffers in the receive buffer.
165+
//
166+
// This environment variable serves as an escape hatch to disable the
167+
// feature if unforeseen issues arise, and it will be removed in a future
168+
// release.
169+
//
170+
// TODO: Remove this env var once v1.85.0 is released.
171+
EnableReceiveBufferCompaction = boolFromEnv("GRPC_GO_EXPERIMENTAL_ENABLE_RECEIVE_BUFFER_COMPACTION", true)
162172
)
163173

164174
func boolFromEnv(envVar string, def bool) bool {

internal/mem/buffer_pool.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,26 @@ import (
2626
"slices"
2727
"sort"
2828
"sync"
29+
30+
"google.golang.org/grpc/internal"
2931
)
3032

3133
const (
3234
goPageSize = 4 * 1024 // 4KiB. N.B. this must be a power of 2.
3335
)
3436

37+
var (
38+
// BufferPoolingThreshold is the minimum size of a buffer that can be pooled.
39+
// This is used to determine whether to pool buffers or allocate them directly.
40+
BufferPoolingThreshold = 1 << 10
41+
)
42+
43+
func init() {
44+
internal.SetBufferPoolingThresholdForTesting = func(threshold int) {
45+
BufferPoolingThreshold = threshold
46+
}
47+
}
48+
3549
var uintSize = bits.UintSize // use a variable for mocking during tests.
3650

3751
// bufferPool is a copy of the public bufferPool interface used to avoid

internal/transport/handler_server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ func (ht *serverHandlerTransport) HandleStreams(ctx context.Context, startStream
424424
st: ht,
425425
headerWireLength: 0, // won't have access to header wire length until golang/go#18997.
426426
}
427-
s.Stream.buf.init()
427+
s.Stream.buf.init(ht.bufferPool)
428428
s.readRequester = s
429429
s.trReader = transportReader{
430430
reader: recvBufferReader{ctx: s.ctx, ctxDone: s.ctx.Done(), recv: &s.buf},

internal/transport/http2_client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ func (t *http2Client) newStream(ctx context.Context, callHdr *CallHdr, handler s
500500
headerChan: make(chan struct{}),
501501
statsHandler: handler,
502502
}
503-
s.Stream.buf.init()
503+
s.Stream.buf.init(t.bufferPool)
504504
s.Stream.wq.init(defaultWriteQuota, s.done)
505505
s.readRequester = s
506506
// The client side stream context should have exactly the same life cycle with the user provided context.

internal/transport/http2_server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ func (t *http2Server) operateHeaders(ctx context.Context, frame *http2.MetaHeade
407407
st: t,
408408
headerWireLength: int(frame.Header().Length),
409409
}
410-
s.Stream.buf.init()
410+
s.Stream.buf.init(t.bufferPool)
411411
var (
412412
// if false, content-type was missing or invalid
413413
isGRPC = false

internal/transport/transport.go

Lines changed: 104 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,14 @@ import (
3030
"sync"
3131
"sync/atomic"
3232
"time"
33+
"unsafe"
3334

3435
"golang.org/x/net/http2"
3536
"google.golang.org/grpc/codes"
3637
"google.golang.org/grpc/credentials"
3738
"google.golang.org/grpc/internal/channelz"
39+
"google.golang.org/grpc/internal/envconfig"
40+
imem "google.golang.org/grpc/internal/mem"
3841
"google.golang.org/grpc/internal/transport/internal"
3942
"google.golang.org/grpc/keepalive"
4043
"google.golang.org/grpc/mem"
@@ -45,7 +48,30 @@ import (
4548
"google.golang.org/grpc/tap"
4649
)
4750

48-
const logLevel = 2
51+
const (
52+
logLevel = 2
53+
// recvMsgSize estimates the memory overhead of a recvMsg in the backlog.
54+
// It accounts for the recvMsg struct itself and the slice header of the
55+
// underlying buffer's data.
56+
recvMsgSize = int(unsafe.Sizeof(recvMsg{}) + unsafe.Sizeof([]byte{}))
57+
58+
// utilizationFactor controls when we consider memory utilization acceptable.
59+
// When backlogHeapSize / payloadSize <= utilizationFactor (meaning at least
60+
// 50% of the heap memory is actual payload data), compaction is skipped.
61+
utilizationFactor = 2
62+
)
63+
64+
var (
65+
// compactionThreshold is approx 57KB (on 64-bit systems). It allows
66+
// accumulating up to 1024 1-byte payloads before triggering compaction.
67+
//
68+
// Because individual payloads <= 1024 bytes are allocated on the heap
69+
// outside mem.BufferPool, waiting for at least 1024 bytes to accumulate
70+
// ensures that compaction coalesces those small heap allocations into a
71+
// single large buffer from mem.BufferPool, enabling buffer reuse while
72+
// avoiding frequent copying for small bursts of frames.
73+
compactionThreshold = imem.BufferPoolingThreshold * (recvMsgSize + 1)
74+
)
4975

5076
func init() {
5177
internal.TimeNowFunc = func() int64 { return time.Now().UnixNano() }
@@ -71,23 +97,31 @@ type recvBuffer struct {
7197
c chan recvMsg
7298
mu sync.Mutex
7399
backlog []recvMsg
74-
err error
100+
// uncompactedSuffixLen tracks the number of consecutive data messages at
101+
// the tail of backlog that have not been compacted.
102+
uncompactedSuffixLen int
103+
// uncompactedBytes tracks the total payload bytes across the trailing
104+
// uncompactedSuffixLen messages.
105+
uncompactedBytes int
106+
err error
107+
bufPool mem.BufferPool
75108
}
76109

77110
// init allows a recvBuffer to be initialized in-place, which is useful
78111
// for resetting a buffer or for avoiding a heap allocation when the buffer
79112
// is embedded in another struct.
80-
func (b *recvBuffer) init() {
113+
func (b *recvBuffer) init(pool mem.BufferPool) {
81114
b.c = make(chan recvMsg, 1)
115+
b.bufPool = pool
82116
}
83117

84118
func (b *recvBuffer) put(r recvMsg) {
85119
b.mu.Lock()
120+
defer b.mu.Unlock()
86121
if b.err != nil {
87122
// drop the buffer on the floor. Since b.err is not nil, any subsequent reads
88123
// will always return an error, making this buffer inaccessible.
89124
r.buffer.Free()
90-
b.mu.Unlock()
91125
// An error had occurred earlier, don't accept more
92126
// data or errors.
93127
return
@@ -96,20 +130,84 @@ func (b *recvBuffer) put(r recvMsg) {
96130
if len(b.backlog) == 0 {
97131
select {
98132
case b.c <- r:
99-
b.mu.Unlock()
100133
return
101134
default:
102135
}
103136
}
104137
b.backlog = append(b.backlog, r)
105-
b.mu.Unlock()
138+
b.compactBacklogLocked(r)
139+
}
140+
141+
func (b *recvBuffer) compactBacklogLocked(r recvMsg) {
142+
if !envconfig.EnableReceiveBufferCompaction {
143+
return
144+
}
145+
if r.buffer == nil {
146+
b.uncompactedBytes = 0
147+
b.uncompactedSuffixLen = 0
148+
return
149+
}
150+
151+
b.uncompactedSuffixLen++
152+
b.uncompactedBytes += r.buffer.Len()
153+
backlogHeapSize := b.uncompactedSuffixLen*recvMsgSize + b.uncompactedBytes
154+
155+
// If the memory overhead is less than 50% of the heap usage (e.g., because
156+
// a large DATA frame arrived), the average message size in the suffix is
157+
// large enough that memory bloat is not a concern. Reset suffix tracking.
158+
if backlogHeapSize <= utilizationFactor*b.uncompactedBytes {
159+
b.uncompactedBytes = 0
160+
b.uncompactedSuffixLen = 0
161+
return
162+
}
163+
// Avoid compacting too frequently for short bursts of small frames.
164+
// Wait until we have accumulated at least ~1024 small messages (~57 KB).
165+
if backlogHeapSize <= compactionThreshold {
166+
// Still can accumulate more payloads.
167+
return
168+
}
169+
170+
// Since the memory utilization is less than 50%, the average payload size
171+
// of each recvMsg must be less than recvMsgSize (approx 56 bytes).
172+
// In the worst case for bytes copied (where the average payload is just
173+
// below recvMsgSize), compaction will occur once every:
174+
// compactionThreshold / (recvMsgSize + avg_payload) = ~520 messages,
175+
// copying ~29KB of data.
176+
177+
start := 0
178+
newBuf := b.bufPool.Get(b.uncompactedBytes)
179+
startIdx := len(b.backlog) - b.uncompactedSuffixLen
180+
181+
for i := startIdx; i < len(b.backlog); i++ {
182+
m := b.backlog[i]
183+
b.backlog[i] = recvMsg{}
184+
start += copy((*newBuf)[start:], m.buffer.ReadOnlyData())
185+
m.buffer.Free()
186+
}
187+
b.backlog[startIdx] = recvMsg{
188+
buffer: mem.NewBuffer(newBuf, b.bufPool),
189+
}
190+
b.backlog = b.backlog[:startIdx+1]
191+
// After compaction, the suffix is replaced with a single message containing
192+
// the combined payload. The new utilization is close to 1.0 (overhead of
193+
// one recvMsg relative to the large compacted payload), which is well
194+
// below the utilization factor of 2.
195+
b.uncompactedBytes = 0
196+
b.uncompactedSuffixLen = 0
106197
}
107198

108199
func (b *recvBuffer) load() {
109200
b.mu.Lock()
110201
if len(b.backlog) > 0 {
111202
select {
112203
case b.c <- b.backlog[0]:
204+
// backlog[0] is only part of the tracked uncompacted suffix if the
205+
// entire backlog currently consists of the suffix. If an earlier
206+
// compaction or reset occurred, backlog[0] is already compacted.
207+
if envconfig.EnableReceiveBufferCompaction && b.uncompactedSuffixLen == len(b.backlog) {
208+
b.uncompactedSuffixLen--
209+
b.uncompactedBytes -= b.backlog[0].buffer.Len()
210+
}
113211
b.backlog[0] = recvMsg{}
114212
b.backlog = b.backlog[1:]
115213
default:

0 commit comments

Comments
 (0)