Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

grpc: fix regression by freeing request bufferslice after processing unary #7571

Merged
merged 2 commits into from
Aug 30, 2024

Conversation

coxley
Copy link
Contributor

@coxley coxley commented Aug 29, 2024

Summary

I was excited to see recycled buffers be released (#6619), and wanted to gauge performance before putting into production. It was surprising to see that the default codec in v1.65.0 performed better than the new one in v1.66.0.

There was a missed (mem.BufferSlice).Free() in the server during processUnaryRPC, leaving the incoming request bytes hanging around while still paying the added allocations that comes with managing the buffers.

I'm not super attached to the added BufferSlice docs, but it would have saved me a lot of time trying to figure out what was wrong with my interaction code. It wasn't clear to me that sometimes the provided slice kept a reference to the buffer, and would be released by SDK logic.

Testing

This is far from a pure benchmark (bufconn, client, etc contributing to allocs). However, it demonstrates the regression and fix.

Results

# Regression
> go test -bench=. -benchmem
goos: darwin
goarch: arm64
pkg: scratch
cpu: Apple M1 Pro
BenchmarkCodec/v1.66.0/sm-10               55629             20031 ns/op            9723 B/op        188 allocs/op
BenchmarkCodec/v1.66.0/md-10               52221             22292 ns/op           16907 B/op        187 allocs/op
BenchmarkCodec/v1.66.0/lg-10               29656             40643 ns/op           76818 B/op        198 allocs/op
PASS
ok      scratch 4.972s

# Baseline
> go test -bench=. -benchmem
goos: darwin
goarch: arm64
pkg: scratch
cpu: Apple M1 Pro
BenchmarkCodec/v1.65.0/sm-10               55014             20053 ns/op            9550 B/op        183 allocs/op
BenchmarkCodec/v1.65.0/md-10               52394             22663 ns/op           19042 B/op        183 allocs/op
BenchmarkCodec/v1.65.0/lg-10               28153             42928 ns/op          109144 B/op        189 allocs/op
PASS
ok      scratch 4.998s

# This PR
➜ go test -bench=. -benchmem
goos: darwin
goarch: arm64
pkg: scratch
cpu: Apple M1 Pro
BenchmarkCodec/fix/sm-10           55530             19899 ns/op            9724 B/op        188 allocs/op
BenchmarkCodec/fix/md-10           55821             21535 ns/op           12742 B/op        183 allocs/op
BenchmarkCodec/fix/lg-10           32937             38130 ns/op           43673 B/op        190 allocs/op
PASS
ok      scratch 5.058s

Code:

package main

import (
	"context"
	"net"
	"os"
	"regexp"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	_ "google.golang.org/grpc/encoding/proto"
	health "google.golang.org/grpc/health/grpc_health_v1"
	"google.golang.org/grpc/test/bufconn"
)

var (
	small  = &health.HealthCheckRequest{Service: "Foo Bar"}
	medium = &health.HealthCheckRequest{Service: strings.Repeat(".", 3<<10)}
	large  = &health.HealthCheckRequest{Service: strings.Repeat(".", 31<<10)}
	msgs   = []*health.HealthCheckRequest{small, medium, large}
)

// hacky way to show the grpc version in the benchmark
func grpcVersion(t testing.TB) string {
	fb, err := os.ReadFile("go.mod")
	require.NoError(t, err)

	versionRE := regexp.MustCompile(`google.golang.org/grpc (\S*)`)
	matches := versionRE.FindStringSubmatch(string(fb))
	require.Len(t, matches, 2)

	replaceRE := regexp.MustCompile(`replace google.golang.org/grpc`)
	if replaceRE.Match(fb) {
		return "fix"
	}
	return matches[1]
}

func BenchmarkCodec(b *testing.B) {
	ctx := context.Background()
	client := testClient(b)

	names := []string{"sm", "md", "lg"}
	b.Run(grpcVersion(b), func(b *testing.B) {
		for i, msg := range msgs {
			b.Run(names[i], func(b *testing.B) {
				for range b.N {
					_, err := client.Check(ctx, msg)
					require.NoError(b, err)
				}
			})
		}
	})
}

func testClient(t testing.TB) health.HealthClient {
	t.Helper()

	srv := grpc.NewServer()
	handler := new(handler)
	health.RegisterHealthServer(srv, handler)

	lis := bufconn.Listen(1 << 20)
	go func() {
		require.NoError(t, srv.Serve(lis))
	}()

	t.Cleanup(func() {
		srv.GracefulStop()
	})

	conn, err := grpc.NewClient(
		"passthrough://bufnet",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) {
			return lis.Dial()
		}),
	)
	require.NoError(t, err)

	healthClient := health.NewHealthClient(conn)
	return healthClient
}

type handler struct {
	health.UnimplementedHealthServer
}

func (handler) Check(context.Context, *health.HealthCheckRequest) (*health.HealthCheckResponse, error) {
	return new(health.HealthCheckResponse), nil
}

RELEASE NOTES:

  • Fix minor server-side regression when pooling large (>1KiB) incoming request buffers

Copy link

linux-foundation-easycla bot commented Aug 29, 2024

CLA Signed

The committers listed above are authorized under a signed CLA.

Copy link

codecov bot commented Aug 29, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 81.76%. Comparing base (55d820d) to head (6deaf1e).
Report is 3 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #7571      +/-   ##
==========================================
- Coverage   81.92%   81.76%   -0.16%     
==========================================
  Files         361      361              
  Lines       27825    27826       +1     
==========================================
- Hits        22796    22753      -43     
- Misses       3840     3868      +28     
- Partials     1189     1205      +16     
Files with missing lines Coverage Δ
mem/buffer_slice.go 96.20% <ø> (ø)
server.go 82.34% <100.00%> (+0.11%) ⬆️

... and 20 files with indirect coverage changes

@coxley coxley changed the title grpc: free incoming request bufferslice after unary processing grpc: fix regression by freeing request bufferslice after processing unary Aug 29, 2024
@dfawley
Copy link
Member

dfawley commented Aug 29, 2024

@PapaCharlie would you mind taking a quick look at this?

@PapaCharlie
Copy link
Contributor

Yeah looking now. The tests are passing so I'm not too concerned there, but I'm surprised I missed this. This isn't restricted to unary RPCs, so it may need fixes elsewhere!

@PapaCharlie
Copy link
Contributor

Nevermind, this is the equivalent for stream RPCs: https://github.com/grpc/grpc-go/blob/master/rpc_util.go#L923

Nice catch! I just have a tiny nit, for consistency with the other places defer Free is called

server.go Outdated
@@ -1391,6 +1391,8 @@ func (s *Server) processUnaryRPC(ctx context.Context, t transport.ServerTranspor
}
ctx = NewContextWithServerTransportStream(ctx, stream)
reply, appErr := md.Handler(info.serviceImpl, ctx, df, s.opts.unaryInt)
defer d.Free()
Copy link
Contributor

Choose a reason for hiding this comment

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

Would you mind moving this up near to where d is initialized, at line 1361?

	d, err := recvAndDecompress(&parser{r: stream, bufferPool: s.opts.bufferPool}, stream, dc, s.opts.maxReceiveMessageSize, payInfo, decomp, true)
	if err != nil {
		if e := t.WriteStatus(stream, status.Convert(err)); e != nil {
			channelz.Warningf(logger, s.channelz, "grpc: Server.processUnaryRPC failed to write status: %v", e)
		}
		return err
	}
	defer d.Free()

Just for consistency

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Whoops, of course.

Done!

@dfawley
Copy link
Member

dfawley commented Aug 29, 2024

Let's do a patch release of 1.66 with this fix when it's ready.

@PapaCharlie
Copy link
Contributor

LGTM!

Copy link
Member

@dfawley dfawley left a comment

Choose a reason for hiding this comment

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

Thanks for the fix!

@easwars easwars merged commit 093e099 into grpc:master Aug 30, 2024
14 checks passed
@sluongng
Copy link

sluongng commented Sep 6, 2024

Will there be a v1.66.1 release with this fix included?

@easwars
Copy link
Contributor

easwars commented Sep 6, 2024

Yes, we are planning on doing that very soon.

purnesh42H pushed a commit to purnesh42H/grpc-go that referenced this pull request Sep 10, 2024
purnesh42H pushed a commit to purnesh42H/grpc-go that referenced this pull request Sep 10, 2024
purnesh42H pushed a commit to purnesh42H/grpc-go that referenced this pull request Sep 11, 2024
purnesh42H pushed a commit to purnesh42H/grpc-go that referenced this pull request Sep 11, 2024
purnesh42H added a commit that referenced this pull request Sep 11, 2024
* estats: remove dependency on testing package (#7579)

* grpc: fix regression by freeing request bufferslice after processing unary (#7571)

---------

Co-authored-by: Easwar Swaminathan <easwars@google.com>
Co-authored-by: Codey Oxley <coxley@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants