腾讯Bugly干货分享,源码优化实践ca88手机版登录网页

本文来源于腾讯bugly开发者社区,非经小编同意,请勿转发,原文地址:http://dev.qq.com/topic/57b58022433221be01499480

come from
2016-08-18张三华
腾讯Bugly😉

作者:张三华

前言

前言

乘胜微信iOS客户端业务的升高,在数据库上相见的品质瓶颈也逐步彰显。在微信的卡顿监控系统上,数据库相关的卡顿不断升腾。而在用户侧也渐渐能感知到那种卡顿,尤其是有大批量群聊、联系人和音讯收发的重度用户。

俺们在对SQLite举行优化的历程中发觉,靠单纯地修改SQLite的参数配置,已经无法彻底解决难点。因而从6.3.16本子开首,我们合入了SQLite的源码,并伊始进行源码层的优化。

本文将享用在SQLite源码上进展的十二线程并发、I/O质量优化等,并介绍优化相关的SQLite原理。

乘势微信 iOS
客户端业务的滋长,在数据库上境遇的性质瓶颈也渐渐展现。在微信的卡顿监控系统上,数据库相关的卡顿不断进步。而在用户侧也逐年能感知到那种卡顿,尤其是有恢宏群聊、联系人和音讯收发的重度用户。

三十二线程并发优化

俺们在对 SQLite 进行优化的进程中发觉,靠单纯地修改 SQLite
的参数配置,已经不可以彻底解决难点。由此从6.3.16版本开头,大家合入了
SQLite 的源码,并初阶进行源码层的优化。

1. 背景

是因为历史由来,旧版本的微信直接利用单句柄的方案,即所有线程共有一个SQLite
Handle,并用线程锁防止三十二线程难题。当二十四线程并发时,各线程的数据库操作同步顺序进行,那就招致新兴的线程会被卡住较长的年华。

正文将享受在 SQLite 源码上拓展的十六线程并发、I/O
质量优化等,并介绍优化相关的 SQLite 原理。

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错误码。

    ca88手机版登录网页 1

    #### 3. SQLite Busy Retry方案的阙如

    Busy
    Retry的方案纵然基本能缓解难题,但对品质的搜刮做的不够极致。在Retry进度中,休眠时间的尺寸和重试次数,是控制质量和操作成功率的严重性。

    不过,它们的最优值,因分化操作不一致意况而各异。若休眠时间太短或重试次数太多,会空耗CPU的资源;若休眠时间过长,会招致等待的时日太长;若重试次数太少,则会回落操作的成功率。

    ca88手机版登录网页 2

    俺们透过A/B Test对差异的蛰伏时间举行了测试,得到了如下的结果:

    ca88手机版登录网页 3

    可以观望,如若休眠时间与重试成功率的关联,根据红色的曲线举行分布,那么p点的值也真是该方案的一个次优解。不过事总不遂人愿,我们须要一个更好的方案。

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

    用作所有十几年发展历史、且被普遍认可的数据库,SQLite的其余方案选用都是有其缘由的。在一齐知道由来此前,切忌盲目自信、直接上手修改。由此,首先要打听SQLite是怎么样支配并发的。

    ca88手机版登录网页 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可以指定一个特定的线程举行提示。

    ca88手机版登录网页 5

    新的方案得以在DB空闲时的第一时间,公告到此外正在等候的线程,最大程度地回落了空等待的光阴,且准确科学。别的,由于Queue的存在,当主线程被此外线程阻塞时,可以将主线程的操作“插队”到Queue的底部。当其他线程发起唤醒文告时,主线程可以有更高的优先级,从而下落用户可感知的卡顿。

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

  • 等待线程锁的造成的卡顿下落领先90%

  • SQLITE_BUSY的发出次数下落当先95%

    ca88手机版登录网页 6

    ca88手机版登录网页 7

    I/O 品质优化

    #### 保留WAL文件大小

    如上文三十二线程优化时涉嫌,开启WAL形式后,写入的数据会先append到WAL文件的最终。待文件增进到自然长度后,SQLite会进行checkpoint。这些长度默许为1000个页大小,在iOS上约为3.9MB。

    一样的,在数据库关闭时,SQLite也会举行checkpoint。分化的是,checkpoint成功之后,会将WAL文件长度删除或truncate到0。下次开拓数据库,并写入数据时,WAL文件需求再行拉长。而对此文件系统来说,那就表示必要消耗时间再次寻找合适的文件块

    明明SQLite的设计是本着容量较小的配备,更加是在十几年前的不得了年代,那样的设施并不在少数。而随着硬盘价格稳步下落,对于像中兴那样的设备,几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的质量。

    ca88手机版登录网页 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会对报名的内存举办计算,而那么些总结的数量都是放置同一个全局变量里展开测算的。这就象征计算前后,都是亟需加线程锁,防止出现三十二线程难题的。

    ca88手机版登录网页 9

    内存申请就算不是老大耗时的操作,但却很频仍。多线程并发时,各线程很简单互相阻塞。

    闭塞即便也很短暂,但屡屡地切换线程,却是个很影响属性的操作,尤其是单核设备。

    从而,要是不须求内存计算的特性,可以经过sqlite3_config(SQLITE_CONFIG_MEMSTATUS, 0)举办倒闭。这几个修改即便不需求变更源码,但要是不查看源码,恐怕是比较难发现的。

    优化上线后,卡顿监控序列监测到

  • DB写操作造成的卡顿下跌超过80%

  • DB读操作导致的卡顿下落超过85%

    ca88手机版登录网页 10

    结语

    挪动客户端数据库就算不如后台数据库那么复杂,但也存在着广大可挖掘的技术点。这次尝试了仅对SQLite原有的方案进行优化,而市面上还有许多绝妙的数据库,如LevelDB、RocksDB、Realm等,它们选用了和SQLite不一致的完成原理。后续大家将以此为戒它们的优化经验,尝试更深入的优化。

二十四线程并发优化

  1. 背景

鉴于历史原因,旧版本的微信直接使用单句柄的方案,即怀有线程共有一个
SQLite
Handle,并用线程锁防止十二线程难点。当二十四线程并发时,各线程的数据库操作同步顺序举行,这就导致新兴的线程会被封堵较长的时间。

  1. SQLite 的多句柄方案及 Busy Retry 方案

SQLite 实际是帮衬多线程(大致)无锁地出现操作。只需

拉开配置PRAGMA SQLITE_THREADSAFE=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. SQLite Busy Retry 方案的阙如

Busy Retry 的方案固然基本能缓解难点,但对质量的压榨做的不够极致。在
Retry 进度中,休眠时间的长短和重试次数,是决定品质和操作成功率的最紧要。

唯独,它们的最优值,因分歧操作不相同景色而分裂。若休眠时间太短或重试次数太多,会空耗
CPU
的资源;若休眠时间过长,会促成等待的年月太长;若重试次数太少,则会回落操作的成功率。

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

可以看来,要是休眠时间与重试成功率的关联,依照粉色的曲线进行分布,那么 p
点的值也不失为该方案的一个次优解。但是事总不遂人愿,大家必要一个更好的方案。

  1. 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
的通报机制。当一个经过的数据库操作停止时,不可能通过锁来第一时间通告到任何进度展开重试。由此不得不退而求其次,通过反复休眠来进展尝试。

  1. 新的方案

通过上边的各个分析、准备,终于得以入手开端修改了。

咱俩驾驭,iOS app 是单进度的,并尚无多进度并发的要求,那和 SQLite
的安排性初衷是不雷同的。那就给大家的优化提供了答辩上的根基。在 iOS
这一一定情景下,大家能够放任包容性,进步并发性。

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

通过pthread_mutex_lock举行线程锁,防止其余线程参与。然后相比状态量,若当前情景不行跳转,则将目先前时期待跳转的气象,插入到一个
FIFO 的 Queue 底部。最终,线程通过pthread_cond_ca88手机版登录网页,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
的筹划是对准容量较小的装置,尤其是在十几年前的那一个年代,这样的装备并不在少数。而随着硬盘价格逐步下跌,对于像
Nokia 那样的设施,几 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年6月才重新打开。所以假设利用的
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
不相同的贯彻原理。后续大家将以此为戒它们的优化经验,尝试更浓厚的优化。

come from

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图