数据库系统的核心作用在于高效地存储和管理数据,并支持从海量信息中快速检索所需记录。传统的 SQL 关系型数据库在数据规模持续增长的情况下,容易遭遇单机性能瓶颈。目前提升性能的主流做法多依赖垂直扩展增强单机硬件能力,而非水平扩展增加节点以线性扩容。由于其架构限制,关系型数据库在弹性扩展性方面存在障碍,难以充分适应多节点分布式环境。同时为满足 ACID 事务特性,这类系统在支持分布式事务处理时也面临一定的技术挑战。
相比之下,NoSQL 数据库更适用于大规模数据存储和高吞吐、低延迟的应用场景。由于 NoSQL 不受固定数据结构和关系的限制,具有更灵活的扩展能力,并且支持节点的动态扩展,从而更好地适应分布式环境。无论是关系型 SQL 数据库还是 NoSQL 数据库,它们都有一个共同的核心组件存储引擎,这是数据库设计与实现的关键核心部分。
目前数据库存储引擎可以分为两大类,一类是基于内存的 (In-Memory)实现,例如 Redis 和 Memcached 这类的将数据全部存储在内存中;另外一种则是类似于 MySQL 的 InnoDB 和 LevelDB 这种将数据存储在磁盘中。两者都有各自的优势和劣势,基于内存的存储引擎通常存储的数据量较少,但其访问速度往往比基于磁盘的存储引擎快好几个数量级,而基于磁盘的存储引擎访问速度较慢,但存储数据量要比内存引擎多几个数量级。通常内存的价格要比磁盘的贵很多,这也是在选择存储硬件时要考虑的成本问题。
存储引擎的开发者需要权衡读写性能和硬件成本的同时,重点在于如何高效存储数据和检索数据。数据库的存储引擎相当于在操作系统文件系统之上,建立的一套用于数据组织、存储与查询的逻辑管理层,最终实现是依赖于操作系统的文件系统 API 接口的,最终数据是被持久化到磁盘文件中保存,这个就对应着 ACID 中的 Durability 数据持久性。
持久性(Durability)是存储引擎最为至关重要的功能实现,目前很多基于内存版本的 NoSQL 数据库例如 Memcached 和 Redis 在这方面做的就比较差,在 NoSQL 服务器运行过程中突然崩溃断电就会导致数据没有被持久化存储的情况,从而导致数据丢失影响到上层的业务程序。
存储是现代计算机最核心的功能之一,计算机中的程序和文件以数据块的形式保存在各种存储介质中,主要可分为持久化存储和非持久化存储两类。非持久化存储通常指内存(如 RAM),其特点是在断电或关机后,数据会被清除。尽管容量通常小于持久化存储,但其高速读写性能使其广泛应用于程序运行过程中的数据处理和临时缓存。相比之下,持久化存储如硬盘、固态硬盘、DVD、磁带等,能够在断电后保留数据,适用于长期保存程序和文件信息。
基础架构
综上所述,针对这些场景的问题 UrnaDB 采用了基于 Log-Structured Megre Tree 日志结构化文件系统的存储引擎实现,存储引擎会以 Append-Only Log 的方式将所有的数据操作写入到数据文件中。同时 UrnaDB 为了高速查询检索数据记录,存储引擎会将数据记录索引信息全部保存在内存中,从而实现高效快速的查询目标数据记录。这样的设计的好处是能以磁盘最大写入性能进行写入数据,并且还能减少读取磁盘索引所需要的时间,通过一次索引定位来读取数据记录,写入和查询流程图:
其核心持久化机制基于预写日志 Write-Ahead Logging 简称 WAL ,在对数据执行任何操作前,都会先将操作记录写入 WAL 日志文件。WAL 文件不仅承担持久化的角色,也作为主要的数据存储载体。在数据库进程崩溃后,只需从 WAL 日志中顺序读取各条 Segment 记录,即可高效恢复内存中的索引结构。
在 UrnaDB 中对这些 WAL 数据文件有一个统称为叫 Region 文件,这些文件有单个固定大小限制,当一个数据文件写满之后就会被关闭,会重新创建一个新的 Active 活跃的 Region 文件进行数据记录的进行写入。被关闭的 Region 文件会被视为冷数据文件,随着数据库不间断长时间运行 Region 文件会不断递增逐渐占用磁盘空间。此时数据库进程就需要对旧的 Region 文件执行压缩 Compaction 和定期清理,以降低存储压力并提高查崩溃数据恢复时的启动效率,压缩流程原理图:
目前工业级的存储系统中,RaikDB 是一个采用类似模型实现的数据库产品,RaikDB 是基于 Amazon Dynamo 论文的设计理念构建而成。其 RaikDB 底层也是采用的顺序写入的日志式存储引擎,并将其命名为 Bitcask 存储模型,以提升写入性能和读写效率。
该模型与 UrnaDB 所采用的存储引擎在设计理念上颇为相似。Bitcask 存储引擎在执行数据压缩 Compaction 时会生成 Hint 文件,其作用是在存储引擎重启时,辅助快速重建内存索引,从而避免每次启动都需全量扫描数据文件,显著提升系统的启动效率。然而 Hint 文件仅在 Compaction 过程中生成,所记录的只是当时内存索引的快照,无法反映实时状态。
在该存储引擎设计的基础上,UrnaDB 构建了内置的 HTTP 服务器,并引入了多种抽象化的数据结构层。整体架构如下图所示:
这些模块相互协作构成了一个完整的单机版 NoSQL 数据库,实现了从底层存储到服务接口的一体化设计。
检查点技术
由于 UrnaDB 数据库存储引擎采用将所有索引数据存储在内存中的设计,一旦数据库进程意外崩溃,内存中的索引信息将会丢失,未能持久化到磁盘。这样在下一次启动时,系统需要重新扫描所有数据文件,以重建内存中的索引结构,如果数据文件体量较大,将导致启动过程变得缓慢,启动耗时较长。
为了解决这个问题,UrnaDB 数据库引入了 checkpoint 功能机制,通过定期将内存中的索引状态保存到磁盘,在发生异常重启时即可快速加载 checkpoint 文件,从而大幅缩短数据库的启动时间。开启 checkpoint 功能可以在数据库崩溃后显著加快启动恢复的速度。然而 checkpoint 文件的生成时间间隔设置同样需要谨慎权衡。如果生成间隔过短,将导致 checkpoint 文件过于频繁地写入磁盘,占用大量 I/O 资源,可能影响数据库的整体性能;而如果间隔设置过长,崩溃恢复时仍需扫描大量 Region 数据记录,反而无法发挥 checkpoint 的优势,导致启动时间较长。因此合理配置 checkpoint 的生成频率,需在性能开销与数据恢复效率之间做出平衡,建议根据实际业务的数据写入频率和系统负载,动态调整该参数以获得最佳效果。