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

发布时间:2018-09-23  栏目:w88优德官网电脑版  评论:0 Comments

正文来源于腾讯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不同之落实原理。后续我们以借鉴它们的优化涉,尝试再度深入之优化。

留下评论