本项目不是生产环境可用!
本项目是一个点对点rpc的demo,尽可能的进行性能优化,然后和rocketmq remoting进行性能对比。希望通过这个简单模型的分析,得出rocketmq remoting的性能改进方向。
本项目相对于rocketmq remoting有3点改进。
- 尽可能减少对象创建、数组复制、线程切换。可以看到,在批量关闭的情况下,大部分场景对比rocketmq remoting都有少量提升。
- 网络IO批量写,包括客户端和服务端都有批量。服务端目前有AUTO、ENABLE、DISABLE 3种模式。异步场景下TPS提升了十多倍。
- 优化了异步背压能力。关于异步背压见下面专门的段落说明。
rocketmq批量操作可以在3个层面进行:业务用户层面、消息处理层面、网络层面。这3种方式各有优劣。让用户来处理(业务用户层面)效果是最好的,但是使用复杂度就上升了,有些业务场景下甚至办不到。
本项目在网络IO层面进行自动批量处理,具体的做法类似TCP里面的Nagle算法,数据包来了以后,先不要写IO,等1ms,看看有没有后续的包可以一起批量写出去。这样做有时候提升了吞吐,但缺点也和Nagle类似:在某些场景下会大幅降低性能。 对于这个项目,具体是单线程(或少量线程)同步发送的场景。试想,如果客户端是单线程同步调用的,要拿到结果才会进行下一次调用,在服务器端白白多等了1ms(但是又等不到任何后续的数据),所以理论上tps就不会超过1000了。 于是这个批量的功能是不能默认打开的,让用户来设置是否打开这个功能也比较难,大部分用户很难说清楚是否应该打开,为了保险起见就只能关闭它,那就没有意义了。
面对这样的问题,本项目的改进方案是提供一个AUTO模式,程序按TCP连接统计近期的数据,根据一个算法来推测这个连接打开批量是否划算,在运行过程中会不断调整。 最终的效果可以看下面的测试结果表格,可以看到,AUTO模式的吞吐量在单线程同步发送时,和DISABLE模式接近,而在异步发送时,和ENABLE模式接近。
运行NettyClientBenchmark和RmqBenchmark的main方法可以得到测试结果。测试在Mac上运行,测试时间均为10秒。 Simple RPC客户端自动批量阈值为200(所以同步发送的时候,线程数是否大于200,程序的逻辑会有差异)。
客户端128线程
rpc框架 | 调用方式 | 参数 | 服务端批量模式 | 结果(TPS) |
---|---|---|---|---|
RMQ Remoting | 同步调用 | 默认 | 101,374 | |
RMQ Remoting | 异步调用 | 默认 | 101,997 | |
Simple RPC | 同步调用 | 默认 | 自动 | 118,032 |
Simple RPC | 同步调用 | 默认 | 开启 | 121,644 |
Simple RPC | 同步调用 | 默认 | 关闭 | 103,662 |
Simple RPC | 异步调用 | 默认 | 自动 | 1,132,824 |
Simple RPC | 异步调用 | 默认 | 开启 | 1,176,219 |
Simple RPC | 异步调用 | 默认 | 关闭 | 107,453 |
Simple RPC | 异步调用 | 使用IO线程处理请求 | 开启 | 1,343,342 |
客户端256线程
rpc框架 | 调用方式 | 参数 | 服务端批量模式 | 结果(TPS) |
---|---|---|---|---|
RMQ Remoting | 同步调用 | 默认 | 108,762 | |
RMQ Remoting | 异步调用 | 默认 | 96,250 | |
Simple RPC | 同步调用 | 默认 | 自动 | 241,368 |
Simple RPC | 同步调用 | 默认 | 开启 | 242,345 |
Simple RPC | 同步调用 | 默认 | 关闭 | 97,612 |
Simple RPC | 异步调用 | 默认 | 自动 | 1,188,924 |
Simple RPC | 异步调用 | 默认 | 开启 | 1,171,117 |
Simple RPC | 异步调用 | 默认 | 关闭 | 103,415 |
客户端32线程
rpc框架 | 调用方式 | 参数 | 服务端批量模式 | 结果(TPS) |
---|---|---|---|---|
RMQ Remoting | 同步调用 | 默认 | 84,892 | |
RMQ Remoting | 异步调用 | 默认 | 104,381 | |
Simple RPC | 同步调用 | 默认 | 自动 | 92,168 |
Simple RPC | 同步调用 | 默认 | 开启 | 22,147 |
Simple RPC | 同步调用 | 默认 | 关闭 | 94,399 |
Simple RPC | 异步调用 | 默认 | 自动 | 1,155,895 |
Simple RPC | 异步调用 | 默认 | 开启 | 1,179,657 |
Simple RPC | 异步调用 | 默认 | 关闭 | 108,141 |
客户端1线程
rpc框架 | 调用方式 | 参数 | 服务端批量模式 | 结果(TPS) |
---|---|---|---|---|
RMQ Remoting | 同步调用 | 默认 | 11,192 | |
RMQ Remoting | 异步调用 | 默认 | 114,825 | |
Simple RPC | 同步调用 | 默认 | 自动 | 13,240 |
Simple RPC | 同步调用 | 默认 | 开启 | 668 |
Simple RPC | 同步调用 | 默认 | 关闭 | 13,520 |
Simple RPC | 异步调用 | 默认 | 自动 | 1,246,949 |
Simple RPC | 异步调用 | 默认 | 开启 | 1,320,533 |
Simple RPC | 异步调用 | 默认 | 关闭 | 111,999 |
Simple RPC | 异步调用 | 使用IO线程处理请求 | 开启 | 1,528,428 |
以上测试为了方便,是在Mac本机运行的。 也在Linux上进行了测试(使用ServerStarter和ClientStarter),客户端和服务端分别位于两台不同的服务器,不调整任何参数的情况下tps是80万,调整一下参数可以上百万。
当rpc客户端(消息生产方)的异步调用速率大于响应速率(同时受服务器处理速率和网络速率的影响)的时候,客户端降低调用速率,使得tps自动适应最大响应速率,而不至于失败,这就是异步背压。
一个典型的例子就是TCP的流量控制,它会协商窗口的大小,控制客户端的发送速率,不至于淹没服务端,对上面的应用而言,TCP协议提供了可靠的服务。 但TCP的客户端和服务端是一对一的,消息服务器的客户端和服务器端是多对多的关系,这使得窗口协商非常困难。
我们无法通过简单的方式提供一个绝对可靠的异步背压,但是可以通过一些手段和配置,使得大部分情况下大tps异步调用不至于失败。 可以用本项目做一个实验,在服务端处理RPC请求的时候sleep1毫秒,在这种情况下,rocketmq remoting异步调用会大量失败,而simple rpc则可以全部成功。
Simple RPC具有基本异步背压能力,核心要点是:
- 通过semaphore来控制pending(已经发出但是没有收到响应)的请求数量,如果无法从semaphore获取到permit,即使是异步调用也应该堵塞,等待permit的释放。
- 客户端buffer(semaphore)要小于服务端buffer。
- 客户端异步发送超时时间要足够长(要大于服务端处理客户端所有pending数据的时间)。
要点2举例:如果客户端允许发送出去1000个请求,客户端在1ms内发出1000个请求,从1001个请求开始,由于无法获取到semaphore而堵塞等待; 而服务端的处理请求的Executor有10个线程(假设处理需要5ms),队列100,最多hold住110个请求,那么客户端发送过来的另外890个请求都无法进入Executor而快速失败, 快速失败的响应到达客户端以后会释放890个semaphore的permit,堵塞的异步发送程序(第1001个请求)开始得到permit继续发送,然后继续失败。最后的结果是,大部分的请求都是失败的。
要点3举例:如果客户端semaphore为1000,服务端处理能力为500tps,那么超时时间应该至少大于2秒。因为异步操作是几乎不需要时间的,这1000个请求马上会送出去,然后异步发送的程序由于获取不到permit陷入堵塞。 按服务器的处理能力,最后一个发送的请求要在2秒以后才能返回,所以超时时间至少要有2秒。
具体到rocketmq,一个异步发送的程序可能因为以下原因失败:
- DefaultMQProduerImpl.send方法提交异步发送任务进线程池,会被拒绝(默认队列50000)。
- 获取semaphoreAsync超时(默认65535个),此处获取semaphore有等待,用的是tryAcquire(long timeout, TimeUnit unit)
- 服务端待处理消息队列满(默认队列10000)
- 客户端超时(默认3000ms)
- 服务端超时(受brokerFastFailureEnable、osPageCacheBusyTimeOutMills控制,默认1000ms)
- 其它跨线程处理时,由于线程池处理队列已满,而被拒绝。比如netty不同的event loop之间(视具体配置,我没有细看)
有以下问题:
- 上面原因1的Executor不利于背压堵塞;
- 上面原因1的50000小于2的65535,结果是semaphore还有permit,但是1的Executor已经放不进去了;
- 客户端buffer明显大于服务端;
- 服务端超时1000ms小于客户端3000ms,客户端还愿意等,但服务端就清理掉了,不过这个osPageCacheBusy一般不容易触发,所以不常见;
- simple rpc需要更好的AUTO算法,更好的适应各种工况
- 当前的AutoBatchWriteHandler不能很好的向write操作的future的listener发出通知。
- simple rpc服务端和客户端最好具备简单的窗口机制。
- rocketmq测试的过程中多次出现"createChannel: connect remote host[127.0.0.1:8888] success, AbstractBootstrap$PendingRegistrationPromise@4f40e8cb(success)",说明rocketmq在这里有一点并发问题
- 自动批量对于同步调用提升不那么大(除非线程很多),而rocketmq异步调用在大tps场景异步调用容易出错,需要做适当的改造以具备基本背压能力。