极客时间专栏——linux性能优化实战——内存部分

时间:May 11, 2019 分类:

目录:

15 | 基础篇:Linux内存是怎么工作的?

内存映射

物理内存只有内核有权限访问,进程使用的是内核为进程提供的一个独立的虚拟地址空间

虚拟地址空间又分内核空间和用户空间,对于32位和64位系统的分配是不同的

只有使用到虚拟地址空间才会被分配物理地址,分配的过程通过内存映射的方式,内核为每个进程维护了一个页表(MMU)的来记录映射的关系

当进程访问的虚拟地址在页表中查不到就会产生一个缺页异常,就需要内核进行分配物理内存,更新进程页表,最后返回用户空间,恢复进程的运行

TLB用于缓存MMU

页的大小是4k

多级页表和大页内存用于解决页表项过多的问题

虚拟内存空间分布

  • 只读段 代码和常量
  • 数据段 全局变量
  • 堆 动态分配的内存
  • 文件映射段 动态库,共享内存
  • 栈 局部变量,函数调用的上下文,大小固定,一般为8MB

内存分配与回收

malloc,C标准库提供的内存分配函数,对应的系统调用为brk()和mmap()

  • brk()用于分配小块内存,在堆顶的位置,分配后可以重复利用,但是在内存繁忙的时候可能会造成内存碎片
  • mmap()用于分配大块内存,在文件映射端分配空闲内存,但是在内存繁忙的时候会出现大量的缺页异常增加内核负担

这两种方式都是需要首次访问的时候通过缺页异常进入内核

内存回收通过free和unmap来回收和释放不用的内存,还有LRU算法回收最近最少使用的内存页,OOM等

  • VIRT进程虚拟内存大小,申请没有使用的也会计算在内
  • RES常驻内存,就是进程实际使用的物理内存大小,不包含swap和共享内存
  • SHR共享内存,与其他进程共享的内存

16 | 基础篇:怎么理解内存中的Buffer和Cache?

  • buffer是要写入磁盘的数据
  • cache是缓存从文件中读取的数据

写文件用到cache,写磁盘用到buffer,读文件用到cache,写磁盘用到buffer

17 | 案例篇:如何利用系统缓存优化程序的运行效率?

工具介绍

缓存命中率

bcc-tools工具的

  • cachestat
  • cachetop
# 升级系统
yum update -y

# 安装 ELRepo
rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org
rpm -Uvh https://www.elrepo.org/elrepo-release-7.0-3.el7.elrepo.noarch.rpm

# 安装新内核
yum remove -y kernel-headers kernel-tools kernel-tools-libs
yum --enablerepo="elrepo-kernel" install -y kernel-ml kernel-ml-devel kernel-ml-headers kernel-ml-tools kernel-ml-tools-libs kernel-ml-tools-libs-devel

# 更新 Grub 后重启
grub2-mkconfig -o /boot/grub2/grub.cfg
grub2-set-default 0
reboot

# 重启后确认内核版本已升级为 4.20.0-1.el7.elrepo.x86_64
uname -r

# 安装 bcc-tools
yum install -y bcc-tools

# 配置 PATH 路径
export PATH=$PATH:/usr/share/bcc/tools

# 验证安装成功
cachestat 

yum安装并配置环境变量export PATH=$PATH:/usr/share/bcc/tools

$ cachestat 1 3
TOTAL MISSES HITS DIRTIES BUFFERS_MB CACHED_MB
2 0 2 2 1 17 279
2 0 2 2 1 17 279
2 0 2 2 1 17 279
  • TOTAL表示总的IO次数
  • MISSES表示缓存未命中次数
  • HITS表示缓存命中次数
  • DIRTIES新增到缓存中的脏页数
  • BUFFERS_MB表示buffer大小
  • CACHED_MB表示cache大小
$ cachetop
11:58:50 Buffers MB: 258 / Cached MB: 347 / Sort: HITS / Order: ascending
PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT%
13029 root python 1 0 0 100.0% 0.0%

READ_HITWRITE_HIT为读写缓存的命中率

pcstat查看内存中缓存大小和缓存比例

安装

$ export GOPATH=~/go
$ export PATH=~/go/bin:$PATH
$ go get golang.org/x/sys/unix
$ go get github.com/tobert/pcstat/pcstat

查看ls在内存中的缓存

$ pcstat /bin/ls
+---------+----------------+------------+-----------+---------+
| Name    | Size (bytes)   | Pages      | Cached    | Percent |
|---------+----------------+------------+-----------+---------|
| /bin/ls | 133792         | 33         | 0         | 000.000 |
+---------+----------------+------------+-----------+---------+

可以看到没有进行缓存

执行一下ls命令之后

ls
$ pcstat /bin/ls
+---------+----------------+------------+-----------+---------+
| Name    | Size (bytes)   | Pages      | Cached    | Percent |
|---------+----------------+------------+-----------+---------|
| /bin/ls | 133792         | 33         | 33        | 100.000 |
+---------+----------------+------------+-----------+---------+

测试读文件和读磁盘

# 生成一个 512MB 的临时文件
$ dd if=/dev/sda1 of=file bs=1M count=512
# 清理缓存
$ echo 3 > /proc/sys/vm/drop_caches

查看文件的缓存

pcstat file
+-------+----------------+------------+-----------+---------+
| Name  | Size (bytes)   | Pages      | Cached    | Percent |
|-------+----------------+------------+-----------+---------|
| file  | 536870912      | 131072     | 0         | 000.000 |
+-------+----------------+------------+-----------+---------+

在一个终端进行cachetop 5命令,另一个终端使用dd进行读文件

$ dd if=file of=/dev/null bs=1M
512+0 records in
512+0 records out
536870912 bytes (537 MB, 512 MiB) copied, 16.0509 s, 33.4 MB/s

看一下cachetop

PID      UID      CMD              HITS     MISSES   DIRTIES  READ_HIT%  WRITE_HIT%
\.\.\.
    3264 root     dd                  37077    37330        0      49.8%      50.2%

可以看到只有50%的读缓存命中率,再读一次

$ dd if=file of=/dev/null bs=1M
512+0 records in
512+0 records out
536870912 bytes (537 MB, 512 MiB) copied, 0.118415 s, 4.5 GB/s

可以看到这次是100%的缓存命中率

10:45:22 Buffers MB: 4 / Cached MB: 719 / Sort: HITS / Order: ascending
PID      UID      CMD              HITS     MISSES   DIRTIES  READ_HIT%  WRITE_HIT%
\.\.\.
   32642 root     dd                 131637        0        0     100.0%       0.0%

pcstat看一下

$ pcstat file
+-------+----------------+------------+-----------+---------+
| Name  | Size (bytes)   | Pages      | Cached    | Percent |
|-------+----------------+------------+-----------+---------|
| file  | 536870912      | 131072     | 131072    | 100.000 |
+-------+----------------+------------+-----------+---------+
$ docker run --privileged --name=app -itd feisky/app:io-direct

具体参考github地址

你还可以通过以下两个个选项来修改磁盘读取的行为:

  • -d 设置要读取的磁盘,默认前缀为 /dev/sd 或者 /dev/xvd 的磁盘
  • -s 设置每次读取的数据量大小,单位为字节,默认为 33554432(也就是 32MB)

这些选项的使用方法为:

$ docker run --privileged --name=app -itd feisky/app:io-direct /app -d /dev/sdb -s 33554432

查看日志

$ docker logs app
Reading data from disk /dev/sdb1 with buffer size 33554432
Time used: 0.929935 s to read 33554432 bytes
Time used: 0.949625 s to read 33554432 bytes

然后看一下cachetop获取的

16:39:18 Buffers MB: 73 / Cached MB: 281 / Sort: HITS / Order: ascending
PID      UID      CMD              HITS     MISSES   DIRTIES  READ_HIT%  WRITE_HIT%
   21881 root     app                  1024        0        0     100.0%       0.0% 

是100%缓存,但是速度很慢,可以看只HITS了1024次,每次是一页,也就是4k,总量是4MB

使用strace看一下

# strace -p $(pgrep app)
strace: Process 4988 attached
restart_syscall(<\.\.\. resuming interrupted nanosleep \.\.\.>) = 0
openat(AT_FDCWD, "/dev/sdb1", O_RDONLY|O_DIRECT) = 4
mmap(NULL, 33558528, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f448d240000
read(4, "8vq\213\314\264u\373\4\336K\224\25@\371\1\252\2\262\252q\221\n0\30\225bD\252\266@J"\.\.\., 33554432) = 33554432
write(1, "Time used: 0.948897 s to read 33"\.\.\., 45) = 45
close(4)                                = 0

调用了openat来打开磁盘分区,参数为O_RDONLY|O_DIRECT只读直接读取的方式,会绕过系统缓存

源代码是

int flags = O_RDONLY | O_LARGEFILE | O_DIRECT; 
int fd = open(disk, flags, 0755);

删除O_DIRECT

# 删除上述案例应用
$ docker rm -f app
# 运行修复后的应用
$ docker run --privileged --name=app -itd feisky/app:io-cached

在看一下

$ docker logs app
Reading data from disk /dev/sdb1 with buffer size 33554432
Time used: 0.037342 s s to read 33554432 bytes
Time used: 0.029676 s to read 33554432 bytes

看一下缓存

16:40:08 Buffers MB: 73 / Cached MB: 281 / Sort: HITS / Order: ascending
PID      UID      CMD              HITS     MISSES   DIRTIES  READ_HIT%  WRITE_HIT%
   22106 root     app                 40960        0        0     100.0%       0.0%

缓存HITS了40960次,乘以4k再除以5s的时间正好是32MB

文件在第二次读取的时候速率会提高,而磁盘不会

18 | 案例篇:内存泄漏了,我该如何定位和处理?

管理内存的问题

  • 没正确回收分配的内存,导致了泄露
  • 访问的是已分配内存边界外的地址,导致异常退出

内存泄露的内存进程不能使用,而系统也不能再进行分配

在系统没有因为OOM而杀死进程,会触发其他进程需要内存,而因为内存不足触发了缓存回收以及SWAP机制

$ docker run --name=app -itd feisky/app:mem-leak

看一下日志

$ docker logs app
2th => 1
3th => 2
4th => 3
5th => 5
6th => 8
7th => 13

这是一个打印斐波那契数列的程序

# 每隔 3 秒输出一组数据
$ vmstat 3
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 6601824  97620 1098784    0    0     0     0   62  322  0  0 100  0  0
0  0      0 6601700  97620 1098788    0    0     0     0   57  251  0  0 100  0  0
0  0      0 6601320  97620 1098788    0    0     0     3   52  306  0  0 100  0  0
0  0      0 6601452  97628 1098788    0    0     0    27   63  326  0  0 100  0  0
2  0      0 6601328  97628 1098788    0    0     0    44   52  299  0  0 100  0  0
0  0      0 6601080  97628 1098792    0    0     0     0   56  285  0  0 100  0  0 

可以看到free在降,而buffer和cache基本不变

bcc的memleak可以进行检测

# -a 表示显示每个内存分配请求的大小以及地址
# -p 指定案例应用的 PID 号
$ /usr/share/bcc/tools/memleak -a -p $(pidof app)
WARNING: Couldn't find .text section in /app
WARNING: BCC can't handle sym look ups for /app
    addr = 7f8f704732b0 size = 8192
    addr = 7f8f704772d0 size = 8192
    addr = 7f8f704712a0 size = 8192
    addr = 7f8f704752c0 size = 8192
    32768 bytes in 4 allocations from stack
        [unknown] [app]
        [unknown] [app]
        start_thread+0xdb [libpthread-2.27.so] 
$ docker cp app:/app /app
$ /usr/share/bcc/tools/memleak -p $(pidof app) -a
Attaching to pid 12512, Ctrl+C to quit.
[03:00:41] Top 10 stacks with outstanding allocations:
    addr = 7f8f70863220 size = 8192
    addr = 7f8f70861210 size = 8192
    addr = 7f8f7085b1e0 size = 8192
    addr = 7f8f7085f200 size = 8192
    addr = 7f8f7085d1f0 size = 8192
    40960 bytes in 5 allocations from stack
        fibonacci+0x1f [app]
        child+0x4f [app]
        start_thread+0xdb [libpthread-2.27.so] 

案例源代码

docker exec app cat /app.c
...
long long *fibonacci(long long *n0, long long *n1)
{
    // 分配 1024 个长整数空间方便观测内存的变化情况
    long long *v = (long long *) calloc(1024, sizeof(long long));
    *v = *n0 + *n1;
    return v;
}


void *child(void *arg)
{
    long long n0 = 0;
    long long n1 = 1;
    long long *v = NULL;
    for (int n = 2; n > 0; n++) {
        v = fibonacci(&n0, &n1);
        n0 = n1;
        n1 = *v;
        printf("%dth => %lld\n", n, *v);
        sleep(1);
    }
}
... 

child()函数调用了fibonacci而没有释放返回的内存

改为

void *child(void *arg)
{
    ...
    for (int n = 2; n > 0; n++) {
        v = fibonacci(&n0, &n1);
        n0 = n1;
        n1 = *v;
        printf("%dth => %lld\n", n, *v);
        free(v);    // 释放内存
        sleep(1);
    }
} 

# 清理原来的案例应用
$ docker rm -f app

# 运行修复后的应用
$ docker run --name=app -itd feisky/app:mem-leak-fix

# 重新执行 memleak 工具检查内存泄漏情况
$ /usr/share/bcc/tools/memleak -a -p $(pidof app)
Attaching to pid 18808, Ctrl+C to quit.
[10:23:18] Top 10 stacks with outstanding allocations:
[10:23:23] Top 10 stacks with outstanding allocations:

老版本的linux可以用valgrind,bcc需要4.1的内核

19 | 案例篇:为什么系统的Swap变高了(上)

除了OOM杀死进程进而释放内存,还有一种可能就是进行内存回收

内存回收通过直接内存回收和定期扫描的方式完成

缓存和缓冲区就可以进行被回收,以内存页的方式进行回收,大部分都可以直接回收,在需要的时候重新从磁盘读取数据

而被程序修改过的数据,还没有写入到磁盘的数据(也就是脏页),需要写入磁盘再进行释放,对于脏页有两种回收方式

  • 应用程序通过fsync写入磁盘
  • 系统的pdflush刷新脏页

通过内存映射文件页,也是可以被释放掉,在下次访问的时候再重新加载

而应用程序分配的匿名页是不能被直接回收的,这可是堆内存啊,但是由于极少被访问可以放到swap中,将这些放入到磁盘中,在访问的时候加载,/proc/sys/vm/min_free_kbytes用于设置定期回收的阀值,/proc/sys/vm/swappiness调整回收文件页和匿名页的倾向

Swap原理

休眠的时候原理也是将系统内存写入到swap中的,再次开启的时候再从swap加载到内存

kswapd0就是用于衡量内存使用情况,设置了三个内存阀值

  • 内存低于页最小阀值由内核分配内存
  • 内存低于页低阀值,高于页最小阀值,kswapd进行回收,直到高于页高阀值
  • 内存低于页高阀值,高于页低阀值,内存有一定压力,但是还能满足新内存需求
  • 内存高于页高阀值,内存剩余较多

/proc/sys/vm/min_free_kbytes可以用于设置页最小阀值,而其他阀值是以这个为基数

pages_low = pages_min*5/4
pages_high = pages_min*3/2

对于java程序一般是关闭swap的,因为gc的时候遍历所有堆内存,而由于在swap会产生大量磁盘IO

NUMA

NUMA架构下,多个处理器被分到不同的Node,每个Node下有自己的内存空间,可以分为不同的内存zone,例如内存访问区DMA,普通内存区NORMAL,伪内存MOVABLE

$ numactl --hardware
available: 1 nodes (0)
node 0 cpus: 0 1
node 0 size: 7977 MB
node 0 free: 4416 MB
...

代表一个Node0,包含2个CPU

查看/proc/zoneinfo

cat /proc/zoneinfo
...
Node 0, zone   Normal
 pages free     227894
       min      14896
       low      18620
       high     22344
...
     nr_free_pages 227894
     nr_zone_inactive_anon 11082
     nr_zone_active_anon 14024
     nr_zone_inactive_file 539024
     nr_zone_active_file 923986
...
  • page的三个值就是刚才列的三个值,free是剩余内存页,等于nr_free_pages
  • nr_zone_inactive_anonnr_zone_active_anon是活跃和非活跃的匿名页数
  • nr_zone_inactive_filenr_zone_active_file是活跃和非活跃的文件页数

当Node内存不足的时候,可以从其他node找空间,或者回收本地内存,模式可以通过/proc/sys/vm/zone_reclaim_mode来调整

  • 0,默认模式,可以从其他node找空间,或者回收本地内存
  • 1,2,4,只回收本地内存,而2是回写脏页,4表示使用swap回收

swappiness

/proc/sys/vm/swappiness用于设置回收的积极程度,是一个权重值

内存回收

缓存回收和swap回收都基于LRU算法,优先回收不常访问的内存

LRU内部维护着一个active和inactive双向列表,active记录活跃页,而inactive记录非活跃页,越接近链表的尾部,表示内存越不经常被访问

# grep 表示只保留包含 active 的指标(忽略大小写)
# sort 表示按照字母顺序排序
$ cat /proc/meminfo | grep -i active | sort
Active(anon):     167976 kB
Active(file):     971488 kB
Active:          1139464 kB
Inactive(anon):      720 kB
Inactive(file):  2109536 kB
Inactive:        2110256 kB

而OOM则是按照oom_score给进程进行排序

查看OOM日志

$ dmesg | grep -i "Out of memory"
Out of memory: Kill process 9329 (java) score 321 or sacrifice child

如果不想进程被OOM杀掉,则需要调整oom_score_adj

20

目前是没有swap被使用

$ free
             total        used        free      shared  buff/cache   available
Mem:        8169348      331668     6715972         696     1121708     7522896
Swap:             0           0           0

使用文件的方式启动swap

# 创建 Swap 文件
$ fallocate -l 8G /mnt/swapfile
# 修改权限只有根用户可以访问
$ chmod 600 /mnt/swapfile
# 配置 Swap 文件
$ mkswap /mnt/swapfile
# 开启 Swap
$ swapon /mnt/swapfile

再度查看swap

$ free
             total        used        free      shared  buff/cache   available
Mem:        8169348      331668     6715972         696     1121708     7522896
Swap:       8388604           0     8388604

模拟大文件读取

# 写入空设备,实际上只有磁盘的读请求
$ dd if=/dev/sda1 of=/dev/null bs=1G count=2048

第二个终端查看

# 间隔 1 秒输出一组数据
# -r 表示显示内存使用情况,-S 表示显示 Swap 使用情况
$ sar -r -S 1
04:39:56    kbmemfree   kbavail kbmemused  %memused kbbuffers  kbcached  kbcommit   %commit  kbactive   kbinact   kbdirty
04:39:57      6249676   6839824   1919632     23.50    740512     67316   1691736     10.22    815156    841868         4

04:39:56    kbswpfree kbswpused  %swpused  kbswpcad   %swpcad
04:39:57      8388604         0      0.00         0      0.00

04:39:57    kbmemfree   kbavail kbmemused  %memused kbbuffers  kbcached  kbcommit   %commit  kbactive   kbinact   kbdirty
04:39:58      6184472   6807064   1984836     24.30    772768     67380   1691736     10.22    847932    874224        20

04:39:57    kbswpfree kbswpused  %swpused  kbswpcad   %swpcad
04:39:58      8388604         0      0.00         0      0.00

…


04:44:06    kbmemfree   kbavail kbmemused  %memused kbbuffers  kbcached  kbcommit   %commit  kbactive   kbinact   kbdirty
04:44:07       152780   6525716   8016528     98.13   6530440     51316   1691736     10.22    867124   6869332         0

04:44:06    kbswpfree kbswpused  %swpused  kbswpcad   %swpcad
04:44:07      8384508      4096      0.05        52      1.27
  • kbcommit是当前系统负载需要的内存,保证系统内存不溢出
  • %commit就是这个的百分比
  • kbactive活跃内存
  • kbinact非活跃内存

%memused一直由24%到98%,都是kbbuffers的占用,证明内存都分配到了buffer,当内存几乎没有的时候,swap开始增大,缓冲区在小范围波动

cachetop 5
12:28:28 Buffers MB: 6349 / Cached MB: 87 / Sort: HITS / Order: ascending
PID      UID      CMD              HITS     MISSES   DIRTIES  READ_HIT%  WRITE_HIT%
   18280 root     python                 22        0        0     100.0%       0.0%
   18279 root     dd                  41088    41022        0      50.0%      50.0%

dd的读写命中率都只有50%,就是dd导致的内存缓冲区升高

# -d 表示高亮变化的字段
# -A 表示仅显示 Normal 行以及之后的 15 行输出
$ watch -d grep -A 15 'Normal' /proc/zoneinfo
Node 0, zone   Normal
  pages free     21328
        min      14896
        low      18620
        high     22344
        spanned  1835008
        present  1835008
        managed  1796710
        protection: (0, 0, 0, 0, 0)
      nr_free_pages 21328
      nr_zone_inactive_anon 79776
      nr_zone_active_anon 206854
      nr_zone_inactive_file 918561
      nr_zone_active_file 496695
      nr_zone_unevictable 2251
      nr_zone_write_pending 0

可以看到free在一个范围波动,就是因为swap进行了回收释放

如果多次运行dd,有时候 swap用的多,而有时候swap用的少但是缓冲区波动较大,这就是系统回收的时候有时候回收的文件页,有时候回收的内存页

查看进程的vmswap可以到/proc/pid/status

# 按 VmSwap 使用量对进程排序,输出进程名称、进程 ID 以及 SWAP 用量
$ for file in /proc/*/status ; do awk '/VmSwap|Name|^Pid/{printf $2 " " $3}END{ print ""}' $file; done | sort -k 3 -n -r | head
dockerd 2226 10728 kB
docker-containe 2251 8516 kB
snapd 936 4020 kB
networkd-dispat 911 836 kB
polkitd 1004 44 kB

也可以直接将进程按照swap使用量排序显示

smem --sort swap

关闭swap

swapoff -a

关闭swap再开启swap也可以很好回收内存

延迟敏感的应用如果运行在swap服务器可以使用库函数mlock()和mlockall()锁定内存,防止swap换出

21 | 套路篇:如何“快准狠”找到系统内存的问题?

内存可能涉及到的问题

  • 已用内存和剩余内存
  • 共享内存tmpfs
  • 可用内存(新进程可以使用的最大内存,包括剩余内存和可回收资源)
  • 缓存,包括页缓存,SLAB可回收内存
  • 缓冲区,对原始磁盘块的临时数据,缓存将要写入磁盘的数据
  • 虚拟内存,包括代码段,数据段,共享内存,已申请的堆内存和已换出内存,已分配未分配物理内存的部分
  • 常驻内存,进程实际使用内存,不包括Swap和共享内存
  • 共享内存

除此之外还有缺页内存

  • 次缺页异常,从内存中分配
  • 主缺页异常,需要IO或者swap接入

常用命令

  • free
  • proc文件系统
  • vmstat 动态看内存变化,区分缓存和缓冲区
  • cachestat 查看进程缓存命中
  • sar 查看换页状态
  • memleak 检查内存泄漏

获取每个进程用到的实际内存

# 使用 grep 查找 Pss 指标后,再用 awk 计算累加值
$ grep Pss /proc/[1-9]*/smaps | awk '{total+=$2}; END {printf "%d kB\n", total }'
391266 kB