作者:thofarq | 来源:互联网 | 2023-09-11 15:25
原创不易,求分享、求一键三连微服务会让我们的应用变多,并且为了高可用一个服务会在线上部署多台,那么进行服务调用就存在节点之间的负载均衡和服务发现,负载均衡是为了让各个节点的负载尽量
原创不易,求分享、求一键三连
微服务会让我们的应用变多,并且为了高可用一个服务会在线上部署多台,那么进行服务调用就存在节点之间的负载均衡和服务发现,负载均衡是为了让各个节点的负载尽量平均,而服务发现是为了解耦服务中provider和consumer的发现和调用。
通常服务发现有两种常用的方式:一种是服务端服务发现,服务发现的和请求路由实现逻辑放在服务端;另外一种是客户端服务发现,因为这种发现和路由逻辑是由发送请求的客户端来实现的,像我们现在使用的kratos微服务框架就是用这种方式进行负载均衡的:
微服务进行服务发现和负载均衡中经常用到一个重要的组件就是注册中心,接下来我们先说一下我们用到的注册中心discovery(参考地址:https://github.com/bilibili/discovery/blob/master/doc/arch.md) 以下是discovery的架构图:
涉及三种角色:
- 提供服务的是service Provider;
- 服务订阅是Service Consumer ;
- 注册中心是Discovery Server;
接下来我们分别说一下注册中心是如何来进行服务注册、发现、注册中心节点之间的数据同步、以及遇到一些问题是如何避免的。
服务注册
当一个Provider启动的时候,首先需要判断服务是否启动成功,一般会定一个监控检测的接口,判断项目运行的依赖是否正常比如:mysql、redis或者mq等。
检测成功则会向discovery server发起register请求,请求成功之后,后续会以心跳的方式发送renew请求保证该服务能在注册中心续租成功,当服务下线会先将自己的健康检测的healthcheck设置下线状态,再发送Cancel到注册中心进行注销,将未处理的业务处理完成等待一段时候时间之后注销服务。
服务发现
discovry中consumer通过长轮询的方式获取使用的provider最新数据,有数据更新则进行本地数据变更,那么来看下什么是长轮询(HTTP Long-Polling)
长轮询通常被叫做“Hanging GET”,大致是这样的:
如果服务器没有数据可供客户端使用,则服务器不会发送空响应,而是保留请求并等待某些数据变为可用;一旦数据可用,将向客户端发送完整响应。
然后客户端立即从服务端重新请求信息,以便服务器几乎总是有一个可用的等待请求,它可以使用该请求来传递数据以响应事件。
使用HTTP长轮询的应用程序的基本生命周期如下:
- 客户端使用常规HTTP发出初始请求,然后等待响应
- 服务器会延迟其响应,知道更新可用数据或者发生超时。
- 当更新可用时,服务器会向客户端发送完整响应。
- 客户端通常会在收到响应后立即发送新的长轮询请求,或者在暂停后发送请求,以允许可接受的延迟期。
- 每个长轮询请求都有一个超时。由于超时,客户端必须在链接关闭后定期重新链接。
Discovery 节点的数据同步
当集群中的discovery接收到provider节点的请求之后,会讲请求转发到其他discovery进行数据同步,保持每个节点都能保持全部的最新数据,这种同步会有一些延迟,后面会说到这种延迟对服务发现的注册中心这种业务场景来说是影响不大的。
go-grpc内置的负载均衡
grpc在官网文档中提供了实现LoadBalance的方法,不同语言的grpc代码API中也提供了命名解析和负载均衡接口供扩展。默认提供的是DNS resolver的实现,接口规范实现简单,只需实现服务注册和服务监听和解析的逻辑就可以了,运行机制如下:
Resolver 解析器,用于从注册中心实时获取当前服务端的列表,同步发送给Balancer;
当grpc注册服务发现时,实际时注册的resolver.Builder, Builder会开启单独的groutine,进行watch逻辑,当调用UpdateState 向ClientConn发送服务地址表的更新;
resolver中的ClientConn结构提供了resolver通知ClientConn更新服务列表的回调方法。
Balancer 平衡器,接收从Resolver发送的服务端列表,建立维护长链接;client发起rpc调用时,按照一定的算法从连接池中选择一个连接进行rpc调用;
Register 注册,用于服务端初始化和在线时,将自己信息上报到注册中心,主要信息包含Ip+端口。
grpc HealthCheck
grpc提供了一个标准的健康检测协议,在grpc的所有语言实现中基本都提供了生成代码和用于设置运行状态的功能。
主动健康检查health check,可以在服务提供者服务不稳定时,被消费者感知,临时从负载节点中移除,减少错误请求。
有了上面说到的服务注册中心和负载均衡的支持,下面看如何实现平滑发布和平滑下线。
平滑发布
外置服务调用 Provider health check 接口,发现接口不通就不会注册,发现通就会注册,外挂有个旁者帮你去注册。
一般服务启动会进行一些数据初始化,环境初始化,等所有完成,然后将接口的 health check 接口状态改为可用,这时候docker 容器就可以放流量进来了,这就是常用的外挂方式。
平滑下线
如果我们这时候有个新功能需要上线,这时候需要先下线走 滚动更新。
k8s 发送 kill 进程id
go main 拦截 sigterm 信号,先告诉服务发现注册中心服务要注销,然后服务发现通知其他服务将自己本地的负载均衡连接池 close 掉,并且流量不要再发送过去了。但是服务发现通知其他 consumer 需要一定的同步时间,所以下一步我们将自己服务的 health check 接口标记为 下线状态。
最终我们一般要等到 2 个服务发现心跳周期,这是最差的情况,一般是实时的退出。
当然,不可避免依旧会遇到一些问题:
当注册中心挂了会怎样?
我们现在技术栈用的是B站开源的Kratos微服务框架,做的是客户端负载均衡,会把用到的数据缓存在客户端内存里,当注册中心挂掉了,就无法获取到注册中心的provider节点变更信息,这段时间不发版本,正常情况下就不会有大量的节点变更,注册中心做好监控告警,快速恢复注册中心。
当注册的节点未全局同步完成会怎样?
注册中心做了高可用会启动多个节点,节点之间的数据同步会遵循 CAP 定理 一致性(Consistency)(等同于所有节点访问同一份最新的数据副本);
可用性(Availability)(每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据);
分区容错性(Partition tolerance)(以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择);
分布式系统智能满足三项中的两项不可能全部满足。Discovry是一个ap系统,数据同步会出现同步不成功、同步延迟等问题,对于未同步到的节点会延迟同步,只有部分节点可能会收到延迟,但是最终会都会同步成功。
当注销的节点未下线掉会怎样?
上面已经说到go-grpc支持健康检查的机制,如果节点未下线,但在provider下线前将健康检查接口置为不可用,则节点会从调用地址列表中移除,即使注册中心延迟下线,也不会有影响。
discovry中的provider是定期发送renew的方式进行续租,超过剔除心跳周期后,服务端会将对应的下线节点踢掉。
注册中心新节点上线,内存无数据会怎样?
新启动一个discovery节点到集群,这个时候新启动内存中没有其他节点的数据,discovery做了处理,在启动2-3个心跳周期才能提供consumer节点发现的服务,等待其他节点的数据全部同步之后才能提供服务。
discovery内部的自保护机制。
如果provider集群和discovery之间存在网络出现异常,但provider服务正常,只是心跳异常的情况,会启到保护的作用。
网络闪断和分区时自我保护模式
60s内丢失大量(小于Instance总数20.85)心跳数,“好”“坏”Instance信息都保留 所有node都会持续提供服务,单个node的注册和发现功能不受影响 最大保护时间,防止分区恢复后大量原先Instance真的已经不存在时,一直处于保护模式
参见(https://github.com/bilibili/discovery/blob/master/doc/arch.md)
染色环境(泳道环境)
为什么需要泳道环境?
我们同时存在几十个需求迭代同时开发,且不同的迭代可能会修改同一个项目,代码的冲突可以通过不同的分支解决,但开发阶段的联调都在同一个开发环境上的话会存在相互影响,造成开发效率低。
许多迭代的代码在同一个开发环境上联调会造成许多问题,研发人员解决这些问题开销的时间和精力成本很大。
这样的环境管理时间一久便造成了开发环境的联调不太稳定,研发人员转而将开发阶段的联调放到了测试环境,从而污染了测试环境,进而影响了测试阶段的效率。我们期望保证研发同学能够在不同的迭代情况下能够独立开发联调,测试同学期望保证测试环境的稳定。
"泳道",顾名思义是可以让不同的迭代开发能够在其自己的"泳道"上玩,不会影响其他的"泳道"。
我们选择了kratos框架,它本来就已经支持节点染色的功能,根据请求节点染色来进行负载能友好的解决上面提到的一些问题,在discovery注册到的节点中有个metadata用来存放节点的一些扩展字段,其中color就用来表示每个节点的染色环境:
在发送http请求的时候将需要访问的染色请求 设置到http header里面,在interface层解析放到ctx中,当去调用其他provider服务的时候,client端进行负载均衡进行节点选择,优先根据ctx中color选择对应染色的节点,不存在则默认color为空的节点进行调用。
上面说的这种情况我们只需要一套测试的基准环境,所有服务的color都是空,当同一个服务不同的需求变更就部署不同的color的多个节点,就能根据上面的方式进行测试。
好了,今天的分享就到这,喜欢的同学可以四连支持:
想要更多交流可以加我微信: