gRPC实战(六、负载均衡(一), Java、Go实现)

1,162 阅读7分钟

六、负载均衡(一)

生产环境中,往往生产者和消费者都是多个,消费者需要从服务列表中挑选一个生产者进行消费,进而有了微服务环境中的一个特性负载均衡。RPC框架基本都支持负载均衡,duboo这方面做的很优秀,框架内部就支持了多种负载均衡策略。接下来,我们来介绍下负载均衡在grpc里的设计和思考。相信大家肯定在其中有所收获。

一、gRPC自带负载均衡算法

1、定义proto接口

syntax = "proto3";//标识 proto版本 建议使用proto3
package casservice;//proto包名 避免命名冲突,也可以作为引入其他proto文件时使用
option java_package = "com.example.casservice" ;//生成的类将带有此包名,不指定则使用package
option cc_generic_services = true;
option go_package = "./pb";
option java_outer_classname = "CasServicePbEntity";

message CasLoginReq{
  string userName = 1;
  string password = 2;
}
message CasLoginRes{
   string uid = 1;
   string sex = 2;
}

service CasService {
  rpc casLogin(CasLoginReq) returns (CasLoginRes);
}

2、Java语言实现

  • 服务端实现
package com.example.casservice.impl;

import com.example.casservice.CasServiceGrpc;
import com.example.casservice.CasServicePbEntity;
import io.grpc.stub.StreamObserver;

import java.util.UUID;

public class CasServiceImpl extends CasServiceGrpc.CasServiceImplBase {

    public CasServiceImpl(Integer port) {
        this.port = port;
    }

    private Integer port = 0 ;
    @Override
    public void casLogin(CasServicePbEntity.CasLoginReq request, StreamObserver<CasServicePbEntity.CasLoginRes> responseObserver) {

        System.out.println("port:"+port+"登录请求,用户名:"+request.getUserName()+"\t密码:"+request.getPassword());
        responseObserver.onNext(CasServicePbEntity.CasLoginRes.newBuilder()
                .setUid("port:"+port+"\t"+UUID.randomUUID().toString().replaceAll("-",""))
                .setSex("男").build());
        responseObserver.onCompleted();

    }
}
  • 启动多个服务端实例
package com.example.casservice;

import com.example.casservice.impl.CasServiceImpl;
import com.google.common.collect.Lists;
import io.grpc.Server;
import io.grpc.ServerBuilder;

import java.io.IOException;
import java.util.List;

public class App {
    
    public static void main(String[] args) throws IOException, InterruptedException {
        List<Integer> ports = Lists.newArrayList();

        List<Server> servers = Lists.newArrayList();
        ports.add(10084);
        ports.add(10085);

        for (Integer port : ports) {
            System.out.println(port + "启动成功...");
            CasServiceImpl casService = new CasServiceImpl(port);
            Server server = ServerBuilder.forPort(port).addService(casService)
                    .build();
            server.start();
            servers.add(server);
        }

        for (Server server : servers) {
            server.awaitTermination();
        }

    }
}
  • 客户端需要实现一个自定的namereslover来将请求的域名解析为服务实例ip
package com.example.casservice.resolve;

import com.google.common.collect.Lists;
import io.grpc.EquivalentAddressGroup;
import io.grpc.NameResolver;

import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import io.grpc.Attributes;

public class CustomNameResolver extends NameResolver {

    private final String path;

    private Listener2 listener;

    public CustomNameResolver(String path) {
        this.path = path;
    }

    @Override
    public String getServiceAuthority() {
        return this.path;
    }

    @Override
    public void shutdown() {
    }

    @Override
    public void start(Listener2 listener) {
        this.listener = listener;
        this.resolve();
    }

    private void resolve() {
        List<SocketAddress> socketAddressList = getAddressList(path).stream()
                .map(this::toSocketAddress)
                .collect(Collectors.toList());

        List<EquivalentAddressGroup> equivalentAddressGroups = Lists.newArrayList();
        for (SocketAddress socketAddress : socketAddressList) {
            EquivalentAddressGroup equivalentAddressGroup = new EquivalentAddressGroup(socketAddress);
            equivalentAddressGroups.add(equivalentAddressGroup);
        }
        ResolutionResult resolutionResult = ResolutionResult.newBuilder()
                .setAddresses(equivalentAddressGroups)
                .setAttributes(Attributes.EMPTY)
                .build();

        this.listener.onResult(resolutionResult);

    }
    private SocketAddress toSocketAddress(InetSocketAddress address) {
        return new InetSocketAddress(address.getHostName(), address.getPort());
    }

    private List<InetSocketAddress> getAddressList(String path) {
        System.out.println("path:"+path);
        InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 10084);
        InetSocketAddress inetSocketAddress2 = new InetSocketAddress("localhost", 10085);
        return Arrays.asList(inetSocketAddress, inetSocketAddress2);
    }

}
  • 新规范需要使用provider注册nameresolver
package com.example.casservice.resolve;

import io.grpc.NameResolver;
import io.grpc.NameResolverProvider;
import java.net.URI;

public class CustomNameResolverProvider extends NameResolverProvider {

    public CustomNameResolverProvider() {

    }
    @Override
    public NameResolver newNameResolver(URI targetUri, NameResolver.Args args) {
        return new CustomNameResolver(targetUri.getPath());
    }

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

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

    @Override
    public String getDefaultScheme() {
        return "example";
    }
}
  • 使用 round_robinpick_first 轮询的方式请求 发起二十次请求 来看看效果
package com.example.casservice;

import com.example.casservice.resolve.CustomNameResolverProvider;
import io.grpc.*;

public class App {

    public static void main(String[] args) {

        NameResolverRegistry.getDefaultRegistry().register(new CustomNameResolverProvider());
        ManagedChannel channel = ManagedChannelBuilder.forTarget("example:///lb.example.grpc.io")
                .defaultLoadBalancingPolicy("pick_first")
                .usePlaintext()
                .build();
        CasServiceGrpc.CasServiceBlockingStub casServiceBlockingStub = CasServiceGrpc.newBlockingStub(channel);
        System.out.println("==== Calling with pick_first ====");

        for (int i = 0; i < 20; i++) {

            CasServicePbEntity.CasLoginRes casLoginRes =casServiceBlockingStub.casLogin(CasServicePbEntity.CasLoginReq.newBuilder()
                    .setUserName("java_test")
                    .setPassword("test_password").build());

            System.out.println("casLoginRes,uid:"+casLoginRes.getUid()+"性别:\t"+casLoginRes.getSex());
        }

        System.out.println("==== Calling  with round_robin ====");
        ManagedChannel channel2 = ManagedChannelBuilder.forTarget("example:///lb.example.grpc.io")
                .defaultLoadBalancingPolicy("round_robin")
                .usePlaintext()
                .build();
        CasServiceGrpc.CasServiceBlockingStub casServiceBlockingStub2 = CasServiceGrpc.newBlockingStub(channel2);
        for (int i = 0; i < 20; i++) {

            CasServicePbEntity.CasLoginRes casLoginRes =casServiceBlockingStub2.casLogin(CasServicePbEntity.CasLoginReq.newBuilder()
                    .setUserName("java_test2")
                    .setPassword("test_password2").build());

            System.out.println("casLoginRes,uid:"+casLoginRes.getUid()+"性别:\t"+casLoginRes.getSex());
        }

    }
}
==== Calling with pick_first ====
path:/lb.example.grpc.io
casLoginRes,uid:port:10084	441f1ce54214475cb366982e3e0fd726性别:	男
casLoginRes,uid:port:10084	a4cfed898db64706a18507983d8589d6性别:	男
casLoginRes,uid:port:10084	a04fb2b71944440b8ed00f02bdc3b15f性别:	男
casLoginRes,uid:port:10084	2b2668b2f1ad472ab35729708f0959d3性别:	男
casLoginRes,uid:port:10084	1fa78e4526c24daf9c6d9779d1626ca9性别:	男
casLoginRes,uid:port:10084	826faf498afc4e1ba65db18a1eb0ccf4性别:	男
casLoginRes,uid:port:10084	c9c992e0b4954b9a9f1e94022c8a65ca性别:	男
casLoginRes,uid:port:10084	08b52ac7e444450c9aaad323d255b168性别:	男
casLoginRes,uid:port:10084	9cde040761cb4de993f3fbc95e9299e3性别:	男
casLoginRes,uid:port:10084	d8aa79dbcd094c0f918b219ba3cbb7a2性别:	男
casLoginRes,uid:port:10084	78575fcb8e2f40cab1a3085e7d609c1f性别:	男
casLoginRes,uid:port:10084	c1863d7b914045d6b339a5be8770b215性别:	男
casLoginRes,uid:port:10084	80bd8a2a1a444ade901c849a7e338e63性别:	男
casLoginRes,uid:port:10084	5e37a848792a41dab612b13089677771性别:	男
casLoginRes,uid:port:10084	1674a08cdcc64b4882805038485742d5性别:	男
casLoginRes,uid:port:10084	192055f9ebfd4a1db03679cc875560c6性别:	男
casLoginRes,uid:port:10084	63d611cab7884ff7b3e5e21073fc6164性别:	男
casLoginRes,uid:port:10084	d2d7ca2e4122428a8d134160e47938e9性别:	男
casLoginRes,uid:port:10084	6e924aae0c3e44348dce641c06652440性别:	男
casLoginRes,uid:port:10084	91a9dd7f159e49a0948ce30c4fb1ecf2性别:	男
==== Calling  with round_robin ====
path:/lb.example.grpc.io
casLoginRes,uid:port:10085	f3d4133b24ac4ab1ae2afac4078e46e7性别:	男
casLoginRes,uid:port:10084	075838a14fdc4626a07892d3701c30ce性别:	男
casLoginRes,uid:port:10085	a627d8c99a2b4b1185e5414565142611性别:	男
casLoginRes,uid:port:10084	58cca91d354b4688807674278cda539e性别:	男
casLoginRes,uid:port:10085	3be7bcd4a43247b4b889feb4daf8724f性别:	男
casLoginRes,uid:port:10084	b7ff85a74db44736804234bc20e1feb8性别:	男
casLoginRes,uid:port:10085	ba0028287c0c4a62b2854c25c1344120性别:	男
casLoginRes,uid:port:10084	f6a045814e2149e9833ae644d563c87b性别:	男
casLoginRes,uid:port:10085	0596a4fc0b644d5a9e734ec9262abf7f性别:	男
casLoginRes,uid:port:10084	574ff946c98547e7a23d30dd9905d9fa性别:	男
casLoginRes,uid:port:10085	b7d6a68b5e344310ae133b11a2b6635e性别:	男
casLoginRes,uid:port:10084	9ab058a644fd45eb9ea90fbb7df0b575性别:	男
casLoginRes,uid:port:10085	1153074d7626465d96ef041dafa55bb3性别:	男
casLoginRes,uid:port:10084	b00ddaec91cd40a1abe9cd83a39bd5ef性别:	男
casLoginRes,uid:port:10085	840417637b264cefb34b284ce55daa65性别:	男
casLoginRes,uid:port:10084	d033dba7c8fb49cdacbe4ae7bd91c714性别:	男
casLoginRes,uid:port:10085	7ccdd8f59a4f448a91df00dff15ac258性别:	男
casLoginRes,uid:port:10084	e6042be3517e47f9a95257507d1aa66c性别:	男
casLoginRes,uid:port:10085	8bf6183990c2449cb7e58f11534e4211性别:	男
casLoginRes,uid:port:10084	54788c7324ce45eb88339c14f03c9be2性别:	男

2、Go语言客户端实现

package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/resolver"
	"grpc-in-action/part06-loadbalancing/go/client/pb"
	"log"
)
const (
	exampleScheme      = "example"
	exampleServiceName = "lb.example.grpc.io"
)

var addrs = []string{"localhost:10084", "localhost:10085"}

func main() {
	pickfirstConn, err := grpc.Dial(
		fmt.Sprintf("%s:///%s", exampleScheme, exampleServiceName), // "example:///lb.example.grpc.io"
		// grpc.WithBalancerName("pick_first"), // "pick_first" is the default, so this DialOption is not necessary.
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	)
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer pickfirstConn.Close()

	log.Println("==== Calling with pick_first ====")
	makeRPCs(pickfirstConn, 10)

	// Make another ClientConn with round_robin policy.
	roundrobinConn, err := grpc.Dial(
		fmt.Sprintf("%s:///%s", exampleScheme, exampleServiceName), // // "example:///lb.example.grpc.io"
		//grpc.WithBalancer(("round_robin"), // This sets the initial balancing policy.
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`), // This sets the initial balancing policy.
	)
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer roundrobinConn.Close()

	log.Println("==== Calling  with round_robin ====")
	makeRPCs(roundrobinConn, 10)
}

func makeRPCs(conn *grpc.ClientConn, count int) {
	client := pb.NewCasServiceClient(conn)
	req:= &pb.CasLoginReq{
		UserName: "go_cas_req"}
	for i := 0; i <count ; i++ {
		res, err := client.CasLogin(context.Background(), req)
		if err != nil {
			log.Printf("err:%v",err)
		}
		log.Printf("res:%v",res)
	}
}

type exampleResolverBuilder struct{}

func (*exampleResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
	r := &exampleResolver{
		target: target,
		cc:     cc,
		addrsStore: map[string][]string{
			"/lb.example.grpc.io":{"localhost:10084", "localhost:10085"}, // "lb.example.grpc.io": "localhost:50051", "localhost:50052"
		},
	}
	r.start()
	return r, nil
}
func (*exampleResolverBuilder) Scheme() string { return "example" } // "example"

type exampleResolver struct {
	target     resolver.Target
	cc         resolver.ClientConn
	addrsStore map[string][]string
}

func (r *exampleResolver) start() {
	addrStrs := r.addrsStore[r.target.URL.Path]
	addrs := make([]resolver.Address, len(addrStrs))
	for i, s := range addrStrs {
		addrs[i] = resolver.Address{Addr: s}
	}
	r.cc.UpdateState(resolver.State{Addresses: addrs})
}
func (*exampleResolver) ResolveNow(o resolver.ResolveNowOptions) {}

func (*exampleResolver) Close()                                 {}

func init() {
	resolver.Register(&exampleResolverBuilder{})
}

二、小结

gRPC默认的负载均衡方式只有三种:pick_firstround_robingrpclb (但这种方式已经过时了,被xds所替代)

gRPC提供的前两种方式,在复杂的生产环境中确实显得有些单薄,那作为一款优秀的框架,它是如何为我们提供解决方案的呢?我们下回分解。

  • 相关文档

github.com/grpc/grpc/b…

github.com/grpc/grpc/b…

grpc.io/blog/grpc-l…