This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
<audio id="audio" title="07 | 架构设计设计一个灵活的RPC框架" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/39/1e/39db1403746007e0bece57ace8123f1e.mp3"></audio>
你好,我是何小锋。到今天为止,基础篇的知识我们就全部学习完了,接下来我们进入进阶篇。
在基础篇里面我们讲了RPC的通信原理以及RPC里各个功能组件的作用不妨用一段话再次回顾下“其实RPC就是把拦截到的方法参数转成可以在网络中传输的二进制并保证在服务提供方能正确地还原出语义最终实现像调用本地一样地调用远程的目的。”**你记住了吗?**
那学到这儿距离实现一个灵活的RPC框架其实还是有距离的。知道了各个功能组件只是迈出了第一步接下来你必须要清楚各个组件之间是怎么完成数据交互的这也是今天这讲的重点我们一起搞清楚RPC的架构设计。
## RPC架构
说起架构设计我相信你一定不陌生。我理解的架构设计呢就是从顶层角度出发厘清各模块组件之间数据交互的流程让我们对系统有一个整体的宏观认识。我们先看看RPC里面都有哪些功能模块。
我们讲过RPC本质上就是一个远程调用那肯定就需要通过网络来传输数据。虽然传输协议可以有多种选择但考虑到可靠性的话我们一般默认采用TCP协议。为了屏蔽网络传输的复杂性我们需要封装一个单独的数据传输模块用来收发二进制数据这个单独模块我们可以叫做传输模块。
用户请求的时候是基于方法调用,方法出入参数都是对象数据,对象是肯定没法直接在网络中传输的,我们需要提前把它转成可传输的二进制,这就是我们说的序列化过程。但只是把方法调用参数的二进制数据传输到服务提供方是不够的,我们需要在方法调用参数的二进制数据后面增加“断句”符号来分隔出不同的请求,在两个“断句”符号中间放的内容就是我们请求的二进制数据,这个过程我们叫做协议封装。
**虽然这是两个不同的过程,但其目的都是一样的,都是为了保证数据在网络中可以正确传输。**这里我说的正确,可不仅指数据能够传输,还需要保证传输后能正确还原出传输前的语义。所以我们可以把这两个处理过程放在架构中的同一个模块,统称为协议模块。
除此之外我们还可以在协议模块中加入压缩功能这是因为压缩过程也是对传输的二进制数据进行操作。在实际的网络传输过程中我们的请求数据包在数据链路层可能会因为太大而被拆分成多个数据包进行传输为了减少被拆分的次数从而导致整个传输过程时间太长的问题我们可以在RPC调用的时候这样操作在方法调用参数或者返回值的二进制数据大于某个阈值的情况下我们可以通过压缩框架进行无损压缩然后在另外一端也用同样的压缩算法进行解压保证数据可还原。
传输和协议这两个模块是RPC里面最基础的功能它们使对象可以正确地传输到服务提供方。但距离RPC的目标——实现像调用本地一样地调用远程还缺少点东西。因为这两个模块所提供的都是一些基础能力要让这两个模块同时工作的话我们需要手写一些黏合的代码但这些代码对我们使用RPC的研发人员来说是没有意义的而且属于一个重复的工作会导致使用过程的体验非常不友好。
这就需要我们在RPC里面把这些细节对研发人员进行屏蔽让他们感觉不到本地调用和远程调用的区别。假设有用到Spring的话我们希望RPC能让我们把一个RPC接口定义成一个Spring Bean并且这个Bean也会统一被Spring Bean Factory管理可以在项目中通过Spring依赖注入到方式引用。这是RPC调用的入口我们一般叫做Bootstrap模块。
**学到这儿一个点对点Point to Point版本的RPC框架就完成了。**我一般称这种模式的RPC框架为单机版本因为它没有集群能力。所谓集群能力就是针对同一个接口有着多个服务提供者但这多个服务提供者对于我们的调用方来说是透明的所以在RPC里面我们还需要给调用方找到所有的服务提供方并需要在RPC里面维护好接口跟服务提供者地址的关系这样调用方在发起请求的时候才能快速地找到对应的接收地址这就是我们常说的“服务发现”。
但服务发现只是解决了接口和服务提供方地址映射关系的查找问题这更多是一种“静态数据”。说它是静态数据是因为对于我们的RPC来说我们每次发送请求的时候都是需要用TCP连接的相对服务提供方IP地址TCP连接状态是瞬息万变的所以我们的RPC框架里面要有连接管理器去维护TCP连接的状态。
有了集群之后提供方可能就需要管理好这些服务了那我们的RPC就需要内置一些服务治理的功能比如服务提供方权重的设置、调用授权等一些常规治理手段。而服务调用方需要额外做哪些事情呢每次调用前我们都需要根据服务提供方设置的规则从集群中选择可用的连接用于发送请求。
那到这儿一个比较完善的RPC框架基本就完成了功能也差不多就是这些了。按照分层设计的原则我将这些功能模块分为了四层具体内容见图示
<img src="https://static001.geekbang.org/resource/image/30/fb/30f52b433aa5f103114a8420c6f829fb.jpg" alt="" title="架构图">
## 可扩展的架构
那RPC架构设计出来就完事了吗当然不技术迭代谁都躲不过。
不知道你有没有这样的经历,你设计的一个系统它看上去很完善,也能很好地运行,然后你成功地把它交付给了业务方。有一天业务方有了新的需求,要加入很多新的功能,这时候你就会发现当前架构面临的可就是大挑战了,要修改很多地方才能实现。
举个例子假如你设计了一个商品发布系统早些年我们只能在网上购买电脑、衣服等实物商品但现在发展成可以在网上购买电话充值卡、游戏点卡等虚拟商品实物商品的发布流程是需要选择购买区域的但虚拟商品并没有这一限制。如果你想要在一套发布系统里面同时完成实物和虚拟商品发布的话你就只能在代码里面加入很多的if else判断逻辑这样是能行可整个代码就臃肿、杂乱了后期也极难维护。
其实我们设计RPC框架也是一样的我们不可能在开始时就面面俱到。那有没有更好的方式来解决这些问题呢这就是我们接下来要讲的插件化架构。
在RPC框架里面我们是怎么支持插件化架构的呢我们可以将每个功能点抽象成一个接口将这个接口作为插件的契约然后把这个功能的接口与功能的实现分离并提供接口的默认实现。在Java里面JDK有自带的SPIService Provider Interface服务发现机制它可以动态地为某个接口寻找服务实现。使用SPI机制需要在Classpath下的META-INF/services目录里创建一个以服务接口命名的文件这个文件里的内容就是这个接口的具体实现类。
但在实际项目中我们其实很少使用到JDK自带的SPI机制首先它不能按需加载ServiceLoader加载某个接口实现类的时候会遍历全部获取也就是接口的实现类得全部载入并实例化一遍会造成不必要的浪费。另外就是扩展如果依赖其它的扩展那就做不到自动注入和装配这就很难和其他框架集成比如扩展里面依赖了一个Spring Bean原生的Java SPI就不支持。
加上了插件功能之后我们的RPC框架就包含了两大核心体系——核心功能体系与插件体系如下图所示
<img src="https://static001.geekbang.org/resource/image/a3/a6/a3688580dccd3053fac8c0178cef4ba6.jpg" alt="" title="插件化RPC">
**这时,整个架构就变成了一个微内核架构**我们将每个功能点抽象成一个接口将这个接口作为插件的契约然后把这个功能的接口与功能的实现分离并提供接口的默认实现。这样的架构相比之前的架构有很多优势。首先它的可扩展性很好实现了开闭原则用户可以非常方便地通过插件扩展实现自己的功能而且不需要修改核心功能的本身其次就是保持了核心包的精简依赖外部包少这样可以有效减少开发人员引入RPC导致的包版本冲突问题。
## 总结
我们都知道软件开发的过程很复杂,不仅是因为业务需求经常变化,更难的是在开发过程中要保证团队成员的目标统一。我们需要用一种可沟通的话语、可“触摸”的愿景达成目标,我认为这就是软件架构设计的意义。
但仅从功能角度设计出的软件架构并不够健壮,系统不仅要能正确地运行,还要以最低的成本进行可持续的维护,因此我们十分有必要关注系统的可扩展性。只有这样,才能满足业务变化的需求,让系统的生命力不断延伸。
## 课后思考
你能分享一下,在日常工作中,你都有哪些地方是用到了插件思想来解决扩展性问题的吗?
欢迎留言和我分享你的思考,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!

View File

@@ -0,0 +1,110 @@
<audio id="audio" title="08 | 服务发现到底是要CP还是AP" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8d/93/8d7ee317944e33a924f75445ceaa3393.mp3"></audio>
你好我是何小锋。在上一讲中我讲了“怎么设计一个灵活的RPC框架”总结起来就是怎么在RPC框架中应用插件用插件方式构造一个基于微内核的RPC框架其关键点就是“插件化”。
今天我要和你聊聊RPC里面的“服务发现”在超大规模集群的场景下所面临的挑战。
## 为什么需要服务发现?
先举个例子,假如你要给一位以前从未合作过的同事发邮件请求帮助,但你却没有他的邮箱地址。这个时候你会怎么办呢?如果是我,我会选择去看公司的企业“通信录”。
同理为了高可用在生产环境中服务提供方都是以集群的方式对外提供服务集群里面的这些IP随时可能变化我们也需要用一本“通信录”及时获取到对应的服务节点这个获取的过程我们一般叫作“服务发现”。
对于服务调用方和服务提供方来说其契约就是接口相当于“通信录”中的姓名服务节点就是提供该契约的一个具体实例。服务IP集合作为“通信录”中的地址从而可以通过接口获取服务IP的集合来完成服务的发现。这就是我要说的PRC框架的服务发现机制如下图所示
<img src="https://static001.geekbang.org/resource/image/51/5d/514dc04df2b8b2f3130b7d44776a825d.jpg" alt="" title="RPC服务发现原理图">
1. 服务注册在服务提供方启动的时候将对外暴露的接口注册到注册中心之中注册中心将这个服务节点的IP和接口保存下来。
1. 服务订阅在服务调用方启动的时候去注册中心查找并订阅服务提供方的IP然后缓存到本地并用于后续的远程调用。
## 为什么不使用DNS
既然服务发现这么“厉害”那是不是很难实现啊其实类似机制一直在我们身边我们回想下服务发现的本质就是完成了接口跟服务提供者IP的映射。那我们能不能把服务提供者IP统一换成一个域名啊利用已经成熟的DNS机制来实现
先带着这个问题简单地看下DNS的流程
<img src="https://static001.geekbang.org/resource/image/3b/18/3b6a23f392b9b8d6fcf31803a5b4ef18.jpg" alt="" title="DNS查询流程">
如果我们用DNS来实现服务发现所有的服务提供者节点都配置在了同一个域名下调用方的确可以通过DNS拿到随机的一个服务提供者的IP并与之建立长连接这看上去并没有太大问题但在我们业界为什么很少用到这种方案呢不知道你想过这个问题没有如果没有现在可以停下来想想这样两个问题
- 如果这个IP端口下线了服务调用者能否及时摘除服务节点呢
- 如果在之前已经上线了一部分服务节点,这时我突然对这个服务进行扩容,那么新上线的服务节点能否及时接收到流量呢?
这两个问题的答案都是“不能”。这是因为为了提升性能和减少DNS服务的压力DNS采取了多级缓存机制一般配置的缓存时间较长特别是JVM的默认缓存是永久有效的所以说服务调用者不能及时感知到服务节点的变化。
这时你可能会想我是不是可以加一个负载均衡设备呢将域名绑定到这台负载均衡设备上通过DNS拿到负载均衡的IP。这样服务调用的时候服务调用方就可以直接跟VIP建立连接然后由VIP机器完成TCP转发如下图所示
<img src="https://static001.geekbang.org/resource/image/d8/b9/d8549f6069a8ca5bd1012a0baf90f6b9.jpg" alt="" title="VIP方案">
这个方案确实能解决DNS遇到的一些问题但在RPC场景里面也并不是很合适原因有以下几点
- 搭建负载均衡设备或TCP/IP四层代理需求额外成本
- 请求流量都经过负载均衡设备,多经过一次网络传输,会额外浪费些性能;
- 负载均衡添加节点和摘除节点,一般都要手动添加,当大批量扩容和下线时,会有大量的人工操作和生效延迟;
- 我们在服务治理的时候,需要更灵活的负载均衡策略,目前的负载均衡设备的算法还满足不了灵活的需求。
由此可见DNS或者VIP方案虽然可以充当服务发现的角色但在RPC场景里面直接用还是很难的。
## 基于ZooKeeper的服务发现
那么在RPC里面我们该如何实现呢我们还是要回到服务发现的本质就是完成接口跟服务提供者IP之间的映射。这个映射是不是就是一种命名服务当然我们还希望注册中心能完成实时变更推送是不是像开源的ZooKeeper、etcd就可以实现我很肯定地说“确实可以”。下面我就来介绍下一种基于ZooKeeper的服务发现方式。
整体的思路很简单就是搭建一个ZooKeeper集群作为注册中心集群服务注册的时候只需要服务节点向ZooKeeper节点写入注册信息即可利用ZooKeeper的Watcher机制完成服务订阅与服务下发功能整体流程如下图
<img src="https://static001.geekbang.org/resource/image/50/75/503fabeeae226a722f83e9fb6c0d4075.jpg" alt="" title="基于ZooKeeper服务发现结构图">
1. 服务平台管理端先在ZooKeeper中创建一个服务根路径可以根据接口名命名例如/service/com.demo.xxService在这个路径再创建服务提供方目录与服务调用方目录例如provider、consumer分别用来存储服务提供方的节点信息和服务调用方的节点信息。
1. 当服务提供方发起注册时,会在服务提供方目录中创建一个临时节点,节点中存储该服务提供方的注册信息。
1. 当服务调用方发起订阅时则在服务调用方目录中创建一个临时节点节点中存储该服务调用方的信息同时服务调用方watch该服务的服务提供方目录/service/com.demo.xxService/provider中所有的服务节点数据。
1. 当服务提供方目录下有节点数据发生变更时ZooKeeper就会通知给发起订阅的服务调用方。
我所在的技术团队早期使用的RPC框架服务发现就是基于ZooKeeper实现的并且还平稳运行了一年多但后续团队的微服务化程度越来越高之后ZooKeeper集群整体压力也越来越高尤其在集中上线的时候越发明显。“集中爆发”是在一次大规模上线的时候当时有超大批量的服务节点在同时发起注册操作ZooKeeper集群的CPU突然飙升导致ZooKeeper集群不能工作了而且我们当时也无法立马将ZooKeeper集群重新启动一直到ZooKeeper集群恢复后业务才能继续上线。
经过我们的排查引发这次问题的根本原因就是ZooKeeper本身的性能问题当连接到ZooKeeper的节点数量特别多对ZooKeeper读写特别频繁且ZooKeeper存储的目录达到一定数量的时候ZooKeeper将不再稳定CPU持续升高最终宕机。而宕机之后由于各业务的节点还在持续发送读写请求刚一启动ZooKeeper就因无法承受瞬间的读写压力马上宕机。
这次“意外”让我们意识到ZooKeeper集群性能显然已经无法支撑我们现有规模的服务集群了我们需要重新考虑服务发现方案。
## 基于消息总线的最终一致性的注册中心
我们知道ZooKeeper的一大特点就是强一致性ZooKeeper集群的每个节点的数据每次发生更新操作都会通知其它ZooKeeper节点同时执行更新。它要求保证每个节点的数据能够实时的完全一致这也就直接导致了ZooKeeper集群性能上的下降。这就好比几个人在玩传递东西的游戏必须这一轮每个人都拿到东西之后所有的人才能开始下一轮而不是说我只要获得到东西之后就可以直接进行下一轮了。
而RPC框架的服务发现在服务节点刚上线时服务调用方是可以容忍在一段时间之后比如几秒钟之后发现这个新上线的节点的。毕竟服务节点刚上线之后的几秒内甚至更长的一段时间内没有接收到请求流量对整个服务集群是没有什么影响的所以我们可以牺牲掉CP强制一致性而选择AP最终一致来换取整个注册中心集群的性能和稳定性。
那么是否有一种简单、高效并且最终一致的更新机制能代替ZooKeeper那种数据强一致的数据更新机制呢
因为要求最终一致性,我们可以考虑采用消息总线机制。注册数据可以全量缓存在每个注册中心内存中,通过消息总线来同步数据。当有一个注册中心节点接收到服务节点注册时,会产生一个消息推送给消息总线,再通过消息总线通知给其它注册中心节点更新数据并进行服务下发,从而达到注册中心间数据最终一致性,具体流程如下图所示:
<img src="https://static001.geekbang.org/resource/image/73/ff/73b59c7949ebed2903ede474856062ff.jpg" alt="" title="流程图">
- 当有服务上线,注册中心节点收到注册请求,服务列表数据发生变化,会生成一个消息,推送给消息总线,每个消息都有整体递增的版本。
- 消息总线会主动推送消息到各个注册中心,同时注册中心也会定时拉取消息。对于获取到消息的在消息回放模块里面回放,只接受大于本地版本号的消息,小于本地版本号的消息直接丢弃,从而实现最终一致性。
- 消费者订阅可以从注册中心内存拿到指定接口的全部服务实例,并缓存到消费者的内存里面。
- 采用推拉模式,消费者可以及时地拿到服务实例增量变化情况,并和内存中的缓存数据进行合并。
为了性能,这里采用了两级缓存,注册中心和消费者的内存缓存,通过异步推拉模式来确保最终一致性。
另外你也可能会想到服务调用方拿到的服务节点不是最新的所以目标节点存在已经下线或不提供指定接口服务的情况这个时候有没有问题这个问题我们放到了RPC框架里面去处理在服务调用方发送请求到目标节点后目标节点会进行合法性验证如果指定接口服务不存在或正在下线则会拒绝该请求。服务调用方收到拒绝异常后会安全重试到其它节点。
通过消息总线的方式我们就可以完成注册中心集群间数据变更的通知保证数据的最终一致性并能及时地触发注册中心的服务下发操作。在RPC领域精耕细作后你会发现服务发现的特性是允许我们在设计超大规模集群服务发现系统的时候舍弃强一致性更多地考虑系统的健壮性。最终一致性才是分布式系统设计中更为常用的策略。
## 总结
今天我分享了RPC框架服务发现机制以及如何用ZooKeeper完成“服务发现”还有ZooKeeper在超大规模集群下作为注册中心所存在的问题。
通常我们可以使用ZooKeeper、etcd或者分布式缓存如Hazelcast来解决事件通知问题但当集群达到一定规模之后依赖的ZooKeeper集群、etcd集群可能就不稳定了无法满足我们的需求。
在超大规模的服务集群下,注册中心所面临的挑战就是超大批量服务节点同时上下线,注册中心集群接受到大量服务变更请求,集群间各节点间需要同步大量服务节点数据,最终导致如下问题:
- 注册中心负载过高;
- 各节点数据不一致;
- 服务下发不及时或下发错误的服务节点列表。
RPC框架依赖的注册中心的服务数据的一致性其实并不需要满足CP只要满足AP即可。我们就是采用“消息总线”的通知机制来保证注册中心数据的最终一致性来解决这些问题的。
另外在今天的内容中很多知识点不只可以应用到RPC框架的“服务发现”中。例如服务节点数据的推送采用增量更新的方式这种方式提高了注册中心“服务下发”的效率而这种方式你还可以利用在其它地方比如统一配置中心用此方式可以提升统一配置中心下发配置的效率。
## 课后思考
目前服务提供者上线后会自动注册到注册中心,服务调用方会自动感知到新增的实例,并且流量会很快打到该新增的实例。如果我想把某些服务提供者实例的流量切走,除了下线实例,你有没有想到其它更便捷的办法呢?
欢迎留言和我分享你的思考和疑惑,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!

View File

@@ -0,0 +1,99 @@
<audio id="audio" title="09 | 健康检测:这个节点都挂了,为啥还要疯狂发请求?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/79/65/79abc049755329a2f22bce8b4e30a365.mp3"></audio>
你好我是何小锋。上一讲我们介绍了超大规模集群“服务发现”的挑战服务发现的作用就是实时感知集群IP的变化实现接口跟服务集群节点IP的映射。在超大规模集群实战中我们更多需要考虑的是保证最终一致性。其实总结来说就一关键词你要记住“推拉结合以拉为准”。接着昨天的内容我们再来聊聊RPC中的健康检测。
因为有了集群所以每次发请求前RPC框架会根据路由和负载均衡算法选择一个具体的IP地址。为了保证请求成功我们就需要确保每次选择出来的IP对应的连接是健康的这个逻辑你应该理解。
但你也知道,调用方跟服务集群节点之间的网络状况是瞬息万变的,两者之间可能会出现闪断或者网络设备损坏等情况,那怎么保证选择出来的连接一定是可用的呢?
从我的角度看,**终极的解决方案是让调用方实时感知到节点的状态变化**,这样他们才能做出正确的选择。这个道理像我们开车一样,车有各种各样的零件,我们不可能在开车之前先去挨个检查下他们的健康情况,转而是应该有一套反馈机制,比如今天我的大灯坏了,那中控台就可以给我提示;明天我的胎压不够了,中控台也能够收到提示。汽车中大部分关键零件的状态变化,我作为调用方,都能够第一时间了解。
那回到RPC框架里我们应该怎么设计这套机制呢你可以先停下来想想汽车的例子看看他们是怎么做的。当然回到我们RPC的框架里这事用专业一点的词来说就是服务的健康检测。今天我们就来详细聊聊这个话题。
## 遇到的问题
在进一步讲解服务健康检测之前,我想先和你分享一个我曾经遇到过的线上问题。
有一天,我们公司某个业务研发团队的负责人急匆匆跑过来,让我帮他解决个问题。仔细听完他的描述后,我才明白,原来是他们发现线上业务的某个接口可用性并不高,基本上十次调用里总会有几次失败。
查看了具体的监控数据之后,我们发现只有请求具体打到某台机器的时候才会有这个问题,也就是说,集群中有某台机器出了问题。于是快刀斩乱麻,我建议他们先把这台“问题机器”下线,以快速解决目前的问题。
<img src="https://static001.geekbang.org/resource/image/de/77/de02d702b4c2dc86400e59de8ae48277.jpg" alt="">
但对于我来说问题并没有结束我开始进一步琢磨“接口调用某台机器的时候已经出现不能及时响应了那为什么RPC框架还会继续把请求发到这台有问题的机器上呢RPC框架还会把请求发到这台机器上也就是说从调用方的角度看它没有觉得这台服务器有问题。”
就像警察破案一样,为了进一步了解事情的真相,我查看了问题时间点的监控和日志,在案发现场发现了这样几个线索:
1. 通过日志发现请求确实会一直打到这台有问题的机器上,因为我看到日志里有很多超时的异常信息。
1. 从监控上看这台机器还是有一些成功的请求这说明当时调用方跟服务之间的网络连接没有断开。因为如果连接断开之后RPC框架会把这个节点标识为“不健康”不会被选出来用于发业务请求。
1. 深入进去看异常日志,我发现调用方到目标机器的定时心跳会有间歇性失败。
1. 从目标机器的监控上可以看到该机器的网络指标有异常出问题时间点TCP重传数比正常高10倍以上。
**有了对这四个线索的分析,我基本上可以得出这样的结论:**那台问题服务器在某些时间段出现了网络故障,但也还能处理部分请求。换句话说,它处于半死不活的状态。但是(是转折,也是关键点),它还没彻底“死”,还有心跳,这样,调用方就觉得它还正常,所以就没有把它及时挪出健康状态列表。
到这里,你应该也明白了,一开始,我们为了快速解决问题,手动把那台问题机器下线了。刨根问底之后,我们发现,其实更大的问题是我们的服务检测机制有问题,有的服务本来都已经病危了,但我们还以为人家只是个感冒。
接下来,我们就来看看服务检测的核心逻辑。
## 健康检测的逻辑
刚刚我们提到了心跳机制我估计你会想搞什么心跳是不是我们把问题复杂化了。当服务方下线正常情况下我们肯定会收到连接断开的通知事件在这个事件里面直接加处理逻辑不就可以了是的我们前面汽车的例子里检测都是这样做的。但咱们这里不行因为应用健康状况不仅包括TCP连接状况还包括应用本身是否存活很多情况下TCP连接没有断开但应用可能已经“僵死了”。
所以,业内常用的检测方法就是用心跳机制。心跳机制说起来也不复杂,其实就是服务调用方每隔一段时间就问一下服务提供方,“兄弟,你还好吧?”,然后服务提供方很诚实地告诉调用方它目前的状态。
结合前面的文章,你也不难想出来,服务方的状态一般会有三种情况,一个是我很好,一个是我生病了,一个是没回复。用专业的词来对应这三个状态就是:
1. 健康状态:建立连接成功,并且心跳探活也一直成功;
1. 亚健康状态:建立连接成功,但是心跳请求连续失败;
1. 死亡状态:建立连接失败。
节点的状态并不是固定不变的,它会根据心跳或者重连的结果来动态变化,具体状态间转换图如下:
<img src="https://static001.geekbang.org/resource/image/87/ac/872d83cb7a604a49302c16ca993c4bac.jpg" alt="">
这里你可以关注下几个状态之间的转换剪头,我再给你解释下。首先,一开始初始化的时候,如果建立连接成功,那就是健康状态,否则就是死亡状态。这里没有亚健康这样的中间态。紧接着,如果健康状态的节点连续出现几次不能响应心跳请求的情况,那就会被标记为亚健康状态,也就是说,服务调用方会觉得它生病了。
生病之后(亚健康状态),如果连续几次都能正常响应心跳请求,那就可以转回健康状态,证明病好了。如果病一直好不了,那就会被断定为是死亡节点,死亡之后还需要善后,比如关闭连接。
当然,死亡并不是真正死亡,它还有复活的机会。如果某个时间点里,死亡的节点能够重连成功,那它就可以重新被标记为健康状态。
这就是整个节点的状态转换思路,你不用死记,它很简单,除了不能复活,其他都和我们人的状态一样。当服务调用方通过心跳机制了解了节点的状态之后,每次发请求的时候,就可以优先从健康列表里面选择一个节点。当然,如果健康列表为空,为了提高可用性,也可以尝试从亚健康列表里面选择一个,这就是具体的策略了。
## 具体的解决方案
理解了服务健康检测的逻辑我们再回到开头我描述的场景里看看怎么优化。现在你理解了一个节点从健康状态过渡到亚健康状态的前提是“连续”心跳失败次数必须到达某一个阈值比如3次具体看你怎么配置了
而我们的场景里,节点的心跳日志只是间歇性失败,也就是时好时坏,这样,失败次数根本没到阈值,调用方会觉得它只是“生病”了,并且很快就好了。那怎么解决呢?我还是建议你先停下来想想。
你是不是会脱口而出,说改下配置,调低阈值呗。是的,这是最快的解决方法,但是我想说,它治标不治本。第一,像前面说的那样,调用方跟服务节点之间网络状况瞬息万变,出现网络波动的时候会导致误判。第二,在负载高情况,服务端来不及处理心跳请求,由于心跳时间很短,会导致调用方很快触发连续心跳失败而造成断开连接。
我们回到问题的本源,核心是服务节点网络有问题,心跳间歇性失败。我们现在判断节点状态只有一个维度,那就是心跳检测,那是不是可以再加上业务请求的维度呢?
起码我当时是顺着这个方向解决问题的。但紧接着,我又发现了新的麻烦:
1. 调用方每个接口的调用频次不一样有的接口可能1秒内调用上百次有的接口可能半个小时才会调用一次所以我们不能把简单的把总失败的次数当作判断条件。
1. 服务的接口响应时间也是不一样的有的接口可能1ms有的接口可能是10s所以我们也不能把TPS至来当作判断条件。
和同事讨论之后,我们找到了**可用率**这个突破口,应该相对完美了。可用率的计算方式是某一个时间窗口内接口调用成功次数的百分比(成功次数/总调用次数)。当可用率低于某个比例就认为这个节点存在问题,把它挪到亚健康列表,这样既考虑了高低频的调用接口,也兼顾了接口响应时间不同的问题。
## 总结
这一讲我给你分享了RPC框架里面的一个核心的功能——健康检测它能帮助我们从连接列表里面过滤掉一些存在问题的节点避免在发请求的时候选择出有问题的节点而影响业务。但是在设计健康检测方案的时候我们不能简单地从TCP连接是否健康、心跳是否正常等简单维度考虑因为健康检测的目的就是要保证“业务无损”所以在设计方案的时候我们可以加入业务请求可用率因素这样能最大化地提升RPC接口可用率。
正常情况下我们大概30S会发一次心跳请求这个间隔一般不会太短如果太短会给服务节点造成很大的压力。但是如果太长的话又不能及时摘除有问题的节点。
除了在RPC框架里面我们会有采用定时“健康检测”其实在其它分布式系统设计的时候也会用到“心跳探活”机制。
比如在应用监控系统设计的时候需要对不健康的应用实例进行报警好让运维人员及时处理。和咱们RPC的例子一样在这个场景里你也不能简单地依赖端口的连通性来判断应用是否存活因为在端口连通正常的情况下应用也可能僵死了。
那有啥其他办法能处理应用僵死的情况吗我们可以让每个应用实例提供一个“健康检测”的URL检测程序定时通过构造HTTP请求访问该URL然后根据响应结果来进行存活判断这样就可以防止僵死状态的误判。你想想这不就是咱们前面讲到的心跳机制吗
不过,这个案例里,我还要卖个关子。**加完心跳机制,是不是就没有问题了呢?**当然不是,因为检测程序所在的机器和目标机器之间的网络可能还会出现故障,如果真出现了故障,不就会误判吗?你以为人家已经生病或者挂了,其实是心跳仪器坏了…
根据我的经验,有一个办法可以减少误判的几率,那就是把检测程序部署在多个机器里面,分布在不同的机架,甚至不同的机房。因为网络同时故障的概率非常低,所以只要任意一个检测程序实例访问目标机器正常,就可以说明该目标机器正常。
## 课后思考
不知道看完今天的分享之后你有何感触,你在工作中会接触到健康检测的场景吗?你可以在留言区给我分享下你是怎么做的,或者给我的方案挑挑毛病,我会第一时间给你反馈。
当然,也欢迎你留言和我分享你的思考和疑惑,期待你能把今天的所学分享给身边的朋友,邀请他一同交流。我们下节课再见!

View File

@@ -0,0 +1,69 @@
<audio id="audio" title="10 | 路由策略:怎么让请求按照设定的规则发到不同的节点上?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/79/b7/79d4a8238e1a46a8aedd67c5027961b7.mp3"></audio>
你好我是何小锋。上一讲我们介绍了健康检测在RPC中的作用简单来讲就是帮助调用方应用来管理所有服务提供方的连接并动态维护每个连接的状态方便服务调用方在每次发起请求的时候都可以拿到一个可用的连接。回顾完上一讲的重点我们就切入今天的主题——RPC中的路由策略。
## 为什么选择路由策略?
在前面我们提到过在真实环境中我们的服务提供方是以一个集群的方式提供服务这对于服务调用方来说就是一个接口会有多个服务提供方同时提供服务所以我们的RPC在每次发起请求的时候都需要从多个服务提供方节点里面选择一个用于发请求的节点。
既然这些节点都可以用来完成这次请求,那么我们就可以简单地认为这些节点是同质的。这里的同质怎么理解呢?就是这次请求无论发送到集合中的哪个节点上,返回的结果都是一样的。
既然服务提供方是以集群的方式对外提供服务,那就要考虑一些实际问题。要知道我们每次上线应用的时候都不止一台服务器会运行实例,那上线就涉及到变更,只要变更就可能导致原本正常运行的程序出现异常,尤其是发生重大变动的时候,导致我们应用不稳定的因素就变得很多。
为了减少这种风险,我们一般会选择灰度发布我们的应用实例,比如我们可以先发布少量实例观察是否有异常,后续再根据观察的情况,选择发布更多实例还是回滚已经上线的实例。
但这种方式不好的一点就是,线上一旦出现问题,影响范围还是挺大的。因为对于我们的服务提供方来说,我们的服务会同时提供给很多调用方来调用,尤其是像一些基础服务的调用方会更复杂,比如商品、价格等等,一旦刚上线的实例有问题了,那将会导致所有的调用方业务都会受损。
那对于我们的RPC框架来说有什么的办法可以减少上线变更导致的风险吗这就不得不提路由在RPC中的应用。具体好在哪里怎么实现我们接着往下看。
## 如何实现路由策略?
可能你会说,我们可以在上线前把所有的场景都重新测试一遍啊?这也是一种方法,而且测试肯定是上线前的一个重要环节。但以我个人的经验来看,由于线上环境太复杂了,单纯从测试角度出发只能降低风险出现的概率,想要彻底验证所有场景基本是不可能的。
那如果没法100%规避风险我们还能怎么办我认为只有一条路可以尝试了就是尽量减小上线出问题导致业务受损的范围。基于这个思路我们是不是可以在上线完成后先让一小部分调用方请求过来进行逻辑验证待没问题后再接入其他调用方从而实现流量隔离的效果。那在RPC框架里面我们具体该怎么实现呢
我们在服务发现那讲讲过在RPC里面服务调用方是通过服务发现的方式拿到了所有服务提供方的IP地址那我们是不是就可以利用这个特点当我们选择要灰度验证功能的时候是不是就可以让注册中心在推送的时候区别对待而不是一股脑的把服务提供方的IP地址推送到所有调用方。换句话说就是注册中心只会把刚上线的服务IP地址推送到选择指定的调用方而其他调用方是不能通过服务发现拿到这个IP地址的。
通过服务发现的方式来隔离调用方请求从逻辑上来看确实可行但注册中心在RPC里面的定位是用来存储数据并保证数据一致性的。如果把这种复杂的计算逻辑放到注册中心里面当集群节点变多之后就会导致注册中心压力很大而且大部分情况下我们一般都是采用开源软件来搭建注册中心要满足这种需求还需要进行二次开发。所以从实际的角度出发通过影响服务发现来实现请求隔离并不划算。
那还有其他更合适的解决方案吗?在我给出方案前,你可以停下来思考下你的解决方案。
我们可以重新回到调用方发起RPC调用的流程。在RPC发起真实请求的时候有一个步骤就是从服务提供方节点集合里面选择一个合适的节点就是我们常说的负载均衡那我们是不是可以在选择节点前加上“筛选逻辑”把符合我们要求的节点筛选出来。那这个筛选的规则是什么呢就是我们前面说的灰度过程中要验证的规则。
举个具体例子你可能就明白了比如我们要求新上线的节点只允许某个IP可以调用那我们的注册中心会把这条规则下发到服务调用方。在调用方收到规则后在选择具体要发请求的节点前会先通过筛选规则过滤节点集合按照这个例子的逻辑最后会过滤出一个节点这个节点就是我们刚才新上线的节点。通过这样的改造RPC调用流程就变成了这样
<img src="https://static001.geekbang.org/resource/image/b7/68/b78964a2db3adc8080364e9cfc79ca68.jpg" alt="" title="调用流程">
这个筛选过程在我们的RPC里面有一个专业名词就是“路由策略”而上面例子里面的路由策略是我们常见的IP路由策略用于限制可以调用服务提供方的IP。使用了IP路由策略后整个集群的调用拓扑如下图所示
<img src="https://static001.geekbang.org/resource/image/23/f7/23f24c545d33ec4d6d72fc10e94a0ff7.jpg" alt="" title="IP路由调用拓扑">
## 参数路由
有了IP路由之后上线过程中我们就可以做到只让部分调用方请求调用到新上线的实例相对传统的灰度发布功能来说这样做我们可以把试错成本降到最低。
但在有些场景下我们可能还需要更细粒度的路由方式。比如在升级改造应用的时候为了保证调用方能平滑地切调用我们的新应用逻辑在升级过程中我们常用的方式是让新老应用并行运行一段时间然后通过切流量百分比的方式慢慢增大新应用承接的流量直到新应用承担了100%且运行一段时间后才能去下线老应用。
在流量切换的过程中为了保证整个流程的完整性我们必须保证某个主题对象的所有请求都使用同一种应用来承接。假设我们改造的是商品应用那主题对象肯定是商品ID在切流量的过程中我们必须保证某个商品的所有操作都是用新应用或者老应用来完成所有请求的响应。
很显然上面的IP路由并不能满足我们这个需求因为IP路由只是限制调用方来源并不会根据请求参数请求到我们预设的服务提供方节点上去。
那我们怎么利用路由策略实现这个需求呢?其实你只要明白路由策略的本质,就不难明白这种参数路由的实现。
我们可以给所有的服务提供方节点都打上标签用来区分新老应用节点。在服务调用方发生请求的时候我们可以很容易地拿到请求参数也就是我们例子中的商品ID我们可以根据注册中心下发的规则来判断当前商品ID的请求是过滤掉新应用还是老应用的节点。因为规则对所有的调用方都是一样的从而保证对应同一个商品ID的请求要么是新应用的节点要么是老应用的节点。使用了参数路由策略后整个集群的调用拓扑如下图所示
<img src="https://static001.geekbang.org/resource/image/78/39/7868289c87ca9de144fe32fac98f8339.jpg" alt="" title="参数路由调用拓扑">
相比IP路由参数路由支持的灰度粒度更小他为服务提供方应用提供了另外一个服务治理的手段。灰度发布功能是RPC路由功能的一个典型应用场景通过RPC路由策略的组合使用可以让服务提供方更加灵活地管理、调用自己的流量进一步降低上线可能导致的风险。
## 总结
在日常工作中,我们几乎每天都在做线上变更,每次变更都有可能带来一次事故,为了降低事故发生的概率,我们不光要从流程上优化操作步骤,还要使我们的基础设施能支持更低的试错成本。
灰度发布功能作为RPC路由功能的一个典型应用场景我们可以通过路由功能完成像定点调用、黑白名单等一些高级服务治理功能。在RPC里面不管是哪种路由策略其核心思想都是一样的就是让请求按照我们设定的规则发送到目标节点上从而实现流量隔离的效果。
## 课后思考
你在使用RPC的过程中除了用路由策略实现过灰度发布、定点调用等功能还用它完成过其他功能吗
欢迎留言和我分享你的思考,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!

View File

@@ -0,0 +1,106 @@
<audio id="audio" title="11 | 负载均衡:节点负载差距这么大,为什么收到的流量还一样?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/01/d9/011830b29f0a089f7e120cddb2e50ed9.mp3"></audio>
你好我是何小锋。上一讲我讲解了“多场景的路由选择”其核心就是“如何根据不同的场景控制选择合适的目标机器”。今天我们来聊一个新的话题看看在RPC中如何实现负载均衡。
## 一个需求
在进入主题之前,我想先和你分享一个需求,这是我们公司的业务部门给我们提的。
他们反馈的问题是这样的:有一次碰上流量高峰,他们突然发现线上服务的可用率降低了,经过排查发现,是因为其中有几台机器比较旧了。当时最早申请的一批容器配置比较低,缩容的时候留下了几台,当流量达到高峰时,这几台容器由于负载太高,就扛不住压力了。业务问我们有没有好的服务治理策略?
<img src="https://static001.geekbang.org/resource/image/a9/f7/a9dfb6a37f8587851a5288b5d6cbf1f7.jpg" alt="" title="业务部门问题示意图">
这个问题其实挺好解决的,我们当时给出的方案是:在治理平台上调低这几台机器的权重,这样的话,访问的流量自然就减少了。
但业务接着反馈了,说:当他们发现服务可用率降低的时候,业务请求已经受到影响了,这时再如此解决,需要时间啊,那这段时间里业务可能已经有损失了。紧接着他们就提出了需求,问:**RPC框架有没有什么智能负载的机制能否及时地自动控制服务节点接收到的访问量**
这个需求其实很合理,这也是一个比较普遍的问题。确实,虽说我们的服务治理平台能够动态地控制线上服务节点接收的访问量,但当业务方发现部分机器负载过高或者响应变慢的时候再去调整节点权重,真的很可能已经影响到线上服务的可用率了。
看到这儿你有没有想到什么好的处理方案呢接下来我们就以这个问题为背景一起看看RPC框架的负载均衡。
## 什么是负载均衡?
我先来简单地介绍下负载均衡。当我们的一个服务节点无法支撑现有的访问量时,我们会部署多个节点,组成一个集群,然后通过负载均衡,将请求分发给这个集群下的每个服务节点,从而达到多个服务节点共同分担请求压力的目的。
<img src="https://static001.geekbang.org/resource/image/f4/b8/f48704443b33df17fc490778c00c71b8.jpg" alt="" title="负载均衡示意图">
负载均衡主要分为软负载和硬负载软负载就是在一台或多台服务器上安装负载均衡的软件如LVS、Nginx等硬负载就是通过硬件设备来实现的负载均衡如F5服务器等。负载均衡的算法主要有随机法、轮询法、最小连接法等。
我刚才介绍的负载均衡主要还是应用在Web服务上Web服务的域名绑定负载均衡的地址通过负载均衡将用户的请求分发到一个个后端服务上。
## RPC框架中的负载均衡
那RPC框架中的负载均衡是不是也是如此呢和我上面讲的负载均衡你觉得会有区别吗
我们可以回想下[[第 08 讲]](https://time.geekbang.org/column/article/208171) 的开头我讲到为什么不通过DNS来实现“服务发现”之后我又讲解了为什么不采用添加负载均衡设备或者TCP/IP四层代理域名绑定负载均衡设备的IP或者四层代理IP的方式。
我的回答是这种方式会面临这样几个问题:
1. 搭建负载均衡设备或TCP/IP四层代理需要额外成本
1. 请求流量都经过负载均衡设备,多经过一次网络传输,会额外浪费一些性能;
1. 负载均衡添加节点和摘除节点,一般都要手动添加,当大批量扩容和下线时,会有大量的人工操作,“服务发现”在操作上是个问题;
1. 我们在服务治理的时候,针对不同接口服务、服务的不同分组,我们的负载均衡策略是需要可配的,如果大家都经过这一个负载均衡设备,就不容易根据不同的场景来配置不同的负载均衡策略了。
我相信看到这儿你应该已经知道了RPC实现的负载均衡所采用的策略与传统的Web服务实现负载均衡所采用策略的不同之处了。
RPC的负载均衡完全由RPC框架自身实现RPC的服务调用者会与“注册中心”下发的所有服务节点建立长连接在每次发起RPC调用时服务调用者都会通过配置的负载均衡插件自主选择一个服务节点发起RPC调用请求。
<img src="https://static001.geekbang.org/resource/image/5e/1c/5e294378a3d86e7d279507f62fe5ee1c.jpg" alt="" title="RPC框架负载均衡示意图">
RPC负载均衡策略一般包括随机权重、Hash、轮询。当然这还是主要看RPC框架自身的实现。其中的随机权重策略应该是我们最常用的一种了通过随机算法我们基本可以保证每个节点接收到的请求流量是均匀的同时我们还可以通过控制节点权重的方式来进行流量控制。比如我们默认每个节点的权重都是100但当我们把其中的一个节点的权重设置成50时它接收到的流量就是其他节点的1/2。
这几种负载均衡算法的实现还是很简单的,网上资料也非常多,在这我就不过多介绍了。有什么问题,咱们可以在留言区交流。
由于负载均衡机制完全是由RPC框架自身实现的所以它不再需要依赖任何负载均衡设备自然也不会发生负载均衡设备的单点问题服务调用方的负载均衡策略也完全可配同时我们可以通过控制权重的方式对负载均衡进行治理。
了解完RPC框架的负载均衡现在我们就可以回到这讲最开头业务提的那个需求有没有什么办法可以动态地、智能地控制线上服务节点所接收到的请求流量
现在答案是不是就显而易见了解决问题的关键就在于RPC框架的负载均衡上。对于这个问题我们当时的方案就是设计一种自适应的负载均衡策略。
## 如何设计自适应的负载均衡?
我刚才讲过RPC的负载均衡完全由RPC框架自身实现服务调用者发起请求时会通过配置的负载均衡插件自主地选择服务节点。那是不是只要调用者知道每个服务节点处理请求的能力再根据服务处理节点处理请求的能力来判断要打给它多少流量就可以了当一个服务节点负载过高或响应过慢时就少给它发送请求反之则多给它发送请求。
这就有点像日常工作中的分配任务,要多考虑实际情况。当一位下属身体欠佳,就少给他些工作;若刚好另一位下属状态很好,手头工作又不是很多,就多分给他一点。
**那服务调用者节点又该如何判定一个服务节点的处理能力呢?**
这里我们可以采用一种打分的策略服务调用者收集与之建立长连接的每个服务节点的指标数据如服务节点的负载指标、CPU核数、内存大小、请求处理的耗时指标如请求平均耗时、TP99、TP999、服务节点的状态指标如正常、亚健康。通过这些指标计算出一个分数比如总分10分如果CPU负载达到70%就减它3分当然了减3分只是个类比需要减多少分是需要一个计算策略的。
**我们又该如果根据这些指标来打分呢?**
这就有点像公司对员工进行年终考核。假设我是老板我要考核专业能力、沟通能力和工作态度这三项的占比分别是30%、30%、40%我给一个员工的评分是10、8、8那他的综合分数就是这样计算的10*30%+8*30%+8*40%=8.6分。
给服务节点打分也一样,我们可以为每个指标都设置一个指标权重占比,然后再根据这些指标数据,计算分数。
**服务调用者给每个服务节点都打完分之后,会发送请求,那这时候我们又该如何根据分数去控制给每个服务节点发送多少流量呢?**
我们可以配合随机权重的负载均衡策略去控制通过最终的指标分数修改服务节点最终的权重。例如给一个服务节点综合打分是8分满分10分服务节点的权重是100那么计算后最终权重就是80100*80%。服务调用者发送请求时会通过随机权重的策略来选择服务节点那么这个节点接收到的流量就是其他正常节点的80%这里假设其他节点默认权重都是100且指标正常打分为10分的情况
到这儿,一个自适应的负载均衡我们就完成了,整体的设计方案如下图所示:
<img src="https://static001.geekbang.org/resource/image/00/af/00065674063f30c98caaa58bb4cd7baf.jpg" alt="" title="RPC自适应负载均衡示意图">
关键步骤我来解释下:
1. 添加服务指标收集器,并将其作为插件,默认有运行时状态指标收集器、请求耗时指标收集器。
1. 运行时状态指标收集器收集服务节点CPU核数、CPU负载以及内存等指标在服务调用者与服务提供者的心跳数据中获取。
1. 请求耗时指标收集器收集请求耗时数据如平均耗时、TP99、TP999等。
1. 可以配置开启哪些指标收集器,并设置这些参考指标的指标权重,再根据指标数据和指标权重来综合打分。
1. 通过服务节点的综合打分与节点的权重,最终计算出节点的最终权重,之后服务调用者会根据随机权重的策略,来选择服务节点。
## 总结
今天我们详细讲解了RPC框架的负载均衡它与Web服务的负载均衡的不同之处在于RPC框架并不是依赖一个负载均衡设备或者负载均衡服务器来实现负载均衡的而是由RPC框架本身实现的服务调用者可以自主选择服务节点发起服务调用。
这样的好处是RPC框架不再需要依赖专门的负载均衡设备可以节约成本还减少了与负载均衡设备间额外的网络传输提升了传输效率并且均衡策略可配便于服务治理。
除此之外,我们今天的重点还涉及到“如何设计一个自适应的负载均衡”,通过它,我们可以就能根据服务调用者依赖的服务集群中每个节点的自身状态,智能地控制发送给每个服务节点的请求流量,防止因某个服务节点负载过高、请求处理过慢而影响到整个服务集群的可用率。
这个自适应负载均衡的实现方案其实不只是应用于RPC框架中的负载均衡它本身便是一个智能负载的解决方案如果你在工作中需要设计一个智能的负载均衡服务那么完全可以参考。
## 课后思考
你知道RPC框架中还有哪些负载均衡策略吗它们的优缺点是什么期待你能在留言区中分享实现方法与我共同探讨。
也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!

View File

@@ -0,0 +1,89 @@
<audio id="audio" title="12 | 异常重试:在约定时间内安全可靠地重试" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9d/b6/9dd4e93b049776b18c49086bc00f45b6.mp3"></audio>
你好我是何小锋。上一讲我讲解了在RPC框架中如何设计自适应的负载均衡其关键点就是调用端收集服务端每个节点的指标数据再根据各方面的指标数据进行计算打分最后根据每个节点的分数将更多的流量打到分数较高的节点上。
今天我们就继续下一个话题讲讲RPC框架中的异常重试机制。
## 为什么需要异常重试?
我们可以考虑这样一个场景。我们发起一次RPC调用去调用远程的一个服务比如用户的登录操作我们会先对用户的用户名以及密码进行验证验证成功之后会获取用户的基本信息。当我们通过远程的用户服务来获取用户基本信息的时候恰好网络出现了问题比如网络突然抖了一下导致我们的请求失败了而这个请求我们希望它能够尽可能地执行成功那这时我们要怎么做呢
我们需要重新发起一次RPC调用那我们在代码中该如何处理呢是在代码逻辑里catch一下失败了就再发起一次调用吗这样做显然不够优雅吧。这时我们就可以考虑使用RPC框架的重试机制。
## RPC框架的重试机制
那什么是RPC框架的重试机制呢
这其实很好理解就是当调用端发起的请求失败时RPC框架自身可以进行重试再重新发送请求用户可以自行设置是否开启重试以及重试的次数。
那这个机制是如何实现的呢?
<img src="https://static001.geekbang.org/resource/image/32/81/32441dc643e64a022acfcbe0b4c77e81.jpg" alt="" title="RPC异常重试">
还是挺简单的。我们可以回想下[[第 11 讲]](https://time.geekbang.org/column/article/210893)通过这一讲我们了解到调用端在发起RPC调用时会经过负载均衡选择一个节点之后它会向这个节点发送请求信息。当消息发送失败或收到异常消息时我们就可以捕获异常根据异常触发重试重新通过负载均衡选择一个节点发送请求消息并且记录请求的重试次数当重试次数达到用户配置的重试次数的时候就返回给调用端动态代理一个失败异常否则就一直重试下去。
RPC框架的重试机制就是调用端发现请求失败时捕获异常之后触发重试那是不是所有的异常都要触发重试呢
当然不是了,因为这个异常可能是服务提供方抛回来的业务异常,它是应该正常返回给动态代理的,所以我们要在触发重试之前对捕获的异常进行判定,只有符合重试条件的异常才能触发重试,比如网络超时异常、网络连接异常等等。
了解了RPC框架的重试机制那用户在使用异常重试时需要注意哪些问题呢
比如我刚才提的那个调用场景,当网络突然抖动了一下导致请求超时了,但这个时候调用方的请求信息可能已经发送到服务提供方的节点上,也可能已经发送到服务提供方的服务节点上,那如果请求信息成功地发送到了服务节点上,那这个节点是不是就要执行业务逻辑了呢?是的。
那如果这个时候发起了重试,业务逻辑是否会被执行呢?会的。
那如果这个服务业务逻辑不是幂等的,比如插入数据操作,那触发重试的话会不会引发问题呢?会的。
综上我们可以总结出在使用RPC框架的时候我们要确保被调用的服务的业务逻辑是幂等的这样我们才能考虑根据事件情况开启RPC框架的异常重试功能。这一点你要格外注意这算是一个高频误区了。
通过上述讲解我相信你已经非常清楚RPC框架的重试机制了这也是现在大多数RPC框架所采用的重试机制。
那看到这儿你觉得这个机制完善了吗有没有想到连续重试对请求超时时间的影响继续考虑这样一个场景我把调用端的请求超时时间设置为5s结果连续重试3次每次都耗时2s那最终这个请求的耗时是6s那这样的话调用端设置的超时时间是不是就不准确了呢
## 如何在约定时间内安全可靠地重试?
我刚才讲到,连续的异常重试可能会出现一种不可靠的情况,那就是连续的异常重试并且每次处理的请求时间比较长,最终会导致请求处理的时间过长,超出用户设置的超时时间。
解决这个问题最直接的方式就是,在每次重试后都重置一下请求的超时时间。
当调用端发起RPC请求时如果发送请求发生异常并触发了异常重试我们可以先判定下这个请求是否已经超时如果已经超时了就直接返回超时异常否则就先重置下这个请求的超时时间之后再发起重试。
那么解决了因多次异常重试引发的超时时间失效的问题,这个重试机制是不是就完全可靠了呢?
我们接着考虑当调用端设置了异常重试策略发起了一次RPC调用通过负载均衡选择了节点将请求消息发送到这个节点这时这个节点由于负载压力较大导致这个请求处理失败了调用端触发了重试再次通过负载均衡选择了一个节点结果恰好仍选择了这个节点那么在这种情况下重试的效果是否受影响了呢
当然有影响。因此,我们需要在所有发起重试、负载均衡选择节点的时候,去掉重试之前出现过问题的那个节点,以保证重试的成功率。
那我们现在再完整地回顾一下,**考虑了业务逻辑必须是幂等的、超时时间需要重置以及去掉有问题的服务节点后,这样的异常重试机制,还有没有可优化的地方呢?**
我刚才讲过RPC框架的异常重试机制是调用端发送请求之后如果发送失败会捕获异常触发重试但并不是所有的异常都会触发重试的只有RPC框架中特定的异常才会如此比如连接异常、超时异常。
而像服务端业务逻辑中抛回给调用端的异常是不能重试的。那么请你想一下这种情况:服务端的业务逻辑抛给调用端一个异常信息,而服务端抛出这个异常是允许调用端重新发起一次调用的。
比如这个场景:服务端的业务逻辑是对数据库某个数据的更新操作,更新失败则抛出个更新失败的异常,调用端可以再次调用,来触发服务端重新执行更新操作。那这个时候对于调用端来说,它接收到了更新失败异常,虽然是服务端抛回来的业务异常,但也是可以进行重试的。
**那么在这种情况下RPC框架的重试机制需要怎么优化呢**
RPC框架是不会知道哪些业务异常能够去进行异常重试的我们可以加个重试异常的白名单用户可以将允许重试的异常加入到这个白名单中。当调用端发起调用并且配置了异常重试策略捕获到异常之后我们就可以采用这样的异常处理策略。如果这个异常是RPC框架允许重试的异常或者这个异常类型存在于可重试异常的白名单中我们就允许对这个请求进行重试。
所有可能出现的问题,我们排查了一圈下来之后,一个可靠的重试机制就诞生了,如下图所示:
<img src="https://static001.geekbang.org/resource/image/5e/81/5e5706e6fc02ef0caaee565ea358f281.jpg" alt="" title="可靠的异常重试机制">
## 总结
今天我们讲解了RPC框架的重试机制还有如何在约定时间内进行安全可靠地重试。
这个机制是当调用端发起的请求失败时如果配置了异常重试策略RPC框架会捕捉异常对异常进行判定符合条件则进行重试重新发送请求。
在重试的过程中,为了能够在约定的时间内进行安全可靠地重试,在每次触发重试之前,我们需要先判定下这个请求是否已经超时,如果超时了会直接返回超时异常,否则我们需要重置下这个请求的超时时间,防止因多次重试导致这个请求的处理时间超过用户配置的超时时间,从而影响到业务处理的耗时。
在发起重试、负载均衡选择节点的时候我们应该去掉重试之前出现过问题的那个节点这样可以提高重试的成功率并且我们允许用户配置可重试异常的白名单这样可以让RPC框架的异常重试功能变得更加友好。
另外在使用RPC框架的重试机制时我们要确保被调用的服务的业务逻辑是幂等的这样才能考虑是否使用重试这一点至关重要。
## 课后思考
请你思考一下在整个RPC调用的流程中异常重试发生在哪个环节
欢迎留言分享你的答案,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!

View File

@@ -0,0 +1,76 @@
<audio id="audio" title="13 | 优雅关闭:如何避免服务停机带来的业务损失?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/98/4e/987227084fb64266aa99330f1fd0c84e.mp3"></audio>
你好,我是何小锋。上一讲我们讲了“异常重试”,总结来说,异常重试就是为了尽最大可能保证接口可用率的一种手段,但这种策略只能用在幂等接口上,否则就会因为重试导致应用系统数据“写花”。
接着昨天的内容今天我们再来聊聊RPC中的关闭流程。
## 关闭为什么有问题?
我们知道在“单体应用”复杂到一定程度后我们一般会进行系统拆分也就是时下流行的微服务架构。服务拆分之后自然就需要协同于是RPC框架就出来了它用来解决各个子系统之间的通信问题。
我再倒回来问你一个非常基础的问题?你觉得系统为啥非要拆分呢?从我的角度,如果只说一个原因,我觉得拆分之后我们可以更方便、更快速地迭代业务。那么问题来了,更快速地迭代业务,说人话不就是我会经常更新应用系统,时不时还老要重启服务器吗?
那具体到我们的RPC体系里你就要考虑在重启服务的过程中RPC怎么做到让调用方系统不出问题呢
要想说明白这事,我们先要简述下上线的大概流程:当服务提供方要上线的时候,一般是通过部署系统完成实例重启。在这个过程中,服务提供方的团队并不会事先告诉调用方他们需要操作哪些机器,从而让调用方去事先切走流量。而对调用方来说,它也无法预测到服务提供方要对哪些机器重启上线,因此负载均衡就有可能把要正在重启的机器选出来,这样就会导致把请求发送到正在重启中的机器里面,从而导致调用方不能拿到正确的响应结果。
<img src="https://static001.geekbang.org/resource/image/c8/67/c899c36097fd5e3f70bf031f4b2c2167.jpg" alt="">
在服务重启的时候,对于调用方来说,这时候可能会存在以下几种情况:
- 调用方发请求前,目标服务已经下线。对于调用方来说,跟目标节点的连接会断开,这时候调用方可以立马感知到,并且在其健康列表里面会把这个节点挪掉,自然也就不会被负载均衡选中。
- 调用方发请求的时候,目标服务正在关闭,但调用方并不知道它正在关闭,而且两者之间的连接也没断开,所以这个节点还会存在健康列表里面,因此该节点就有一定概率会被负载均衡选中。
## 关闭流程
当然还存在目标服务正在启动的情况如何优雅地启动我会在下一讲详细地讲这也是重点。今天我们要聚焦讨论的就是当出现第二种情况的时候在RPC里面怎么避免调用方业务受损。
这时候你可能会想到,我是不是在重启服务机器前,先通过“某种方式”把要下线的机器从调用方维护的“健康列表”里面删除就可以了,这样负载均衡就选不到这个节点了?你说得一点都没错,但这个具体的“某种方式”是怎么完成呢?
最没有效率的办法就是人工通知调用方,让他们手动摘除要下线的机器,这种方式很原始也很直接。但这样对于提供方上线的过程来说太繁琐了,每次上线都要通知到所有调用我接口的团队,整个过程既浪费时间又没有意义,显然不能被正常接受。
这时候可能你还会想到RPC里面不是有服务发现吗它的作用不就是用来“实时”感知服务提供方的状态吗当服务提供方关闭前是不是可以先通知注册中心进行下线然后通过注册中心告诉调用方进行节点摘除关闭流程如下图所示
<img src="https://static001.geekbang.org/resource/image/a1/50/a15be58b32195422bd5a18dba0e68050.jpg" alt="" title="关闭流程图">
这样不就可以实现不通过“人肉”的方式,从而达到一种自动化方式,但这么做就能完全保证实现无损上下线吗?
如上图所示整个关闭过程中依赖了两次RPC调用一次是服务提供方通知注册中心下线操作一次是注册中心通知服务调用方下线节点操作。注册中心通知服务调用方都是异步的我们在“服务发现”一讲中讲过在大规模集群里面服务发现只保证最终一致性并不保证实时性所以注册中心在收到服务提供方下线的时候并不能成功保证把这次要下线的节点推送到所有的调用方。所以这么来看通过服务发现并不能做到应用无损关闭。
不能强依赖“服务发现”来通知调用方要下线的机器那服务提供方自己来通知行不行因为在RPC里面调用方跟服务提供方之间是长连接我们可以在提供方应用内存里面维护一份调用方连接集合当服务要关闭的时候挨个去通知调用方去下线这台机器。这样整个调用链路就变短了对于每个调用方来说就一次RPC可以确保调用的成功率很高。大部分场景下这么做确实没有问题我们之前也是这么实现的但是我们发现线上还是会偶尔会出现因为服务提供方上线而导致调用失败的问题。
那到底哪里出问题了呢我后面分析了调用方请求日志跟收到关闭通知的日志并且发现了一个线索如下出问题请求的时间点跟收到服务提供方关闭通知的时间点很接近只比关闭通知的时间早不到1ms如果再加上网络传输时间的话那服务提供方收到请求的时候它应该正在处理关闭逻辑。这就说明服务提供方关闭的时候并没有正确处理关闭后接收到的新请求。
## 优雅关闭
知道了根本原因,问题就很好解决了。因为服务提供方已经开始进入关闭流程,那么很多对象就可能已经被销毁了,关闭后再收到的请求按照正常业务请求来处理,肯定是没法保证能处理的。所以我们可以在关闭的时候,设置一个请求“挡板”,挡板的作用就是告诉调用方,我已经开始进入关闭流程了,我不能再处理你这个请求了。
如果大家经常去银行办理业务,就会很熟悉这个流程。在交接班或者有其他要事情处理的时候,银行柜台工作人员会拿出一个纸板,放在窗口前,上面写到“该窗口已关闭”。在该窗口排队的人虽然有一万个不愿意,也只能换到其它窗口办理业务,因为柜台工作人员会把当前正在办理的业务处理完后正式关闭窗口。
基于这个思路我们可以这么处理当服务提供方正在关闭如果这之后还收到了新的业务请求服务提供方直接返回一个特定的异常给调用方比如ShutdownException。这个异常就是告诉调用方“我已经收到这个请求了但是我正在关闭并没有处理这个请求”然后调用方收到这个异常响应后RPC框架把这个节点从健康列表挪出并把请求自动重试到其他节点因为这个请求是没有被服务提供方处理过所以可以安全地重试到其他节点这样就可以实现对业务无损。
但如果只是靠等待被动调用,就会让这个关闭过程整体有点漫长。因为有的调用方那个时刻没有业务请求,就不能及时地通知调用方了,所以我们可以加上主动通知流程,这样既可以保证实时性,也可以避免通知失败的情况。
说到这里,我知道你肯定会问,那要怎么捕获到关闭事件呢?
在我的经验里可以通过捕获操作系统的进程信号来获取在Java语言里面对应的是Runtime.addShutdownHook方法可以注册关闭的钩子。在RPC启动的时候我们提前注册关闭钩子并在里面添加了两个处理程序一个负责开启关闭标识一个负责安全关闭服务对象服务对象在关闭的时候会通知调用方下线节点。同时需要在我们调用链里面加上挡板处理器当新的请求来的时候会判断关闭标识如果正在关闭则抛出特定异常。
看到这里,感觉问题已经比较好地被解决了。但细心的同学可能还会提出问题,关闭过程中已经在处理的请求会不会受到影响呢?
如果进程结束过快会造成这些请求还没有来得及应答,同时调用方会也会抛出异常。为了尽可能地完成正在处理的请求,首先我们要把这些请求识别出来。这就好比日常生活中,我们经常看见停车场指示牌上提示还有多少剩余车位,这个是如何做到的呢?如果仔细观察一下,你就会发现它是每进入一辆车,剩余车位就减一,每出来一辆车,剩余车位就加一。我们也可以利用这个原理在服务对象加上引用计数器,每开始处理请求之前加一,完成请求处理减一,通过该计数器我们就可以快速判断是否有正在处理的请求。
服务对象在关闭过程中会拒绝新的请求同时根据引用计数器等待正在处理的请求全部结束之后才会真正关闭。但考虑到有些业务请求可能处理时间长或者存在被挂住的情况为了避免一直等待造成应用无法正常退出我们可以在整个ShutdownHook里面加上超时时间控制当超过了指定时间没有结束则强制退出应用。超时时间我建议可以设定成10s基本可以确保请求都处理完了。整个流程如下图所示。
<img src="https://static001.geekbang.org/resource/image/77/cc/7752081ec658f1d56ac4219f1c07fbcc.jpg" alt="" title="优雅关闭流程图">
## 总结
在RPC里面关闭虽然看似不属于RPC主流程但如果我们不能处理得很好的话可能就会导致调用方业务异常从而需要我们加入很多额外的运维工作。一个好的关闭流程可以确保使用我们框架的业务实现平滑的上下线而不用担心重启导致的问题。
其实“优雅关闭”这个概念除了在RPC里面有在很多框架里面也都挺常见的比如像我们经常用的应用容器框架Tomcat。Tomcat关闭的时候也是先从外层到里层逐层进行关闭先保证不接收新请求然后再处理关闭前收到的请求。
## 课后思考
今天我只讲了优雅关闭,其实应用重启上下线的时候,还涉及到应用启动流程。那么如何做到优雅启动,避免请求分发到没有就绪的服务节点呢?请你先自行考虑一下,我会在下一讲给你细说。
当然,也欢迎你留言先和我分享你的思考和疑惑,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!

View File

@@ -0,0 +1,75 @@
<audio id="audio" title="14 | 优雅启动:如何避免流量打到没有启动完成的节点?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/75/65/75f9071ca3e9711be018614db7208165.mp3"></audio>
你好,我是何小锋。上一讲我们介绍了优雅停机,就是为了让服务提供方在停机应用的时候,保证所有调用方都能“安全”地切走流量,不再调用自己,从而做到对业务无损。其中实现的关键点就在于,让正在停机的服务提供方应用有状态,让调用方感知到服务提供方正在停机。
接着上一讲的内容,今天我们来聊聊优雅启动。
是不是很诧异?应用启动居然也要这么“讲究”吗?这就好比我们日常生活中的热车,行驶之前让发动机空跑一会,可以让汽车的各个部件都“热”起来,减小磨损。
换到应用上来看原理也是一样的。运行了一段时间后的应用执行速度会比刚启动的应用更快。这是因为在Java里面在运行过程中JVM虚拟机会把高频的代码编译成机器码被加载过的类也会被缓存到JVM缓存中再次使用的时候不会触发临时加载这样就使得“热点”代码的执行不用每次都通过解释从而提升执行速度。
但是这些“临时数据”,都在我们应用重启后就消失了。重启后的这些“红利”没有了之后,如果让我们刚启动的应用就承担像停机前一样的流量,这会使应用在启动之初就处于高负载状态,从而导致调用方过来的请求可能出现大面积超时,进而对线上业务产生损害行为。
在上一讲我们说过,在微服务架构里面,上线肯定是频繁发生的,那我们总不能因为上线,就让过来的请求出现大面积超时吧?所以我们得想点办法。既然问题的关键是在于“刚重启的服务提供方因为没有预跑就承担了大流量”,那我们是不是可以通过某些方法,让应用一开始只接少许流量呢?这样低功率运行一段时间后,再逐渐提升至最佳状态。
这其实就是我今天要和你分享的重点RPC里面的一个实用功能——启动预热。
## 启动预热
那什么叫启动预热呢?
简单来说,就是让刚启动的服务提供方应用不承担全部的流量,而是让它被调用的次数随着时间的移动慢慢增加,最终让流量缓和地增加到跟已经运行一段时间后的水平一样。
**那在RPC里面我们该怎么实现这个功能呢**
我们现在是要控制调用方发送到服务提供方的流量。我们可以先简单地回顾下调用方发起的RPC调用流程是怎样的调用方应用通过服务发现能够获取到服务提供方的IP地址然后每次发送请求前都需要通过负载均衡算法从连接池中选择一个可用连接。那这样的话我们是不是就可以让负载均衡在选择连接的时候区分一下是否是刚启动不久的应用对于刚启动的应用我们可以让它被选择到的概率特别低但这个概率会随着时间的推移慢慢变大从而实现一个动态增加流量的过程。
**现在方案有了,我们就可以考虑具体实现了。**
首先对于调用方来说我们要知道服务提供方启动的时间这个怎么获取呢我这里给出两种方法一种是服务提供方在启动的时候把自己启动的时间告诉注册中心另外一种就是注册中心收到的服务提供方的请求注册时间。这两个时间我认为都可以不过可能你会犹豫我们该怎么确保所有机器的日期时间是一样的这其实不用太关心因为整个预热过程的时间是一个粗略值即使机器之间的日期时间存在1分钟的误差也不影响并且在真实环境中机器都会默认开启NTP时间同步功能来保证所有机器时间的一致性。
不管你是选择哪个时间最终的结果就是调用方通过服务发现除了可以拿到IP列表还可以拿到对应的启动时间。我们需要把这个时间作用在负载均衡上在[[第 11 讲]](https://time.geekbang.org/column/article/210893) 我们介绍过一种基于权重的负载均衡,但是这个权重是由服务提供方设置的,属于一个固定状态。现在我们要让这个权重变成动态的,并且是随着时间的推移慢慢增加到服务提供方设定的固定值,整个过程如下图所示:
<img src="https://static001.geekbang.org/resource/image/e7/d4/e796da8cf26f056479a59fd97b43d0d4.jpg" alt="" title="预热过程图">
通过这个小逻辑的改动,我们就可以保证当服务提供方运行时长小于预热时间时,对服务提供方进行降权,减少被负载均衡选择的概率,避免让应用在启动之初就处于高负载状态,从而实现服务提供方在启动后有一个预热的过程。
看到这儿,你可能还会有另外一个疑问,就是当我在大批量重启服务提供方的时候,会不会导致没有重启的机器因为扛的流量太大而出现问题?
关于这个问题,我是这么考虑的。当你大批量重启服务提供方的时候,对于调用方来说,这些刚重启的机器权重基本是一样的,也就是说这些机器被选中的概率是一样的,大家都是一样得低,也就不存在权重区分的问题了。但是对于那些没有重启过的应用提供方来说,它们被负载均衡选中的概率是相对较高的,但是我们可以通过[[第 11 讲]](https://time.geekbang.org/column/article/210893) 学到的自适应负载的方法平缓地切换,所以也是没有问题的。
启动预热更多是从调用方的角度出发,去解决服务提供方应用冷启动的问题,让调用方的请求量通过一个时间窗口过渡,慢慢达到一个正常水平,从而实现平滑上线。但对于服务提供方本身来说,有没有相关方案可以实现这种效果呢?
当然有,这也是我今天要分享的另一个重点,和热启动息息相关,那就是延迟暴露。
## 延迟暴露
我们应用启动的时候都是通过main入口然后顺序加载各种相关依赖的类。以Spring应用启动为例在加载的过程中Spring容器会顺序加载Spring Bean如果某个Bean是RPC服务的话我们不光要把它注册到Spring-BeanFactory里面去还要把这个Bean对应的接口注册到注册中心。注册中心在收到新上线的服务提供方地址的时候会把这个地址推送到调用方应用内存中当调用方收到这个服务提供方地址的时候就会去建立连接发请求。
但这时候是不是存在服务提供方可能并没有启动完成的情况因为服务提供方应用可能还在加载其它的Bean。对于调用方来说只要获取到了服务提供方的IP就有可能发起RPC调用但如果这时候服务提供方没有启动完成的话就会导致调用失败从而使业务受损。
**那有什么办法可以避免这种情况吗?**
在解决问题前我们先看下出现上述问题的根本原因。这是因为服务提供方应用在没有启动完成的时候调用方的请求就过来了而调用方请求过来的原因是服务提供方应用在启动过程中把解析到的RPC服务注册到了注册中心这就导致在后续加载没有完成的情况下服务提供方的地址就被服务调用方感知到了。
这样的话其实我们就可以把接口注册到注册中心的时间挪到应用启动完成后。具体的做法就是在应用启动加载、解析Bean的时候如果遇到了RPC服务的Bean只先把这个Bean注册到Spring-BeanFactory里面去而并不把这个Bean对应的接口注册到注册中心只有等应用启动完成后才把接口注册到注册中心用于服务发现从而实现让服务调用方延迟获取到服务提供方地址。
这样是可以保证应用在启动完后才开始接入流量的但其实这样做我们还是没有实现最开始的目标。因为这时候应用虽然启动完成了但并没有执行相关的业务代码所以JVM内存里面还是冷的。如果这时候大量请求过来还是会导致整个应用在高负载模式下运行从而导致不能及时地返回请求结果。而且在实际业务中一个服务的内部业务逻辑一般会依赖其它资源的比如缓存数据。如果我们能在服务正式提供服务前先完成缓存的初始化操作而不是等请求来了之后才去加载我们就可以降低重启后第一次请求出错的概率。
**那具体怎么实现呢?**
我们还是需要利用服务提供方把接口注册到注册中心的那段时间。我们可以在服务提供方应用启动后接口注册到注册中心前预留一个Hook过程让用户可以实现可扩展的Hook逻辑。用户可以在Hook里面模拟调用逻辑从而使JVM指令能够预热起来并且用户也可以在Hook里面事先预加载一些资源只有等所有的资源都加载完成后最后才把接口注册到注册中心。整个应用启动过程如下图所示
<img src="https://static001.geekbang.org/resource/image/3c/bd/3c84f9cf6745f2d50e34bd8431c84abd.jpg" alt="" title="启动顺序图">
## 总结
包括[[第 11 讲]](https://time.geekbang.org/column/article/210893) 在内到今天为止我们就已经把整个RPC里面的启停机流程都讲完了。就像前面说过的那样虽然启停机流程看起来不属于RPC主流程但是如果你能在RPC里面把这些“微小”的工作做好就可以让你的技术团队感受到更多的微服务带来的好处。
另外我们今天的两大重点——启动预热与延迟暴露它们并不是RPC的专属功能我们在开发其它系统时也可以利用这两点来减少冷启动对业务的影响。
## 课后思考
在启动预热那部分,我们特意提到过一个问题,就是“当大批量重启服务提供方的时候,会导致请求大概率发到没有重启的机器上,这时服务提供方有可能扛不住”,不知道你是怎么看待这个问题的,是否有好的解决方案呢?
欢迎留言和我分享你的思考,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!

View File

@@ -0,0 +1,91 @@
<audio id="audio" title="15 | 熔断限流:业务如何实现自我保护?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3b/88/3b6f784dc25ef9e964e6b4ecc3b09888.mp3"></audio>
你好我是何小锋。上一讲我讲解了RPC框架中的优雅启动这块的重点就是启动预热与延迟暴露。今天我们换一个新的话题看看在使用RPC时业务是如何实现自我保护的。
## 为什么需要自我保护?
我在[开篇词](https://time.geekbang.org/column/article/199649)中说过RPC是解决分布式系统通信问题的一大利器而分布式系统的一大特点就是高并发所以说RPC也会面临高并发的场景。在这样的情况下我们提供服务的每个服务节点就都可能由于访问量过大而引起一系列的问题比如业务处理耗时过长、CPU飘高、频繁Full GC以及服务进程直接宕机等等。但是在生产环境中我们要保证服务的稳定性和高可用性这时我们就需要业务进行自我保护从而保证在高访问量、高并发的场景下应用系统依然稳定服务依然高可用。
**那么在使用RPC时业务又如何实现自我保护呢**
最常见的方式就是限流了简单有效但RPC框架的自我保护方式可不只有限流并且RPC框架的限流方式可以是多种多样的。
我们可以将RPC框架拆开来分析RPC调用包括服务端和调用端调用端向服务端发起调用。下面我就分享一下服务端与调用端分别是如何进行自我保护的。
## 服务端的自我保护
我们先看服务端举个例子假如我们要发布一个RPC服务作为服务端接收调用端发送过来的请求这时服务端的某个节点负载压力过高了我们该如何保护这个节点
<img src="https://static001.geekbang.org/resource/image/9b/17/9bae10ba8a5b96b03102fb9ef4f30e17.jpg" alt="" title="示意图">
这个问题还是很好解决的,既然负载压力高,那就不让它再接收太多的请求就好了,等接收和处理的请求数量下来后,这个节点的负载压力自然就下来了。
那么就是限流吧是的在RPC调用中服务端的自我保护策略就是限流那你有没有想过我们是如何实现限流的呢是在服务端的业务逻辑中做限流吗有没有更优雅的方式
限流是一个比较通用的功能我们可以在RPC框架中集成限流的功能让使用方自己去配置限流阈值我们还可以在服务端添加限流逻辑当调用端发送请求过来时服务端在执行业务逻辑之前先执行限流逻辑如果发现访问量过大并且超出了限流的阈值就让服务端直接抛回给调用端一个限流异常否则就执行正常的业务逻辑。
<img src="https://static001.geekbang.org/resource/image/f8/ad/f8e8a4dd16f2fd2af366f810404057ad.jpg" alt="" title="示意图">
**那服务端的限流逻辑又该如何实现呢?**
方式有很多,比如最简单的计数器,还有可以做到平滑限流的滑动窗口、漏斗算法以及令牌桶算法等等。其中令牌桶算法最为常用。上述这几种限流算法我就不一一讲解了,资料很多,不太清楚的话自行查阅下就可以了。
我们可以假设下这样一个场景我发布了一个服务提供给多个应用的调用方去调用这时有一个应用的调用方发送过来的请求流量要比其它的应用大很多这时我们就应该对这个应用下的调用端发送过来的请求流量进行限流。所以说我们在做限流的时候要考虑应用级别的维度甚至是IP级别的维度这样做不仅可以让我们对一个应用下的调用端发送过来的请求流量做限流还可以对一个IP发送过来的请求流量做限流。
这时你可能会想使用方该如何配置应用维度以及IP维度的限流呢在代码中配置是不是不大方便我之前说过RPC框架真正强大的地方在于它的治理功能而治理功能大多都需要依赖一个注册中心或者配置中心我们可以通过RPC治理的管理端进行配置再通过注册中心或者配置中心将限流阈值的配置下发到服务提供方的每个节点上实现动态配置。
看到这儿你有没有发现在服务端实现限流配置的限流阈值是作用在每个服务节点上的。比如说我配置的阈值是每秒1000次请求那么就是指一台机器每秒处理1000次请求如果我的服务集群拥有10个服务节点那么我提供的服务限流阈值在最理想的情况下就是每秒10000次。
接着看这样一个场景我提供了一个服务而这个服务的业务逻辑依赖的是MySQL数据库由于MySQL数据库的性能限制我们是需要对其进行保护。假如在MySQL处理业务逻辑中SQL语句的能力是每秒10000次那么我们提供的服务处理的访问量就不能超过每秒10000次而我们的服务有10个节点这时我们配置的限流阈值应该是每秒1000次。那如果之后因为某种需求我们对这个服务扩容了呢扩容到20个节点我们是不是就要把限流阈值调整到每秒500次呢这样操作每次都要自己去计算重新配置显然太麻烦了。
我们可以让RPC框架自己去计算当注册中心或配置中心将限流阈值配置下发的时候我们可以将总服务节点数也下发给服务节点之后由服务节点自己计算限流阈值这样就解决问题了吧
解决了一部分还有一个问题存在那就是在实际情况下一个服务节点所接收到的访问量并不是绝对均匀的比如有20个节点而每个节点限流的阈值是500其中有的节点访问量已经达到阈值了但有的节点可能在这一秒内的访问量是450这时调用端发送过来的总调用量还没有达到10000次但可能也会被限流这样是不是就不精确了那有没有比较精确的限流方式呢
我刚才讲解的限流方式之所以不精确,是因为限流逻辑是服务集群下的每个节点独立去执行的,是一种单机的限流方式,而且每个服务节点所接收到的流量并不是绝对均匀的。
我们可以提供一个专门的限流服务,让每个节点都依赖一个限流服务,当请求流量打过来时,服务节点触发限流逻辑,调用这个限流服务来判断是否到达了限流阈值。我们甚至可以将限流逻辑放在调用端,调用端在发出请求时先触发限流逻辑,调用限流服务,如果请求量已经到达了限流阈值,请求都不需要发出去,直接返回给动态代理一个限流异常即可。
这种限流方式可以让整个服务集群的限流变得更加精确,但也由于依赖了一个限流服务,它在性能和耗时上与单机的限流方式相比是有很大劣势的。至于要选择哪种限流方式,就要结合具体的应用场景进行选择了。
## 调用端的自我保护
刚才我讲解了服务端如何进行自我保护,最简单有效的方式就是限流。那么调用端呢?调用端是否需要自我保护呢?
举个例子假如我要发布一个服务B而服务B又依赖服务C当一个服务A来调用服务B时服务B的业务逻辑调用服务C而这时服务C响应超时了由于服务B依赖服务CC超时直接导致B的业务逻辑一直等待而这个时候服务A在频繁地调用服务B服务B就可能会因为堆积大量的请求而导致服务宕机。
<img src="https://static001.geekbang.org/resource/image/dc/31/dc2a18f1e2c495380cc4053b92ed3131.jpg" alt="" title="服务异常示意图">
由此可见服务B调用服务C服务C执行业务逻辑出现异常时会影响到服务B甚至可能会引起服务B宕机。这还只是A-&gt;B-&gt;C的情况试想一下A-&gt;B-&gt;C-&gt;D-&gt;……呢?在整个调用链中,只要中间有一个服务出现问题,都可能会引起上游的所有服务出现一系列的问题,甚至会引起整个调用链的服务都宕机,这是非常恐怖的。
所以说,在一个服务作为调用端调用另外一个服务时,为了防止被调用的服务出现问题而影响到作为调用端的这个服务,这个服务也需要进行自我保护。**而最有效的自我保护方式就是熔断。**
<img src="https://static001.geekbang.org/resource/image/90/64/903fa4374beb753c1db8f1f8b82ff464.jpg" alt="" title="示意图">
我们可以先了解下熔断机制。
熔断器的工作机制主要是关闭、打开和半打开这三个状态之间的切换。在正常情况下,熔断器是关闭的;当调用端调用下游服务出现异常时,熔断器会收集异常指标信息进行计算,当达到熔断条件时熔断器打开,这时调用端再发起请求是会直接被熔断器拦截,并快速地执行失败逻辑;当熔断器打开一段时间后,会转为半打开状态,这时熔断器允许调用端发送一个请求给服务端,如果这次请求能够正常地得到服务端的响应,则将状态置为关闭状态,否则设置为打开。
了解完熔断机制,你就会发现,在业务逻辑中加入熔断器其实是不够优雅的。**那么在RPC框架中我们该如何整合熔断器呢**
熔断机制主要是保护调用端调用端在发出请求的时候会先经过熔断器。我们可以回想下RPC的调用流程
<img src="https://static001.geekbang.org/resource/image/59/87/59b7479220a415ef034fb6edb589ec87.jpg" alt="" title="RPC调用流程">
你看图的话,有没有想到在哪个步骤整合熔断器会比较合适呢?
我的建议是动态代理因为在RPC调用的流程中动态代理是RPC调用的第一个关口。在发出请求时先经过熔断器如果状态是闭合则正常发出请求如果状态是打开则执行熔断器的失败策略。
## 总结
今天我们主要讲解了RPC框架是如何实现业务的自我保护。
服务端主要是通过限流来进行自我保护我们在实现限流时要考虑到应用和IP级别方便我们在服务治理的时候对部分访问量特别大的应用进行合理的限流服务端的限流阈值配置都是作用于单机的而在有些场景下例如对整个服务设置限流阈值服务进行扩容时限流的配置并不方便我们可以在注册中心或配置中心下发限流阈值配置的时候将总服务节点数也下发给服务节点让RPC框架自己去计算限流阈值我们还可以让RPC框架的限流模块依赖一个专门的限流服务对服务设置限流阈值进行精准地控制但是这种方式依赖了限流服务相比单机的限流方式在性能和耗时上有劣势。
调用端可以通过熔断机制进行自我保护防止调用下游服务出现异常或者耗时过长影响调用端的业务逻辑RPC框架可以在动态代理的逻辑中去整合熔断器实现RPC框架的熔断功能。
## 课后思考
在使用RPC的过程中业务要实现自我保护针对这个问题你是否还有其他的解决方案
欢迎留言分享你的思考,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!

View File

@@ -0,0 +1,65 @@
<audio id="audio" title="16 | 业务分组:如何隔离流量?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e7/21/e7d45a7b123ddd092ec6f4a29abe7921.mp3"></audio>
你好我是何小锋。上一讲我们介绍了RPC中常用的保护手段“熔断限流”熔断是调用方为了避免在调用过程中服务提供方出现问题的时候自身资源被耗尽的一种保护行为而限流则是服务提供方为防止自己被突发流量打垮的一种保护行为。虽然这两种手段作用的对象不同但出发点都是为了实现自我保护所以一旦发生这种行为业务都是有损的。
那说起突发流量限流固然是一种手段但其实面对复杂的业务以及高并发场景时我们还有别的手段可以最大限度地保障业务无损那就是隔离流量。这也是我今天重点要和你分享的内容接下来我们就一起看看分组在RPC中的应用。
## 为什么需要分组?
在我们的日常开发中,我们不都提倡让用户使用起来越简单越好吗?如果在接口上再加一个分组维度去管理,不就让事情变复杂了吗?
实则不然,举个例子。在没有汽车的年代,我们的道路很简单,就一条,行人、洋车都在上边走。那随着汽车的普及以及猛增,我们的道路越来越宽,慢慢地有了高速、辅路、人行道等等。很显然,交通网的建设与完善不仅提高了我们的出行效率,而且还更好地保障了我们行人的安全。
同样的道理我们用在RPC治理上也是一样的。假设你是一个服务提供方应用的负责人在早期业务量不大的情况下应用之间的调用关系并不会复杂请求量也不会很大我们的应用有足够的能力扛住日常的所有流量。我们并不需要花太多的时间去治理调用请求过来的流量我们通常会选择最简单的方法就是把服务实例统一管理把所有的请求都用一个共享的“大池子”来处理。这就类似于“简单道路时期”服务调用方跟服务提供方之间的调用拓扑如下图所示
<img src="https://static001.geekbang.org/resource/image/e8/85/e814da49a9c35b5e71df58b870234285.jpg" alt="" title="无隔离调用拓扑">
后期因为业务发展丰富了,调用你接口的调用方就会越来越多,流量也会渐渐多起来。可能某一天,一个“爆炸式惊喜”就来了。其中一个调用方的流量突然激增,让你整个集群瞬间处于高负载运行,进而影响到其它调用方,导致它们的整体可用率下降。而这时候作为应用负责人的你,那就得变身“救火队长”了,要想尽各种办法来保证应用的稳定。
在经过一系列的救火操作后,我们肯定要去想更好的应对办法。那回到问题的根本去看,关键就在于,早期为了管理方便,我们把接口都放到了同一个分组下面,所有的服务实例是以一个整体对外提供能力的。
但后期因为业务发展这种粗暴的管理模式已经不适用了这就好比“汽车来了我们的交通网也得抓紧建设”一样让人车分流。此时道路上的人和车就好比我们应用的调用方我们可以尝试把应用提供方这个大池子划分出不同规格的小池子再分配给不同的调用方而不同小池子之间的隔离带就是我们在RPC里面所说的分组它可以实现流量隔离。
## 怎么实现分组?
现在分组是怎么回事我们搞清楚了那放到RPC里我们该怎么实现呢
既然是要求不同的调用方应用能拿到的池子内容不同那我们就要回想下服务发现了因为在RPC流程里能影响到调用方获取服务节点的逻辑就是它了。
在[[第 08 讲]](https://time.geekbang.org/column/article/208171) 我们说过,服务调用方是通过接口名去注册中心找到所有的服务节点来完成服务发现的,那换到这里的话,这样做其实并不合适,因为这样调用方会拿到所有的服务节点。因此为了实现分组隔离逻辑,我们需要重新改造下服务发现的逻辑,调用方去获取服务节点的时候除了要带着接口名,还需要另外加一个分组参数,相应的服务提供方在注册的时候也要带上分组参数。
通过改造后的分组逻辑,我们可以把服务提供方所有的实例分成若干组,每一个分组可以提供给单个或者多个不同的调用方来调用。那怎么分组好呢,有没有统一的标准?
坦白讲,这个分组并没有一个可衡量的标准,但我自己总结了一个规则可以供你参考,就是按照应用重要级别划分。
非核心应用不要跟核心应用分在同一个组,核心应用之间应该做好隔离,一个重要的原则就是保障核心应用不受影响。比如提供给电商下单过程中用的商品信息接口,我们肯定是需要独立出一个单独分组,避免受其它调用方污染的。有了分组之后,我们的服务调用方跟服务提供方之间的调用拓扑就如下图所示:
<img src="https://static001.geekbang.org/resource/image/12/69/128923fefc27a36d056393f9e9f25f69.jpg" alt="" title="分组调用拓扑">
通过分组的方式隔离调用方的流量,从而避免因为一个调用方出现流量激增而影响其它调用方的可用率。对服务提供方来说,这种方式是我们日常治理服务过程中一个高频使用的手段,那通过这种分组进行流量隔离,对调用方应用会不会有影响呢?
## 如何实现高可用?
分组隔离后单个调用方在发RPC请求的时候可选择的服务节点数相比没有分组前减少了那对于单个调用方来说出错的概率就升高了。比如一个集中交换机设备突然坏了而这个调用方的所有服务节点都在这个交换机下面在这种情况下对于服务调用方来说它的请求无论如何也到达不了服务提供方从而导致这个调用方业务受损。
那有没有更高可用一点的方案呢?回到我们前面说的那个马路例子上,正常情况下我们是必须让车在车道行驶,人在人行道上行走。但当人行道或者车道出现抢修的时候,在条件允许的情况下,我们一般都是允许对方借道行驶一段时间,直到道路完全恢复。
**我们同样可以把这个特性用到我们的RPC中要怎么实现呢**
在前面我们也说了,调用方应用服务发现的时候,除了带上对应的接口名,还需要带上一个特定分组名,所以对于调用方来说,它是拿不到其它分组的服务节点的,那这样的话调用方就没法建立起连接发请求了。
因此问题的核心就变成了调用方要拿到其它分组的服务节点,但是又不能拿到所有的服务节点,否则分组就没有意义了。一个最简单的办法就是,允许调用方可以配置多个分组。但这样的话,这些节点对于调用方来说就都是一样的了,调用方可以随意选择获取到的所有节点发送请求,这样就又失去了分组隔离的意义,并且还没有实现我们想要的“借道”的效果。
所以我们还需要把配置的分组区分下主次分组,只有在主分组上的节点都不可用的情况下才去选择次分组节点;只要主分组里面的节点恢复正常,我们就必须把流量都切换到主节点上,整个切换过程对于应用层完全透明,从而在一定程度上保障调用方应用的高可用。
## 总结
今天我们通过一个道路划分的案例引出了在RPC里面我们可以通过分组的方式人为地给不同的调用方划分出不同的小集群从而实现调用方流量隔离的效果保障我们的核心业务不受非核心业务的干扰。但我们在考虑问题的时候不能顾此失彼不能因为新加一个的功能而影响到原有系统的稳定性。
其实我们不仅可以通过分组把服务提供方划分成不同规模的小集群,我们还可以利用分组完成一个接口多种实现的功能。正常情况下,为了方便我们自己管理服务,我一般都会建议每个接口完成的功能尽量保证唯一。但在有些特殊场景下,两个接口也会完全一样,只是具体实现上有那么一点不同,那么我们就可以在服务提供方应用里面同时暴露两个相同接口,但只是接口分组不一样罢了。
## 课后思考
在我们的实际工作中,测试人员和开发人员的工作一般都是并行的,这就导致一个问题经常出现:开发人员在开发过程中可能需要启动自身的应用,而测试人员为了能验证功能,会在测试环境中部署同样的应用。如果开发人员和测试人员用的接口分组名刚好一样,在这种情况下,就可能会干扰其它正在联调的调用方进行功能验证,进而影响整体的工作效率。不知道面对这种情况,你有什么好办法吗?
欢迎留言和我分享你的思考,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!

View File

@@ -0,0 +1,69 @@
<audio id="audio" title="答疑课堂 | 基础篇与进阶篇思考题答案合集" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7c/ad/7cf467e1e8eafac923a4a4c4872ba5ad.mp3"></audio>
你好,我是何小锋。到今天为止,基础篇和进阶篇我们就都学习完了,在这个过程中我一直在看大家的留言,知道你可能还有很多地方存在着疑问,今天这一讲我整理了一些关注度比较高的课后思考题答案,希望能给你带来帮助。
## [第二讲](https://time.geekbang.org/column/article/199651)
**思考题:**在RPC里面我们是怎么实现请求跟响应关联的
首先我们要弄清楚为什么要把请求与响应关联。这是因为在RPC调用过程中调用端会向服务端发送请求消息之后它还会收到服务端发送回来的响应消息但这两个操作并不是同步进行的。在高并发的情况下调用端可能会在某一时刻向服务端连续发送很多条消息之后才会陆续收到服务端发送回来的各个响应消息这时调用端需要一种手段来区分这些响应消息分别对应的是之前的哪条请求消息所以我们说RPC在发送消息时要请求跟响应关联。
解决这个问题不难只要调用端在收到响应消息之后从响应消息中读取到一个标识告诉调用端这是哪条请求消息的响应消息就可以了。在这一讲中你会发现我们设计的私有协议都会有消息ID这个消息ID的作用就是起到请求跟响应关联的作用。调用端为每一个消息生成一个唯一的消息ID它收到服务端发送回来的响应消息如果是同一消息ID那么调用端就可以认为这条响应消息是之前那条请求消息的响应消息。
## [第五讲](https://time.geekbang.org/column/article/205910)
**思考题:**如果没有动态代理帮我们完成方法调用拦截用户该怎么完成RPC调用
这个问题我们可以参考下gRPC框架。gRPC框架中就没有使用动态代理它是通过代码生成的方式生成Service存根当然这个Service存根起到的作用和RPC框架中的动态代理是一样的。
gRPC框架用代码生成的Service存根来代替动态代理主要是为了实现多语言的客户端因为有些语言是不支持动态代理的比如C++、go等但缺点也是显而易见的。如果你使用过gRPC你会发现这种代码生成Service存根的方式与动态代理相比还是很麻烦的并不如动态代理的方式使用起来方便、透明。
## [第六讲](https://time.geekbang.org/column/article/206281)
**思考题:**在 gRPC 调用的时候,我们有一个关键步骤就是把对象转成可传输的二进制,但是在 gRPC 里面,我们并没有直接转成二进制数组,而是返回一个 InputStream你知道这样做的好处是什么吗
RPC调用在底层传输过程中也是需要使用Stream的直接返回一个InputStream而不是二进制数组可以避免数据的拷贝。
## [第八讲](https://time.geekbang.org/column/article/208171)
**思考题:**目前服务提供者上线后会自动注册到注册中心,服务调用方会自动感知到新增的实例,并且流量会很快打到该新增的实例。如果我想把某些服务提供者实例的流量切走,除了下线实例,你有没有想到其它更便捷的办法呢?
解决这个问题的方法还是有很多的比如留言中提到的改变服务提供者实例的权重将权重调整为0或者通过路由的方式也可以。
但解决这个问题最便捷的方式还是使用动态分组,在[[第 16 讲]](https://time.geekbang.org/column/article/215668) 中我讲解了业务分组的概念,通过业务分组来实现流量隔离。如果业务分组是动态的,我们就可以在管理平台动态地自由调整,那是不是就可以实现动态地流量切换了呢?这个问题我们还会在高级篇中详解,期待一下。
## [第十二讲](https://time.geekbang.org/column/article/211261)
**思考题:**在整个RPC调用的流程中异常重试发生在哪个环节
在回答这个问题之前我们先回想下这一讲中讲过的内容。我在讲RPC为什么需要异常重试时我说过如果在发出请求时恰好网络出现问题了导致我们的请求失败我们可能需要进行异常重试。从这一点我们可以看出异常重试的操作是要在调用端进行的。因为如果在调用端发出请求时恰好网络出现问题导致请求失败那么这个请求很可能还没到达服务端服务端当然就没办法去处理重试了。
另外,我还讲过,我们需要在所有发起重试、负载均衡选择节点的时候,去掉重试之前出现过问题的那个节点,以保证重试的成功率。由此可见异常重试的操作应该发生在负载均衡之前,在发起重试的时候,会调用负载均衡插件来选择一个服务节点,在调用负载均衡插件时我们要告诉负载均衡需要刨除哪些有问题的服务节点。
在整个RPC调用的过程中从动态代理到负载均衡之间还有一系列的操作如果你研究过开源的RPC框架你会发现在调用端发送请求消息之前还会经过过滤链对请求消息进行层层的过滤处理之后才会通过负载均衡选择服务节点发送请求消息而异常重试操作就发生在过滤链处理之后调用负载均衡选择服务节点之前这样的重试是可以减少很多重复操作的。
## [第十四讲](https://time.geekbang.org/column/article/213967)
**思考题:**在启动预热那部分,我们特意提到过一个问题,就是“当大批量重启服务提供方的时候,会导致请求大概率发到没有重启的机器上,这时服务提供方有可能扛不住”,不知道你是怎么看待这个问题的,是否有好的解决方案呢?
我们可以考虑在非流量高峰的时候重启服务,将影响降到最低;也可以考虑分批次重启,控制好每批重启的服务节点的数量,当一批服务节点的权重与访问量都到正常水平时,再去重启下一批服务节点。
## [第十五讲](https://time.geekbang.org/column/article/213998)
**思考题:**在使用RPC的过程中业务要实现自我保护针对这个问题你是否还有其他的解决方案
通过这一讲我们知道在RPC调用中无论服务端还是调用端都需要自我保护服务端自我保护的最简单有效的方式是“限流”调用端则可以通过“熔断”机制来进行自我保护。
除了“熔断”和“限流”外,相信你一定听过“降级”这个词。简单来说就是当一个服务处理大量的请求达到一定压力的时候,我们可以让这个服务在处理请求时减少些非必要的功能,从而降低这个服务的压力。
还有就是我们可以通过服务治理,降低一个服务节点的权重来减轻某一方服务节点的请求压力,达到保护这个服务节点的目的。
## [第十六讲](https://time.geekbang.org/column/article/215668)
**思考题:**在我们的实际工作中,测试人员和开发人员的工作一般都是并行的,这就导致一个问题经常出现:开发人员在开发过程中可能需要启动自身的应用,而测试人员为了能验证功能,会在测试环境中部署同样的应用。如果开发人员和测试人员用的接口分组名刚好一样,在这种情况下,就可能会干扰其它正在联调的调用方进行功能验证,进而影响整体的工作效率。不知道面对这种情况,你有什么好办法吗?
我们可以考虑配置不同的注册中心开发人员将自己的服务注册到注册中心A上而测试人员可以将自己的服务注册到测试专属的注册中心B上这样测试人员在验证功能的时候调用端会从注册中心B上拉取服务节点开发人员重启自己的服务是影响不到测试人员的。
如果你使用过或者了解k8s的话你一定知道“命名空间”的概念RPC框架如果支持“命名空间”也是可以解决这一问题的。
今天的答疑就到这里,如果你有更多问题,欢迎继续在留言区中告知,我们共同讨论。下节课再见!