优先缓存
对于持久数据库而言,I/O 操作(从持久介质读取和写入)在性能方面是最“昂贵”的操作。为了将 I/O 的影响降至最低,SmartEDB 实现了一个与操作系统文件系统缓存交互的磁盘管理器(DM)缓存。默认情况下,SmartEDB 使用一种名为“优先缓存”的 CLOCK 缓存算法变体。也可以通过重建 SmartEDB 运行时设置一个内部条件编译(#ifdef)开关来选择最近最少使用(LRU)算法。(如果需要 LRU 算法,请联系 McObject 支持以获取更多详细信息。)
以下各节将阐述一些实现细节,以帮助开发人员针对特定应用程序的需求优化 SmartEDB 磁盘管理器的性能。
实现
最近最少使用(LRU)算法
LRU 策略基于局部性原理,即程序和数据引用倾向于聚集。LRU 选择最长时间未被引用的页面进行替换,曾被认为是最佳在线策略之一。然而,对于小内存实体,实现复杂且开销大,因为需要为每个页面标记最后一次引用时间,并在每次内存访问时更新。
典型的 LRU 实现通过保留“年龄位”来追踪最近最少使用的页面。SmartEDB 改进了这一策略,允许应用程序影响页面在磁盘管理器缓存中的保留时间。具体方法是为每个页面添加缓存优先级属性。当 LRU 找到一个“受害者”页面时,如果其 caching_priority 不为零,则减 1 并将页面重新链接到 L2 列表开头。优先级越高,页面在缓存中保留的时间越长。
这种实现的主要缺点是较大的内存开销(每个实体 2 个指针)和并发访问问题(操作 L2 列表需要全局锁)。此外,顺序扫描可能会将有用数据刷出缓存。为此,可以采用更复杂的 LRU 版本或更改扫描算法。因此,出于性能考虑,SmartEDB 默认使用 CLOCK 算法。
LRU 策略基于局部性原理,该原理指出进程中的程序和数据引用倾向于聚集。LRU 替换策略选择最长时间未被引用的页面进行替换。多年来,LRU 被认为是最优的在线策略之一。然而,对于内存中的小实体而言,这种方法的实现较为复杂。例如,为每个页面标记其最后一次引用的时间需要在每次内存引用(包括指令和数据)时进行更新,这会带来显著的开销。
典型的 LRU 算法实现需要为缓存页面保留“年龄位”,并根据这些年龄位追踪“最近最少使用的”缓存页面。SmartEDB 通过引入一种改进技术,使应用程序能够影响某些页面在磁盘管理器缓存中保留的时间。这一改进的关键在于为每个页面添加了一个缓存优先级属性。当 LRU 算法找到一个“受害者”页面时,不会立即释放它(即从 L2 链表尾部移除),而是检查其 caching_priority 字段。如果值不为零,则将 caching_priority 减 1,并将页面重新链接到 L2 列表的开头。缓存优先级为零表示默认行为;优先级为 1 表示页面将在 LRU 列表中从头移动到尾部两次;优先级为 2 表示页面会在 LRU 列表中循环三次,依此类推。优先级越高,页面在 LRU 列表中保持的时间就越长,从而更长时间地留在缓存中。
然而,这种实现方式的主要缺点是较大的内存开销(每个实体需要 2 个指针)以及并发访问的问题(对 L2 列表的操作需要全局锁)。特别是在使用 LRU 算法对持久存储中的数据库页面进行缓存时,顺序扫描可能会将有用的缓存数据刷出。为了解决这个问题,可以采用更复杂的 LRU 算法版本(例如,将缓存分为两部分:频繁访问和很少访问的对象)或更改顺序扫描算法以使用某种循环缓冲区。因此,出于性能考虑,SmartEDB 默认使用的是下面描述的 CLOCK 算法。
CLOCK 算法
在 CLOCK 算法中,页面帧以循环列表形式排列,类似时钟。指针指向最旧的页面,每个页面有一个引用计数器。当发生页面缺失时,检查指针所指页面的引用位。如果引用位已设置,则重置为零并移动指针到下一个页面,直到找到引用位为零的页面并将其替换。
每次页面访问(“固定”)使计数器加 1,上限为 5。每转一圈,计数器减 1。页面释放(“解除固定”)时,计数器设为 max(当前值,缓存优先级 + 1)。因此,移除页面至少需要缓存优先级 + 1 圈。缓存优先级越高,页面在缓存中保留的时间越长。缓存优先级参数在打开数据库时设置。
CLOCK 算法的优势:
- 内存开销小
- 不需要全局锁
- 顺序扫描不会立即刷新缓存
据观察,CLOCK 算法以最小开销高效近似 LRU。
缓存优先级
应用程序可以为特定数据库对象分配缓存优先级。创建数据库时,可为索引、内存分配器位图页和对象页(不包括 BLOB)设置优先级。默认情况下,所有页面优先级为零,但运行时也可更改类(对象)的缓存优先级。通过调整对象优先级,可以为大且很少访问的对象分配较低优先级,为小且频繁访问的对象分配较高优先级。运行时分配的缓存优先级会存储在数据库中,直到被显式覆盖。
其他影响性能的内存初始化因素包括缓存大小和数据库的最大磁盘空间,这些将在后续章节中详细说明。
缓存大小
缓存的内存地址和大小在传递给数据库打开 API 的内存设备中指定。内存可以是共享内存或本地内存(多个进程共享数据库时必须使用共享内存)。较大的缓存通常能提高性能,但对持久介质的更新频率(缓存页面刷新)更为关键。数据库更新如何写入持久介质由事务提交策略决定。
最大数据库大小
SmartEDB 运行时使用数据库参数 MaxDiskDatabaseSize 的值来分配“脏页位图”。位图在创建缓存时在缓存中分配。位图大小大致可按以下方式计算:
MaxDiskDatabaseSize / MemPageSize / 8.
预留页池
SmartEDB 运行时通过在缓存中添加预留页池来处理缓存溢出。在调试模式下,缓存溢出会触发断言(致命错误)。但在发布模式下,磁盘管理器假定将页添加到页池的操作总是成功(不验证 MCO_PIN() 的返回码)。预留池确保这一假设成立:如果无法从正常空间分配页,则从预留池分配。发生这种情况时,磁盘管理器将当前事务标记为错误状态,事务回滚,并返回“内存不足”错误给应用程序。
预留页池机制有助于处理内存不足错误,并确保在数据库运行时耗尽页面池的情况下,活动事务仍可提交或回滚。为此,数据库使用两个阈值:低阈值(黄色区域)和高阈值(红色区域)。黄色区域为池大小的 2/3,但不超过红色区域;红色区域基于最大活动页面数(默认 32)和最大连接数计算。
越过黄色阈值时,运行时尝试卸载事务修改或创建的页。越过红色阈值时,事务被标记为错误。
连接缓存
除了磁盘管理器缓存(也称页池),SmartEDB 还提供每个连接的缓存。运行时为每个连接固定一定数量的页,称为连接缓存。当事务加载的页总数小于连接缓存大小时,这些页会保留在缓存中直到事务提交或连接中断。
默认情况下,连接缓存大小为 4 页。通常,事务期间访问的页数较少,默认值 4 页效果良好。在这种情况下,连接缓存使用顺序搜索。然而,在某些情况下,更改连接缓存大小会对性能产生显著影响。可以指定更大的大小以容纳整个工作集的页,此时使用哈希表查找缓存页。但需重新编译 SmartEDB 库才能修改连接缓存大小,如有需要请联系 McObject 技术支持。
连接缓存默认启用,但可在运行时禁用或重置(提交到数据库)。这些 API 用于管理大量连接和长时间事务的场景。在这种场景中,连接缓存可能导致页面池耗尽空闲页。为解决此问题,可以经常关闭或重置连接缓存。但在正常情况下,应用程序无需控制连接缓存。
内存页分配
SmartEDB 运行时使用每个连接的缓存分配器来减少分配和释放页时的同步开销。每个连接拥有私有页集,可自行管理,无需同步。
在 MVCC 模式下,每个连接分配器持有的最小和最大页数由数据库参数控制,可通过 API 修改。MVCC 事务管理器通过一次预分配一定数量的页并分配给连接,优化对共享内存池的访问。默认最小值为 256 页,最大值为 512 页。调整这些值可以在频繁访问共享资源和分配额外内存之间进行权衡。如果应用程序有明确的对象分配和释放模式,更改这些默认值可能会更有效。
本地语言 API
目前只有 C API 提供了管理缓存参数的功能。有关实现细节,请参阅 C API 缓存管理页面。