测试目标
获取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的竞争,导致并发度严重下降。所以共享内存模式虽然能减少内存的使用,但是以牺牲并发性能为代价的。
上面的测试实际上是多线程模式下的共享和非共享模式下的测试结果。实际上使用sqlite连接有几种方式:单线程模式,多线程串行化模式和多线程模式,一般情况下,我们会选择多线程模式,这几种模式可以编译阶段指定,也可以在打开数据库连接时指定,还可以在运行时指定。
(1)编译阶段
这几种模式可以通过参数SQLITE_THREADSAFE在编译阶段指定,可以取值0,1,2,默认是1。这三种取值的含义如下:
0:单线程模式,即内部不做mutex保护,多线程运行sqlite不安全。
1:多线程的串行模式,sqlite帮助多线程实现串行化。
2:多线程的并发模式,要求同一个时刻,同一个连接不被多个线程使用。
(2)打开数据库阶段
除了可以在编译阶段指定运行模式,还可以在打开数据库时(sqlite3_open_v2())通过参数指定,主要的几个参数以及含义如下:
SQLITE_OPEN_NOMUTEX: 设置数据库连接运行在多线程模式(没有指定单线程模式的情况下)
SQLITE_OPEN_FULLMUTEX:设置数据库连接运行在串行模式。
SQLITE_OPEN_SHAREDCACHE:设置运行在共享缓存模式。
SQLITE_OPEN_PRIVATECACHE:设置运行在非共享缓存模式。
SQLITE_OPEN_READWRITE:指定数据库连接可以读写。
SQLITE_OPEN_CREATE:如果数据库不存在,则创建。
(3)运行时阶段
通过调用sqlite3_config接口,也可以设置运行模式。
若编译参数SQLITE_THREADSAFE=1 or SQLITE_THREADSAFE=2,那么可以在运行时设置线程模式。
SQLITE_CONFIG_SINGLETHREAD:单线程模式
SQLITE_CONFIG_MULTITHREAD:多线程模式,应用层保证同一个时刻,同一个连接只有一个线程使用。
SQLITE_CONFIG_SERIALIZED:串行模式,sqlite帮助多线程实现串行化。
批量载入测试
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的更新性能。统计更新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,一定伴随着大量的随机写,导致更新性能比较差。