前言
前不久,我们接到客户长江电力的反馈,称在生产环境中进行高并发查询,例如包含数百个测点的近千个并发作业,在从近三月的数据中取数或聚合计算时,会出现作业超时,但 CPU 利用率却很低。
接到反馈后,我们的技术团队第一时间组织人员复现场景,本以为是一次普通平常的性能优化问题,没想到解决问题的过程堪比福尔摩斯探案。从脚本分析至核心代码,再深入到操作系统内核,我们抽丝剥茧,拨开层层迷雾,最终为客户揭开了性能不佳现象背后的谜团。

长江电力是中国最大的电力上市公司和全球最大的水电上市公司,其水力发电业务下属乌东德、白鹤滩、溪洛渡、向家坝、三峡、葛洲坝等六座水电站。作为长江电力工业互联网的底层数据库架构,近两年来,DolphinDB 一直为长江电力的水力发电项目提供高性能的数据存储和计算的能力支撑。
CPU 利用率偏低

在明确以提升 CPU 利用率为目标的思路下,我们首先从查询的脚本入手分析问题。
第一个猜想:是否是查询并发度不够,或者是数据倾斜导致了某些节点闲置,
从而导致了资源利用率低的情况。
第二步:我们在脚本结构层面进行了优化分析。
该场景下,查询语句的 where 条件包含排序键,在解析的过程中执行效率可能受到分区剪枝和谓词下推两个特性的影响。如果在访问分区表时,优化器可以通过分区剪枝消除不必要的分区,或者在执行查询时可以将过滤条件下推,直接让存储进程将符合范围的数据过滤掉,理论上这样可以让查询更加高效。
带着这个目标,我们对脚本进行了优化,但结果发现相同效果的查询语句,用到谓词下推和不能用到谓词下推的版本执行时间差别不大,并且没有经过优化的查询语句的 CPU 利用率是优化过脚本的两倍,这可能是因为没有谓词下推的脚本花在解压缩等工作上的时间更多,虽然涉及的 I/O 也更多,但总体还是表现为 CPU 利用率更高。综上,我们也基本排除了脚本优化不足导致的资源利用率低的情况。

前面说到,由于进程中的 worker 没有闲置,但 CPU 利用率仍然不高。这时我们一般使用 Off-CPU Flame Graph(火焰图)从锁争用和 I/O 两个方面来排查问题,我们选择使用 Intel® VTune™ 的 Threading Analysisc 分析类型来生成火焰图。
VTune 用户态采集模式
我们首先用 VTune 用户态采样模式进行分析。这个模式可以收集用户态线程在同步以及线程等待时的信息,给出每个线程详细的执行状态。
第一个发现是 CentralFreeList 的操作上锁争用问题频发。
我们通过设置 TCMALLOC_MAX_TOTAL_THREAD_CACHE_BYTES 环境变量规避了内存分配器上锁争用的问题,但 CPU 利用率仍然没有变化。并且根据采集结果显示,锁争用问题转移到了 DolphinDB TSDB 引擎层面。
跟随采集结果的指示,我们在 TSDB 引擎层面,先后对三处不同的锁进行了优化,但最终 CPU 利用率仍然没有变化。此时,我们已经对 VTune 给出结果的准确性有一些质疑。其实一开始使用 VTune 时,我们就发现了一个现象:在单独测试时,CPU 利用率一般是在20% - 30%,用了 VTune 之后,利用率直接降到了5%以内。这已经可以说明 VTune 在用户态采集模式下自身开销极大,导致测试结果非常不准确。因此,我们转而使用基于硬件事件的 VTune 采集方式,这种模式开销更小。
VTune 基于硬件事件的采集模式
在更改了采集模式之后,采集前后的 CPU 利用率并没有变化,但采集的结果却与之前完全不同。在硬件采集模式下,结果显示锁争用的问题定位到了 Linux 内核 page_fault 内部 mmap_sem 的获取读锁层面。
有了之前用户态模式踩坑的经历,我们决定用其他工具再测试一次,做双重验证。于是我们又用 bcc offcputime 工具测试了一次,得到了与 VTune 相同的采集结果。


经过一层层的排查和验证,我们在 VTune 的栈结果里发现了一处不常见的 mmap 相关调用栈,他将线索指向了 fseek,具体表现为在 TSDB 引擎使用 DolphinDB 文件流对象,按 offset 读取数据调用 fseek 时,调用了 mmap,并且在文件流对象析构时调用了 munmap。这让我们做出了一个假设:是否是 Linux 上的文件操作在内部频繁调用 mmap 才导致了冲突呢?

int main(int argc, char const* argv[]){printf("GNU libc version: %s\n", gnu_get_libc_version());FILE* fd = fopen("./result.csv", "rb");fseek(fd, 8192, SEEK_CUR);fclose(fd);}(gdb) bt#0 0x00007ffff6ceef90 in mmap64 () from lib64/libc.so.6#1 0x00007ffff6c64021 in __GI__IO_file_doallocate () from lib64/libc.so.6#2 0x00007ffff6c72e57 in __GI__IO_doallocbuf () from lib64/libc.so.6#3 0x00007ffff6c6fc03 in __GI__IO_file_seekoff () from lib64/libc.so.6#4 0x00007ffff6c6d607 in fseek () from lib64/libc.so.6#5 0x00000000004006ec in main (argc=1, argv=0x7fffffffe228) at main.cpp:17
#include <stdio.h>#include <stdlib.h>#include <sys/mman.h>#include <unistd.h>#include <errno.h>#include <vector>#include <thread>#include <stdio.h>#include <stdlib.h>#include <gnu/libc-version.h>int main(int argc, char const* argv[]){printf("GNU libc version: %s\n", gnu_get_libc_version());int tn = argc >= 2 ? std::atoi(argv[1]) : 1;std::vector<std::thread> ts;for (int i = 0; i < tn; i++) {ts.push_back(std::thread([](){while (true) {char* buf = (char*)mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);for (int i = 0; i < 4096; i++) {buf[i] = 1;}munmap(buf, 4096);}}));}while (true) {FILE* fd = fopen("./result.csv", "rb");fseek(fd, 8192, SEEK_CUR);fclose(fd);}for (auto& t : ts) {t.join();}}


于是我们查看了 glibc 2.17和 glibc 2.35(此时已经发现了 glibc 2.35 fseek 不会调用 mmap 了)__GI__IO_file_doallocate 和 _IO_setb 的实现。
__GI__IO_file_doallocate 在glibc 2.17 会主动调用 mmap,而在 glibc 2.35 是通过 malloc 分配内存。类似的,_IO_setb 在 glibc 2.17 是主动调用 munmap,在glibc 2.35 是调用 free。
我们立即修改了原测试环境的 glibc 版本进行原并发查询场景的测试,测试结果让我们心里的石头都落了地。

如果您也遇到这样的问题,快升级你的 glibc 2.17吧!详细升级手册点击文末阅读全文即可获取!

由表及里,深入核心。每一行脚本都是对问题刨根问底的见证,每一次灵光乍现都是深入思考的结晶,每一次重新出发都是对技术的不懈追求。
无数次猜测、讨论、验证之后,留下的不只是一份解决方案,更是一份对技术极度热爱、对挑战毫不畏惧的承诺。现在如此,未来亦然,我们将保持对技术的敬畏,持续不断地学习下去。
同样,随着越来越多客户将 DolphinDB 部署在关键的生产系统上,我们面临的技术场景也越发丰富而充满挑战。如果你也有志于精进技术、乐于钻研,欢迎你加入 DolphinDB 的技术团队,与我们一同探索技术的无限可能!
[注1] mmap_sem 相关讨论:
Db2 LUW (Linux): poor IO performance with VERITAS File System (VxFS) if nommapcio mount option is not enabled (ibm.com)
https://www.ibm.com/support/pages/db2-luw-linux-poor-io-performance-veritas-file-system-vxfs-if-nommapcio-mount-option-not-enabled
Re: [v8-dev] mmap contention (mail-archive.com)
https://www.mail-archive.com/v8-dev@googlegroups.com/msg161249.html
On the surprising behaviour of memory operations at high thread counts | by Fabien Reumont-Locke | Medium https://medium.com/@The_Zorg/on-the-surprising-behaviour-of-memory-operations-at-high-thread-counts-f0ce630d9240
Explore More








