c++ async rpc framework. 14w+qps.
作者:**ikerli** **2022-05-13**
**使用 TinyRPC, 轻松地构建高性能分布式 RPC 服务!**
- [1. 概述](#1-概述)
- [1.1. TinyRPC 特点](#11-tinyrpc-特点)
- [1.2. TinyRPC 支持的协议报文](#12-tinyrpc-支持的协议报文)
- [1.3. TinyRPC 的 RPC 调用](#13-tinyrpc-的-rpc-调用)
- [1.3.1. 阻塞协程式异步调用](#131-阻塞协程式异步调用)
- [1.3.2. 非阻塞协程式异步调用](#132-非阻塞协程式异步调用)
- [2. 性能测试](#2-性能测试)
- [2.1. HTTP echo 测试 QPS](#21-http-echo-测试-qps)
- [3. 安装 TinyRPC](#3-安装-tinyrpc)
- [3.1. 安装必要的依赖库](#31-安装必要的依赖库)
- [3.1.1. protobuf](#311-protobuf)
- [3.1.2. tinyxml](#312-tinyxml)
- [3.2. 安装和卸载 (makefile)](#32-安装和卸载-makefile)
- [3.2.1. 安装 TinyRPC](#321-安装-tinyrpc)
- [3.2.2. 卸载 TinyRPC](#322-卸载-tinyrpc)
- [3.3. 安装和卸载 (cmake)](#33-安装和卸载-cmake)
- [3.3.1. 安装 TinyRPC](#331-安装-tinyrpc)
- [3.3.2. 卸载 TinyRPC](#332-卸载-tinyrpc)
- [4. 快速上手](#4-快速上手)
- [4.1. 搭建基于 TinyPB 协议的 RPC 服务](#41-搭建基于-tinypb-协议的-rpc-服务)
- [4.1.1. 实现 Protobuf 文件接口](#411-实现-protobuf-文件接口)
- [4.1.2. 准备配置文件](#412-准备配置文件)
- [4.1.3. 实现业务接口](#413-实现业务接口)
- [4.1.4. 启动 RPC 服务](#414-启动-rpc-服务)
- [4.2. 搭建基于 HTTP 协议的 RPC 服务](#42-搭建基于-http-协议的-rpc-服务)
- [4.2.1. 准备配置文件](#421-准备配置文件)
- [4.2.2. 实现 Servlet 接口](#422-实现-servlet-接口)
- [4.2.3. 启动 RPC 服务](#423-启动-rpc-服务)
- [4.3. RPC 服务调用](#43-rpc-服务调用)
- [4.3.1. 阻塞协程式异步调用](#431-阻塞协程式异步调用)
- [4.3.2. 非阻塞协程式异步调用](#432-非阻塞协程式异步调用)
- [4.4. TinyRPC 脚手架(tinyrpc_generator)](#44-tinyrpc-脚手架tinyrpc_generator)
- [4.4.1 准备 protobuf 文件](#441-准备-protobuf-文件)
- [4.4.2 生成 TinyRPC 框架](#442-生成-tinyrpc-框架)
- [4.4.3 业务逻辑开发](#443-业务逻辑开发)
- [4.4.4 Protobuf 接口升级怎么办?](#444-protobuf-接口升级怎么办)
- [4.4.5 tinyrpc_generator 选项详解](#445-tinyrpc_generator-选项详解)
- [5. 概要设计](#5-概要设计)
- [5.1. 异步日志模块](#51-异步日志模块)
- [5.2. 协程模块](#52-协程模块)
- [5.2.1. 协程封装](#521-协程封装)
- [5.2.2. m:n 线程:协程模型](#522-mn-线程协程模型)
- [5.3. Reactor 模块](#53-reactor-模块)
- [5.4. Tcp 模块](#54-tcp-模块)
- [5.4.1. TcpServer](#541-tcpserver)
- [5.4.2. TcpConnection](#542-tcpconnection)
- [5.5. TinyPB 协议](#55-tinypb-协议)
- [5.5.1. TinyPB 协议报文格式分解](#551-tinypb-协议报文格式分解)
- [5.6. Http 模块](#56-http-模块)
- [5.7. RPC 调用封装](#57-rpc-调用封装)
- [6. 错误码](#6-错误码)
- [6.1. 错误码判断规范](#61-错误码判断规范)
- [6.2. 错误码释义文档](#62-错误码释义文档)
- [7. 问题反馈](#7-问题反馈)
- [8. 参考资料](#8-参考资料)
# 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 类,一个简单的调用例子如下:
```c++
tinyrpc::TinyPbRpcChannel channel(std::make_shared("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,一个简单调用例子如下:
```
…
```
注意在这种调用方式中,query_age 会立马返回,协程 C1 可以继续执行下面的代码。但这并不代表着调用 RPC 完成,如果你需要获取调用结果,请使用:
```c++
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** |
```
…
```
由以上测试结果,**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** 下安装所有的头文件。
如果编译出现问题,欢迎提 [issue](https://github.com/Gooddbird/tinyrpc/issues/), 我会尽快回应。
### 3.2.2. 卸载 TinyRPC
卸载也很简单,如下即可:
```
make uninstall
```
**注:如果此前已经安装过 TinyRPC, 建议先执行卸载命令后再重新 make install 安装.**
## 3.3. 安装和卸载 (cmake)
### 3.3.1. 安装 TinyRPC
```shell
$ git clone https://github.com/Gooddbird/tinyrpc
# 需要先生成 pb 文件
$ cd tinyrpc/testcases
$ protoc --cpp_out=./ test_tinypb_server.proto
$ cd ..
$ mkdir bin && mkdir lib && mkdir build
# 安装
$ sudo ./build.sh
```
`build.sh` 也是通过 `cmake` 安装的,当然你也可以手动通过 `cmake` 去创建
### 3.3.2. 卸载 TinyRPC
```shell
$ sudo rm -rf /usr/include/tinyrpc/
$ sudo rm -rf /usr/lib/libtinyrpc.a
# 如果没有更改 makefile 中和 CMakeLists 中的 头文件 和 静态库 的存储路径的话,也可以直接执行:make uninstall
```
# 4. 快速上手
## 4.1. 搭建基于 TinyPB 协议的 RPC 服务
### 4.1.1. 实现 Protobuf 文件接口
TinyPB 协议基于 Protobuf 来序列化的,在搭建基于 TinyPB 协议的 RPC 服务之前,需要先定义接口文档。具体的 Protobuf 文档需要根据业务的实际功能来编写,这里给出一个例子如下:
```
…
```
使用 protoc 工具生成对应的 C++ 代码:
```
protoc --cpp_out=./ test_tinypb_server.proto
```
### 4.1.2. 准备配置文件
**TinyRPC** 读取标准的 **xml** 配置文件完成一些服务初始化设置,这个配置文件模板如下,一般只需要按需调整参数即可:
```
…
```
### 4.1.3. 实现业务接口
protobuf 文件提供的只是接口说明,而实际的业务逻辑需要自己实现。只需要继承 QueryService 并重写方法即可,例如:
```
…
```
### 4.1.4. 启动 RPC 服务
TinyRPC 服务启动非常简单,只需寥寥几行代码即可:
```c++
int main(int argc, char* argv[]) {
if (argc != 2) {
printf("Start TinyRPC server error, input argc is not 2!");
printf("Start TinyRPC server like this: \n");
printf("./server a.xml\n");
return 0;
}
// 1. 读取配置文件
tinyrpc::InitConfig(argv[1]);
// 2. 注册 service
REGISTER_SERVICE(QueryServiceImpl);
// 3. 启动 RPC 服务
tinyrpc::StartRpcServer();
return 0;
}
```
生成可执行文件 **test_tinypb_server** 后,启动命令如下:
```
nohup ./test_tinypb_server ../conf/test_tinypb_server.xml &
```
如果没什么报错信息,那么恭喜你启动成功了。如果不放心,可以使用 ps 命令查看进程是否存在:
```
ps -elf | grep 'test_tinypb_server'
```
或者使用 netstat 命令查看端口是否被监听:
```
netstat -tln | grep 39999
```
至此,基于 TinyPB 协议的 RPC 服务已经启动成功,后续我们将调用这个服务。
## 4.2. 搭建基于 HTTP 协议的 RPC 服务
### 4.2.1. 准备配置文件
同上,准备一个配置文件 **test_http_server.xml**:
```
…
```
### 4.2.2. 实现 Servlet 接口
**TinyRPC** 提供类似 JAVA 的 **Servlet** 接口来实现 HTTP 服务。你只需要简单的继承 HttpServlet 类并实现 handle 方法即可,如一个 HTTP 的 echo 如下:
```
…
```
### 4.2.3. 启动 RPC 服务
将 Servlet 注册到路径下,启动 RPC 服务即可。注意这个注册路径相对于项目的根路径而言:
```c++
// test_http_server.cc
int main(int argc, char* argv[]) {
if (argc != 2) {
printf("Start TinyRPC server error, input argc is not 2!");
printf("Start TinyRPC server like this: \n");
printf("./server a.xml\n");
return 0;
}
tinyrpc::InitConfig(argv[1]);
// 访问 http://127.0.0.1:19999/qps, 即对应 QPSHttpServlet 这个接口
REGISTER_HTTP_SERVLET("/qps", QPSHttpServlet);
tinyrpc::StartRpcServer();
return 0;
}
```
启动命令同样如下:
```
nohup ./test_http_server ../conf/test_http_server.xml &
```
使用 curl 工具可以测试 HTTP 服务是否启动成功:
```
[ikerli@localhost bin]$ curl -X GET 'http://127.0.0.1:19999/qps?id=1'
Welcome to TinyRPC, just enjoy it!
QPSHttpServlet Echo Success!! Your id is,1
```
## 4.3. RPC 服务调用
这一节将使用 test_http_server 服务调用 test_rpc_server,前面说过,TinyRPC 支持两种 RPC 调用方式:**阻塞协程式异步调用** 和 **非阻塞协程式异步调用**
### 4.3.1. 阻塞协程式异步调用
这种调用方式适用于我们依赖 RPC 调用结果的场景,必须等待 RPC 调用返回后才能进行下一步业务处理。BlockHttpServlet 即属于这种调用方式:
```
…
```
注册此 Servlet, 然后重启 **test_http_server**
```
REGISTER_HTTP_SERVLET("/block", BlockCallHttpServlet);
```
使用 curl 测试
```
[ikerli@localhost bin]$ curl -X GET 'http://127.0.0.1:19999/block?id=1'
Welcome to TinyRPC, just enjoy it!
Success!! Your age is,100100111 and Your id is 1
```
### 4.3.2. 非阻塞协程式异步调用
这种调用方式适用于我们不依赖 RPC 调用结果的场景,即我们可以继续业务处理,而不关心何时 RPC 调用成功。NonBlockHttpServlet 即属于这种调用方式:
```
…
```
注册此 Servlet, 然后重启 **test_http_server**
```
REGISTER_HTTP_SERVLET("/nonblock", NonBlockCallHttpServlet);
```
使用 curl 测试
```
[ikerli@localhost bin]$ curl -X GET 'http://127.0.0.1:19999/nonblock?id=1'
Welcome to TinyRPC, just enjoy it!
Success!! Your age is,0 and Your id is 0
```
## 4.4. TinyRPC 脚手架(tinyrpc_generator)
TinyRPC 提供了代码生成工具,简单到只需要一个 protobuf 文件,就能生成全部框架代码,作为使用者只需要写业务逻辑即可,不必关心框架的原理,也不用再去写繁琐的重复代码,以及考虑如何链接 tinyrpc 库的问题。接下来用一个实例来说明如何使用 `tinyrpc_generator`.
### 4.4.1 准备 protobuf 文件
例如我们需要搭建一个订单服务: `order_server`. 它的提供一些简单的订单操作:查询订单、生成订单、删除订单等。
首先定义 `order_server.proto` 如下:
```
…
```
### 4.4.2 生成 TinyRPC 框架
这一步很简单,简单到只需要一行命令:
```
tinyrpc/generator/tinyrpc_generato