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

examples/features/loadbalancing: Add custom lb example #6649

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions examples/features/load_balancing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,34 @@ Note that it's possible to see two continues RPC sent to the same backend.
That's because `round_robin` only picks the connections ready for RPCs. So if
one of the two connections is not ready for some reason, all RPCs will be sent
to the ready connection.

# custom_round_robin

```
Copy link
Member

Choose a reason for hiding this comment

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

Please add a similar explanation of this case, keeping a similar structure, rather than just starting with output text. "The third client is configured to use a custom ............"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

this is examples/load_balancing (from :50052)
this is examples/load_balancing (from :50051)
this is examples/load_balancing (from :50051)
this is examples/load_balancing (from :50052)
this is examples/load_balancing (from :50051)
this is examples/load_balancing (from :50051)
this is examples/load_balancing (from :50052)
this is examples/load_balancing (from :50051)
this is examples/load_balancing (from :50051)
this is examples/load_balancing (from :50052)
this is examples/load_balancing (from :50051)
this is examples/load_balancing (from :50051)
this is examples/load_balancing (from :50052)
this is examples/load_balancing (from :50051)
this is examples/load_balancing (from :50051)
this is examples/load_balancing (from :50052)
this is examples/load_balancing (from :50051)
this is examples/load_balancing (from :50051)
this is examples/load_balancing (from :50052)
this is examples/load_balancing (from :50051)
```

It is also possible to configure a custom load balancing policy through the
service config. Specify the balancer type through the service config which you
register in the registry. In this example, a custom balancer which routes to the
first SubConn it receives except every n times, where n is configurable, in
which is chooses the second SubConn it receives.
103 changes: 102 additions & 1 deletion examples/features/load_balancing/client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,19 @@ package main

import (
"context"
"encoding/json"
"fmt"
"log"
"sync/atomic"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/balancer"
"google.golang.org/grpc/balancer/base"
"google.golang.org/grpc/credentials/insecure"
ecpb "google.golang.org/grpc/examples/features/proto/echo"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/serviceconfig"
)

const (
Expand Down Expand Up @@ -84,11 +89,24 @@ func main() {

fmt.Println("--- calling helloworld.Greeter/SayHello with round_robin ---")
makeRPCs(roundrobinConn, 10)
// You can also plug in your own custom lb policy, which needs to be
Copy link
Member

Choose a reason for hiding this comment

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

Add a blank line above this for better visual separation please.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

// configurable. This n is configurable. Try changing it and see how the
// behavior changes.
customroundrobinConn, err := grpc.Dial(
fmt.Sprintf("%s:///%s", exampleScheme, exampleServiceName),
Copy link
Member

Choose a reason for hiding this comment

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

Let's pull this out at the top of the function:

target := fmt.Sprintf("%s:///%s", exampleScheme, exampleServiceName)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Good point.

grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"custom_round_robin":{"n": 3}}]}`),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer customroundrobinConn.Close()
fmt.Println("--- calling helloworld.Greeter/SayHello with custom_round_robin ---")
makeRPCs(customroundrobinConn, 20)
}

// Following is an example name resolver implementation. Read the name
// resolution example to learn more about it.

type exampleResolverBuilder struct{}
Copy link
Member

Choose a reason for hiding this comment

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

I don't believe this example intends to show off custom resolvers - can this be replaced with the manual resolver instead? We may not have had it when this was created.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Switched.


func (*exampleResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
Expand Down Expand Up @@ -123,4 +141,87 @@ func (*exampleResolver) Close() {}

func init() {
resolver.Register(&exampleResolverBuilder{})
balancer.Register(&customRoundRobinBuilder{})
Copy link
Member

Choose a reason for hiding this comment

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

Maybe put the custom LB policy into its own file (or even its own sub-package), since that's how we expect it to be used typically?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

}

const customRRName = "custom_round_robin"

type customRRConfig struct {
serviceconfig.LoadBalancingConfig `json:"-"`

// N represents how often pick iterations chose the second SubConn
// in the list. Defaults to 3.
Copy link
Member

Choose a reason for hiding this comment

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

Correction: 2

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah good catch. Switched the default in ParseConfig() to 3.

N uint32 `json:"n,omitempty"`
}

func (customRoundRobinBuilder) ParseConfig(s json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
lbConfig := &customRRConfig{
N: 2,
}
if err := json.Unmarshal(s, lbConfig); err != nil {
return nil, fmt.Errorf("custom-round-robin: unable to unmarshal customRRConfig: %v", err)
}
return lbConfig, nil
}

type customRoundRobinBuilder struct{}

func (customRoundRobinBuilder) Name() string {
return customRRName
}

func (customRoundRobinBuilder) Build(cc balancer.ClientConn, bOpts balancer.BuildOptions) balancer.Balancer {
crr := &customRoundRobin{}
baseBuilder := base.NewBalancerBuilder(customRRName, crr, base.Config{HealthCheck: true})
crr.Balancer = baseBuilder.Build(cc, bOpts)
Copy link
Member

Choose a reason for hiding this comment

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

I'm really not a fan of the base balancer...are we sure we want users using it as a template for how to do their own LB policy? I think it would be better to show the actual connection management APIs here instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Switched to petiole policy wrapping pick first after offline discussion.

return crr
}

type customRoundRobin struct {
// Embeds balancer.Balancer because needs to intercept UpdateClientConnState
// to learn about N.
balancer.Balancer
n uint32
}

func (crr *customRoundRobin) UpdateClientConnState(state balancer.ClientConnState) error {
crrCfg, ok := state.BalancerConfig.(*customRRConfig)
if !ok {
return balancer.ErrBadResolverState
}
crr.n = crrCfg.N
return crr.Balancer.UpdateClientConnState(state)
}

func (crr *customRoundRobin) Build(info base.PickerBuildInfo) balancer.Picker {
if len(info.ReadySCs) == 0 {
Copy link
Member

Choose a reason for hiding this comment

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

If you want to make this deterministic, then also do this until len(info.ReadySCs) == 2?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Made deterministic in new PR based off child balancer connectivity states.

return base.NewErrPicker(balancer.ErrNoSubConnAvailable)
}

scs := make([]balancer.SubConn, 0, len(info.ReadySCs))
for sc := range info.ReadySCs {
scs = append(scs, sc)
}
return &customRoundRobinPicker{
subConns: scs,
n: crr.n,
next: 0,
}
}

type customRoundRobinPicker struct {
subConns []balancer.SubConn
n uint32

next uint32
}

func (crrp *customRoundRobinPicker) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
next := atomic.AddUint32(&crrp.next, 1)
index := 0
if next%crrp.n == 0 {
index = 1
}
sc := crrp.subConns[index%len(crrp.subConns)]
return balancer.PickResult{SubConn: sc}, nil
}
Loading