飛哥帶你揭秘:為什么HugePage能讓Oracle數(shù)據(jù)庫(kù)如虎添翼?
大家如果有人部署過 Oracle 數(shù)據(jù)庫(kù)的話,一定也看到過 Oracle 為了性能考慮,是推薦開啟大頁(yè)(HugePage)的。
那么為什么開啟大頁(yè) 能有性能提升,它的優(yōu)化原理是啥,又是如何實(shí)現(xiàn)的呢?今天飛哥就來和你一起深入地聊聊這個(gè) Topic。
一、 內(nèi)核四級(jí)頁(yè)表之殤
為了更好了解 HugePage,我們需要溫習(xí)一下內(nèi)核的頁(yè)表機(jī)制。
在這個(gè)機(jī)制中有兩個(gè)前提知識(shí)點(diǎn),那就是
- 第一、應(yīng)用程序申請(qǐng)內(nèi)存時(shí)不會(huì)分配物理內(nèi)存,訪問觸發(fā)缺頁(yè)中斷時(shí)才分配!
- 第二、頁(yè)是內(nèi)核分配物理內(nèi)存的最小單位!
我們應(yīng)用程序使用的都是虛擬內(nèi)存地址。在程序?qū)嶋H運(yùn)行的時(shí)候,需要轉(zhuǎn)換成實(shí)際的物理地址。如果轉(zhuǎn)換后的物理地址所在的頁(yè)面正好存在,那么直接訪問就可以了。如果頁(yè)面不存在,那么需要觸發(fā)缺頁(yè)中斷并申請(qǐng)一個(gè)完整的頁(yè)面后再供應(yīng)用程序繼續(xù)訪問。頁(yè)的最小單位是 4 KB。
在《深入理解Linux進(jìn)程與內(nèi)存》里的第六章「進(jìn)程如何使用內(nèi)存」中,我們提到過 Linux 將虛擬地址到物理地址中用到的四級(jí)頁(yè)表機(jī)制。
圖片
內(nèi)核四級(jí)頁(yè)表機(jī)制把 64 位的內(nèi)存地址范圍分成了幾段。
- 第 63-48 位,額。。64位內(nèi)存地址太大了,這段屬于廢棄不用的。
- 第 39-47(9)位指定在一級(jí)頁(yè)表 PGD 中索引位置
- 第 30-38(9)位指定在二級(jí)頁(yè)表 PUD 中索引位置
- 第 21-29(9)位指定在對(duì)應(yīng)三級(jí)頁(yè)表 PMD 中索引位置
- 第 12-20(9)位指定在四級(jí)頁(yè)表 PTE 中索引位置
大家注意下,每一級(jí)頁(yè)表管理的地址范圍都是 9 個(gè)位。為啥是 9 ,不是 8 ,也不是 10。原因是為了將數(shù)據(jù)結(jié)構(gòu)對(duì)齊到 4 KB。這樣具體的一個(gè) PGD/PUD/PMD/PTE,保存著 2 的 9 次方, 512 個(gè) 64 位物理地址(8個(gè)字節(jié))。512 * 8 = 正好是 4 KB。
在將某進(jìn)程的一個(gè)具體的 64 位的虛擬內(nèi)存地址轉(zhuǎn)換為物理地址時(shí),首先按照上述地址范圍把虛擬地址切分成幾段。然后經(jīng)過下面幾步轉(zhuǎn)換成物理地址。
- 第一步:從 CPU 中名為 CR3 的寄存器中找到當(dāng)前進(jìn)程的一級(jí)頁(yè)表 PGD 的地址
- 第二步:以虛擬地址中的 39 ~ 47 位作為索引,找到 PUD 所在的內(nèi)存地址
- 第三步:再以虛擬地址中的 30 ~ 38 位作為索引,找到 PMD 所在的內(nèi)存地址
- 第四步:再以虛擬地址中的 21 ~ 29 位作為索引,找到 PTE 所在的內(nèi)存地址
- 第五步:再以虛擬內(nèi)存地址的 0 ~ 11 位作為物理內(nèi)存頁(yè)的偏移量,得到最終的物理地址
Linux分頁(yè)機(jī)制就帶領(lǐng)大家簡(jiǎn)單回憶這么一下。今天我們的重點(diǎn)是想說頁(yè)表機(jī)制帶來的額外的問題。
頁(yè)表是存在內(nèi)存里的。完成一個(gè)虛擬地址轉(zhuǎn)換的過程中需要把當(dāng)前虛擬地址對(duì)應(yīng)的四個(gè)頁(yè)表全部找出來才能完成虛擬地址到物理地址的轉(zhuǎn)換。那就是一次內(nèi)存 IO 光是虛擬地址到物理地址的轉(zhuǎn)換就要去內(nèi)存查 4 次頁(yè)表。再算上真正的內(nèi)存訪問,最壞情況下需要 5 次內(nèi)存 IO 才能獲取一個(gè)內(nèi)存數(shù)據(jù)!
為了提升地址轉(zhuǎn)換效率。既然進(jìn)行地址轉(zhuǎn)換需要的內(nèi)存 IO 次數(shù)多,且耗時(shí)。那么干脆就和 CPU 的 L1、L2、L3 的緩存思想一樣,在 CPU 里把頁(yè)表中的數(shù)據(jù)盡可能地緩存起來不就行了么,
所以 CPU 硬件中有個(gè) TLB(Translation Lookaside Buffer) 模塊,專門用于加速虛擬地址到物理地址轉(zhuǎn)換速度的緩存。其訪問速度非???,和寄存器相當(dāng),比 L1 訪問還快。
雖然有了 TLB 加速的方案,但這個(gè)方案并不是萬能的。最大的缺點(diǎn)是 TLB 太小了。一般的 CPU 中 L1 TLB 一般也就幾十個(gè)條目容量,L2 TLB 一般也就小幾千。
再看需求端,我們假設(shè)每個(gè)進(jìn)程需要 40 GB 物理內(nèi)存,那換算成 4 KB 頁(yè)面的話就是大約 1000 萬個(gè)頁(yè)面,也就對(duì)應(yīng) 1000 萬個(gè)頁(yè)表?xiàng)l目。TLB 里這點(diǎn)點(diǎn)容量還是捉襟見肘。
正因?yàn)樵谒募?jí)頁(yè)表下有這樣潛在的性能隱患。所以 Oracle 這種內(nèi)存密集型的應(yīng)用就推薦配置 HugePage 來提高它的運(yùn)行性能了。
二、HugePage 如何使用
可見,四級(jí)頁(yè)表最大的問題是在于頁(yè)面太多時(shí)性能較差。頁(yè)面一多,管理這些頁(yè)面的頁(yè)表項(xiàng)就多,TLB緩存命中率就會(huì)很差。那如果能把頁(yè)面數(shù)量給降下來,TLB 緩存命中率一定會(huì)有大幅度的提升。
假如說我們把 4 KB 的頁(yè)面換成 2 MB 的頁(yè)面,那么同樣對(duì)于 40 GB 物理內(nèi)存消耗,那僅僅只需要 2 萬個(gè)頁(yè)面就夠了。相比于原來的 1000 萬 降低到了 500 之一。
另外這樣不光是 TLB 緩存命中率會(huì)有大幅度的提升。內(nèi)核的虛擬地址轉(zhuǎn)換時(shí)的頁(yè)表機(jī)制也可以簡(jiǎn)化成下面這樣的三級(jí)頁(yè)表,少了一次轉(zhuǎn)換開銷。
所以,一個(gè)結(jié)論是把 4 KB 的頁(yè)面換成 2 MB 的頁(yè)面,可以大幅度提升虛擬地址轉(zhuǎn)換物理地址時(shí)的性能??!
那么,如果你想獲取這個(gè)性能提升的話,該如何操作呢?
第一步首先是大頁(yè)的預(yù)留。
預(yù)留的方式分為啟動(dòng)階段預(yù)留和運(yùn)行時(shí)預(yù)留。
對(duì)于啟動(dòng)階段預(yù)留,需要修改 Linux 內(nèi)核的啟動(dòng)參數(shù)。編輯/boot/grub/grub.cfg 文件找到啟動(dòng)參數(shù)行(不同的發(fā)行版可能修改方式會(huì)有一些出入)。添加以下內(nèi)容,指定 HugePage 的頁(yè)面大小,指定預(yù)留的大頁(yè)數(shù)量。:
hugepagesz=2M hugepages=512
對(duì)于運(yùn)行時(shí)預(yù)留,直接修改內(nèi)核 hugetlbfs 暴露出來的偽文件即可。
// 預(yù)留特定size的大頁(yè)
echo 5 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
第二步是大頁(yè)的申請(qǐng)
申請(qǐng)的時(shí)候,先打開通過 open 打開 hugepage 偽文件句柄,再通過 mmap 來申請(qǐng)即可。
int main(){
// 打開 hugepage 句柄
fd = open("/mnt/huge/hugepage...", O_CREAT|O_RDWR);
// 申請(qǐng)大頁(yè)
addr = mmap(0, MAP_LENGTH, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
}
這樣,你的應(yīng)用程序就能享受 TLB 緩存命中率提升帶來的飛翔感覺了。
三、內(nèi)核啟動(dòng)時(shí) HugePage 處理
咱們「開發(fā)內(nèi)功修煉」公眾號(hào)的風(fēng)格是不僅要會(huì)用,還要懂內(nèi)部原理。接下來飛哥再來帶你看下內(nèi)核是如何管理 HugePage 的!
3.1 回顧普通頁(yè)的伙伴系統(tǒng)
在《深入理解Linux進(jìn)程與內(nèi)存》里的第五章「系統(tǒng)物理內(nèi)存初始化」中介紹過,
- 內(nèi)核先是通過固件 ACPI E820 規(guī)范探測(cè)安裝的內(nèi)存的物理地址范圍
- 將探測(cè)到的內(nèi)存交給 memblock 初期內(nèi)存分配器來管理,同時(shí)會(huì)再讀取 ACPI 中的 SRAT 表獲取 NUMA 信息
- 接著在初期內(nèi)存分配器中申請(qǐng)管理所有頁(yè)面的 struct page 對(duì)象(一個(gè) struct page 一般是 64 字節(jié))
- 最后釋放其余的可用內(nèi)存交給伙伴系統(tǒng)來管理
start_kernel
-> setup_arch
---> e820__memory_setup // 內(nèi)核把物理內(nèi)存檢測(cè)保存從boot_params.e820_table保存到e820_table中,并打印出來
---> e820__memblock_setup // 根據(jù)e820信息構(gòu)建memblock內(nèi)存分配器,開啟調(diào)試能打印
---> initmem_init // 內(nèi)存中 NUMA 機(jī)制初始化)
---> x86_init.paging.pagetable_init(native_pagetable_init)
-----> paging_init // 頁(yè)管理機(jī)制的初始化
-> mm_init
---> mem_init
-----> memblock_free_all // 向伙伴系統(tǒng)移交控制權(quán)
// file:include/linux/mmzone.h
struct zone {
......
// zone的名稱
const char *name;
// 管理zone下面所有頁(yè)面的伙伴系統(tǒng)
struct free_area free_area[MAX_ORDER];
......
}
圖片
3.2 空閑 HugePage 的管理
相比伙伴系統(tǒng)中 4KB 頁(yè)面的管理,內(nèi)核對(duì) HugePage 頁(yè)面的管理要簡(jiǎn)單許多。內(nèi)核中維持一個(gè)各種 HugePage 頁(yè)面(內(nèi)核支持多種大小的 HugePage,不僅僅只有 2 MB)的 struct hstate 數(shù)組。
// file:mm/hugetlb.c
struct hstate hstates[HUGE_MAX_HSTATE];
在每一個(gè) hstate 成員內(nèi),有一個(gè)空閑鏈表 hugepage_freelists,會(huì)把所有的空閑頁(yè)面給串起來。
我們來看大致看下空閑頁(yè)面的初始化過程。內(nèi)核啟動(dòng)過程中,還會(huì)按照一定的順序執(zhí)行初始化函數(shù)。HugePage 的初始化函數(shù) hugetlb_init 通過 subsys_initcall 注冊(cè)。
// file:mm/hugetlb.c
subsys_initcall(hugetlb_init);
這樣內(nèi)核啟動(dòng)的時(shí)候,就會(huì)執(zhí)行到 hugetlb_init 進(jìn)行 HugePage 的初始化。
// file:mm/hugetlb.c
static int __init hugetlb_init(void)
{
...
// 初始化默認(rèn)大頁(yè) state,空閑大內(nèi)存頁(yè)鏈表 hugepage_freelists
hugetlb_add_hstate(HUGETLB_PAGE_ORDER);
// 申請(qǐng)大內(nèi)存頁(yè), 并且保存到 hugepage_freelists 鏈表中
hugetlb_init_hstates();
...
// 創(chuàng)建/sys/kernel/mm/hugepages相關(guān)目錄文件
hugetlb_sysfs_init();
// 創(chuàng)建/sys/device/system/node/node*/hugepages相關(guān)目錄文件
hugetlb_register_all_nodes();
...
}
hugetlb_init 函數(shù)主要完成兩個(gè)工作:
第一:初始化默認(rèn)大頁(yè) state。在 Linux 中是支持多種規(guī)格的大頁(yè)的,存在一個(gè)全局變量 states 數(shù)組,其中每一個(gè)元素都對(duì)應(yīng)一個(gè)規(guī)格的大頁(yè)的管理數(shù)據(jù)結(jié)構(gòu),包括所有空閑頁(yè)面管理用的鏈表 hugepage_freelists。
第二:為系統(tǒng)申請(qǐng)空閑的大內(nèi)存頁(yè),并且保存到空閑鏈表 hugepage_freelists 中。
第三:創(chuàng)建 hugetlbfs 相關(guān)偽文件,如 /sys/kernel/mm/hugepages、/sys/device/system/node/node*/hugepages。用戶后續(xù)可以通過這些偽文件來和內(nèi)核交互。
我們來重點(diǎn)看下申請(qǐng)空閑大內(nèi)存頁(yè)的邏輯,這是依次調(diào)用 hugetlb_init_hstates -> hugetlb_hstate_alloc_pages,在執(zhí)行到 hugetlb_hstate_alloc_pages_onenode 中完成的。
// file:mm/hugetlb.c
static void __init hugetlb_hstate_alloc_pages_onenode(struct hstate *h, int nid)
{
...
for (i = 0; i < h->max_huge_pages_node[nid]; ++i) {
page = alloc_fresh_huge_page(h, gfp_mask, nid,
&node_states[N_MEMORY], NULL);
if (page)
break;
}
free_huge_page(page);
return 1;
}
其中 alloc_fresh_huge_page 是在申請(qǐng)頁(yè)面,free_huge_page 會(huì)將其放到空閑鏈表 hugepage_freelists 中。
四、mmap 申請(qǐng)內(nèi)存
大頁(yè)的內(nèi)存申請(qǐng)內(nèi)核工作原理大概分三步:
- 第一先是要打開 HugePage 偽文件句柄,
- 第二是通過 mmap 申請(qǐng)大頁(yè)
- 第三是在訪問缺頁(yè)中斷時(shí)實(shí)際申請(qǐng)真正的物理大頁(yè)
int main(){
// 打開 hugepage 句柄
fd = open("/mnt/huge/hugepage...", O_CREAT|O_RDWR);
// 申請(qǐng)大頁(yè)
addr = mmap(0, MAP_LENGTH, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
}
4.1 打開 HugePage 偽文件句柄
調(diào)用 open 打開 hugetlbfs 下的文件時(shí),會(huì)執(zhí)行到 hugetlb_file_setup 函數(shù),在這里會(huì)給申請(qǐng)文件內(nèi)核對(duì)象,為它指定它所綁定的各種 operations 方法。
// file:fs/hugetlbfs/inode.c
struct file *hugetlb_file_setup(const char *name, ...)
{
...
file = alloc_file_pseudo(inode, mnt, name, O_RDWR,
&hugetlbfs_file_operations);
...
}
其中 hugetlbfs_file_operations 指定了這類文件的各種具體的方法。
const struct file_operations hugetlbfs_file_operations = {
.read_iter = hugetlbfs_read_iter,
.mmap = hugetlbfs_file_mmap,
.fsync = noop_fsync,
.get_unmapped_area = hugetlb_get_unmapped_area,
......
};
這樣當(dāng)對(duì)該文件執(zhí)行 mmap 操作時(shí),就會(huì)調(diào)用到內(nèi)核中的 hugetlbfs_file_mmap 函數(shù)。
4.2 mmap 分配虛擬內(nèi)存
mmap 系統(tǒng)調(diào)用執(zhí)行經(jīng)過如下的復(fù)雜調(diào)用鏈后,最終會(huì)調(diào)用到 file 內(nèi)核對(duì)象的 map 方法。
mmap // offset轉(zhuǎn)成頁(yè)為單位
+-- sys_mmap_pgoff // 通過fd獲取file
+-- vm_mmap_pgoff // 信號(hào)量保護(hù),映射完成后populate
+-- do_mmap_pgoff // 簡(jiǎn)單封裝
+-- do_mmap // 映射長(zhǎng)度頁(yè)對(duì)齊,prot和flags檢查,設(shè)置vm_flags,獲取映射虛擬地址
+-- mmap_region // 地址空間檢查,vma_merge,vma分配及初始化
|-- call_mmap // 文件映射,簡(jiǎn)單封裝
| +-- file->f_op->mmap // 調(diào)用實(shí)際文件的mmap方法
....
執(zhí)行到的 file->f_op->mmap 是一個(gè)函數(shù)指針。在上一小節(jié)我們看到對(duì)于 hugetlbfs 下的文件,其 mmap 函數(shù)指針對(duì)應(yīng)的是 hugetlbfs_file_mmap 函數(shù)。
// file:fs/hugetlbfs/inode.c
static int hugetlbfs_file_mmap(struct file *file, struct vm_area_struct *vma)
{
...
// 為映射分配所需的大頁(yè)框
hugetlb_reserve_pages(inode,
vma->vm_pgoff >> huge_page_order(h),
len >> huge_page_shift(h), vma,
vma->vm_flags)
...
}
在該函數(shù)中主要做的就是調(diào)用 hugetlb_reserve_pages 預(yù)留大頁(yè)。
4.3 缺頁(yè)中斷處理
當(dāng)缺頁(yè)中斷發(fā)生時(shí),內(nèi)核會(huì)調(diào)用到 handle_mm_fault 函數(shù)。在這里對(duì)于 HugePage、普通缺頁(yè)、透明大頁(yè)的處理都是不一樣的。
// file:mm/memory.c
vm_fault_t handle_mm_fault(struct vm_area_struct *vma, ...)
{
...
// 是否是大頁(yè)缺頁(yè)
if (is_vm_hugetlb_page(vma))
ret = hugetlb_fault(vma->vm_mm, vma, address, flags);
else
// 普通的缺頁(yè)中斷,包括透明大頁(yè)也都在這里
ret = __handle_mm_fault(vma, address, flags);
...
}
HugePage 缺頁(yè)會(huì)執(zhí)行到 hugetlb_fault 函數(shù),然后再調(diào)用 hugetlb_no_page。
static vm_fault_t hugetlb_no_page(struct mm_struct *mm, ...)
{
page = find_lock_page(mapping, idx);
if (!page) {
...
// 1. 從空閑大內(nèi)存頁(yè)鏈表 hugepage_freelists 中申請(qǐng)一個(gè)大內(nèi)存頁(yè)
page = alloc_huge_page(vma, haddr, 0);
}
// 2. 通過大內(nèi)存頁(yè)的物理地址生成頁(yè)表表項(xiàng)
new_pte = make_huge_pte(vma, page, ((vma->vm_flags & VM_WRITE)
&& (vma->vm_flags & VM_SHARED)));
// 3. 將頁(yè)表表項(xiàng)掛到頁(yè)表中
set_huge_pte_at(mm, haddr, ptep, new_pte);
...
return ret;
}
在 hugetlb_no_page 中主要做了兩件事:
- 第一件:調(diào)用 alloc_huge_page 從空閑鏈表中 hugepage_freelists 摘一個(gè)頁(yè)面下來
- 第二件:設(shè)置頁(yè)表。先是通過大內(nèi)存頁(yè)的物理地址生成頁(yè)表表項(xiàng),再將頁(yè)表表項(xiàng)掛到頁(yè)表中
這樣,應(yīng)用程序就申請(qǐng)到了大頁(yè)物理內(nèi)存了。
五、總結(jié)
我們應(yīng)用程序使用的都是虛擬內(nèi)存地址。在程序?qū)嶋H運(yùn)行的時(shí)候,需要轉(zhuǎn)換成實(shí)際的物理地址。
為了提升地址轉(zhuǎn)換效率。CPU 硬件中設(shè)計(jì)有 TLB 模塊,用于緩存內(nèi)存中的頁(yè)表項(xiàng),加速訪問。這樣 CPU 在執(zhí)行虛擬地址轉(zhuǎn)換時(shí),就可以避免很多的內(nèi)存訪問,極大地提升效率。
但可惜的是 TLB 緩存容量都不大,一般 CPU 中 L1 TLB 一般也就幾十個(gè)條目容量,L2 TLB 一般也就小幾千,我手頭的一臺(tái)服務(wù)器 L2 TLB 才是 1500 個(gè)條目。
如果使用 4 KB 的小頁(yè)面。假設(shè)每個(gè)進(jìn)程需要 40 GB 物理內(nèi)存,每個(gè)頁(yè)面 4 KB,那就是大約 1000 萬個(gè)頁(yè)面,也就要管理 1000 萬個(gè)頁(yè)表?xiàng)l目。區(qū)區(qū) 1500 個(gè) TLB 緩存條目空間,顯然是捉襟見肘。
如果使用 2 MB 的 HugePage, 40 GB / 2 MB,只需要 2 萬個(gè)頁(yè)面。管理的頁(yè)表?xiàng)l目一下子從 1000 萬下降到了 2萬,這樣 1500 個(gè)條目就挺充裕的了。
使用 HugePage 能幫助 TLB 緩存命中率得到了大大的提升。應(yīng)用程序在執(zhí)行虛擬地址到物理地址的轉(zhuǎn)換過程中就會(huì)節(jié)約許多開銷。
Oracle 數(shù)據(jù)庫(kù)是一個(gè)存儲(chǔ)密集型的應(yīng)用,會(huì)申請(qǐng)大量的內(nèi)存,也會(huì)涉及到大量的內(nèi)存訪問。那么用 HugePage 優(yōu)化一下性能的話,對(duì)于它來講再合適不過了。
要補(bǔ)充提的一點(diǎn)是,如果你的應(yīng)用程序使用的內(nèi)存很小,例如只有幾百 M,那建議你還是不要費(fèi)這個(gè)勁兒了,提升不了多少。