前言

上篇介绍了如何使用etcd实现服务发现,本篇将基于etcd的服务发现前提下,介绍如何实现gRPC客户端负载均衡。

gRPC负载均衡

gRPC官方文档提供了关于gRPC负载均衡方案Load Balancing in gRPC,此方案是为gRPC设计的,下面我们对此进行分析。

1、对每次调用进行负载均衡

gRPC中的负载平衡是以每次调用为基础,而不是以每个连接为基础。换句话说,即使所有的请求都来自一个客户端,我们仍希望它们在所有的服务器上实现负载平衡。

2、负载均衡的方法

  • 集中式(Proxy Model)

在服务消费者和服务提供者之间有一个独立的负载均衡(LB),通常是专门的硬件设备如 F5,或者基于软件如 LVS,HAproxy等实现。LB上有所有服务的地址映射表,通常由运维配置注册,当服务消费方调用某个目标服务时,它向LB发起请求,由LB以某种策略,比如轮询(Round-Robin)做负载均衡后将请求转发到目标服务。LB一般具备健康检查能力,能自动摘除不健康的服务实例。

该方案主要问题:服务消费方、提供方之间增加了一级,有一定性能开销,请求量大时,效率较低。

可能有读者会认为集中式负载均衡存在这样的问题,一旦负载均衡服务挂掉,那整个系统将不能使用。

解决方案:可以对负载均衡服务进行DNS负载均衡,通过对一个域名设置多个IP地址,每次DNS解析时轮询返回负载均衡服务地址,从而实现简单的DNS负载均衡。

  • 客户端负载(Balancing-aware Client)

针对第一个方案的不足,此方案将LB的功能集成到服务消费方进程里,也被称为软负载或者客户端负载方案。服务提供方启动时,首先将服务地址注册到服务注册表,同时定期报心跳到服务注册表以表明服务的存活状态,相当于健康检查,服务消费方要访问某个服务时,它通过内置的LB组件向服务注册表查询,同时缓存并定期刷新目标服务地址列表,然后以某种负载均衡策略选择一个目标服务地址,最后向目标服务发起请求。LB和服务发现能力被分散到每一个服务消费者的进程内部,同时服务消费方和服务提供方之间是直接调用,没有额外开销,性能比较好。

该方案主要问题:要用多种语言、多个版本的客户端编写和维护负载均衡策略,使客户端的代码大大复杂化。

  • 独立LB服务(External Load Balancing Service)

该方案是针对第二种方案的不足而提出的一种折中方案,原理和第二种方案基本类似。

不同之处是将LB和服务发现功能从进程内移出来,变成主机上的一个独立进程。主机上的一个或者多个服务要访问目标服务时,他们都通过同一主机上的独立LB进程做服务发现和负载均衡。该方案也是一种分布式方案没有单点问题,服务调用方和LB之间是进程内调用性能好,同时该方案还简化了服务调用方,不需要为不同语言开发客户库。

本篇将介绍第二种负载均衡方法,客户端负载均衡。

实现gRPC客户端负载均衡

gRPC已提供了简单的负载均衡策略(如:Round Robin),我们只需实现它提供的BuilderResolver接口,就能完成gRPC客户端负载均衡。

type Builder interface {
Build(target Target, cc ClientConn, opts BuildOption) (Resolver, error)
Scheme() string
}

Builder接口:创建一个resolver(本文称之服务发现),用于监视名称解析更新。

Build方法:为给定目标创建一个新的resolver,当调用grpc.Dial()时执行。

Scheme方法:返回此resolver支持的方案,Scheme定义可参考:https://github.com/grpc/grpc/blob/master/doc/naming.md

type Resolver interface {
ResolveNow(ResolveNowOption)
Close()
}

Resolver接口:监视指定目标的更新,包括地址更新和服务配置更新。

ResolveNow方法:被 gRPC 调用,以尝试再次解析目标名称。只用于提示,可忽略该方法。

Close方法:关闭resolver

根据以上两个接口,我们把服务发现的功能写在Build方法中,把获取到的负载均衡服务地址返回到客户端,并监视服务更新情况,以修改客户端连接。

修改服务发现代码,discovery.go

package etcdv3

import (
"context"
"log"
"sync"
"time" "github.com/coreos/etcd/mvcc/mvccpb"
"go.etcd.io/etcd/clientv3"
"google.golang.org/grpc/resolver"
) const schema = "grpclb" //ServiceDiscovery 服务发现
type ServiceDiscovery struct {
cli *clientv3.Client //etcd client
cc resolver.ClientConn
serverList map[string]resolver.Address //服务列表
lock sync.Mutex
} //NewServiceDiscovery 新建发现服务
func NewServiceDiscovery(endpoints []string) resolver.Builder {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatal(err)
} return &ServiceDiscovery{
cli: cli,
}
} //Build 为给定目标创建一个新的`resolver`,当调用`grpc.Dial()`时执行
func (s *ServiceDiscovery) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOption) (resolver.Resolver, error) {
log.Println("Build")
s.cc = cc
s.serverList = make(map[string]resolver.Address)
prefix := "/" + target.Scheme + "/" + target.Endpoint + "/"
//根据前缀获取现有的key
resp, err := s.cli.Get(context.Background(), prefix, clientv3.WithPrefix())
if err != nil {
return nil, err
} for _, ev := range resp.Kvs {
s.SetServiceList(string(ev.Key), string(ev.Value))
}
s.cc.NewAddress(s.getServices())
//监视前缀,修改变更的server
go s.watcher(prefix)
return s, nil
} // ResolveNow 监视目标更新
func (s *ServiceDiscovery) ResolveNow(rn resolver.ResolveNowOption) {
log.Println("ResolveNow")
} //Scheme return schema
func (s *ServiceDiscovery) Scheme() string {
return schema
} //Close 关闭
func (s *ServiceDiscovery) Close() {
log.Println("Close")
s.cli.Close()
} //watcher 监听前缀
func (s *ServiceDiscovery) watcher(prefix string) {
rch := s.cli.Watch(context.Background(), prefix, clientv3.WithPrefix())
log.Printf("watching prefix:%s now...", prefix)
for wresp := range rch {
for _, ev := range wresp.Events {
switch ev.Type {
case mvccpb.PUT: //新增或修改
s.SetServiceList(string(ev.Kv.Key), string(ev.Kv.Value))
case mvccpb.DELETE: //删除
s.DelServiceList(string(ev.Kv.Key))
}
}
}
} //SetServiceList 新增服务地址
func (s *ServiceDiscovery) SetServiceList(key, val string) {
s.lock.Lock()
defer s.lock.Unlock()
s.serverList[key] = resolver.Address{Addr: val}
s.cc.NewAddress(s.getServices())
log.Println("put key :", key, "val:", val)
} //DelServiceList 删除服务地址
func (s *ServiceDiscovery) DelServiceList(key string) {
s.lock.Lock()
defer s.lock.Unlock()
delete(s.serverList, key)
s.cc.NewAddress(s.getServices())
log.Println("del key:", key)
} //GetServices 获取服务地址
func (s *ServiceDiscovery) getServices() []resolver.Address {
addrs := make([]resolver.Address, 0, len(s.serverList)) for _, v := range s.serverList {
addrs = append(addrs, v)
}
return addrs
}

代码主要修改以下地方:

  1. 把获取的服务地址转成resolver.Address,供gRPC客户端连接。

  2. 根据schema的定义规则,修改key格式。

服务注册主要修改key存储格式,register.go

package etcdv3

import (
"context"
"log"
"time" "go.etcd.io/etcd/clientv3"
) //ServiceRegister 创建租约注册服务
type ServiceRegister struct {
cli *clientv3.Client //etcd client
leaseID clientv3.LeaseID //租约ID
//租约keepalieve相应chan
keepAliveChan <-chan *clientv3.LeaseKeepAliveResponse
key string //key
val string //value
} //NewServiceRegister 新建注册服务
func NewServiceRegister(endpoints []string, serName, addr string, lease int64) (*ServiceRegister, error) {
cli, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatal(err)
} ser := &ServiceRegister{
cli: cli,
key: "/" + schema + "/" + serName + "/" + addr,
val: addr,
} //申请租约设置时间keepalive
if err := ser.putKeyWithLease(lease); err != nil {
return nil, err
} return ser, nil
} //设置租约
func (s *ServiceRegister) putKeyWithLease(lease int64) error {
//设置租约时间
resp, err := s.cli.Grant(context.Background(), lease)
if err != nil {
return err
}
//注册服务并绑定租约
_, err = s.cli.Put(context.Background(), s.key, s.val, clientv3.WithLease(resp.ID))
if err != nil {
return err
}
//设置续租 定期发送需求请求
leaseRespChan, err := s.cli.KeepAlive(context.Background(), resp.ID) if err != nil {
return err
}
s.leaseID = resp.ID
s.keepAliveChan = leaseRespChan
log.Printf("Put key:%s val:%s success!", s.key, s.val)
return nil
} //ListenLeaseRespChan 监听 续租情况
func (s *ServiceRegister) ListenLeaseRespChan() {
for leaseKeepResp := range s.keepAliveChan {
log.Println("续约成功", leaseKeepResp)
}
log.Println("关闭续租")
} // Close 注销服务
func (s *ServiceRegister) Close() error {
//撤销租约
if _, err := s.cli.Revoke(context.Background(), s.leaseID); err != nil {
return err
}
log.Println("撤销租约")
return s.cli.Close()
}

客户端修改gRPC连接服务的部分代码即可:

func main() {
r := etcdv3.NewServiceDiscovery(EtcdEndpoints)
resolver.Register(r)
// 连接服务器
conn, err := grpc.Dial(r.Scheme()+"://8.8.8.8/simple_grpc", grpc.WithBalancerName("round_robin"), grpc.WithInsecure())
if err != nil {
log.Fatalf("net.Connect err: %v", err)
}
defer conn.Close() // 建立gRPC连接
grpcClient = pb.NewSimpleClient(conn)

gRPC内置了简单的负载均衡策略round_robin,根据负载均衡地址,以轮询的方式进行调用服务。

服务端启动时,把服务地址注册到etcd中即可:

func main() {
// 监听本地端口
listener, err := net.Listen(Network, Address)
if err != nil {
log.Fatalf("net.Listen err: %v", err)
}
log.Println(Address + " net.Listing...")
// 新建gRPC服务器实例
grpcServer := grpc.NewServer()
// 在gRPC服务器注册我们的服务
pb.RegisterSimpleServer(grpcServer, &SimpleService{})
//把服务注册到etcd
ser, err := etcdv3.NewServiceRegister(EtcdEndpoints, SerName, Address, 5)
if err != nil {
log.Fatalf("register service err: %v", err)
}
defer ser.Close()
//用服务器 Serve() 方法以及我们的端口信息区实现阻塞等待,直到进程被杀死或者 Stop() 被调用
err = grpcServer.Serve(listener)
if err != nil {
log.Fatalf("grpcServer.Serve err: %v", err)
}
}

运行效果

我们先启动并注册三个服务

然后客户端进行调用

看服务端接收到的请求

关闭localhost:8000服务,剩余localhost:8001localhost:8002服务接收请求

重新打开localhost:8000服务

可以看到,gRPC客户端负载均衡运行良好。

总结

本文介绍了gRPC客户端负载均衡的实现,它简单实现了gRPC负载均衡的功能。但在对接其他语言时候比较麻烦,需要每种语言都实现一套服务发现和负载策略。

目前官方只提供了取第一个地址pick_first和轮询round_robin两种负载均衡策略。下篇将介绍如何自定义负载均衡策略。

有兴趣了解第三种负载均衡方法External Load Balancing Service的,可以参考这个项目:https://github.com/bsm/grpclb

源码地址:https://github.com/Bingjian-Zhu/etcd-example

参考:

gRPC负载均衡(客户端负载均衡)的更多相关文章

  1. 服务注册发现Eureka之三:Spring Cloud Ribbon实现客户端负载均衡(客户端负载均衡Ribbon之三:使用Ribbon实现客户端的均衡负载)

    在使用RestTemplate来消费spring boot的Restful服务示例中,我们提到,调用spring boot服务的时候,需要将服务的URL写死或者是写在配置文件中,但这两种方式,无论哪一 ...

  2. grpc服务发现与负载均衡

    前言 在后台服务开发中,高可用性是构建中核心且重要的一环.服务发现(Service discovery)和负载均衡(Load Balance)一直都是我关注的话题.今天来谈一下我在实际中是如何理解及落 ...

  3. Spring Cloud官方文档中文版-客户端负载均衡:Ribbon

    官方文档地址为:http://cloud.spring.io/spring-cloud-static/Dalston.SR2/#_spring_cloud_netflix 文中例子我做了一些测试在:h ...

  4. 笔记:Spring Cloud Ribbon 客户端负载均衡

    Spring Cloud Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,基于 Netflix Ribbon 实现,通过Spring Cloud 的封装,可以让我们轻松的将面向服 ...

  5. Spring Cloud入门教程-Ribbon实现客户端负载均衡

    简介 我们继续以之前博客的代码为基础,增加Ribbon组件来提供客户端负载均衡.负载均衡是实现高并发.高性能.可伸缩服务的重要组成部分,它可以把请求分散到一个集群中不同的服务器中,以减轻每个服务器的负 ...

  6. SpringCloud实战-Ribbon客户端负载均衡

    前面我们已经完成了注册中心和服务提供者两个基础组件.接着介绍使用Spring Cloud Ribbon在客户端负载均衡的调用服务. ribbon 是一个客户端负载均衡器,可以简单的理解成类似于 ngi ...

  7. Spring Cloud微服务笔记(四)客户端负载均衡:Spring Cloud Ribbon

    客户端负载均衡:Spring Cloud Ribbon 一.负载均衡概念 负载均衡在系统架构中是一个非常重要,并且是不得不去实施的内容.因为负载均衡对系统的高可用性. 网络压力的缓解和处理能力的扩容的 ...

  8. 基于Spring cloud Ribbon和Eureka实现客户端负载均衡

    前言 本案例将基于Spring cloud Ribbon和Eureka实现客户端负载均衡,其中Ribbon用于实现客户端负载均衡,Eureka主要是用于服务注册及发现: 传统的服务端负载均衡 常见的服 ...

  9. Spring Cloud 2-Ribbon 客户端负载均衡(二)

    Spring Cloud Eureka  1.Hello-Service服务端配置 pom.xml application.yml 启动两个service 2.Ribbon客户端配置 pom.xml ...

随机推荐

  1. tp5.1部署到IIS服务器404

    本人小白一枚,本地用的apache环境,公司让做了一个小项目,在本地写好之后,一切调试好,就直接交给了主管,之后别人负责上线. 就这样平淡的度过了两天,今天突然和我说,除了首页其他链接都是404,我想 ...

  2. 《Metasploit魔鬼训练营》第一章实践作业

    <Metasploit魔鬼训练营>第一章实践作业 1.搜集Samba服务usermap_script安全漏洞的相关信息,画出该安全漏洞的生命周期图,标注各个重要事件点的日期,并提供详细描述 ...

  3. [Inno Setup] 在 File Section 之前解压文件

    Prototype: procedure ExtractTemporaryFile(const FileName: String); Description: Extracts the specifi ...

  4. 理解分布式一致性:Raft协议

    理解分布式一致性:Raft协议 什么是分布式一致性 Leader选举 日志复制流程 term选举周期 timeout 选举和选举timeout 选举分裂 日志复制和心跳timeout 在分布式系统中, ...

  5. 使用IBM Blockchain Platform extension开发你的第一个fabric智能合约

    文章目录 安装IBM Blockchain Platform extension for VS Code 创建一个智能合约项目 理解智能合约 打包智能合约 Local Fabric Ops 安装智能合 ...

  6. CF1285 --- Dr. Evil Underscores

    CF1285 --- Dr. Evil Underscores 题干 Today as a friendship gift, Bakry gave Badawy \(n\) integers \(a_ ...

  7. Javascript基础之-var,let和const深入解析(二)

    你想在在变量声明之前就使用变量?以后再也别这样做了. 新的声明方式(let,const)较之之前的声明方式(var),还有一个区别,就是新的方式不允许在变量声明之前就使用该变量,但是var是可以得.请 ...

  8. pod setup命令失败解决方法

    最近运行pod setup出现以下问题: remote: Compressing objects: 100% (34/34), done.error: RPC failed; curl 56 SSLR ...

  9. TCP 3-Way Handshake

    TCP是面向连接的协议,其数据传输过程分为建立连接.数据传送.释放连接三个阶段. 0 建立连接 建立连接的过程也就是常说的"三次握手": 客户端向服务器端发送一个SYN报文(SYN ...

  10. POJ 1330 Nearest Common Ancestors(裸LCA)

    Nearest Common Ancestors Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 39596   Accept ...