性能/避免在您的下一个Firefox功能中使用SQLite

2020-06-15 00:38:37

许多Firefox开发人员将SQLite视为存储任何重要数据量的默认选择,这个wiki解释了为什么该视图是不正确的。

在第一次设计特性时,SQLite似乎是一个非常吸引人的选择。它提供了熟悉而强大的SQL语言,我们的代码库有很好的C++和JavaScriptAPI,并且它已经在代码库中的很多地方使用过了。但是,在选择存储格式时,我们必须记住SQLite是一个功能强大且可靠的数据库,而且它具有很多隐藏的复杂性。人们很容易从表面上接受它提供的抽象概念,而忽略了幕后发生的复杂性。因此,我们多次看到SQLite成为性能问题的根源。这并不是对SQLite本身的控诉--任何其他关系型嵌入式DB都会带来同样的挑战。

SQLite数据库太复杂了,无法满足相对简单的数据存储需求。下面的列表概述了SQLite可能给开发人员带来事与愿违的一些方式。

定期向数据库添加数据的功能(例如,访问的URI),但没有数据的到期策略,很容易导致数据库膨胀到数十或数百MB。这在移动设备上尤其不受欢迎。

众所周知,WAL日志会变得非常大,例如错误609122、错误608422,它们的大小必须以性能为代价加以限制。

每个索引都包含索引数据的完整副本。在冗长的文本字段(如URI)上创建索引将导致在磁盘上存储非常大的索引。

由于常见的SQL模式,您的数据库文件可能会受到逻辑/内部碎片的影响,例如,将数据附加到带有索引的列会导致索引和表数据交错。这意味着在执行真空之前,表和索引扫描都是非顺序的。

由于SQLite使用了不适当的默认设置,也可能会发生物理/外部碎片。默认情况下,SQLite不预分配文件。在多个会话中将数据附加到SQLite通常意味着操作系统必须开始一个与前一个数据块不相邻的新数据块。

默认情况下,sqlite将按|page_size|增大/缩小数据库文件。此行为还会导致分配新的数据块。这在OSX、Linux XFS上尤其有问题。

出厂默认SQLite页面大小为1024字节。根据遥测,当Mozilla默认页面大小在错误416330中更改为32KB时,SQLite IO等待时间减少了4倍。这可能是由于减少了系统调用和操作系统预读。

随着时间的推移,性能可能会显著下降;计划定期重建数据库是必要的。Vacuum会清空数据库文件的空闲页面空间,这意味着在真空之后,在有新的空闲空间之前,插入将会稍微昂贵一些,因为它们将不得不增大文件。

SQLite使用fsync来保证事务持久性并支持从崩溃中恢复。fsync可能非常昂贵,并且在默认的回滚日志模式下相对频繁地发生。对于简单的存储需求,这种性能/可靠性权衡可能没有必要,特别是在重要数据可以随时重建的情况下。

JSON文件或日志文件几乎每次都会显示更好的I/O模式,特别是在每次都对它们进行完整压缩和读/写的情况下。

默认情况下,我们的SQLite包装器在每个连接上最多使用2MB内存缓存。此高速缓存大小对于Fennec和B2G可能太大,特别是在有多个连接的情况下。高速缓存大小应使用PRAGMA进行调整。

无法使用索引的查询将使用扫描表遍历所有行,这将使页面内存高速缓存快速增长到最大值,在使用PRADMA SHEEP_MEMORY或关闭连接之前,永远不会回调此空间。

用户发现某些Firefox功能占用了15%的CPU时间,因为SQL语句经常在后台执行

主线程SQL是一种已知的邪恶,幸运的是,代码库中只剩下几个主要的来源,但缓慢的SQL仪表板显示,开发人员仍然受到来自意外来源的主线程I/O的影响:

设置高速缓存大小的PRADGMA语句在主线程上完成,但是如果这是会话的第一个事务,它可以触发SQLite崩溃恢复操作。如果使用默认的WAL日志大小,这一点尤其糟糕。还要注意的是,撞车事故在移动设备上尤其常见,因为我们从来没有干净的出口。

主线程糟糕的SQLite性能也会降低Firefox的响应性,因为它可能会与主线程争夺对存储的访问。遗憾的是,我们的代码仍然执行主线程I/O,此外,还会有来自附加组件和交换的主线程IO。

即使是看似简单的SQL查询也可能需要数秒才能执行,例如在错误966469中,用户报告以下语句需要22秒才能执行:DELETE FROM MOZ_PAGES WHERE id NOT IN(SELECT DISTINCT(PID)FROM MOZ_SUBRESOURCES);

缓慢的数据库操作损害了用户体验。例如,您可能已经注意到Awesomebar有时需要很长时间才能获取任何自动完成建议。

必须仔细设计模式,并且必须定期维护数据库。

碎片会导致额外的寻道,而磁性硬盘和廉价或旧的SSD会使问题变得更糟。清理数据库很有帮助,但是清理是资源密集型的,安排它们可能会很棘手。请参阅提示部分。

如果需要存储少量数据(小于1MB),则应该使用JSON文件,并在主线程上执行所有I/O操作。具有线性最坏情况性能的简单解是理想的。如果您正在处理大量数据(大约1MB),那么在将其写入磁盘之前,您应该使用snappy或lz4压缩JSON数据

如果您的工作负载涉及很多字符串,请不要使用SQLite。将数据存储在外部文件中。

如果您的工作负载涉及很多BLOB,请不要使用SQLite。将数据存储在外部文件中。

如果您有一个大型数据集,但不需要对其运行复杂的查询,请评估磁盘上的JSON文件结构。

如果每次都必须读取和写入大部分数据,请使用JSON文件进行评估。

对于较大的数据集或当SQL绝对必要时,请使用SQLite。确保您了解SQLite的工作原理,仔细设计您的模式,然后分析您的实现。

注意:我们目前正在开发一个基于日志存储的中间解决方案,它将减少对磁盘上已有数据进行少量修改而写入的数据量。

查看SQLite编译指示列表,默认设置可能不正常。我们已经覆盖了moz.build文件和mozStorageConnection.cpp中的一些默认值。

对较少的fsync使用预写日志,但通过使用较小的自动检查点和大小约为该大小三倍的Journal_size_limit来限制日志的大小

根据设备规格设置DB缓存大小,并考虑根据移动设备的内存压力动态调整。

对于容易出现碎片的大型数据库,可以通过调用Connection::SetGrowthIncrement()来更改SQLite的SQLITE_FCNTL_CHUNK_SIZE的值,从而在开始时通过增加数据库的增长增量来预分配一个大型的空数据库。有关参考,请参阅错误581606。

如果内存实体中的数据量可接受,请使用内存临时存储,否则临时数据将写入磁盘,从而导致意外I/O。

不用说,您永远不应该在主线程上执行SQL。

为您的数据库在台式机/移动设备上的增长规模建模。向遥测报告数据库大小和内存使用情况,以确认您的估计是准确的。实施过期政策!

评测您的实现,并测量在典型会话过程中执行的SQL操作的数量(如果它是一个“始终在线”的功能,并且您怀疑它可能会导致性能问题。

不对文本字段进行索引,而是对文本字段的散列进行索引。索引可能会变得非常大。

不要使用时间戳的默认精度(微秒)。桌面应用程序不太可能需要这种级别的精度

在Firefox版本升级期间或空闲时间使用吸尘器维护数据库。如果需要,定期运行分析,您应该从一开始就有一个真空计划。这是所有新代码的要求。

如果处理来自多个连接的相同数据,请考虑使用非共享内存缓存,以避免内存争用。这将提高并发性。不过,它也会使每个连接数量的内存缓存成倍增加,因此这是一个内存/性能选择,您应该小心操作