Hugepages详解

IBM的创始人Thomas J. Watson曾经说: “全世界只需要5台电脑就足够了”。

Microsoft的创始人Bill Gates曾在一次演讲中说:“个人用户的计算机内存只需640K ”。

Intel创始人之一Gordon Moore曾经说:“当价格不变时,集成电路上可容纳的晶体管数目,约每隔18个月便会增加一倍,性能也会增加一倍”。

前面两句话在今天看来实际上是十分荒谬的,而最后那条就是著名的摩尔定律。

hugepages的出现同样也是因为摩尔定律导致了主机内存越来越大,最后不得不对于标准pages的改进以提高效率。要了解hugepages首先必须清楚Unix/Linux内存的分配机制。本文从入门的角度介绍Linux的内存分配机制,当然这一部分内容相信很多人都在计算机体系结构这门课程中学到过,这里就重新温习一下。

早期因为程序非常简单并且使用的内存也非常小,程序的内存地址都是使用程序地址对应物理地址这种这种直接映射的模式,通俗的说就是物理地址是在程序中写死的。想想使用汇编直接操作物理地址是多么壮观的工程呀。对于一个大型的软件系统来说,这部分工作几乎是不可能完成的。后来开始使用“段页式”的方式来管理内存的寻址。用一个时髦的词来说就是引入了“虚拟化”技术。现代操作系统都会使用受保护的虚拟地址模式(protected virtual address mode)来管理内存,Linux下分为三类地址:逻辑地址(Logic Address)、线性地址(Linear Address)与物理地址(Physics Address)。这三者的对应关系如下所示:    

segmentation and paging

简单的说就是逻辑地址通过分段机制映射转化为线性地址,然后线性地址通过分页机制映射转化为物理地址。因为这里是介绍hugepages,所以我这里不打算介绍分段机制,主要对分页机制做简单的阐述。 事实上,地址映射的过程远远不止上图中这么简单。例如在分页机制映射中,一般需要经过四级映射,也就是将线性地址映射为物理地址的过程中,需要从内存中读取至少四次页目录表(Page Directory)和页表 (Page Table)。众所周知,CPU寄存器与内存的访问速率相差至少一个数量级,所以如果使用传统的分页机制的开销将会非常大。所以在现代的X86架构中,集成了一种被成为TLB(Translation Lookaside Buffer)的硬件级缓存。TLB读写速度非常快,基本和CPU寄存器的速率在一个数量级,而TLB中保缓存了最近使用过的线性地址和物理地址的映射关系。如下图所示:

 

那么将线性地址映射为物理地址的过程如下:首先到TLB中找到这个条目是否存在,如果不存在则为TLB miss,然后使用页表的方式进行寻址,最后把这个映射关系更新到TLB中以备下次使用。由于TLB是大小有限,而一旦出现TLB miss,则其查询的代价很高,所以现代CPU架构基本都进行了一些优化以提高地址映射的效率。例如:

  • 线性地址到物理地址转换一开始就选择同时在TLB和页表进行查询,而不经过TLB查找是否成功的等待;
  • 使用多级TLB以及软TLB,
  • 在CPU context swtch的时候不flush整个TLB。

下面用例子来说明为什么使用传统的 4k大小的页表相比hugepages对大内存的管理效率会很低。在x86平台,一条PTE的大小为4Byte;而在x86_64平台,一条PTE的大小为8Byte。 以下这种场景并不罕见:

Linux x86_64, SGA大小为100G,使用常规的4k的page,连接到数据库的进程数约1000。page table一共需要100×1024×1024K/4K=26214400条PTE;

那么26214400条PTE需要100×1024×1024K/4K×8Byte=209715200Byte=200M;

从而1000个进程需要100×1024×1024K/4K×8Byte×1000=209715200000Byte=200G。

计算出来的结果真是令人崩溃,但是事实上在你没有崩溃以前,数据库就已经因为没有内存而崩溃了。同样条件下,如果使用2M的hugepages进行对比,则以上的计算方法变为:

page table一共需要100×1024M/2M=51200条PTE;

那么51200条PTE需要100×1024M/2M×8Byte=409600Byte=400K;

从而1000个进程需要100×1024M/2M×8Byte×1=409600Byte=400K。

综上,可以看到同样是1000个进程,同样是管理100G的SGA,结果却大不相同。使用传统的4k大小的page开销竟然会达到惊人的200G;而使用2M的hugepages,开销只有400K。 这其中不仅仅只是对于因为单个进程而言,2M page需要的PTE小于4K  page的PTE,最大的一个原因是在于使用4K page的话,有多少进程使用SGA,就需要多少套PTE,相反如果使用2M page则无论有多少进程使用SGA,共享的都是同一套PTE。

众所周知,x86是32位的,所以默认情况下,其可寻址的空间为2的32次方——4G。在X86设计之初,4G内存似乎是一个遥不可及的天文数字,但是摩尔定律打破了这一切,所以软硬件的设计和开发商必须想出一个对策来解决4G以上不可寻址的问题。注意:这里没有说4G以上的内存不可寻址,而是说4G以上的地址空间不可寻址,这两者实际上有区别的。例如4G内存的CPU在32bit的Windows(非server版本)能识别到的内存一般在3G左右。 这其中的主要是主板或者操作系统的限制。因为计算机上一些其他的设备同样需要可寻址,而这一部分地址需要从总的可寻址空间中预留,例如BIOS芯片的ROM,显卡上的显存(RAM)和BIOS(ROM),以及PCI、PCI-E设备上的RAM和ROM都需要占用一定的可寻址的空间。这个叫MMIO(Memory-mapped I/O)。这里有点off the topic了,所以不再赘述,要了解更详细的原因和机制,请参考以下链接:(RAM limit    PCI hole,   Conventional memory,   3GB barrier )

CPU的设计者碰到了难题了:既要解决4G以上可寻址,又要兼容已有的架构和程序,那该怎么办呢?一种最可行的方式就是通过增加扩展地址来解决4G以上的寻址问题。于是Intel引入一个workaround PAE (Physical Address Extensions)机制:增加20位扩展地址,将原来的32位的物理地址扩展为52位,那么可寻址的空间就增加到了2的52次方——4PB,但是实际上x86只使用了其中的36位,也就是X86实际可寻址的物理地址为64G,而转化的过程为操作系统通过使用页表将4GB的地址空间映射到大小为64GB的物理地址空间。尽管物理地址为52位,但是线性地址还是32位,所以在这种架构下单个程序/进程使用的内存限制实际上仍然还是4G。     注:PSE(Page Size Extension)使得用户可以使用4M的页表。PAE(Physical Address Extension)使得32位的系统就能够使用接近64GB的内存的一种技术。如果PAE和PSE同时使用,则只能使用2M的页表。x86_64默认使用PAE的扩展—— long mode

 

 

通过上图可知:是否使用分页是通过CR0寄存器的PG表示来控制的,如果只使用传统的分段模式,则将CR0.PG置为0, 如果启用分页则需要将PG设置为1。使用page的大小则是由IA32_EFER寄存器LME标志以及CR4寄存器的PAE标志和PSE标志控制的,其中IA32_EFER.LME标志控制是否是否启用IA-32e模式,而CR4.PAE标志为控制是否启用PAE, CR4.PSE标志控制是否启用PSE。但是并不是这三者的任意组合都是存在的,现实中存在的情况为以下几种: 当前x86/x86_64架构支持的page的大小包括: 4k, 2M, 4M, 1G (操作系统到目前还不支持1G的page table) 以下以x86_64的Linux为例,说明page的大小是如何得到的:在内核源代码include/asm-x86_64/pgtable.h文件中,可以找到如下宏定义:

#define PMD_SHIFT 21

...

#define PMD_SIZE (1UL <<PMD_SHIFT)

可知使用long mode的page的大小为:

1 << 21 = 2097152

这个移位运算的结果,请参考 http://www.wolframalpha.com/input/?i=1%3C%3C21

需要注意的是page table的大小用户无法自定义,在Linux中即使修改这个宏定义,重新编译内核,也无法修改page的大小。

Architecture Page Size Huge Page Size Large Page Size
i386 4 KB 4M (2M in PAE mode) 1 GB
IA-64 4 KB 4K, 8K, 64K, 256K, 1M, 4M, 16M, 256M
ppc64 4 KB 16M
sparc 8 KB 8K, 64K, 4M, 256M, 2G

在Exadata甚至所有的大内存Linux平台, hugepages的重要性怎么强调都不过分。那么为什需要配置hugepages呢?使用hugepages的好处,总结起来包括如下五个方面:

Not swappable (使得SGA不可交换)

为啥SGA是可以被换到虚拟内存的?? Bug 160033 – Kernel swaps out Oracle instead of releasing cache 这个bug最终关闭的状态为Not a bug, 但是同时在这个bug中提到:   The basic design of the VM in RHEL 4 and RHEL 5 will not allow a complete fix for this issue, only tweaks to make it behave better most of the time.In the upstream kernel (and for RHEL 6), this problem has been addressed with the split LRU VM, which was merged in 2.6.28 and continues to get small fixes and tweaks.  In RHEL 6 this issue should be resolved. Oracle SGA区的内存被swap到磁盘只有到Linux Kernel2.6.28或者RHEL 6以后才通过引入新的机制得到解决。而一旦SGA区被换出内存会造成很大的性能抖动。所以在这个问题彻底解决之前,绝大多数版本的Linux依然需要通过hugepages来固定SGA区。(Linux平台并不支持通过设置lock_sga和参数的方法来固定SGA, pre_page_sga这个参数存在太多的弊端已经强烈不推荐使用)

Relief of TLB pressure: (减轻TLB的压力)

我们知道TLB是直接缓存虚拟地址到物理地址的缓存表,用于提升性能,省去查找page table减少开销,但是如果出现的大量的TLB miss,必然会给系统的性能带来较大的负面影响,尤其对于连续的读操作。从第二篇文章中我们知道如果使用hugepages能大量减少PTE的数量,也就意味着访问同样多的内容需要的PTE会更少,而通常TLB的槽位是有限的,一般只有512个,所以更少的PTE也就意味着更高的TLB的命中率。

Decreased page table overhead: (减少页表的开销)

在前面的内容中我们通过计算,对4k page和hugepages的页表开销的进行对照发现,使用hugepages可以极大的减少维护页表的开销。这篇文章中也提到了一个计算方法:

Each page table entry can be as large as 64 bytes and if we are trying to handle 50GB of RAM, the pagetable will be approximately 800MB in size which is practically will not fit in 880MB size lowmem (in 2.4 kernels – the page table is not necessarily in lowmem in 2.6 kernels) considering the other uses of lowmem. When 95% of memory is accessed via 256MB hugepages, this can work with a page table of approximately 40MB in total.

注意这里说PTE为64Bytes是一种最极端的情况,考虑的是极值,所以实际上这种估算方法并不是太合理。前面说过:一般来说x86的PTE的大小为4Bytes, x86_64的PTE的大小为8Bytes,这里说的PTE为64Bytes实际上指的是由8个PTE组成的PTEG(PTE group),在《Microprocessor 8085, 8086》这本书的425页中提到A PTEG contains eight Page Table Entries (PTEs) of eight bytes each

Eliminated page table lookup overhead: (减少页表查询的开销)

PTE的数量减少,那么使得很多页表的查询就不需要了,并且更少的PTE使得页表的查询更快。如果TLB miss,则可能需要额外三次内存读取操作才能将线性地址翻译为物理地址。

Faster overall memory performance: (提升内存访问的整体性能)

现代操作系统都是使用虚拟内存管理,每一次对内存的访问实际上都是由两次抽象的内存操作组成。如果只要使用更少的页面,那么原本在页表访问的瓶颈也得以避免。

来看一个具体的问题。

某客户新上线的Oracle数据库系统,运行在Linux x86_64平台上,主机配置较高,32核+120G内存,SGA设置90G左右,但是每当数据库运行大约一周以后,前台应用就会变得异常缓慢,经常假死,有时新连接都没有响应,如果DBA不加干涉杀掉部分local=NO的进程,过一段时间就可能有节点被驱逐。 经过简单的分析,当前数据库主机有如下一些特征:

  • 前台响应缓慢或者新连接无法建立时,CPU占用率并不高,但是奇怪的是有一个系统进程kswapd0占据了单核CPU的100%,其它进程的CPU占用率都控制在单核的各位数;
  • 通过free命令来查看剩余内存,发现所剩的内存已经不多,通过sar -B和vmstat查看发现有较为严重的page in和page out。
  • 问题发生时刻,连接数有一定程度的增加,但是基本都是呈缓慢线性的方式增加,没有剧增的情况。从oracle进程来看,每个连接占的CPU和内存资源都差不多;
  • 从/proc/meminfo来看,页表占用了内存的绝大部分; 操作系统重启的时候,在/var/logs/message中有类似的信息:
messages: Feb  10 10:11:40 rac01 kernel: SysRq : Resetting 
messages: Feb  10 10:16:35 rac01 syslogd 1.4.1: restart.  

这个问题并不复杂:本质就是一个内存耗尽的问题,但是另客户非常不解的是:按照规划,物理内存应该是绰绰有余的,那为什么还出现了内存耗竭的情况呢? 从前面的例子可以看到,因为没有配置hugepages,导致了随着连接数的增加页表急剧膨胀,并且SGA被频繁的从swap交换空间换入/换出。在Linux中,  kswapd是负责内核页面交换管理的一个守护进程,它的职责是保证Linux内存管理操作的高效。当物理内存不够时,它就会变得非常aggressive,就像上文中所说的能占用单核CPU的100%.   kswapd 进程负责确保内存空间总是在被释放中,它监控内核中的pages_high和pages_low阀值。如果空闲内存的数值低于pages_low,则每次 kswapd 进程启动扫描并尝试释放32个free pages.并一直重复这个过程,直到空闲内存的数值高于 pages_high。kswapd 进程完成以下几个操作:

  • 如果该页处于未修改状态,则将该页放置回空闲列表中.
  • 如果该页处于已修改状态并可备份回文件系统,则将页内容写入到磁盘.
  • 如果该页处于已修改状态但没有任何磁盘备份,则将页内容写入到swap device.

注: 每次 kswapd 进程启动扫描并尝试释放32free pages 不再适用于RHEL 5.4以后的版本。 以上方框中的内容来自Linux System and Performance Monitoring, 原文链接已不可考。当然也可以参考MOS文档kswapd / krefilld Process Consumes All the CPU Resources [ID 272249.1]。

在Linux系统中,有两种方式可以用来使用hugepages。一种是2.6内核就已经引入的Hugetlbfs虚拟文件系统,还有一种方式就是从2.6.38版本开始(RHEL 6)引入的THP(Transparent Hugepages).

在现实世界中,hugetlbfs主要用于数据库,需要专门的进行配置以及应用程序的代码支持,而THP则可用于更广泛的应用程序,一切都交给操作系统来完成,也不再需要额外的配置,所以对于应用程序是透明的。 我们首先来看hugetlbfs。hugetlbfs是一个虚拟文件系统,它运行在内核层,不与磁盘空间相对应。如果需要使用hugetlbfs,则需要在编译Linux内核的时候在file system这个配置栏中勾选CONFIG_HUGETLB_PAGE和CONFIG_HUGETLBFS选项(如果勾选了CONFIG_HUGETLB_PAGE则CONFIG_HUGETLBFS会自动勾选)。内核编译完成并启动操作系统以后,将hugetlbfs文件系统挂载到特定的目录(不同的发行版目录可能不一样,Red Hat一般mount在/dev/hugepages),则hugetlbfs就可以访问了。完成以后可以通过

cat /proc/filesystems | grep tlb

来查看hugetlbfs是否已经开启。因为RHEL/OL提供二进制内核的rpm包,所以这些操作在RHEL/OL都不需要做。RHEL/OL上惟一需要做的就是在nr_hugepages中输入需要使用的hugepages的数量,例如:

echo 25000 > /proc/sys/vm/nr_hugepages

当然我们都知道/proc文件系统的内容只对当前生效,如果需要重启后依然有效则需要把vm.nr_hugepages = 25000这个条目加入到/etc/sysctl.conf内核配置文件中,然后重启操作系统或者使用sysctl -p 刷新内核参数配置才能生效。 如果一个程序要使用hugetlbfs,那么用户只能使用mmap 标准的SYSV函数shmget/ shmat来调用/访问它。如果使用mmap调用,则在mmap函数的第四个参数带入的标志位为MAP_HUGETLB,具体实例请参考Linux内核文档:https://www.kernel.org/doc/Documentation/vm/map_hugetlb.c

如果使用shmget接口来调用,则需要则shmget的第三个参数的位置带入SHM_HUGETLB参数(shmat没有这个参数, 所以通过listener新建一个连接无法获知是否使用了hugepages。可参看shmat的man页面: http://linux.die.net/man/2/shmat)。具体实例请参考Linux内核文https://www.kernel.org/doc/Documentation/vm/hugepage-shm.c。可能有同学觉得知道这个对于一个dba毫无意义,因为可能我永远不会进行与hugepages开发相关的任何工作,但是实际上却不是这样的。

Linux平台上无法从操作系统层面得知数据库是不是真正使用了hugepages。那么这个时候我们通过strace跟踪数据库的启动过程(启动以后无法看到)发现,其调用了shmget, 并且第三个参数为SHM_HUGETLB标志。这样我们就可以断定数据库使用了hugepages。例如:如果kernel.shmmax为32G,通过strace跟踪就能看到类似的调用信息。(可以看出shmget的第二个参数为申请的内存大小,单位为Bytes)

$ strace -fo strace.out 
$ sqlplus / as sysdba   
SQL>startup nomount;   
$ grep HUGETLB strace.out | less   
shmget(0xxxxxxxxx, 13625196544, IPC_CREAT|IPC_EXCL|SHM_HUGETLB|0660)   
... 

那么能否可以通过/proc/meminfo中的信息来确认数据库使用了hugepages,我这里举例说明:以下是一个Exadata用户的hugepages信息:

 #cat /proc/meminfo | grep -i hugepage 
HugePages_Total:   13007 
HugePages_Free:    11813 
HugePages_Rsvd:     3372 
HugePages_Surp:        0 
Hugepagesize:       2048 kB 

从上面的信息可以得出以下结论:当前hugepages一共实际使用了13007-11813=1194个pages (不包括预留的),预留了3372个pages。永远不会使用达到了11813-3372=8841个pages。每个page大小为2M,也就是说有接近16.5G的hugepages内存被浪费了(因为hugepages无法swap,所以不能被其它程序所使用)。换句话说:当前系统使用中或者即将被使用的hugepages的总和为HugePages_Total – HugePages_Free + HugePages_Rsvd=13007-11813+3372=4566个。产生这样令人困惑的结果是因为用户为了省资源,在这台主机上配置了多个数据库实例,但是只有一个实例使用了hugepages。 之所以要提这个事情,是因为在Oracle 11.2.0.3版本以前,如果当前操作系统配置了hugepages,但是数据库实例设置的SGA大小如果比hugepages总的大小还要大,则SGA无法使用hugepages的内存,然后会去使用其它非hugepages的内存,并且在alert中也没有任何信息,进而导致系统内存占用率高,数据库性能非常糟糕。

Kevin Closson在他的blog中有抱怨过这个问题,但是他却没说这实际上是一个Bug 12654172: EXHAUST THE AVAILABLE HUGEPAGES FOR ALLOCATING A REALM。当然11.2.0.3以后的版本,会在alert日志提示hugepages没有使用的警告信息。 目前在Linux的主流版本,依然无法像其它平台那样使用操作系统工具来获取到数据库是否使用了hugepages。例如:在Solaris中可以使用pmap -s来查看,在AIX中可以使用vmstat来查看,在RHEL/OL 6可以使用cat /proc/<pid>/smaps来查看。

配置hugepages

接下来讲述如何在RHEL/OL/SLES如何设置hugepages,Exadata后期的版本onecommand默认为数据库配置了hugepages,早期的版本配置方式与RHEL/OL 类似。 在RHEL/OL上,大致有如下步骤:

1 首先需要配置memlock

在/etc/security/limits.conf最后添加:

oracle hard memlock lock_value 
oracle soft memlock lock_value 

其中上述lock_value使用比SGA值略大的一个值代替。

2)估算hugepages的大小

在RHEL/OL平台,如果是数据库没有启动,则可以按照如下公式大致估算所需要的hugepages数目: #hugepages=(SGA(MB)+(20k * # of Oracle processes running)/1024) / hugepage_size (MB) 例如:如果sga的大小为64G,运行的oracle进程数为1000, 那么则大致需要32777个hugepages。RedHat Consultant Scott Croft认为计算的时候应该加上PGA的值,但是pga是无法用到hugetlbfs的,所以这里没有算到pga的值。 当然在数据库实例没有启动的时候只能是估算,如果Oracle数据库正在运行,则可以得到更准确的值。以下脚本根据数据库启动以后的的共享内存段总和除上hugepages的大小,计算出来所需的hugepages的数量。

#
# hugepages_settings.sh
#
# Linux bash script to compute values for the
# recommended HugePages/HugeTLB configuration
#
# Note: This script does calculation for all shared memory
# segments available when the script is run, no matter it
# is an Oracle RDBMS shared memory segment or not.
# Check for the kernel version
#查看Oracle Kernel的版本,因为2.4和2.6使用的hugepages的参数是不一样的;
#2.4使用vm.hugetlb_pool,而2.6使用vm.nr_hugepages。
KERN='uname -r | awk -F. '{ printf("%d.%d\n",$1,$2); }''
# Find out the HugePage size
#查找Hugepages的大小,x86非PAE为4096,x86+PAE以及x86_64为2048,注意# 这里单位为K。
HPG_SZ=`grep Hugepagesize /proc/meminfo | awk {'print $2'}`
# Start from 1 pages to be on the safe side and guarantee 1 free HugePage
#保证至少有1个page,也就是计数从1开始,MOS文档401749.1的初始计数从0开始。
NUM_PG=1
# Cumulative number of pages required to handle the running shared memory segments
#循环计算一共需要多少hugepages
#ipcs -m | awk {'print $5'} | grep "[0-9][0-9]*"的结果是列出所有的shared memory的大#小,单位为Bytes;echo "$SEG_BYTES/($HPG_SZ*1024)" | bc -q 为将shared memory处理单
#个page的大小,得到单个shared memory所需的hugepages的数量。将所有的shared memory
#循环累加,最终得到总的hugepages的数量。
for SEG_BYTES in `ipcs -m | awk {'print $5'} | grep "[0-9][0-9]*"`
do
MIN_PG='echo "$SEG_BYTES/($HPG_SZ*1024)" | bc -q'
if [ $MIN_PG -gt 0 ]; then
NUM_PG=`echo "$NUM_PG+$MIN_PG+1" | bc -q`
fi
done
# Finish with results
#根据不同的内核,提示设置不同的hugepages参数
case $KERN in
'2.4') HUGETLB_POOL=`echo "$NUM_PG*$HPG_SZ/1024" | bc -q`;
echo "Recommended setting: vm.hugetlb_pool = $HUGETLB_POOL" ;;
'2.6') echo "Recommended setting: vm.nr_hugepages = $NUM_PG" ;;
*) echo "Unrecognized kernel version $KERN. Exiting." ;;
esac
# End

3设置hugepages内核参数

运行上述hugepages_settings.sh,得到一个值。将这个值写入/etc/sysctl.conf, 例如:

#echo "vm.nr_hugepages=20480">>/etc/sysctl.conf
#sysctl -p
#grep Huge /proc/meminfo

4禁用AMM特性

因为Oracle自动内存管理的AMM特性与hugetlbfs不兼容,所以需要禁用这个特性,但是可以使用SGA自动管理。具体做法将MEMORY_TARGET/MEMORY_MAX_TARGET设置为0,并且手工指定SGA_TARGET/SGA_MAX_SIZE/PGA_AGGREGATE_TARGET的值。

5)重启主机和数据库确认生效

设置完成以后,最好重启主机(不是必须的,但是建议)和数据库(必须)使得hugepages生效。如果使用的Linux是SLES (SUSE Linux Enterprise Server) ,则Oracle提供了一个名为orarun的rpm包,可以用于自动配置其hugepages。 This package creates the user and the groups for Oracle, sets the Oracle environment variables, sets kernel parameters to values recommended by Oracle, and provides for automated start and stop of Oracle components at system start and stop time. SLES 11的orarun包可以在以下ftp站点找到:  http://ftp.novell.com/partners/oracle/sles-10/ SLES 11的orarun包可以在以下ftp站点找到: http://ftp.novell.com/partners/oracle/sles-11/  


Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *