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文件状态,并且只访问在此此前的数据。那就保障了四线程发展历史,读与读读与写里头可以并发地开展。

    唯独,阻塞的情状并非不会爆发。

  • 当八线程写操作并发时,后来者依然必须在源码层等待此前的写操作完结后才能继续。

    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是一个适配分裂平台的数据库,不仅支持三十二线程并发,还援助多进度并发。它的主导逻辑可以分成两有些:

  • 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层的sqlite3OsLocksqlite3OsUnlocksqlite3OsShmLock上实际完结。

    它们在锁的兑现比较相近。以lock操作在iOS上的贯彻为例:

  1. 通过pthread_mutex_lock举行线程锁,幸免其他线程参预。然后比较状态量,若当前气象不行跳转,则赶回SQLITE_BUSY
  2. 通过fcntl举行文件锁,幸免其余进度插手。若锁退步,则赶回SQLITE_BUSY

    而SQLite选用Busy
    Retry的方案的原故也多亏在此---文件锁没有线程锁类似pthread_cond_signal的通告机制。当一个经过的数据库操作截至时,不可以透过锁来第一时间文告到任何进度展开重试。因而只可以退而求其次,通过反复蛰伏来展开尝试。

    #### 5. 新的方案

    经过地点的各个分析、准备,终于可以出手起初修改了。

    我们了然,iOS
    app是单进度的,并从没多进度并发的急需,那和SQLite的统筹初衷是区其他。那就给大家的优化提供了驳斥上的基本功。在iOS这一一定情景下,我们得以放任包容性,进步并发性。

    新的方案修改为,当OS层进行lock操作时:

  3. 通过pthread_mutex_lock进行线程锁,幸免其他线程到场。然后相比较状态量,若当前事态不行跳转,则将近来希望跳转的情景,插入到一个FIFO的Queue底部。最后,线程通过pthread_cond_wait进去
    休眠状态,等待其余线程的提示。

  4. 不经意文件锁

    当OS层的unlock操作停止后:

  5. 取出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的头顶。当其余线程发起唤醒公告时,主线程可以有更高的优先级,从而降低用户可感知的卡顿。

    该方案上线后,卡顿检测系统检测到

  • 等待线程锁的诱致的卡顿下落超越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的统筹是对准容量较小的装置,越发是在十几年前的可怜时代,那样的配备并不在少数。而随着硬盘价格渐渐下跌,对于像HTC那样的设施,几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年六月才重新打开。所以如若接纳的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差别的兑现原理。后续大家将以此为戒它们的优化经验,尝试更尖锐的优化。

发表评论

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