Sqlite学习笔记(三)&&WAL性能测试中列出了几种典型场景下WAL的性能数据,了解到WAL确实有性能优势,这篇文章将会详细分析WAL的原理,做到知其然,更要知其所以然。
WAL是什么
WAL(Write ahead logging)是一种日志模式,它是一种思想,普遍应用于关系型数据库。每个事务执行变更时,修改数据页,同时会产生日志,这样在事务提交后,不需要将修改的脏页刷盘,只需要将事务产生的日志落盘即可返回。WAL保证日志一定先于对应的脏页落盘,就是所谓的WAL。SQLITE在3.7版本以后引入WAL,它的WAL也基本采用这个原理,只不过SQLite实现比较简单,日志记录的是修改后的页,而不是所谓的修改日志。WAL模式下,SQlite中除了db文件,还包含了两个文件,.wal文件和.shm文件,前者是日志文件,后者是日志索引文件。
日志模式
SQLite中日志模式主要有DELETE和WAL两种,其他几种比如TRUNCATE,PERSIST,MEMORY基本原理都与DELETE模式相同,不作详细展开。DELETE模式采用影子分页技术(Shadow paging),DELETE模式下,日志中记录的变更前数据页内容;WAL模式下,日志中记录的是变更后的数据页内容。事务提交时,DELETE模式将日志刷盘,将DB文件刷盘,成功后,再将日志文件清理;WAL模式则是将日志文件刷盘,即可完成提交过程。那么WAL模式下,数据文件何时更新呢?这里引入了检查点概念,检查点的作用就是定期将日志中的新页覆盖DB文件中的老页,并通过参数wal_autocheckpoint来控制检查点时机,达到权衡读写的目的。
DELETE模式下,写事务直接更新db-page,并将old-page写入日志,读事务则直接读db-page,因为db-page中保存了提交的所有事务的更新。事务提交后,直接将日志文件删除;若事务需要回滚,则将日志中old-page中的内容覆盖db-page,恢复原始内容。WAL模式下,写事务将更新写到日志文件中,不更新db-page,事务提交时,也不影响db-page,只是将日志持久化而已。若事务回滚,则不将日志写入文件即可。由于最新的数据在日志文件中,那么如何读取到最新的数据呢?WAL模式通过end-mark(事务提交位点)达到这一目的。具体而已,事务开始时,会首先扫描日志文件,获取最近一个end-mark,在读取数据时,首先会判断page是否则在wal日志文件中存在,因为同一个page,一定是wal文件中的比db文件中的要新。如果存在,则使用,否则,再从db文件中获取指定的page。从流程上来看,这个过程比较慢,因为极端情况下,每次读都需要扫描wal文件和db文件。为了提高性能,WAL模式中有一个wal-index文件,这个文件记录了页号和该页在WAL文件中的偏移,并且wal-index文件采用共享缓存实现,从文件名也可以看到,后缀是.shm,因此判断page是否在wal文件存在的操作实质是一次内存读。wal-index采用hash表存储,因此查询效率也非常高。
与传统的DBMS不同,SQLite中记录的日志,实质是dirty-page,重做实质是对利用WAL中的日志页覆盖db-page,这种实现方式比较简单,同时也比较浪费空间,因为一个page是1k,即使只更新1byte,也会导致日志记录1k。
WAL的优势与劣势
1) 并发优势
SQLite为什么引入WAL,一定是WAL有很多好的特性。其中最主要的一点是WAL支持读写并发。在DELETE模式下,读写是互斥的。为什么WAL可以并发,而DELETE不行?我这里不打算详细展开WAL模式和DELETE模式的锁机制,后面有机会再单独写这一部分。从上面一节的分析可以知道,WAL模式下,写事务以append方式记录new-page,而读事务只会读取db-page和end-mark之前的wal日志,因此不会发生读写冲突的问题,读写可以并发。而DELETE模式下,写事务写的是db-page,读事务也是读db-page,所以读写不能并发。
2) 写性能优势
从前面的分析可知,WAL模式下,事务提交只需要写入日志文件即可,为了持久化,只需要一次fsync调用。而DELETE模式下,事务提交过程中,首先要确保日志落盘(保存old-page,用来rollback),这里需要一次fsync调用,然后再执行db文件刷盘,这里还需要一次fsync,并且修改的db-page可能是离散的,也会影响性能。而WAL写日志都是顺序写,相对于离散写又有很大的优势。因此DELETE模式下写性能会比WAL模式要差。测试结果也证明了这一点,这里可以参考测试报告。
3) WAL劣势
开启WAL后,每次读取page,都需要通过wal-index来确认page是否在WAL中,这个会产生一定的性能损耗。另外,会引入WAL文件,这个文件如果使用不当,可能会急剧膨胀,WAL文件变大后,意味着检索wal-index的代价也变高。而且由于SQLite一般用于端设备,空间也比较稀缺,因此要严格控制好WAL文件的大小。此外,WAL的索引文件采用共享内存实现,因此访问SQlite的进程不能跨机器。
开启WAL模式
通过命令pragma journal_mode=wal可以开启wal模式。前面我们提到开启WAL模式后,如果使用不当,可能导致WAL文件空间暴增,但我们有办法避免这种情况发生。这里主要介绍两个参数,wal_autocheckpoint和journal_size_limit。wal_autocheckpoint用来设置触发检查点的时机,默认是1000页,即当日志增长到1000页时,开始做检查点操作。这里要说明一点的是,SQLite中没有单独的检查点线程,如果设置1000,则触发写1000页的事务来进行检查点操作。因此这个事务的响应时间会比较长,而其它事务则不受影响。用来设置日志文件的大小,默认情况为-1,当这个参数设置时,若累计更新页大小超过journal_size_limit,也会导致检查点触发,用以重复利用日志文件,避免日志继续增长。
问题
1. WAL模式下,检查点是否会导致锁等待?
检查点包括自动检查点和手动检查点。通过PRAGMA wal_autocheckpoint=N命令,可以设置自动检查点,当N<=0时,自动检查点关闭,所有自动检查点的类型都是PASSIVE。默认情况下,自动检查点是开启的,N为1000。对于PASSIVE类型的检查点,不会影响读写事务。通过 PRAGMA schema.wal_checkpoint 命令可以手工触发一次检查点,比如PRAGMA schema.wal_checkpoint(PASSIVE)触发一次PASSIVE类型检查点,PRAGMA schema.wal_checkpoint(FULL)触发一次FULL类型检查点。对于FULL类型,由于需要将所有更新合并到DB文件,如果有读写事务没有结束,则需要等待;而且做检查点过程中,会堵塞新的读写事务。所以PASSIVE类型不会导致锁等待,而FULL类型,RESTART和TRUNCATE类型,会导致锁等待。
2. 检查点是否会导致事务响应时间变长?
对于自动检查点,根据wal_autocheckpoint=N设置,当更新page超过N时会触发一次检查点,那么当前的这个事务就需要等检查点执行完毕才返回,所以触发检查点的事务响应时间会变长。
3.WAL隔离级别是什么?
WAL模式下,读写可以并发,事务能否获得新数据的关键点在于wal-index的位点,SQLite中,只会在每个事务开始时获取一次位点,事务中多次读位点都是同一个,因此隔离级别是可重复读。
相关参数
1) journal_mode(日志模式)
默认是DELETE模式
DELETE:原始数据页存放在日志文件中,事务提交时,将文件删除。
TRUNCATE :与DELETE模式的区别是,清空日志文件,但不删除文件清空文件往往比删除文件要快。
PERSIST:与DELETE和TRUNCATE模式区别是,既不删除文件,也不清空文件,而是将日志文件第一个页设置标记(置0),这个也是为了提高性能。
MEMORY :内存模式,修改不落盘,无法保证事务的原子性。
OFF:不开启日志,这样没法保证事务的原子性。
WAL :write ahead
log,3.7.0引入,日志中记录修改页,提交时只需刷修改页。
2) journal_size_limit(日志文件大小)
默认值为-1,表示没有限制,单位是字节。
DELETE模式下,当日志增长超过阀值时,则进行截断。
WAL模式下,当日志增长超过阀值时,日志文件不再会被截断,而是重复利用,
因为通常情况下重复写的性能要好于追加的性能,而且也省磁盘空间。
default_journal_size_limit,用于设置日志文件的默认大小。
3) wal_checkpoint(检查点模式)
PASSIVE,默认自动检查点和主动检查点都是PASSIVE类型,将所有可以同步到db的数据都进行同步(不超过所有线程的end mark),不持有排它锁,因此不会影响其他读写事务。
FULL,将wal与db文件完全同步,需要等待所有读写事务都结束,并且会堵塞新的读写事务
RESTART,与FULL模式的区别是,下一个写线程从头开始写wal文件。
TRUNCATE,与FULL模式的区别是,将wal文件截断为0。
4) wal_autocheckpoint(检查点触发时机)
默认值为1000页,单位是页。当日志的增量到N页时,触发检查点操作,将wal_autocheckpoint设置为0或者-1,表示关闭检查点。
5) synchronous(同步模式)
默认设置是FULL
0(OFF):事务提交时,不作sync操作,直接返回。
1(NORMAL):事务提交时,日志头不作sync操作
2(FULL):每次事务提交,都强制刷日志(WAL),强制数据页(journal)
6) cache_size
默认值2000,单位为页
修改db的缓存页数目,临时生效
7) default_cache_size
默认值2000,单位为页
修改缓存页数目,永久生效若同时设置了cache_size和default_cache_size,以default_cache_size为准
参考文档
https://www.sqlite.org/wal.html