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 条评论
登录 后参与评论

相关文章

来自专栏大宽宽的碎碎念

聊聊BIO,NIO和AIO (2)磁盘IO磁盘IO的优化AIO反思AIO

5379
来自专栏张善友的专栏

使用SuperWebSocket 构建实时 Web 应用

Web 应用的信息交互过程通常是客户端通过浏览器发出一个请求,服务器端接收和审核完请求后进行处理并返回结果给客户端,然后客户端浏览器将信息呈现出来,这种机制对于...

1908
来自专栏IMWeb前端团队

缓存策略

本文作者:IMWeb daihuimi 原文出处:IMWeb社区 未经同意,禁止转载 学习整理了web缓存的一些策略,如有不正确的地方,欢迎指正。 ?...

2088
来自专栏沈玉琛的专栏

当 MySQL 连接池遇上事务(二):消失的记录

ySQL连接池是一个很好的设计,通过将大量短连接转化为少量的长连接,从而提高整个系统的吞吐率。但是当跟事务一起使用时,如果使用方式不恰当时,就会发生一些奇怪的事...

3733
来自专栏枕边书

PHP 调用 Go 服务的正确方式 - Unix Domain Sockets

问题 可能是由于经验太少,工作中经常会遇到问题,探究和解决问题的过程总想记录一下,所以我写博客经常是问题驱动,首先介绍一下今天要解决的问题: 服务耦合 我们在开...

24111
来自专栏Clive的技术分享

缓存穿透、缓存击穿、缓存雪崩概念及解决方案缓存穿透缓存雪崩缓存击穿

缓存穿透 概念 访问一个不存在的key,缓存不起作用,请求会穿透到DB,流量大时DB会挂掉。 解决方案 采用布隆过滤器,使用一个足够大的bitmap,用于存储可...

5858
来自专栏编程

前端性能优化指南——网络篇

网络,在我们开发的页面的访问过程中,是最开始的一个环节,同时,也是一个非常重要的环节。 当我们在提及网络优化的时候,我们都会说些什么呢。 事实上来讲,如果可以话...

1959
来自专栏程序猿DD

为Spring Cloud Ribbon配置请求重试【Camden.SR2+】

当我们使用Spring Cloud Ribbon实现客户端负载均衡的时候,通常都会利用@LoadBalanced来让RestTemplate具备客户端负载功能,...

1969
来自专栏解Bug之路

解Bug之路-串包Bug

笔者很热衷于解决Bug,同时比较擅长(网络/协议)部分,所以经常被唤去解决一些网络IO方面的Bug。现在就挑一个案例出来,写出分析思路,以飨读者,希望读者在以后...

551
来自专栏开发技术

Redis Cluster的搭建与部署,实现redis的分布式方案

  上篇Redis Sentinel安装与部署,实现redis的高可用实现了redis的高可用,针对的主要是master宕机的情况,我们发现所有节点的数据都是一...

1401

扫码关注云+社区