SkillAgentSearch skills...

Tinyrpc

c++ async rpc framework. 14w+qps.

Install / Use

/learn @Gooddbird/Tinyrpc
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

GitHub repo size GitHub issues GitHub pull requests GitHub forks GitHub Repo stars GitHub contributors GitHub last commit

作者:ikerli 2022-05-13 使用 TinyRPC, 轻松地构建高性能分布式 RPC 服务!

<!-- TOC --> <!-- /TOC -->

1. 概述

1.1. TinyRPC 特点

TinyRPC 是一款基于 C++11 标准开发的小型异步 RPC 框架。TinyRPC 的核心代码应该也就几千行样子,尽量保持了简洁且较高的易读性。

麻雀虽小五脏俱全,从命名上就能看出来,TinyRPC 框架主要用义是为了让读者能快速地轻量化地搭建出具有较高性能的异步RPC 服务。至少用 TinyRPC 搭建的 RPC 服务能应付目前大多数场景了。

TinyRPC 没有实现跨平台,只支持 Linux 系统,并且必须是 64 位的系统,因为协程切换只实现了 64 位系统的代码,而没有兼容 32 位系统。这是有意的,因为作者只会 Linux 下开发,没能力做到跨平台。

TinyRPC 的核心思想有两个:

  1. 让搭建高性能 RPC 服务变得简单
  2. 让异步调用 RPC 变得简单

必须说明的是, TinyRPC 代码没有达到工业强度,最好不要直接用到生产环境,也可能存在一些未知 BUG,甚至 coredump。读者请自行辨别,谨慎使用!

1.2. TinyRPC 支持的协议报文

TinyRPC 框架目前支持两类协议:

  1. HTTP 协议: TinyRPC 实现了简单的很基本的 HTTP(1.1) 协议的编、解码,完全可以使用 HTTP 协议搭建一个 RPC 服务。
  2. 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 推荐的方式,它的优点如下:

  1. 代码实现很简单,直接同步式调用,不需要写回调函数。
  2. 对IO线程数没有限制,即使只有 1 个 IO 线程,仍然能达到这种效果。
  3. 对于线程来说,他是不会阻塞线程的。

当然,它的缺点也存在:

  1. 对于当前协程来说,他是阻塞的,必须等待协程再次被唤醒(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 等待结果)。

这个调用链路如图:

总之,非阻塞协程式异步调用的优点如下:

  1. RPC 调用不阻塞当前协程 C1,C1 可以继续往下执行代码(若遇到 wait 则会阻塞)。

而缺点如下:

  1. 所有 RPC 调用相关的对象,必须是堆上的对象,而不是栈对象, 包括 req、res、controller、async_rpc_channel。强烈推荐使用 shared_ptr,否则可能会有意想不到的问题(基本是必须使用了)。
  2. 在 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

protobufgoogle 开源的有名的序列化库。谷歌出品,必属精品!TinyRPCTinyPB 协议是基于 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 下安装

View on GitHub
GitHub Stars1.6k
CategoryDevelopment
Updated5d ago
Forks229

Languages

C++

Security Score

100/100

Audited on Apr 2, 2026

No findings