GO微服务实战第二十四节 案例:如何在 Go 微服务中实现负载均衡?

在上一课时中,我们已经介绍了负载均衡的相关概念以及在服务高可用架构中的重要性,也了解了几种主流负载均衡算法的实现。在本课时中,我们将在 Go 微服务实例中具体使用负载均衡技术,并详细说明如何基于服务发现来实现负载均衡的微服务间 HTTP 调用。

基于服务发现和注册的负载均衡

我们仍然以之前课时提到的电商商品系统为例,商品详情页面需要展示下游评论系统的具体评论信息,所以商品系统通过 HTTP 请求调用评论系统获取商品评论。下图展示了两个系统的交互情况:

Drawing 0.png

商品和评论系统交互示意图

评论系统单实例性能欠佳,需要多实例部署,平均请求压力;而商品系统则需要在发起请求前,从所有可用的评论系统实例中挑选一个,再发起请求。查看商品详情的外部请求数量往往在促销时增加,因此评论系统实例的数量并不是固定的,而是动态变化的,请求量大时增加服务实例,请求量少时,则减少服务实例。

所以,商品系统的负载均衡机制需要基于服务注册与发现机制,动态获取评论系统的可用实例列表,而不是将其固化在代码或者配置文件中。

下面,我们就来具体看一下如何在商品系统项目中实现负载均衡。本课时的相关代码在课程代码库中的 section28 文件夹下,地址为https://github.com/longjoy/micro-go-course

服务初始化

首先,我们需要使用前面 14 课时讲解的基于 Consul 的负载均衡客户端(若是记不太清了,你可以回过头再温故一下)。下面代码展示了商品系统在启动时的初始化过程:

// 传入consul的地址和端口初始化服务注册和发现客户端
client := discovery.NewDiscoveryClient(*consulAddr, *consulPort)
// 使用uuid生成客户端实例ID
instanceId := *serviceName + "-" + uuid.New().String()
// 将实例自己注册到 consul 上,包括服务名称,实例ID,健康检查地址,host和端口等
err := client.Register(context.Background(), *serviceName, instanceId, "/health", *serviceAddr, *servicePort, nil, nil)
// 初始化负载均衡器,可以初始化携带不同负载均衡策略的负载均衡器
loadbalancer := loadbalancer.NewRandomLoadBalancer()
// 使用服务注册与发现客户端和负载均衡器初始化service
srv := service.NewGoodsServiceImpl(client, loadbalancer)

其中,loadbalancer是本课时的重点,它是定义负载均衡策略的接口,只有一个 SelectService 方法,接受 ServiceInstance 也就是可用服务列表作为参数,根据一定负载均衡策略从服务实例列表中选择一个服务实例返回。而可用服务列表则可以通过服务注册和发现客户端从 Consul 等服务注册和发现中心获取。

// 负载均衡器
type LoadBalance interface {
// 基于可用服务列表的负载均衡接口
SelectService(service []common.ServiceInstance) (common.Service Instance, error)
// 基于可用服务列表和键值辅助的负载均衡接口
SelectServiceByKey(service []*discovery.InstanceInfo, key string) (*discovery.InstanceInfo, error)
​
}

具体的负载均衡器都要实现该接口,并给出具体不同负载均衡策略的 SelectService 方法的实现,比如上一课时中介绍的随机负载均衡策略和权重平滑负载均衡策略。

我们下面再讲解一下项目中使用的一致性负载均衡策略,根据商品 ID 将不同的获取商品评价的 HTTP 请求分发到某一个固定的评级服务实例上,这样有利于使用本地缓存等缓存机制,提高系统的性能。

一致性哈希负载均衡的核心思想是首先将服务器 key 进行 Hash 运算,将其映射到一个圆形的哈希环上,key 计算出来的整数值则为该服务实例在哈希环上的位置,然后再将请求的 key 值,用同样的方法计算出哈希环上的位置,按顺时针方向,找到第一个大于或等于该哈希环位置的服务实例 key,从而得到本次请求需要分配的服务实例。

Drawing 1.png

一致性哈希负载均衡示意图

如上图所示,服务实例 node1~4 都计算出 Hash 值并映射到哈希环上,而请求的 key 值也能计算出 Hash 值并映射到环上,如图右侧的键值,然后按照顺时针方向找到了服务实例 node2,则该请求就被负载转发到服务实例上。

一致性哈希负载均衡策略能够很好地应对服务实例上线或者下线的场景,以防止大量请求被负载转发到不同的服务实例,减少其对整体系统带来的影响,而一般的哈希负载均衡策略就很难满足这点。比如说服务实例 node2 突然宕机下线,按照该算法,只有 Hash 值落在在服务实例 node1 和 node2 之间的请求受到了影响,被负载转发到了服务实例 node4 上,其他的大部分请求不受影响。

一致性哈希负载均衡策略的具体实现,如下所示:

type HashLoadBalancer struct {
}
​
func (loadBalance *HashLoadBalancer) SelectServiceByKey(services []*discovery.InstanceInfo, key string) (*discovery.InstanceInfo, error) {
// 检查可用服务实例列表不为空
lens := len(services)
if services == nil || lens == 0 {
return nil, errors.New("service instances are not exist")
}
// 使用crc32将key值算出hash值
crcTable := crc32.MakeTable(crc32.IEEE)
hashVal := crc32.Checksum([]byte(key), crcTable)
// 根据hash值和列表长度取余获得服务实例
index := int(hashVal) % lens
return services[index],nil
}

在使用该负载均衡策略时,我们就将商品 ID 作为 key 值传递进来,该算法会使用 crc32 计算该商品 ID 对应的 Hash 值,然后根据取余结果从可用服务列表选出本次负载均衡的目标实例。

发起网络请求

商品系统服务和评论系统初始化启动好之后,对外建立 HTTP 服务,当有用户查看详情时,商品系统会向评论系统发起网络请求,具体代码如下所示:

func (service *GoodsDetailServiceImpl) GetGoodsComments(ctx context.Context, id string) (common.CommentResult, error) {
var result common.CommentResult
// 使用服务注册和发现客户端从consul中获取名为comment的可用服务实例列表
serviceName := "comment"
instances, err := service.discoveryClient.DiscoverServices(ctx, serviceName)
.... // 省略,异常检查
// 使用负载均衡器根据商品id和可用服务实例列表获取本次网络调用的目标comment服务实例
selectedInstance, err2 := service.loadbalancer.SelectService(instances,id)
​
if err2 != nil {
log.Printf("loadbalancer get selected instance  err: %s", err2)
return result, ErrLoadBalancer
}
call_err := hystrix.Do(serviceName, func() error {
// 使用选中comment服务实例的信息来拼接HTTP请求
requestUrl := url.URL{
Scheme:   "http",
Host:     selectedInstance.Address + ":" + strconv.Itoa(selectedInstance.Port),
Path:     "/comments/detail",
RawQuery: "id=" + id,
}
resp, err := http.Get(requestUrl.String())
.... // 省略
}, func(e error) error {
// 断路器打开时的处理逻辑,本示例是直接返回错误提示
return errors.New("Http errors!")
})
.... //省略
}

所以,每次发起查询商品评论信息的网络请求前,都会先调用服务注册和发现客户端的 DiscoverServices 方法来获取当前 comment 可用的服务实例列表,然后调用负载均衡器的 SelectService 方法,根据商品的 ID 从可用列表中选中一个服务实例,最后根据该服务实例的信息构建网络请求,比如 host 和 port 信息等。整个过程如下图所示:

Drawing 2.png

基于服务发现和注册的负载均衡示意图

以上就是在 Go 微服务中实现客户端负载均衡的主流实现原理,很多开源负载均衡器(比如,Ribbon 等)都是以这套原理实现的,不过这个过程还是有许多可以优化的细节,比如负载均衡客户端可以使用缓存可用服务列表数据等方式,来避免每次都从 Consul 处获取可用服务列表数据,以此提高效率。

运行展示

下面,我们就来具体运行和展示一下本课时的案例项目。

首先,我们启动一个商品系统服务(good)和三个评论系统服务(comment),它们都会将自己注册到服务注册和发现中心 Consul 上。如下是 Consul 相关的截图:

Drawing 3.png

Consul 所有服务列表截图

我们可以从 Consul 的服务列表页面发现有三个 comment 服务实例和一个商品服务实例,这三个 comment 服务实例的具体信息如下图所示,从图中可以看出,它们的 host 信息都是127.0.0.1,但是端口号不同,你也可以将这三个 comment 服务实例部署在不同 IP 地址的服务器上,这样它们的 host 信息就不一样了。

Drawing 4.png

Comment 服务实例具体信息截图

接着,我们使用 postman 或者 curl 向 good 发起查看多个商品详情的网络请求,请求多次,具体命令如下所示:

curl http://127.0.0.1:12313/goods/detail?id=1
curl http://127.0.0.1:12313/goods/detail?id=2
curl http://127.0.0.1:12313/goods/detail?id=3

然后,我们到 good 服务实例的日志中进行查看,可以看到如下日志信息:

get good 1 comment from comment service host:127.0.0.1 port 13312
get good 2 comment from comment service host:127.0.0.1 port 11312
get good 3 comment from comment service host:127.0.0.1 port 12312
get good 1 comment from comment service host:127.0.0.1 port 13312
get good 2 comment from comment service host:127.0.0.1 port 11312
get good 3 comment from comment service host:127.0.0.1 port 12312

从日志中可以看出,不同 ID 的商品会请求不同的 comment 服务实例,并且不会改变请求的实例,这正是使用一致性哈希负载均衡策略想要得到的效果。

接下来,我们将端口号为11312的 comment 服务下线,此时就只有两个 comment 服务实例,我们再次发起上述的查询商品详情的请求,可以看到如下日志:

get good 1 comment from comment service host:127.0.0.1 port 13312
get good 2 comment from comment service host:127.0.0.1 port 13312
get good 3 comment from comment service host:127.0.0.1 port 12312
get good 1 comment from comment service host:127.0.0.1 port 13312
get good 2 comment from comment service host:127.0.0.1 port 13312
get good 3 comment from comment service host:127.0.0.1 port 12312

从这段日志我们可以看出:原来 ID 为 1 的商品详情会向端口为 13312 的 comment 服务实例进行请求,现在也是如此;而且 ID 为 3 的商品详情也跟原来一样,都是向端口为 12312 的服务实例进行请求,二者没有发生变化,这也是一致性哈希负载均衡策略的功效。

小结

在本课时,我为你讲述了在 Go 微服务中使用基于服务注册和发现的负载均衡机制,通过该机制,可以很方便地为下游集群增加和删除服务实例,上游服务也可以对其进行自动适配和负载均衡。除此之外,我们还以商品详情为例,给出了 Go 微服务负载均衡机制的具体实现,向你展示了使用一致性哈希负载均衡策略将请求发送给不同评论服务实例的场景。

文中讲解的基于服务注册和发现机制的负载均衡过程还有大量可以优化的细节,你还知道哪些呢?欢迎你留言,我们一起讨论。


精选评论

**源:

在serviceMesh结构下,是否可以把获取负载均衡调度结果的逻辑从业务代码中解耦出来呢 比如结合k8s的service申明

**波:

可以讲一下grpc在istio下面的负载均衡实现方式吗?

    讲师回复:

    istio支持grpc负载均衡,详情可以参考:https://cloud.google.com/solutions/using-istio-for-internal-load-balancing-of-grpc-services?hl=zh-cn

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340