RPC框架:10万QPS下如何实现毫秒级的服务调用?

  • Post author:
  • Post category:其他


RPC框架:10万QPS下如何实现毫秒级的服务调用?

我们已经决定对系统做服务化拆分,以便解决扩展性和研发成本高的问题。与此同时,我们在不断学习的过程中还发现,系统做了服务化拆分 之后,会引入一些新的问题,这些问题我在上节课提到过,归纳起来主要是两点:

  • 服务拆分单独部署后,引入的服务跨网络通信的问题;
  • 在拆分成多个小服务之后,服务如何治理的问题;

如果想要解决这两方面问题,就需要了解,微服务化所需要的中间件的基本原理,和使用技巧,我们先解决第一点问题的核心组件:RPC 框架。

来思考这样一个场景:我们的系统的 QPS 已经达到了每秒 2 万次,在做了服务化拆 分之后,由于我们把业务逻辑,都拆分到了单独部署的服务中,那么假设你在完成一次完整 的请求时,需要调用 4~5 次服务,计算下来,RPC 服务需要承载大概每秒 10 万次的请 求。那么,你该如何设计 RPC 框架,来承载如此大的请求量呢?我们要做的是:

  • 选择合适的网络模型,有针对性地调整网络参数,以优化网络传输性能;
  • 选择合适的序列化方式,以提升封包、解包的性能;



RPC

说到 RPC(Remote Procedure Call,远程过程调用),我们并不陌生,它指的是通过网络,调用另一台计算机上部署服务的技术。

而 RPC 框架就封装了网络调用的细节,让你像调用本地服务一样,调用远程部署的服务。 也许我们觉得只有像 Dubbo、Grpc、Thrift 这些新兴的框架才算是 RPC 框架,其实严格来 说,很早之前就接触到与 RPC 相关的技术了。

RPC 并不是互联网时代的产物,也不是服务化之后才衍生出来的技术,而是一种规范,只要是封装了网络调用的细节,能够实现远程调用其他服务,就可以算作是一种 RPC 技术了。

那么我们的垂直电商项目在使用 RPC 框架之后,会产生什么变化呢?

在性能上的变化是不可忽视的,举个例子。 比方说,你的电商系统中, 商品详情页面需要商品数据、评论数据还有店铺数据,如果在一体化的架构中,你需要从商品库,评论库和店铺库获取数据就可以了,不考虑缓存的情况下有三次网络请求。

但是,如果独立出商品服务、评论服务和店铺服务之后,那么就需要分别调用这三个服务,而这三个服务又会分别调用各自的数据库,这就是六次网络请求。如果服务拆分的更细粒度,那么多出的网络调用就会越多,请求的延迟就会更长,而这就是我们为了提升系统的扩展性,在性能上所付出的代价。

在这里插入图片描述

那么,我们要如果优化 RPC 的性能,从而尽量减少网络调用,对于性能的影响呢?在这 里,首先需要了解一次 RPC 的调用都经过了哪些步骤,因为这样,才可以针对这些步骤中可能存在的性能瓶颈点提出优化方案。步骤如下:

  • 在一次 RPC 调用过程中,客户端首先会将调用的类名、方法名、参数名、参数值等信 息,序列化成二进制流;
  • 然后客户端将二进制流,通过网络发送给服务端;
  • 服务端接收到二进制流之后,将它反序列化,得到需要调用的类名、方法名、参数名和参数值,再通过动态代理的方式,调用对应的方法得到返回值;
  • 服务端将返回值序列化,再通过网络发送给客户端;
  • 客户端对结果反序列化之后,就可以得到调用的结果了;


过程图如下:


在这里插入图片描述

从这张图中可以看到,有网络传输的过程,也有将请求序列化和反序列化的过程, 所 以,如果要提升 RPC 框架的性能,需要从网络传输和序列化两方面来优化。



如何提升网络传输性能

在网络传输优化中,首要做的,是选择一种高性能的 I/O 模型。所谓 I/O 模型,就是我们处理 I/O 的方式。而一般单次 I/O 请求会分为两个阶段,每个阶段对于 I/O 的处理方式 是不同的。


首先,I/O 会经历一个等待资源的阶段

,比方说,等待网络传输数据可用,在这个过程中我 们对 I/O 会有两种处理方式:

  • 阻塞。指的是在数据不可用时,I/O 请求一直阻塞,直到数据返回;
  • 非阻塞。指的是数据不可用时,I/O 请求立即返回,直到被通知资源可用为止;


然后是使用资源的阶段

,比如说从网络上接收到数据,并且拷贝到应用程序的缓冲区里面。 在这个阶段我们也会有两种处理方式:

  • 同步处理。指的是 I/O 请求在读取或者写入数据时会阻塞,直到读取或者写入数据完成;
  • 异步处理。指的是 I/O 请求在读取或者写入数据时立即返回,当操作系统处理完成 I/O 请求,并且将数据拷贝到用户提供的缓冲区后,再通知应用 I/O 请求执行完成。

将这两个阶段的四种处理方式,做一些排列组合,再做一些补充,就得到了我们常见的五种 I/O 模型:

  • 同步阻塞 I/O
  • 同步非阻塞 I/O
  • 同步多路 I/O 复用
  • 信号驱动 I/O
  • 异步 I/O

这五种 I/O 模型,我们需要理解它们的区别和特点,不过在理解上可能会有些难度,所以来做个比喻,方便我们理解。

我们来把 I/O 过程比喻成烧水倒水的过程,等待资源(就是烧水的过程),使用资源(就 是倒水的过程):

  • 如果我们站在烧水壶旁一直等着(等待资源)水烧开,然后倒水(使用资源),那么就是同步阻塞 I/O;
  • 如果我们想偷点儿懒,在烧水的时候躺在沙发上看会儿电视(不再时时刻刻等待资源),但是还是要时不时的去看看水开了没有,一旦水开了,马上去倒水(使用资源),那么这就是同步非阻塞 I/O;
  • 如果我们想要洗澡,需要同时烧好多壶水,那我们就在看电视的间隙去看看哪壶水开了(等待多个资源),哪一壶开了就先倒哪一壶,这样就加快了烧水的速度,这就是同步多路 I/O 复用;
  • 不过我们发现自己总是跑去看水开了没,太累了,于是就考虑给我们的水壶加一个报警器(信号),只要水开了就马上去倒水,这就是信号驱动 I/O;
  • 最后一种就高级了,我们发明了一个智能水壶,在水烧好后自动就可以把水倒好,这就是异步 I/O。

这五种 I/O 模型中最被广泛使用的是多路 I/O 复用,Linux 系统中的 select、epoll 等系统 调用都是支持多路 I/O 复用模型的,Java 中的高性能网络框架 Netty 默认也是使用这种模 型。所以,我们可以选择它。

那么,选择好了一种高性能的 I/O 模型,是不是就能实现,数据在网络上的高效传输呢??其实并没有那么简单,网络性能的调优涉及很多方面,其中不可忽视的一项就是网络参数的调优。



选择合适的序列化方式

在对网络数据传输完成调优之后,另外一个需要关注的点就是,

数据的序列化和反序列化

。 通常所说的序列化,是将传输对象转换成二进制串的过程,而反序列化则是相反的动作,是 将二进制串转换成对象的过程。

从上面的 RPC 调用过程中你可以看到,一次 RPC 调用需要经历两次数据序列化的过程, 和两次数据反序列化的过程,可见它们对于 RPC 的性能影响是很大的,那么我们在选择序 列化方式的时候需要考虑哪些因素呢?

首先需要考虑的肯定是性能嘛,性能包括时间上的开销和空间上的开销,时间上的开销就是序列化和反序列化的速度,这是显而易见需要重点考虑的,而空间上的开销则是序列化后的二进制串的大小,过大的二进制串也会占据传输带宽,影响传输效率。

除去性能之外,我们需要考虑的是它是否可以跨语言,跨平台,这一点也非常重要,因为一 般的公司的技术体系都不是单一的,使用的语言也不是单一的,那么如果选择的 RPC 框架中传输的数据只能被一种语言解析,那么这无疑限制了框架的使用。

另外,扩展性也是一个需要考虑的重点问题。你想想,如果对象增加了一个字段就会造成传输协议的不兼容,导致服务调用失败,这会是多么可怕的事情。

综合上面的几个考虑点,在我看来,我们的序列化备选方案主要有以下几种:

  • 首先是大家熟知的 JSON,它起源于 JavaScript,是一种最广泛使用的序列化协议,它的优势简单易用,人言可读,同时在性能上相比 XML 有比较大的优势。
  • 另外的 Thrift 和 Protobuf 都是需要引入 IDL(Interface description language)的, 就是需要按照约定的语法写一个 IDL 文件,然后通过特定的编译器将它转换成各语言对应 的代码,从而实现跨语言的特点。

Thrift 是 Facebook 开源的高性能的序列化协议,也是一个轻量级的 RPC 框架; Protobuf 是谷歌开源的序列化协议。它们的共同特点是,无论在空间上还是时间上都有着很高的性能,缺点就是由于 IDL 存在带来一些使用上的不方便。

那么,要如何选择这几种序列化协议呢?这里有几点建议:

  • 如果对于性能要求不高,在传输数据占用带宽不大的场景下,可以使用 JSON 作为序列化协议;
  • 如果对于性能要求比较高,那么使用 Thrift 或者 Protobuf 都可以。而 Thrift 提供了配 套的 RPC 框架,所以想要一体化的解决方案,你可以优先考虑 Thrift;
  • 在一些存储的场景下,比如说你的缓存中存储的数据占用空间较大,那么你可以考虑使用 Protobuf 替换JSON,作为存储数据的序列化方式。



总结:

为了优化 RPC 框架的性能,我们了解了网络 I/O 模型和序列化方式的选择,它 们是实现高并发 RPC 框架的要素,总结起来有三个要点:

  1. 选择高性能的 I/O 模型,这里我推荐使用同步多路 I/O 复用模型;
  2. 调试网络参数,这里面有一些经验值的推荐。比如将 tcp_nodelay 设置为 true,也有一 些参数需要在运行中来调试,比如接受缓冲区和发送缓冲区的大小,客户端连接请求缓冲队 列的大小(back log)等等;
  3. 序列化协议依据具体业务来选择。如果对性能要求不高,可以选择 JSON,否则可以从 Thrift 和 Protobuf 中选择其一。



版权声明:本文为zhanglu0302原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。