1.2 范围和预览
在本文的大部分,我们专注的是支持数据库核心功能的架构基础。我们并不尝试去提供一个已经在文献中被详细证明了的综合的数据库算法复习。我们也只提供最少的对现代DBMS中的许多拓展的讨论,其中大部分都提供了除了核心的数据管理之外的功能,但是并没有如何更改系统的架构。但是,在这篇论文的不同部分,我们列出了超出论文范围的有趣的话题,并且在可能的情况下,我们提供额外阅读资料指示。
我们由一个对数据库系统架构的整体调查开始我们的讨论。任何服务器系统架构的第一个主题都是它的整体流程结构,并且在前面我们将谈谈一系列可行的替代方案,首先是单处理器机器,然后是目前的各种并行架构。对核心服务器系统架构的讨论适用于一系列系统,但是某种程度上却是DBMS设计的先驱。紧接着,我们讨论更多DBMS的特定域的组件。我们从一个单查询系统视图开始,专注在关系型查询处理器。接着,我们进入存储架构和事务存储管理设计。最后,我们介绍一些大多数DBMS中存在的却很少在教科书中讨论的共享组件和工具。
2 进程模型
当设计任何一个多用户服务器的时候,关于并发用户请求的执行和如何将这些执行映射到操作系统的进程和线程的决定需要尽早做出。这些决策对系统的软件架构有着深远的影响,体现在性能,可伸缩性和跨系统的可移植性。在这个部分,我们从一个简化的框架开始,假设系统是对友好支持线程的,并且我们最初只瞄准单处理器系统。然后我们扩展这个简化的讨论来覆盖现代DBMS是如何实现他们的线程模型的。在第三部分,我们讨论利用集群的技术,以及多处理器和多核系统。
之后的讨论将依赖如下的这些定义:
- 一个操作系统进程中包含了一个操作系统执行单元(一个控制线程),和一块私有的地址空间。在一个进程所维持的状态中有系统的资源句柄和安全上下文。单独的一个程序执行单元被系统内核所调度,并且每个进程有自己唯一的地址空间。
- 一个操作系统线程是一个没有额外的私有系统上下文,没有私有的地址空间的程序执行单元。每个系统线程都有对同一个多线程系统进程中的其他线程的内存资源的完全的访问权限。线程的执行是被操作系统内核所调度的,并且这些线程常被称为“系统线程”或者“k-threads”。
- 一个轻量级线程包是在一个单独的系统进程中支持多线程的一个应用级别结构。不像系统线程被系统调度,轻量级线程被一个应用级别的线程调度器调度。轻量级线程和内核级线程的区别是轻量级线程是在用户空间被调度,而内核调度器不参加也不知道它的存在。用户空间的调度器和它所有的轻量级线程一起运行在一个系统进程中,并且是以一个单独的执行线程出现在系统调度器面前。
轻量级线程相对系统线程具有更快的线程切换速度,因为不需要进行系统内核模式的切换以调度下一个线程。然而,轻量级线程也有劣势,任何堵塞操作比如随意一个线程的某个同步I/O操作都会阻塞这个进程中的所有其他的线程。当一个线程在等在系统资源的到时候,它阻止了其他线程的运行。轻量级线程包避免了这个问题,通过(1)仅发布非同步的I/O请求,并且(2)不调用任何阻塞的系统操作。总的来说,在编写软件时,轻量级线程提供了一个相对于不管是基于系统进程还是系统线程都更难的编程模型。
- 有些DBMS实现了自己的轻量级线程包。这些是一般LWT的特例。我们将这些线程称之为DBMS线程,并且在DBMS,一般LWT,和系统线程对我们的讨论无关紧要的时候就简单称之为线程。
- 一个DBMS客户端是实现了被应用程序用来和DBMS交流的API的软件组件。一些示例数据库的访问API是JDBC,ODBC,OLE或者DB。另外还有许多独有的数据库访问API。一些程序是用嵌入式SQL来编写的,这是一种混合了编程语言表述和数据库访问状态的技术。它的最初应用是在IBM COBOL 和 PL/I中,很久之后,在SQL/J中用java实现了嵌入式SQL。嵌入式SQL被预处理器处理并翻译成对数据访问API的直接调用。无论在客户端程序中使用什么语法,最终的结果都是对数据访问API的直接调用的序列。对这些API的调用被客户端组件封装并通过某种协议发送到DBMS。这个协议通常是专有的并且不被文档化。在过去,已经有一些努力去标准化客户端到数据库的交流协议,其中Open Group DRDA 也许是最著名的,但是没有一个取得广泛的采纳。
- 一个DBMS Worker是DBMS中代表DBMS客户工作的执行线程。一个存在于DBMS Worker和DBMS 客户端间的1:1映射:DBMS Worker处理所有来自客户端的SQL请求。客户端发送SQL请求到服务器。Worker执行每一个请求,并且将结果返回。接下来,我们调查每一个不同商业DBMS所使用的映射Worker到系统线程或者进程的方法。当区别很明显时,我们会称他们为Worker线程或者Worker进程。否则,我们简称他们为Worker。
2.1 单处理器和轻量级线程
在这个子部分中,我们概述一个简化的DBMS进程模型分类。很少有流行的DBMS完全像这部分描述的那样构造,但是描述的这些东西奠定了我们将要讨论的几代数据库系统的基础。今天的流行数据库,在它的核心,都至少增强或者拓展了这里展示的一个模型。
我们从做两个简化的假设开始(之后我们会进行放宽假设条件):
- 操作系统线程支持:我们假设操作系统为我们提供了有效的内核线程支持和能够拥有大量线程的进程。我们假设线程切换的内存和上下文切换的代价是便宜的。这在而今的系统上是一定正确的,但是在那个多数DBMS被设计的年代却是不一定的。因为在某些平台上,系统线程要么没有,要么可伸缩性不好,许多DBMS的实现并没有用到底层的系统线程的支持。
- 单处理器硬件:我们假设我们正在设计一台单处理器的机器。考虑到而今多核系统到处可见,这是一个不切实际的假设,即便是在最底端。然而这个假设会简化我们的初步讨论。
在这部分简单的内容当中,一个DBMS有三个自然的进程模型选择。从最简单到最复杂,她们是:(1) 每个Worker分配一个进程 (2) 每个Worker分配一个线程 (3) 进程池。尽管这三个模型是简化的,但是却在今天的商业DBMS中被使用着。
2.1.1 每个Worker一个进程
每个Worker一个进程模型在早期的DBMS实现中被使用,并且在现在的商业系统中仍被使用。模型是相对简单的,因为Workers被直接映射到了系统进程。系统调度管理着Workers的时间片,并且程序员们可以依赖系统的保护措施来孤立像内存超支这样的标准错误。此外,各种编程工具像是调试器和内存检查器非常适合这种进程模型。这个模型中复杂的是内存中的数据结构在DBMS连接间被共享,包括锁表和缓冲池(分别在6.3和5.3中讨论更多细节)。这些共享数据结构必须在所有DBMS进程都可访问的内存共享区中被明确分配空间。。这需要系统的支持(这很普遍)和一些特别的DBMS编码。实践中,模型中需要使用额外的共享内存削减了地址空间隔离的优势,考虑到在进程间共享“需要的”内存有很好的表现。
在扩展到非常大数量的并发连接时,每个Worker一个进程并不是最有吸引力的模型。难以拓展的原因是一个进程比一个线程有更多的状态,且消耗更多内存。一个进程切换需要切换安全上下文,内存管理器状态,文件和网络句柄表,还有其他进程内容。而这些线程切换都不需要。尽管如此,这个模型仍然很流行,并且被IBM DB2, PostgreSQL和Oracle所支持。
)