轉自:https://www.sunliaodong.cn/2021/03/11/Linux-PageCache%E8%AF%A6%E8%A7%A3/
應用程序要存儲或訪問數據時,只需讀或者寫”文件”的一維地址空間即可,而這個地址空間與存儲設備上存儲塊之間的對應關係則由操作系統維護。說白了,文件就是基於內核態Page Cache的一層抽象。
相關場景
- 服務器的 load 飆高;
- 服務器的 I/O 吞吐飆高;
- 業務響應時延出現大的毛刺;
- 業務平均訪問時延明顯增加。
什麼是page cache
page cache是內存管理的內存,屬於內核不屬於用戶。
查看方式
- /proc/meminfo
- free命令
- vmstat命令
page cache指標說明
通過/proc/meminfo查看內存信息如下:
1
|
MemTotal: 2046920 kB
|
通過計算發現:
Buffers + Cached + SwapCached = Active(file) + Inactive(file) + Shmem + SwapCached
在 Page Cache 中,Active(file)+Inactive(file) 是 File-backed page(與文件對應的內存頁),平時用的 mmap() 內存映射方式和 buffered I/O 來消耗的內存就屬於這部分,這部分在真實的生產環境上也最容易產生問題。
SwapCached說明(生產環境中不建議開啓,防止IO引起性能抖動)
SwapCached 是在打開了 Swap 分區後,把 Inactive(anon)+Active(anon) 這兩項裏的匿名頁給交換到磁盤(swap out),然後再讀入到內存(swap in)後分配的內存。由於讀入到內存後原來的 Swap File 還在,所以 SwapCached 也可以認爲是 File-backed page,即屬於 Page Cache。
Shmem
- Shmem 是指匿名共享映射這種方式分配的內存(free 命令中 shared 這一項)
- 進程使用mmap(MAP_ANON|MAP_SHARED)的方式申請內存
- tmpfs: 磁盤的速度是遠遠低於內存的,有些應用程序爲了提升性能,會避免將一些無需持續化存儲的數據寫入到磁盤,而是把這部分臨時數據寫入到內存中,然後定期或者在不需要這部分數據時,清理掉這部分內容來釋放出內存。在這種需求下,就產生了一種特殊的 Shmem:tmpfs
frem命令的說明
數據來源於/proc/meminfo
1
|
free -k
|
通過源碼可知:buff/cache = Buffers + Cached + SReclaimable
SReclaimable 是指可以被回收的內核內存,包括 dentry 和 inode。
緩存的具體含義
官方定義
1
|
Buffers %lu
|
具體解釋
- Buffers 是對原始磁盤塊的臨時存儲,也就是用來緩存磁盤的數據,通常不會特別大(20MB 左右)。這樣,內核就可以把分散的寫集中起來,統一優化磁盤的寫入,比如可以把多次小的寫合併成單次大的寫等等。
- Cached 是從磁盤讀取文件的頁緩存,也就是用來緩存從文件讀取的數據。這樣,下次訪問這些文件數據時,就可以直接從內存中快速獲取,而不需要再次訪問緩慢的磁盤。
- SReclaimable 是 Slab 的一部分。Slab 包括兩部分,其中的可回收部分,用 SReclaimable 記錄;而不可回收部分,用 SUnreclaim 記錄。
- Buffer 是對磁盤數據的緩存,而 Cache 是文件數據的緩存,它們既會用在讀請求中,也會用在寫請求中
PageCache數據結構
- 內存管理系統與Page Cache交互,負責維護每項 Page Cache 的分配和回收,同時在使用 memory map 方式訪問時負責建立映射;
- VFS 與Page Cache交互,負責 Page Cache 與用戶空間的數據交換,即文件讀寫;
- 具體文件系統則一般只與 Buffer Cache 交互,它們負責在外圍存儲設備和 Buffer Cache 之間交換數據。
- 一個Page Cache包含多個Buffer Cache,一個Buffer Cache與一個磁盤塊一一對應;假定了 Page 的大小是 4K,則文件的每個4K的數據塊最多隻能對應一個 Page Cache 項,它通過一個是 radix tree來管理文件塊和page cache的映射關係,Radix tree 是一種搜索樹,Linux 內核利用這個數據結構來通過文件內偏移快速定位 Cache 項。
爲什麼使用page cache
減少 I/O,提升應用的 I/O 速度
- 具體文件系統:,如 ext2/ext3、jfs、ntfs 等,負責在文件 Cache和存儲設備之間交換數據
- 虛擬文件系統VFS: 負責在應用程序和文件 Cache 之間通過 read/write 等接口交換數據
- 內存管理系統: 負責文件 Cache 的分配和回收
- 虛擬內存管理系統(VMM): 則允許應用程序和文件 Cache 之間通過 memory map的方式交換數據
- 在 Linux 系統中,文件 Cache 是內存管理系統、文件系統以及應用程序之間的一個聯繫樞紐。
page cache的產生
產生方式
- Buffered I/O(標準 I/O)如:read/write/sendfile等;
- Memory-Mapped I/O(存儲映射 I/O)如:mmap;
- sendfile和mmap都是零拷貝的實現方案。
產生方式的區別
- 標準 I/O 是寫的 (write(2)) 用戶緩衝區 (Userpace Page 對應的內存),然後再將用戶緩衝區裏的數據拷貝到內核緩衝區 (Pagecache Page 對應的內存);如果是讀的 (read(2)) 話則是先從內核緩衝區拷貝到用戶緩衝區,再從用戶緩衝區讀數據,也就是 buffer 和文件內容不存在任何映射關係。
- 對於存儲映射 I/O 而言,則是直接將 Pagecache Page 給映射到用戶地址空間,用戶直接讀寫 Pagecache Page 中內容。
常規文件讀寫
FileChannel#read,FileChannel#write,共涉及四次上下文切換(內核態和用戶態的切換,包括read調用,read返回,write調用,write返回)和四次數據拷貝。
髒頁
1
|
cat /proc/vmstat | egrep "dirty|writeback"
|
nr_dirty 表示當前系統中積壓了多少髒頁,nr_writeback 則表示有多少髒頁正在回寫到磁盤中,他們兩個的單位都是 Page(4KB)。
mmap
- 文件(page cache)直接映射到用戶虛擬地址空間,內核態和用戶態共享一片page cache,避免了一次數據拷貝
- 建立mmap之後,並不會立馬加載數據到內存,只有真正使用數據時,纔會引發缺頁異常並加載數據到內存
memory map具體步驟如下
- 應用程序調用mmap(圖中1),先到內核中
- 後調用do_mmap_pgoff(圖中2),該函數從應用程序的地址空間中分配一段區域作爲映射的內存地址,並使用一個VMA(vm_area_struct)結構代表該區域,
- 之後就返回到應用程序(圖中3)
- 當應用程序訪問mmap所返回的地址指針時(圖中4),由於虛實映射尚未建立,會觸發缺頁中斷(圖中5)。之後系統會調用缺頁中斷處理函數(圖中6),在缺頁中斷處理函數中,內核通過相應區域的VMA結構判斷出該區域屬於文件映射,於是調用具體文件系統的接口讀入相應的Page Cache項(圖中7、8、9),並填寫相應的虛實映射表。
- 經過這些步驟之後,應用程序就可以正常訪問相應的內存區域了。
sendfile
- 使用sendfile的方式避免了用戶空間與內核空間的交互,複製次數減少到三次,內核態與用戶態切換減少到兩次。
- 在 Linux 內核 2.4 及後期版本中,針對套接字緩衝區描述符做了相應調整,DMA自帶了收集功能,對於用戶方面,用法還是一樣。內部只把包含數據位置和長度信息的描述符追加到套接字緩衝區,DMA 引擎直接把數據從內核緩衝區傳到協議引擎,從而消除了最後一次 CPU參與的拷貝動作。
順序讀寫
文件預讀
文件的預讀機制,它是一種將磁盤塊預讀到page cache的機制,執行步驟如下:
- 對於每個文件的第一個讀請求,系統讀入所請求的頁面並讀入緊隨其後的少數幾個頁面(不少於一個頁面,通常是三個頁面),這時的預讀稱爲同步預讀。
- 對於第二次讀請求,如果所讀頁面不在Cache中,即不在前次預讀的group中,則表明文件訪問不是順序訪問,系統繼續採用同步預讀;如果所讀頁面在Cache中,則表明前次預讀命中,操作系統把預讀group擴大一倍,並讓底層文件系統讀入group中剩下尚不在Cache中的文件數據塊,這時的預讀稱爲異步預讀。
- 無論第二次讀請求是否命中,系統都要更新當前預讀group的大小。
- 系統中定義了一個window,它包括前一次預讀的group和本次預讀的group。任何接下來的讀請求都會處於兩種情況之一:
- 第一種情況是所請求的頁面處於預讀window中,這時繼續進行異步預讀並更新相應的window和group;
- 第二種情況是所請求的頁面處於預讀window之外,這時系統就要進行同步預讀並重置相應的window和group。
圖中group指一次讀入page cached的集合;window包括前一次預讀的group和本次預讀的group;淺灰色代表要用戶想要查找的page cache,深灰色代表命中的page。
順序讀寫高效的原因
以順序讀爲例,當用戶發起一個 fileChannel.read(4kb) 之後,實際發生了兩件事
- 操作系統從磁盤加載了 16kb 進入 PageCache,這被稱爲預讀
- 操作通從 PageCache 拷貝 4kb 進入用戶內存
- 當用戶繼續訪問接下來的 [4kb,16kb] 的磁盤內容時,便是直接從 PageCache 去訪問了
page cache的消亡
page cache的回收主要是針對free 命令中的 buff/cache 中的這些就是“活着”的 Page Cache。回收的過程如下圖所示:
回收的方式主要是兩種:直接回收和後臺回收,具體的回收行爲,可以使用以下命令查看:
1
|
sar -B 1
|
- pgscank/s : kswapd(後臺回收線程) 每秒掃描的 page 個數。
- pgscand/s: Application 在內存申請過程中每秒直接掃描的 page 個數。
- pgsteal/s: 掃描的 page 中每秒被回收的個數。
- %vmeff: pgsteal/(pgscank+pgscand), 回收效率,越接近 100 說明系統越安全,越接近 0 說明系統內存壓力越大。
sar -B與/proc/vmstat比對
sar -B | /proc/vmstat |
---|---|
pgscank | pgscan_kswapd |
pgscand | pgscan_direct |
pgsteal | pgsteal_kswapd+pgsteal_direct |
其他
DMA(Direct Memory Access,直接存儲器訪問)
- DMA的出現就是爲了解決批量數據的輸入/輸出問題。DMA是指外部設備不通過CPU而直接與系統內存交換數據的接口技術
- DMA控制器需要具備的功能:
- 能向CPU發出系統保持信號,提出總線接管請求
- 當CPU同意接管請求之後,對總線的控制交給DMA
- 能對存儲器尋址及能修改地址指針,實現對內存的讀寫
- 能決定本次DMA傳送的字節數,判斷DMA傳送是否借宿
- 發送DMA結束信號,使CPU恢復正常工作狀態
堆外內存
堆內存與堆外內存的關係
堆內內存 | 堆外內存 | |
---|---|---|
底層實現 | 數組,JVM內存 | unsafe.allocateMemory(size)返回直接內存 |
分配大小限制 | -Xms-Xmx, 數組大小,當前JVM free memory大於1.5G時,ByteBuffer.allocate(900M)會報錯 |
-XX:MaxDirectMemorySize參數從JVM層面限制,同時受到機器虛擬內存的限制 |
垃圾回收 | 當前DirectByteBuffer不再被使用時,會觸發內部cleaner的鉤子 保險起見,可以考慮手動回收 ((DirectBuffer)buffer).cleaner().clean() |
|
內存複製 | 堆內內存---堆外內存---pageCache | 堆外內存--pageCache |
最佳實踐
- 當需要申請大塊的內存時,堆內內存會受到限制,只能分配堆外內存。
- 堆外內存適用於生命週期中等或較長的對象。(如果是生命週期較短的對象,在 YGC 的時候就被回收了,就不存在大內存且生命週期較長的對象在 FGC 對應用造成的性能影響)。
- 堆內內存刷盤的過程中,還需要複製一份到堆外內存,這部分內容可以在 FileChannel 的實現源碼中看到細節
- 使用 HeapByteBuffer 讀寫都會經過 DirectByteBuffer,寫入數據的流轉方式其實是:HeapByteBuffer -> DirectByteBuffer -> PageCache -> Disk,讀取數據的流轉方式正好相反。
- 使用 HeapByteBuffer 讀寫會申請一塊跟線程綁定的 DirectByteBuffer。這意味着,線程越多,臨時 DirectByteBuffer 就越會佔用越多的空間。
- 堆外內存就是把內存對象分配在Java虛擬機堆以外的內存,這些內存直接受操作系統管理(而不是虛擬機),這樣做的結果就是能夠在一定程度上減少垃圾回收對應用程序造成的影響。
- 內存回收流程
PageCache內存回收
回收過程
在內存緊張的時候會觸發內存回收,內存回收會嘗試去回收reclaimable(可被回收)的內存。包括PageCache以及reclaimable kernel memory(比如slab)。
避免PageCache回收出現的性能問題
memory cgroup protection
- memory.max:memory cgroup 內的進程最多能夠分配的內存,如果不設置的話,就默認不做內存大小的限制
- memory.high:當 memory cgroup 內進程的內存使用量超過了該值後就會立即被回收掉,目的是爲了儘快的回收掉不活躍的 Page Cache。
- memory.low:用來保護重要數據的,當 memory cgroup 內進程的內存使用量低於了該值後,在內存緊張觸發回收後就會先去回收不屬於該 memory cgroup 的 Page Cache,等到其他的 Page Cache 都被回收掉後再來回收這些 Page Cache。
- memory.min:用來保護重要數據的,只不過與 memoy.low 有所不同的是,當 memory cgroup 內進程的內存使用量低於該值後,即使其他不在該 memory cgroup 內的 Page Cache 都被回收完了也不會去回收這些 Page Cache。
- 總結:如果你想要保護你的 Page Cache 不被回收,你就可以考慮將你的業務進程放在一個 memory cgroup 中,然後設置 memory.{min,low} 來進行保護;與之相反,如果你想要儘快釋放你的 Page Cache,那你可以考慮設置 memory.high 來及時的釋放掉不活躍的 Page Cache。
出現load過高的原因
直接內存回收引起
內存回收過程
後臺回收原理:
通過調整參數vm.min_free_kbytes來提高後臺進程回收頻率。
1
|
cat /proc/sys/vm/min_free_kbytes
|
通過調整內存水位,在一定程度上保障了應用的內存申請,但是同時也帶來了一定的內存浪費,因爲系統始終要保障有這麼多的 free 內存,這就壓縮了 Page Cache 的空間。調整的效果你可以通過 /proc/zoneinfo 來觀察
系統中髒頁積壓過多
內存申請過程
解決方法
設置配置:/proc/vmstat
1
|
vm.dirty_background_bytes = 0
|
系統numa策略配置不當
內存泄漏
OOM KILL邏輯
可以調整oom_score_adj來防止進程被殺掉(不建議配置)
如何觀察內核內存泄漏
- 如果 /proc/meminfo 中內核內存(比如 VmallocUsed 和 SUnreclaim)太大,那很有可能發生了內核內存泄漏
- 週期性地觀察 VmallocUsed 和 SUnreclaim 的變化,如果它們持續增長而不下降,也可能是發生了內核內存泄漏
- 通過 /proc/vmallocinfo 來看到該模塊的內存使用情況
- kmemleak 內核內存分析工具
排查思路
/proc/meminfo | 含義以及排查思路 |
---|---|
Active(anon) | 在active anon lru的page,與下一項相互轉換 |
Inactive(anon) | 在inactive anonlru的page,可以交換到swap分區,(active anno也是)但是不能回收 程序使用malloc()或mmap()匿名方式申請並且寫後的內存。如果過大,排除思路: 1.使用top找出內存消耗最大的進程 2.使用pmap分析該進程 3.如果沒有任何進程內存開銷大,則重點排除tmpfs |
Unevictable | 在系統內存緊張時不能被回收,主要組成: 1.ram disk或ramfs消耗的內存 2.以SHM_LOCK方式申請的Shmem 3.使用mlock()序列函數來管理的內存 |
Mlocked | 屬於Unevictable的一種,重點排除mlock()方式包含的內存 |
AnonPages | AnonPages!=Active(anon)+Inactive(anon) 因爲shmem(包括tmpfs)雖然屬於active(anon)或Inactive(anon),但是他們有自己的內存文件,所以不屬於AnonPages active anon和Inactive anon表示不可回收但是可以被交換到swap分區的內存 AnonPages沒有對應文件的內存 排除malloc()方式申請的內存或mmap(PROT_WRITE,MAP_ANON|MAP_PRIVATE)方式申請的內存 |
Mapped | 使用mmap(2)申請,沒有被unmap的內存;unmap包含主動調用unmap(2)以及內核內存回收時的unmap 排查mmap()申請的內存 |
Shmem | 共享內存,特別注意tmpfs,排查思路 1. 使用top找出shr最大的進程 2.使用pmap分析該進程 3.如果沒有任何進程消耗shr內存,則重點排查tmpfs |
Slab | 分爲可被回收(SReclaimable)和不可以被回收(SUnreclaim),其中不可被回收的slab如果發生泄漏, 比如kmalloc申請的內存沒有釋放,排查思路 1.使用slaptop分析slab最大的數據 2.排查驅動程序以kmalloc()方式申請的內存 |
VmallocUsed | 通過vmalloc分配的內核內存,可以使用/proc/vmallocinfo,來判斷哪些驅動程序以vmalloc方式申請的內存較多 可以嘗試卸載驅動,釋放內存 |
- 應用程序可以通過 malloc() 和 free() 在用戶態申請和釋放內存,與之對應,可以通過 kmalloc()/kfree() 以及 vmalloc()/vfree() 在內核態申請和釋放內存
- vmalloc 申請的內存會體現在 VmallocUsed 這一項中,即已使用的 Vmalloc 區大小;而 kmalloc 申請的內存則是體現在 Slab 這一項中,它又分爲兩部分,其中 SReclaimable 是指在內存緊張的時候可以被回收的內存,而 SUnreclaim 則是不可以被回收只能主動釋放的內存。
其他
清理緩存buffer/cache
運行sync將dirty的內容寫回硬盤
通過修改proc系統的drop_caches清理free的cache
1
|
echo 3 > /proc/sys/vm/drop_caches
|
可以通過/proc/vmstat
文件判斷是否執行過drop_caches:
1
|
[root@instance-gctg007a ~]# cat /proc/vmstat | grep drop
|
可以調用crond定時任務:每10分鐘執行一次
1
|
*/10 * * * * sync;echo 3 > /proc/sys/vm/drop_caches;
|
重要配置參數
/proc/sys/vm/dirty_ratio(同步刷盤)
這個參數控制文件系統的文件系統寫緩衝區的大小,單位是百分比,表示系統內存的百分比,表示當寫緩衝使用到系統內存多少的時候,開始向磁盤寫出數據。增大之會使用更多系統內存用於磁盤寫緩衝,也可以極大提高系統的寫性能。但是,當你需要持續、恆定的寫入場合時,應該降低其數值,一般啓動上缺省是 10。設1加速程序速度
/proc/sys/vm/dirty_background_ratio(異步刷盤)
這個參數控制文件系統的pdflush進程,在何時刷新磁盤。單位是百分比,表示系統內存的百分比,意思是當寫緩衝使用到系統內存多少的時 候,pdflush開始向磁盤寫出數據。增大之會使用更多系統內存用於磁盤寫緩衝,也可以極大提高系統的寫性能。但是,當你需要持續、恆定的寫入場合時, 應該降低其數值,一般啓動上缺省是 5
/proc/sys/vm/dirty_writeback_centisecs
這個參數控制內核的髒數據刷新進程pdflush的運行間隔。單位是 1/100 秒。缺省數值是500,也就是 5 秒。如果你的系統是持續地寫入動作,那麼實際上還是降低這個數值比較好,這樣可以把尖峯的寫操作削平成多次寫操
/proc/sys/vm/dirty_expire_centisecs
這個參數聲明Linux內核寫緩衝區裏面的數據多“舊”了之後,pdflush進程就開始考慮寫到磁盤中去。單位是 1/100秒。缺省是 30000,也就是 30 秒的數據就算舊了,將會刷新磁盤。對於特別重載的寫操作來說,這個值適當縮小也是好的,但也不能縮小太多,因爲縮小太多也會導致IO提高太快。建議設置爲 1500,也就是15秒算舊。
/proc/sys/vm/drop_caches
釋放已經使用的cache
/proc/sys/vm/page_cluster
該文件表示在寫一次到swap區的時候寫入的頁面數量,0表示1頁,1表示2頁,2表示4頁。
/proc/sys/vm/swapiness
該文件表示系統進行交換行爲的程度,數值(0-100)越高,越可能發生磁盤交換。
/proc/sys/vm/vfs_cache_pressure
該文件表示內核回收用於directory和inode cache內存的傾向