分布式数据库调优实践( 二 )


分布式数据库调优实践

文章插图
CPU 火焰图

分布式数据库调优实践

文章插图
锁火焰图
1.Memory allocation
内存是个好东西,现在计算机系统内存越来越大,软件也尽量通过使用内存来实现空间换时间以提高系统相应速度 。但是动态内存分配常常成为了高性能软件的性能瓶颈 。我们通过perf 来抓取系统内存的使用情况,并用火焰图显示出来:
分布式数据库调优实践

文章插图
 
这里明显看到的是很多动态内存分配发生在一个set的插入过程中 。Std::set内部使用的红黑树,每次结点的插入都要进行内存分配 。为了减少系统内存的动态分配与回收,SequoiaDB实现了一整套自己的内存管理机制 。最开始尽量在线程预分配好的内存池上分配空间,这点和tcmalloc的原理很接近,这时的开销最小,内存事先已经从操作系统分配好了,而且本线程上分配是无锁的 。但是如果线程内存池用完了,我们会到一个共享的预分配好的内存池上分配,这时会多一个锁的开销 。但这两处都用完了,我们才向操作系统申请 。从火焰图上看,我们基本上都走到向操作系统分配的分支中了 。针对这种情况,我们优化了set的实现 。当set中结点数量较小时,我们用一个flat的较小的array存放数据,避免了动态内存分配 。当结点数较多时,我们再转化成树型结构以提高查找效率 。但是我们会提高线程上允许的缓冲池的大小,特别是小结构线程池大小 。最终我们避免了绝大多少的动态内存分配与回收,提升了系统性能 。通过这块的分析,我们也反过来帮助确定那些query会用到大量数据,并优化对应的query 。
 
2.Cache line misses
大家知道现代CPU的主频非常高,常有超过3GHz,执行指令速度非常快 。但是我们存储访问速度始终跟不上,高速的内存又非常贵,这就是现代CPU里有几级不同速度不同大小内存的原因,常见的CPU内集成有L1,L2,L3级缓存 。CPU执行时需要从缓存中获取指令和数据 。在我们编译程序的时候,编译器会试图优化程序,使得CPU能有效的重用或预提取数据和指令 。当CPU在缓存中找不到合适的指令和数据时,就不等不从主存甚至磁盘上读取他们,这样的开销非常大,我们用CPU cache line miss来衡量这种情况出现的频繁程度 。
我们还是通过perf命令来搜集cache line miss的情况,
分布式数据库调优实践

文章插图
 
详细信息分解开来,最大一块是由monitor引起的
分布式数据库调优实践

文章插图
 
然后我们检查monitor相关的代码,发现代码中有个switch语句公有14个分支,但最常用的一个分支放在了后面 。我们只需要将其挪到前面,我们的miss就有显著下降 。
分布式数据库调优实践

文章插图
 
还有另外一种情况造成严重的cache line miss,就是使用原子变量,特别是频繁使用的原子变量 。因为一旦该变量被变更了,所有cache 里的值都会变成无效,那么CPU使用时一定会碰到cache line miss 。我们通过分析代码逻辑,对于某些常用的确不需要时时精确的值,我们可以在程序逻辑开始存为本地变量,避免过多的直接访问 。对于一些只需要单线程访问的变量,我们也避免使用原子变量 。
 
小结:
上面我们通过几个例子,为大家展现了如何通过系统工具进行数据库内核性能优化,同样的思路也可以适用于其他底层软件的开发调试 。在实际的实践过程中,除了使用合适的工具,更重要的是还要细心,有耐心和钻研的精神,一步步的下手,从现象中抽丝剥茧,找到根本原因 。




推荐阅读