Introduction
在这篇专栏里,我们将从开发者的角度来探讨SQL Server内存管理内幕。就是说,我们将讨论SQL Server使用API和操作系统功能管理内存的方式及其工作原理。通过这种方式探讨一个产品,将有助于我们理解产品开发者的思路,以及他们所设计的使用方法。理解一个产品的工作原理和它的设计用途,是掌握这个产品的关键。
我们将从一些基础的Windows内存管理基本原理介绍开始。和所有的32位Windows应用程序一样,SQL Server使用Windows内存管理功能分配、释放、管理内存资源。正如所有其它的Windows应用程序,SQL Server调用Win32内存管理API函数,与操作系统提供的内存资源进行交互。
由于SQL Server中几乎所有的内存分配都使用虚拟内存(不是内存堆),因此绝大部分内存分配代码最终都是通过调用Win32的VirtualAlloc或者是VirtualFree函数完成。SQL Server调用VirtualAlloc保留、提交分配的虚拟内存,调用VirtualFree释放虚拟内存。
Virtual Memory vs Physical Memory
在x86系列处理器上,Windows为所有进程提供一个4GB虚拟内存工作空间。用"虚拟"这个词,意思是这个内存并不是通常意义上的内存,它只是一个地址范围,并没有和物理存储单元关联在一起。当进程请求内存分配时,这些地址空间才被使用,和具体的物理存储单元关联起来。然而这些物理存储单元并不一定是物理内存,它通常可能会是磁盘空间,确切的说,是操作系统的分页文件(System Paging Files)。这就是为什么多个应用程序可以同时运行在一个128M内存的系统上,每个应用程序都有一个4GB的虚拟内存地址空间--它不是真正的内存,但对应用程序来说可以理解为内存。Windows透明的处理paging files的数据拷贝,以使应用程序能够使用的内存可以超过机器的实际物理内存,并使应用程序能够公平的存取机器的物理内存。
这个4GB的地址空间被分成两部分:user mode部分和kernal mode部分。默认情况下,每个部分的大小为2GB,在Windows NT系列的操作系统上,可以通过BOOT.INI中的开关来改变这个默认设置(Windows NT, Windows 2000, Windows XP和Windows Server 2003属于Windows NT系列,Windows 9x和Windows ME不属于)。
图1:Windows将进程的虚拟地址空间分成user mode(应用程序)和kernal mode(操作系统)两个部分
每个应用程序拥有自己的虚拟内存地址空间,但操作系统和设备驱动程序共享同一个私有地址空间。每一个虚拟内存页(memory page)都和特定的处理器模式(processor mode)相关联,为了存取某个虚拟内存页,处理器必须工作在要求的模式下。这意味着应用程序不能直接存取kernal mode的虚拟内存,系统必须切换到kernal mode才能存取kernal mode的内存空间。
Application Memory Tuning
3GB启动选项(Windows 2000的Advanced Server和DataCenter及后续Windows版本中可用)允许改变这两个地址空间部分的默认大小。它允许将进程的user mode地址空间从2GB扩展到3GB,相应的代价是kernal mode的地址空间从2GB减小到1GB。用Windows的说法,这个功能叫做Application Memory Tuning或者是4GB Tuning(4GT)。你可以通过在BOOT.INI文件的[Operating Systems]部分添加/3GB开关启用应用Application Memory Tuning。通常情况下,人们通过设置BOOT.INI文件的[Operating Systems]部分,将系统配置为可以使用3GB或者不使用3GB启动,以使在系统启动时可以进行选择。
警告:你也可以在Windows 2000 Professional和Windows 2000 Server上使用/3GB开关,这样做的负面结果是,将kernal mode的空间减小到了1GB,但并不会增加user mode的空间。换句话说,你减小了kernal mode的空间但并没有获得任何好处。
注意:Windows XP和Windows Server 2003引入了一个新的启动选项/USERVA,和/3GB一起使用,比单独使用/3GB能够更好的控制。你在BOOT.INI中添加/3GB的时候可以同时添加/USERVA,/USERVA比单独使用/3GB的优点是它允许你指定一个准确的地址空间大小值供user mode存取。例如,/USERVA=2560为user mdoe配置2.5G的空间,剩余的1.5G用于kernal mode。上面的警告信息在使用/USERVA选项时同样适用。
Large-Address-Aware Executables
在/3GB支持加入Windows之前,应用程序无法使用指针的最高位,User mode的应用程序只能够对32位指针的前31位表示的地址空间进行存取。对于剩下的1位,一些聪明的开发者不希望浪费进程空间里的这1个位,把它用于了其它的目的,例如用于标识那些应用程序特定的地址分配类型的指针。这在引入/3GB后带来一个难题,因为这种类型的应用程序无法区分引用2GB以上内存的指针,和那些引用2GB以下内存但是最高位由于其它原因而被设置的指针。基本上,使用/3GB启动机器,会使这样的应用程序崩溃。为了解决这个问题,微软在Win32 PE文件格式(定义Windows下可执行文件Exe和Dll结构的格式)的Characteristics字段加入一个新标识位的支持,用于指示应用程序是否支持大的寻址能力。设置可执行文件头中Characteristics字段的第32位启用IMAGE_FILE_LARGE_ADDRESS_AWARE标识位。通过设置应用程序头的这个标识位,表明应用程序能够处理那些最高位被设置的指针,不会由于这个位带来任何多意性。当设置了这个标识位,在正确的Windows版本上使用/3GB选项启动,系统将为进程提供一个私有的扩展user mode地址空间。你可以使用DumPBin、ImageCfg等可以分析可执行文件头的工具,查看应用程序是否启用了这个标识位。Visual C++通过/LARGEADDRESSAWARE连接开关提供对IMAGE_FILE_LARGE_ADDRESS_AWARE的支持。SQL Server启用了这个标识位,因此当你在正确的Windows版本上使用/3GB开关启动,系统将扩展SQL Server的user mode地址空间。
注意:Windows在进程启动时检查IMAGE_FILE_LARGE_ADDRESS_AWARE标识,忽略Dll的标识。对那些最高位被设置的指针,dll代码必须能够正确处理。
Physical Address Extension
从Pentium Pro开始,Intel处理器提供一种叫做Physical Address Extension(PAE)的内存映射模式。PAE支持高达64GB的物理内存存取。PAE模式下,内存管理单元(Memory Management Unit - MMU)仍然实现了页目录条目(Page Directory Entries - PDEs)和页表条目(Page Table Entries - PTEs),但是在这个之上有一个新的层级:页目录指针表(Page Directory Pointer Table)。PAE模式下系统能够寻址更大的内存,因为PDEs和PTEs为64位宽,是之前标准宽度的两倍,而并不是通过PAE模式下的页目录指针表实现。页目录指针表把这些高存储容量的表和索引管理起来。使用PAE模式需要一个特殊版本的Windows内核,在Windows 2000及后续版本中均有提供,单处理器机器上位于Ntkrnlpa.exe中,多处理器机器上位于Ntkrnlpamp中。和/3GB、/USERVA一样,在BOOT.INI文件中添加/PAE启用PAE模式。
Address Windowing Extensions
Widnows中的Address Windowing Extensiongs功能允许应用程序存取超过4GB的物理内存。32位的指针是一个整型,只能够存储小于等于0xFFFFFFFF的值,因此只能够引用一个4GB的线性内存地址空间。AWE使应用程序可以突破这个限制,存取所有操作系统支持的内存。
在概念上,AWE并不是一个新的东西,实际上,从计算机诞生开始,操作系统和应用程序就围绕指针限制开始使用类似的机制来处理。例如回到DOS时代,32位扩展(象Phar Lap、Plinks及其它的一些)就普遍运用于16位应用程序,以存取正常地址空间之外的内存。用于扩展内存特殊用途的管理器、API非常普遍。也许你还记得象Quarterdeck QEMM-386这样的产品,在那个时代普遍的用于这类用途中。在这些允许指针存取超过本身表达范围的内存的机制中,具有代表性的方式,是在指针可直接存取的地址空间中提供一个窗口或者是区域,用于和指针无法直接存取的内存区域的转换。这正是AWE的工作原理:在进程地址空间中提供一个区域,或者说一个窗口,用作和user mode的代码无法直接存取的内存区域进行内存存取交换的中专站。
为了使用AWE,应用程序必须:(译者注:下面讲的"需要存取的物理内存"指那些user mode进程在自己的地址空间中无法直接访问到的内存)
1. 使用Win32的AllocateUsERPhisycalPages API函数分配要存取的物理内存。该函数需要调用者具有将内存页锁定的权限。
2. 使用VirtualAlloc API函数在进程的地址空间中创建一个区域,作为与需要存取的物理内存进行映射的一个窗口。
3. 使用MapUserPhysicalPages或者MapUserPhysicalPagesScatter API函数,将需要存取的物理内存映射到这个虚拟内存窗口中。
Windows 2000及后续版本支持AWE,尽管可以在低于2G物理内存的机器上使用AWE,但一般只是在2G或者超过2G内存的机器上使用,因为AWE是32位进程存取超过3GB内存的唯一方法。如果你在低于3GB物理内存的系统上,在SQL Server中启用AWE支持,系统会忽略这个选项并使用正常的虚拟内存管理方法。AWE内存一个比较有意思的特性是它不会使用磁盘,你将注意到AWE相关的API函数只对物理内存进行存取,这就是说AWE内存就是物理内存,不会与系统分页文件发生交换。
用于AWE提供的物理内存缓存的虚拟内存窗口,需要具有读、写存取权限,因此当你设置这个虚拟窗口时,传给VirtualAlloc的保护属性只能是PAGE_READWRITE。这也意味着你无法使用VirtualProtect保护这个区域中的内存页,来防止被修改或存取。
注意:你常用的一些检测应用程序内存使用的工具,例如任务管理器、Perfmon/Sysmon等,都无法显示各个进程AWE内存的使用量。并没有什么可以指示各个进程AWE内存的使用量,也就没有什么可以报告给定进程工作区中AWE内存的大小。
/3GB vs AWE
在Windows的内存管理功能中,Application Memory Tuning(/3GB)可以给私有进程增加50%的地址空间,使用方便,因此成为一种常用方法,但AWE功能更具有弹性和扩展性。前面提到,当你为私有进程地址空间增加1GB,这1GB来自kernal mode的地址空间,kernal mode地址空间也由2GB被压缩到1GB。对于kernal mode代码,完整2GB的工作空间已经显得狭窄,压缩这部分空间意味着某些内部核心结构也必须要压缩。这些结构中主要有机器上用于管理内存的表窗口(table Windows)。当你将kernal mode部分压缩到1GB后,这个表最大就只能管理16GB的物理内存了。例如你在一台具有64GB物理内存的机器上运行Windows 2000 DataCenter,启动时使用了/3GB选项,你就只能够存取这台机器25%的内存,剩余的48BG将无法被操作系统和应用程序使用。AWE允许你访问超过3GB的内存,而通过/3GB,你仅仅为私有进程空间获得额外的1GB。Large Address Aware自动透明的使得这个额外空间对应用程序可用,但它被限制在1GB之内。理论上,AWE通过Win32 AWE API函数,使得所有对操作系统可用的物理内存对应用程序可用。尽管AWE更难于使用和存取,但它更具弹性和扩展。
并不是说任何情况下AWE都比/3GB好,只是通常状况下是这样。比如说当你需要很多空间以分配内存,而又不能放在AWE内存中(例如象线程栈Thread Stacks、锁内存Lock Memory、存储过程计划Procedure Plans等),你也许会发现/3GB更合适。
Memory Regions
SQL Server将分配的内存组织成两个独立的区域:BPool和MemToLeave。实际上如果你使用AWE模式,还有另外一个区域:在Windows AWE支持下可以存取的3GB以上的物理内存。
BPool在这三个区域中是比较突出的一个,它是SQL Server主要的分配池,主要用于数据和索引页的缓存,也用于小于8K的内存分配。MemToLeave包含user mode地址空间中BPool没有使用的那部分虚拟内存空间。3GB之上的AWE内存作为BPool的扩展,为数据和索引页缓存提供额外的空间。
当你启动SQL Server的时候,SQL Server基于机器的物理内存和user mode地址空间的大小计算BPool的上限。在计算出这个值后,MemToLeave区域被保留,这有利于防止BPool随后的预留造成内存碎片。接下来,BPool被保留,它可以分成多达32个独立预留块,用于满足在BPool保留时SQL Server进程中那些正在请求虚拟地址空间的dll及其它分配请求。在保留BPool区域之后,MemToLeave区域被释放。MemToLeave用于SQL Server内部超过8KB的连续空间分配请求,以及象OLEDB Provider、进程内COM对象等外部客户(指SQL Server主要引擎之外,驻留在SQL Server进程中的那些内存请求者)分配请求。
因此,一旦SQL Server启动,BPool就被保留,但未被提交,MemToLeave基本就是进程的虚拟内存地址空间中的空闲部分。如果你在SQL Server启动之后查看SQL Server进程的Virtual Bytes Perfmon计数器,你将发现它反映的是BPool的预留。我曾经看到人们因为这个数字经常很高而惊慌,毕竟,它通常是机器总的物理内存或者是最大user mode地址空间,减去MemToLeave区域大小。这没什么担心的,因为它仅仅是保留但没有提交的空间。之前提到过,保留的空间仅仅是一个地址空间,直到被提交时才会真正的和物理存储单元关联。在这些之后,被提交到BPool中的内存将会增加,直到达到SQL Server启动时计算出的BPool上限值。
Monitoring SQL Server Virtual Memory Use
你可以通过SQL Server:Buffer Manager"Target Pages Perfmon计数器跟踪计算出的BPool最大值。SQL Server不同部分需要内存时,BPool提交一开始就被保留的8KB大小的页直到达到计算的上限值,你可以通过SQL Server:Buffer Manager"Total Pages Perfmon计数器跟踪BPool中被提交的虚拟内存的使用状况。另外你可以通过Private Bytes计数器跟踪SQL Server进程中所有被提交的虚拟内存的使用状况。
因为SQL Server中绝大部分虚拟内存的使用都来自BPool,因此通常情况下,这两个计数器将一前一后的增加或平稳下来(记住,当启用AWE支持后,Private Bytes计数器不会反映SQL Server全部的内存使用)。如果Total Pages计数器平稳下来,而Private Bytes持续增加,这通常表明MemToLeave区域中连续的内存分配。这种内存分配可能比较常见,例如可能是SQL Server创建额外的工作线程时和线程栈相关的内存分配,或者是进程内COM对象、扩展存储过程等外部请求者的内存泄漏等。如果由于内存泄漏或者内存使用过大,导致MemToLeave区域耗尽,使SQL Server进程用完了虚拟内存地址空间的内存(或者是MemToLeave区域中的最大空闲块低于0.5M的默认进程栈大小),就算是并没有达到使用sp_configure配置的最大工作进程数,SQL Server将无法创建新的工作进程。这种情况下,如果SQL Server需要创建一个新的工作进程来执行一个工作请求,例如处理SQL Server新的连接请求等,那么这个工作请求将被延迟,知道服务器有足够的资源创建工作进程,或者是其它工作进程被释放出来。这可能会导致用户无法连接到服务器,因为在从MemToLeave中获得足够的空闲空间或者是其它工作进程被释放能够处理当前工作请求之前,连接可能会超时。
Allocations
SQL Server中的内存请求者在初始化内存请求时,先创建一个内存对象管理当前的请求,当内存对象执行请求时,它调用SQL Server中相应的内存管理器从BPool或者是MemToLeave区域获取内存。请求小于8KB时,通常从BPool中获取内存;当请求8KB或者更大的连续空间时,通常从MemToLeave区域中获取。因为一个内存对象可能会产生多个分配请求,因此有可能会从MemToLeave区域中分配小于8KB的分配请求。向SQL Server进程空间请求内存一般情况下都是内部请求者,就是说SQL Server的内部对象需要内存以执行某个任务,当然不是绝对的,象上面提到过的也有可能是外部请求者。通常,这些外部请求者使用Win32内存API函数分配和管理内存,因此是从MemToLeave区域中分配,因为(对于操作系统而言,译者注)SQL Server进程中只有MemToLeave区域可用(BPool区域被SQL Server保留,译者注)。但对于扩展存储过程是个特殊情况,扩展存储过程调用ODS的srv_alloc API函数实现,这使得它同SQL Server内部请求者被同等的处理,通常srv_alloc请求小于8KB的内存时从BPool中分配,大的内存分配则来自MemToLeave区域。
The Memory Manager
服务器运行时,内存管理器进行检查,以确保为服务器预留了一定数量的可用物理内存,使Windows和服务器上其它应用程序能够继续平稳的运行。这个数量从4M到10M左右(Windows Server 2003上接近10M),基于系统负载和BPool中内存页生命期得出。如果服务器上可用物理内存开始低于这个极限值,服务器释放BPool中的部分内存页,以收缩BPool的内存使用量(假设SQL Server的动态内存配置被启用)。内存管理器也确保任何时候保留了一定数量的空闲内存页,以使新的分配请求到达时,不必等待内存分配。这里的空闲,意思是指这些内存页被提交了,但是未使用。被提交但未被使用的BPool内存页通过一个空闲列表跟踪,当列表中的页被使用时,内存管理器从BPool的预留中分配更多的内存页,直到整个BPool预留被提交。你将看到Process:Private Bytes Perfmon计数器由于这个行为而逐渐的增长(通常是线性增长)。
系统中对应每一个CPU都有一个单独的空闲列表,当需要使用空闲页用于满足一个分配请求时,先检查和当前分配请求CPU相关的空闲列表,然后再检查系统中其它CPU相关的列表。这在多处理器系统上,有利于各个处理器更好的使用本地缓存,提高扩展性。你可以使用SQL Server:Buffer Partition Perfmon计数器监控特定的BPool分区,通过SQL Server:Buffer Manager"Free Pages Perfmon计数器监控所有分区的空闲列表。
整个运行过程中,SQL Server内存管理器进程(可能运行在内存管理器线程或其它服务器线程中)监控系统内存状态,为系统其它应用程序保留合理数量的空闲物理内存,为新的内存分配请求预留一个安全数量的内存页。当在服务器上使用AWE时,其中的某些方面必须改变。在使用AWE时,BPool一开始就获取并锁定机器的物理内存,锁定的内存数量根据是否设置了maximum server memory确定。如果设置了,BPool尝试锁定由maximum server memory确定的数量;如果没有设置,BPool只留出大致128M,供其它进程使用,锁定机器上其余的全部物理内存。然后,BPool使用3GB之上的内存(AWE内存)作为数据和索引的分页文件(paging files),它将这些区域(3GB之上)的物理内存页映射到适当的虚拟内存地址空间中,使32位指针能够引用到。
作为一个丈夫和父亲的Ken Henderson,居住在德克萨斯州的达拉斯郊区。他是8本不同技术主题书籍的作者,包括最近发行的《The Guru's Guide to SQL Server Architecture and Internals》。Ken Henderson是达拉斯小牛队的球迷,业余时间喜欢看着他的孩子们玩闹,喜欢体育运动、园艺。