Linux NUMA 架构 :基础软件工程师需要知道一些知识
文章目錄
- 前言
- 從物理CPU、core到HT(hyper-threading)
- UMA(Uniform memory access)
- NUMA架構
- NUMA下的內存分配策略
- 1. MPOL_DEFAULT
- 2. MPOL_BIND
- 3. MPOL_INTERLEAVE
- 4. MPOL_PREFERRED
- 5. 一些NUMA架構下的內核配置
- 總結
- 參考
前言
NUMA(Non-Uniform memory access)非一致性內存訪問 作為當下新型的硬件架構,其對操作系統CPU和內存資源整合的底層實現細節是作為分布式存儲/數據庫研發的工程師需要掌握得一種技術。這樣,我們才能夠在這一些架構以及更新的存儲介質(AEP/BPS)之上開發出性能更友好的基礎軟件。
通過本篇 你能夠知道如下幾個關于NUMA的知識:
- NUMA 出現的緣由
- NUMA 的基本架構 及 相關工具
- NUMA 的幾種內存模式
從物理CPU、core到HT(hyper-threading)
我們的CPU是在服務器主板之上,十幾年前的一個物理CPU只會有一個物理核心(core),因為主板就這么大,不能再增加物理CPU的個數了,為了提升CPU性能,硬件廠商只能在物理CPU基礎上增加物理核心數目,由單core到2個、4個。。。為了進一步利用好core資源,Intel以及其他芯片廠商開發了超線程(hyper-threading)技術,來讓一個物理core能夠運行2個內核線程,從而產生了邏輯核(processor)的概念。
在linux相關的系統可以通過如下指令查看相關的指標:
- 查看物理CPU數目:
cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l - 查看core數目:
cat /proc/cpuinfo | grep "cpu cores" | uniq - 查看總的邏輯核的數目:
cat /proc/cpuinfo| grep "processor"| wc -l - 查看cpu型號:
cat /proc/cpuinfo | grep name | cut -f2 -d: | uniq -c
以上相關指標也可以直接通過命令lscpu看到,正因為CPU的算力越來越高,我們不能讓算力資源浪費,應該盡己所能讓cpu每時每刻都能夠釋放自己強大的算力,這也是NUMA架構的出現的主要原因,為了更好得展現這個過程,可以繼續向下看。
UMA(Uniform memory access)
在介紹NUMA之前,肯定是需要先了解一下一致性內存訪問UMA 的設計,這里的介紹能夠告訴你為什么系統硬件架構會演化成當前NUMA這樣架構。
如上圖,是NUMA架構之前的CPU訪問內存的方式。在主板上的兩個物理CPU 通過前端總線鏈接,來同步各自內部的cpu cache的數據,當cpu要訪問的數據不在cpu cache中,則需要通過北橋控制器 以及與其鏈接的數據地址總線訪問內存。
隨著CPU物理core增加以及HT技術的產生,這么多core的數據訪問都需要通過共享的frontside bus以及北橋控制器來訪問內存,這個時候的frontside bus便成為了CPU性能的瓶頸。
NUMA架構
這個時候通過引入NUMA架構,來提升CPU對內存訪問的效率。
如上圖,物理CPU之間通過QPI進行通信,而內存插槽則設置到各自CPU附近,去掉了北橋控制器,物理CPU直接通過數據總線來訪問內存,這個架構下整個CPU訪存可以橫向擴展了,物理CPU的個數可以增加,且只需要為其設置相臨近的可訪問內存就可以了。
但也會引入一些問題,這個時候對于CPU1 ,內存條3和4就距離自己比較近,而內存條1和2 就距離自己比較遠了。也就是NUMA架構引入了針對內存訪問的local 和 remote概念。
通過下圖來更進一步得了解一下NUMA架構下cpu訪問內存的remote和local方式:
其中:
- 每一個黃色的方塊代表一個用戶進程
- Node0,Node1 就是將我們上面介紹的NUMA架構中的CPU和距離自己近的內存進行綁定,形成一個Node
- 每一個Node對應一個物理cpu,其中的cpu0,cpu1,cpu2…代表之前提到的core,也就是每個物理cpu之上會有多個core
- 藍色的部分代表自己的距離物理CPU比較近的內存條。
用戶進程ProcessA綁定在一個核心/HT上運行(cpu0),如果操作系統為這個用戶進程分配的內存頁在node1上,那么cpu0需要通過QPI+DDR從node1的內存中讀取數據,這樣就產生了remote訪存的概念。這個時候有人會說為什么不把ProcessA遷移到Node1的CPU核心之上呢,但是ProcessA的內存頁可能在兩個node內存之上都有分配,這個時候總會有一些內存頁需要remote訪問。
所以NUMA架構下, 我們想要讓用戶進程擁有良好的內存訪問性能,需要讓用戶進程只在一個node下運行,也就是需要綁定NUMA。
具體可已通過如下方式:
numactl --cpunodebind=0 --membind=0 user_process
用戶進程綁定在node0的CPU和內存上,也就用戶進程由node0的cpu來調度,并且就近分配node0中的內存。
如果我們僅僅想要進程從單一的node上分配內存,可以通過如下方式來運行。
測試如下代碼:
#include <stdio.h>
#include <stdlib.h>
void main(int argc, char *argv[])
{int i, j;char *c;/* Allocate 4GB memory */for (i = 0; i < 1024; i++) {for (j = 0; j < 1024; j++) {c = malloc(4096);*c = 0xff;}}while (1);
}
總共分配4G的內存,通過如下方式運行,僅僅綁定內存從node1上分配,不限制cpu的調度node。
$ gcc malloc.c; $ numactl --membind=1 --cpunodebind=1 ./a.out &
[2] 43565
$ sudo numastat -p 43565Per-node process memory usage (in MBs) for PID 43565 (a.out)Node 0 Node 1--------------- ---------------
Huge 0.00 0.00
Heap 0.00 4112.00
Stack 0.00 0.01
Private 0.00 0.07
---------------- --------------- ---------------
Total 0.00 4112.09
接下來我們可以通過NUMA的幾種內存模式來看操作系統如何使用NUMA。
可以通過
numastat或者lscpu來確認自己的系統是否支持NUMA,如果有多個node,則表示自己的系統是支持NUMA的。
NUMA下的內存分配策略
這里主要介紹四種NUMA下的內核分配策略
command 工具可以指定設置:numactl/numastat/migratepages
系統調用 函數也可以設置:
#include <numaif.h>
// 為當前進程以及該進程的子進程 設置內存分配策略,默認是default方式
long set_mempolicy(int mode, const unsigned long *nodemask, unsigned long maxnode);// 為一個內存range 設置內存分配策略,比如MPOL_BIND
long mbind(void *addr, unsigned long len, int mode, const unsigned long *nodemask, unsigned long maxnode, unsigned flags);// 將當前進程的所有內存page遷移到其他的node中
long migrate_pages(int pid, unsigned long maxnode, const unsigned long *old_nodes, const unsigned long *new_nodes);
接下來看看詳細的內存分配策略
1. MPOL_DEFAULT
這個策略下,在用戶進程申請內存過程中 系統會優先從local node進行分配,如果local node中的內存不足,則會從nearby的node內存中進行分配。
2. MPOL_BIND
這個策略下的內存分配可以通過set_mempolicy 設置進程的內存分配方式 或者 通過mbind 來指定一段 memory range的分配方式。
在這個策略下,我們通過在對應系統調用中設置nodemask來指定當前進程內存可分配內存頁的node,且這個策略的內存分配方式比較嚴格。
如下圖:
以ProcessA為例,其設置的nodemask指定了(0,1)兩個node,也就是processA在CPU0上運行時會優先從node0上分配內存,如果node0的內存已經被分配滿了,則系統會從node1位processA分配,也就是上圖中node1的內存頁中有紅色部分的原因。
同理,其他的進程需要內存時操作系統也是按照其設置的nodemask中的節點來進行分配。
如果nodemask設置的node都沒有內存了,即使其他的node中仍然有內存,也會發生OOM 或者其他的內存不足問題。
3. MPOL_INTERLEAVE
這個策略也是linux-kernel 啟動過程默認的內存分配策略。
大體就是 從nodemask設置的node中交錯分配內存。
如上圖,ProcessA 在 interleave模式下 設置了node0和node1 ,則第一個內存頁會先從node0上分配,第二個內存頁從node1上分配,依此。
操作系統boot-up時設置這樣的分配策略的目的是為了降低單個node下的內存訪問負載,當初始化進程啟動之后 操作系統會將kernel的內存分配策略切換成 DEFAULT方式,也就是默認從local node進行內存分配。
4. MPOL_PREFERRED
這個策略下操作系統的內存分配會優先從距離自己近的node進行分配,如果nodemask 設置了一個或者多個node,則nodemask中的node會被當作就近內存 ,從這一些設置的node中進行內存分配。
如下圖:
ProcessC 的nodemask配置了node0, 則ProcessC的內存分配會優先從node0中進行分配,可以看到node0中綠色的內存頁。如果node0滿了,且nodemask只設置了一個node,則ProcessC會從就近node中進行分配,比如node1。
關于不同node的 distance,這個是由操作系統來維護的,主要根據不同node之間訪問內存消耗的時間來設置的。映射到實際的主板上,就類似前文介紹NUMA架構中的node和node之間的物理距離,只是這里node會更多一些。
5. 一些NUMA架構下的內核配置
$ sudo sysctl -a | grep -i numa
kernel.numa_balancing = 1 # How to disable numa balance? It can also be put into the kernel command by "numa_balancing=disable"
kernel.numa_balancing_scan_delay_ms = 1000
kernel.numa_balancing_scan_period_max_ms = 60000
kernel.numa_balancing_scan_period_min_ms = 1000
kernel.numa_balancing_scan_size_mb = 256
vm.numa_zonelist_order = default
總結
NUMA架構 是當下高性能服務器主流架構,尤其是搭配PMEM或者NVM 這樣的非易失性內存存儲介質,我們想要發揮這一些硬件高吞吐低延時的性能,一定是需要利用好NUMA特性才能發揮其性能。
比如針對PMEM的測試中,綁定NUAM和不綁定NUMA 測試出來的性能差異還是比較大的,感興趣的同學可以用如下fio配置測試一下。
[global]
ioengine=libpmem
direct=1
norandommap=1
randrepeat=0
runtime=60
time_based
size=1G
directory=./fio
group_reporting
[read256B-rand]
bs=256B
rw=randread
numjobs=32
iodepth=4
cpus_allowed=0-15,16-31 #綁定NUMA
總的來說,如果大家做基礎軟件相關的研發,像分布式存儲/分布式數據庫等,在當今主流服務器架構下,一定需要對NUMA架構有足夠的了解,否則無法將系統性能做到極致。
關于NUMA相關的內核實現,內核如何調度不同的numa node 中的CPU和內存以及I/O,如何實現numa node之間的互信 ,這一些底層實現都是非常復雜卻十分精妙的,想要完全掌握,內核的基礎實現是需要有一個概覽的過程的,后續繼續探索。
參考
相關參考論文均已放在github:numa-github
總結
以上是生活随笔為你收集整理的Linux NUMA 架构 :基础软件工程师需要知道一些知识的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 奥铃多少钱啊?
- 下一篇: Rocksdb 利用recycle_lo