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

add firstready balancer #349

Merged
merged 5 commits into from
May 21, 2024
Merged

Conversation

demmer
Copy link
Collaborator

@demmer demmer commented May 16, 2024

Description

Implement a "first_ready" load balancer and a config to select it.

Despite how simple the code actually is, this balancer (I think!) does what (I think!) we want it to do (basically!):

  • When the discovery returns N targets, the base code in the balancer will attempt to establish a SubConn to each of the targets, and will maintain that subconn with keepalives and retries.
  • However when picking one of these for a given request, it always returns the first "ready" subconn.

This means that in practice, in steady state a webapp host will have one "hot" connection to a single vtgate, and will route all queries to it. At the same time it will maintain N-1 idle connections to other vtgates, and if the connection to the primary vtgate goes away or is interrupted, queries will get routed to one of the other ones as a backup.

AZ Affinity: Just like the case of round_robin if there aren't N hosts in the same AZ, then some amount of cross-AZ queries are inevitable. When using first_ready it can be especially bad since if the "first" ready subconn is in a different AZ then webapp will be stuck crossing AZs until discovery runs again.

In practice I think this isn't actually a problem since we should always have at least N vtgates in each AZ.

Testing

This seems to do what it's supposed to with some basic testing, including using iptables rules to temporarily black hole network traffic to the "selected" gate. After the keepalive timeout, the connection was marked as TRANSIENT_FAILURE and the query was routed to a different gate, so the app saw a slowdown but not a failure.

Related Issue(s)

Checklist

  • "Backport to:" labels have been added if this change should be back-ported
  • Tests were added or are not required
  • Did the new or modified tests pass consistently locally and on the CI
  • Documentation was added or is not required

Deployment Notes


var subConn balancer.SubConn
for sc := range info.ReadySCs {
subConn = sc
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Note that we might want to actually pick randomly from the set of ready subconns rather than just taking the first one in the map.

I'm frankly not sure it matters in practice since the order should be basically nondeterministic anyway, and this is downstream from the already randomized shuffle from discovery, but in case it's possible that something like the order in which vtgates are started up somehow influences this, then maybe there's extra value in doing so?

Copy link

Choose a reason for hiding this comment

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

I think we want this to be deterministic in the input - we assume that Build will be called only when there is a change in the ready list, but maybe there are circumstances where nothing has changed but it gets re-invoked. In those situations we don't want to jump around vtgates. (This is different from round_robin which explicitly does want to choose a different starting point each time, and has code to do that).

Copy link

Choose a reason for hiding this comment

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

Thinking further - we only want to pick a new gate if the currently picked one is no longer ready. So the Nth subconn changing state isn't something we should act on. We should pick the same subconn if it's still in the ready list, which is tricky because we don't know what we last picked.

Without this, any vtgate subconn state change causes us to pick a new gate. We'd need some stable, but host-specific, way of deterministically choosing the same gate from a list (like sorting by hash of <conn-id>-<hostname>.

Copy link
Collaborator

@rjlaine rjlaine May 16, 2024

Choose a reason for hiding this comment

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

If deterministic order matters, we can't rely on the ordering of this map, since map ordering is not guaranteed (in past versions of go it was intentionally shuffled). We would need to take the output of this map and sort it deterministically. If we don't care about order, then this is fine, but we'll have to be aware of the fact that the target may or may not change during every discovery.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Great points! -- I pushed a rework where we actually simplify the model so that the same struct implements both the Builder and the Picker interfaces (golang is really nice sometimes!). This way we will stick to the current conn as long as it's READY.

type frPickerBuilder struct{}

func (*frPickerBuilder) Build(info base.PickerBuildInfo) balancer.Picker {
logger.Infof("firstreadyPicker: Build called with info: %v", info)

Choose a reason for hiding this comment

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

worth normalizing on names, we have: first_ready, firstready, firstreadyPicker all in labels in this file

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

good point. done.

Comment on lines 61 to 63
var subConn balancer.SubConn
for sc := range info.ReadySCs {
subConn = sc
Copy link

Choose a reason for hiding this comment

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

am I missing something clever by not just returning info.ReadSCs[0]?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It's a map, the values aren't indexible or sorted deterministically

grpcclient.RegisterGRPCDialOptions(func(opts []grpc.DialOption) ([]grpc.DialOption, error) {
return append(opts, grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`)), nil
log.Infof("registering %s load balancer", *balancerType)
return append(opts, grpc.WithDefaultServiceConfig(fmt.Sprintf(`{"loadBalancingConfig": [{"%s":{}}]}`, *balancerType))), nil
Copy link

Choose a reason for hiding this comment

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

we should sanitize the input here - I'm not sure what the behaviour of gRPC is if you ask it for a balancer it doesn't know about.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

good point. done.


var subConn balancer.SubConn
for sc := range info.ReadySCs {
subConn = sc
Copy link

Choose a reason for hiding this comment

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

I think we want this to be deterministic in the input - we assume that Build will be called only when there is a change in the ready list, but maybe there are circumstances where nothing has changed but it gets re-invoked. In those situations we don't want to jump around vtgates. (This is different from round_robin which explicitly does want to choose a different starting point each time, and has code to do that).

Comment on lines 73 to 74
// created. The slice is immutable. Each Get() will do a round robin
// selection from it and return the selected SubConn.
Copy link

Choose a reason for hiding this comment

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

round robin selection - The C&P is showing 😆

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed


var subConn balancer.SubConn
for sc := range info.ReadySCs {
subConn = sc
Copy link

Choose a reason for hiding this comment

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

Thinking further - we only want to pick a new gate if the currently picked one is no longer ready. So the Nth subconn changing state isn't something we should act on. We should pick the same subconn if it's still in the ready list, which is tricky because we don't know what we last picked.

Without this, any vtgate subconn state change causes us to pick a new gate. We'd need some stable, but host-specific, way of deterministically choosing the same gate from a list (like sorting by hash of <conn-id>-<hostname>.

)

// Name is the name of first_ready balancer.
const Name = "first_ready"
Copy link
Collaborator

@rjlaine rjlaine May 20, 2024

Choose a reason for hiding this comment

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

this will become a public package level constant, so vtgateproxy.Name will become "first_ready" which might be confusing. (PascalCase names are automatically public in go)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Removed

// Name is the name of first_ready balancer.
const Name = "first_ready"

var logger = grpclog.Component("firstready")
Copy link
Collaborator

Choose a reason for hiding this comment

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

This will also become a package level variable, although at least private. But any file that references logger in the vtgateproxy package will reference this instance.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

good point - removed

@demmer demmer force-pushed the vtgateproxy-firstready-balancer branch from ee7e879 to 60dc5df Compare May 21, 2024 16:35
Copy link
Collaborator

@rjlaine rjlaine left a comment

Choose a reason for hiding this comment

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

Nice, this all looks good to me. Let's give it a spin!

@demmer demmer merged commit 91cc200 into vtgateproxy May 21, 2024
151 of 240 checks passed
@demmer demmer deleted the vtgateproxy-firstready-balancer branch May 21, 2024 16:54
dedelala pushed a commit that referenced this pull request Jul 30, 2024
* add firstready balancer

* fail if an invalid balancer is picked

* maintain the currentConn even if another one becomes ready

* remove unnecessary globals

* normalize the names
dedelala pushed a commit that referenced this pull request Nov 12, 2024
* add firstready balancer

* fail if an invalid balancer is picked

* maintain the currentConn even if another one becomes ready

* remove unnecessary globals

* normalize the names
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants