Search code examples
javagrpcgrpc-java

gRPC client-side load balancing


I'm not sure I understand correctly how channels and client-side load balancing work in grpc. I did everything based on one tutorial.

I have several servers where I want to go with requests. I wrote a simple NameResolverProvider.

public class BalancingNameResolverProvider extends NameResolverProvider {
    private Set<String> replicas;
    private Optional<Map<String, Object>> config;
    private String schema;
    private int priority = 5;

    public BalancingNameResolverProvider(Set<String> replicas, Optional<Map<String, Object>> config, String schema, int priority) {
        this.replicas = replicas;
        this.config = config;
        this.schema = schema;
        this.priority = priority;
    }

    @Override
    protected boolean isAvailable() {
        return true;
    }

    @Override
    protected int priority() {
        return priority;
    }

    @Override
    public NameResolver newNameResolver(URI targetUri, NameResolver.Args args) {
        List<EquivalentAddressGroup> delegates = replicas.stream()
                .map(x -> new InetSocketAddress(x.split(":")[0], Integer.parseInt(x.split(":")[1])))
                .map(EquivalentAddressGroup::new)
                .collect(Collectors.toList());

        return new NameResolver() {
            private Optional<NameResolver.ConfigOrError> parsedConfig = config.map(x ->
                    args.getServiceConfigParser().parseServiceConfig(x)
            );

            @Override
            public String getServiceAuthority() {
                return targetUri.getAuthority();
            }

            @Override
            public void shutdown() {
            }

            @Override
            public void start(final NameResolver.Listener2 listener) {
                ResolutionResult.Builder builder = ResolutionResult.newBuilder()
                        .setAddresses(delegates)
                        .setAttributes(Attributes.EMPTY);
                parsedConfig.ifPresent(builder::setServiceConfig);
                listener.onResult(builder.build());
            }
        };
    }

    @Override
    public String getDefaultScheme() {
        return schema;
    }
}

And I wrote a simple client.

            NameResolverRegistry.getDefaultRegistry().register(resolverConfig.toProvider());
            ManagedChannel channel = NettyChannelBuilder
                    .forTarget("???") //or forAddress("???")
                    .enableRetry()
                    .usePlaintext()
                    .build();
            try {
                HelloServiceGrpc.HelloServiceBlockingStub client = HelloServiceGrpc.newBlockingStub(channel);
                for (int i = 0; i < count; i++) {
                    System.out.println(client.hello(HelloRequest.newBuilder()
                            .setFirstName("first_" + i)
                            .setLastName("lastName_" + i)
                            .build())
                            .getGreeting());
                }
            } finally {
                channel.shutdown();
            }

But in all the manuals that I looked at, either one host and port are specified for the channel (forAddress), or some name in "forTarget()".

But I have several servers, how can I specify them all?

And at what point is the server selected? I understand correctly that NameResolverProvider is involved in this, where I specified the list of servers

I use the round_robin policy. Maybe I don't need NameResolverProvider?


Edit

In order to use dns, I added DnsNameResolverProvider.

NameResolverRegistry.getDefaultRegistry().register(new DnsNameResolverProvider());

But I don’t understand yet how to specify two servers, for example, with addresses first.example.com:5000 and second.example.com:5001 in forAddress or forTarget. How will it look like?


Solution

  • The easiest way to resolve multiple addresses is to just leverage DNS or your /etc/hosts file. The default DNS name resolver will load all the addresses and you can call managedChannel.defaultLoadBalancingPolicy("round_robin") to connect to all the addresses instead of only the first that works.

    The name resolver is selected by scheme of the target string passed to forTarget(). So if getScheme() for your resolver returns fixed-replicas, you'd pass a string like fixed-replicas:/// as the target string. If no scheme is in the target string, then the default name resolver is used.

    forAddress() is a convenience that converts to a target string of host:port, but with logic to manage IPv6 addresses which need percent-encoding to be within a URI. It's only useful when using the default name resolver (generally dns).

    DnsNameResolver uses priority 5, and you probably don't want to override it, so you should probably use a lower priority like 4 for your provider.

    Be careful when creating InetSocketAddress to make sure to only pass it IP addresses. If you pass it a hostname it will do a DNS resolution within the constructor. NameResolvers should not do I/O or blocking operations in the normal threads they are called on. A NameResolver can use Args.getOffloadExecutor() for I/O and the like. If you were using hostnames here then you'd end up resolving them to a single IP address each and never re-resolve them, which means if they change you'd need to restart your binary.