测试目标
获取SQlite的常规性能指标
测试环境
CPU:8核,Intel(R) Xeon(R) CPU E5-2430 0 @ 2.20GHz
内存:16G
磁盘:SSD
Linux 2.6.32
SQlite最新版本3.8.11
测试场景
1) 主键查询测试
2) 主键更新测试
3) 批量导入测试
初始化
1) 测试表结构
CREATE TABLE user(id integer primary key autoincrement,c1 int,c2 varchar(1000),c3 varchar(1000));
CREATE TABLE orders(id integer primary key autoincrement,user_id int,c1 varchar(1000),c2 varchar(1000));
2) 初始化数据
通过程序往user表和orders表中导入10w条记录,整个db文件在400M左右。
3) 测试说明
sqlite本身通过PRAGMA命令可以设置程序缓存大小( cache_size),但同时sqlite的缓存策略中并没有忽略操作系统缓存的影响,因此本文的测试结果使用默认的cache_size(2000个page),通过多次测试取平均值,来得到一个大概的性能指标。此外,sqlite主要用于嵌入式设备,而本文的测试基于PC,因此测试数据仅作参考。
单表主键查询
1) 测试说明
该项测试主要测试主键查询的性能,测试语句形如:
“select * from user where id = xxx”,xxx通过随机函数生成,由于生成的测试数据id的范围是[1-100000],通过随机函数生成[1-1000000]的随机数,基本能保证1%的命中率(实际测试中得到印证)。Sqlite支持读并发,因此该项测试测试了多线程并发情况下的性能,测试结果的时间单位为毫秒(ms)。多线程测试模型很简单,每个线程执行同样的查询10w次,计算总耗时时间,然后根据平均值与时间的比值,计算出QPS和TPS,通过参数SQLITE_OPEN_SHAREDCACHE控制是否启用共享缓存模式。
2) 测试结果
a) 非共享缓存模式
线程数目 | 1 | 2 | 4 | 8 | 16 |
第一轮 | 2886 | 3641 | 8392 | 19615 | 27875 |
第二轮 | 2867 | 3933 | 8088 | 21010 | 28635 |
第三轮 | 2821 | 4131 | 8077 | 21220 | 28689 |
第四轮 | 2941 | 4011 | 7787 | 20983 | 27965 |
第五轮 | 2896 | 3724 | 7881 | 21332 | 28654 |
平均值 | 2881 | 3949 | 7958 | 21136 | 28363 |
CPU% | 80% | 180% | 320% | 670% | 710% |
QPS | 3.4w | 5w | 5w | 3.8w | 5.6w |
表一
b) 共享缓存模式
线程数目 | 1 | 2 | 4 |
第一轮 | 3050 | 12616 | 26554 |
第二轮 | 3077 | 12331 | 26396 |
第三轮 | 3131 | 12327 | 27070 |
第四轮 | 3096 | 13014 | 27031 |
第五轮 | 2972 | 12866 | 27778 |
平均值 | 3065 | 12634 | 26965 |
CPU% | 80% | 120% | 120% |
QPS | 3.3w | 1.5w | 1.4w |
表二
3) 结果分析
从表一结果来,随着并发度提升,主机CPU利用率也随着上升;QPS由单线程3.4w,上升到4线程并发5w左右,但是到8线程又出现了一定的回落,16线程并发时,QPS又回到5w左右。测试过程中,通过观察CPU利用率和磁盘IO,基本上可以断定是CPU限制了QPS的上升。因为主机CPU核数为8核,因此CPU的利用率在高并发下可以接近800%,基本上已经到达极限。当然,从绝对值来看每秒5w的查询性能,也确实很不错!
从表二结果来看,设置共享缓存模式后,并发性能有很大的下降,从CPU利用率就可见一斑,QPS由单线程3.3w降低到8线程1.4w左右。关于这一点我一直很疑惑,为啥开了共享缓存后,并发性能还下降了。通过在程序运行过程中抓取堆栈并结合源码找到了原因,并发查询时,大量的线程会堵塞在sqlite3BtreeEnter函数中的mutex里面。共享内存模式下,进程内的多个线程通过共享同一个B树对象,达到共享内存的目的,B树对象通过一个mutex保护,正是由于这个mutex的竞争,导致并发度严重下降。所以共享内存模式虽然能减少内存的使用,但是以牺牲并发性能为代价的。
批量载入测试
1) 测试说明
导入数据是db最常用的一个功能,该项测试主要测试了3种模式的导入性能,单行单事务,多行事务和prepare模式的多行事务。主要模型如下:
a) 单行单事务
begininsert into user values(1,’xxx’);commit;begininsert into user values(1,’xxx’);commit;……
b) 多行单事务
begininsert into user values(1,’xxx’);insert into user values(2,’xxx’);……commit;
c) prepare绑定
beginprepare insert into user(id, c1) values(?,?);bind (id,c1)……commit;
2) 测试结果
| 单行事务 | 10w行事务 | 10w行事务 (prepare) |
第一轮 | 1693533 | 11856 | 9079 |
第二轮 | 1673983 | 11667 | 8375 |
第三轮 | 略 | 12075 | 8566 |
第四轮 | 略 | 11611 | 8773 |
第五轮 | 略 | 11331 | 8660 |
平均值 |
| 11671 | 8593 |
TPS | 60 | 8568 | 1.16w |
表三
3) 结果分析
从测试结果来看,单行事务和多行事务差别非常大,这也充分说明了,对于db而言,事务提交动作是非常耗时的。单行事务TPS只有60,而10w行事务TPS则达到了8500,有超过100倍的提升。与传统DBMS一样,sqlite提交事务时,也需要进行较慢的刷盘动作,因此刷1次盘与刷10w次盘,性能差别非常大。第三栏是prepare类型的事务,也是采用了10w行作为一个事务单位,但效果会更优。这主要原因是采用prepare模型事务,10w行记录只需要解析1次,而前者需要解析10w次,虽然解析时间不长,但积少成多,所以第三栏仅仅这一个优化点,就将TPS从8500提升到1.16w。
主键更新
1) 测试说明
本测试用例的语句也非常简单,就是简单的主键更新,将列值自增1。测试语句形如:update user set c1=c1+1 where id=xxx。SQLite不支持并发更新,因此测试写都是单线程。分别模拟单行事务,多行事务,观察SQLite的更新性能。统计更新1w行,程序执行的时间,并根据更新记录数目与执行时间计算TPS。
2) 测试结果
| 单行事务 | 1000行事务 | 1w行事务 |
第一轮 | 164784 | 16623 | 16232 |
第二轮 | 170256 | 16382 | 17514 |
第三轮 | 166387 | 17099 | 17696 |
第四轮 | 172987 | 17030 | 17753 |
第五轮 | 166543 | 16386 | 17787 |
平均值 | 169043 | 16724 | 17832 |
TPS | 59 | 598 | 560.7 |
表四
3) 结果分析
关于多行事务这一块,基本与导入操作类似,多行事务可以显著提高性能。同时,也要看到更新的TPS相比插入的TPS要相差很多。个人推断这个现象与磁盘IO有莫大关系,因为插入时,由于主键自增,写都是顺序写;而本测例的更新都是随机更新,而且产生的脏页远远大于cache_size,一定伴随着大量的随机写,导致更新性能比较差。