发展历史【腾讯Bugly干货分享】微信iOS SQLite源码优化履

本文来源于腾讯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实际是支撑多线程(几乎)无锁地冒出操作。只待

  1. 开启配置 PRAGMA SQLITE_THREADSAFE=2
  2. 担保与一个句子柄同一时间只发一个线程在操作

    Multi-thread. In this mode, SQLite can be safely used by multiple
    threads provided that no single database connection is used
    simultaneously in two or more
    threads.

    若是再展SQLite的WAL模式(Write-Ahead-Log),多线程的并发性将获得更进一步的升级换代。

    这时候勾勒操作会先append到wal文件末尾,而非是直挂旧数据。而念操作起来时,会记录当前之WAL文件状态,并且只是看在此之前的数据。这就算保险了大多线程读与读读与写次可并作地展开。

    但是,阻塞的场面并非无会见发出。

  3. 当多线程写操作并发时,后来者还是必须在源码层等待之前的刻画操作就后才能够继承。

    SQLite提供了Busy Retry的方案,即来围堵时,会触发Busy
    Handler,此时可以吃线程休眠一段时间后,重新尝试操作。重试一定次数依然失败后,则赶回SQLITE_BUSY错误码。

    发展历史 1

    #### 3. SQLite Busy Retry方案的贫乏

    Busy
    Retry的方案虽基本能够化解问题,但对性的压迫做的不够极致。在Retry过程遭到,休眠时间之长和重试次数,是控制性能与操作成功率的重要性。

    只是,它们的卓绝优值,因不同操作不同场景而各异。若休眠时间太短或重试次数太多,会空耗CPU的资源;若休眠时间了长,会造成等待的时间太长;若重试次数太少,则会下跌操作的成功率。

    发展历史 2

    咱们经过A/B Test对两样之蛰伏时间开展了测试,得到了如下的结果:

    发展历史 3

    好看来,倘若休眠时间及重试成功率的关系,按照绿色的曲线进行分布,那么p点的价为算该方案的一个蹩脚优解。然而从究竟不遂人愿,我们用一个再次好之方案。

    #### 4. SQLite中的线程锁和经过锁

    用作持有十几年提高历史、且被大规模认同的数据库,SQLite的任何方案选都是发那个原因之。在一齐了解由来前,切忌盲目自信、直接上亲手修改。因此,首先使了解SQLite是什么支配并发的。

    发展历史 4

    SQLite是一个适配不同平台的数据库,不仅支持多线程并发,还支持多进程并发。它的中心逻辑可以分成两局部:

  4. Core层。包括了接口层、编译器和虚拟机。通过接口传入SQL语句,由编译器编译SQL生成虚拟机的操作码opcode。而虚拟机是基于生成的操作码,控制Backend的行。

  5. Backend层。由B-Tree、Pager、OS三片段构成,实现了数据库的存取数据的要害逻辑。

    以搭最底端的OS层是对不同操作系统的体系调用的抽象层。它实现了一个VFS(Virtual
    File
    System),将OS层的接口在编译时映射到对应操作系统的体系调用。锁的贯彻啊是以这边开展的。

    SQLite通过简单单锁来支配并发。第一只锁对应DB文件,通过5栽状态进行管制;第二个锁对应WAL文件,通过修改一个16-bit的unsigned
    short
    int的各国一个bit进行管理。尽管锁之逻辑来部分复杂,但这里并无待关注。这片栽锁最终还获得于OS层的sqlite3OsLocksqlite3OsUnlocksqlite3OsShmLock落得具体实现。

    其以沿之落实比较相近。以lock操作以iOS上之兑现呢例:

  6. 通过pthread_mutex_lock进行线程锁,防止其他线程介入。然后比较状态量,若当前状态不行跳转,则赶回SQLITE_BUSY

  7. 通过fcntl开展文件锁,防止其他进程与。若锁失败,则赶回SQLITE_BUSY

    使SQLite选择Busy
    Retry的方案的案由呢亏在这---文本锁没有线程锁类似pthread_cond_signal的通告机制。当一个经过的数据库操作结束时,无法透过锁来第一时间通知到外进程展开重试。因此只好降落而告其次,通过反复蛰伏来进行尝试。

    #### 5. 新的方案

    透过地方的各种分析、准备,终于得入手开始改了。

    我们理解,iOS
    app是单独进程的,并未曾多进程并发的求,这同SQLite的宏图初衷是免平等的。这就算为咱的优化提供了理论及的根基。在iOS这同一一定情景下,我们可舍兼容性,提高并发性。

    乍的方案改也,当OS层进行lock操作时:

  8. 通过pthread_mutex_lock进行线程锁,防止其他线程介入。然后比较状态量,若当前状态不行跳转,则以眼前期待跳转的状态,插入到一个FIFO的Queue尾部。最后,线程通过pthread_cond_wait
    休眠状态,等待其他线程的提示。

  9. 不经意文件锁

    当OS层的unlock操作结束晚:

  10. 取出Queue头部的状态量,并较状态是不是会跳转。若会跳转,则通过pthread_cond_signal_thread_np唤醒对应的线程重试。

    pthread_cond_signal_thread_np凡是Apple在pthread库中新增的接口,与pthread_cond_signal看似,它能够唤起一个等候条件锁之线程。不同之是,pthread_cond_signal_thread_np可以指定一个特定的线程进行提示。

    发展历史 5

    乍的方案得以以DB空闲时之第一时间,通知到其它正在等候的线程,最要命程度地落了空等待的流年,且准确是。此外,由于Queue的是,当主线程给其他线程阻塞时,可以用主线程的操作“插队”到Queue的脑袋。当其他线程发起唤醒通知时,主线程可以有再次强的优先级,从而降低用户可感知的卡顿。

    该方案上线后,卡顿检测体系检测及

  11. 等待线程锁的致的卡顿下降超过90%

  • SQLITE_BUSY的产生次数下降超过95%

    发展历史 6

    发展历史 7

    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:

    Many operations, especially I/O intensive operations, can be much
    faster since content does need to be copied between kernel space
    and user space. In some cases, performance can nearly
    double.

    The SQLite library may need less RAM since it shares pages with
    the operating-system page cache and does not always need its own
    copy of working pages.

    不过,你于iOS上这么安排或者不见面起任何功效。因为首的iOS版本的存在部分bug,SQLite在编译层就关闭了于iOS上针对mmap的支持,并且后知后觉地当16年1月才再度打开。所以只要利用的SQLite版本较逊色,还欲注释掉相关代码后,重新编译生成后,才堪享上mmap的属性。

    发展历史 8

    翻开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会对申请的内存进行统计,而这些统计的数码还是停放与一个全局变量里展开测算的。这就象征统计前后,都是急需加线程锁,防止出现多线程问题之。

    发展历史 9

    内存申请虽然非是怪耗时的操作,但也坏频繁。多线程并发时,各线程很轻互相阻塞。

    堵塞虽然也不行短暂,但反复地切换线程,却是个老影响性的操作,尤其是仅对设备。

    故此,如果无欲内存统计的性状,可以透过sqlite3_config(SQLITE_CONFIG_MEMSTATUS, 0)拓展倒闭。这个修改则非需变更源码,但如若非查看源码,恐怕是较难以发现的。

    优化上线后,卡顿监控体系监测及

  • DB写操作导致的卡顿下降超过80%

  • DB读操作造成的卡顿下降超过85%

    发展历史 10

    结语

    动客户端数据库虽然不如后台数据库那么复杂,但也在在群可是打的技术点。本次尝试了光针对SQLite原有的方案展开优化,而市面上还有众多良的数据库,如LevelDB、RocksDB、Realm等,它们运了与SQLite不同的贯彻原理。后续我们将以此为戒它们的优化涉,尝试还深切的优化。

发表评论

电子邮件地址不会被公开。 必填项已用*标注