Tinyrpc
c++ async rpc framework. 14w+qps.
Install / Use
/learn @Gooddbird/TinyrpcREADME
作者:ikerli 2022-05-13 使用 TinyRPC, 轻松地构建高性能分布式 RPC 服务!
<!-- TOC -->- 1. 概述
- 2. 性能测试
- 3. 安装 TinyRPC
- 4. 快速上手
- 5. 概要设计
- 6. 错误码
- 7. 问题反馈
- 8. 参考资料
1. 概述
1.1. TinyRPC 特点
TinyRPC 是一款基于 C++11 标准开发的小型异步 RPC 框架。TinyRPC 的核心代码应该也就几千行样子,尽量保持了简洁且较高的易读性。
麻雀虽小五脏俱全,从命名上就能看出来,TinyRPC 框架主要用义是为了让读者能快速地、轻量化地搭建出具有较高性能的异步RPC 服务。至少用 TinyRPC 搭建的 RPC 服务能应付目前大多数场景了。
TinyRPC 没有实现跨平台,只支持 Linux 系统,并且必须是 64 位的系统,因为协程切换只实现了 64 位系统的代码,而没有兼容 32 位系统。这是有意的,因为作者只会 Linux 下开发,没能力做到跨平台。
TinyRPC 的核心思想有两个:
- 让搭建高性能 RPC 服务变得简单
- 让异步调用 RPC 变得简单
必须说明的是, TinyRPC 代码没有达到工业强度,最好不要直接用到生产环境,也可能存在一些未知 BUG,甚至 coredump。读者请自行辨别,谨慎使用!
1.2. TinyRPC 支持的协议报文
TinyRPC 框架目前支持两类协议:
- 纯 HTTP 协议: TinyRPC 实现了简单的很基本的 HTTP(1.1) 协议的编、解码,完全可以使用 HTTP 协议搭建一个 RPC 服务。
- TinyPB 协议: 一种基于 Protobuf 的自定义协议,属于二进制协议。
1.3. TinyRPC 的 RPC 调用
TinyRPC 是一款异步的 RPC 框架,这就意味着服务之前的调用是非常高效的。目前来说,TinyRPC 支持两种RPC 调用方式:阻塞协程式异步调用 和 非阻塞协程式异步调用。
1.3.1. 阻塞协程式异步调用
阻塞协程式异步调用这个名字看上去很奇怪,阻塞像是很低效的做法。然而其实他是非常高效的。他的思想是用同步的代码,实现异步的性能。 也就是说,TinyRPC 在 RPC 调用时候不需要像其他异步操作一样需要写复杂的回调函数,只需要直接调用即可。这看上去是同步的过程,实际上由于内部的协程封装实现了完全的异步。而作为外层的使用者完全不必关系这些琐碎的细节。
阻塞协程式异步调用对应 TinyPbRpcChannel 类,一个简单的调用例子如下:
tinyrpc::TinyPbRpcChannel channel(std::make_shared<tinyrpc::IPAddress>("127.0.0.1", 39999));
QueryService_Stub stub(&channel);
tinyrpc::TinyPbRpcController rpc_controller;
rpc_controller.SetTimeout(10000);
DebugLog << "RootHttpServlet begin to call RPC" << count;
stub.query_name(&rpc_controller, &rpc_req, &rpc_res, NULL);
DebugLog << "RootHttpServlet end to call RPC" << count;
这看上去跟普通的阻塞式调用没什么区别,然而实际上在 stub.query_name 这一行是完全异步的,简单来说。线程不会阻塞在这一行,而会转而去处理其他协程,只有当数据返回就绪时,query_name 函数自动返回,继续下面的操作。 这个过程的执行流如图所示:

从图中可以看出,在调用 query_name 到 query_name 返回这段时间 T,CPU 的执行权已经完全移交给主协程了,也就说是这段时间主协程可以用来做任何事情:包括响应客户端请求、执行定时任务、陷入 epoll_wait 等待事件就绪等。对单个协程来说,它的执行流被阻塞了。但对于整个线程来说是完全没有被阻塞,它始终在执行着任务。
另外这个过程完全没有注册回调函数、另起线程之类的操作,可它确确实实达到异步了。这也是 TinyRPC 的核心思想之一。
这种调用方式是 TinyRPC 推荐的方式,它的优点如下:
- 代码实现很简单,直接同步式调用,不需要写回调函数。
- 对IO线程数没有限制,即使只有 1 个 IO 线程,仍然能达到这种效果。
- 对于线程来说,他是不会阻塞线程的。
当然,它的缺点也存在:
- 对于当前协程来说,他是阻塞的,必须等待协程再次被唤醒(RESUME)才能执行下面的代码。
1.3.2. 非阻塞协程式异步调用
非阻塞协程式异步调用是 TinyRPC 支持的另一种 RPC 调用方式,它解决了阻塞协程式异步调用 的一些缺点,当然也同时引入了一些限制。这种方式有点类似于 C++11 的 future 特性, 但也不完全一样。
非阻塞协程式异步调用对应 TinyPbRpcAsyncChannel,一个简单调用例子如下:
{
std::shared_ptr<queryAgeReq> rpc_req = std::make_shared<queryAgeReq>();
std::shared_ptr<queryAgeRes> rpc_res = std::make_shared<queryAgeRes>();
AppDebugLog << "now to call QueryServer TinyRPC server to query who's id is " << req->m_query_maps["id"];
rpc_req->set_id(std::atoi(req->m_query_maps["id"].c_str()));
std::shared_ptr<tinyrpc::TinyPbRpcController> rpc_controller = std::make_shared<tinyrpc::TinyPbRpcController>();
rpc_controller->SetTimeout(10000);
tinyrpc::IPAddress::ptr addr = std::make_shared<tinyrpc::IPAddress>("127.0.0.1", 39999);
tinyrpc::TinyPbRpcAsyncChannel::ptr async_channel =
std::make_shared<tinyrpc::TinyPbRpcAsyncChannel>(addr);
async_channel->saveCallee(rpc_controller, rpc_req, rpc_res, nullptr);
QueryService_Stub stub(async_channel.get());
stub.query_age(rpc_controller.get(), rpc_req.get(), rpc_res.get(), NULL);
}
注意在这种调用方式中,query_age 会立马返回,协程 C1 可以继续执行下面的代码。但这并不代表着调用 RPC 完成,如果你需要获取调用结果,请使用:
async_channel->wait();
此时协程 C1 会阻塞直到异步 RPC 调用完成,注意只会阻塞当前协程 C1,而不是当前线程(其实调用 wait 后就相当于把当前协程 C1 Yiled 了,等待 RPC 完成后自动 Resume)。
当然,wait() 是可选的。如果你不关心调用结果,完全可以不调用 wait。即相当于一个异步的任务队列。
这种调用方式的原理很简单,会新生成一个协程 C2 去处理这次 RPC 调用,把这个协程 C2 加入调度池任务里面,而原来的协程 C1 可以继续往下执行。
新协程 C2 会在适当的时候被IO线程调度(可能是IO线程池里面任意一个 IO线程), 当 RPC 调用完成后,会唤醒原协程 C1 通知调用完成(前提是 C1 中调用了 wait 等待结果)。
这个调用链路如图:

总之,非阻塞协程式异步调用的优点如下:
- RPC 调用不阻塞当前协程 C1,C1 可以继续往下执行代码(若遇到 wait 则会阻塞)。
而缺点如下:
- 所有 RPC 调用相关的对象,必须是堆上的对象,而不是栈对象, 包括 req、res、controller、async_rpc_channel。强烈推荐使用 shared_ptr,否则可能会有意想不到的问题(基本是必须使用了)。
- 在 RPC 调用前必须调用 TinyPbRpcAsyncChannel::saveCallee(), 提前预留资源的引用计数。实际上是第1点的补充,相当于强制要求使用 shared_ptr 了。
解释一下第一点:调用相关的对象是在线程 A 中声明的,但由于是异步 RPC 调用,整个调用过程是又另外一个线程 B 执行的。因此你必须确保当线程 B 在这些 RPC 调用的时候,这些对象还存在,即没有被销毁。 那为什么不能是栈对象?想像一下,假设你在某个函数中异步调用 RPC,如果这些对象都是栈对象,那么当函数结束时这些栈对象自动被销毁了,线程 B 此时显然会 coredump 掉。因此请在堆上申请对象。另外,推荐使用 shared_ptr 是因为 TinyPbRpcAsyncChannel 内部已经封装好细节了,当异步 RPC 完成之后会自动销毁对象,你不必担心内存泄露的问题!
2. 性能测试
TinyRPC 底层使用的是 Reactor 架构,同时又结合了多线程,其性能是能得到保障的。进行几个简单的性能测试结果如下:
2.1. HTTP echo 测试 QPS
测试机配置信息:Centos虚拟机,内存6G,CPU为4核
测试工具:wrk: https://github.com/wg/wrk.git
部署信息:wrk 与 TinyRPC 服务部署在同一台虚拟机上, 关闭 TinyRPC 日志
测试命令:
// -c 为并发连接数,按照表格数据依次修改
wrk -c 1000 -t 8 -d 30 --latency 'http://127.0.0.1:19999/qps?id=1'
测试结果: | QPS | WRK 并发连接 1000 | WRK 并发连接 2000 | WRK 并发连接 5000 | WRK 并发连接 10000 | | ---- | ---- | ---- | ---- | ---- | | IO线程数为 1 | 27000 QPS | 26000 QPS | 20000 QPS |20000 QPS | | IO线程数为 4 | 140000 QPS | 130000 QPS | 123000 QPS| 118000 QPS | | IO线程数为 8 | 135000 QPS | 120000 QPS| 100000 QPS| 100000 QPS | | IO线程数为 16 | 125000 QPS | 127000 QPS |123000 QPS | 118000 QPS |
// IO 线程为 4, 并发连接 1000 的测试结果
[ikerli@localhost bin]$ wrk -c 1000 -t 8 -d 30 --latency 'http://127.0.0.1:19999/qps?id=1'
Running 30s test @ http://127.0.0.1:19999/qps?id=1
8 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 9.79ms 63.83ms 1.68s 99.24%
Req/Sec 17.12k 8.83k 97.54k 72.61%
Latency Distribution
50% 4.37ms
75% 7.99ms
90% 11.65ms
99% 27.13ms
4042451 requests in 30.07s, 801.88MB read
Socket errors: connect 0, read 0, write 0, timeout 205
Requests/sec: 134442.12
Transfer/sec: 26.67MB
由以上测试结果,TinyRPC 框架的 QPS 可达到 14W 左右。
3. 安装 TinyRPC
3.1. 安装必要的依赖库
要正确编译 TinyRPC, 至少要先安装这几个库:
3.1.1. protobuf
protobuf 是 google 开源的有名的序列化库。谷歌出品,必属精品!TinyRPC 的 TinyPB 协议是基于 protobuf 来 序列化/反序列化 的,因此这个库是必须的。 其地址为:https://github.com/protocolbuffers/protobuf
推荐安装版本 3.19.4 及以上。安装过程不再赘述, 注意将头文件和库文件 copy 到对应的系统路径下。
3.1.2. tinyxml
由于 TinyRPC 读取配置使用了 xml 文件,因此需要安装 tinyxml 库来解析配置文件。
下载地址:https://sourceforge.net/projects/tinyxml/
要生成 libtinyxml.a 静态库,需要简单修改 makefile 如下:
# 84 行修改为如下
OUTPUT := libtinyxml.a
# 194, 105 行修改如下
${OUTPUT}: ${OBJS}
${AR} $@ ${LDFLAGS} ${OBJS} ${LIBS} ${EXTRA_LIBS}
安装过程如下:
cd tinyxml
make -j4
# copy 库文件到系统库文件搜索路径下
cp libtinyxml.a /usr/lib/
# copy 头文件到系统头文件搜索路径下
mkdir /usr/include/tinyxml
cp *.h /usr/include/tinyxml
3.2. 安装和卸载 (makefile)
3.2.1. 安装 TinyRPC
在安装了前置的几个库之后,就可以开始编译和安装 TinyRPC 了。安装过程十分简单,只要不出什么意外就好了。
祈祷一下一次性成功,然后直接执行以下几个命令即可:
git clone https://github.com/Gooddbird/tinyrpc
cd tinyrpc
mkdir bin && mkdir lib && mkdir obj
// 生成测试pb桩文件
cd testcases
protoc --cpp_out=./ test_tinypb_server.proto
cd ..
// 先执行编译
make -j4
// 编译成功后直接安装就行了
make install
注意, make install 完成后,默认会在 /usr/lib 路径下安装 libtinyrpc.a 静态库文件,以及在 /usr/include/tinyrpc 下安装
