mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2026-05-11 04:04:34 +08:00
del
This commit is contained in:
111
极客时间专栏/geek/RPC实战与核心原理/高级篇/17 | 异步RPC:压榨单机吞吐量.md
Normal file
111
极客时间专栏/geek/RPC实战与核心原理/高级篇/17 | 异步RPC:压榨单机吞吐量.md
Normal 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框架的异步策略?
|
||||
|
||||
欢迎留言分享你的答案,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!
|
||||
79
极客时间专栏/geek/RPC实战与核心原理/高级篇/18 | 安全体系:如何建立可靠的安全体系?.md
Normal file
79
极客时间专栏/geek/RPC实战与核心原理/高级篇/18 | 安全体系:如何建立可靠的安全体系?.md
Normal 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接口定义里面一般会包含多个方法,但我们目前只是解决了你能不能调用接口的问题,并没有解决你能调用我接口里面的哪些方法。像这种问题,你有什么好方案吗?
|
||||
|
||||
欢迎留言和我分享你的答案,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!
|
||||
103
极客时间专栏/geek/RPC实战与核心原理/高级篇/19 | 分布式环境下如何快速定位问题?.md
Normal file
103
极客时间专栏/geek/RPC实战与核心原理/高级篇/19 | 分布式环境下如何快速定位问题?.md
Normal 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->B->C->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->B->C->D这个过程中,我们就可以很快地定位到是C服务出现了问题,服务接口是com.demo.CSerivce,调用端IP是192.168.1.2,服务端IP是192.168.1.3,而出现问题的原因就是业务线程池满了。
|
||||
|
||||
由此可见,一款优秀的RPC框架要对异常进行详细地封装,还要对各类异常进行分类,每类异常都要有明确的异常标识码,并整理成一份简明的文档。使用方可以快速地通过异常标识码在文档中查阅,从而快速定位问题,找到原因;并且异常信息中要包含排查问题时所需要的重要信息,比如服务接口名、服务分组、调用端与服务端的IP,以及产生异常的原因。总之就是,要让使用方在复杂的分布式应用系统中,根据异常信息快速地定位到问题。
|
||||
|
||||
以上是对于RPC框架本身的异常来说的,比如序列化异常、响应超时异常、连接异常等等。那服务端业务逻辑的异常呢?服务提供方提供的服务的业务逻辑也要封装自己的业务异常信息,从而让服务调用方也可以通过异常信息快速地定位到问题。
|
||||
|
||||
### 方法2:借助分布式链路跟踪
|
||||
|
||||
无论是RPC框架本身,还是服务提供方提供的服务,只要对异常信息进行合理地封装,就可以让我们在分布式环境下定位问题变得更加容易。那这样是不是就满足我们定位问题的需求了呢?
|
||||
|
||||
我们还是回到前面提过的那个分布式场景:我们搭建了一个分布式的应用系统,它由4个子服务组成,4个服务的依赖关系为A->B->C->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->B->C->D的情况下,在整个调用链中,正常情况下会产生3个Span,分别是Span1(A->B)、Span2(B->C)、Span3(C->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,以及产生异常的原因等信息,这样对于使用方来说就非常便捷了。
|
||||
|
||||
另外,服务提供方在提供服务时也要对异常进行封装,以方便上游排查问题。
|
||||
|
||||
在分布式环境下,我们可以通过分布式链路跟踪来快速定位问题,尤其是在多个部门的合作中,这样做可以一步到位,减少排查问题的时间,降低沟通成本,以最高的效率解决实际问题。
|
||||
|
||||
## 课后思考
|
||||
|
||||
在分布式环境下,你还知道哪些快速定位问题的方法?
|
||||
|
||||
期待你能在留言区中和我分享,也欢迎你把文章分享给你的朋友,邀请他加入学习,共同探讨。我们下节课再见!
|
||||
102
极客时间专栏/geek/RPC实战与核心原理/高级篇/20 | 详解时钟轮在RPC中的应用.md
Normal file
102
极客时间专栏/geek/RPC实战与核心原理/高级篇/20 | 详解时钟轮在RPC中的应用.md
Normal 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个任务,分别是任务A(90毫秒之后执行)、任务B(610毫秒之后执行)与任务C(1秒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框架中,除了我说过的那几个例子,你还知道有哪些功能的实现可以应用到时钟轮?
|
||||
|
||||
欢迎留言和我分享你的答案,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!
|
||||
57
极客时间专栏/geek/RPC实战与核心原理/高级篇/21 | 流量回放:保障业务技术升级的神器.md
Normal file
57
极客时间专栏/geek/RPC实战与核心原理/高级篇/21 | 流量回放:保障业务技术升级的神器.md
Normal 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的人来说,他们有了这个功能之后,就可以更放心地升级自己的应用了。
|
||||
|
||||
## 课后思考
|
||||
|
||||
除了上面我提到的可以使用流量回放功能来验证改造后的应用逻辑,我们还可以用流量回放来做哪些有意义的事儿?
|
||||
|
||||
欢迎留言和我分享你的思考,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!
|
||||
63
极客时间专栏/geek/RPC实战与核心原理/高级篇/22 | 动态分组:超高效实现秒级扩缩容.md
Normal file
63
极客时间专栏/geek/RPC实战与核心原理/高级篇/22 | 动态分组:超高效实现秒级扩缩容.md
Normal 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),我们讲了分组后带来的收益,它可以帮助服务提供方实现调用方的隔离。但是因为调用方流量并不是一成不变的,而且还可能会因为突发事件导致某个分组的流量溢出,而在整个大集群还有富余能力的时候,又因为分组隔离不能为出问题的集群提供帮助。
|
||||
|
||||
为了解决这种突发流量的问题,我们提供了一种更高效的方案,可以实现分组的快速扩缩容。事实上我们还可以利用动态分组解决分组后给每个分组预留机器冗余的问题,我们没有必要把所有冗余的机器都分配到分组里面,我们可以把这些预留的机器做成一个共享的池子,从而减少整体预留的实例数量。
|
||||
|
||||
## 课后思考
|
||||
|
||||
在服务治理的过程中,我们通常会给服务进行逻辑分组,但之后某个分组可能会遇到突发流量调用的问题,在本讲我给出了一个动态分组的方案。但是动态分组的过程中,我们只是把注册中心的数据改了,而服务提供方提供真实的分组名并没有改变,这时候用动态分组名的调用方调用过来的请求可能就会报错,因为服务提供方会验证调用方过来的分组名跟自身的是否一样。针对这个问题,你能想到什么解决方案?
|
||||
|
||||
欢迎留言和我分享你的答案,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!
|
||||
89
极客时间专栏/geek/RPC实战与核心原理/高级篇/23 | 如何在没有接口的情况下进行RPC调用?.md
Normal file
89
极客时间专栏/geek/RPC实战与核心原理/高级篇/23 | 如何在没有接口的情况下进行RPC调用?.md
Normal 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,方法的返回值就是CompletableFuture,GenericService接口的具体定义如下:
|
||||
|
||||
```
|
||||
class GenericService {
|
||||
|
||||
Object $invoke(String methodName, String[] paramTypes, Object[] params);
|
||||
|
||||
CompletableFuture<Object> $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,不能正常地进行序列化与反序列化,我们可以为泛化调用提供专属的序列化插件,来解决实际问题。
|
||||
|
||||
## 课后思考
|
||||
|
||||
在讲解泛化调用时,我讲到服务端在收到调用端通过泛化调用的方式发送过来的请求时,会使用泛化调用专属的序列化插件实现对其进行反序列化,那么服务端是如何判定这个请求消息是通过泛化调用的方式发送过来的消息呢?
|
||||
|
||||
欢迎留言和我分享你的答案,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!
|
||||
57
极客时间专栏/geek/RPC实战与核心原理/高级篇/24 | 如何在线上环境里兼容多种RPC协议?.md
Normal file
57
极客时间专栏/geek/RPC实战与核心原理/高级篇/24 | 如何在线上环境里兼容多种RPC协议?.md
Normal 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里面内置各种协议,但通过枚举的方式可能会遗漏,不知道针对这种问题你有什么好的办法吗?
|
||||
|
||||
欢迎留言和我分享你的答案,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!
|
||||
Reference in New Issue
Block a user