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

DEPS: upgrade grpc to v1.68.0 #136278

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
Draft

DEPS: upgrade grpc to v1.68.0 #136278

wants to merge 15 commits into from

Conversation

tbg
Copy link
Member

@tbg tbg commented Nov 27, 2024

TODO:

  • benchmark
  • sort out major alloc regression due to DecompressedSize no longer being used (ref)
  • fork and default GRPC_ENFORCE_ALPN_ENABLED to false on the fork (see comment below) - done, switched to fork.
  • figure out why v1.56.3 isn't doing proper ALPN: filed server: gRPC server does not set NextProtocols #136367

From v1.56.3 to v1.68.0. Full commit list below1 created by script2.

The main benefit of the upgrade is that we can benefit from some great work they did to reuse memory (i.e. reduce memory pressure). Part of this is internal, but one significant new piece is exposed through the CodecV2 interface, we currently implement currently a CodecV0. They also moved to protov2 internally, so their default CodecV2 would be slow for us (due to the need to "dress up" the protov1 messages as v2, if it would work at all).

Details

Here's where their default new codec transforms protov1 messages:

/encoding/proto/proto.go#L69-L70

func (c *codecV2) Unmarshal(data mem.BufferSlice, v any) (err error) {
	vv := messageV2Of(v)

gRPC internally operates on protoV2 messages. We would hand it v1 messages, meaning messageV2Of will call this code which wraps the message in an interface:

// legacyWrapMessage wraps v as a protoreflect.Message,
// where v must be a *struct kind and not implement the v2 API already.
func legacyWrapMessage(v reflect.Value) protoreflect.Message {
	t := v.Type()
	if t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct {
		return aberrantMessage{v: v}
	}
	mt := legacyLoadMessageInfo(t, "")
	return mt.MessageOf(v.Interface())
}

I think there's reflection involved. This is probably not efficient. This conversion is new as of grpc/grpc-go#6965, shortly after gRPC moved to protoV2 internally.

Here's what CodecV2 looks like.

/encoding/encoding_v2.go#L31-L39

	// Marshal returns the wire format of v. The buffers in the returned
	// [mem.BufferSlice] must have at least one reference each, which will be freed
	// by gRPC when they are no longer needed.
	Marshal(v any) (out mem.BufferSlice, err error)
	// Unmarshal parses the wire format into v. Note that data will be freed as soon
	// as this function returns. If the codec wishes to guarantee access to the data
	// after this function, it must take its own reference that it frees when it is
	// no longer needed.
	Unmarshal(data mem.BufferSlice, v any) error

Note the mem.BufferSlice. So basically, whatever the implementation (that's our code) needs to allocate, it can put it into a mem.BufferSlice, which one can get like this:

/encoding/proto/proto.go#L57-L58

		pool := mem.DefaultBufferPool()
		buf := pool.Get(size)

then when gRPC is done with the buffer, it will release it back into the pool. This seems pretty nice, but it means that our actual proto unmarshaling code (rather, gogoprotos) would need to plumb down this buffer pool. In their default v2 codec, they run into this same issue with google-protobuf:

/encoding/proto/proto.go#L75-L80

	buf := data.MaterializeToBuffer(mem.DefaultBufferPool())
	defer buf.Free()
	// TODO: Upgrade proto.Unmarshal to support mem.BufferSlice. Right now, it's not
	//  really possible without a major overhaul of the proto package, but the
	//  vtprotobuf library may be able to support this.
	return proto.Unmarshal(buf.ReadOnlyData(), vv)

Our codec is here (a legacy V1 codec):

/pkg/rpc/codec.go#L23-L42

type codec struct{}

var _ encoding.Codec = codec{}

func (codec) Marshal(v interface{}) ([]byte, error) {
	if pm, ok := v.(proto.Marshaler); ok {
		return pm.Marshal()
	}
	return gproto.Marshal(v.(gproto.Message))
}

func (codec) Unmarshal(data []byte, v interface{}) error {
	if pm, ok := v.(proto.Unmarshaler); ok {
		return pm.Unmarshal(data)
	}
	return gproto.Unmarshal(data, v.(gproto.Message))
}

func (codec) Name() string {
	return name
}

Perf-related commits3 below.

Closes #134971.
Epic: CRDB-43584

Footnotes

  1. https://github.com/cockroachdb/cockroach/pull/136278#issuecomment-2503756022

  2. https://gist.github.com/tbg/c518ba3844f94abf4fff826f13be5300

  3. git log ^v1.56.3 v1.68.0 --grep pool --grep reuse --grep memory --grep perf --grep alloc --grep bench --oneline | sed -e 's/#/grpc\/grpc-go#/g' | sed -E -e 's~^([0-9a-f]+)~\[\1\](https://github.com/grpc/grpc-go/commit/\1)~' | sed -e 's/^/- /' and also read the release notes for all intermediate releases.

@cockroach-teamcity
Copy link
Member

This change is Reviewable

@tbg tbg added the o-perf-efficiency Related to performance efficiency label Nov 27, 2024
@tbg tbg force-pushed the grpc-1.68.0 branch 2 times, most recently from 142b2a1 to 27e5b9d Compare November 27, 2024 12:08
@tbg
Copy link
Member Author

tbg commented Nov 27, 2024

Full PR/commit list

@tbg
Copy link
Member Author

tbg commented Nov 28, 2024

re: the acceptance/version-upgrade failure, I see the problem and think we'll need a small fork to work around it. But I also need to figure out why the problem exists. Roughly speaking, v1.68.0 (and older versions, but not v1.56.3) has a check that all connections use ALPN. We see this check fire whenever cockroach as of this PR tries to interop with any cockroach pre this PR.

The most self-contained way to see this is

roachprod create -n 3 local
roachprod put local:1-2 cockroach-master cockroach # some master SHA, or previous release, your call, does not matter
roachprod put local:3 cockroach-grpc cockroach # this PR

n3 will not manage to join:

W241128 13:28:08.615942 205 server/init.go:402 ⋮ [T1,Vsystem,n?] 25 outgoing join rpc to ‹127.0.0.1:29000› unsuccessful: ‹rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: credentials: cannot check peer: missing selected ALPN property"›

These checks were introduced in grpc/grpc-go#7184.

If we start n3 with --env GRPC_ENFORCE_ALPN_ENABLED=false, it works:

roachprod start local:3 --env GRPC_ENFORCE_ALPN_ENABLED=false

Now, due to mixed version constraints, we definitely have to bypass this check. We can't reliably inject an env var in our customer environments, and we can't mutate EnforceALPNEnabled at init() time (since it's in an internal package tree), so we need a fork that simply changes the default to false. Easy enough; I'll get on that.


What I don't understand why the old release isn't getting this right. After all, we're using gRPC, and gRPC fills this in by default - both now and back in v1.56.3.

The way this should work is as follows. Assume we're the "master" binary, i.e. cockroach with gRPC 1.56.3.

Our gRPC server has its credentials defined here:

/pkg/rpc/context.go#L112-L118

	if !rpcCtx.ContextOptions.Insecure {
		tlsConfig, err := rpcCtx.GetServerTLSConfig()
		if err != nil {
			return nil, sii, err
		}
		grpcOpts = append(grpcOpts, grpc.Creds(credentials.NewTLS(tlsConfig)))
	}

which calls into this

/credentials/tls.go#L141-L146

// NewTLS uses c to construct a TransportCredentials based on TLS.
func NewTLS(c *tls.Config) TransportCredentials {
	tc := &tlsCreds{credinternal.CloneTLSConfig(c)}
	tc.config.NextProtos = credinternal.AppendH2ToNextProtos(tc.config.NextProtos)
	return tc
}

note the AppendH2ToNextProtos, which basically makes sure that NextProtos has "h2" in it. This code has shifted around a bit in v1.68.0, but it looks essentially unchanged. And clearly the servers we define using v1.56.3 do support HTTP2, or nothing would be working. A few other places where credentials are defined exist, but they also all call NewTLS and they're also not the one active here (new n3 tries to join old n1, so it's a problem with n1's grpc server creds, which we've looked at above).

I'll need to poke at this some more.

tbg added a commit to cockroachdb/grpc-go that referenced this pull request Nov 28, 2024
CRDB at v1.68.0 fails to communicate with CRDB at v1.56.3 due to this
check. This is strange, since CRDB uses gRPC throughout, and in both
v1.56.3 and v1.68.0 uses `tls.NewConfig` which ensures that `NextProtos`
always contains `h2`.

gRPC introduced this check in grpc#7535.

See: cockroachdb/cockroach#136278 (comment)
@tbg
Copy link
Member Author

tbg commented Nov 28, 2024

This ALPN thing is sorted out, see #136367. Seems benign - we just need to make sure we keep that check disabled for long enough to not have to interop with versions of CRDB that precede this PR.

@tbg
Copy link
Member Author

tbg commented Nov 28, 2024

re: the memory blow-up seen here, I think I have a handle on this as well. sysbench sends relatively small requests, but gRPC (perhaps unwisely) calls io.Copy here

/rpc_util.go#L902-L904

	var out mem.BufferSlice
	_, err = io.Copy(mem.NewWriter(&out, pool), io.LimitReader(dcReader, int64(maxReceiveMessageSize)+1))
	if err != nil {

and io.Copy initially allocates 32kb buffers:

/src/io/io.go#L417-L427

	if buf == nil {
		size := 32 * 1024
		if l, ok := src.(*LimitedReader); ok && int64(size) > l.N {
			if l.N < 1 {
				size = 1
			} else {
				size = int(l.N)
			}
		}
		buf = make([]byte, size)
	}

There should be ways to avoid that, now that we're switching to a fork of gRPC already anyway, but I assume there'll be interest to fix this upstream, too.

@tbg
Copy link
Member Author

tbg commented Nov 29, 2024

I added a patch (+decompsize) that avoids this large regression. Unfortunately, v1.68.0 still allocates more after this patch, and performance is reduced significantly relative to v1.56.3. I poked around at some profiles but found no obvious single source of them. I think in switching to these buffer pools, they also eat lots of extra small allocations due to replacing naked byte slices with wrappers that often need to be heap allocated as well.

name                                   old time/op    new time/op    delta
Sysbench/SQL/3node/oltp_read_write-24    14.2ms ± 3%    14.7ms ± 2%   +3.50%  (p=0.000 n=9+9)

name                                   old alloc/op   new alloc/op   delta
Sysbench/SQL/3node/oltp_read_write-24    2.57MB ± 2%    2.63MB ± 2%   +2.27%  (p=0.001 n=10+10)

name                                   old allocs/op  new allocs/op  delta
Sysbench/SQL/3node/oltp_read_write-24     17.4k ± 1%     20.2k ± 1%  +16.24%  (p=0.000 n=10+10)

This benchmarks simple unary requests across a gRPC server with
CockroachDB-specific settings (snappy compression, etc).
tbg and others added 14 commits December 2, 2024 13:54
Use an existing test service that also does streaming-streaming, which
allows the benchmark to highlight the overhead of unary RPCs as
implemented by gRPC.
These will fire after the gRPC bump in the subsequent commits.
from v1.56.3 to v1.68.0.

TODO: changelog
Similar but different error:

```
link: package conflict error: google.golang.org/genproto/googleapis/cloud/location: package imports google.golang.org/genproto/googleapis/api/annotations
	  was compiled with: @org_golang_google_genproto//googleapis/api/annotations:annotations
	but was linked with: @org_golang_google_genproto_googleapis_api//annotations:annotations
```

Compare with the original:

```
link: package conflict error: cloud.google.com/go/pubsub/apiv1/pubsubpb: package imports google.golang.org/genproto/googleapis/api/annotations
	  was compiled with: @org_golang_google_genproto_googleapis_api//annotations:annotations
	but was linked with: @org_golang_google_genproto//googleapis/api/annotations:annotations
```
Error unchanged from last commit. I did remember to `./dev gen bazel`.

```
link: package conflict error: google.golang.org/genproto/googleapis/cloud/location: package imports google.golang.org/genproto/googleapis/api/annotations
          was compiled with: @org_golang_google_genproto//googleapis/api/annotations:annotations
        but was linked with: @org_golang_google_genproto_googleapis_api//annotations:annotations
```
…f68ea54

See googleapis/go-genproto#1015.

Sadly grpc-gateway is incompatible with this version of `genproto`:

```
ERROR: no such package '@@org_golang_google_genproto//googleapis/api/httpbody': BUILD file not found in directory 'googleapis/api/httpbody' of external repository @@org_golang_google_genproto. Add a BUILD file to a directory to mark it as a package.
ERROR: /private/var/tmp/_bazel_tbg/b1346cddcc70d57afdaa90f7f09f9b2c/external/com_github_grpc_ecosystem_grpc_gateway/runtime/BUILD.bazel:5:11: no such package '@@org_golang_google_genproto//googleapis/api/httpbody': BUILD file not found in directory 'googleapis/api/httpbody' of external repository @@org_golang_google_genproto. Add a BUILD file to a directory to mark it as a package. and referenced by '@@com_github_grpc_ecosystem_grpc_gateway//runtime:go_default_library'
```

Since we are on the final `grpc-gateway/v1` version already[^1], we'll
have to make the leap to v2 to fix this.

[^1]: https://github.com/grpc-ecosystem/grpc-gateway/releases/tag/v1.16.0
This also adds the bytestream and rpc packages from genproto
which are required.

Epic: none
Release note: None
This works around the issue discussed in cockroachdb#136367.  Apparently, we have
some misconfiguration or bug in gRPC v1.56.3 that makes the gRPC server
seem unable to properly support HTTP2.  This effectively breaks
communication between CRDB nodes at these two different gRPC versions.

Switch to a fork that disables the check (there is no other way to
disable it other than changing code).
Replacements unconditionally replace the dependency,
so it's nice to point out that whatever the version
in go.mod claims is not actually what is being used.
This is less efficient since it needs to scan the entire reader
but our hand is forced by gRPC changing away from a byte slice.
See
https://github.com/cockroachdb/grpc-go/releases/tag/v1.68.0-noalpncheck%2Bdecompsize:

    Re-instate (Decompressor).DecompressedSize optimization
    This is a) for parity with how gRPC v1.56.3 worked. But also
    it showed up as a giant regression in allocations, as we were
    now going through `io.Copy` which allocates a temporary
    32KiB buffer. Our payloads are often much smaller.
@tbg tbg force-pushed the grpc-1.68.0 branch 2 times, most recently from cbe8f5b to 1301656 Compare December 2, 2024 12:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
o-perf-efficiency Related to performance efficiency
Projects
None yet
Development

Successfully merging this pull request may close these issues.

go.mod: upgrade gRPC
3 participants