Skip to content

alts: Release read buffer when blocked on socket read#8964

Merged
arjan-bal merged 8 commits into
grpc:masterfrom
arjan-bal:alts-read-buffer-pool
Apr 1, 2026
Merged

alts: Release read buffer when blocked on socket read#8964
arjan-bal merged 8 commits into
grpc:masterfrom
arjan-bal:alts-read-buffer-pool

Conversation

@arjan-bal

@arjan-bal arjan-bal commented Mar 9, 2026

Copy link
Copy Markdown
Contributor

Problem

Normally, since the Go net.Conn interface provides the abstraction of a blocking read to hide the complexity of non-blocking I/O (epoll, kqueue), users need to pass a read buffer to the net.Conn.Read call. Even when the TCP socket doesn't have data, the application needs to hold onto the read buffer. This results in the ALTS conn and the gRPC HTTP/2 stack holding on to 32KB read buffers each, even for non-readable transports.

Solution

On Unix platforms, there is a RawConn interface that exposes a non-blocking mechanism. The idea is that the Go runtime will call a user-registered callback when the socket is readable. gRPC can use this callback to allocate a buffer from the pool and return it once it has passed the plaintext to the HTTP/2 layer. The same RawConn interface is also available in other OSs, but with slightly different method signatures for the blocking Read method. In the future, we can add the same optimization for them and have CI runners to catch regressions.

The main abstraction that allows these non-memory-pinning reads is the following interface:

// ReadyReader is an optional interface that can be implemented by net.Conn
// implementations to enable gRPC to perform non-memory-pinning reads.
type ReadyReader interface {
	// ReadOnReady waits for data to arrive, fetches a buffer, and performs a
	// read. It returns a pointer to the buffer so you can return it to the pool
	// later.
	ReadOnReady(bufSize int, pool mem.BufferPool) (*[]byte, int, error)
}

In this PR, an implementation is provided that wraps a RawConn. This allows the ALTS conn to perform efficient reads.

In a future PR, the following changes will enable getting rid of the bufio.Reader in the HTTP/2 layer:

  1. ReadyReader will be implemented by the ALTS conn.
  2. gRPC will implement its own buffered reader that releases the buffer when it's empty.
  3. The buffered reader will call ReadOnReady() instead of Read() on the underlying conn, if supported, to delay the re-allocation of the buffer.

Benchmarks

The following micro-benchmarks show no regression in QPS (LargeMessage test) and a significant reduction in memory usage while performing reads (ReadMemoryUsage). There is an increase in 2 allocs in conn construction due to the use of pointer fields for the ReadyReader and read buffer handle, but these happen only when creating a subchannel, not per-RPC. There is an increase in sec/op for the WriteMemoryUsage test, but these tests are not meant to measure conn construction time, only the memory effeciency.

goos: linux
goarch: amd64
pkg: google.golang.org/grpc/credentials/alts/internal/conn
cpu: Intel(R) Xeon(R) CPU @ 2.60GHz
                    │   old.txt   │               new.txt               │
                    │   sec/op    │   sec/op     vs base                │
LargeMessage-48       77.54m ± 2%   76.51m ± 1%        ~ (p=0.202 n=15)
WriteMemoryUsage-48   6.816µ ± 1%   7.519µ ± 1%  +10.31% (p=0.000 n=15)
ReadMemoryUsage-48    9.754µ ± 1%   7.021µ ± 1%  -28.02% (p=0.000 n=15)
geomean               172.7µ        159.3µ        -7.81%

                    │   old.txt    │               new.txt                │
                    │     B/op     │     B/op      vs base                │
LargeMessage-48       4.578Mi ± 0%   4.579Mi ± 7%        ~ (p=0.373 n=15)
WriteMemoryUsage-48   41.60Ki ± 0%   41.77Ki ± 0%   +0.39% (p=0.000 n=15)
ReadMemoryUsage-48    83.06Ki ± 0%   43.25Ki ± 0%  -47.94% (p=0.000 n=15)
geomean               253.0Ki        203.8Ki       -19.44%

                    │  old.txt   │               new.txt                │
                    │ allocs/op  │ allocs/op   vs base                  │
LargeMessage-48       2.000 ± 0%   2.000 ± 0%        ~ (p=1.000 n=15) ¹
WriteMemoryUsage-48   5.000 ± 0%   7.000 ± 0%  +40.00% (p=0.000 n=15)
ReadMemoryUsage-48    16.00 ± 0%   18.00 ± 0%  +12.50% (p=0.000 n=15)
geomean               5.429        6.316       +16.35%

In a real-world benchmark, where a GCS directpath client downloads a file in a loop, the average "in use" memory falls from ~35MB to ~20MB.

RELEASE NOTES:

  • alts: pool read buffers to lower memory utilization when sockets are unreadable.

@arjan-bal arjan-bal added this to the 1.81 Release milestone Mar 9, 2026
@arjan-bal arjan-bal requested a review from easwars March 9, 2026 06:39
@arjan-bal arjan-bal added the Type: Performance Performance improvements (CPU, network, memory, etc) label Mar 9, 2026
@arjan-bal arjan-bal changed the title non-blocking read alts: Release read buffer when blocked on socket reads Mar 9, 2026
@codecov

codecov Bot commented Mar 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.13043% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.09%. Comparing base (b71c262) to head (0cae26e).
⚠️ Report is 5 commits behind head on master.

Files with missing lines Patch % Lines
internal/transport/ready_reader.go 75.00% 5 Missing and 3 partials ⚠️
credentials/alts/internal/conn/record.go 95.65% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #8964      +/-   ##
==========================================
+ Coverage   83.01%   83.09%   +0.08%     
==========================================
  Files         411      413       +2     
  Lines       32960    33051      +91     
==========================================
+ Hits        27361    27464     +103     
+ Misses       4196     4191       -5     
+ Partials     1403     1396       -7     
Files with missing lines Coverage Δ
internal/mem/buffer_pool.go 96.52% <100.00%> (+0.09%) ⬆️
internal/transport/raw_conn_linux.go 100.00% <100.00%> (ø)
credentials/alts/internal/conn/record.go 85.00% <95.65%> (+2.79%) ⬆️
internal/transport/ready_reader.go 75.00% <75.00%> (ø)

... and 24 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@arjan-bal arjan-bal force-pushed the alts-read-buffer-pool branch from a937698 to bc023c9 Compare March 9, 2026 09:15
@arjan-bal arjan-bal changed the title alts: Release read buffer when blocked on socket reads alts: Release read buffer when blocked on socket read Mar 9, 2026
@easwars

easwars commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

@arjan-bal : Can you please merge changes from master when you get a chance to get the CI going. Thanks.

@easwars easwars left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked much at the alts changes. Should we get someone from the security team to review that?

Comment thread internal/transport/raw_conn_nonlinux.go
Comment thread internal/transport/ready_reader.go Outdated
Comment thread internal/transport/ready_reader.go Outdated
Comment thread internal/transport/ready_reader.go Outdated
Comment thread internal/transport/ready_reader.go Outdated
Comment thread internal/transport/ready_reader_test.go Outdated
Comment thread internal/transport/ready_reader_test.go Outdated
Comment thread internal/mem/buffer_pool.go
Comment thread internal/mem/buffer_pool.go Outdated
Comment thread credentials/alts/internal/conn/record.go Outdated
@easwars easwars assigned arjan-bal and unassigned easwars Mar 27, 2026
@arjan-bal arjan-bal removed their assignment Mar 27, 2026
@arjan-bal arjan-bal requested a review from gtcooke94 March 27, 2026 10:13
@arjan-bal

Copy link
Copy Markdown
Contributor Author

I haven't looked much at the alts changes. Should we get someone from the security team to review that?

Adding @gtcooke94 for reviewing the ALTS changes.

@arjan-bal arjan-bal force-pushed the alts-read-buffer-pool branch from b169d72 to f226417 Compare March 27, 2026 10:27

@easwars easwars left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM for the non-alts files

@easwars easwars removed their assignment Mar 31, 2026
@arjan-bal

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a ReadyReader interface and a SimpleBufferPool to optimize memory usage during ALTS record reads by avoiding long-term memory pinning when connections are idle. It includes platform-specific implementations for non-blocking reads on Unix systems and updates the ALTS connection logic to utilize these pooled buffers. The review feedback correctly identifies several compilation errors in the new test file ready_reader_test.go where values received from testutils.Channel (which return interface{}) are compared directly to integers without necessary type assertions.

Comment thread internal/transport/ready_reader_test.go
Comment thread internal/transport/ready_reader_test.go
Comment thread internal/transport/ready_reader_test.go
Comment thread credentials/alts/internal/conn/record.go
Comment thread credentials/alts/internal/conn/record.go Outdated
@arjan-bal arjan-bal force-pushed the alts-read-buffer-pool branch from a14f26a to b6b7d6c Compare March 31, 2026 17:44
@arjan-bal arjan-bal requested a review from gtcooke94 March 31, 2026 18:01
@arjan-bal arjan-bal assigned arjan-bal and unassigned gtcooke94 Mar 31, 2026
@arjan-bal arjan-bal force-pushed the alts-read-buffer-pool branch from ef4bb79 to 9636c85 Compare March 31, 2026 18:24
@arjan-bal arjan-bal assigned gtcooke94 and unassigned arjan-bal Mar 31, 2026
@arjan-bal arjan-bal assigned arjan-bal and unassigned gtcooke94 Apr 1, 2026
@arjan-bal arjan-bal merged commit c33aa45 into grpc:master Apr 1, 2026
13 of 14 checks passed
@arjan-bal arjan-bal deleted the alts-read-buffer-pool branch April 1, 2026 07:00
arjan-bal added a commit that referenced this pull request Apr 1, 2026
## Problem
The `ReadyReader` interface was added to support non-blocking reads
using the `syscall` package in #8964. While working on [follow-up
changes](https://github.com/grpc/grpc-go/compare/master...arjan-bal:h2-read-buffer-pool?expand=1)
to support pooling read buffers in the http2 layer, I noticed test
failures where servers were closing the TCP connection, but the client
channel remained in the READY state.

## Root Cause
The [unix.Read](https://pkg.go.dev/golang.org/x/sys/unix#Read) function
doesn't consider the graceful closure of a connection to be an error and
instead returns `(0, nil)`. Since consumers of the `io.Reader` interface
expect an `io.EOF` error in such cases, the translation from `(0, nil)`
to `(0, io.EOF)` needs to be handled by the reader implementation.

RELEASE NOTES: N/A
arjan-bal added a commit that referenced this pull request Apr 22, 2026
## Problem
The HTTP/2 framer in gRPC uses a `bufio.Reader` with a 32KB buffer by
default. When there are a large number of transports, these buffers
consume significant memory, even when the transport is idle.

## Solution
#8964 added a `ReadyReader` interface that allows non-memory-pinning
reads. This PR replaces the standard `bufio.Reader` with a custom
`io.Reader` implementation that uses pooled buffers and releases the
buffer once all data is consumed.

To defer the re-allocation of the read buffer, the reader calls
`ReadOnReady` on the underlying `io.Reader`. For this to work, the
underlying `io.Reader` must implement either the `ReadyReader` interface
or `syscall.RawConn`. If neither condition is met, the framer gracefully
falls back to using the regular `bufio.Reader`.

Additional Changes:
* The ALTS connection has been refactored to implement the `ReadyReader`
interface.
* The write buffer pools used by the framer are updated to use the
`mem.BufferPool` interface, allowing the pools to be shared across both
read and write operations.
* Use `syscall.Read` instead of `unix.Read` to avoid triggering the race
detector, see comment for details.
* Add environment variable protection for the changes to allow fast
rollback.

## Benchmarks
In a [real-world
benchmark](https://github.com/arjan-bal/custom-go-client-benchmark/tree/retry-dp),
where a GCS directpath client downloads a file in a loop, the average
"in use" memory falls from 28.3MB to 21.3MB (-24%).

Local Benchmarks show no significant difference
```
❯ go run benchmark/benchresult/main.go streaming-before streaming-after                 
streaming-networkMode_Local-bufConn_false-keepalive_false-benchTime_2m0s-trace_false-latency_0s-kbps_0-MTU_0-maxConcurrentCa
lls_120-reqSize_1024B-respSize_1024B-compressor_off-channelz_false-preloader_false-clientReadBufferSize_-1-clientWriteBuffer
Size_-1-serverReadBufferSize_-1-serverWriteBufferSize_-1-sleepBetweenRPCs_0s-connections_1-recvBufferPool_simple-sharedWrite
Buffer_true
               Title       Before        After Percentage
            TotalOps     29981273     29966908    -0.05%
             SendOps            0            0      NaN%
             RecvOps            0            0      NaN%
            Bytes/op      4971.06      4971.41     0.00%
           Allocs/op        19.79        19.79     0.00%
             ReqT/op 2046721570.13 2045740919.47    -0.05%
            RespT/op 2046721570.13 2045740919.47    -0.05%
            50th-Lat    461.523µs    460.906µs    -0.13%
            90th-Lat    654.435µs    655.327µs     0.14%
            99th-Lat   1.225856ms   1.240984ms     1.23%
             Avg-Lat    478.845µs    479.553µs     0.15%
           GoVersion     go1.25.0     go1.25.0
         GrpcVersion   1.81.0-dev   1.81.0-dev
```

RELEASE NOTES:
* transport: Pool HTTP/2 framer read buffers to reduce idle memory
consumption. Currently limited to Linux for ALTS and non-encrypted
transports (TCP, Unix). To disable, set
`GRPC_GO_EXPERIMENTAL_HTTP_FRAMER_READ_BUFFER_POOLING=false` and report
any issues.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Type: Performance Performance improvements (CPU, network, memory, etc)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants