本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57b58022433221be01499480
作者:张三华
前言
随着微信iOS客户端业务的增长,在数据库上遇到的性能瓶颈也逐渐凸显。在微信的卡顿监控系统上,数据库相关的卡顿不断上升。而在用户侧也逐渐能感知到这种卡顿,尤其是有大量群聊、联系人和消息收发的重度用户。
我们在对SQLite进行优化的过程中发现,靠单纯地修改SQLite的参数配置,已经不能彻底解决问题。因此从6.3.16版本开始,我们合入了SQLite的源码,并开始进行源码层的优化。
本文将分享在SQLite源码上进行的多线程并发、I/O性能优化等,并介绍优化相关的SQLite原理。
多线程并发优化
1. 背景
由于历史原因,旧版本的微信一直使用单句柄的方案,即所有线程共有一个SQLite Handle,并用线程锁避免多线程问题。当多线程并发时,各线程的数据库操作同步顺序进行,这就导致后来的线程会被阻塞较长的时间。
2. SQLite的多句柄方案及Busy Retry方案
SQLite实际是支持多线程(几乎)无锁地并发操作。只需
- 开启配置
PRAGMA SQLITE_THREADSAFE=2
-
确保同一个句柄同一时间只有一个线程在操作
倘若再开启SQLite的WAL模式(Write-Ahead-Log),多线程的并发性将得到进一步的提升。
此时写操作会先append到wal文件末尾,而不是直接覆盖旧数据。而读操作开始时,会记下当前的WAL文件状态,并且只访问在此之前的数据。这就确保了多线程读与读、读与写之间可以并发地进行。
然而,阻塞的情况并非不会发生。
-
当多线程写操作并发时,后来者还是必须在源码层等待之前的写操作完成后才能继续。
SQLite提供了Busy Retry的方案,即发生阻塞时,会触发Busy Handler,此时可以让线程休眠一段时间后,重新尝试操作。重试一定次数依然失败后,则返回
SQLITE_BUSY
错误码。3. SQLite Busy Retry方案的不足
Busy Retry的方案虽然基本能解决问题,但对性能的压榨做的不够极致。在Retry过程中,休眠时间的长短和重试次数,是决定性能和操作成功率的关键。
然而,它们的最优值,因不同操作不同场景而不同。若休眠时间太短或重试次数太多,会空耗CPU的资源;若休眠时间过长,会造成等待的时间太长;若重试次数太少,则会降低操作的成功率。
我们通过A/B Test对不同的休眠时间进行了测试,得到了如下的结果:
可以看到,倘若休眠时间与重试成功率的关系,按照绿色的曲线进行分布,那么p点的值也不失为该方案的一个次优解。然而事总不遂人愿,我们需要一个更好的方案。
4. SQLite中的线程锁及进程锁
作为有着十几年发展历史、且被广泛认可的数据库,SQLite的任何方案选择都是有其原因的。在完全理解由来之前,切忌盲目自信、直接上手修改。因此,首先要了解SQLite是如何控制并发的。
SQLite是一个适配不同平台的数据库,不仅支持多线程并发,还支持多进程并发。它的核心逻辑可以分为两部分:
Core层。包括了接口层、编译器和虚拟机。通过接口传入SQL语句,由编译器编译SQL生成虚拟机的操作码opcode。而虚拟机是基于生成的操作码,控制Backend的行为。
-
Backend层。由B-Tree、Pager、OS三部分组成,实现了数据库的存取数据的主要逻辑。
在架构最底端的OS层是对不同操作系统的系统调用的抽象层。它实现了一个VFS(Virtual File System),将OS层的接口在编译时映射到对应操作系统的系统调用。锁的实现也是在这里进行的。
SQLite通过两个锁来控制并发。第一个锁对应DB文件,通过5种状态进行管理;第二个锁对应WAL文件,通过修改一个16-bit的unsigned short int的每一个bit进行管理。尽管锁的逻辑有一些复杂,但此处并不需关心。这两种锁最终都落在OS层的
sqlite3OsLock
、sqlite3OsUnlock
和sqlite3OsShmLock
上具体实现。它们在锁的实现比较类似。以lock操作在iOS上的实现为例:
- 通过
pthread_mutex_lock
进行线程锁,防止其他线程介入。然后比较状态量,若当前状态不可跳转,则返回SQLITE_BUSY
-
通过
fcntl
进行文件锁,防止其他进程介入。若锁失败,则返回SQLITE_BUSY
而SQLite选择Busy Retry的方案的原因也正是在此---文件锁没有线程锁类似pthread_cond_signal的通知机制。当一个进程的数据库操作结束时,无法通过锁来第一时间通知到其他进程进行重试。因此只能退而求其次,通过多次休眠来进行尝试。
5. 新的方案
通过上面的各种分析、准备,终于可以动手开始修改了。
我们知道,iOS app是单进程的,并没有多进程并发的需求,这和SQLite的设计初衷是不相同的。这就给我们的优化提供了理论上的基础。在iOS这一特定场景下,我们可以舍弃兼容性,提高并发性。
新的方案修改为,当OS层进行lock操作时:
通过
pthread_mutex_lock
进行线程锁,防止其他线程介入。然后比较状态量,若当前状态不可跳转,则将当前期望跳转的状态,插入到一个FIFO的Queue尾部。最后,线程通过pthread_cond_wait
进入 休眠状态,等待其他线程的唤醒。-
忽略文件锁
当OS层的unlock操作结束后:
-
取出Queue头部的状态量,并比较状态是否能够跳转。若能够跳转,则通过
pthread_cond_signal_thread_np
唤醒对应的线程重试。pthread_cond_signal_thread_np
是Apple在pthread库中新增的接口,与pthread_cond_signal
类似,它能唤醒一个等待条件锁的线程。不同的是,pthread_cond_signal_thread_np
可以指定一个特定的线程进行唤醒。新的方案可以在DB空闲时的第一时间,通知到其他正在等待的线程,最大程度地降低了空等待的时间,且准确无误。此外,由于Queue的存在,当主线程被其他线程阻塞时,可以将主线程的操作“插队”到Queue的头部。当其他线程发起唤醒通知时,主线程可以有更高的优先级,从而降低用户可感知的卡顿。
该方案上线后,卡顿检测系统检测到
- 等待线程锁的造成的卡顿下降超过90%
-
SQLITE_BUSY的发生次数下降超过95%
I/O 性能优化
保留WAL文件大小
如上文多线程优化时提到,开启WAL模式后,写入的数据会先append到WAL文件的末尾。待文件增长到一定长度后,SQLite会进行checkpoint。这个长度默认为1000个页大小,在iOS上约为3.9MB。
同样的,在数据库关闭时,SQLite也会进行checkpoint。不同的是,checkpoint成功之后,会将WAL文件长度删除或truncate到0。下次打开数据库,并写入数据时,WAL文件需要重新增长。而对于文件系统来说,这就意味着需要消耗时间重新寻找合适的文件块。
显然SQLite的设计是针对容量较小的设备,尤其是在十几年前的那个年代,这样的设备并不在少数。而随着硬盘价格日益降低,对于像iPhone这样的设备,几MB的空间已经不再是需要斤斤计较的了。
因此我们可以修改为:
-
数据库关闭并checkpoint成功时,不再truncate或删除WAL文件只修改WAL的文件头的Magic Number。下次数据库打开时,SQLite会识别到WAL文件不可用,重新从头开始写入。
保留WAL文件大小后,每个数据库都会有这约3.9MB的额外空间占用。如果数据库较多,这些空间还是不可忽略的。因此,微信中目前只对读写频繁且检测到卡顿的数据库开启,如聊天记录数据库。
mmap优化
mmap对I/O性能的提升无需赘言,尤其是对于读操作。SQLite也在OS层封装了mmap的接口,可以无缝地切换mmap和普通的I/O接口。只需配置
PRAGMA mmap_size=XXX
即可开启mmap。There are advantages and disadvantages to using memory-mapped I/O. Advantages include:
然而,你在iOS上这样配置恐怕不会有任何效果。因为早期的iOS版本的存在一些bug,SQLite在编译层就关闭了在iOS上对mmap的支持,并且后知后觉地在16年1月才重新打开。所以如果使用的SQLite版本较低,还需注释掉相关代码后,重新编译生成后,才可以享受上mmap的性能。
开启mmap后,SQLite性能将有所提升,但这还不够。因为它只会对DB文件进行了mmap,而WAL文件享受不到这个优化。
WAL文件长度是可能变短的,而在多句柄下,对WAL文件的操作是并行的。一旦某个句柄将WAL文件缩短了,而没有一个通知机制让其他句柄进行更新mmap的内容。此时其他句柄若使用mmap操作已被缩短的内容,就会造成crash。而普通的I/O接口,则只会返回错误,不会造成crash。因此,SQLite没有实现对WAL文件的mmap。
还记得我们上一个优化吗?没错,我们保留了WAL文件的大小。因此它在这个场景下是不会缩短的,那么不能mmap的条件就被打破了。实现上,只需在WAL文件打开时,用
unixMapfile
将其映射到内存中,SQLite的OS层即会自动识别,将普通的I/O接口切换到mmap上。其他优化
禁用文件锁
如我们在多线程优化时所说,对于iOS app并没有多进程的需求。因此我们可以直接注释掉
os_unix.c
中所有文件锁相关的操作。也许你会很奇怪,虽然没有文件锁的需求,但这个操作耗时也很短,是否有必要特意优化呢?其实并不全然。耗时多少是比出来。SQLite中有cache机制。被加载进内存的page,使用完毕后不会立刻释放。而是在一定范围内通过LRU的算法更新page cache。这就意味着,如果cache设置得当,大部分读操作不会读取新的page。然而因为文件锁的存在,本来只需在内存层面进行的读操作,不得不进行至少一次I/O操作。而我们知道,I/O操作是远远慢于内存操作的。
禁用内存统计锁
SQLite会对申请的内存进行统计,而这些统计的数据都是放到同一个全局变量里进行计算的。这就意味着统计前后,都是需要加线程锁,防止出现多线程问题的。
内存申请虽然不是非常耗时的操作,但却很频繁。多线程并发时,各线程很容易互相阻塞。
阻塞虽然也很短暂,但频繁地切换线程,却是个很影响性能的操作,尤其是单核设备。
因此,如果不需要内存统计的特性,可以通过
sqlite3_config(SQLITE_CONFIG_MEMSTATUS, 0)
进行关闭。这个修改虽然不需要改动源码,但如果不查看源码,恐怕是比较难发现的。优化上线后,卡顿监控系统监测到
DB写操作造成的卡顿下降超过80%
-
DB读操作造成的卡顿下降超过85%
结语
移动客户端数据库虽然不如后台数据库那么复杂,但也存在着不少可挖掘的技术点。本次尝试了仅对SQLite原有的方案进行优化,而市面上还有许多优秀的数据库,如LevelDB、RocksDB、Realm等,它们采用了和SQLite不同的实现原理。后续我们将借鉴它们的优化经验,尝试更深入的优化。
【腾讯Bugly干货分享】微信iOS SQLite源码优化实践的更多相关文章
-
【腾讯Bugly干货分享】iOS黑客技术大揭秘
本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/5791da152168f2690e72daa4 “8小时内拼工作,8小时外拼成长 ...
-
【腾讯Bugly干货分享】iOS 中 HTTPS 证书验证浅析
本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:https://mp.weixin.qq.com/s/-fLLTtip509K6pNOTkflPQ 导语 本 ...
-
【腾讯Bugly干货分享】iOS App 签名的原理
本文来自 WeRead 团队博客: http://wereadteam.github.io/ iOS 签名机制挺复杂,各种证书,Provisioning Profile,entitlements,Ce ...
-
【腾讯Bugly干货分享】React移动web极致优化
本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/579083d1c9da73584b02587d 最近一个季度,我们都在为手Q家校 ...
-
【腾讯Bugly干货分享】微信读书iOS性能优化
本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/578c93ca9644bd524bfcabe8 “8小时内拼工作,8小时外拼成长 ...
-
【腾讯Bugly干货分享】微信mars 的高性能日志模块 xlog
本文来自于腾讯bugly开发者社区,未经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/581c2c46bef1702a2db3ae53 Dev Club 是一个交流移动 ...
-
【腾讯Bugly干货分享】微信热补丁Tinker的实践演进之路
本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57ad7a70eaed47bb2699e68e Dev Club 是一个交流移动 ...
-
【腾讯Bugly干货分享】微信终端跨平台组件 Mars 系列 - 我们如约而至
导语 昨天上午,微信在广州举办了微信公开课Pro.于是,精神哥这两天的朋友圈被小龙的"八不做"刷屏了.小伙伴们可能不知道,下午,微信公开课专门开设了技术分论坛.在分论坛中,微信开源 ...
-
【腾讯Bugly干货分享】聊聊苹果的Bug - iOS 10 nano_free Crash
本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:https://mp.weixin.qq.com/s/hnwj24xqrtOhcjEt_TaQ9w 作者:张 ...
随机推荐
-
PLSQL数据导入导出问题解决(空表、大字段表、表空间错误等)
PLSQL使用方法简单,平常使用较多,但在平常使用过程中,遇到一些问题,下面简单罗列并进行解决.这些解决方法大多通过网络查找获得,这里只是进行简单整理. 使用的数据库版本为:Oracle11g. 通用 ...
-
[纯干货] MySQL索引背后的数据结构及算法原理
摘要 本文以MySQL数据库为研究对象,讨论与数据库索引相关的一些话题.特别需要说明的是,MySQL支持诸多存储引擎,而各种存储引擎对索引的支持也各不相同,因此MySQL数据库支持多种索引类型,如BT ...
-
c# r3 inline hook
前言 老婆喜欢在QQ游戏玩拖拉机,且安装了一个记牌器小软件,打开的时候弹出几个IE页面加载很多广告,于是叫我去掉广告.想想可以用OD进行nop填充,也可以写api hook替换shellexecute ...
-
linux shutdown关闭系统命令使用介绍(转)
shutdown命令是最常用的关闭系统命令,不仅可以用于立即关闭系统,还可以在指定时间关闭系统 linux关闭系统命令之shutdown 关闭.重启系统 Linux是一个多用户.多任务系统,如果不 ...
-
浅谈Log4net在项目中如何记录日志
一 引入背景 在软件开发周期中,无论是开发中,或是测试中,或是上线后,选择合适的工具监控程序的运行状态至关重要,只有如此,才能更好地排查程序问题和检测程序性能问题等.本篇文章主要与大家分享,如何 ...
-
CODING 敏捷实践完全指南
你好,欢迎使用 CODING! 这份最佳实践将帮助你掌握 CODING 敏捷管理工具,更好地实践敏捷开发流程. 更多实践案例持续更新中 什么是敏捷研发 敏捷研发是涉及整个软件工程的理念与实践,它的核心 ...
-
导入Dynamic Web Project后程序有红叉但是可以运行
解决方法: 进入工程下的.setting文件夹,用记事本编辑org.eclipse.wst.common.project.facet.core.xml, 把<runtime name=" ...
-
Android--通知之Toast
前言 这篇博客讲解一下Android下的一个简单信息提示的方式:Toast.如果一直看我的博客,会发现在之前的Demo中,一直有用到Toast去提示消息,在这篇博客中就专门讲它.Toast提供一个浮动 ...
-
uniDBGrid实行多选表格行
http://blog.csdn.net/shuiying/article/details/11374655 uniDBGrid本身是支持checkBox多选的,但必须是Boolean的字段才行,只要 ...
-
JMeter压力测试及服务器状态监控教程
转载自:https://blog.csdn.net/cbzcbzcbzcbz/article/details/78023327 前段时间公司需要对服务器进行压力测试,包括登录前的页面和登录后的页面,主 ...