本文来自于腾讯bugly开发者社区,非经做者赞成,请勿转载,原文地址:http://dev.qq.com/topic/57b6a449433221be01499486android
Dev Club 是一个交流移动开发技术,结交朋友,扩展人脉的社群,成员都是通过审核的移动开发工程师。每周都会举行嘉宾分享,话题讨论等活动。算法
本期,咱们邀请了腾讯 WXG iOS 开发工程师——张三华,为你们分享《微信 iOS SQLite 源码优化实践》。sql
分享内容简介:数据库
SQLite是微信iOS选用的数据库,随着微信iOS客户端业务的增加,在重度用户的场景下,性能瓶颈逐渐显现。靠单纯地修改SQLite的参数配置,已经不能完全解决问题,所以咱们尝试从源码开始作深刻的优化。性能优化
内容大致框架:微信
下面是本期分享内容整理多线程
Hello,你们好,我是张三华,目前在微信主要负责 iOS 的基础优化工做。第一次进行这种微信群分享,可能准备的不是太充分。如有任何疑问,欢迎在分享结束后提问。架构
下面开始咱们今天的分享。并发
SQLite 是咱们在移动端经常使用的数据库,微信也是基于它封装了一层 ObjC 接口。咱们知道,微信里消息的收发是很频繁的,尤为是对于重度用户,这对于数据库的多线程并发和 I/O 是很大的挑战。app
一般对这部分作优化,有两种方式:
然而,前者有明显的瓶颈,后者则是个 endless 的工做。咱们但愿能一劳永逸地解决同类问题。这就是咱们本次所要分享的优化。
咱们先讲 SQLite 所提供的多线程并发方案。它对这方面的支持作的很不错,在使用上,只需
PRAGMA SQLITE_THREADSAFE=2
PRAGMA journal_mode=WAL
此时写操做会先 append 到 wal 文件末尾,而不是直接覆盖旧数据。而读操做开始时,会记下当前的 WAL 文件状态,而且只访问在此以前的数据。这就确保了多线程读与读、读与写之间能够并发地进行。
而写与写之间仍会互相阻塞。SQLite 提供了 Busy Retry 的方案,即发生阻塞时,会触发 Busy Handler,此时可让线程休眠一段时间后,从新尝试操做。重试必定次数依然失败后,则返回 SQLITE_BUSY
错误码。
下面这段代码是 SQLite 默认的 Busy Handler
上面介绍了 SQLite 多线程并发方案,接下来咱们把焦点放在 Busy Retry 这个方案的不足上。
Busy Retry 的方案虽然基本能解决问题,但对性能的压榨作的不够极致。在 Retry 过程当中,休眠时间的长短和重试次数,是决定性能和操做成功率的关键。
然而,它们的最优值,因不一样操做不一样场景而不一样。若休眠时间过短或重试次数太多,会空耗 CPU 的资源;若休眠时间过长,会形成等待的时间太长;若重试次数太少,则会下降操做的成功率。以下图
能够看到
对于这个的优化,简单的方法能够是修改休眠时间,尽最大限度缩短以上两段空耗的资源。
咱们经过 A/B Test 对不一样休眠时间进行了实验,获得了以下的结果
能够看到,假若休眠时间与重试成功率的关系,按照绿色的曲线进行分布,那么 p 点的值也不失为该方案的一个次优解。然而不一样业务和操做的需求,仍是有很大的不一样的。
既然 SQLite 的方案不行,咱们就要开始往深层探索新的可能性了。
SQLite是一个适配不一样平台的数据库,不只支持多线程并发,还支持多进程并发。它的核心逻辑能够分为两部分:
在架构最底端的 OS 层是对不一样操做系统的系统调用的抽象层。它实现了一个 VFS(Virtual File System),将 OS 层的接口在编译时映射到对应操做系统的系统调用。锁的实现也是在这里进行的。
SQLite 经过两个锁来控制并发。第一个锁对应 DB 文件,经过5种状态进行管理;第二个锁对应WAL文件,经过修改一个 16-bit 的 unsigned short int 的每个 bit 进行管理。尽管锁的逻辑有一些复杂,但此处并不需关心。这两种锁最终都落在 OS 层的 sqlite3OsLock、sqlite3OsUnlock 和 sqlite3OsShmLock 上具体实现。
它们在锁的实现比较相似。以 lock 操做在 iOS 上的实现为例:
而 SQLite 选择 Busy Retry 的方案的缘由也正是在此
文件锁没有线程锁相似 pthread_cond_signal 的通知机制。当一个进程的数据库操做结束时,没法经过锁来第一时间通知到其余进程进行重试。所以只能退而求其次,经过屡次休眠来进行尝试。
搞清楚了 SQLite 并发的实现,咱们就是能够开始改造了。
咱们知道,iOS app 是单进程的,并没有多进程并发的需求,这和 SQLite 的设计初衷是不相同的。这就给咱们的优化提供了理论上的基础。在 iOS 这一特定场景下,咱们能够舍弃兼容性,提升并发性。
新的方案修改成,当 OS 层进行 lock 操做时:
当 OS 层的 unlock 操做结束后:
新的方案能够在 DB 空闲时的第一时间,通知到其余正在等待的线程,最大程度地下降了空等待的时间,且准确无误。
此外,因为 Queue 的存在,当主线程被其余线程阻塞时,能够将主线程的操做“插队”到 Queue 的头部。当其余线程发起唤醒通知时,主线程能够有更高的优先级,从而下降用户可感知的卡顿
上面介绍了多线程并发的优化,接下来将介绍 I/O 方面的优化。
提到 I/O 效率的提高,最容易想到的就是 mmap了,它能够减小数据从 kernel 层到 user 层的数据拷贝,从而提升效率。
SQLite 不只支持 mmap,并且推荐使用,在大多数平台是在必定程度上默认打开的。然而早期的 iOS 版本的存在一些 bug,SQLite 在编译层就关闭了在 iOS 上对 mmap 的支持,而且后知后觉地在16年1月才从新打开。因此若是使用的 SQLite 版本较低,还需注释掉相关代码后,从新编译生成后,才能够享受上 mmap 的性能。
下图就是 SQLite 注释掉相关代码的 commit
开启 mmap 后,SQLite 性能将有所提高,但这还不够。由于它只会对 DB 文件进行了 mmap,而 WAL 文件享受不到这个优化。缘由以下:
开启 WAL 模式后,写入的数据会先 append 到 WAL 文件的末尾。待文件增加到必定长度后,SQLite 会进行 checkpoint。这个长度默认为1000个页大小,在 iOS 上约为3.9MB。
而在多句柄下,对 WAL 文件的操做是并行的。一旦某个句柄将 WAL 文件缩短了,而没有一个通知机制让其余句柄进行更新 mmap 的内容。此时其余句柄若使用 mmap 操做已被缩短的内容,就会形成 crash。而普通的 I/O 接口,则只会返回错误,不会形成 crash。所以,SQLite 没有实现对 WAL 文件的 mmap。
显然 SQLite 的设计是针对容量较小的设备,尤为是在十几年前的那个年代,这样的设备并不在少数。而随着硬盘价格日益下降,对于像 iPhone 这样的设备,几 MB 的空间已经再也不是须要斤斤计较的了。
另外一方面,文件从新增加,对于文件系统来讲,这就意味着须要消耗时间从新寻找合适的文件块。
**权衡二者,咱们能够改成 **
这里我没有贴具体代码须要改哪些地方,一方面是由于改动点较零散,另外一方面是代码上的改动并不难。这个优化的工做量主要是在 SQLite 原理和优化点的挖掘上了,你们能够根据优化方案去尝试。
不过咱们还有一些简单易行且效果还不错的小优化,但愿能够成为你们打开 SQLite 黑盒的一个契机。
如咱们在多线程优化时所说,对于 iOS app 并无多进程的需求。所以咱们能够直接注释掉 os_unix.c 中全部文件锁相关的操做。也许你会很奇怪,虽然没有文件锁的需求,但这个操做耗时也很短,是否有必要特地优化呢?其实并不全然。耗时多少是比出来。
SQLite 中有 cache 机制。被加载进内存的 page,使用完毕后不会马上释放。而是在必定范围内经过 LRU 的算法更新 page cache。这就意味着,若是 cache 设置得当,大部分读操做不会读取新的 page。然而由于文件锁的存在,原本只需在内存层面进行的读操做,不得不进行至少一次 I/O 操做。而咱们知道,I/O 操做是远远慢于内存操做的。
SQLite 会对申请的内存进行统计,而这些统计的数据都是放到同一个全局变量里进行计算的。这就意味着统计先后,都是须要加线程锁,防止出现多线程问题的。
如下 SQLite 内存申请的函数能够看到,当内存统计打开时,会跑代码的第二个 if,malloc 的先后被锁保护了起来。
其实这里内存申请的量不大,并非很是耗时的操做,但却很频繁。多线程并发时,各线程很容易互相阻塞。由于耗时很短,因此被阻塞的时间也很短暂。彷佛不会有太大问题。但频繁地阻塞却意味着线程不断地切换,这是个很影响性能的操做,尤为对于单核设备。
所以,若是不须要内存统计的特性,能够经过 sqlite3_config(SQLITE_CONFIG_MEMSTATUS, 0)进行关闭。这个修改虽然不须要改动源码,但若是不查看源码,恐怕是比较难发现的。
总的来讲,移动客户端数据库虽然不如后台数据库那么复杂,但也存在着很多可挖掘的技术点。
此次也只尝试了对 SQLite 原有的方案进行优化,而市面上还有许多优秀的数据库,如 LevelDB、RocksDB、Realm 等,它们采用了和 SQLite 不一样的实现原理。后续咱们将借鉴它们的优化经验,尝试更深刻的优化。
以上就是我今天的分享,谢谢你们。
Q1 :前一阵微信提示我微信数据文件发现有损坏,这个是什么缘由呢?
这个是数据库损坏,SQLite 是以B树结构存储的,若是某一个节点发生损坏,可能致使没法读取数据。损坏的缘由多种多样,如断电、文件系统错误、硬盘损坏等。据我所知不少产品都出现了相似问题。
你看到的那个是微信的损坏监测和修复逻辑,咱们作了自研的工具进行修复。这块咱们后续也会分享 db 损坏的监测、保护、修复方案的
Q2 :请问 sqlite 有时候会出 signal 11的错误,多是什么缘由致使的
signal 11 就是 SQLITE_CORRUPT,上面提到的数据库损坏的其中一种。另外一种是26 SQLITE_NOTADB
Q3 :请问微信在全文索引上有实践吗?有没有本身作本地的搜索索引
SQLite 是支持有全文索引的支持的,咱们要作的是提供一个好的,支持中文的分词器。
Q4 :请问微信在 db 文件修复上有什么心得呢?
看来你们对 db 文件损坏很关注啊。SQLite 提供了 PRAGMA integrity_check 的工具检测损坏 和 DUMP 工具导出损坏 db。但从实践来看,效果并不理想。咱们采用了按 BTree 结构遍历修复的方式,之后有机会能够分享给你们
Q5 :目前有没有已有的优化过的 sqlite 框架可供使用呢?
iOS上SQLite 的框架彷佛只有 FMDB 和 CoreData,坦白说两个都不是很好。咱们是本身封装的 WCDB 框架。
Q6 :微信的 orm 是怎么搞的
经过封装和规范来处理 ORM
Q7 :请问下多句柄怎么开启,是修改 sqlite 源码后再编译的吗?
这个最开始有提到了
Q8 :微信是怎么分析它的锁竞争的?
最重要的是读懂源码。辅助手段能够有 SQLite 官方的 Technical/Design Document 和 Instrument 工具
Q9 :请问有没有对能耗的监测和优化经验?
检测相关的咱们有卡顿监控系统,能够到咱们的公众号 WeMobileDev 上了解
Q10 :请问 sqlite 优化后有性能对比数据吗,差异有多大?
性能数据我以咱们的卡顿系统为准,多线程并发优化使得卡顿率从4.08%降至0.19,I/O 优化使得读卡顿从1.50%降至0.20%,写卡顿从1.18%降至0.21%
Q11 :iOS 客户端用操做数据库须要每次先 open,执行完了再 close,每次都这样,仍是 app 只须要开关一次比较好呢?
经常使用的 db 没有必要常常开关,db 占用的内存并不高,能够权衡一下
Q12 :微信对于本地空间不足会有一个强提醒,这是出于什么考虑?不一样机型有不一样的策略吗?
空间不足是个硬伤,所谓巧妇难为无米之炊。如16GB 的 iPhone,其实很影响正常使用了。不一样机型会作细化
Q13 :请问 sqlite 多线程机制,大概能应付多大量级的数据库操做(基本无卡顿),微信有这方面的测试体验吗,而后是使用了底层代码修改多线程机制后,有大概的提高量级吗?
优化的效果咱们是以卡顿系统检测到的为准的。可否减小用户感知到的卡顿,优化用户体验才是重点,而不在于能承受多大的量级
Q14 :微信对于数据库升级有没有特别优化的地方?或者说不一样版本的跳版本升级
不知道这个问题指的是 SQLite 的升级仍是表结构的升级。前者的话,暂时没看到 SQLite 新版本有比较大的特性值得咱们跟进。后者能够用 alter table 在封装层支持升级,性能损耗不大
Q15 :请问微信的 SQLite 有没有开启加密?若是有,性能是否有提高空间?
iOS 版本目前没有开启加密
Q16 :微信 sqllite 数据库用的内存数据库吗?那和文件数据库导入导出怎么控制的?
没有使用内存数据库
Q17 :能够问一下,目前作 iOS 版,没有针对 android 版么?
此次分享的大部份内容,对Android也是通用的,举一反三便可。
Q18 :请问下,句柄开几个比较合适?读写分离开来对性能是否会有提高呢?
咱们是按需生成新句柄的,并设了上限,若超过上限会有报警。若是同一时间并发量太大的话,其实更多要考虑业务层是否适用得当。至于业务层的使用,若能作细化那天然是更好
更多精彩内容欢迎关注bugly的微信公众帐号:
腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的状况以及解决方案。智能合并功能帮助开发同窗把天天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同窗定位到出问题的代码行,实时上报能够在发布后快速的了解应用的质量状况,适配最新的 iOS, Android 官方操做系统,鹅厂的工程师都在使用,快来加入咱们吧!