最近在调研各种RPC框架的性能表现,发现Facebook开源的fbthrift在C++生态中表现相当亮眼。作为一个轻量级、高性能的RPC框架,fbthrift特别适合需要极致性能的场景。今天我们就来实测一下它的基础性能——搭建一个最简单的RPC服务,服务端只实现一个空函数调用,测量在这种极限简单场景下的吞吐量和延迟表现。
为什么要测试空函数调用?这其实是个非常经典的基准测试方法。通过剥离业务逻辑的干扰,我们可以纯粹测量RPC框架本身的性能开销,包括序列化、网络传输、线程调度等核心环节的损耗。这个数据对后续业务开发中的性能预估非常有参考价值。
测试环境我选择了Ubuntu 20.04 LTS,内核版本5.4.0,硬件配置为8核CPU/16GB内存。fbthrift对Linux的支持最完善,建议在Linux环境下进行测试。以下是需要提前安装的依赖:
bash复制# 基础编译工具
sudo apt install -y g++ cmake make bison flex
# fbthrift依赖
sudo apt install -y libboost-all-dev libevent-dev libssl-dev
从GitHub克隆最新稳定版本(本文使用v2023.06.26):
bash复制git clone https://github.com/facebook/fbthrift.git
cd fbthrift
git checkout v2023.06.26
编译时需要特别注意几个关键参数:
bash复制mkdir build && cd build
cmake .. -DBUILD_SHARED_LIBS=ON \
-DCMAKE_BUILD_TYPE=Release \
-DFBTHRIFT_USE_SYSTEM_BOOST=ON
make -j$(nproc)
sudo make install
提示:如果遇到openssl相关报错,可以尝试指定openssl路径:
-DOPENSSL_ROOT_DIR=/usr/local/ssl
创建benchmark.thrift文件定义我们的测试接口:
thrift复制namespace cpp benchmark
service BenchmarkService {
void execute(),
}
这个IDL定义了一个最简单的服务,只有一个无参数无返回值的execute方法。
使用thrift编译器生成代码:
bash复制thrift1 --gen cpp -out . benchmark.thrift
这会生成以下关键文件:
BenchmarkService.h:服务接口定义BenchmarkService.cpp:接口实现骨架BenchmarkService_server.skeleton.cpp:服务端启动模板修改生成的skeleton文件,实现我们的空函数:
cpp复制// BenchmarkService_server.skeleton.cpp
class BenchmarkServiceHandler : virtual public BenchmarkServiceIf {
public:
void execute() override {
// 空实现,仅用于性能测试
}
};
关键配置参数需要特别注意:
cpp复制int main(int argc, char **argv) {
auto handler = std::make_shared<BenchmarkServiceHandler>();
auto server = std::make_shared<apache::thrift::ThriftServer>();
server->setInterface(handler);
server->setPort(9090);
server->setNumIOWorkerThreads(4); // IO线程数
server->setNumCPUWorkerThreads(8); // 工作线程数
std::cout << "Starting server..." << std::endl;
server->serve();
return 0;
}
注意:线程数设置需要根据实际CPU核心数调整,一般IO线程设为物理核心数1/4,工作线程设为物理核心数
创建同步客户端用于基准测试:
cpp复制void runSyncClient(int callCount) {
auto socket = std::make_shared<TSocket>("localhost", 9090);
auto transport = std::make_shared<TBufferedTransport>(socket);
auto protocol = std::make_shared<TBinaryProtocolT<TBufferedTransport>>(transport);
BenchmarkServiceClient client(protocol);
transport->open();
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < callCount; ++i) {
client.execute();
}
auto end = std::chrono::high_resolution_clock::now();
transport->close();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << "Sync calls: " << callCount
<< ", Total time: " << duration.count() << "us"
<< ", Avg latency: " << duration.count()/callCount << "us/call"
<< std::endl;
}
对于更高性能要求的场景,可以实现异步客户端:
cpp复制void runAsyncClient(int callCount) {
auto evb = std::make_shared<folly::EventBase>();
auto socket = TAsyncSocket::newSocket(evb, "localhost", 9090);
auto channel = HeaderClientChannel::newChannel(socket);
BenchmarkServiceAsyncClient client(std::move(channel));
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < callCount; ++i) {
client.sync_execute();
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << "Async calls: " << callCount
<< ", Total time: " << duration.count() << "us"
<< ", Avg latency: " << duration.count()/callCount << "us/call"
<< std::endl;
}
我们设计了三组测试场景:
测试脚本如下:
bash复制# 启动服务端
./BenchmarkService_server &
SERVER_PID=$!
# 等待服务启动
sleep 2
# 运行测试
./client --sync-single 100000
./client --sync-multi 4 25000
./client --async 100000
kill $SERVER_PID
在我的测试环境(i7-9700K,32GB DDR4)上得到如下结果:
| 测试场景 | 调用次数 | 总耗时(ms) | 平均延迟(μs) | QPS |
|---|---|---|---|---|
| 单线程同步 | 100,000 | 1,850 | 18.5 | 54,054 |
| 多线程同步(4线程) | 100,000 | 620 | 6.2 | 161,290 |
| 异步调用 | 100,000 | 420 | 4.2 | 238,095 |
从火焰图分析可以看到主要耗时在:
提示:可以使用perf工具生成火焰图:
perf record -g ./server,然后使用FlameGraph工具生成可视化结果
修改服务端线程模型可以显著提升性能:
cpp复制server->setNumIOWorkerThreads(2); // 减少IO线程数
server->setNumCPUWorkerThreads(16); // 增加工作线程
server->setQueueTimeout(std::chrono::milliseconds(0)); // 禁用队列超时
server->setTaskExpireTime(std::chrono::milliseconds(0)); // 禁用任务过期
cpp复制// 连接池示例
std::vector<std::shared_ptr<BenchmarkServiceClient>> createClientPool(
int size, const std::string& host, int port) {
std::vector<std::shared_ptr<BenchmarkServiceClient>> pool;
for (int i = 0; i < size; ++i) {
auto socket = std::make_shared<TSocket>(host, port);
auto transport = std::make_shared<TBufferedTransport>(socket);
auto protocol = std::make_shared<TBinaryProtocolT<TBufferedTransport>>(transport);
transport->open();
pool.emplace_back(std::make_shared<BenchmarkServiceClient>(protocol));
}
return pool;
}
fbthrift支持多种协议,性能差异明显:
| 协议类型 | 平均延迟(μs) | 序列化大小(bytes) |
|---|---|---|
| BinaryProtocol | 4.2 | 24 |
| CompactProtocol | 3.8 | 18 |
| JSONProtocol | 12.5 | 48 |
推荐生产环境使用CompactProtocol,它在空间和时间效率上取得了很好的平衡。
监控指标:必须监控的四个黄金指标
超时设置:根据业务特点合理设置
cpp复制server->setIdleTimeout(std::chrono::seconds(30)); // 连接空闲超时
server->setTaskExpireTime(std::chrono::milliseconds(100)); // 任务处理超时
优雅退出:正确处理SIGTERM信号
cpp复制folly::EventBase eb;
auto server = std::make_shared<ThriftServer>();
// ...其他配置...
folly::EventBase::SignalEvent signals(&eb, SIGTERM);
signals.setCallback([&] {
server->stop();
eb.terminateLoopSoon();
});
在实际项目中,我发现当QPS超过10万时,需要特别注意操作系统层面的调优,比如调整以下参数:
bash复制# 增加TCP缓冲区大小
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216
# 增加文件描述符限制
ulimit -n 1000000
# 调整线程栈大小
ulimit -s 1024