测试目标
获取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 |
第一轮 |
2886 |
3641 |
8392 |
19615 |
第二轮 |
2867 |
3933 |
8088 |
21010 |
第三轮 |
2821 |
4131 |
8077 |
21220 |
第四轮 |
2941 |
4011 |
7787 |
20983 |
第五轮 |
2896 |
3724 |
7881 |
21332 |
平均值 |
2881 |
3949 |
7958 |
21136 |
CPU% |
80% |
180% |
320% |
670% |
QPS |
34w |
50.6w |
50.2w |
37.85w |
表一
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 |
32.6w |
15.8w |
14.8w |
表二
3) 结果分析
从表一结果来,随着并发度提升,主机CPU利用率也随着上升;QPS由单线程34w,上升到4线程并发50w左右,但是到8线程又出现了一定的回落,这说明,在高并发情况下,QPS由于其它因素,比如磁盘IO,或者程序本身的并发问题,会达到一定的瓶颈。从绝对值来看每秒50w的查询性能,也确实很不错!
从表二结果来看,设置共享缓存模式后,并发性能有很大的下降,从CPU利用率就可见一斑,QPS由单线程32.6w降低到8线程14.8w左右。关于这一点我一直很疑惑,为啥开了共享缓存后,并发性能还下降了。通过在程序运行过程中抓取堆栈并结合源码找到了原因,并发查询时,大量的线程会堵塞在sqlite3BtreeEnter函数中的mutex里面。共享内存模式下,进程内的多个线程通过共享同一个B树对象,达到共享内存的目的,B树对象通过一个mutex保护,正是由于这个mutex的竞争,导致并发度严重下降。所以共享内存模式虽然能减少内存的使用,但是以牺牲并发性能为代价的。
批量载入测试
1) 测试说明
导入数据是db最常用的一个功能,该项测试主要测试了3种模式的导入性能,单行单事务,多行事务和prepare模式的多行事务。主要模型如下:
a) 单行单事务
begin insert into user values(1,’xxx’); commit; begin insert into user values(1,’xxx’); commit; ……
b) 多行单事务
begin insert into user values(1,’xxx’);insert into user values(2,’xxx’);…… commit;
c) prepare绑定
begin prepare 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的更新性能。
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,一定伴随着大量的随机写,导致更新性能比较差。