精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

K8s 里我的容器到底用了多少內存?

系統 Linux
本文我們將了解缺頁中斷的概念,RSS的統計,認識了Linux Memcg內存控制組,觀察了pagecache的分配和回收,初識了tmpfs,以及在容器中使用共享內存等等。

作者 | frostchen

導語  

Linux下開發者習慣在物理機或者虛擬機環境下使用top和free等命令查看機器和進程的內存使用量,近年來越來越多的應用服務完成了微服務容器化改造,過去查看、監控和定位內存使用量的方法似乎時常不太奏效。如果你的應用程序剛剛遷移到K8s中,經常被諸如以下問題所困擾:容器的內存使用率為啥總是接近99%?malloc/free配對沒問題,內存使用量卻一直上漲?內存使用量超過了限制量卻沒有被OOM Kill? 登錄容器執行top,free看到的輸出和平臺監控視圖完全對不上?... 本文假設讀者熟悉Linux環境,擁有常見后端開發語言(C/C++ /Go/Java等)使用經驗,希望后面的內容能在讀者面臨此類疑惑時提供一些有效思路。

K8s中監控數據主要來源是 cadvisor, 容器內存使用量的相關指標有以下:

這些指標究竟是什么含義?在不同的應用場景下需要重點關注哪些指標?讓我們從回顧linux進程地址空間開始,逐步挖掘容器內存使用奧秘。

一、進程是怎么分配內存的?

回憶一下linux進程虛擬地址空間分布圖。

+-------------------------------+ 0xFFFFFFFFFFFFFFFF (64-bit)
|       Kernel Space            |  內核空間,用于操作系統內核和內核模塊
+-------------------------------+ 0x00007FFFFFFFFFFF
|       User Space              |  用戶空間進程的虛擬地址空間
|       Shared Libraries        |  動態加載的共享庫 (.so 文件)
+-------------------------------+ 0x00007FFFC0000000
|       Heap                    |  動態內存分配區域 (malloc, calloc, realloc)
|       (malloc, etc.)          |  堆的大小可以動態增長或收縮
+-------------------------------+ 0x00007FFFB0000000
|       BSS Segment             |  未初始化的全局變量和靜態變量
|       (Uninitialized Data)    |  在程序啟動時被初始化為零
+-------------------------------+ 0x00007FFFA0000000
|       Data Segment            |  已初始化的全局變量和靜態變量
|       (Initialized Data)      |  在程序啟動時被初始化為特定的值
+-------------------------------+ 0x00007FFF90000000
|       Text Segment            |  可執行代碼段
|       (Code)                  |  通常是只讀的,以防止代碼被意外修改
+-------------------------------+ 0x00007FFF80000000
|       Stack                   |  用于存儲函數調用的局部變量、參數和返回地址
|                               |  棧通常從高地址向低地址增長
+-------------------------------+ 0x0000000000000000

在linux內核里描述上述圖的結構是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)     | | |
| | +-----------------------+ | |
| +---------------------------+ |+-------------------------------+

可以發現,linux進程地址空間是由一個個vm_area_struct(vma)組成,每個vma都有自己地址區間。如果你的代碼panic或者Segmentation Fault崩潰,最直接的原因就是你引用的指針值不在進程的任意一個vma區間內。你可以通過 /proc/<pid>/maps 來觀察進程的vma分布。

1. malloc分配內存

malloc函數增大了進程虛擬地址空間的heap容量,擴大了mm描述符中vma的start和end長度,或者插入了新的vma;但是它剛完成調用后,并沒有增大進程的實際內存使用量。

以下是個代碼示例證明上述言論。

#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

階段總結1

當memset 128MB長度的數據完成后,我們立刻觀察到進程發生了32768次minor pagefault, 同時RSS內存占用提升到129MB。注意 32768 * 4096正好等于128MB,而4096正好是linux page默認大小。可以在程序sleep的時段用top觀察監控統計進一步證實結論。

進一步說,malloc申請到的地址,在得到真實的使用之前,必須經歷缺頁中斷,完成建立虛擬地址到物理地址的映射。完成物理頁分配的虛擬地址空間才會被計算到內存使用量中。

二、container_memory_rss

1.. 進程的RSS

進程的RSS(Resident Set Size)是當前使用的實際物理內存大小,包括代碼段、堆、棧和共享庫等所使用的內存, 實際上就是頁表中物理頁部分的全部大小。

更精確地說,根據內核的 get_mm_rss, RSS由FilePages, AnnoPages和ShmemPages組成。

以下是一個例子,分別展示了這三種內存的申請和使用方式,FilePages, 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); // 使用內存

    printf("Allocated %d MiB of file-mapped memory\n", FILE_SIZE / (1024 * 1024));

    // 保持映射,直到程序結束
    // 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); // 使用內存

    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); // 使用內存
    printf("Allocated %d MiB of shared memory\n", SHM_SIZE / (1024 * 1024));

    // 保持映射,直到程序結束
    // 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發現,進程的RSS 是24184KiB, 比我們申請的22MiB,也就是22528KiB, 要大1656KiB。

進一步觀察/proc/$pid/status,發現:

....
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,因為它還包括了共享庫。內核mm_struct計數并不總是完全及時和精準的。

階段總結2

進程RSS組成

描述

匿名頁

通常來源于 malloc,進入 brk 或者 mmap 匿名映射

共享內存

來自 shmget 系列調用

mmap 文件映射

通過 mmap 調用映射文件到進程地址空間

棧 (stack)

進程的調用棧

二進制文件

加載進程本身的二進制文件占用的內存

動態鏈接庫

加載的動態鏈接庫(共享庫)占用的內存

頁表

內核中存儲頁表的部分

2. 容器(memcg)的RSS

K8s容器環境下,容器里的進程都歸屬同一個cgroup控制組,本文只關注內存控制組(memcg)。把剛才的代碼做成容器鏡像,部署在TKEx環境里, 觀察容器內存使用相關指標。

觀察到container_memory_rss只有2047 * 4096 Bytes, 略小于8MiB, 遠遠低于上一節top觀察到的24MiB,這是為什么?

1.1中通過觀察/proc/$pid/status和top的輸出,我們得出了進程的RSS估算方法,即:

  • 占主要部分的 malloc導致的匿名頁(brk/mmap匿名映射) + 使用shmem共享內存 + mmap文件映射;
  • stack部分,text部分和動態鏈接庫部分,頁表部分,通常占比很小。

那memory cgroup的RSS的計算方法是不是就是簡單地把memcg下歸屬的所有的進程RSS簡單求和呢?顯然不是。通過追溯cadvisor相關代碼, 發現這個數值來來自容器所屬cgroup path下的memory.stat文本中的rss字段。

(1) 如何找到容器對應的memcg path?

每個容器的 Memory Cgroup 路徑根據其 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后,可以發現目錄下有很多記錄文件,這里關注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)的相關源碼,memcg統計了以下內存使用:

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的記錄情況,發現只有匿名頁的數量被統計到MEMCG_RSS里,這和前面觀察的進程的RSS不一樣。共享內存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文件映射的部分。這樣得出的結論是,memory cgroup的RSS只統計了上述代碼中malloc分配出的內存,不包含另外兩部分。

階段總結3

類別

進程的 RSS

容器的 RSS

brk 分配

?

?

mmap 匿名映射

?

?

共享內存

?


mmap 文件映射

?


棧 (stack)

?

?

二進制文件

?

?

動態鏈接庫

?

?

頁表

?

?

三、container_memory_cache

1. 初識PageCache

Page cache 是操作系統內核用來緩存文件系統數據的一種機制。它通過將文件數據緩存到內存中,從而減少磁盤 I/O 操作,提高文件讀取的性能。當應用程序讀取文件時,內核會首先檢查 page cache,如果數據已經在緩存中,則直接從內存中讀取,避免了磁盤訪問。

以下是一個C語言小程序來演示如何通過讀寫文件來產生PageCache, 這個程序寫100MiB數據到指定的文本文件中。

#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;

    // 初始化緩沖區
    for (int i = 0; i < BUFFER_SIZE; i++) {
        buffer[i] = 'A' + (i % 26); // 填充緩沖區以生成一些數據
    }

    // 打開文件進行寫操作
    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;
    }

    // 關閉文件
    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;
}

在執行這個程序前,做一次drop cache操作,用來清理系統已有的pagecache:

 # sync && echo 3 > /proc/sys/vm/drop_caches

然后記錄此時系統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

編譯運行小程序,再次查看系統 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

觀察發現 /proc/meminfo中的Cached增加了102996KiB,約100.5MiB;free -m中buff/cache輸出增長了104MiB,兩者都約等于我們寫入的文件大小, 之所以略有不同,是因為系統還有其他進程也在運行影響pagecache。

2. Active File和 Inactive File

仔細觀察剛才/proc/meminfo的內容可以發現, 增加的100MiB pagecache全部體現在Inactive(File)這一項, Active(File) 基本沒有變化。

事實上,第一次讀寫文件產生的pagecache,都是Inactive的,只有當它再次被讀寫后,才會被對應的page放在Active LRU鏈表里。Linux使用了2個LRU鏈表來分別管理Active 和Inactive pagecache,當系統內存不足時,處于Inactive LRU上的pagecache會優先被回收釋放,有很多情況下文件內容往往只被讀一次,比如日志文件,它們占用的pagecache需要首先被回收掉。

下面我們再測試一個小程序,創建一個文件并寫入100MiB數據,然后連續兩次讀文件,觀察/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";

    // 創建一個測試文件
    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); // 初始化文件內容
    munmap(file_mem, FILE_SIZE);
    close(fd);

    // 第一次讀取文件內容
    read_file(filename);

    // 第二次讀取文件內容
    read_file(filename);

    return 0;
}

測試前進行dropcache并記錄數據。

# 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

完成測試,再次記錄數據。

# ./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

這時發現,Active(File)增長了103MiB,說明第二次讀文件后,對應的pagecache被移動到Active LRU中。

3. 容器中的pagecache

追溯cadvisor的源碼可以發現,container_memory_cache 來自memcg中memory.stat里的cache字段。再追溯linux源碼,可以發現cache的取值源自memcg中的MEMCG_CACHE統計字段。注意memcg中的MEMCG_CACHE不僅包含了前面提到的ActiveFile和InactiveFile pagecache,它還包括了前面1.1中提到的共享內存。

將2.2中的程序稍作修改令其常駐不退出,然后制作成容器鏡像,部署在TKEx平臺中,觀察內容監控數據如下。

可以發現接近pagecache占了接近100MiB,而rss使用量非常少。必須認識到,pagecache也屬于容器內存使用量。

開發者可能很少感知自身程序pagecache的使用情況,容器平臺會對程序的內存使用做限制,那么是否需要擔心pagecache的上漲導致程序內存使用量超過容器內存限制,導致程序被OOM Kill?

實驗探索這個問題。在一個1GiB Memory Limit容器中,已經通過malloc/memset使用了0.8GiB的rss內存,然后通過讀100MiB磁盤文件產生100MiB左右的pagecache,此時容器內存使用量大約為0.9GiB,距離1GiB的限制量還差100MiB。

這時候程序還能malloc/memset 150Mi內存嗎? 程序是否會因為超過memcg limit而被Kill?

編寫如下程序然后制作容器鏡像,部署到TKEx平臺,將容器內存限制設置為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;
}

運行發現最后這150MiB內存是可以分配使用的,程序并沒有被Kill。

這是申請150MiB內存前,容器的內存使用監控記錄:

這是申請150MiB內存后,容器的內存使用監控記錄。

發現rss確實增長了150MiB,pagecache少了45MiB,總內存達到1023MiB, 并沒有超過1GiB的限制。原因是在memset進入缺頁中斷分配物理頁時,系統發現內存使用量會超過memcg limit的情況下,會先嘗試回收pagecache以滿足分配需求, 優先回收前面提到的Inactive File。由此可知,進程的rss不超過memcg limit的前提下, 可以放心申請使用內存,系統會及時釋放pagecache來滿足需求。pagecache屬于內核,不屬于用戶,當用戶需要內存時,內核會通過回收pagecache來歸還內存,但這可能是有代價的。

代價是什么?

  • pagecache用于提升磁盤文件讀寫性能,pagecache被回收意味著程序IO性能下降,延遲增加。因此生產環境一般嚴禁dropcache操作。
  • 缺頁中斷進入更復雜的流程,page申請變慢, 直接阻塞用戶進程,造成應用程序性能下降。

頻繁進行文件讀寫的容器經常會遇到內存使用率一直接近99%的情況,就是由于linux為了提升文件讀寫性能,在memcg的限制內,盡可能地分配更多的pagecache。

階段總結4

容器中的cache占用統計既包含了讀寫文件產生的pagecache,也包括了使用共享內存的大小。

容器環境下, 內存使用量接近memcg限制時候,繼續嘗試申請分配內存會先觸發pagecache回收,以滿足分配需求。

四、container_memory_mapped_file

1. mmap文件映射

mmap不僅可以為程序分配匿名頁, 它還是一種內存映射文件的方法,允許將文件或設備的內容映射到進程的地址空間中。通過 mmap,可以直接訪問甚至修改文件內容,就像訪問內存一樣,這通常比傳統的文件 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的文本文件,內容全部是字母A; 然后通過mmap將文件映射到程序地址空間里,通過memset將文件內容全改成字母B。借助mmap文件映射,使用內存操作就能完成文件讀寫。相較于標準buffered io, mmap文件映射會擁有更好的性能,因為它避開了用戶空間和內核空間的相互拷貝,這個優勢在一次讀寫幾十上百MiB的場景下尤為突出。

將這個程序制作成容器鏡像,部署在TKEx平臺中,觀察內存監控記錄。

可以發現, mmap, 即container_memory_mmaped_file的監控值接近100MiB,而容器的rss依然非常低。觀察/proc/<pid>/status:

...
VmRSS:    103932 kB
...

發現進程的rss依然約101MiB。因此和前面提到的共享內存一樣,mmap文件映射部分的大小屬于進程的rss而不屬于容器的rss。

2. mmap共享內存

(1) 共享文件映射

基于4.1的啟發,只要多個進程mmap相同一個文件,就可以通過這個文件實現共享內存,完成多進程通信,這種方式叫做共享文件映射。

調用 mmap 進行文件映射的時候,內核首先會在進程的虛擬內存空間中創建一個新的虛擬內存區域 VMA 用于映射文件,通過 vm_area_struct->vm_file 將映射文件的 struct flle 結構與虛擬內存映射關聯起來。

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并預讀部分磁盤文件內容到pagecache中,然后在頁表中創建PTE并與pagecache文件頁關聯,完成缺頁中斷,此后對vma的訪問實質上都是對pagecache的訪問。進程1和進程2的共享文件映射,實質上是各自vma里的file字段最終指向了相同的文件,即相同的inode。進程1和進程2對各自vma的訪問也實質上是對相同的pagecache進行訪問,這就是基于文件映射實現共享內存的原理。當然,對vma的內容修改也會導致對pagecache的修改,最終通過臟頁回寫完成對磁盤文件的修改,因此這種共享內存的方式會產生真實的磁盤IO。

(2) 共享匿名映射

相對于共享文件映射,共享匿名映射也能實現共享內存,但只適用于父子進程之間。實現原理相對于共享文件映射略有類似,同樣依賴了pagecache,但這里的文件不再是具體的磁盤文件,而是tmpfs。tmpfs是一個基于內存實現的文件系統,因此基于tmpfs的共享內存不會產生真實的磁盤IO。后面會了解到,基于ipc的共享內存,即1.1里通過shmget和shmat實現的共享內存,也是依靠tmpfs完成的。

3. 容器中的mapped file

回到cadvisor源碼里,container_memory_mapped_file取值于memcg memory.stat里的mapped_file字段,實際上就是memcg中的NR_FILE_MAPPED字段。所有mmap調用產生的文件頁,都會被統計到container_memory_mapped_file中。根據3.2.1的描述,mmap文件映射的原理與pagecache的行為緊密相關, mapped_file也會伴隨著pagecache一起出現。

此外,mapped_file還包括tmpfs的使用量,下面來介紹tmpfs和shmem。

五、tmpfs與shmem

1. emptyDir的問題

emptyDir允許用戶選擇內存作為掛載介質。

當這么做的時候,會發現掛載點(下圖的/data)對應的文件系統是tmpfs,這意味著/data里的數據實際上都存儲在內存中。

# 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的內存;如果Pod沒有設置內存limit,則/data可能消耗掉Node上全部的內存。

日常排障中經常收到客戶的工單疑惑,進程似乎沒有內存泄漏的情況,但內存使用量一直在上漲。通過面板發現pagecache一路上漲,最后發現掛載在tmpfs的/data/目錄一直在輸出程序log。 因此,請注意不要將emptyDir以內存為介質掛載后,將其作為輸出日志目錄。

2. System V IPC 共享內存

公司內部存在大量的IPC共享內存的使用場景,比如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);  // 創建共享內存段

    if (shmid == -1) {
        perror("shmget failed");
        exit(1);
    }

    char *data = (char *)shmat(shmid, (void *)0, 0);  // 連接到共享內存段

    if (data == (char *)(-1)) {
        perror("shmat failed");
        exit(1);
    }

    // 寫入數據到共享內存
    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);  // 獲取共享內存段

    if (shmid == -1) {
        perror("shmget failed");
        exit(1);
    }

    char *data = (char *)shmat(shmid, (void *)0, 0);  // 連接到共享內存段

    if (data == (char *)(-1)) {
        perror("shmat failed");
        exit(1);
    }

    // 讀取共享內存中的數據
    printf("Data read from shared memory: %s\n", data);

    sleep(3600); 
    // 斷開連接
    if (shmdt(data) == -1) {
        perror("shmdt failed");
        exit(1);
    }

    // 刪除共享內存段
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl failed");
        exit(1);
    }

    return 0;
}

分辨編譯執行5.2.1和5.2.2,會發現5.2.2能讀取到來自5.2.1的 Hello, this is a message from the writer process!。

同時執行 ipcs -m可以看到我們分配到的36MiB共享內存。

# ipcs -m
------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      
...
0xffffffff 7          root       666        37748736   2

這時需要注意的是,當Writer和Reader進程都退出后,這部分內存依然在機器的tmpfs中,必須通過ipcrm命令來顯示刪除釋放。

來到容器環境中,某個容器退出后,原進程中共享內存中的數據同樣不會消失。如果剩余的容器沒有使用該共享內存,這部分內存用量則只計入Pod Level Memcg的使用量。

如果你發現Pod的內存使用量明顯大于所有容器內存使用量之和,可以通過ipcs查看是否存在Shmem數據。

六、監控實踐

1. 程序自監控內存用量的小技巧

linux提供了一個系統調用getrusage(2)用于獲取進程自身以及其子進程的資源使用情況,在1.1中我們已經初步接觸過了,再提供一個go語言的調用示例。

package main

import (
        "fmt"
        "syscall"
        "time"
)

func main() {
        // 調用 getrusage 系統調用
        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)

        // 再次調用 getrusage 系統調用
        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) 還能幫助開發者自監控CPU使用率。

2. Top和Pid Namespace

在容器內執行top查看到的cpu和memory使用率通常并不是容器的真實使用率,因為/proc/stat和/proc/meminfo的視野是整個機器而非Pod或者容器。詳情見以下。

Node類型

CVM Node

TKE Serverless Node

CPU/內存使用率范圍

Node全部,包括其他Pod

包括自身容器和虛機其他進程,如洋蔥安全,eklet-agent等

如果你的容器部署在TKE Serverless節點中,TKEx和TKE AppFabric也提供了Pod所在虛機的基礎監控,如下圖所示。

虛機的監控數據與Top的輸出吻合。

如果你在容器內使用top觀察進程的監控數據,需要明確的是Pod內不同容器的Pid Namespace默認是不共享的,你無法觀察另一個容器的進程數據。

開啟Pid Namespace共享可以獲得更多的觀測手段,比如使用帶有dlv, gdb等調試工具的sidecar容器來調試主容器進程。但需要開啟對應的特權,比如ptrace,以及不能使用Systemd拉起富容器的模式部署業務。

3. 我的容器內存使用率超過了100%

我好像白薅了平臺的內存,這是怎么回事?

如上圖所示,內存使用量已經大幅度超過了容器本身的內存限制量,按照常識,容器會被OOM Kill。然而現網中存在一些明顯超過內存限制量卻依然在正常運行的容器。

前文說過,K8s為容器設置了Pod和Container級的memcg內存限制,任何一個容器內存使用量突破了Container層級的限制,會觸發OOM Kill; 所有容器內存使用和突破了Pod層級的限制,也會觸發OOM Kill。出現超限使用意味著這兩道限制都已經失效。

排查發現,這類超限運行Pod普遍存在2個特征:

  • 存在一個用Systemd拉起的富容器,Systemd版本早于236;
  • 存在一個未配置Limit的sidecar容器。

兩個特征同時滿足的時候,K8s設置的兩層限制都會失效。如果容器開啟特權并且/sys/fs/cgroup被掛載,Systemd會覆蓋K8s為容器設置的cgroup limit;任意一個未配置Limit的容器會使得Pod的QOS降級到Burstable甚至BestEffort, Pod層級的內存限制變成無窮大。

超限使用內存會導致Node的內存被占用,滋生穩定性風險。建議使用較新的ubuntu/centos/tlinux基礎鏡像,搭載較新版本的Systemd拉起業務容器,避免超限使用內存。

4. 我擔心OOM Kill,配置哪個指標做內存使用告警?

通常基于container_memory_working_set_bytes做內存使用告警,內存使用率的計算公式為:

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可以迅速回收而不會給業務進程造成顯著的負載壓力,可以不計入容器的內存使用量。如下是cadvisor的統計代碼細節。

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

七、結尾

一路過來,我們了解缺頁中斷的概念,RSS的統計,認識了Linux Memcg內存控制組,觀察了pagecache的分配和回收,初識了tmpfs,以及在容器中使用共享內存等等。讀到這里,文章開頭提到的幾個問題應該有了清晰的答案。祝大家的程序穩如泰山,永不OOM。

責任編輯:趙寧寧 來源: 騰訊技術工程
相關推薦

2023-12-26 15:05:00

Linux共享內存配置

2023-07-04 07:30:03

容器Pod組件

2020-11-10 07:05:41

DockerK8S云計算

2023-11-06 07:16:22

WasmK8s模塊

2022-04-22 13:32:01

K8s容器引擎架構

2025-02-10 00:20:00

2022-06-01 09:38:36

KubernetesPod容器

2022-05-18 20:01:07

K8sIP 地址云原生

2022-12-28 10:52:34

Etcd備份

2022-01-02 08:42:50

架構部署容器

2024-03-04 08:03:50

k8sClusterNode

2023-09-06 08:12:04

k8s云原生

2024-01-26 14:35:03

鑒權K8sNode

2025-09-19 09:39:26

2021-03-15 23:11:12

內存虛擬化技術

2020-05-12 10:20:39

K8s kubernetes中間件

2022-09-05 08:26:29

Kubernetes標簽

2023-08-03 08:36:30

Service服務架構

2023-08-04 08:19:02

2023-05-25 21:38:30

點贊
收藏

51CTO技術棧公眾號

97超碰人人看| 日韩午夜视频在线观看| 欧美日韩一级大片| 国产精品天天看天天狠| 色婷婷久久99综合精品jk白丝| 日本成人黄色免费看| 这里只有精品6| 欧美三级特黄| 亚洲偷欧美偷国内偷| 欧美一级小视频| 91黄页在线观看| 国产日韩精品一区| 亚洲综合视频1区| 国产成人精品一区二三区| 欧美一区三区| 欧美va亚洲va国产综合| 人妻丰满熟妇av无码区app| 99福利在线| 久久久精品国产99久久精品芒果| 成人美女av在线直播| 91美女免费看| 你懂的视频一区二区| 亚洲人精品午夜在线观看| 国产xxxxhd| 吞精囗交69激情欧美| 亚洲精品菠萝久久久久久久| 欧美成人dvd在线视频| 国产女主播福利| 亚洲女同在线| 欧美—级高清免费播放| 影音先锋男人看片资源| 亚洲精品无吗| 亚洲福利在线播放| 在线免费看v片| avav成人| 一本一道久久a久久精品综合蜜臀| 中国一级黄色录像| 98在线视频| 337p粉嫩大胆色噜噜噜噜亚洲| 91成人伦理在线电影| 国产日韩久久久| 免费在线成人| 国内外成人免费激情在线视频| 黄色香蕉视频在线观看| 日本电影一区二区| 亚洲丝袜在线视频| 在线 丝袜 欧美 日韩 制服| 国产伦乱精品| 亚洲成人久久网| 岛国大片在线免费观看| 成人在线视频国产| 56国语精品自产拍在线观看| 91插插插插插插插插| 桃花岛成人影院| 一本在线高清不卡dvd| 久久久久久久久久久视频| 1024在线看片你懂得| 一区二区三区四区不卡在线| 女女百合国产免费网站| av毛片在线| 亚洲精品成人精品456| 日本成人在线不卡| 色黄网站在线观看| 亚洲综合久久久久| 欧美午夜性视频| gogo高清在线播放免费| 亚洲第一久久影院| 1024av视频| 台湾佬中文娱乐久久久| 欧洲人成人精品| jizz18女人| 成人在线视频国产| 精品剧情在线观看| 在线视频 日韩| 伊人久久大香线蕉无限次| 亚洲人成在线观看| 91制片厂在线| 激情久久久久久| 91精品国产电影| 高潮毛片又色又爽免费 | 精品视频在线观看网站| 日韩欧美中文一区| 人体私拍套图hdxxxx| 在线成人动漫av| 在线精品播放av| jizz亚洲少妇| 99pao成人国产永久免费视频| 欧美一级视频免费在线观看| 一级久久久久久| 狠狠色狠狠色综合系列| av在线亚洲男人的天堂| 亚洲av电影一区| 国产精品久久久久7777按摩| 国产 国语对白 露脸| 天堂在线中文网官网| 欧美日韩五月天| 四虎精品一区二区| 国产精品欧美三级在线观看| 久久九九全国免费精品观看| 五月天综合激情| 蜜臀av国产精品久久久久| 99在线观看| 国产视频三级在线观看播放| 一区二区三区自拍| 精品视频无码一区二区三区| 精品视频在线播放一区二区三区| 日韩av在线免费观看一区| 国产wwwwxxxx| 一本一本久久| 91性高湖久久久久久久久_久久99| 污污网站在线免费观看| 国产精品久线观看视频| 精品少妇人妻av免费久久洗澡| 国精品产品一区| 亚洲国模精品一区| 欧美做爰啪啪xxxⅹ性| 国产精品毛片一区二区三区| 亚洲va码欧洲m码| 黄网在线观看| 亚洲大片免费看| 亚洲成人av免费观看| 欧美理论在线播放| 91极品视频在线| 国产视频一二三四区| 国产喂奶挤奶一区二区三区| 国产夫妻自拍一区| 北岛玲精品视频在线观看| 亚洲老司机av| 一区二区三区免费高清视频| 国内欧美视频一区二区| 日韩一二三区不卡在线视频| 毛片在线网站| 亚洲精品一区二区三区在线观看| frxxee中国xxx麻豆hd| 日韩国产精品91| 欧美极品视频一区二区三区| 2021中文字幕在线| 精品久久国产字幕高潮| 波多野结衣在线网址| 蜜桃一区二区三区在线观看| 欧美一区亚洲二区| 国产超碰精品| 国产视频综合在线| 800av免费在线观看| 99这里只有久久精品视频| www.69av| 蜜桃精品视频| 久久久久999| 国产精品久久婷婷| 亚洲欧洲一区二区在线播放| 国产成人精品无码播放| 免费成人av| 国产成人福利视频| av基地在线| 欧美色图12p| 精品国产aaa| 蜜臀av性久久久久蜜臀aⅴ四虎| 亚洲v国产v在线观看| 欧美不卡高清一区二区三区| 一区二区欧美日韩视频| 中文天堂在线视频| 国产精品传媒视频| 久久精品视频在线观看免费| 欧美日韩国产欧| 国产精品二区二区三区| 91福利区在线观看| 精品亚洲国产视频| 日本黄色中文字幕| 国产精品免费看片| 国产毛片久久久久久| 亚洲精品成人| 国产精品青青草| 日本三级一区| 伊人久久大香线蕉av一区二区| 中文字幕日韩国产| 亚洲欧美另类图片小说| 久久精品无码专区| 免费国产自线拍一欧美视频| 日韩国产欧美精品| www.91精品| 2019中文字幕在线| 91在线视频免费看| 日韩一区二区三区在线观看| 国产精品成人网站| 久久青草欧美一区二区三区| 亚洲天堂网一区| 欧美暴力喷水在线| 精品国产一区二区三区麻豆小说 | 97超级碰碰| 日韩脚交footjobhdboots| 国产一区二区三区在线| 国产日韩欧美一区二区东京热| 亚洲h在线观看| 精品无码在线观看| 国产成人一区在线| 久热免费在线观看| 欧美在线免费一级片| 欧美人xxxxx| 精品视频一区二区三区| 51久久精品夜色国产麻豆| 日本中文字幕在线看| 亚洲国产97在线精品一区| 最新在线中文字幕| 午夜精品久久久久久不卡8050| 最新中文字幕av| 成人免费视频免费观看| 成人黄色片视频| 中文无码久久精品| 日本亚洲导航| h视频久久久| 成人国产在线视频| 亚洲精品福利电影| 欧美风情在线观看| 日本中文字幕伦在线观看| 亚洲福利视频网站| www.久久色| 欧美日韩精品系列| 国产成人综合欧美精品久久| 亚洲狼人国产精品| 国产黄色录像视频| 2022国产精品视频| 亚洲一区和二区| 国产在线日韩欧美| 9久久婷婷国产综合精品性色 | 图片婷婷一区| 91久久大香伊蕉在人线| 欧美a视频在线| 国产va免费精品高清在线| 波多野结衣中文字幕久久| 日韩视频免费在线| 第一视频专区在线| 日韩精品视频在线观看网址| 午夜老司机福利| 欧美日韩不卡一区二区| 国产日韩在线免费观看| 黑人巨大精品欧美一区二区一视频 | 久久九九热免费视频| 91精品国产综合久久久久久豆腐| 日韩精品中文在线观看| 少妇精品高潮欲妇又嫩中文字幕| 日韩精品综合一本久道在线视频| 国产精品久久影视| 欧美日韩国产免费| 亚洲一级片免费看| 精品视频免费看| 免费一级a毛片| 欧美在线免费视屏| 免费黄色一级大片| 欧美三级日韩三级国产三级| 国产精品xxxxxx| 欧美日韩国产首页| 11024精品一区二区三区日韩| 欧美日韩在线电影| 一级特黄aa大片| 7878成人国产在线观看| 国产伦精品一区二区三区免.费 | 蜜桃视频久久一区免费观看入口| 精品剧情在线观看| 视频二区在线观看| 日韩福利视频在线观看| 日韩资源在线| 国产午夜精品全部视频播放| 国产天堂在线| 久久精品免费播放| www视频在线免费观看| 欧美成人一二三| 国内高清免费在线视频| 91高清在线免费观看| 国产日韩电影| 国产精品稀缺呦系列在线| 四虎精品永久免费| 99r国产精品视频| 精品三级av在线导航| 蜜桃91精品入口| 成人黄色小视频| 五月天色婷婷综合| 国模吧视频一区| 欧美成人xxxxx| 麻豆成人av在线| 图片区偷拍区小说区| 成人午夜激情视频| 欧美一级片黄色| 久久久久综合网| 激情无码人妻又粗又大| 一区二区三区四区在线| 成人免费视频毛片| 欧美日韩aaaaaa| 二区三区在线视频| 亚洲天堂男人的天堂| 成人影院www在线观看| 国产69精品99久久久久久宅男| 新片速递亚洲合集欧美合集| 国产日韩精品入口| 成人爽a毛片| 午夜视频久久久| 亚洲激情精品| 亚洲精品高清无码视频| 国产成人免费在线| 波多野结衣a v在线| 亚洲少妇30p| 欧美精品韩国精品| 日韩欧美在线影院| 国产私人尤物无码不卡| 色综合久综合久久综合久鬼88| 欧美黑人巨大xxxxx| 亚洲a∨日韩av高清在线观看| 香蕉久久精品| 狠狠干视频网站| 日本美女视频一区二区| 国产黑丝在线观看| **欧美大码日韩| 精品人妻一区二区三区潮喷在线 | 亚洲精品国产首次亮相| 各处沟厕大尺度偷拍女厕嘘嘘| 国产美女主播视频一区| 亚洲成人黄色av| 天天爽夜夜爽夜夜爽精品视频| 91禁在线观看| 亚洲免费av电影| 丁香花在线电影小说观看| 国产精品视频网| 亚瑟一区二区三区四区| 热久久最新地址| 麻豆久久久久久久| 好吊视频在线观看| 午夜视黄欧洲亚洲| 国产黄色片免费| 日韩在线观看免费| 欧美日韩在线精品一区二区三区激情综合| 成人性色av| 综合日韩在线| 亚洲黄色av片| 国产精品你懂的在线欣赏| 亚洲图片欧美日韩| 国产丝袜一区二区| 国产传媒在线| 国产精品日韩一区二区| 欧美久久影院| 五月六月丁香婷婷| 中文字幕一区二区三区在线播放| 亚洲精品久久久久久久蜜桃| 亚洲精品日韩丝袜精品| 狠狠操一区二区三区| 国产精品一区免费观看| 欧美三级网页| 成人在线观看一区二区| 一区二区三区波多野结衣在线观看| 一区二区日韩在线观看| 色婷婷成人综合| 国内自拍亚洲| 一本一本a久久| 黄一区二区三区| 国产传媒免费在线观看| 91精品国产综合久久久蜜臀粉嫩| 欧美私人网站| 91天堂在线视频| 欧美激情视频一区二区三区免费| 在线播放黄色av| 亚洲猫色日本管| 亚洲精品无amm毛片| 国语自产精品视频在线看抢先版图片| 超碰97成人| 日本免费一级视频| 久久久久久久综合日本| 99re热视频| 粗暴蹂躏中文一区二区三区| 精品国产不卡一区二区| 国产精品videossex国产高清| 成人小视频在线| 日韩黄色在线播放| 在线日韩精品视频| 91精品国产一区二区在线观看 | 污污的网站在线免费观看| 古典武侠综合av第一页| a91a精品视频在线观看| 午夜时刻免费入口| 欧美肥胖老妇做爰| 久久av色综合| 日本一区网站| 精品无码三级在线观看视频| 九九视频免费在线观看| 日韩电影视频免费| 黄色精品视频网站| 日韩精品一区二区三区四| 91丨porny丨户外露出| 中文字幕免费视频观看| 久久这里有精品视频| 美女视频亚洲色图| 手机视频在线观看| 亚洲午夜视频在线观看| 久久电影中文字幕| 91九色在线观看| 日韩综合小视频| 亚洲国产精品久| 国产亚洲精品久久| 日本成人精品| www黄色在线| 亚洲成人免费在线| 日本中文字幕在线视频| 久久久久久九九|