仅花200行代码,如何将60万行的RocksDB改造成协程
本文摘要:
采用少量手动修改+自动代码转换的方式,将大型多线程程序改造成协程。在某些重IO、高并发的场景中,帮助业务取得了性能翻倍的效果。
背景
RocksDB是业界知名的可嵌入的、持久化的KV数据库,它使用一套日志结构的存储引擎,为快速而又低延迟的存储设备做了特殊优化处理。RocksDB使用C++编写,2013年开源,其代码风格成熟稳定,测试覆盖率高,项目中还附带了丰富的性能测试工具。可以说,研究RocksDB原理,并学习其工程实践,是每个做存储和底层系统优化的工程师都绕不开的话题。 RocksDB本身是多线程模型,支持并发读写。众所周知,协程相比较于线程,在IO繁重或者并发量大时,有着更轻量且更高效的特性。据测试,在系统负载较高时,一次线程切换的时间最高可达30μs;而使用协程,最低仅需十几个ns,相差了几个数量级。 PhotonLibOS(以下简称Photon)是阿里云存储DADI团队开源的一款高性能协程库和IO引擎,我们曾经拿自己用协程实现的IO程序与fio比较过,以及用协程实现的网络程序与Nginx比较过,都取得了更好的性能。恰逢存储内部的某个业务团队正在使用RocksDB,且网络+存储的整体方案遇到了一些性能瓶颈,于是,我们便开始调研用协程改造RocksDB,这是Photon第一次在大规模的成熟软件上进行嫁接尝试。
协程化改造
先说结论:改造的过程出奇地顺利,没有变更RocksDB的主逻辑,只是手动修改了200多行代码,然后利用一个能够扫描代码并自动转换成协程版本的小脚本,就顺利地完成了编译和运行。
按照业务需求,我们使用的是2019年的RocksDB 6.1.2版本,总共3175个test case。经过测试,Photon协程版本的RocksDB通过了3170个case,
成功率达到99.87%
。经过初步分析,失败的5个都是因为涉及到了线程自身的特性,或者test case里显式地认为自身运行在线程环境里,协程版本无法满足因此失败。且这些失败的case不会影响RocksDB的正常运行。
在
性能
方面,利用自带的db_bench工具,在四种典型的KV读写场景下测试对比了Photon版本RocksDB与原版RocksDB的OPS,
两者达到了相近的数据;在某些重IO、高并发的场景下
,会比原版的性能更好
(见后文)。
Photon库介绍
-
用户代码的显式调用下,某个协程需要让出处理器(yield),并切换到下一个协程执行单元
-
跨vcpu迁移(migrate)或唤醒(interrupt)事件
-
关注的一些fd发生了IO事件
-
定时器到期,等等
经过替换后,代码变成如下:bool condition = false;std::mutex mu;std::condition_variable cv;new std::thread([&] {std::this_thread::sleep_for(std::chrono::seconds(1));std::lock_guard<std::mutex> lock(mu);condition = true;cv.notify_one();});std::unique_lock<std::mutex> lock(mu);while (!condition) {cv.wait(lock);}
bool condition = false;photon::std::mutex mu;photon::std::condition_variable cv;new photon::std::thread([&] {photon::std::this_thread::sleep_for(std::chrono::seconds(1));photon::std::lock_guard<photon::std::mutex> lock(mu);condition = true;cv.notify_one();});photon::std::unique_lock<photon::std::mutex> lock(mu);while (!condition) {cv.wait(lock);}
// 编译器支持的thread_local关键字thread_local Value value = "123";// 替换成新的thread_local_ptr模板类static photon::thread_local_ptr<Value, std::string> value("123");
db_bench单机性能测试
此外,CPU密集型场景下新版性能不如原版还有一个重要的原因就是,新版代码只做了语法替换,而没有进行针对性的调优。举个例子来说,原版多线程在某些情况下会使用 asm volatile("pause") 进行CPU忙等,那么可否在协程场景下修改为协程的sleep?原版中包含一个 core_local 模块,它在协程场景下应该如何改造,等等。这里受限于篇幅,不一一列举。
杀手锏:协程化的网络数据库
看到这里有人可能会问,既然做单机测试时,协程版本的RocksDB貌似并没有很出彩,那么为什么还要做这些改造工作。其实,
协程化的最大价值在于发掘一个基于网络的数据库的最大性能,特别是多连接、高并发的场景下。
长久以来,epoll循环一直是实现一个高性能网络服务器的不二之选。不管是类似Java netty、boost asio的异步回调方案,还是类似Golang的协程方案,留给开发者的问题一直是如何在少量的线程里实现高并发的IO。回到RocksDB的场景下,它本身对多线程很友好,但嵌入到网络服务器之后,就不得不引入线程池技术来分发和维护数据读写请求。一边是
异步多路复用系统
,一边是
同步
系统,中间的连接器反而容易成为性能瓶颈。
-
RPC server + 线程池 + 原版RocksDB
-
RPC server + 协程版RocksDB
总结
我们通过引入Photon库,花费少量代码,成功地将一个大规模的数据库软件改造成协程。一方面验证了协程在重IO、高并发场景下的理论优势,另一方面也检验了Photon自身的成熟度,输出了DADI技术在存储加速领域的最佳实践。
需要声明的是,由于我们本身并不是RocksDB的专家,对其的改造仅停留在语法层面。我们相信运行在协程上之后,RocksDB的内部逻辑和调优手段还会存在一些需要调整的地方,以便适应协程、线程、CPU core的三级模型,最大化地增加cache命中率,降低资源争抢概率,解决目前协程版本RocksDB在CPU密集型任务下性能没有完全发挥出来的问题。关于这些工作,就需要后续再慢慢研究了。
最后补充一句,PhotonLibOS的开源地址 是:
https://github.com/alibaba/PhotonLibOS 如果大家对C++协程及高性能IO感兴趣,欢迎前来试用。