记一次深入内核的数据库高并发性能优化实践

news2024/11/25 13:29:28

前不久,我们接到客户长江电力的反馈,称在生产环境中进行高并发查询,例如包含数百个测点的近千个并发作业,在从近三月的数据中取数或聚合计算时,会出现作业超时,但CPU利用率却很低。

接到反馈后,我们的技术团队第一时间组织人员复现场景,本以为是一次普通平常的性能优化问题,没想到解决问题的过程堪比福尔摩斯探案。从脚本分析至核心代码,再深入到操作系统内核,我们抽丝剥茧,拨开层层迷雾,最终为客户揭开了性能不佳现象背后的谜团。

高并发查询性能不佳?

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

长江电力每个水电站包含多台机组,全天候采集的数据测点峰值高达200万。采集数据经 Kafka 实时推送至 DolphinDB 高可用集群,目前每天产生的数据行数在百亿级级别

一般来讲,处理高并发查询下性能不足的问题,我们首先检查资源使用率,主要是 CPU 利用率、网络带宽、磁盘 I/O 三个方面。借助 dstat 工具 ,我们发现 CPU 利用率不高是首先暴露出来的明显问题:上千个并发作业的情况下,集群的 CPU 利用率只有30%左右,网络和磁盘的资源也仍有冗余。那么如何提高 CPU 利用率,或者能够排查出资源瓶颈,是我们对解决这个问题的预期。

CPU 利用率偏低

脚本分析无功而返

在明确以提升 CPU 利用率为目标的思路下,我们首先从查询的脚本入手分析问题。

第一个猜想:是否是查询并发度不够,或者是数据倾斜导致了某些节点闲置,从而导致了资源利用率低的情况。

我们首先分析了工作负载:在复现场景下,用户同时提交上千个查询任务,每个查询会涉及所有分区。由于 DolphinDB 会把总任务改写成针对每个分区的任务并行执行,理论上应该能够用到每个节点上所有的100个 worker,通过 DolphinDB Web 端监控确认,查询过程中每个节点的 worker 一直是满负载,等待队列上也一直有足够的任务。综上基本排除是工作负载本身并发度不够导致资源利用率低的情况。

第二步我们在脚本结构层面进行了优化分析。该场景下,查询语句的 where 条件包含排序键,在解析的过程中执行效率可能受到分区剪枝和谓词下推两个特性的影响。如果在访问分区表时,优化器可以通过分区剪枝消除不必要的分区,或者在执行查询时可以将过滤条件下推,直接让存储进程将符合范围的数据过滤掉,理论上这样可以让查询更加高效。

带着这个目标,我们对脚本进行了优化,但结果发现相同效果的查询语句,用到谓词下推和不能用到谓词下推的版本执行时间差别不大,并且没有经过优化的查询语句的 CPU 利用率是优化过脚本的两倍,这可能是因为没有谓词下推的脚本花在解压缩等工作上的时间更多,虽然涉及的 I/O 也更多,但总体还是表现为 CPU 利用率更高。综上,我们也基本排除了脚本优化不足导致的资源利用率低的情况。

一波三折的火焰图:DolphinDB 代码分析

查询层面的分析似乎没有什么进展。我们打算换个思路,从代码层面入手,看看能否提高 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 相同的采集结果。

bcc offcpu 火焰图

结合 VTune 和 bcc offcpu 的测试结果,我们基本可以确定是 mmap_sem 锁竞争问题导致的 CPU 利用率低。

具体表现为:系统调用 mmap 和 munmap 时需要获取 mmap_sem 互斥锁,缺页异常处理时需要获取 mmap_sem 共享锁,两个地方会出现锁争用的问题,产生资源冲突。

深入内核,确认写锁来源

此时,我们对排查的方向有了一些动摇。按理来说,Linux 在缺页这种非常正常的操作上不应该存在这么严重的扩展性问题,虽然问题很少见,但是本着已经找到问题就要打破砂锅问到底的态度,我们决定继续深入下去。

因为上述测试本身确实会使用到大量内存,所以我们大胆排除了是因为内存分配器没有缓存,而导致频繁缺页,从而频繁读锁的可能性。

既然缺页获取 mmap_sem 读锁的情况无法避免,那么我们只能试着找到读锁来源,看看能否优化了。

然而我们在 VTune 的栈结果里并没有搜到 mmap_sem 获取写锁的栈,因此我们想到修改 bcc offcputime 脚本,把 finish_task_switch 事件改成在 down_write 和 up_write。但由于测试的服务器内核版本太低(CentOS 7),只能显示内核栈,无法显示用户栈,这一操作无法实现。

直接查看写锁调用栈的方法暂时卡住了,在寻找新的思路同时,我们也搜索了大量 mmap_sem 相关的问题,寻找灵感。在阅读了大量材料后,确实发现有一些相关的讨论。[注1]

综合这些讨论,我们猜测可能是内核里 mmap 操作太频繁,导致缺页时 mmap_sem 锁争用。

我们使用 strace 和 sysdig 这两个工具来确认 mmap 的调用频率。经过测试后,除了大致确认 mmap 调用确实过于频繁外,我们还从 sysdig 的输出中发现,在调用mmap 前伴随着多次的 open 和 fstat操作(而且这些文件就是level file,不过mmap本身是匿名映射)。

这个现象让我们不禁怀疑,也许是因为 tcmalloc 里 mmap 调用太频繁而导致了资源争用问题。我们试图通过在 tcmalloc 中禁用 mmap(即使这样性能也许会更差)来验证这个问题,但是即便如此,VTune 仍然显示瓶颈是缺页时 mmap_sem 锁争用。

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

事实证明,正是这个差点被忽略的线索,让我们找到了解决问题的关键。

验证猜测:Linux文件操作对mmap的影响

我们首先查看了 fopen 的文档,得知如果通过 'm' 模式来打开文件,文件操作确实会调用 mmap,但是我们并没有使用该模式。然而 VTune 给出的栈信息明确显示是 fseek 内部调用了 mmap,为了进一步验证猜想,我们写了一个简单的程序,通过 gdb 在 mmap 设置断点的方式测试,发现了 fseek 内部确实会调用 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

值得一提的是,我们非常幸运是直接在服务器上(CentOS 7, glibc 2.17)写的这个 demo,才发现了 fseek 的问题。如果是在本地机器上测试(Ubuntu 22, glibc 2.35),很有可能就错过了问题的关键。

通过 demo 测试 ,我们让一个线程一直执行 fopen/fseek/fclose,另外多个线程执行 mmap/memcpy/munmap(该测试为8个线程),结果发现在 glibc 2.17中,top 显示 cpu 利用率只有300%,而在 glibc 2.35中,cpu 利用率可以达到880%!

#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升级前后,CPU 利用率对比

真相大白!glbic 2.17 实现问题

到这里,我们基本确定了 fseek 的拓展性问题。

于是我们查看了 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 版本进行原并发查询场景的测试,测试结果让我们心里的石头都落了地。

至此,经过一步步的猜测、推理、验证和推翻后再验证,“真相”终于水落石出:在用户的高并发查询场景下,系统读取 levelfile 非常频繁,加上一些/proc文件系统的读取频繁调用了 fseek,glibc 2.17的实现问题更导致了这一操作会频繁调用 mmap 和 munmap ,进而获取 mmap_sem的写锁,使得 page fault 无法获取 mmap_sem 的读锁,导致两个调用栈锁争用过高,最终产生了CPU利用率不高的问题,影响了高并发查询场景下的性能。

如果您也遇到这样的问题,快升级你的glibc 2.17吧!详细升级手册请见:基于 Glibc 版本升级的 DolphinDB 数据查询性能优化实践 - 知乎在高并发查询、查询需要涉及很多个分区的情况下,低版本的 glibc(低于2.23)会严重影响查询性能。需要升级 glibc 解决该问题优化性能。我们撰写了本文,通过 patchelf 工具修改可执行文件和动态库的 rpath,达到…icon-default.png?t=N7T8https://zhuanlan.zhihu.com/p/667724630

结语:千淘万漉虽辛苦,吹尽狂沙始到金

由表及里,深入核心。每一行脚本都是对问题刨根问底的见证,每一次灵光乍现都是深入思考的结晶,每一次重新出发都是对技术的不懈追求。

无数次猜测、讨论、验证之后,留下的不只是一份解决方案,更是一份对技术极度热爱、对挑战毫不畏惧的承诺。现在如此,未来亦然,我们将保持对技术的敬畏,持续不断地学习下去。

同样,随着越来越多客户将 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)
  • Re: [v8-dev] mmap contention (mail-archive.com)
  • On the surprising behaviour of memory operations at high thread counts | by Fabien Reumont-Locke | Medium

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1267735.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

快速操控鼠标行为!Vue鼠标按键修饰符让你事半功倍

&#x1f3ac; 江城开朗的豌豆&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 &#x1f4dd; 个人网站 :《 江城开朗的豌豆&#x1fadb; 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! ⭐ 专栏简介 欢迎来到前端入门之旅&#xff01;这个…

【Qt之QSqlRelationalTableModel】描述及使用

描述 QSqlRelationalDelegate链接: https://blog.csdn.net/MrHHHHHH/article/details/134690139 QSqlRelationalTableModel类为单个数据库表提供了一个可编辑的数据模型&#xff0c;并支持外键。 QSqlRelationalTableModel的行为类似于QSqlTableModel&#xff0c;但允许将列设…

解锁Jira本地部署的数据中心版高级功能,打造高效、智能、精细化的项目管理

近日&#xff0c;在龙智携手Atlassian与JFrog共同举办的“大规模开发创新&#xff1a;如何提升企业级开发效率与质量”的线下研讨会中&#xff0c;龙智高级咨询顾问、Atlassian认证专家叶燕秀为大家带来了精彩演讲&#xff0c;解锁Jira Data Center版的诸多高级功能&#xff0c…

RLHF:强化学习结合大预言模型的训练方式

RLHF (Reinforcement Learning from Human Feedback) 以强化学习方式依据人类反馈优化语言模型。 文章目录 一、简介二、一般的流程三、微调gpt介绍示例 参考文章 一、简介 强化学习从人类反馈中学习&#xff08;RLHF&#xff0c;Reinforcement Learning from Human Feedback&a…

6 Redis缓存设计与性能优化

缓存穿透 缓存穿透是指查询一个根本不存在的数据&#xff0c; 缓存层和存储层都不会命中&#xff0c; 通常出于容错的考虑&#xff0c; 如果从存储层查不到数据则不写入缓存层。缓存穿透将导致不存在的数据每次请求都要到存储层去查询&#xff0c; 失去了缓存保护后端存储的意义…

Linux CentOS7 安装Docker | 中文输入法

CentOS7安装中文输入法&#xff1a; 在安装CentOS时&#xff0c;我们为了方便使用&#xff0c;语言选择了中文&#xff0c;但是我们发现&#xff0c;在Linux命令行或者是浏览器中输入时&#xff0c;我们只能输入英文&#xff0c;无法输入汉字。 用yum 安装ibus 命令&#xff…

05 # 基本类型

类型注解 作用&#xff1a;相当于强类型语言中的类型声明 语法&#xff1a; (变量/函数): type 原始类型: let bool: boolean true; let num: number 313; let str: string kaimo;数组: let arr: number[] [1, 2, 3]; let arr2: Array<number | string> [1, 2,…

【数据库】基于索引的扫描算法,不同类型索引下的选择与连接操作,不同的代价及优化

基于索引的算法 ​专栏内容&#xff1a; 手写数据库toadb 本专栏主要介绍如何从零开发&#xff0c;开发的步骤&#xff0c;以及开发过程中的涉及的原理&#xff0c;遇到的问题等&#xff0c;让大家能跟上并且可以一起开发&#xff0c;让每个需要的人成为参与者。 本专栏会定期更…

WordPress更改文章分类插件

当WP网站内容比较多的时候&#xff0c;有时候如果涉及到批量修改文章分类&#xff0c;如果一个个的去操作的话就太费事了&#xff0c;如果使用后台批量修改分类的话是增加旧分类不会取消选择&#xff0c;就就导致我们适得其反还需要一个一个的去编辑取消&#xff0c;实在繁琐了…

机器人与3D视觉 Robotics Toolbox Python 一 安装 Robotics Toolbox Python

一 安装python 库 前置条件需要 Python > 3.6&#xff0c;使用pip 安装 pip install roboticstoolbox-python测试安装是否成功 import roboticstoolbox as rtb print(rtb.__version__)输出结果 二 Robotics Toolbox Python样例程序 加载机器人模型 加载由URDF文件定义…

Pinia仓库统一管理

pinia独立维护 在src/stores文件夹下创建index.js文件&#xff0c;将main.js中关于pinia的语句放到index.js中 index.js文件内容&#xff1a; import { createPinia } from pinia import piniaPluginPersistedstate from pinia-plugin-persistedstate const pinia createPi…

电商API接口的接入|京东商品API接口接入说明

京东联盟商品接口API申请&#xff1a; 一、京东联盟 - 注册/登录 1、网址&#xff1a;https://union.jd.com 2、首次登录请先注册&#xff0c;注册成功后即可登录&#xff0c;需要完善个人信息&#xff0c;用于佣金结算。如果有京东商城的账号&#xff0c;也可以直接登录。 …

【存储】blotdb的原理及实现(2)

【存储】etcd的存储是如何实现的(3)-blotdb 在etcd系列中&#xff0c;我们对作为etcd底层kv存储的boltdb进行了比较全面的介绍。但是还有两个点没有涉及。 第一点是boltdb如何和磁盘文件交互。 持久化存储和我们一般业务应用程序的最大区别就是其强依赖磁盘文件。一方面文件数…

想要更快的文件传输?看看这些aspera的替代方案吧

随着数据量的增大&#xff0c;文件传输已经成为许多公司和组织日常工作中必不可少的环节之一。而对于大容量、海量数据的传输&#xff0c;普通的传输方式可能甚至无法胜任。Aspera作为一种高效的文件传输协议应运而生&#xff0c;其能够在处理大容量、高速传输方面表现出色。然…

Excel导出操作

<div class"right"> <el-button size"mini" click"exportEmployee">excel导出</el-button></div>安装file-saver $ npm i file-saver $ yarn add file-saver //下包后引入 import FileSaver from "file-sav…

开放式耳机怎么选?自费千元测评,百元、千元价位选哪个

开放式耳机以其不入耳式设计&#xff0c;更容易带给用户舒适的佩戴体验&#xff0c;也不影响使用中聆听周围声响&#xff0c;还可以保证长时间的舒适佩戴&#xff0c;适配漫长的通勤、游玩旅程。当然&#xff0c;开放式耳机种类也有许多&#xff0c;究竟哪一款更适合大家呢&…

离散系统的频域分析(数字信号处理实验2-1)

创建具有15 Hz和40 Hz分量频率的信号&#xff0c;叠加两个信号形成混合信号x&#xff0c;使用fft命令绘制x的频域图&#xff0c;标注频率为横坐标&#xff0c;平均能量为纵坐标。 文章目录 一.题目二.实验目的三.实验仪器四.实验原理1.MATLAB使用函数2.离散傅里叶变换(DFT)实验…

人工智能(pytorch)搭建模型21-基于pytorch搭建卷积神经网络VoVNetV2模型,并利用简单数据进行快速训练

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下人工智能(pytorch)搭建模型21-基于pytorch搭建卷积神经网络VoVNetV2模型&#xff0c;并利用简单数据进行快速训练。VoVNetV2模型是计算机视觉领域的一个重要研究成果&#xff0c;它采用了Voice of Visual Residual&…

“2024上海智博会、2024北京智博会”双展联动,3月上海,6月北京

“2024上海智博会、2024北京智博会”双展联动&#xff0c;将分别于3月和6月在上海和北京举办。这两个展会旨在充分展示智慧城市、人工智能、物联网、大数据、软件等新兴行业的最新产品和技术。 作为中国最具影响力和创新力的智能科技展会&#xff0c;上海智博会和北京智博会吸引…

C#——Delegate(委托)与Event(事件)

C#——Delegate&#xff08;委托&#xff09;与Event&#xff08;事件&#xff09; 前言一、Delegate&#xff08;委托&#xff09;1.是什么&#xff1f;2.怎么用&#xff1f;Example 1&#xff1a;无输入无返回值Example 2&#xff1a;有输入Example 3&#xff1a;有返回值Exa…