This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
<audio id="audio" title="17 | 异步RPC压榨单机吞吐量" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/88/1e/88c25b278ef567f4e7b7c50592f0831e.mp3"></audio>
你好,我是何小锋。从今天开始,我们就正式进入高级篇了。
在上个篇章我们学习了RPC框架的基础架构和一系列治理功能以及一些与集群管理相关的高级功能如服务发现、健康检查、路由策略、负载均衡、优雅启停机等等。
有了这些知识储备你就已经对RPC框架有了较为充分的认识。但如果你想要更深入地了解RPC更好地使用RPC你就必须从RPC框架的整体性能上去考虑问题了。你得知道如何去提升RPC框架的性能、稳定性、安全性、吞吐量以及如何在分布式的场景下快速定位问题等等这些都是我们在高级篇中重点要讲解的内容。难度有一定提升希望你能坚持学习呀
那么今天我们就先来讲讲RPC框架是如何压榨单机吞吐量的。
## 如何提升单机吞吐量?
在我运营RPC的过程中“如何提升吞吐量”是我与业务团队经常讨论的问题。
记得之前业务团队反馈过这样一个问题我们的TPS始终上不去压测的时候CPU压到40%50%就再也压不上去了TPS也不会提高问我们这里有没有什么解决方案可以提升业务的吞吐量
之后我是看了下他们服务的业务逻辑发现他们的业务逻辑在执行较为耗时的业务逻辑的基础上又同步调用了好几个其它的服务。由于这几个服务的耗时较长才导致这个服务的业务逻辑耗时也长CPU大部分的时间都在等待并没有得到充分地利用因此CPU的利用率和服务的吞吐量当然上不去了。
**那是什么影响到了RPC调用的吞吐量呢**
在使用RPC的过程中谈到性能和吞吐量我们的第一反应就是选择一款高性能、高吞吐量的RPC框架那影响到RPC调用的吞吐量的根本原因是什么呢
其实根本原因就是由于处理RPC请求比较耗时并且CPU大部分的时间都在等待而没有去计算从而导致CPU的利用率不够。这就好比一个人在干活但他没有规划好时间并且有很长一段时间都在闲着当然也就完不成太多工作了。
那么导致RPC请求比较耗时的原因主要是在于RPC框架本身吗事实上除非在网络比较慢或者使用方使用不当的情况下否则在大多数情况下刨除业务逻辑处理的耗时时间RPC本身处理请求的效率就算在比较差的情况下也不过是毫秒级的。可以说RPC请求的耗时大部分都是业务耗时比如业务逻辑中有访问数据库执行慢SQL的操作。所以说在大多数情况下影响到RPC调用的吞吐量的原因也就是业务逻辑处理慢了CPU大部分时间都在等待资源。
弄明白了原因,咱们就可以解决问题了,该如何去提升单机吞吐量?
这并不是一个新话题比如现在我们经常提到的响应式开发就是为了能够提升业务处理的吞吐量。要提升吞吐量其实关键就两个字“异步”。我们的RPC框架要做到完全异步化实现全异步RPC。试想一下如果我们每次发送一个异步请求发送请求过后请求即刻就结束了之后业务逻辑全部异步执行结果异步通知这样可以增加多么可观的吞吐量
效果不用我说我想你也清楚了。那RPC框架都有哪些异步策略呢
## 调用端如何异步?
说到异步我们最常用的方式就是返回Future对象的Future方式或者入参为Callback对象的回调方式而Future方式可以说是最简单的一种异步方式了。我们发起一次异步请求并且从请求上下文中拿到一个Future之后我们就可以调用Future的get方法获取结果。
就比如刚才我提到的业务团队的那个问题他们的业务逻辑中调用了好几个其它的服务这时如果是同步调用假设调用了4个服务每个服务耗时10毫秒那么业务逻辑执行完至少要耗时40毫秒。
那如果采用Future方式呢
连续发送4次异步请求并且拿到4个Future由于是异步调用这段时间的耗时几乎可以忽略不计之后我们统一调用这几个Future的get方法。这样一来的话业务逻辑执行完的时间在理想的情况下是多少毫秒呢没错10毫秒耗时整整缩短到了原来的四分之一也就是说我们的吞吐量有可能提升4倍
<img src="https://static001.geekbang.org/resource/image/35/ef/359a5ef2c76c3a9ac84375e970915fef.jpg" alt="" title="示意图">
那RPC框架的Future方式异步又该如何实现呢
通过基础篇的学习我们了解到一次RPC调用的本质就是调用端向服务端发送一条请求消息服务端收到消息后进行处理处理之后响应给调用端一条响应消息调用端收到响应消息之后再进行处理最后将最终的返回值返回给动态代理。
这里我们可以看到对于调用端来说向服务端发送请求消息与接收服务端发送过来的响应消息这两个处理过程是两个完全独立的过程这两个过程甚至在大多数情况下都不在一个线程中进行。那么是不是说RPC框架的调用端对于RPC调用的处理逻辑内部实现就是异步的呢
不错对于RPC框架无论是同步调用还是异步调用调用端的内部实现都是异步的。
通过[[第 02 讲]](https://time.geekbang.org/column/article/199651) 我们知道调用端发送的每条消息都一个唯一的消息标识实际上调用端向服务端发送请求消息之前会先创建一个Future并会存储这个消息标识与这个Future的映射动态代理所获得的返回值最终就是从这个Future中获取的当收到服务端响应的消息时调用端会根据响应消息的唯一标识通过之前存储的映射找到对应的Future将结果注入给那个Future再进行一系列的处理逻辑最后动态代理从Future中获得到正确的返回值。
所谓的同步调用不过是RPC框架在调用端的处理逻辑中主动执行了这个Future的get方法让动态代理等待返回值而异步调用则是RPC框架没有主动执行这个Future的get方法用户可以从请求上下文中得到这个Future自己决定什么时候执行这个Future的get方法。
<img src="https://static001.geekbang.org/resource/image/5d/55/5d6999a1ac6646faa34905539a0fba55.jpg" alt="" title="Future示意图">
现在你应该很清楚RPC框架是如何实现Future方式的异步了。
## 如何做到RPC调用全异步
刚才我讲解了Future方式的异步Future方式异步可以说是调用端异步的一种方式那么服务端呢服务端是否需要异步有什么实现方式
通过基础篇的学习我们了解到RPC服务端接收到请求的二进制消息之后会根据协议进行拆包解包之后将完整的消息进行解码并反序列化获得到入参参数之后再通过反射执行业务逻辑。那你有没有想过在生产环境中这些操作都在哪个线程中执行呢是在一个线程中执行吗
当然不会在一个对二进制消息数据包拆解包的处理是一定要在处理网络IO的线程中如果网络通信框架使用的是Netty框架那么对二进制包的处理是在IO线程中而解码与反序列化的过程也往往在IO线程中处理那服务端的业务逻辑呢也应该在IO线程中处理吗原则上是不应该的业务逻辑应该交给专门的业务线程池处理以防止由于业务逻辑处理得过慢而影响到网络IO的处理。
这时问题就来了我们配置的业务线程池的线程数都是有限制的在我运营RPC的经验中业务线程池的线程数一般只会配置到200因为在大多数情况下线程数配置到200还不够用就说明业务逻辑该优化了。那么如果碰到特殊的业务场景呢让配置的业务线程池完全打满了比如这样一个场景。
我这里启动一个服务业务逻辑处理得就是比较慢当访问量逐渐变大时业务线程池很容易就被打满了吞吐量很不理想并且这时CPU的利用率也很低。
对于这个问题,你有没有想到什么解决办法呢?是不是会马上想到调大业务线程池的线程数?那这样可以吗?有没有更好的解决方式呢?
我想服务端业务处理逻辑异步是个好方法。
调大业务线程池的线程数的确勉强可以解决这个问题但是对于RPC框架来说往往都会有多个服务共用一个线程池的情况即使调大业务线程池比较耗时的服务很可能还会影响到其它的服务。所以最佳的解决办法是能够让业务线程池尽快地释放那么我们就需要RPC框架能够支持服务端业务逻辑异步处理这对提高服务的吞吐量有很重要的意义。
那服务端如何支持业务逻辑异步呢?
这是个比较难处理的问题,因为服务端执行完业务逻辑之后,要对返回值进行序列化并且编码,将消息响应给调用端,但如果是异步处理,业务逻辑触发异步之后方法就执行完了,来不及将真正的结果进行序列化并编码之后响应给调用端。
这时我们就需要RPC框架提供一种回调方式让业务逻辑可以异步处理处理完之后调用RPC框架的回调接口将最终的结果通过回调的方式响应给调用端。
说到服务端支持业务逻辑异步处理结合我刚才讲解的Future方式异步你有没有想到更好的处理方式呢其实我们可以让RPC框架支持CompletableFuture实现RPC调用在调用端与服务端之间完全异步。
CompletableFuture是Java8原生支持的。试想一下假如RPC框架能够支持CompletableFuture我现在发布一个RPC服务服务接口定义的返回值是CompletableFuture对象整个调用过程会分为这样几步
- 服务调用方发起RPC调用直接拿到返回值CompletableFuture对象之后就不需要任何额外的与RPC框架相关的操作了如我刚才讲解Future方式时需要通过请求上下文获取Future的操作直接就可以进行异步处理
- 在服务端的业务逻辑中创建一个返回值CompletableFuture对象之后服务端真正的业务逻辑完全可以在一个线程池中异步处理业务逻辑完成之后再调用这个CompletableFuture对象的complete方法完成异步通知
- 调用端在收到服务端发送过来的响应之后RPC框架再自动地调用调用端拿到的那个返回值CompletableFuture对象的complete方法这样一次异步调用就完成了。
通过对CompletableFuture的支持RPC框架可以真正地做到在调用端与服务端之间完全异步同时提升了调用端与服务端的两端的单机吞吐量并且CompletableFuture是Java8原生支持业务逻辑中没有任何代码入侵性这是不是很酷炫了
## 总结
今天我们主要讲解了如果通过RPC的异步去压榨单机的吞吐量。
影响到RPC调用的吞吐量的主要原因就是服务端的业务逻辑比较耗时并且CPU大部分时间都在等待而没有去计算导致CPU利用率不够而提升单机吞吐量的最好办法就是使用异步RPC。
RPC框架的异步策略主要是调用端异步与服务端异步。调用端的异步就是通过Future方式实现异步调用端发起一次异步请求并且从请求上下文中拿到一个Future之后通过Future的get方法获取结果如果业务逻辑中同时调用多个其它的服务则可以通过Future的方式减少业务逻辑的耗时提升吞吐量。服务端异步则需要一种回调方式让业务逻辑可以异步处理之后调用RPC框架提供的回调接口将最终结果异步通知给调用端。
另外我们可以通过对CompletableFuture的支持实现RPC调用在调用端与服务端之间的完全异步同时提升两端的单机吞吐量。
其实RPC框架也可以有其它的异步策略比如集成RxJava再比如gRPC的StreamObserver入参对象但CompletableFuture是Java8原生提供的无代码入侵性并且在使用上更加方便。如果是Java开发让RPC框架支持CompletableFuture可以说是最佳的异步解决方案。
## 课后思考
对于RPC调用提升吞吐量这个问题你是否还有其它的解决方案你还能想到哪些RPC框架的异步策略
欢迎留言分享你的答案,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!

View File

@@ -0,0 +1,79 @@
<audio id="audio" title="18 | 安全体系:如何建立可靠的安全体系?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/27/f3/27013de07b5abb3b076613e8ee3217f3.mp3"></audio>
你好我是何小锋。上一讲我们学习了在RPC里面该如何提升单机资源的利用率你要记住的关键点就一个那就是“异步化”。调用方利用异步化机制实现并行调用多个服务以缩短整个调用时间而服务提供方则可以利用异步化把业务逻辑放到自定义线程池里面去执行以提升单机的OPS。
回顾完上一讲的重点我们就切入今天的主题一起来看看RPC里面的安全问题。
## 为什么需要考虑安全问题?
说起安全问题你可能会想到像SQL注入、XSS攻击等恶意攻击行为还有就是相对更广义的安全像网络安全、信息安全等那在RPC里面我们说的安全一般指什么呢
我们知道RPC是解决应用间互相通信的框架而应用之间的远程调用过程一般不会暴露在公网换句话讲就是说RPC一般用于解决内部应用之间的通信而这个“内部”是指应用都部署在同一个大局域网内。相对于公网环境局域网的隔离性更好也就相对更安全所以在RPC里面我们很少考虑像数据包篡改、请求伪造等恶意行为。
**那在RPC里面我们应该关心什么样的安全问题呢**要搞清楚这个问题我们可以先看一个完整的RPC应用流程。
我们一般是先由服务提供方定义好一个接口并把这个接口的Jar包发布到私服上去然后在项目中去实现这个接口最后通过RPC提供的API把这个接口和其对应的实现类完成对外暴露如果是Spring应用的话直接定义成一个Bean就好了。到这儿服务提供方就完成了一个接口的对外发布了。
对于服务调用方来说就更简单了只要拿到刚才上传到私服上的Jar的坐标就可以把发布到私服的Jar引入到项目中来然后借助RPC提供的动态代理功能服务调用方直接就可以在项目完成RPC调用了。
这里面其实存在一个安全隐患问题因为私服上所有的Jar坐标我们所有人都可以看到只要拿到了Jar的坐标我们就可以把发布到私服的Jar引入到项目中完成RPC调用了吗
理论上确实是这样,当然我相信在公司内部这种不向服务提供方咨询就直接调用的行为很少发生,而且一般真实业务的接口出入参数都不会太简单,这样不经过咨询只靠调用方自己猜测完成调用的工作效率实在太低了。
虽然这种靠猜测调用的概率很小,但是当调用方在其它新业务场景里面要用之前项目中使用过的接口,就很有可能真的不跟服务提供方打招呼就直接调用了。这种行为对于服务提供方来说就很危险了,因为接入了新的调用方就意味着承担的调用量会变大,有时候很有可能新增加的调用量会成为压倒服务提供方的“最后一根稻草”,从而导致服务提供方无法正常提供服务,关键是服务提供方还不知道是被谁给压倒的。
当然你可能会说,这是一个流程问题,我们只要在公司内部规范好调用流程,就可以避免这种问题发生了。
确实是这样我们可以通过流程宣贯让我们所有的研发人员达成一个“君子约定”就是在应用里面每次要用一个接口的时候必须先向服务提供方进行报备这样确实能在很大程度上避免这种情况的发生。但就RPC本身来说我们是不是可以提供某种功能来解决这种问题呢毕竟对于人数众多的团队来说光靠口头约定的流程并不能彻底杜绝这类问题依然存在隐患且不可控。
## 调用方之间的安全保证
那在RPC里面我们该怎么解决这种问题呢
我们先总结下刚才的问题,根本原因就是服务提供方收到请求后,不知道这次请求是哪个调用方发起的,没法判断这次请求是属于之前打过招呼的调用方还是没有打过招呼的调用方,所以也就没法选择拒绝这次请求还是继续执行。
问题说明白了就好解决了,我们只需要给每个调用方设定一个唯一的身份,每个调用方在调用之前都先来服务提供方这登记下身份,只有登记过的调用方才能继续放行,没有登记过的调用方一律拒绝。
这就好比我们平时坐火车,我们拿着身份证去购买火车票,买票成功就类似服务调用方去服务提供方这儿进行登记。当你进站准备上火车的时候,你必须同时出示你的身份证和火车票,这两个就是代表你能上这趟火车的“唯一身份”,只有验证了身份,负责检票的工作人员才会让你上车,否则会直接拒绝你乘车。
**现在方案有了那在RPC里面我们该怎么实现呢**
首先我们要有一个可以供调用方进行调用接口登记的地方,我们姑且称这个地方为“授权平台”,调用方可以在授权平台上申请自己应用里面要调用的接口,而服务提供方则可以在授权平台上进行审批,只有服务提供方审批后调用方才能调用。但这只是解决了调用数据收集的问题,并没有完成真正的授权认证功能,缺少一个检票的环节。
既然有了刚搭建的授权平台,而且接口的授权数据也在这个平台上,我们自然就很容易想到是不是可以把这个检票的环节放到这个授权平台上呢?调用方每次发起业务请求的时候先去发一条认证请求到授权平台上,就说:“哥们儿,我能调用这个接口吗?”只有授权平台返回“没问题”后才继续把业务请求发送到服务提供方那去。整个流程如下图所示:
<img src="https://static001.geekbang.org/resource/image/8c/66/8c39a1ffdf4166a4e3506556897da266.jpg" alt="" title="集中式认证">
从使用功能的角度来说目前这种设计是没有问题的而且整个认证过程对RPC使用者来说也是透明的。但有一个问题就是这个授权平台承担了公司内所有RPC请求的次数总和当公司内部RPC使用程度高了之后这个授权平台就会成为一个瓶颈点而且必须保证超高可用一旦这个授权平台出现问题影响的可就是全公司的RPC请求了。
可能你会说我们可以改进下,我们是不是不需要把这个认证的逻辑放到业务请求过程中,而是可以把这个认证过程挪到初始化过程中呢?这样确实可以在很大程度上减少授权平台的压力,但本质并没有发生变化,还是一个集中式的授权平台。
**我们可以想一个更优雅一点的方案。**
其实调用方能不能调用相关接口,是由服务提供方说了算,我服务提供方认为你是可以的,你就肯定能调,那我们是不是就可以把这个检票过程放到服务提供方里面呢?在调用方启动初始化接口的时候,带上授权平台上颁发的身份去服务提供方认证下,当认证通过后就认为这个接口可以调用。
现在新的问题又来了,服务提供方验票的时候对照的数据来自哪儿,我总不能又去请求授权平台吧?否则就又会遇到和前面方案一样的问题。
你还记得我们加密算法里面有一种叫做不可逆加密算法吗HMAC就是其中一种具体实现。服务提供方应用里面放一个用于HMAC签名的私钥在授权平台上用这个私钥为申请调用的调用方应用进行签名这个签名生成的串就变成了调用方唯一的身份。服务提供方在收到调用方的授权请求之后我们只要需要验证下这个签名跟调用方应用信息是否对应得上就行了这样集中式授权的瓶颈也就不存在了。
## 服务发现也有安全问题?
现在我们已经解决了调用方之间的安全认证问题。那在RPC里面我们还有其它的安全问题吗
回到我们上面说的那个完整的RPC应用流程里面服务提供方会把接口Jar发布到私服上以方便调用方能引入到项目中快速完成RPC调用那有没有可能有人拿到你这个Jar后发布出来一个服务提供方呢这样的后果就是导致调用方通过服务发现拿到的服务提供方IP地址集合里面会有那个伪造的提供方。
当然这种情况相对上面说的调用方未经过咨询就直接调用的概率会小很多但为了让我们的系统整体更安全我们也需要在RPC里面考虑这种情况。要解决这个问题的根本就是需要把接口跟应用绑定上一个接口只允许有一个应用发布提供者避免其它应用也能发布这个接口。
那怎么实现呢?在[[第 08 讲]](https://time.geekbang.org/column/article/208171) 我们提到过,服务提供方启动的时候,需要把接口实例在注册中心进行注册登记。我们就可以利用这个流程,注册中心可以在收到服务提供方注册请求的时候,验证下请求过来的应用是否跟接口绑定的应用一样,只有相同才允许注册,否则就返回错误信息给启动的应用,从而避免假冒的服务提供者对外提供错误服务。
## 总结
安全问题在任何一个领域都很重要,但又经常被我们忽视,只有每次出安全事故后,我们才会意识到安全防护的重要性。所以在日常写代码的过程中,我们一定要保持一个严谨的态度,防止细小错误引入线上安全问题。
虽然RPC经常用于解决内网应用之间的调用内网环境相对公网也没有那么恶劣但我们也有必要去建立一套可控的安全体系去防止一些错误行为。对于RPC来说我们所关心的安全问题不会有公网应用那么复杂我们只要保证让服务调用方能拿到真实的服务提供方IP地址集合且服务提供方可以管控调用自己的应用就够了。
## 课后思考
前面讲的调用方之间的安全问题我们更多只是解决认证问题并没有解决权限问题。在现实开发过程中一个RPC接口定义里面一般会包含多个方法但我们目前只是解决了你能不能调用接口的问题并没有解决你能调用我接口里面的哪些方法。像这种问题你有什么好方案吗
欢迎留言和我分享你的答案,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!

View File

@@ -0,0 +1,103 @@
<audio id="audio" title="19 | 分布式环境下如何快速定位问题?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/33/a4/33227e29b6c6c410cf4feb812b1b78a4.mp3"></audio>
你好我是何小锋。上一讲我们学习了如何建立可靠的安全体系关键点就是“鉴权”我们可以通过统一的鉴权服务动态生成秘钥提高RPC调用的安全性。
回顾完上一讲的重点我们就切入今天的主题一起看看RPC在分布式环境下如何快速定位问题。重要性看字面也是不言而喻了只有准确地定位问题我们才能更好地解决问题。
## 分布式环境下定位问题有哪些困难?
在此之前,我想先请你想想,在开发以及生产环境运行的过程中,如果遇见问题,我们是如何定位的?
在开发过程中遇见问题其实很好排查我们可以用IDE在自己本地的开发环境中运行一遍代码进行debug在这个过程中是很容易找到问题的。
那换到生产环境代码在线上运行业务我们是不能进行debug的这时我们就可以通过打印日志来查看当前的异常日志这也是最简单有效的一种方式了。事实上大部分问题的定位我们也是这样做的。
那么如果是在分布式的生产环境中呢?比如下面这个场景:
我们搭建了一个分布式的应用系统在这个应用系统中我启动了4个子服务分别是服务A、服务B、服务C与服务D而这4个服务的依赖关系是A-&gt;B-&gt;C-&gt;D而这些服务又都部署在不同的机器上。在RPC调用中如果服务端的业务逻辑出现了异常就会把异常抛回给调用端那么如果现在这个调用链中有一个服务出现了异常我们该如何定位问题呢
<img src="https://static001.geekbang.org/resource/image/f7/82/f70e402a1634ae9c384c6cd6c4b89182.jpg" alt="" title="服务异常">
可能你的第一反应仍然是打印日志,好,那就打印日志吧。
假如这时我们发现服务A出现了异常那这个异常有没有可能是因为B或C或D出现了异常抛回来的呢当然很有可能。那我们怎么确定在整个应用系统中是哪一个调用步骤出现的问题以及是在这个步骤中的哪台机器出现的问题呢我们该在哪台机器上打印日志而且为了排查问题如果要打印日志我们就必须要修改代码这样的话我们就得重新对服务进行上线。如果这几个服务又恰好是跨团队跨部门的呢想想我们要面临的沟通成本吧。
所以你看,分布式环境下定位问题的难点就在于,各子应用、子服务间有着复杂的依赖关系,我们有时很难确定是哪个服务的哪个环节出现的问题。简单地通过日志排查问题,就要对每个子应用、子服务逐一进行排查,很难一步到位;若恰好再赶上跨团队跨部门,那不死也得去半条命了。
## 如何做到快速定位问题?
明白了难点我们其实就可以有针对性地去攻克它了。有关RPC在分布式环境下如何快速定位问题我给出两个方法很实用。
### 方法1借助合理封装的异常信息
我们前面说是因为各子应用、子服务间复杂的依赖关系,所以通过日志难定位问题。那我们就想办法通过日志定位到是哪个子应用的子服务出现问题就行了。
其实在RPC框架打印的异常信息中是包括定位异常所需要的异常信息的比如是哪类异常引起的问题如序列化问题或网络超时问题是调用端还是服务端出现的异常调用端与服务端的IP是什么以及服务接口与服务分组都是什么等等。具体如下图所示
<img src="https://static001.geekbang.org/resource/image/b8/1b/b8fee37688d39ae7913429f6cbc06f1b.jpg" alt="" title="链路调用异常">
这样的话在A-&gt;B-&gt;C-&gt;D这个过程中我们就可以很快地定位到是C服务出现了问题服务接口是com.demo.CSerivce调用端IP是192.168.1.2服务端IP是192.168.1.3,而出现问题的原因就是业务线程池满了。
由此可见一款优秀的RPC框架要对异常进行详细地封装还要对各类异常进行分类每类异常都要有明确的异常标识码并整理成一份简明的文档。使用方可以快速地通过异常标识码在文档中查阅从而快速定位问题找到原因并且异常信息中要包含排查问题时所需要的重要信息比如服务接口名、服务分组、调用端与服务端的IP以及产生异常的原因。总之就是要让使用方在复杂的分布式应用系统中根据异常信息快速地定位到问题。
以上是对于RPC框架本身的异常来说的比如序列化异常、响应超时异常、连接异常等等。那服务端业务逻辑的异常呢服务提供方提供的服务的业务逻辑也要封装自己的业务异常信息从而让服务调用方也可以通过异常信息快速地定位到问题。
### 方法2借助分布式链路跟踪
无论是RPC框架本身还是服务提供方提供的服务只要对异常信息进行合理地封装就可以让我们在分布式环境下定位问题变得更加容易。那这样是不是就满足我们定位问题的需求了呢
我们还是回到前面提过的那个分布式场景我们搭建了一个分布式的应用系统它由4个子服务组成4个服务的依赖关系为A-&gt;B-&gt;C-&gt;D。
假设这4个服务分别由来自不同部门的4个同事维护在A调用B的时候维护服务A的同事可能是不知道存在服务C和服务D的对于服务A来说它的下游服务只有B服务那这时如果服务C或服务D出现异常最终在整个链路中将异常抛给A了呢
在这种情况下维护服务A的同事该如何定位问题呢
因为对于A来说它可能是不知道下游存在服务C和服务D的所以维护服务A的同事会直接联系维护服务B的同事之后维护服务B的同事会继续联系下游服务的服务提供方直到找到问题。可这样做成本很高啊
现在我们换个思路其实我们只要知道整个请求的调用链路就可以了。服务A调用下游服务B服务B又调用了B依赖的下游服务如果维护服务A的同事能清楚地知道整个调用链路并且能准确地发现在整个调用链路中是哪个环节出现了问题那就好了。
这就好比我们收发快递,我们可以在平台上看到快递配送的轨迹,实时获知快递在何时到达了哪个站点,这样当我们没有准时地收到快递时,我们马上就能知道快递是在哪里延误了。
在分布式环境下,要想知道服务调用的整个链路,我们可以用“分布式链路跟踪”。
先介绍下分布式链路跟踪系统。从字面上理解,分布式链路跟踪就是将一次分布式请求还原为一个完整的调用链路,我们可以在整个调用链路中跟踪到这一次分布式请求的每一个环节的调用情况,比如调用是否成功,返回什么异常,调用的哪个服务节点以及请求耗时等等。
这样如果我们发现服务调用出现问题,通过这个方法,我们就能快速定位问题,哪怕是多个部门合作,也可以一步到位。
**紧接着我们再看看在RPC框架中是如何整合分布式链路跟踪的**
分布式链路跟踪有Trace与Span的概念什么意思呢我逐一解释。
Trace就是代表整个链路每次分布式都会产生一个Trace每个Trace都有它的唯一标识即TraceId在分布式链路跟踪系统中就是通过TraceId来区分每个Trace的。
Span就是代表了整个链路中的一段链路也就是说Trace是由多个Span组成的。在一个Trace下每个Span也都有它的唯一标识SpanId而Span是存在父子关系的。还是以讲过的例子为例子在A-&gt;B-&gt;C-&gt;D的情况下在整个调用链中正常情况下会产生3个Span分别是Span1A-&gt;B、Span2B-&gt;C、Span3C-&gt;D这时Span3的父Span就是Span2而Span2的父Span就是Span1。
Trace与Span的关系如下图所示
<img src="https://static001.geekbang.org/resource/image/47/65/47df54d3d38cb30fddf25e8b8b2c4b65.jpg" alt="" title="示意图">
分布式链路跟踪系统的实现方式有很多但它们都脱离不开我刚才说的Trace和Span这两点可以说非常重要掌握了这两个概念其实你就掌握了大部分实现方式的原理。接着我们看看在RPC框架中如何利用这两个概念去整合分布式链路跟踪。
RPC在整合分布式链路跟踪需要做的最核心的两件事就是“埋点”和“传递”。
所谓“埋点”就是说分布式链路跟踪系统要想获得一次分布式调用的完整的链路信息就必须对这次分布式调用进行数据采集而采集这些数据的方法就是通过RPC框架对分布式链路跟踪进行埋点。
RPC调用端在访问服务端时在发送请求消息前会触发分布式跟踪埋点在接收到服务端响应时也会触发分布式跟踪埋点并且在服务端也会有类似的埋点。这些埋点最终可以记录一个完整的Span而这个链路的源头会记录一个完整的Trace最终Trace信息会被上报给分布式链路跟踪系统。
那所谓“传递”就是指上游调用端将Trace信息与父Span信息传递给下游服务的服务端由下游触发埋点对这些信息进行处理在分布式链路跟踪系统中每个子Span都存有父Span的相关信息以及Trace的相关信息。
## 总结
今天我们讲解了在分布式环境下如何快速定位问题。这里面的难点就是分布式系统有着较为复杂的依赖关系,我们很难判断出是哪个环节出现的问题,而且在大型的分布式系统中,往往会有跨部门、跨团队合作的情况,在排查问题的时候会面临非常高的沟通成本。
为了在分布式环境下能够快速地定位问题RPC框架应该对框架自身的异常进行详细地封装每类异常都要有明确的异常标识码并将其整理成一份简明的文档异常信息中要尽量包含服务接口名、服务分组、调用端与服务端的IP以及产生异常的原因等信息这样对于使用方来说就非常便捷了。
另外,服务提供方在提供服务时也要对异常进行封装,以方便上游排查问题。
在分布式环境下,我们可以通过分布式链路跟踪来快速定位问题,尤其是在多个部门的合作中,这样做可以一步到位,减少排查问题的时间,降低沟通成本,以最高的效率解决实际问题。
## 课后思考
在分布式环境下,你还知道哪些快速定位问题的方法?
期待你能在留言区中和我分享,也欢迎你把文章分享给你的朋友,邀请他加入学习,共同探讨。我们下节课再见!

View File

@@ -0,0 +1,102 @@
<audio id="audio" title="20 | 详解时钟轮在RPC中的应用" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3f/10/3fdb8cab3e7a7d89da65b02945c02410.mp3"></audio>
你好我是何小锋。上一讲我们学习了在分布式环境下如何快速定位问题简单回顾下重点。在分布式环境下RPC框架自身以及服务提供方的业务逻辑实现都应该对异常进行合理地封装让使用方可以根据异常快速地定位问题而在依赖关系复杂且涉及多个部门合作的分布式系统中我们也可以借助分布式链路跟踪系统快速定位问题。
现在切换到咱们今天的主题一起看看时钟轮在RPC中的应用。
## 定时任务带来了什么问题?
在讲解时钟轮之前我们先来聊聊定时任务。相信你在开发的过程中很多场景都会使用到定时任务在RPC框架中也有很多地方会使用到它。就以调用端请求超时的处理逻辑为例下面我们看一下RPC框架是如果处理超时请求的。
回顾下[[第 17 讲]](https://time.geekbang.org/column/article/216803)我讲解Future的时候说过无论是同步调用还是异步调用调用端内部实行的都是异步而调用端在向服务端发送消息之前会创建一个Future并存储这个消息标识与这个Future的映射当服务端收到消息并且处理完毕后向调用端发送响应消息调用端在接收到消息后会根据消息的唯一标识找到这个Future并将结果注入给这个Future。
那在这个过程中,如果服务端没有及时响应消息给调用端呢?调用端该如何处理超时的请求?
没错就是可以利用定时任务。每次创建一个Future我们都记录这个Future的创建时间与这个Future的超时时间并且有一个定时任务进行检测当这个Future到达超时时间并且没有被处理时我们就对这个Future执行超时逻辑。
**那定时任务该如何实现呢?**
有种实现方式是这样的也是最简单的一种。每创建一个Future我们都启动一个线程之后sleep到达超时时间就触发请求超时的处理逻辑。
这种方式吧确实简单在某些场景下也是可以使用的但弊端也是显而易见的。就像刚才我讲的那个Future超时处理的例子如果我们面临的是高并发的请求单机每秒发送数万次请求请求超时时间设置的是5秒那我们要创建多少个线程用来执行超时任务呢超过10万个线程这个数字真的够吓人了。
别急我们还有另一种实现方式。我们可以用一个线程来处理所有的定时任务还以刚才那个Future超时处理的例子为例。假设我们要启动一个线程这个线程每隔100毫秒会扫描一遍所有的处理Future超时的任务当发现一个Future超时了我们就执行这个任务对这个Future执行超时逻辑。
这种方式我们用得最多,它也解决了第一种方式线程过多的问题,但其实它也有明显的弊端。
同样是高并发的请求那么扫描任务的线程每隔100毫秒要扫描多少个定时任务呢如果调用端刚好在1秒内发送了1万次请求这1万次请求要在5秒后才会超时那么那个扫描的线程在这个5秒内就会不停地对这1万个任务进行扫描遍历要额外扫描40多次每100毫秒扫描一次5秒内要扫描近50次很浪费CPU。
在我们使用定时任务时它所带来的问题就是让CPU做了很多额外的轮询遍历操作浪费了CPU这种现象在定时任务非常多的情况下尤其明显。
## 什么是时钟轮?
这个问题也不难解决我们只要找到一种方式减少额外的扫描操作就行了。比如我的一批定时任务是5秒之后执行我在4.9秒之后才开始扫描这批定时任务这样就大大地节省了CPU。这时我们就可以利用时钟轮的机制了。
我们先来看下我们生活中用到的时钟。
<img src="https://static001.geekbang.org/resource/image/d6/cf/d6366d3aec1af5f1b028b5939a7753cf.jpg" alt="" title="时钟示意图">
很熟悉了吧时钟有时针、分针和秒针秒针跳动一周之后也就是跳动60个刻度之后分针跳动1次分针跳动60个刻度时针走动一步。
而时钟轮的实现原理就是参考了生活中的时钟跳动的原理。
<img src="https://static001.geekbang.org/resource/image/b6/5a/b61d5a5d1ec29384ba8730b71b4beb5a.jpg" alt="" title="时钟轮示意图">
在时钟轮机制中,有时间槽和时钟轮的概念,时间槽就相当于时钟的刻度,而时钟轮就相当于秒针与分针等跳动的一个周期,我们会将每个任务放到对应的时间槽位上。
时钟轮的运行机制和生活中的时钟也是一样的每隔固定的单位时间就会从一个时间槽位跳到下一个时间槽位这就相当于我们的秒针跳动了一次时钟轮可以分为多层下一层时钟轮中每个槽位的单位时间是当前时间轮整个周期的时间这就相当于1分钟等于60秒钟当时钟轮将一个周期的所有槽位都跳动完之后就会从下一层时钟轮中取出一个槽位的任务重新分布到当前的时钟轮中当前时钟轮则从第0槽位从新开始跳动这就相当于下一分钟的第1秒。
为了方便你了解时钟轮的运行机制,我们用一个场景例子来模拟下,一起看下这个场景。
假设我们的时钟轮有10个槽位而时钟轮一轮的周期是1秒那么我们每个槽位的单位时间就是100毫秒而下一层时间轮的周期就是10秒每个槽位的单位时间也就是1秒并且当前的时钟轮刚初始化完成也就是第0跳当前在第0个槽位。
<img src="https://static001.geekbang.org/resource/image/a6/e9/a661c85b35508dea9cd9db6969f58de9.jpg" alt="" title="时钟轮示意图">
现在我们有3个任务分别是任务A90毫秒之后执行、任务B610毫秒之后执行与任务C1秒610毫秒之后执行我们将这3个任务添加到时钟轮中任务A被放到第0槽位任务B被放到第6槽位任务C被放到下一层时间轮的第1槽位如下面这张图所示。
<img src="https://static001.geekbang.org/resource/image/6d/de/6da0e7a3dc3d327f7f7b7a3cf7f658de.jpg" alt="" title="时钟轮任务分布示意图">
当任务A刚被放到时钟轮就被即刻执行了因为它被放到了第0槽位而当前时间轮正好跳到第0槽位实际上还没开始跳动状态为第0跳600毫秒之后时间轮已经进行了6跳当前槽位是第6槽位第6槽位所有的任务都被取出执行1秒钟之后当前时钟轮的第9跳已经跳完从新开始了第0跳这时下一层时钟轮从第0跳跳到了第1跳将第1槽位的任务取出分布到当前的时钟轮中这时任务C从下一层时钟轮中取出并放到当前时钟轮的第6槽位1秒600毫秒之后任务C被执行。
<img src="https://static001.geekbang.org/resource/image/92/11/9224564f0bad64e130061e0bb1e07411.jpg" alt="" title="任务C槽位转换示意图">
看完了这个场景相信你对时钟轮的机制已经有所了解了。在这个例子中时钟轮的扫描周期仍是100毫秒但是其中的任务并没有被过多的重复扫描它完美地解决了CPU浪费的问题。
这个机制其实不难理解,但实现起来还是很有难度的,其中要注意的问题也很多。具体的代码实现我们这里不展示,这又是另外一个比较大的话题了。有兴趣的话你可以自行查阅下相关源码,动手实现一下。到哪里卡住了,我们可以在留言区交流。
## 时钟轮在RPC中的应用
通过刚才对时钟轮的讲解相信你可以看出它就是用来执行定时任务的可以说在RPC框架中只要涉及到定时相关的操作我们就可以使用时钟轮。
那么RPC框架在哪些功能实现中会用到它呢
刚才我举例讲到的调用端请求超时处理这里我们就可以应用到时钟轮我们每发一次请求都创建一个处理请求超时的定时任务放到时钟轮里在高并发、高访问量的情况下时钟轮每次只轮询一个时间槽位中的任务这样会节省大量的CPU。
调用端与服务端启动超时也可以应用到时钟轮以调用端为例假设我们想要让应用可以快速地部署例如1分钟内启动如果超过1分钟则启动失败。我们可以在调用端启动时创建一个处理启动超时的定时任务放到时钟轮里。
除此之外你还能想到RPC框架在哪些地方可以应用到时钟轮吗还有定时心跳。RPC框架调用端定时向服务端发送心跳来维护连接状态我们可以将心跳的逻辑封装为一个心跳任务放到时钟轮里。
这时你可能会有一个疑问,心跳是要定时重复执行的,而时钟轮中的任务执行一遍就被移除了,对于这种需要重复执行的定时任务我们该如何处理呢?在定时任务的执行逻辑的最后,我们可以重设这个任务的执行时间,把它重新丢回到时钟轮里。
## 总结
今天我们主要讲解了时钟轮的机制以及时钟轮在RPC框架中的应用。
这个机制很好地解决了定时任务中因每个任务都创建一个线程导致的创建过多线程的问题以及一个线程扫描所有的定时任务让CPU做了很多额外的轮询遍历操作而浪费CPU的问题。
时钟轮的实现机制就是模拟现实生活中的时钟,将每个定时任务放到对应的时间槽位上,这样可以减少扫描任务时对其它时间槽位定时任务的额外遍历操作。
在时间轮的使用中,有些问题需要你额外注意:
- 时间槽位的单位时间越短时间轮触发任务的时间就越精确。例如时间槽位的单位时间是10毫秒那么执行定时任务的时间误差就在10毫秒内如果是100毫秒那么误差就在100毫秒内。
- 时间轮的槽位越多那么一个任务被重复扫描的概率就越小因为只有在多层时钟轮中的任务才会被重复扫描。比如一个时间轮的槽位有1000个一个槽位的单位时间是10毫秒那么下一层时间轮的一个槽位的单位时间就是10秒超过10秒的定时任务会被放到下一层时间轮中也就是只有超过10秒的定时任务会被扫描遍历两次但如果槽位是10个那么超过100毫秒的任务就会被扫描遍历两次。
结合这些特点,我们就可以视具体的业务场景而定,对时钟轮的周期和时间槽数进行设置。
在RPC框架中只要涉及到定时任务我们都可以应用时钟轮比较典型的就是调用端的超时处理、调用端与服务端的启动超时以及定时心跳等等。
## 课后思考
在RPC框架中除了我说过的那几个例子你还知道有哪些功能的实现可以应用到时钟轮
欢迎留言和我分享你的答案,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!

View File

@@ -0,0 +1,57 @@
<audio id="audio" title="21 | 流量回放:保障业务技术升级的神器" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c5/df/c520d62f94ada8aaaa0859f5e4988fdf.mp3"></audio>
你好我是何小锋。上一讲我们学习了时钟轮在RPC中的应用核心原理就一个关键字“分而治之”我们可以把它用在任何需要高效处理大量定时任务的场景中最具有代表性的就是在高并发场景下的请求超时检测。
回顾完上一讲的重点我们就进入咱们今天的主题一起看看流量回放在RPC里面的应用。
如果你经常翻阅一些技术文章的话可能你会不止一次看到过“流量回放”这个词。我简单地介绍一下所谓的流量就是某个时间段内的所有请求我们通过某种手段把发送到A应用的所有请求录制下来然后把这些请求统一转发到B应用让B应用接收到的请求参数跟A应用保持一致从而实现A接收到的请求在B应用里面重新请求了一遍。整个过程我们称之为“流量回放”。
这就好比今晚有场球赛,但我没空看,但我可以利用视频录播技术把球赛录下来,我随时想看都可以拿出来看,画面是一模一样的。
那在系统开发的过程中,回放功能可以用来做什么呢?
## 流量回放可以做什么?
我个人感觉,在我们日常开发过程中,可以专心致志地写代码、完成业务功能,是件很幸福的事儿,让我比较头疼的是代码开发完成后的测试环节。
在团队中我们经常是多个需求并行开发的在开发新需求的过程中我们还可能夹杂着应用的重构和拆分。每到这个时候我们基本很难做到不改动老逻辑那只要有改动就有可能会存在考虑不周全的情况。如果你比较严谨的话那可能在开发完成后你会把项目里面的TestCase都跑一遍并同时补充新功能的TestCase只有所有的TestCase都跑通后才能安心。
在代码里面算小改动的业务需求这种做法一般不会出问题。但对于大改动的应用比如应用中很多基础逻辑都被改动过这时候如果你还是通过已有的Case去验证功能的正确性就很难保证应用上线后不出故障了毕竟我们靠自己维护的Case相对线上运行的真实环境来说还是少了很多。
这时候我们会向更专业的QA测试人员求助希望他们能从QA角度多加入一些Case。但因为我们改动代码逻辑影响范围比较大想要圈定一个比较确定的测试范围又很难坦白讲这时候相对保险的方式就是QA把整个项目都回归测试一遍。这种方式已经是在最大程度上避免上线出问题了但从概率角度上来讲也不是万无一失的因为线上不仅环境复杂而且使用场景也并不好评估还有就是这种方式耗时也很长。
这就是我认为最让人头疼的原因靠传统QA测试的方式不仅过程费时结果也不是完全可靠。那有没有更可靠、更廉价的方案呢
传统QA测试出问题的根本原因就是因为改造后的应用在上线后出现跟应用上线前不一致的行为。而我们测试的目的就是为了保证改造后的应用跟改造前应用的行为一致我们测试Case也都是在尽力模拟应用在线上的运行行为但仅通过我们自己的枚举方式维护的Case并不能代表线上应用的所有行为。因此最好的方式就是用线上流量来验证但是直接把新应用上线肯定是不行的因为一旦新改造的应用存在问题就可能会导致线上调用方业务受损。
我们可以换一种思路,我可以先把线上一段时间内的请求参数和响应结果保存下来,然后把这些请求参数在新改造的应用里重新请求一遍,最后比对一下改造前后的响应结果是否一致,这就间接达到了使用线上流量测试的效果。有了线上的请求参数和响应结果后,我们再结合持续集成过程,就可以让我们改动后的代码随时用线上流量进行验证,这就跟我录制球赛视频一样,只要我想看,我随时都可以拿出来重新看一遍。
## RPC怎么支持流量回放
那在实际工作中,我们该怎么实现流量回放呢?
我们常见的方案有很多比如像TcpCopy、Nginx等。但在线上环境要使用这些工具的时候我们还得需要找运维团队帮我们把应用安装到应用实例里面然后再按照你的需求给配置好才能使用整个过程繁琐而且总数重复做无用功那有没有更好的办法呢尤其是在应用使用了RPC的情况下。
在前面我们不止一次说过RPC是用来完成应用之间通信的换句话就是说应用之间的所有请求响应都会经过RPC。
既然所有的请求都会经过RPC那么我们在RPC里面是不是就可以很方便地拿到每次请求的出入参数拿到这些出入参数后我们只要把这些出入参数旁录下来并把这些旁录结果用异步的方式发送到一个固定的地方保存起来这样就完成了流量回放里面的录制功能。
有了真实的请求入参之后剩下的就是怎么把这些请求参数转发到我们要回归测试的应用里面。在RPC中我们把能够接收请求的应用叫做服务提供方那就是说我们只需要模拟一个应用调用方把刚才收到的请求参数重新发送一遍到要回归测试的应用里面然后比对录制拿到的请求结果和新请求的结果就可以完成请求回放的效果。整个过程如下图所示
<img src="https://static001.geekbang.org/resource/image/df/c8/df79756a8c5345d1ccaca96a77c9f1c8.jpg" alt="" title="RPC回放过程">
相对其它现成的流量回放方案我们在RPC里面内置流量回放功能使用起来会更加方便并且我们还可以做更多定制比如在线启停、方法级别录制等个性化需求。
## 总结
保障线上应用的稳定,是我们研发同学每天都在努力耕耘的一件事,不管是通过应用架构升级,还是修复现有问题的方式。实际情况就是我们不仅要保障已有业务的稳定,还需要快速去完成各种新业务的需求,这期间我们的应用代码就会经常发生变化,而发生变化后就可能会引入新的不稳定因素,而且这个过程会一直持续不断发生。
为了保障应用升级后我们的业务行为还能保持和升级前一样我们在大多数情况下都是依靠已有的TestCase去验证但这种方式在一定程度上并不是完全可靠的。最可靠的方式就是引入线上Case去验证改造后的应用把线上的真实流量在改造后的应用里面进行回放这样不仅节省整个上线时间还能弥补手动维护Case存在的缺陷。
应用引入了RPC后所有的请求流量都会被RPC接管所以我们可以很自然地在RPC里面支持流量回放功能。虽然这个功能本身并不是RPC的核心功能但对于使用RPC的人来说他们有了这个功能之后就可以更放心地升级自己的应用了。
## 课后思考
除了上面我提到的可以使用流量回放功能来验证改造后的应用逻辑,我们还可以用流量回放来做哪些有意义的事儿?
欢迎留言和我分享你的思考,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!

View File

@@ -0,0 +1,63 @@
<audio id="audio" title="22 | 动态分组:超高效实现秒级扩缩容" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3d/0b/3d8695a2b1d7f3cab04e4803e706e60b.mp3"></audio>
你好我是何小锋。上一讲我们介绍了在RPC里面怎么支持流量回放应用在引入RPC后所有的请求都会被RPC接管而我们在RPC里面引入回放的原因也很简单就是想通过线上流量来验证改造后应用的正确性而线上流量相比手动维护TestCase的场景更丰富所以用线上流量进行测试的覆盖率会更广。
回顾完上一讲的重点我们就切入今天的主题一起看看动态分组在RPC里面的应用。
在[[第 16 讲]](https://time.geekbang.org/column/article/215668) 我们讲过,在调用方复杂的情况下,如果还是让所有调用方都调用同一个集群的话,很有可能会因为非核心业务的调用量突然增长,而让整个集群变得不可用了,进而让核心业务的调用方受到影响。为了避免这种情况发生,我们需要把整个大集群根据不同的调用方划分出不同的小集群来,从而实现调用方流量隔离的效果,进而保障业务之间不会互相影响。
## 分组后容量评估
通过人为分组的方式确实能帮服务提供方硬隔离调用方的流量,让不同的调用方拥有自己独享的集群,从而保障各个调用方之间互不影响。但这对于我们服务提供方来说,又带来了一个新的问题,就是我们该给调用方分配多大的集群才合适呢?
在[[第 16 讲]](https://time.geekbang.org/column/article/215668) 我们也有聊到过这样的问题,就是该怎么划分集群的分组?当然,最理想的情况就是给每个调用方都分配一个独立的分组,但是如果在服务提供方的调用方相对比较多的情况下,对于服务提供方来说要维护这些关系还是比较困难的。因此实际在给集群划分分组的时候,我们一般会选择性地合并一些调用方到同一个分组里。这就需要我们服务提供方考虑该怎么合并,且合并哪些调用方?
因为这个问题并没有统一的标准,所以我当时给的建议就是我们可以按照应用的重要级别来划分,让非核心业务应用跟核心业务应用不要公用一个分组,核心应用之间也最好别用同一个分组。但这只是一个划分集群分组的建议,并没有具体告诉你该如何划分集群大小。换句话就是,你可以按照这个原则去规划设计自己的集群要分多少个组。
按照上面的原则我们把整个集群从逻辑上分为不同的分组之后接下来我们要做的事情就是给每个分组分配相应的机器数量。那每个分组对应的机器数量我们该怎么计算呢我相信这个问题肯定难不倒你。在这儿我先分享下我们团队常用的做法我们一般会先通过压测去评估下服务提供方单台机器所能承受的QPS然后再计算出每个分组里面的所有调用方的调用总量。有了这两个值之后我们就能很容易地计算出这个分组所需要的机器数。
通过计算分组内所有调用方QPS的方式来算出单个分组内所需的机器数整体而言还是比较客观准确的。但因为每个调用方的调用量并不是一成不变的比如商家找个网红做个直播卖货那就很有可能会导致今天的下单量相对昨天有小幅度的上涨。就是因为这些不确定性因素的存在所以服务提供方在给调用方做容量评估的时候通常都会在现有调用量的基础上加一个百分比而这个百分比多半来自历史经验总结。
总之,就是在我们算每个分组所需要的机器数的时候,需要额外给每个分组增加一些机器,从而让每个小集群有一定的抗压能力,而这个抗压能力取决于给这个集群预留的机器数量。作为服务提供方来说,肯定希望给每个集群预留的机器数越多越好,但现实情况又不允许预留太多,因为这样会增加团队的整体成本。
## 分组带来的问题
通过给分组预留少量机器的方式,以增加单个集群的抗压能力。一般情况下,这种机制能够运行得很好,但在应对大的突发流量时,就会显得有点捉襟见肘了。因为机器成本的原因,我们给每个分组预留的机器数量都不会太多,所以当突发流量超过预留机器的能力的时候,就会让这个分组的集群处于一个危险状态了。
这时候我们唯一能做的就是给这个分组去扩容新的机器,但临时扩容新机器通常需要一个比较长的时间,而且花的时间越长,业务受影响的范围就越大。
那有没有更便捷一点的方案呢?前面我们说过,我们在给分组做容量评估的时候,通常都会增加了一些富余。换句话就是,除了当前出问题的分组,其它分组的服务提供方在保障自己调用方质量的同时,还是可以额外承担一些流量的。我们可以想办法快速利用这部分已有的能力。
但因为我们实现了流量隔离功能,整个集群被我们划分成了不同的分组,所以当前出问题的调用方并不能把请求发送到其它分组的机器上。那可能你会说,既然临时去申请机器进行扩容时间长,那我能不能把上面说的那些富余的机器直接拿过来,把部署在机器上的应用改成出问题的分组,然后进行重启啊?这样出问题的那个分组的服务提供方机器数就会变多了。
从结果上来看,这样处理确实能够解决问题,但有一个问题就是这样处理的时间还是相对较长的,而且当这个分组的流量恢复后,你还得把临时借过来的机器还回原来的分组。
问题分析到这儿,我想说,动态分组就可以派上用场了。
## 动态分组的应用
上面的问题,其根本原因就是某个分组的调用方流量突增,而这个分组所预留的空间也不能满足当前流量的需求,但是其它分组的服务提供方有足够的富余能力。但这些富余的能力,又被我们的分组进行了强制的隔离,我们又不能抛弃分组功能,否则老问题就要循环起来了。
那这样的话,我们就只能在出问题的时候临时去借用其它分组的部分能力,但通过改分组进行重启应用的方式,不仅操作过程慢,事后还得恢复。因此这种生硬的方式显然并不是很合适。
想一下啊,我们改应用分组然后进行重启的目的,就是让出问题的服务调用方能通过服务发现找到更多的服务提供方机器,而服务发现的数据来自注册中心,那我们是不是可以通过修改注册中心的数据来解决呢?
我们只要把注册中心里面的部分实例的别名改成我们想要的别名,然后通过服务发现进而影响到不同调用方能够调用的服务提供方实例集合。
举个例子服务提供方有3个服务实例其中A分组有2个实例B分组有1个实例调用方1调用A分组调用方2调用B分组。我们把A分组里面的一个实例分组在注册中心由A分组改为B分组经过服务发现影响后整个调用拓扑就变成了这样
<img src="https://static001.geekbang.org/resource/image/2e/92/2e4f55f8ab9c40108524539a178e9692.jpg" alt="" title="动态别名过程">
通过直接修改注册中心数据,我们可以让任何一个分组瞬间拥有不同规模的集群能力。我们不仅可以实现把某个实例的分组名改成另外一个分组名,还可以让某个实例分组名变成多个分组名,这就是我们在动态分组里面最常见的两种动作——追加和替换。
## 总结
在[[第 16 讲]](https://time.geekbang.org/column/article/215668),我们讲了分组后带来的收益,它可以帮助服务提供方实现调用方的隔离。但是因为调用方流量并不是一成不变的,而且还可能会因为突发事件导致某个分组的流量溢出,而在整个大集群还有富余能力的时候,又因为分组隔离不能为出问题的集群提供帮助。
为了解决这种突发流量的问题,我们提供了一种更高效的方案,可以实现分组的快速扩缩容。事实上我们还可以利用动态分组解决分组后给每个分组预留机器冗余的问题,我们没有必要把所有冗余的机器都分配到分组里面,我们可以把这些预留的机器做成一个共享的池子,从而减少整体预留的实例数量。
## 课后思考
在服务治理的过程中,我们通常会给服务进行逻辑分组,但之后某个分组可能会遇到突发流量调用的问题,在本讲我给出了一个动态分组的方案。但是动态分组的过程中,我们只是把注册中心的数据改了,而服务提供方提供真实的分组名并没有改变,这时候用动态分组名的调用方调用过来的请求可能就会报错,因为服务提供方会验证调用方过来的分组名跟自身的是否一样。针对这个问题,你能想到什么解决方案?
欢迎留言和我分享你的答案,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!

View File

@@ -0,0 +1,89 @@
<audio id="audio" title="23 | 如何在没有接口的情况下进行RPC调用" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fa/44/fa88615ff18b8a21dbec7d9e7e4d6944.mp3"></audio>
你好我是何小锋。上一讲我们学习了RPC如何通过动态分组来实现秒级扩缩容其关键点就是“动态”与“隔离”。今天我们来聊聊如何在没有接口的情况下进行RPC调用。
## 应用场景有哪些?
在RPC运营的过程中让调用端在没有接口API的情况下发起RPC调用的需求不只是一个业务方和我提过这里我列举两个非常典型的场景例子。
**场景一:**我们要搭建一个统一的测试平台可以让各个业务方在测试平台中通过输入接口、分组名、方法名以及参数值在线测试自己发布的RPC服务。这时我们就有一个问题要解决我们搭建统一的测试平台实际上是作为各个RPC服务的调用端而在RPC框架的使用中调用端是需要依赖服务提供方提供的接口API的而统一测试平台不可能依赖所有服务提供方的接口API。我们不能因为每有一个新的服务发布就去修改平台的代码以及重新上线。这时我们就需要让调用端在没有服务提供方提供接口的情况下仍然可以正常地发起RPC调用。
<img src="https://static001.geekbang.org/resource/image/fc/bc/fc0027ad042768d9aabf68182de5d2bc.jpg" alt="" title="示意图">
**场景二:**我们要搭建一个轻量级的服务网关可以让各个业务方用HTTP的方式通过服务网关调用其它服务。这时就有与场景一相同的问题服务网关要作为所有RPC服务的调用端是不能依赖所有服务提供方的接口API的也需要调用端在没有服务提供方提供接口的情况下仍然可以正常地发起RPC调用。
<img src="https://static001.geekbang.org/resource/image/09/c5/09bd6312f3bdb5d4e9276bd0cb0025c5.jpg" alt="" title="示意图">
这两个场景都是我们经常会碰到的而让调用端在没有服务提供方提供接口API的情况下仍然可以发起RPC调用的功能在RPC框架中也是非常有价值的。
## 怎么做?
RPC框架要实现这个功能我们可以使用泛化调用。那什么是泛化调用呢我们带着这个问题先学习下如何在没有接口的情况下进行RPC调用。
我们先回想下我在基础篇讲过的内容通过前面的学习我们了解到在RPC调用的过程中调用端向服务端发起请求首先要通过动态代理正如[[第 05 讲]](https://time.geekbang.org/column/article/205910) 中我说过的动态代理可以帮助我们屏蔽RPC处理流程真正地让我们发起远程调用就像调用本地一样。
那么在RPC调用的过程中既然调用端是通过动态代理向服务端发起远程调用的那么在调用端的程序中就一定要依赖服务提供方提供的接口API因为调用端是通过这个接口API自动生成动态代理的。那如果没有接口API呢我们该如何让调用端仍然能够发起RPC调用呢
所谓的RPC调用本质上就是调用端向服务端发送一条请求消息服务端接收并处理之后向调用端发送一条响应消息调用端处理完响应消息之后一次RPC调用就完成了。那是不是说我们只要能够让调用端在没有服务提供方提供接口的情况下仍然能够向服务端发送正确的请求消息就能够解决这个问题了呢
没错,只要调用端将服务端需要知道的信息,如接口名、业务分组名、方法名以及参数信息等封装成请求消息发送给服务端,服务端就能够解析并处理这条请求消息,这样问题就解决了。过程如下图所示:
<img src="https://static001.geekbang.org/resource/image/a3/89/a3c5ddba4960645b77d73e503da34b89.jpg" alt="" title="示意图">
现在我们已经清楚了解决问题的关键但RPC的调用端向服务端发送消息是需要以动态代理作为入口的我们现在得继续想办法让调用端发送我刚才讲过的那条请求消息。
我们可以定义一个统一的接口GenericService调用端在创建GenericService代理时指定真正需要调用的接口的接口名以及分组名而GenericService接口的$invoke方法的入参就是方法名以及参数信息。
这样我们传递给服务端所需要的所有信息包括接口名、业务分组名、方法名以及参数信息等都可以通过调用GenericService代理的$invoke方法来传递。具体的接口定义如下
```
class GenericService {
Object $invoke(String methodName, String[] paramTypes, Object[] params);
}
```
这个通过统一的GenericService接口类生成的动态代理来实现在没有接口的情况下进行RPC调用的功能我们就称之为泛化调用。
通过泛化调用功能我们可以解决在没有服务提供方提供接口API的情况下进行RPC调用那么这个功能是否就完美了呢
回顾下[[第 17 讲]](https://time.geekbang.org/column/article/216803) 我过的内容RPC框架可以通过异步的方式提升吞吐量还有如何实现全异步的RPC框架其关键点就是RPC框架对CompletableFuture的支持那么我们的泛化调用是否也可以支持异步呢
当然可以。我们可以给GenericService接口再添加一个异步方法$asyncInvoke方法的返回值就是CompletableFutureGenericService接口的具体定义如下
```
class GenericService {
Object $invoke(String methodName, String[] paramTypes, Object[] params);
CompletableFuture&lt;Object&gt; $asyncInvoke(String methodName, String[] paramTypes, Object[] params);
}
```
学到这里相信你已经对泛化调用的功能有一定的了解了那你有没有想过这样一个问题在没有服务提供方提供接口API的情况下我们可以用泛化调用的方式实现RPC调用但是如果没有服务提供方提供接口API我们就没法得到入参以及返回值的Class类也就不能对入参对象进行正常的序列化。这时我们会面临两个问题
**问题1**调用端不能对入参对象进行正常的序列化,那调用端、服务端在接收到请求消息后,入参对象又该如何序列化与反序列化呢?
回想下[[第 07 讲]](https://time.geekbang.org/column/article/207137)在这一讲中我讲解了如何设计可扩展的RPC框架我们通过插件体系来提高RPC框架的可扩展性在RPC框架的整体架构中就包括了序列化插件我们可以为泛化调用提供专属的序列化插件通过这个插件解决泛化调用中的序列化与反序列化问题。
**问题2**调用端的入参对象params与返回值应该是什么类型呢
在服务提供方提供的接口API中被调用的方法的入参类型是一个对象那么使用泛化调用功能的调用端可以使用Map类型的对象之后通过泛化调用专属的序列化方式对这个Map对象进行序列化服务端收到消息后再通过泛化调用专属的序列化方式将其反序列成对象。
## 总结
今天我们主要讲解了如何在没有接口的情况下进行RPC调用泛化调用的功能可以实现这一目的。
这个功能的实现原理就是RPC框架提供统一的泛化调用接口GenericService调用端在创建GenericService代理时指定真正需要调用的接口的接口名以及分组名通过调用GenericService代理的$invoke方法将服务端所需要的所有信息包括接口名、业务分组名、方法名以及参数信息等封装成请求消息发送给服务端实现在没有接口的情况下进行RPC调用的功能。
而通过泛化调用的方式发起调用由于调用端没有服务端提供方提供的接口API不能正常地进行序列化与反序列化我们可以为泛化调用提供专属的序列化插件来解决实际问题。
## 课后思考
在讲解泛化调用时,我讲到服务端在收到调用端通过泛化调用的方式发送过来的请求时,会使用泛化调用专属的序列化插件实现对其进行反序列化,那么服务端是如何判定这个请求消息是通过泛化调用的方式发送过来的消息呢?
欢迎留言和我分享你的答案,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!

View File

@@ -0,0 +1,57 @@
<audio id="audio" title="24 | 如何在线上环境里兼容多种RPC协议" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/bb/ee/bbd08da839ab498ade98796d2e8f63ee.mp3"></audio>
你好我是何小锋。上一讲我们学习了如何在没有接口的情况下完成RPC调用其关键在于你要理解接口定义在RPC里面的作用。除了我们前面说的动态代理生成的过程中需要用到接口定义剩余的其它过程中接口的定义只是被当作元数据来使用而动态代理在RPC中并不是一个必须的环节所以在没有接口定义的情况下我们同样也是可以完成RPC调用的。
回顾完上一讲的重点咱们就言归正传切入今天的主题一起看看如何在线上环境里兼容多种RPC协议。
看到这个问题后可能你的第一反应就是在真实环境中为什么会存在多个协议呢我们说过RPC是能够帮助我们屏蔽网络编程细节实现调用远程方法就跟调用本地一样的体验。大白话说就是RPC是能够帮助我们在开发过程中完成应用之间的通信而又不需要我们关心具体通信细节的工具。
## 为什么要支持多协议?
既然应用之间的通信都是通过RPC来完成的而能够完成RPC通信的工具有很多比如像Web Service、Hessian、gRPC等都可以用来充当RPC使用。这些不同的RPC框架都是随着互联网技术的发展而慢慢涌现出来的而这些RPC框架可能在不同时期会被我们引入到不同的项目中解决当时应用之间的通信问题这样就导致我们线上的生成环境中存在各种各样的RPC框架。
很显然这种混乱使用RPC框架的方式肯定不利于公司技术栈的管理最明显的一个特点就是我们维护RPC框架的成本越来越高因为每种RPC框架都需要有专人去负责升级维护。
为了解决早期遗留的一些技术负债我们通常会去选择更高级的、更好用的工具来解决治理RPC框架混乱的问题也是一样。为了解决同时维护多个RPC框架的困难我们肯定希望能够用统一用一种RPC框架来替代线上所有的RPC框架这样不仅能降低我们的维护成本而且还可以让我们在一种RPC上面去精进。
**既然目标明确后,我们该如何实施呢?**
可能你会说这很简单啊我们只要把所有的应用都改造成新RPC的使用方式然后同时上线所有改造后的应用就可以了。如果在团队比较小的情况下这种断崖式的更新可能确实是最快的方法但如果是在团队比较大的情况下要想做到同时上线所有改造后的应用暂且不讨论这种方式是否存在风险光从多个团队同一时间上线所有应用来看这也几乎是一件不可能做到的事儿。
那对于多人团队来说有什么办法可以让其把多个RPC框架统一到一个工具上呢我们先看下多人团队在升级过程中所要面临的困难人数多就意味着要维护的应用会比较多应用多了之后线上应用之间的调用关系就会相对比较复杂。那这时候如果单纯地把任意一个应用目前使用的RPC框架换成新的RPC框架的话就需要让所有调用这个应用的调用方去改成新的调用方式。
通过这种自下而上的滚动升级方式最终是可以让所有的应用都切换到统一的RPC框架上但是这种升级方式存在一定的局限性首先要求我们能够清楚地梳理出各个应用之间的调用关系只有这样我们才能按部就班地把所有应用都升级到新的RPC框架上其次要求应用之间的关系不能存在互相调用的情况最好的情况就是应用之间的调用关系像一颗树有一定的层次关系。但实际上我们应用的调用关系可能已经变成了网状结构这时候想再按照这种方式去推进升级的话就可能寸步难行了。
为了解决上面升级过程中遇到的问题你可能还会想到另外一个方案那就是在应用升级的过程中先不移除原有的RPC框架但同时接入新的RPC框架让两种RPC同时提供服务然后等所有的应用都接入完新的RPC以后再让所有的应用逐步接入到新的RPC上。这样既解决了上面存在的问题同时也可以让所有的应用都能无序地升级到统一的RPC框架上。
在保持原有RPC使用方式不变的情况下同时引入新的RPC框架的思路是可以让所有的应用最终都能升级到我们想要升级的RPC上但对于开发人员来说这样切换成本还是有点儿高整个过程最少需要两次上线才能彻底地把应用里面的旧RPC都切换成新RPC。
那有没有更好的方式可以让应用上线一次就可以完成新老RPC的切换呢关键就在于要让新的RPC能同时支持多种RPC调用当一个调用方切换到新的RPC之后调用方和服务提供方之间就可以用新的协议完成调用当调用方还是用老的RPC进行调用的话调用方和服务提供方之间就继续沿用老的协议完成调用。对于服务提供方来说所要处理的请求关系如下图所示
<img src="https://static001.geekbang.org/resource/image/c6/87/c6e87eea6d8f312e949af71b3e1eea87.jpg" alt="" title="调用关系">
## 怎么优雅处理多协议?
要让新的RPC同时支持多种RPC调用关键就在于要让新的RPC能够原地支持多种协议的请求。怎么才能做到在[[第 02 讲]](https://time.geekbang.org/column/article/199651) 我们说过协议的作用就是用于分割二进制数据流。每种协议约定的数据包格式是不一样的而且每种协议开头都有一个协议编码我们一般叫做magic number。
当RPC收到了数据包后我们可以先解析出magic number来。获取到magic number后我们就很容易地找到对应协议的数据格式然后用对应协议的数据格式去解析收到的二进制数据包。
协议解析过程就是把一连串的二进制数据变成一个RPC内部对象但这个对象一般是跟协议相关的所以为了能让RPC内部处理起来更加方便我们一般都会把这个协议相关的对象转成一个跟协议无关的RPC对象。这是因为在RPC流程中当服务提供方收到反序列化后的请求的时候我们需要根据当前请求的参数找到对应接口的实现类去完成真正的方法调用。如果这个请求参数是跟协议相关的话那后续RPC的整个处理逻辑就会变得很复杂。
当完成了真正的方法调用以后RPC返回的也是一个跟协议无关的通用对象所以在真正往调用方写回数据的时候我们同样需要完成一个对象转换的逻辑只不过这时候是把通用对象转成协议相关的对象。
在收发数据包的时候我们通过两次转换实现RPC内部的处理逻辑跟协议无关同时保证调用方收到的数据格式跟调用请求过来的数据格式是一样的。整个流程如下图所示
<img src="https://static001.geekbang.org/resource/image/43/37/43451aea86fef673c3928230191fac37.jpg" alt="" title="多协议处理流程">
## 总结
在我们日常开发的过程中最难的环节不是从0到1完成一个新应用的开发而是把一个老应用通过架构升级完成从70分到80分的跳跃。因为在老应用升级的过程中我们不仅需要考虑既有的功能逻辑也需要考虑切换到新架构上的成本这就要求我们在设计新架构的时候要考虑如何让老应用能够平滑地升级就像在RPC里面支持多协议一样。
在RPC里面支持多协议不仅能让我们更从容地推进应用RPC的升级还能为未来在RPC里面扩展新协议奠定一个良好的基础。所以我们平时在设计应用架构的时候不仅要考虑应用自身功能的完整性还需要考虑应用的可运维性以及是否能平滑升级等一些软性能力。
## 课后思考
在RPC里面支持多协议的时候有一个关键点就是能够识别出不同的协议并且根据不同的magic number找到不同协议的解析逻辑。如果线上协议存在很多种的话就需要我们事先在RPC里面内置各种协议但通过枚举的方式可能会遗漏不知道针对这种问题你有什么好的办法吗
欢迎留言和我分享你的答案,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!