极客时间专栏——linux性能优化实战——内存部分
目录:
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_HIT
和WRITE_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_anon
和nr_zone_active_anon
是活跃和非活跃的匿名页数nr_zone_inactive_file
和nr_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