为啥需要更换 RPC?
很多小伙伴都遇到过需要为分布式系统调用更换 RPC 的问题, 为什么会遇到这种事呢? 其实, 在系统搭建初期, 需求简单, 架构简单, 最重要的是请求量也少, 所以很多系统都采用快速原型开发模式, 对 rpc 的要求不高, 随便找一个顺手的或者熟悉的 rpc 框架套进系统中即可. 但是随着业务复杂度增高, 系统承载的请求量增高, 可能一开始所采用的 RPC 框架显现出一些致命的问题, 比如大扇出问题. 我们以 Thrift 为例. 例如随着业务复杂度的增长, 我们面临着如下的需求.
如图所示, 每一次请求, 上游服务都要获取下游 A~Z 一共 26 个服务的结果, 然后把这 26 个服务的结果拼装返回给前端服务. 有人说, 26 个服务是不是有些夸张了, 我的系统中根本没有遇到过这个情况. 这实际一点不夸张, 一个业务复杂的系统经过服务拆分, 最后拆成一些高内聚低耦合的独立服务, 非常容易达到这样一个服务种类数, 而且 26 还远远不是很多. 那么遇到这种问题, 传统的同步的 RPC 怎么解决这个问题呢?
以 Thrift 为例, 如果需要访问 26 个服务, 为了保证请求处理速度, 必须要并行访问各个下游服务(不能串行请求, 因为这将导致 一次请求的响应时间至少为 timeA + timeB + ...... + timeZ), 那么我们只能通过多线程进行并发.
通过多线程并发请求, 我们基本能够达到处理一次请求至多需要 max(timeA, timeB, ......, timeZ), 但是实际上要比这个稍多. 看样子我们必须弄一个请求线程池, 可是这个池子要多大呢? 假如现在前端请求速率为 P, 那么为了保证每个请求处理时间都尽可能快, 我们需要一个大小为 26 * P 的线程池. 虽然, 初看起来可能还可以应付, 毕竟请求线程在发送网络请求后, 会阻塞在 IO, 它会放弃 CPU, 从而使得计算线程获得 CPU, 不会浪费多少 CPU 的资源, 但是当 P 太大就不好了. 比如 P 为 100 或者 1000, 这个时候线程数过多可能就会造成 CPU 调度开销增大, 因为它会增加 CPU 的线程切换负担.
所以, 我们更换 RPC, 当且仅当, 当前的 RPC 已经造成了系统负担, 对于业务量不大的系统, RPC 的更换并没有必要, 但是为了技术提升你也可以更换 RPC, 只不过收益可能不大.
需要什么样的 RPC?
考虑到 Thrift 对于大扇出并不合适, 我们可能需要下面这样工作模式的 RPC.
这种反应器模型 (只是简单举例子) 可以减少请求线程数. 这种 RPC 使用系统的 Epoll 进行后端服务的请求以及数据的接收, 这样无论多少请求, 只使用一个线程完成, 通过 Epoll 的机制在数据到来或者可发送的情况下通知用户进程, 只不过最后需要把接收到的数据返回给计算线程使用. 这种模型其实要比 Thrift 那种那好一些. 我自己也在业余时间实现了一个简单的 RPC 框架: http://www.cnblogs.com/haolujun/p/7527313.html , 比较粗糙但是足够小.
还有有很多开源的 RPC 框架, fbthrift,GRPC 都可以应对大扇出, 找到适合你的系统, 并且改动量和后期维护成本最低的那个.
如何迁移到新的 RPC?
把系统迁移到新的 RPC 上, 除了改动代码外, 就是要做到兼容, 系统在迁移过程中可能需要在两套 RPC 框架上运行, 并且必须做到平滑迁移. 例如, 一般的分布式系统可能会长成如下的样子.
服务 B1~B4 把自己的地址写入到 ETCD 中, 但是由于我们一开始并未考虑到 RPC 的迁移, 所以 value 对应的是服务的地址, 没有服务使用的 rpc 类型等等.
方案 1 添加新 key
对于 A1~A2,B1~B4, 可以先选择一部分进行平滑过渡, 例如我们选择 A1,B1~B2 进行迁移.
上线步骤如下:
下线 A1,B1,B2.
更新 A1 配置, 使其从新的 key:service_new_rpc 中读取后端服务列表.
更新 B1,B2 配置, 使其在新的 key:service_new_rpc 中注册自己.
启动 B1,B2.
启动 A1.
对于 A2,B3,B4 重复如上步骤.
通过这种方式, 我们可以平滑的进行服务迁移. 但是它的缺点很明显, 需要一个新的 key, 而且后期还需要一点点把服务挪回到旧的 key 上.
方案 2 代码兼容
这个方案必须更改一些解析代码, 使其能够兼容新的 ETCD 中 value 的格式, 如下图.
首先改造 A 代码, 使其能够兼容新地址解析格式. 新地址格式在每个地址后加上 RPC 类型标识: T(Thrift),G(GRPC), 新格式和旧格式的兼容很容易, 只需在解析的时候找一下分割符, 并判断分隔符最后一部分是 T 是 G 还是什么都没有, 没有就默认为 T.
改造 A 代码, 使其能够根据后端服务在 ETCD 中的 RPC 类型使用不同的 RPC 框架调用后端.
改造 B1~B4 的配置, 在 ETCD 中注册自己的时候把 RPC 类型顺便加上.
改造 B1~B2, 使用新 RPC 作为服务端, 并且在注册的时候把 RPC 类型设置为 G.
改造 B3~B4, 使用新 RPC 作为服务端, 并且在注册的时候把 RPC 类型设置为 G.
通过这个步骤, 我们就能做到 RPC 的平滑迁移. 这个方式的缺点也有: 需要同时维护两套 RPC 框架, 直到其中一种 RPC 彻底下线. 但是优点也有, 没有增加新 key.
总结
更换 RPC 并不像想象中的那样困难, 只要理清前后逻辑, 一点点的迁移, 最终你的服务会全部搞定. 最重要的问题是你的系统真的达到了非得换 RPC 的地步了么?
来源: http://www.bubuko.com/infodetail-2574075.html