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