K8s 里我的容器到底用了多少內(nèi)存?
作者 | frostchen
導語
Linux下開發(fā)者習慣在物理機或者虛擬機環(huán)境下使用top和free等命令查看機器和進程的內(nèi)存使用量,近年來越來越多的應用服務完成了微服務容器化改造,過去查看、監(jiān)控和定位內(nèi)存使用量的方法似乎時常不太奏效。如果你的應用程序剛剛遷移到K8s中,經(jīng)常被諸如以下問題所困擾:容器的內(nèi)存使用率為啥總是接近99%?malloc/free配對沒問題,內(nèi)存使用量卻一直上漲?內(nèi)存使用量超過了限制量卻沒有被OOM Kill? 登錄容器執(zhí)行top,free看到的輸出和平臺監(jiān)控視圖完全對不上?... 本文假設讀者熟悉Linux環(huán)境,擁有常見后端開發(fā)語言(C/C++ /Go/Java等)使用經(jīng)驗,希望后面的內(nèi)容能在讀者面臨此類疑惑時提供一些有效思路。
K8s中監(jiān)控數(shù)據(jù)主要來源是 cadvisor, 容器內(nèi)存使用量的相關(guān)指標有以下:
這些指標究竟是什么含義?在不同的應用場景下需要重點關(guān)注哪些指標?讓我們從回顧linux進程地址空間開始,逐步挖掘容器內(nèi)存使用奧秘。
一、進程是怎么分配內(nèi)存的?
回憶一下linux進程虛擬地址空間分布圖。
+-------------------------------+ 0xFFFFFFFFFFFFFFFF (64-bit)
| Kernel Space | 內(nèi)核空間,用于操作系統(tǒng)內(nèi)核和內(nèi)核模塊
+-------------------------------+ 0x00007FFFFFFFFFFF
| User Space | 用戶空間進程的虛擬地址空間
| Shared Libraries | 動態(tài)加載的共享庫 (.so 文件)
+-------------------------------+ 0x00007FFFC0000000
| Heap | 動態(tài)內(nèi)存分配區(qū)域 (malloc, calloc, realloc)
| (malloc, etc.) | 堆的大小可以動態(tài)增長或收縮
+-------------------------------+ 0x00007FFFB0000000
| BSS Segment | 未初始化的全局變量和靜態(tài)變量
| (Uninitialized Data) | 在程序啟動時被初始化為零
+-------------------------------+ 0x00007FFFA0000000
| Data Segment | 已初始化的全局變量和靜態(tài)變量
| (Initialized Data) | 在程序啟動時被初始化為特定的值
+-------------------------------+ 0x00007FFF90000000
| Text Segment | 可執(zhí)行代碼段
| (Code) | 通常是只讀的,以防止代碼被意外修改
+-------------------------------+ 0x00007FFF80000000
| Stack | 用于存儲函數(shù)調(diào)用的局部變量、參數(shù)和返回地址
| | 棧通常從高地址向低地址增長
+-------------------------------+ 0x0000000000000000
在linux內(nèi)核里描述上述圖的結(jié)構(gòu)是mm_struct,它還可以展開得更詳細:
+-------------------------------+
| task_struct (/bin/gonzo) |
| |
| mm |
| | |
| v |
| +---------------------------+ |
| | mm_struct | |
| | | |
| | mmap | |
| | | | |
| | v | |
| | +-----------------------+ | |
| | | vm_area_struct | | |
| | | VM_READ | VM_EXEC | | |
| | |-----------------------| | |
| | | Text (file-backed) | | |
| | +-----------------------+ | |
| | | | |
| | v | |
| | +-----------------------+ | |
| | | vm_area_struct | | |
| | | VM_READ | VM_WRITE | | |
| | |-----------------------| | |
| | | Data (file-backed) | | |
| | +-----------------------+ | |
| | | | |
| | v | |
| | +-----------------------+ | |
| | | vm_area_struct | | |
| | | VM_READ | VM_WRITE | | |
| | |-----------------------| | |
| | | BSS (anonymous) | | |
| | +-----------------------+ | |
| | | | |
| | v | |
| | +-----------------------+ | |
| | | vm_area_struct | | |
| | | VM_READ | VM_WRITE | | |
| | |-----------------------| | |
| | | Heap (anonymous) | | |
| | +-----------------------+ | |
| | | | |
| | v | |
| | +-----------------------+ | |
| | | vm_area_struct | | |
| | | VM_READ | VM_EXEC | | |
| | |-----------------------| | |
| | | Memory mapping | | |
| | +-----------------------+ | |
| | | | |
| | v | |
| | +-----------------------+ | |
| | | vm_area_struct | | |
| | | VM_READ | VM_WRITE | | |
| | | VM_GROWS_DOWN | | |
| | |-----------------------| | |
| | | Stack (anonymous) | | |
| | +-----------------------+ | |
| +---------------------------+ |+-------------------------------+
可以發(fā)現(xiàn),linux進程地址空間是由一個個vm_area_struct(vma)組成,每個vma都有自己地址區(qū)間。如果你的代碼panic或者Segmentation Fault崩潰,最直接的原因就是你引用的指針值不在進程的任意一個vma區(qū)間內(nèi)。你可以通過 /proc/<pid>/maps 來觀察進程的vma分布。
1. malloc分配內(nèi)存
malloc函數(shù)增大了進程虛擬地址空間的heap容量,擴大了mm描述符中vma的start和end長度,或者插入了新的vma;但是它剛完成調(diào)用后,并沒有增大進程的實際內(nèi)存使用量。
以下是個代碼示例證明上述言論。
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/resource.h>
#include <stdio.h>
#include <time.h>
const int64_t GB = 1024 * 1024 * 1024;
const int64_t MB = 1024 * 1024;
const int64_t KB = 1024;
void max_rss() {
struct rusage r_usage;
getrusage(RUSAGE_SELF, &r_usage);
printf("Current max rss %ld kb, pagefault minor %ld, major %ld\n",
r_usage.ru_maxrss, r_usage.ru_minflt, r_usage.ru_majflt);
}
int main() {
printf("Pid %lu\n", getpid());
int number = 128;
void *ptr = malloc(number * MB);
if (ptr == 0) {
printf("Out of memory\n");
exit(EXIT_FAILURE);
}
printf("Allocated %d MB memory by malloc(3), ptr %p\n", number, ptr);
max_rss();
sleep(60);
memset(ptr, 0, number * MB);
printf("Used %d MB memory by memset(3)\n", number);
max_rss();
sleep(60);
free(ptr);
printf("Memory ptr %p freed by free(3)\n", ptr);
max_rss();
sleep(60);
return 0;
}
可見輸出:
Pid 932451
Allocated 128 MB memory by malloc(3), ptr 0x7f3e6cdff010
Current max rss 3800 kb, pagefault minor 122, major 0
Used 128 MB memory by memset(3)
Current max rss 132732 kb, pagefault minor 187, major 0
Memory ptr 0x7f3e6cdff010 freed by free(3)Current max rss 132732 kb, pagefault minor 187, major 0
階段總結(jié)1
當memset 128MB長度的數(shù)據(jù)完成后,我們立刻觀察到進程發(fā)生了32768次minor pagefault, 同時RSS內(nèi)存占用提升到129MB。注意 32768 * 4096正好等于128MB,而4096正好是linux page默認大小。可以在程序sleep的時段用top觀察監(jiān)控統(tǒng)計進一步證實結(jié)論。
進一步說,malloc申請到的地址,在得到真實的使用之前,必須經(jīng)歷缺頁中斷,完成建立虛擬地址到物理地址的映射。完成物理頁分配的虛擬地址空間才會被計算到內(nèi)存使用量中。
二、container_memory_rss
1.. 進程的RSS
進程的RSS(Resident Set Size)是當前使用的實際物理內(nèi)存大小,包括代碼段、堆、棧和共享庫等所使用的內(nèi)存, 實際上就是頁表中物理頁部分的全部大小。
更精確地說,根據(jù)內(nèi)核的 get_mm_rss, RSS由FilePages, AnnoPages和ShmemPages組成。
以下是一個例子,分別展示了這三種內(nèi)存的申請和使用方式,F(xiàn)ilePages, AnnoPages和ShmemPages 分別為4MiB, 8MiB和10MiB,供給22MiB.
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/shm.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FILE_SIZE (4 * 1024 * 1024) // 4 MiB
#define ANON_SIZE (8 * 1024 * 1024) // 8 MiB
#define SHM_SIZE (10 * 1024 * 1024) // 10 MiB
void allocate_filepages() {
int fd = open("tempfile", O_RDWR | O_CREAT | O_TRUNC, 0600);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
if (ftruncate(fd, FILE_SIZE) == -1) {
perror("ftruncate");
close(fd);
exit(EXIT_FAILURE);
}
void *file_mem = mmap(NULL, FILE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (file_mem == MAP_FAILED) {
perror("mmap");
close(fd);
exit(EXIT_FAILURE);
}
memset(file_mem, 0, FILE_SIZE); // 使用內(nèi)存
printf("Allocated %d MiB of file-mapped memory\n", FILE_SIZE / (1024 * 1024));
// 保持映射,直到程序結(jié)束
// munmap(file_mem, FILE_SIZE);
// close(fd);
// unlink("tempfile");
}
void allocate_anonpages() {
void *anon_mem = malloc(ANON_SIZE);
if (anon_mem == NULL) {
perror("malloc");
exit(EXIT_FAILURE);
}
memset(anon_mem, 0, ANON_SIZE); // 使用內(nèi)存
printf("Allocated %d MiB of anonymous memory\n", ANON_SIZE / (1024 * 1024));
// free(anno_mem);
}
void allocate_shmempages() {
int shmid = shmget(IPC_PRIVATE, SHM_SIZE, IPC_CREAT | 0600);
if (shmid == -1) {
perror("shmget");
exit(EXIT_FAILURE);
}
void *shm_mem = shmat(shmid, NULL, 0);
if (shm_mem == (void *)-1) {
perror("shmat");
shmctl(shmid, IPC_RMID, NULL);
exit(EXIT_FAILURE);
}
memset(shm_mem, 0, SHM_SIZE); // 使用內(nèi)存
printf("Allocated %d MiB of shared memory\n", SHM_SIZE / (1024 * 1024));
// 保持映射,直到程序結(jié)束
// shmdt(shm_mem);
// shmctl(shmid, IPC_RMID, NULL);
}
int main() {
printf("Process %d\n", getpid());
allocate_filepages();
allocate_anonpages();
allocate_shmempages();
sleep(3600);
return 0;
}
觀察top -p $pid的輸出:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3881259 root 20 0 28540 24184 15872 S 0.0 0.1 0:00.01 a.out
通過top發(fā)現(xiàn),進程的RSS 是24184KiB, 比我們申請的22MiB,也就是22528KiB, 要大1656KiB。
進一步觀察/proc/$pid/status,發(fā)現(xiàn):
....
VmRSS: 24184 kB
RssAnon: 8312 kB
RssFile: 5632 kB
RssShmem: 10240 kB
VmData: 8436 kB
VmStk: 132 kB
VmExe: 4 kB
VmLib: 1576 kB
VmPTE: 100 kB
VmSwap: 0 kB
....
VmRSS和top里看到RES完全一致。RssAnno 比8092KiB多了120KiB,因為它還包括了stack。RssFile比 4096KiB多了1536KiB,因為它還包括了共享庫。內(nèi)核mm_struct計數(shù)并不總是完全及時和精準的。
階段總結(jié)2
進程RSS組成 | 描述 |
匿名頁 | 通常來源于 malloc,進入 brk 或者 mmap 匿名映射 |
共享內(nèi)存 | 來自 shmget 系列調(diào)用 |
mmap 文件映射 | 通過 mmap 調(diào)用映射文件到進程地址空間 |
棧 (stack) | 進程的調(diào)用棧 |
二進制文件 | 加載進程本身的二進制文件占用的內(nèi)存 |
動態(tài)鏈接庫 | 加載的動態(tài)鏈接庫(共享庫)占用的內(nèi)存 |
頁表 | 內(nèi)核中存儲頁表的部分 |
2. 容器(memcg)的RSS
K8s容器環(huán)境下,容器里的進程都歸屬同一個cgroup控制組,本文只關(guān)注內(nèi)存控制組(memcg)。把剛才的代碼做成容器鏡像,部署在TKEx環(huán)境里, 觀察容器內(nèi)存使用相關(guān)指標。
觀察到container_memory_rss只有2047 * 4096 Bytes, 略小于8MiB, 遠遠低于上一節(jié)top觀察到的24MiB,這是為什么?
1.1中通過觀察/proc/$pid/status和top的輸出,我們得出了進程的RSS估算方法,即:
- 占主要部分的 malloc導致的匿名頁(brk/mmap匿名映射) + 使用shmem共享內(nèi)存 + mmap文件映射;
- stack部分,text部分和動態(tài)鏈接庫部分,頁表部分,通常占比很小。
那memory cgroup的RSS的計算方法是不是就是簡單地把memcg下歸屬的所有的進程RSS簡單求和呢?顯然不是。通過追溯cadvisor相關(guān)代碼, 發(fā)現(xiàn)這個數(shù)值來來自容器所屬cgroup path下的memory.stat文本中的rss字段。
(1) 如何找到容器對應的memcg path?
每個容器的 Memory Cgroup 路徑根據(jù)其 QoS 類別和唯一標識符來確定。路徑的基本格式如下:
Burstable:
/sys/fs/cgroup/memory/kubepods/burstable/pod<uid>/<container-id>
BestEffort:
/memory/kubepods/besteffort/pod<uid>/<container-id>
Guaranteed:
/sys/fs/cgroup/memory/kubepods/pod<uid>/<container-id>
可以通過查看Pod Yaml里的Status來確認Pod的Qos類別。
找到memcg path后,可以發(fā)現(xiàn)目錄下有很多記錄文件,這里關(guān)注memory.stat:
root@memory-0:~# ls /sys/fs/cgroup/memory/kubepods/burstable/pod2d08e58b-50f7-41fa-bd42-946402c34646/b366c08f2ecedd6acdb38e4ec24913aea0ca3babeed297abbcfafafa4e8027de
cgroup.clone_children memory.bind_blkio memory.kmem.tcp.max_usage_in_bytes memory.memsw.max_usage_in_bytes memory.pressure memory.usage_in_bytes
cgroup.event_control memory.failcnt memory.kmem.tcp.usage_in_bytes memory.memsw.usage_in_bytes memory.pressure_level memory.use_hierarchy
cgroup.priority memory.force_empty memory.kmem.usage_in_bytes memory.move_charge_at_immigrate memory.priority_wmark_ratio memory.use_priority_oom
cgroup.procs memory.kmem.failcnt memory.limit_in_bytes memory.numa_stat memory.sli memory.vmstat
memory.alloc_bps memory.kmem.limit_in_bytes memory.max_usage_in_bytes memory.oom.group memory.sli_max notify_on_release
memory.async_distance_factor memory.kmem.max_usage_in_bytes memory.meminfo memory.oom_control memory.soft_limit_in_bytes tasks
memory.async_high memory.kmem.slabinfo memory.meminfo_recursive memory.pagecache.current memory.stat
memory.async_low memory.kmem.tcp.failcnt memory.memsw.failcnt memory.pagecache.max_ratio memory.swappiness
memory.async_ratio memory.kmem.tcp.limit_in_bytes memory.memsw.limit_in_bytes memory.pagecache.reclaim_ratio memory.sync
(2) memory.stat里的 rss 是怎么計算的?
追溯linux memory cgroup(后面記做memcg)的相關(guān)源碼,memcg統(tǒng)計了以下內(nèi)存使用:
static const unsigned int memcg1_stats[] = {
MEMCG_CACHE,
MEMCG_RSS,
MEMCG_RSS_HUGE,
NR_SHMEM,
NR_FILE_MAPPED,
NR_FILE_DIRTY,
NR_WRITEBACK,
MEMCG_SWAP,
};
跟蹤MEMCG_RSS的記錄情況,發(fā)現(xiàn)只有匿名頁的數(shù)量被統(tǒng)計到MEMCG_RSS里,這和前面觀察的進程的RSS不一樣。共享內(nèi)存page只被計入MEMCG_CACHE,即便它位于匿名LRU。
static void mem_cgroup_charge_statistics(struct mem_cgroup *memcg,
struct page *page,
bool compound, int nr_pages)
{
/*
* Here, RSS means 'mapped anon' and anon's SwapCache. Shmem/tmpfs is
* counted as CACHE even if it's on ANON LRU.
*/
if (PageAnon(page))
__mod_memcg_state(memcg, MEMCG_RSS, nr_pages);
else {
__mod_memcg_state(memcg, MEMCG_CACHE, nr_pages);
if (PageSwapBacked(page))
__mod_memcg_state(memcg, NR_SHMEM, nr_pages);
}
....
}
而我們之前觀察到 container_memory_cache接近14MiB, 包括了Shmem和mmap文件映射的部分。這樣得出的結(jié)論是,memory cgroup的RSS只統(tǒng)計了上述代碼中malloc分配出的內(nèi)存,不包含另外兩部分。
階段總結(jié)3
類別 | 進程的 RSS | 容器的 RSS |
brk 分配 | ? | ? |
mmap 匿名映射 | ? | ? |
共享內(nèi)存 | ? | |
mmap 文件映射 | ? | |
棧 (stack) | ? | ? |
二進制文件 | ? | ? |
動態(tài)鏈接庫 | ? | ? |
頁表 | ? | ? |
三、container_memory_cache
1. 初識PageCache
Page cache 是操作系統(tǒng)內(nèi)核用來緩存文件系統(tǒng)數(shù)據(jù)的一種機制。它通過將文件數(shù)據(jù)緩存到內(nèi)存中,從而減少磁盤 I/O 操作,提高文件讀取的性能。當應用程序讀取文件時,內(nèi)核會首先檢查 page cache,如果數(shù)據(jù)已經(jīng)在緩存中,則直接從內(nèi)存中讀取,避免了磁盤訪問。
以下是一個C語言小程序來演示如何通過讀寫文件來產(chǎn)生PageCache, 這個程序?qū)?00MiB數(shù)據(jù)到指定的文本文件中。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#define BUFFER_SIZE 4096
#define FILE_SIZE_MB 100
void generate_page_cache(const char *filename) {
int fd;
char buffer[BUFFER_SIZE];
ssize_t bytes_written, bytes_read;
size_t total_bytes_written = 0;
// 初始化緩沖區(qū)
for (int i = 0; i < BUFFER_SIZE; i++) {
buffer[i] = 'A' + (i % 26); // 填充緩沖區(qū)以生成一些數(shù)據(jù)
}
// 打開文件進行寫操作
fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 寫入文件,直到文件大小達到 FILE_SIZE_MB
while (total_bytes_written < FILE_SIZE_MB * 1024 * 1024) {
bytes_written = write(fd, buffer, BUFFER_SIZE);
if (bytes_written == -1) {
perror("write");
close(fd);
exit(EXIT_FAILURE);
}
total_bytes_written += bytes_written;
}
// 關(guān)閉文件
close(fd);
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
exit(EXIT_FAILURE);
}
generate_page_cache(argv[1]);
printf("Page cache generated for file: %s\n", argv[1]);
return 0;
}
在執(zhí)行這個程序前,做一次drop cache操作,用來清理系統(tǒng)已有的pagecache:
# sync && echo 3 > /proc/sys/vm/drop_caches
然后記錄此時系統(tǒng)pagecache的信息。
# free -m
total used free shared buff/cache available
Mem: 32096 2470 29742 872 1152 29626
Swap: 0 0 0
# cat /proc/meminfo
...
Buffers: 4760 kB
Cached: 1096448 kB
SwapCached: 0 kB
Active: 766032 kB
Inactive: 1263964 kB
Active(anon): 590144 kB
Inactive(anon): 1231776 kB
Active(file): 175888 kB
Inactive(file): 32188 kB
編譯運行小程序,再次查看系統(tǒng) pagecache信息。
# ./a.out cache.txt
Page cache generated for file: cache.txt
# free -m
total used free shared buff/cache available
Mem: 32096 2469 29640 872 1256 29627
Swap: 0 0 0
# cat /proc/meminfo
Buffers: 5116 kB
Cached: 1199444 kB
SwapCached: 0 kB
Active: 766652 kB
Inactive: 1366800 kB
Active(anon): 590216 kB
Inactive(anon): 1231776 kB
Active(file): 176436 kB
Inactive(file): 135024 kB
觀察發(fā)現(xiàn) /proc/meminfo中的Cached增加了102996KiB,約100.5MiB;free -m中buff/cache輸出增長了104MiB,兩者都約等于我們寫入的文件大小, 之所以略有不同,是因為系統(tǒng)還有其他進程也在運行影響pagecache。
2. Active File和 Inactive File
仔細觀察剛才/proc/meminfo的內(nèi)容可以發(fā)現(xiàn), 增加的100MiB pagecache全部體現(xiàn)在Inactive(File)這一項, Active(File) 基本沒有變化。
事實上,第一次讀寫文件產(chǎn)生的pagecache,都是Inactive的,只有當它再次被讀寫后,才會被對應的page放在Active LRU鏈表里。Linux使用了2個LRU鏈表來分別管理Active 和Inactive pagecache,當系統(tǒng)內(nèi)存不足時,處于Inactive LRU上的pagecache會優(yōu)先被回收釋放,有很多情況下文件內(nèi)容往往只被讀一次,比如日志文件,它們占用的pagecache需要首先被回收掉。
下面我們再測試一個小程序,創(chuàng)建一個文件并寫入100MiB數(shù)據(jù),然后連續(xù)兩次讀文件,觀察/proc/meminfo前后變化。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#define FILE_SIZE (100 * 1024 * 1024) // 100 MiB
void read_file(const char *filename) {
int fd = open(filename, O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
char *buffer = malloc(FILE_SIZE);
if (buffer == NULL) {
perror("malloc");
close(fd);
exit(EXIT_FAILURE);
}
ssize_t bytes_read = read(fd, buffer, FILE_SIZE);
if (bytes_read == -1) {
perror("read");
free(buffer);
close(fd);
exit(EXIT_FAILURE);
}
printf("Read %zd bytes from file\n", bytes_read);
free(buffer);
close(fd);
}
int main() {
const char *filename = "testfile";
// 創(chuàng)建一個測試文件
int fd = open(filename, O_RDWR | O_CREAT | O_TRUNC, 0600);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
if (ftruncate(fd, FILE_SIZE) == -1) {
perror("ftruncate");
close(fd);
exit(EXIT_FAILURE);
}
char *file_mem = mmap(NULL, FILE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (file_mem == MAP_FAILED) {
perror("mmap");
close(fd);
exit(EXIT_FAILURE);
}
memset(file_mem, 'A', FILE_SIZE); // 初始化文件內(nèi)容
munmap(file_mem, FILE_SIZE);
close(fd);
// 第一次讀取文件內(nèi)容
read_file(filename);
// 第二次讀取文件內(nèi)容
read_file(filename);
return 0;
}
測試前進行dropcache并記錄數(shù)據(jù)。
# cat /proc/meminfo
Buffers: 4000 kB
Cached: 1108280 kB
SwapCached: 0 kB
Active: 778248 kB
Inactive: 1274056 kB
Active(anon): 599416 kB
Inactive(anon): 1241900 kB
Active(file): 178832 kB
Inactive(file): 32156 kB
完成測試,再次記錄數(shù)據(jù)。
# ./a.out
Read 104857600 bytes from file
Read 104857600 bytes from file
# cat /proc/meminfo
Buffers: 6340 kB
Cached: 1215868 kB
SwapCached: 0 kB
Active: 884284 kB
Inactive: 1277620 kB
Active(anon): 599088 kB
Inactive(anon): 1241900 kB
Active(file): 285196 kB
Inactive(file): 35720 kB
這時發(fā)現(xiàn),Active(File)增長了103MiB,說明第二次讀文件后,對應的pagecache被移動到Active LRU中。
3. 容器中的pagecache
追溯cadvisor的源碼可以發(fā)現(xiàn),container_memory_cache 來自memcg中memory.stat里的cache字段。再追溯linux源碼,可以發(fā)現(xiàn)cache的取值源自memcg中的MEMCG_CACHE統(tǒng)計字段。注意memcg中的MEMCG_CACHE不僅包含了前面提到的ActiveFile和InactiveFile pagecache,它還包括了前面1.1中提到的共享內(nèi)存。
將2.2中的程序稍作修改令其常駐不退出,然后制作成容器鏡像,部署在TKEx平臺中,觀察內(nèi)容監(jiān)控數(shù)據(jù)如下。
可以發(fā)現(xiàn)接近pagecache占了接近100MiB,而rss使用量非常少。必須認識到,pagecache也屬于容器內(nèi)存使用量。
開發(fā)者可能很少感知自身程序pagecache的使用情況,容器平臺會對程序的內(nèi)存使用做限制,那么是否需要擔心pagecache的上漲導致程序內(nèi)存使用量超過容器內(nèi)存限制,導致程序被OOM Kill?
實驗探索這個問題。在一個1GiB Memory Limit容器中,已經(jīng)通過malloc/memset使用了0.8GiB的rss內(nèi)存,然后通過讀100MiB磁盤文件產(chǎn)生100MiB左右的pagecache,此時容器內(nèi)存使用量大約為0.9GiB,距離1GiB的限制量還差100MiB。
這時候程序還能malloc/memset 150Mi內(nèi)存嗎? 程序是否會因為超過memcg limit而被Kill?
編寫如下程序然后制作容器鏡像,部署到TKEx平臺,將容器內(nèi)存限制設置為1GiB。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#define ONE_GIB (1024 * 1024 * 1024)
#define EIGHT_TENTHS_GIB (0.8 * ONE_GIB)
#define ONE_HUNDRED_MIB (100 * 1024 * 1024)
#define ONE_FIFTY_MIB (150 * 1024 * 1024)
#define FILE_PATH "/root/test.txt"
void allocate_memory(size_t size) {
char *buffer = (char *)malloc(size);
if (buffer == NULL) {
perror("malloc");
exit(EXIT_FAILURE);
}
memset(buffer, 0, size);
}
void create_file(const char *filename, size_t size) {
int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
char *buffer = (char *)malloc(size);
if (buffer == NULL) {
perror("malloc");
close(fd);
exit(EXIT_FAILURE);
}
// Fill the buffer with random data
for (size_t i = 0; i < size; i++) {
buffer[i] = rand() % 256;
}
if (write(fd, buffer, size) != size) {
perror("write");
free(buffer);
close(fd);
exit(EXIT_FAILURE);
}
free(buffer);
close(fd);
}
void read_file(const char *filename, size_t size) {
FILE *file = fopen(filename, "r");
if (file == NULL) {
perror("fopen");
exit(EXIT_FAILURE);
}
char *buffer = (char *)malloc(size);
if (buffer == NULL) {
perror("malloc");
fclose(file);
exit(EXIT_FAILURE);
}
fread(buffer, 1, size, file);
fclose(file);
free(buffer);
}
int main() {
printf("Allocating 0.8 GiB of RSS memory...\n");
allocate_memory(EIGHT_TENTHS_GIB);
printf("Waiting for 3 minutes...\n");
sleep(180);
printf("Creating a 100 MiB file with random data...\n");
create_file(FILE_PATH, ONE_HUNDRED_MIB);
printf("Waiting for 3 minutes...\n");
sleep(180);
printf("Reading 100 MiB from the file to generate pagecache...\n");
read_file(FILE_PATH, ONE_HUNDRED_MIB);
printf("Waiting for 3 minutes...\n");
sleep(180);
printf("Trying to allocate 150 MiB of memory...\n");
allocate_memory(ONE_FIFTY_MIB);
printf("Successfully allocated 150 MiB of memory.\n");
sleep(3600);
return 0;
}
運行發(fā)現(xiàn)最后這150MiB內(nèi)存是可以分配使用的,程序并沒有被Kill。
這是申請150MiB內(nèi)存前,容器的內(nèi)存使用監(jiān)控記錄:
這是申請150MiB內(nèi)存后,容器的內(nèi)存使用監(jiān)控記錄。
發(fā)現(xiàn)rss確實增長了150MiB,pagecache少了45MiB,總內(nèi)存達到1023MiB, 并沒有超過1GiB的限制。原因是在memset進入缺頁中斷分配物理頁時,系統(tǒng)發(fā)現(xiàn)內(nèi)存使用量會超過memcg limit的情況下,會先嘗試回收pagecache以滿足分配需求, 優(yōu)先回收前面提到的Inactive File。由此可知,進程的rss不超過memcg limit的前提下, 可以放心申請使用內(nèi)存,系統(tǒng)會及時釋放pagecache來滿足需求。pagecache屬于內(nèi)核,不屬于用戶,當用戶需要內(nèi)存時,內(nèi)核會通過回收pagecache來歸還內(nèi)存,但這可能是有代價的。
代價是什么?
- pagecache用于提升磁盤文件讀寫性能,pagecache被回收意味著程序IO性能下降,延遲增加。因此生產(chǎn)環(huán)境一般嚴禁dropcache操作。
- 缺頁中斷進入更復雜的流程,page申請變慢, 直接阻塞用戶進程,造成應用程序性能下降。
頻繁進行文件讀寫的容器經(jīng)常會遇到內(nèi)存使用率一直接近99%的情況,就是由于linux為了提升文件讀寫性能,在memcg的限制內(nèi),盡可能地分配更多的pagecache。
階段總結(jié)4
容器中的cache占用統(tǒng)計既包含了讀寫文件產(chǎn)生的pagecache,也包括了使用共享內(nèi)存的大小。
容器環(huán)境下, 內(nèi)存使用量接近memcg限制時候,繼續(xù)嘗試申請分配內(nèi)存會先觸發(fā)pagecache回收,以滿足分配需求。
四、container_memory_mapped_file
1. mmap文件映射
mmap不僅可以為程序分配匿名頁, 它還是一種內(nèi)存映射文件的方法,允許將文件或設備的內(nèi)容映射到進程的地址空間中。通過 mmap,可以直接訪問甚至修改文件內(nèi)容,就像訪問內(nèi)存一樣,這通常比傳統(tǒng)的文件 I/O 操作更高效。例如以下程序:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#define FILE_PATH "/root/test.txt"
#define FILE_SIZE (100 * 1024 * 1024) // 100 MiB
void create_file(const char *filename, size_t size) {
int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
char *buffer = (char *)malloc(size);
if (buffer == NULL) {
perror("malloc");
close(fd);
exit(EXIT_FAILURE);
}
// Fill the buffer with 'A'
memset(buffer, 'A', size);
if (write(fd, buffer, size) != size) {
perror("write");
free(buffer);
close(fd);
exit(EXIT_FAILURE);
}
free(buffer);
close(fd);
}
int main() {
// Step 1: Create a 100 MiB file with 'A'
printf("Creating a 100 MiB file with 'A'...\n");
create_file(FILE_PATH, FILE_SIZE);
// Step 2: Open the file for reading and writing
int fd = open(FILE_PATH, O_RDWR);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// Step 3: Memory-map the file
char *mapped = (char *)mmap(NULL, FILE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mapped == MAP_FAILED) {
perror("mmap");
close(fd);
exit(EXIT_FAILURE);
}
// Step 4: Modify the file content through the memory-mapped region
printf("Modifying the file content to 'B'...\n");
memset(mapped, 'B', FILE_SIZE);
printf("File content successfully modified to 'B'.\n");
sleep(240);
// Step 5: Clean up
if (munmap(mapped, FILE_SIZE) == -1) {
perror("munmap");
}
close(fd);
return 0;
}
先初始化一個100MiB的文本文件,內(nèi)容全部是字母A; 然后通過mmap將文件映射到程序地址空間里,通過memset將文件內(nèi)容全改成字母B。借助mmap文件映射,使用內(nèi)存操作就能完成文件讀寫。相較于標準buffered io, mmap文件映射會擁有更好的性能,因為它避開了用戶空間和內(nèi)核空間的相互拷貝,這個優(yōu)勢在一次讀寫幾十上百MiB的場景下尤為突出。
將這個程序制作成容器鏡像,部署在TKEx平臺中,觀察內(nèi)存監(jiān)控記錄。
可以發(fā)現(xiàn), mmap, 即container_memory_mmaped_file的監(jiān)控值接近100MiB,而容器的rss依然非常低。觀察/proc/<pid>/status:
...
VmRSS: 103932 kB
...
發(fā)現(xiàn)進程的rss依然約101MiB。因此和前面提到的共享內(nèi)存一樣,mmap文件映射部分的大小屬于進程的rss而不屬于容器的rss。
2. mmap共享內(nèi)存
(1) 共享文件映射
基于4.1的啟發(fā),只要多個進程mmap相同一個文件,就可以通過這個文件實現(xiàn)共享內(nèi)存,完成多進程通信,這種方式叫做共享文件映射。
調(diào)用 mmap 進行文件映射的時候,內(nèi)核首先會在進程的虛擬內(nèi)存空間中創(chuàng)建一個新的虛擬內(nèi)存區(qū)域 VMA 用于映射文件,通過 vm_area_struct->vm_file 將映射文件的 struct flle 結(jié)構(gòu)與虛擬內(nèi)存映射關(guān)聯(lián)起來。
struct vm_area_struct {
struct file * vm_file; /* File we map to (can be NULL). */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE */
}
在缺頁中斷處理過程中,如果vma非匿名(即文件映射),linux首先通過 vm_area_struct->vm_pgoff激活對應的pagecache并預讀部分磁盤文件內(nèi)容到pagecache中,然后在頁表中創(chuàng)建PTE并與pagecache文件頁關(guān)聯(lián),完成缺頁中斷,此后對vma的訪問實質(zhì)上都是對pagecache的訪問。進程1和進程2的共享文件映射,實質(zhì)上是各自vma里的file字段最終指向了相同的文件,即相同的inode。進程1和進程2對各自vma的訪問也實質(zhì)上是對相同的pagecache進行訪問,這就是基于文件映射實現(xiàn)共享內(nèi)存的原理。當然,對vma的內(nèi)容修改也會導致對pagecache的修改,最終通過臟頁回寫完成對磁盤文件的修改,因此這種共享內(nèi)存的方式會產(chǎn)生真實的磁盤IO。
(2) 共享匿名映射
相對于共享文件映射,共享匿名映射也能實現(xiàn)共享內(nèi)存,但只適用于父子進程之間。實現(xiàn)原理相對于共享文件映射略有類似,同樣依賴了pagecache,但這里的文件不再是具體的磁盤文件,而是tmpfs。tmpfs是一個基于內(nèi)存實現(xiàn)的文件系統(tǒng),因此基于tmpfs的共享內(nèi)存不會產(chǎn)生真實的磁盤IO。后面會了解到,基于ipc的共享內(nèi)存,即1.1里通過shmget和shmat實現(xiàn)的共享內(nèi)存,也是依靠tmpfs完成的。
3. 容器中的mapped file
回到cadvisor源碼里,container_memory_mapped_file取值于memcg memory.stat里的mapped_file字段,實際上就是memcg中的NR_FILE_MAPPED字段。所有mmap調(diào)用產(chǎn)生的文件頁,都會被統(tǒng)計到container_memory_mapped_file中。根據(jù)3.2.1的描述,mmap文件映射的原理與pagecache的行為緊密相關(guān), mapped_file也會伴隨著pagecache一起出現(xiàn)。
此外,mapped_file還包括tmpfs的使用量,下面來介紹tmpfs和shmem。
五、tmpfs與shmem
1. emptyDir的問題
emptyDir允許用戶選擇內(nèi)存作為掛載介質(zhì)。
當這么做的時候,會發(fā)現(xiàn)掛載點(下圖的/data)對應的文件系統(tǒng)是tmpfs,這意味著/data里的數(shù)據(jù)實際上都存儲在內(nèi)存中。
# df -h
Filesystem Size Used Available Use% Mounted on
overlay 49.1G 2.7G 46.4G 5% /
tmpfs 8.0G 0 8.0G 0% /data
如果沒有為emptyDir卷設置sizeLimit,/data目錄下的文件將占用Pod的內(nèi)存;如果Pod沒有設置內(nèi)存limit,則/data可能消耗掉Node上全部的內(nèi)存。
日常排障中經(jīng)常收到客戶的工單疑惑,進程似乎沒有內(nèi)存泄漏的情況,但內(nèi)存使用量一直在上漲。通過面板發(fā)現(xiàn)pagecache一路上漲,最后發(fā)現(xiàn)掛載在tmpfs的/data/目錄一直在輸出程序log。 因此,請注意不要將emptyDir以內(nèi)存為介質(zhì)掛載后,將其作為輸出日志目錄。
2. System V IPC 共享內(nèi)存
公司內(nèi)部存在大量的IPC共享內(nèi)存的使用場景,比如spp服務端框架。例如以下C語言程序例子:
(1) Writer
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#define SHM_SIZE 36 * 1024 * 1024 // 36 MiB
int main() {
key_t key = ftok("shmfile", 65); // 生成一個唯一的key
int shmid = shmget(key, SHM_SIZE, 0666 | IPC_CREAT); // 創(chuàng)建共享內(nèi)存段
if (shmid == -1) {
perror("shmget failed");
exit(1);
}
char *data = (char *)shmat(shmid, (void *)0, 0); // 連接到共享內(nèi)存段
if (data == (char *)(-1)) {
perror("shmat failed");
exit(1);
}
// 寫入數(shù)據(jù)到共享內(nèi)存
strcpy(data, "Hello, this is a message from the writer process!");
printf("Data written to shared memory: %s\n", data);
sleep(3600);
// 斷開連接
if (shmdt(data) == -1) {
perror("shmdt failed");
exit(1);
}
return 0;
}
(2) Reader
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#define SHM_SIZE 36 * 1024 * 1024 // 36 MiB
int main() {
key_t key = ftok("shmfile", 65); // 生成一個唯一的key
int shmid = shmget(key, SHM_SIZE, 0666); // 獲取共享內(nèi)存段
if (shmid == -1) {
perror("shmget failed");
exit(1);
}
char *data = (char *)shmat(shmid, (void *)0, 0); // 連接到共享內(nèi)存段
if (data == (char *)(-1)) {
perror("shmat failed");
exit(1);
}
// 讀取共享內(nèi)存中的數(shù)據(jù)
printf("Data read from shared memory: %s\n", data);
sleep(3600);
// 斷開連接
if (shmdt(data) == -1) {
perror("shmdt failed");
exit(1);
}
// 刪除共享內(nèi)存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl failed");
exit(1);
}
return 0;
}
分辨編譯執(zhí)行5.2.1和5.2.2,會發(fā)現(xiàn)5.2.2能讀取到來自5.2.1的 Hello, this is a message from the writer process!。
同時執(zhí)行 ipcs -m可以看到我們分配到的36MiB共享內(nèi)存。
# ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
...
0xffffffff 7 root 666 37748736 2
這時需要注意的是,當Writer和Reader進程都退出后,這部分內(nèi)存依然在機器的tmpfs中,必須通過ipcrm命令來顯示刪除釋放。
來到容器環(huán)境中,某個容器退出后,原進程中共享內(nèi)存中的數(shù)據(jù)同樣不會消失。如果剩余的容器沒有使用該共享內(nèi)存,這部分內(nèi)存用量則只計入Pod Level Memcg的使用量。
如果你發(fā)現(xiàn)Pod的內(nèi)存使用量明顯大于所有容器內(nèi)存使用量之和,可以通過ipcs查看是否存在Shmem數(shù)據(jù)。
六、監(jiān)控實踐
1. 程序自監(jiān)控內(nèi)存用量的小技巧
linux提供了一個系統(tǒng)調(diào)用getrusage(2)用于獲取進程自身以及其子進程的資源使用情況,在1.1中我們已經(jīng)初步接觸過了,再提供一個go語言的調(diào)用示例。
package main
import (
"fmt"
"syscall"
"time"
)
func main() {
// 調(diào)用 getrusage 系統(tǒng)調(diào)用
var usage syscall.Rusage
err := syscall.Getrusage(syscall.RUSAGE_SELF, &usage)
if err != nil {
fmt.Printf("Error getting resource usage: %v\n", err)
return
}
// 打印資源使用情況
fmt.Printf("User CPU time used: %+v \n", usage.Utime)
fmt.Printf("System CPU time used: %+v \n", usage.Stime)
fmt.Printf("Maximum resident set size: %v \n", usage.Maxrss)
fmt.Printf("Integral shared memory size: %v \n", usage.Ixrss)
fmt.Printf("Integral unshared data size: %v \n", usage.Idrss)
fmt.Printf("Integral unshared stack size: %v \n", usage.Isrss)
fmt.Printf("Page reclaims (soft page faults): %v\n", usage.Minflt)
fmt.Printf("Page faults (hard page faults): %v\n", usage.Majflt)
fmt.Printf("Swaps: %v\n", usage.Nswap)
fmt.Printf("Block input operations: %v\n", usage.Inblock)
fmt.Printf("Block output operations: %v\n", usage.Oublock)
fmt.Printf("IPC messages sent: %v\n", usage.Msgsnd)
fmt.Printf("IPC messages received: %v\n", usage.Msgrcv)
fmt.Printf("Signals received: %v\n", usage.Nsignals)
fmt.Printf("Voluntary context switches: %v\n", usage.Nvcsw)
fmt.Printf("Involuntary context switches: %v\n", usage.Nivcsw)
// 模擬一些 CPU 負載
for i := 0; i < 1e8; i++ {
_ = i * i
}
time.Sleep(2 * time.Second)
// 再次調(diào)用 getrusage 系統(tǒng)調(diào)用
err = syscall.Getrusage(syscall.RUSAGE_SELF, &usage)
if err != nil {
fmt.Printf("Error getting resource usage: %v\n", err)
return
}
// 打印資源使用情況
fmt.Printf("\nAfter sleep:\n")
fmt.Printf("User CPU time used: %+v \n", usage.Utime)
fmt.Printf("System CPU time used: %+v \n", usage.Stime)
fmt.Printf("Maximum resident set size: %v \n", usage.Maxrss)
fmt.Printf("Integral shared memory size: %v \n", usage.Ixrss)
fmt.Printf("Integral unshared data size: %v \n", usage.Idrss)
fmt.Printf("Integral unshared stack size: %v \n", usage.Isrss)
fmt.Printf("Page reclaims (soft page faults): %v\n", usage.Minflt)
fmt.Printf("Page faults (hard page faults): %v\n", usage.Majflt)
fmt.Printf("Swaps: %v\n", usage.Nswap)
fmt.Printf("Block input operations: %v\n", usage.Inblock)
fmt.Printf("Block output operations: %v\n", usage.Oublock)
fmt.Printf("IPC messages sent: %v\n", usage.Msgsnd)
fmt.Printf("IPC messages received: %v\n", usage.Msgrcv)
fmt.Printf("Signals received: %v\n", usage.Nsignals)
fmt.Printf("Voluntary context switches: %v\n", usage.Nvcsw)
fmt.Printf("Involuntary context switches: %v\n", usage.Nivcsw)
}
可見,getrusage(2) 還能幫助開發(fā)者自監(jiān)控CPU使用率。
2. Top和Pid Namespace
在容器內(nèi)執(zhí)行top查看到的cpu和memory使用率通常并不是容器的真實使用率,因為/proc/stat和/proc/meminfo的視野是整個機器而非Pod或者容器。詳情見以下。
Node類型 | CVM Node | TKE Serverless Node |
CPU/內(nèi)存使用率范圍 | Node全部,包括其他Pod | 包括自身容器和虛機其他進程,如洋蔥安全,eklet-agent等 |
如果你的容器部署在TKE Serverless節(jié)點中,TKEx和TKE AppFabric也提供了Pod所在虛機的基礎監(jiān)控,如下圖所示。
虛機的監(jiān)控數(shù)據(jù)與Top的輸出吻合。
如果你在容器內(nèi)使用top觀察進程的監(jiān)控數(shù)據(jù),需要明確的是Pod內(nèi)不同容器的Pid Namespace默認是不共享的,你無法觀察另一個容器的進程數(shù)據(jù)。
開啟Pid Namespace共享可以獲得更多的觀測手段,比如使用帶有dlv, gdb等調(diào)試工具的sidecar容器來調(diào)試主容器進程。但需要開啟對應的特權(quán),比如ptrace,以及不能使用Systemd拉起富容器的模式部署業(yè)務。
3. 我的容器內(nèi)存使用率超過了100%
我好像白薅了平臺的內(nèi)存,這是怎么回事?
如上圖所示,內(nèi)存使用量已經(jīng)大幅度超過了容器本身的內(nèi)存限制量,按照常識,容器會被OOM Kill。然而現(xiàn)網(wǎng)中存在一些明顯超過內(nèi)存限制量卻依然在正常運行的容器。
前文說過,K8s為容器設置了Pod和Container級的memcg內(nèi)存限制,任何一個容器內(nèi)存使用量突破了Container層級的限制,會觸發(fā)OOM Kill; 所有容器內(nèi)存使用和突破了Pod層級的限制,也會觸發(fā)OOM Kill。出現(xiàn)超限使用意味著這兩道限制都已經(jīng)失效。
排查發(fā)現(xiàn),這類超限運行Pod普遍存在2個特征:
- 存在一個用Systemd拉起的富容器,Systemd版本早于236;
- 存在一個未配置Limit的sidecar容器。
兩個特征同時滿足的時候,K8s設置的兩層限制都會失效。如果容器開啟特權(quán)并且/sys/fs/cgroup被掛載,Systemd會覆蓋K8s為容器設置的cgroup limit;任意一個未配置Limit的容器會使得Pod的QOS降級到Burstable甚至BestEffort, Pod層級的內(nèi)存限制變成無窮大。
超限使用內(nèi)存會導致Node的內(nèi)存被占用,滋生穩(wěn)定性風險。建議使用較新的ubuntu/centos/tlinux基礎鏡像,搭載較新版本的Systemd拉起業(yè)務容器,避免超限使用內(nèi)存。
4. 我擔心OOM Kill,配置哪個指標做內(nèi)存使用告警?
通?;赾ontainer_memory_working_set_bytes做內(nèi)存使用告警,內(nèi)存使用率的計算公式為:
100 * container_memory_working_set_bytes{container="$container", pod="$pod", namespace="$namespace"}
/ kube_pod_container_resource_limits{resource="memory", container="$container", pod="$pod", namespace="$namespace"} %
container_memory_working_set_bytes在memcg的全部使用量的基礎上,減去了Inactive File部分, 認為這部分pagecache可以迅速回收而不會給業(yè)務進程造成顯著的負載壓力,可以不計入容器的內(nèi)存使用量。如下是cadvisor的統(tǒng)計代碼細節(jié)。
workingSet := ret.Memory.Usage
if v, ok := s.MemoryStats.Stats[inactiveFileKeyName]; ok {
ret.Memory.TotalInactiveFile = v
if workingSet < v {
workingSet = 0
} else {
workingSet -= v
}
}
ret.Memory.WorkingSet = workingSet
七、結(jié)尾
一路過來,我們了解缺頁中斷的概念,RSS的統(tǒng)計,認識了Linux Memcg內(nèi)存控制組,觀察了pagecache的分配和回收,初識了tmpfs,以及在容器中使用共享內(nèi)存等等。讀到這里,文章開頭提到的幾個問題應該有了清晰的答案。祝大家的程序穩(wěn)如泰山,永不OOM。