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

前不久,我们接到客户长江电力的反馈,称在生产环境中进行高并发查询,例如包含数百个测点的近千个并发作业,在从近三月的数据中取数或聚合计算时,会出现作业超时,但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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

rabbitMQ对优先级队列的使用

注意事项&#xff1a; 1.队列设置优先级 权制范围&#xff08;0-255&#xff09;推荐0-10 否则浪费CPU与内存 2.发消息时给消息设置优先级 3.消息需要完全事先在队列中&#xff0c;在被消费者消费 会被排序&#xff0c;否则边生产边消费不会达到预期的队列优先效果。 优先级队列…

Skywalking接入实际应用做日志跟踪

Skywalking客户端挂载 从官网下载skywalking-agent客户端&#xff0c;并挂在到应用服务器指定目录 挂载到应用主机中,好处是解决打包应用镜像的时候&#xff0c;镜像过大&#xff0c;部署成本过高。 docker-compose部署应用,并接入skywalking服务,这里以gateway为例 versio…

vr红色教育虚拟展馆全景制作提升单位品牌形象

720全景展馆编辑平台以其独特的优势&#xff0c;为展览行业带来了革命性的变革。这种创新的技术应用为参展商提供了更高效、更便捷、更全面的展示解决方案&#xff0c;进一步提升了展览行业的水平和影响力。 一、提升展示效果&#xff0c;增强品牌形象 720全景展馆编辑平台通过…

Aseprite for mac(像素动画制作工具)

Aseprite是一款专业的像素绘图软件&#xff0c;旨在方便用户创建动画和像素艺术作品。该软件提供了一系列强大的绘图工具和动画功能&#xff0c;使其成为许多游戏开发者、动画师和艺术家的首选工具之一。 Aseprite具有用户友好的界面&#xff0c;易于上手&#xff0c;使用户可以…

MAX/MSP SDK学习06:内存管理

提供两种内存分配方式&#xff1a;①简单指针&#xff0c;②句柄&#xff08;二级指针&#xff09;&#xff1b;官方文档建议使用前者。 // 简单指针 char *ptr; ptr sysmem_newptr(2000); post("I have a pointer %lx and it is %ld bytes in size",ptr, sysmem_p…

存在即合理,低代码的探索之路

目录 一、前言 二、低代码迅速流行的原因 三、稳定性和生产率的最佳实践 四、程序员用低代码开发应用有哪些益处&#xff1f; 1、提升开发价值 2、利于团队升级 一、前言 低代码的热潮至今未消停&#xff0c;从阿里钉钉跨平台协作方式&#xff0c;再到飞书上的审批流程&#xf…

OMP: Error #15: Initializing libiomp5md.dll

问题描述 在conda虚拟环境运行程序时&#xff0c;出现以下的错误&#xff1a; 问题原因 anaconda的环境下存在两个libiomp5md.dll文件。 解决方法 一、在代码上加上限制&#xff08;每次都得加&#xff09; import os os.environ[KMP_DUPLICATE_LIB_OK]True 这种方法解决不…

6.一维数组——用冒泡法,选择法将5个整数由大到小排序

文章目录 前言一、题目描述 二、题目分析 三、解题 程序运行代码&#xff08;冒泡法&#xff09;程序运行代码&#xff08;选择法&#xff09; 前言 本系列为一维数组编程题&#xff0c;点滴成长&#xff0c;一起逆袭。 一、题目描述 用冒泡法将5个整数由大到小排序 二、题目…

印刷企业建设数字工厂管理系统的工作内容有哪些

随着科技的不断进步&#xff0c;数字工厂管理系统在印刷企业中的应用越来越广泛。这种系统可以有效地整合企业内外资源&#xff0c;提高生产效率&#xff0c;降低生产成本&#xff0c;并为印刷企业提供更好的业务运营与管理模式。本文将从以下几个方面探讨印刷企业建设数字工厂…

奇异值分解SVD(Singular Value Decomposition)

一种理解方式&#xff0c;值得学习&#xff08;分解时空矩阵&#xff09; 先在这里阐述一下SVD的用途吧&#xff0c;具体细节稍后再做补充 1.通过SVD对数据的处理&#xff0c;我们可以使用小得多的数据集来表示原始数据集&#xff0c;这样做实际上是去除了噪声和冗余信息&…

开发定制化抖音票务小程序的技术解析

通过定制化抖音票务小程序&#xff0c;可以为用户提供更加个性化的活动体验&#xff0c;同时也为企业和品牌提供了更多的营销机会。 一、小程序开发框架的选择 在开发定制化抖音票务小程序之前&#xff0c;选择合适的小程序开发框架至关重要。目前&#xff0c;主流的小程序框…

提高项目估算准确性的常用技巧

项目估算准确性对项目而言非常重要&#xff0c;其为项目决策提供依据&#xff0c;有助于进行资源规划&#xff0c;更好地进行风险管理和进度管理等。如果项目估算不准确&#xff0c;很可能导致项目成本超出预算&#xff0c;资源不足等问题&#xff0c;这增加了项目的风险和不确…

【Linux】匿名管道+进程池

文章目录 前置知识一、管道的原理二、管道的特性三、管道的接口四、使用管道实现简单的进程池解决进程池的一个小问题 前置知识 一个进程在创建时&#xff0c;会默认打开三个文件&#xff0c;分别是&#xff1a;stdin&#xff0c;stdout&#xff0c;stderr 进程中有一个维护进…

Leetcode—55.跳跃游戏【中等】

2023每日刷题&#xff08;四十&#xff09; Leetcode—55.跳跃游戏 贪心法实现代码 #define MAX(a, b) ((a > b)? (a): (b))bool canJump(int* nums, int numsSize) {int k 0;for(int i 0; i < numsSize; i) {if(i > k) {return false;}k MAX(k, i nums[i]);}r…

DDR-MIG 学习记录

MIG调试总结&#xff1a; 对vivado软件中用于控制DDR2 / DDR3 的 控制器MIG(Memory Interface Generator)IP核进行了仿真测试&#xff0c;以学习如何用IP核来控制FPGA板载SDRAM的读写。我们只需要学会MIG的接口控制就可以。 ①配置IP核 Xilinx 的 DDR 控制器的名称简写为 MIG&…

vue3+vite+ts项目打包时出错

项目中引入了element-plus国家化的配置&#xff0c;然后进行项目打包&#xff0c;报下面的错误 解决方法&#xff1a; 在main.ts中添加 // ts-ignore

【SQL SERVER】定时任务

oracle是定时JOB&#xff0c;sqlserver是创建作业&#xff0c;通过sqlserver代理实现 先看SQL SERVER代理得服务有没有开 选择计算机右键——>管理——>服务与应用程序——>服务——>SQL server 代理 然后把SQL server 代理&#xff08;MSSQLSERVER&#xff09;启…

Vue和React配置解决跨域,proxy代理两步搞定

Vue配置&#xff1a; 第一步&#xff1a; 找到 vite.config.js 文件 进行如下代码配置 import { defineConfig } from "vite"; import vue from "vitejs/plugin-vue"; export default defineConfig({plugins: [vue()],server: {/*** /api 是代理标识*/p…

基于vue框架的美团类药品点单系统

基于VUE框架的美团类药品点单管理系统 摘要&#xff1a; 2019年12月以来&#xff0c;中国湖北省武汉市爆发新型冠状病毒引发的肺炎疫情&#xff0c;并通过人传人的感染方式快速向全国其他地区扩散。全国上下万众一心抗击病毒&#xff0c;湖北广东浙江等24省市启动重大卫生突发…

从四个典型场景看如何将数据集成“用到实处”

一、数据集成概念 数据集成是指将来自不同数据源的数据整合到一个统一的数据存储中&#xff0c;并确保这些数据能够互相关联、交换和共享的过程。在数据集成的过程中&#xff0c;数据通常需要经过清洗、转换和统一格式化等步骤&#xff0c;以确保数据的一致性、完整性和可用性…