😳

linux内存性能分析

Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。这样进程就可以很方便地访问内存,更确切地说是访问虚拟内存。

虚拟地址空间的内部又被分为内核空间和用户空间两部分。

虽然每个进程的地址空间都包含了内核空间,但这些内核空间,其实关联的都是相同的物理内存。

虚拟内存和物理内存,通过内存映射来管理。为了完成内存映射,内核为每个进程都维护了一张页表,记录虚拟地址与物理地址的映射关系。当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。

img

TLB (Translation Lookaside Buffer, 转译后备缓存器 ) 会影响 CPU 的内存访问性能,TLB 其实就是 MMU 中页表的高速缓存。

由于进程的虚拟地址空间时独立的,而 TLB 的访问速度又比 MMU 快得多,所以,通过减少进程的上下文切换,减少 TLB 的刷新次数,就可以提高 TLB 缓存的使用率,进而提高 CPU 的内存访问性能。

Linux 使用多级页表来管理内存。多级页表就是把俄内存分成区块来管理,将原来的映射关系改成区块索引和区块内的偏移。

img

虚拟空间内存分布

img

在这五个内存段中,堆和文件映射段的内存是动态分配的。比如说,使用 C 标准库的 malloc() 或 mmap() ,就可以分别在堆和文件映射段动态分配内存。

malloc 底层使用 brk 或者 mmap 来分配内存。这里后续需要详细了解下

内存紧张时,系统会通过一系列的机制来回收内存

  • 回收缓存,比如使用 LRU 算法,回收最近使用最少的内存页面
  • 回收不常访问的内存,把不常用的内存通过交换分区直接写到磁盘中
  • 杀死进程,内存紧张时系统还会通过 OOM ,直接杀掉占用大量内存的进程

OOM 是通过 oom_score 分数来控制的,分数越高越容易被杀死,可以通过 /proc 文件系统,手动设置进程的 oom_adj, 从而调整进程的 oom_score。

Oom_adj 的范围是 [-17, 15], 数值越大,表示进程越容易被 OOM 杀死;数值越小,表示进程越不容易被 OOM 杀死,其中 -17 表示禁止 OOM

比如下面命令,可以把 sshd 进程设置为 -16

$ echo -16 > /proc/$(pidof sshd)/oom_adj

如何查看内存使用情况

参考播客

https://blog.holbertonschool.com/hack-the-virtual-memory-malloc-the-heap-the-program-break/

free

$ free
               total        used        free      shared  buff/cache   available
Mem:         3881060      344980      744700        4088     2791380     3336264
Swap:              0           0           0

available 是新进程可用内存大小,

查看进程占用内存

$ top
top - 00:58:26 up 18 days, 17:13,  1 user,  load average: 0.00, 0.00, 0.00
Tasks: 165 total,   1 running, 164 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.1 us,  0.2 sy,  0.0 ni, 99.8 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :   3790.1 total,    724.1 free,    339.9 used,   2726.1 buff/cache
MiB Swap:      0.0 total,      0.0 free,      0.0 used.   3255.1 avail Mem

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
2503043 hahastr+  20   0   10376   3276   2684 R   1.0   0.1   0:00.17 top
2498801 root      20   0       0      0      0 I   0.3   0.0   0:00.89 kworker/3:2-eve+
      1 root      20   0  169092  11088   6080 S   0.0   0.3   1:59.08 systemd
      2 root      20   0       0      0      0 S   0.0   0.0   0:06.48 kthreadd
      3 root       0 -20       0      0      0 I   0.0   0.0   0:00.00 rcu_gp
      4 root       0 -20       0      0      0 I   0.0   0.0   0:00.00 rcu_par_gp
      5 root       0 -20       0      0      0 I   0.0   0.0   0:00.00 slub_

内存中的 Buffer 和 Cache

buffer 和 cache 的介绍

buffers
       Memory used by kernel buffers (Buffers in /proc/meminfo)

cache  Memory  used  by  the  page cache and slabs (Cached and SReclaimable in
       /proc/meminfo)

buffers 是内核缓冲区用到的内存,对应的是 /proc/meminfo 中的 Buffers

cache 是内核页缓存和 slab 用到的内存,对应的是 /proc/meminfo 中的 cached 和 SReclaimable 之和

通过查看 proc 得到 这个文件系统的详细文档

Buffers %lu
    Relatively temporary storage for raw disk blocks that shouldn't get tremendously large (20MB or so).

Cached %lu
   In-memory cache for files read from the disk (the page cache).  Doesn't include SwapCached.
...
SReclaimable %lu (since Linux 2.6.19)
    Part of Slab, that might be reclaimed, such as caches.
    
SUnreclaim %lu (since Linux 2.6.19)
    Part of Slab, that cannot be reclaimed on memory pressure.
  • Buffers 是对原始磁盘块的临时存储,也就是用来缓存磁盘的数据,通常只有 20 M。这样就可以将分散的写集中起来,统一优化磁盘的写入。
  • Cached 是从磁盘读区文件的页缓存,用来缓存从文件读取的数据。这样下次就可直接从内存中快速获取,而不需要再次访问缓慢的磁盘了。

案例

在一个终端运行 vmstat , 观察 buffer 和 cache

$ vmstat 1

在另一个终端执行 dd 命令,通过读取随机设备,生成一个 500M 大小的文件

$ dd if=/dev/urandom of=/tmp/file bs=1M count=500

vmstat 的输出

procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
0  0      0 7499460   1344 230484    0    0     0     0   29  145  0  0 100  0  0
 1  0      0 7338088   1752 390512    0    0   488     0   39  558  0 47 53  0  0
 1  0      0 7158872   1752 568800    0    0     0     4   30  376  1 50 49  0  0
 1  0      0 6980308   1752 747860    0    0     0     0   24  360  0 50 50  0  0
 0  0      0 6977448   1752 752072    0    0     0     0   29  138  0  0 100  0  0
 0  0      0 6977440   1760 752080    0    0     0   152   42  212  0  1 99  1  0
...
 0  1      0 6977216   1768 752104    0    0     4 122880   33  234  0  1 51 49  0
 0  1      0 6977440   1768 752108    0    0     0 10240   38  196  0  0 50 50  0

可以看出 cache 不断增长,buffer 基本保持不变

再进一步观察 I/O 的情况,我们可以看到

  • 在 cache 刚开始增长时,块设备 I/O 很少,bi 只出现了一次,bo 则只有一次 4 kb/s. 而过一段时间后,才会出现大量的块设备写
  • 当 dd 命令结束后,cache 不再增长,但块设备还会持续一段时间,并且多次 I/O 写的结果加起来,才是 dd 要写的 500M 数据

向块设备写入

$ echo 3 > /proc/sys/vm/drop_caches
$ dd if=/dev/urandom of=/dev/sdb1 bs=1M count=2048

观察 vmstat 的输出

$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
1  0      0 7584780 153592  97436    0    0   684     0   31  423  1 48 50  2  0
 1  0      0 7418580 315384 101668    0    0     0     0   32  144  0 50 50  0  0
 1  0      0 7253664 475844 106208    0    0     0     0   20  137  0 50 50  0  0
 1  0      0 7093352 631800 110520    0    0     0     0   23  223  0 50 50  0  0
 1  1      0 6930056 790520 114980    0    0     0 12804   23  168  0 50 42  9  0
 1  0      0 6757204 949240 119396    0    0     0 183804   24  191  0 53 26 21  0
 1  1      0 6591516 1107960 123840    0    0     0 77316   22  232  0 52 16 33  0

从上面可以看出,虽然都是写数据,写磁盘跟写文件的现象还是不同的。写磁盘时,Buffer 和 Cache 都在增长,但 buff 增长的块得多

这说明写磁盘用到了大量的 Buffer。

总结

  • Buffer 既可以用作 将要写入磁盘数据的缓存,也可以用作 从磁盘读区数据的缓存
  • Cache 既可以用作 从文件读取数据的页缓存,也可以用作 写文件的页缓存

如何利用缓存个优化程序的运行效率

缓存命中率

缓存命中率,是指通过缓存获取数据的请求次数,占所有数据请求次数的百分比。

命中率越高,表示使用缓存带来的收益越高,应用程序的性能也就越好

工具

  • cachestat 提供了整个操作系统缓存的读写命中情况
  • cachetop 提供了每个进程的缓存命中情况
  • pcstat 指定文件的缓存大小: go install github.com/tobert/pcstat@latest

需要安装 bcc 工具

https://github.com/iovisor/bcc/blob/master/INSTALL.md#ubuntu---source

export PATH=$PATH:/usr/share/bcc/tools

案例

测试文件读取

# 生成一个临时文件
$ dd if=/dev/sda1 of=file bs=1M count=512

# 清理缓存
$ echo 3 > /proc/sys/vm/drop_caches

查看 file 文件的缓存

root@hahastrong:/home/hahastrong# pcstat file
+-------+----------------+------------+-----------+---------+
| Name  | Size (bytes)   | Pages      | Cached    | Percent |
|-------+----------------+------------+-----------+---------|
| file  | 536870912      | 131072     | 0         |   0.000 |
+-------+----------------+------------+-----------+---------+

在另一个终端中 运行 cachetop

# 每隔五秒刷新一次数据
$ cachetop 1

image-20231027075801637

在第一个终端中,运行 dd 命令测试文件的读取速度

$ dd if=file of=/dev/numm bs=1M

root@hahastrong:/home/hahastrong# dd if=file of=/dev/null bs=1M
512+0 records in
512+0 records out
536870912 bytes (537 MB, 512 MiB) copied, 0.374987 s, 1.4 GB/s

# 第二次
root@hahastrong:/home/hahastrong# dd if=file of=/dev/null bs=1M
512+0 records in
512+0 records out
536870912 bytes (537 MB, 512 MiB) copied, 0.161763 s, 3.3 GB/s

从 cachetop 的结果来看,读请求的缓存命中率只有 50 %

再次执行相同的 dd 命令时,磁盘的读性能提高 3 倍。

看来 file 文件被系统缓存了,我们使用。pcstat 来验证下

root@hahastrong:/home/hahastrong# pcstat file
+-------+----------------+------------+-----------+---------+
| Name  | Size (bytes)   | Pages      | Cached    | Percent |
|-------+----------------+------------+-----------+---------|
| file  | 536870912      | 131072     | 131072    | 100.000 |
+-------+----------------+------------+-----------+---------+

从上述数据中,可以发现 file 文件已经被全部缓存了,命中率是 100%

案例2 不可中断进程的读

如何程序直接读取文件,绕过系统缓存,也会导致看起来命中率 100%,实际上很少文件走系统缓存。

此时,借助 strace 查看程序以什么方式打开文件,如果参数中有 O_DIRECT 则需要去掉

$ strace -p $(pgrep app)
...
openat(AT_FDCWD, "/dev/sdb1", O_RDONLY|O_DIRECT)
...

直接IO是跳过Buffer,裸IO是跳过文件系统(还是有buffer的)

内存泄漏了,如何快速定位和处理

进程可以自主申请内存,以及可能存在内存泄漏的区域 – 堆 和 内存映射段

使用 bcc 的 memleak 可以查看内存泄漏问题

root@hahastrong:/home/hahastrong# memleak -a -p $(pidof app)
Attaching to pid 8190, Ctrl+C to quit.
[00:36:50] Top 10 stacks with outstanding allocations:
	addr = 7f6858268260 size = 8192
	addr = 7f6858262230 size = 8192
	addr = 7f6858266250 size = 8192
	addr = 7f6858264240 size = 8192
	32768 bytes in 4 allocations from stack
		0x000056485fa00879	fibonacci+0x1f [app]  ## 内存泄漏的函数
		0x000056485fa008ea	child+0x4f [app]
		0x00007f685d2dc6db	start_thread+0xdb [libpthread-2.27.so]
[00:36:55] Top 10 stacks with outstanding allocations:
	addr = 7f6858268260 size = 8192
	addr = 7f685826a270 size = 8192
	addr = 7f6858262230 size = 8192
	addr = 7f68582702a0 size = 8192
	addr = 7f6858266250 size = 8192
	addr = 7f6858264240 size = 8192
	addr = 7f68582722b0 size = 8192
	addr = 7f685826c280 size = 8192
	addr = 7f685826e290 size = 8192
	73728 bytes in 9 allocations from stack
		0x000056485fa00879	fibonacci+0x1f [app]
		0x000056485fa008ea	child+0x4f [app]
		0x00007f685d2dc6db	start_thread+0xdb [libpthread-2.27.so]

为什么系统的 Swap 变高了

内存回收,也就是系统释放掉可以回收的内存,比如前面提到的缓存和缓冲区,就属于可回收内存。他们在内存管理中,通常被叫做文件页(Filebacked Page)

大部分文件页,都可以直接回收,以后有需要时,再从磁盘重新读取就可以了。而哪些被应用程序修改过,并且暂时还没写入磁盘的数据,就得先写入磁盘,然后才能进行内存释放。(内存页中会有个dirty 标识)

对于应用程序申请的堆内存(匿名页),系统没法直接释放,需要使用 swap 机制,将这些不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。

Swap 原理

swap 将一块磁盘空间或者一个本地文件当成内存来使用。它包括换入和换出两个过程

  • 换出,就是把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存
  • 换入,则是在进程再次访问这些内存时,把他们从磁盘中读到内存中来。

常见的笔记本电脑的休眠和快速开机的功能,都是基于 swap 的。

内存回收的场景

  • 直接内存回收:有新的大块内存分配请求,但是剩余内存不足。这个时候系统就需要回收一部分内存(比如前面提到的缓存),进而尽可能地满足新内存请求
  • 定期内存回收:系统中有一个专门的内核线程来定期回收内存(kswapd0)。为了衡量内存的使用情况,kswapd0 定义了三个内存阈值(watermark,也称为水位),分别是
    • 页最小阈值(pages_min)
    • 页最低阈值(pages_low)
    • 页最高阈值(pages_high)
    • 剩余内存使用 pages_free 表示

img

定期内存回收策略

  1. 剩余内存小于 页最小阈值:说明进程可用内存都耗尽了,只有内核才可以分配内存
  2. 剩余内存落在页最小阈值和页低阈值中间,说明内存压力比较大剩余内存不多了。这时 kswapd0 会执行内存回收, 直至剩余内存大于高阈值为止。
  3. 剩余内存落在页低阈值和页高阈值中间,说明内存有一定压力,但还可以满足新内存请求
  4. 剩余内存大于页高阈值,说明剩余内存比较多,没有内存压力

页低阈值

/proc/sys/vm/min_free_kbytes
45056

NUMA 和 swap

有时候,可用内存还很多,但还是发生了 swap,可能是 numa,导致每个 node 拥有自己的本地内存空间

img

查看内存使用信息

$ cat /proc/zoneinfo
Node 0, zone      DMA
  per-node stats
      nr_inactive_anon 43304
      nr_active_anon 982
      nr_inactive_file 375495
      nr_active_file 366544
      nr_unevictable 8460
      nr_slab_reclaimable 80804
      nr_slab_unreclaimable 17511
      nr_isolated_anon 0
      nr_isolated_file 0
      workingset_nodes 7693
      workingset_refault_anon 0
      workingset_refault_file 4745613
      workingset_activate_anon 0
      workingset_activate_file 1801658
      workingset_restore_anon 0
      workingset_restore_file 708146
      workingset_nodereclaim 118059
      nr_anon_pages 50821
      nr_mapped    35310
      nr_file_pages 743964
      nr_dirty     7
      nr_writeback 0
      nr_writeback_temp 0
      nr_shmem     970
      nr_shmem_hugepages 0
      nr_shmem_pmdmapped 0
      nr_file_hugepages 0
      nr_file_pmdmapped 0
      nr_anon_transparent_hugepages 0
      nr_vmscan_write 0
      nr_vmscan_immediate_reclaim 19382
      nr_dirtied   28710062
      nr_written   27975958
      nr_kernel_misc_reclaimable 0
      nr_foll_pin_acquired 630
      nr_foll_pin_released 630
      nr_kernel_stack 4336
      nr_page_table_pages 1162
      nr_swapcached 0
  pages free     22000
        min      2453
        low      3066
        high     3679
        spanned  262144
        present  242688
        managed  220517
        cma      16384
        protection: (0, 2928, 2928, 2928)
      nr_free_pages 22000
      nr_zone_inactive_anon 636  // 匿名页
      nr_zone_active_anon 10
      nr_zone_inactive_file 95512 // 文件页
      nr_zone_active_file 71928
      nr_zone_unevictable 0
      nr_zone_write_pending 0
      nr_mlock     0
      nr_bounce    0
      nr_zspages   0
      nr_free_cma  897
  pagesets
    cpu: 0
              count: 182
              high:  766
              batch: 63
  vm stats threshold: 24
    cpu: 1
              count: 311
              high:  766
              batch: 63
  vm stats threshold: 24
    cpu: 2
              count: 685
              high:  766
              batch: 63
  vm stats threshold: 24
    cpu: 3
              count: 676
              high:  766
              batch: 63
  vm stats threshold: 24
  node_unreclaimable:  0
  start_pfn:           0

查看进程换出的虚拟内存

$ cat /proc/pid/staus | grep VmSwap
// 用于查看进程换出的 虚拟内存大小
VmPeak:	  119476 kB
VmSize:	  119476 kB
VmLck:	       0 kB
VmPin:	     752 kB
VmHWM:	   21412 kB
VmRSS:	   13276 kB
RssAnon:	   10464 kB
RssFile:	    2812 kB
RssShmem:	       0 kB
VmData:	   45360 kB
VmStk:	     132 kB
VmExe:	    3100 kB
VmLib:	   18560 kB
VmPTE:	     256 kB
VmSwap:	       0 kB

查看最大虚拟内存排行

$ for line in /proc/*/status ; do awk '/VmSwap|Name|^Pid/{printf $2 " " $3}END {print ""}' $line; done | sort -k 3 -n -r | head
writeback 21
watchdogd 78
vfio-irqfd-clea 91
v2ray 137053 0 kB
tpm_dev_wq 73
systemd-udevd 246 0 kB
systemd-resolve 416 0 kB
systemd-network 404 0 kB
systemd-logind 459 0 kB
systemd-journal 219 0 kB

关闭虚拟内存

$ swapoff -a

套路篇: 如何快准狠找到系统内存的问题

内存性能指标

  • 系统

    • 已用内存和剩余内存

    • 共享内存:通过 tmpfs 实现的,他的大小就是 tmpfs 使用内存大小。tmpfs 其实也是一种特殊的缓存

      • 最近在工作中遇到一个tmpfs的坑。 rc.canlog会不断的向 /tmp/canlog/下面写文件,而/tmp 挂载的就是tmpfs。可以看到可用内存在不断减少,而通过top,ps等命令,没有明显的看到是tmpfs引起的。df -h才能发现,/tmpfs /tmp竟然占用了100MB内存。

    • 可用内存是新进程可以使用的最大内存,它包括剩余内存和可回收缓存

    • 缓存包括两部分:一部分是磁盘读取文件的页缓存,用来缓存从磁盘读取的数据,可以加快后续的再次访问。另一部分是,slab 分配器中的可回收内存(内核中的内存)

    • 缓冲区是对原始磁盘块的临时存储,用来缓存将要写入磁盘的数据。统一优化磁盘的写入

  • 引用进程

    • 虚拟内存,包括了进程代码段、数据段、共享内存、已经申请的堆内存和已经换出的内存。
    • 常驻内存是进程实际使用的物理内存。不包括 swap 和 共享内存
    • 共享内存,既包括与其他进程共同使用的真实的共享内存,还包括了加载的动态链接库以及程序的代码段等
    • swap 内存,是指通过 swap 换出到磁盘的内存。

性能分析工具

img

快速定位内存问题

使用 free、top、vmstat、pidstat

分析思路:

  1. 先用 free 和 top,查看系统整体的内存使用情况
  2. 在用 vmstat 和 pidstat ,查看一段时间的趋势,从而判断出内存问题的类型
  3. 最后进行详细分析,比如内存分配分析、缓存/缓冲区分析、具体进程的内存使用分析等
  4. 针对进程,可以进行 pprof 来查看内存问题,可能是 gc 等导致的

img