WCDB 的 WAL 模式和异步 Checkpoint

WAL 模式是 SQLite 3.7.0 版本推出的改进写性能和并发性的功能,至今已经7年多了,但由于WAL是默认关闭的,可能有相当多的应用并没有用上,仍然使用性能较差的传统模式。

微信 APP 开启了 WAL 模式,同时还针对 WAL 做了一点改进 —— 异步 Checkpoint。通过 A/B Test,最终相比传统 Rollback 模式写耗时减少 70% 以上,还稍稍降低了 DB 损坏率。

WAL 和异步 Checkpoint

SQLite 实现 原子性提交和回滚操作 的默认方法是 rollback journal。当对 DB 进行写操作的时候,SQLite 首先将准备要修改部分的原始内容(以 Page 为单位)拷贝到“回滚日志”中,用于后续可能的 Rollback 操作以及 Crash、断电等意外造成写操作中断时恢复 DB 的原始状态,回滚日志存放于名为“DB文件名-journal”的独立文件中(以下简称“-journal”)。对原始内容做备份后,才能写入修改后的内容到 DB 主文件中,当写入操作完成,用户提交事务后,SQLite 清空 -journal 的内容,至此完成一个完整的写事务。

图:Rollback journal 工作模式

Rollback 模式中,每次拷贝原始内容或写入新内容后,都需要确保之前写入的数据真正写入到磁盘,而不是缓存在操作系统中,这需要发起一次 fsync 操作,通知并等待操作系统将缓存真正写入磁盘,这个过程十分耗时。

除了耗时的 fsync 操作,写入 -journal 以及 DB 主文件的时候,是需要独占整个 DB 的,否则别的线程/进程可能读取到写到一半的内容。这样的设计使得写操作与读操作是互斥的,并发性很差。

WAL 模式则改变了上述流程,写操作不直接写入 DB 主文件,而是写到“DB文件名-wal”文件(以下简称“-wal”)的末尾,并且通过 -shm 共享内存文件来实现 -wal 内容的索引。读操作时,将结合 DB 主文件以及 -wal 的内容返回结果。由于读操作只读取 DB 主文件和 -wal 前面没在写的部分,不需要读取写操作正在写到一半的内容,WAL 模式下读与写操作的并发由此实现。WCDB 的多线程并发,也是基于 WAL 模式下实现连接池实现的。

WAL 写操作除了上面的流程,还增加了一步:Checkpoint,即将 -wal 的内容与合并到 DB 主文件。 由于写操作将内容临时写到 -wal 文件,-wal 文件会不断增大且拖慢读操作,因此需要定期进行 Checkpoint 操作将 -wal 文件保持在合理的大小。Checkpoint 操作比较耗时且会阻塞读操作,但由于时效性要求较低,遇到堵塞可以暂时放弃继续 DB 读写操作,不至于太过影响读写性能。SQLite 官方默认的 Checkpoint 阈值是 1000 page,即当 -wal 文件达到 1000 page 大小时,写操作的线程在完成写操作后同步进行 Checkpoint 操作;Android Framework 的 Checkpoint 阈值是 100 page。

图:WAL 工作模式

基于 WAL 的基本工作方式,我们很容易想到两个优化点:

  • 写入 -wal 文件时不进行 fsync 操作,因为 -wal 文件损坏只影响新写入的没 Checkpoint 部分数据而非整个数据库损坏,影响相对小
  • 将需要进行 fsync 的 Checkpoint 操作放到独立线程执行,让写操作能尽快返回

这个就是异步 Checkpoint 的基本思路,减少和转移耗时较多而且性能不稳定的 fsync 操作,增加写操作性能和减少突然卡顿的可能性,同时不增加 DB 损坏率。基本思路确定了,就剩下参数上的调整了,Checkpoint 操作多频繁比较好?怎样的策略能得到最佳性能和损坏率的平衡?这些都是我们需要考虑的问题。

关于 WAL 模式和 Checkpoint 其他资料,可以参考 SQLite 官方文档。

策略选择与 A/B Test

异步 Checkpoint 策略中,最关键的点为 Checkpoint 的阈值。通过前面的先验知识,我们推测:阈值越低,Checkpoint 越频繁,写磁盘次数越多,-wal 大小越小,非阻塞时读性能越好(-wal 大小影响索引速度);阈值越高,则相反。

考虑到我们在独立线程做 Checkpoint,频繁 Checkpoint 的耗时可以掩盖掉,而维持 -wal 较小的话可以最优化读速度,所以首先尝试的策略是将阈值设为0,也就是一有任何提交,马上尝试 Checkpoint。

然而这个策略很快就发现问题了:如果一直有读写请求,频繁尝试 Checkpoint 会一直失败,以至 -wal 文件不断增大最终严重影响性能。现实中 APP 可能没有 Demo 中的情况极端,但也不能排除这种情况存在。为了解决这个问题,我们多引入了一个阻塞阈值,如果 Checkpoint 一直没有完成导致 -wal 堆积的 page 数达到阻塞阈值,则会阻塞其他读写操作让 Checkpoint 优先完成。

引入了阻塞阈值之后,经过线下测试,阻塞阈值设置在 100 ~ 300 左右在高压极端情况下性能损失较少,也不至于一直阻塞做 Checkpoint,最后我们选择普通阈值 0,阻塞阈值 100 的配置,记作 ACP(0/100)【ACP = Asynchronous Checkpoint】。

另一组策略是,引入异步 Checkpoint 但维持普通阈值在 100,这样 Checkpoint 频率会和 Android WAL 默认策略差不多。阻塞阈值则设置为 300,靠近高压下的性能拐点。这个策略记作 ACP(100/300)。

由于每次 Commit 都会 Checkpoint,每次 Checkpoint 前 SQLite 都会做 fsync 操作,因此写操作也做 fsync 就浪费了,基于这一点考虑,我们设置了 PRAGMA SYNCHRONOUS=NORMAL,在写操作时不做 fsync。

线上 A/B Test,使用 WCDB for Android 1.0.5 版本,一开始选取了三种不同配置:传统 Rollback 模式、默认 WAL 模式、WAL + ACP(0/100)。关注点主要是读写性能以及 DB 损坏概率 DB 损坏概率

性能数据对比

性能数据采集上,我们使用了两个指标:

  • 操作时间,即排除等待锁后 SQLite 真正处理读写请求的时间,主要反映 操作本身的性能
  • 等待时间,即真正进行操作前花在等待锁等步骤的平均时间,主要反映 并发能力

我们在灰度版本中挑选部分用户分别使用不同的模式,分别统计每个模式的读和写操作的操作时间和等待时间(单位:毫秒),汇总后得出统计数据。

配置

写耗时

写等待

Rollback

13.022

0.246

WAL

5.894

0.160

ACP

3.577

0.269

其中,ACP 的数据是 ACP(0/100) 与 ACP(100/300) 的混合,由于 ACP(100/300) 是后续上线的,没能将 ACP(0/100) 与 ACP(100/300) 分开收集。线下测试 ACP(100/300) 由于 Checkpoint 次数少因此造成的阻塞场景也少,写等待性能优于 ACP(0/100) ,写耗时差距不大。

从统计数据分析得出,使用 WAL 模式默认配置平均比 Rollback 模式写耗时减少 50% 以上;开启异步 Checkpoint 后比 WAL 模式默认配置还能再优化约 40%。

配置

读耗时

读等待

Rollback

1.294

0.239

WAL

1.394

0.025

ACP

1.436

0.025

读性能则如官方文档所说,WAL 模式单线程性能要稍稍差于 Rollback 模式,但由于 WAL 模式支持读写并发,WCDB 也开启了线程池,因此 WAL 模式的并发性要远远好于 Rollback 模式。

损坏率对比

性能指标以外,DB 损坏率也是我们关注的重点,SQLite 一些性能选项会影响到 DB 损坏的概率,提高性能的同时牺牲 DB 稳定性和损坏率的话,我们是不能接受的。损坏率的测量我们选用了一个指标是 每百亿次写操作损坏次数,由于不同配置的使用场景是完全一样的,因此我们认为写操作次数和使用人数是成正比关系的,用发生损坏与写操作次数的比值作比较,可以大致得出每种配置的损坏率比例关系。

经过多天的测量,不同配置的损坏率如图所示:

图:不同配置损坏率分布

图:不同配置损坏率比值

可以看出不同配置的损坏率,WAL 和 ACP(100/300) 处于同等级水平,Rollback 和 ACP(0/100) 分别是前者的 1.5 ~ 2 倍和 3 ~ 4 倍。

通过不同配置损坏率的比例和全网人数可以算出每天按人数算的损坏率,损坏最低的 WAL 和 ACP(100/300) 约为 1/30,000;最高的 ACP(0/100) 约为 1/10,000。

上面四种不同配置,对 DB 主文件的写操作和 fsync 操作频率是有明显区别的:

  • Rollback 模式每个事务,首先要将改动前数据写入 -journal 文件,再将改动后数据写入 DB 主文件,均进行 fsync;
  • WAL 模式默认使用 SYNCHRONOUS = FULL,每次事务都写入 -wal 文件并且 fsync,当 -wal 累计够阈值(100 page)后进行 Checkpoint 写入 DB 主文件并且 fsync。
  • ACP 情况与 WAL 默认相似,只是 Checkpoint 操作交给另外的线程进行;ACP(100/300) 的 Checkpoint 频率和 WAL 默认配置相似,ACP(0/100) 则要高不少。

SQLite 进行 fsync 操作目的是保证 fsync 成功后,达到原子性操作边界的 page 完整确切地写入了磁盘,但从上面损坏率的比例我们可以定性推测,操作系统上的 fsync 返回时并不能保证数据真正 100% 写到磁盘,因此对 DB 主文件的写操作越多,因为突然关机或操作系统 Crash 等场景导致 DB 文件处于中间状态而中断写入的概率就越高,因此损坏率越高。即 对 DB 主文件进行写操作的次数与 DB 损坏率正相关。

在 WCDB 中使用 WAL 和异步 Checkpoint

WAL 和异步 Checkpoint 是微信客户端数据库组件 WCDB 的重要优化点之一。iOS 版本默认开启 WAL 与异步 Checkpoint;Android 版本由于要保持与官方接口一致,默认不开启 WAL 与 Checkpoint,可以通过以下方式开启。从 Rollback 模式迁移到 WAL + 异步 Checkpoint 不需要做数据迁移,建议使用 WCDB 的 Android App 均尝试打开 WAL + 异步 Checkpoint。

// 以 WAL 模式打开
 DBSQLiteDatabase db = 
SQLiteDatabase.openOrCreateDatabaseInWalMode(path, 
password, cipher, null);

// 开启异步 Checkpoint
db.setAsyncCheckpointEnabled(true);

// 在 SQLiteOpenHelper 中打开 WAL
SQLiteOpenHelper helper = new SQLiteOpenHelper() {
        // 重载 onCreate 等方法
 };
helper.setWriteAheadLoggingEnabled(true);  

// 获取 DB 对象,然后开启异步 Checkpoint
SQLiteDatabase db = helper.getWritableDatabase();
db.setAsyncCheckpointEnabled(true);

原文发布于微信公众号 - WeMobileDev(WeMobileDev)

原文发表时间:2018-01-14

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏小白课代表

Autodesk 3DS MAX 2019安装教程

3D Studio Max,常简称为3d Max或3dsMAX,是Discreet公司开发的(后被Autodesk公司合并)基于PC系统的三维动画渲染和制作软件...

20320
来自专栏Debian社区

Pardus 17.1 发布,基于 Debian 的发行版

此次更新内容:新版本提供了三个不同名称的独立 ISO 映像表明意图 — Xfce,DDE(Deepin 桌面环境)和服务器。

13240
来自专栏Albert陈凯

常见编程语言对REPL支持情况小结

最近跟一个朋友聊起编程语言的一些特性,他有个言论让我略有所思:“不能REPL的都是渣”。当然这个观点有点偏激,但我们可以探究一下,我们常用的编程语言里面,哪些支...

36340
来自专栏沃趣科技

Oracle压缩黑科技(二)—压缩数据的修改

原文链接 https://www.red-gate.com/simple-talk/sql/oracle/compression-in-oracle-part-...

28560
来自专栏MYSQL轻松学

Mysql Group Replication介绍

一、Mysql Group Replication简介 Mysql Group Replication(MGR)是一个全新的高可用和高扩展的MySQL集群服务...

72540
来自专栏IT技术精选文摘

解Bug之路-记一次JVM堆外内存泄露Bug的查找

15030
来自专栏工科狗和生物喵

Mac OS X 下非官方软件自启动处理

正文之前 说是处理,想必也没几个人喜欢自启动的软件,我是一个控制欲比较强的人,开机自启搜狗输入法这个我能接受,但是像印象笔记啊,向日葵圆孔Client客户端这些...

68960
来自专栏AhDung

【Web】一个非常简单的移动web消息框

最近在写个简单的公众号页面,前端验证时有些信息要提示,很简单的需求实在不想找啥现成的轮子,又不至于用alert这么粗暴,遂写了个非常简单的消息框,效果如图:

20630
来自专栏解Bug之路

解Bug之路-记一次JVM堆外内存泄露Bug的查找 顶

JVM的堆外内存泄露的定位一直是个比较棘手的问题。此次的Bug查找从堆内内存的泄露反推出堆外内存,同时对物理内存的使用做了定量的分析,从而实锤了Bug的源头。笔...

10540
来自专栏软件开发 -- 分享 互助 成长

CY7C68013A的一点总结

一、 值得参考的资料:FX2 TechRefManual、USB应用开发宝典、 LabVIEW-USB通信简单教程(用于参考生成labview驱动程序)、USB...

24980

扫码关注云+社区

领取腾讯云代金券