Linux性能优化实战笔记(内存篇)
Linux内存是怎么工作的?
内存映射
- 每个进程会有对应的虚拟地址空间
- 虚拟地址空间包括:用户空间(128TB,64位系统)和内核空间(128TB,64位系统)
- 内存映射,其实就是将虚拟内存地址映射到物理内存地址。为了完成内存映射,内核为每个进程都维护了一张页表,记录虚拟地址与物理地址的映射关系
- 页表实际上存储在 CPU 的内存管理单元 MMU 中
- 当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行
- MMU 并不以字节为单位来管理内存,而是规定了一个内存映射的最小单位,也就是页,通常是4KB大小
解决页表过大问题:多级页表
- 把内存分成区块来管理,将原来的映射关系改成区块索引和区块内的偏移
- Linux 用的是四级页表来管理内存页,虚拟地址被分为5个部分,前4个表项用于选择页,而最后一个索引表示页内偏移
虚拟内存空间分布
用户空间内存,地址空间从低到高分别是五种不同的内存段
- 只读段:包括代码和常量等
- 数据段:包括全局变量等
- 堆:包括动态分配的内存,从低地址开始向上增长
- 文件映射段:包括动态库、共享内存等,从高地址开始向下增长
- 栈:包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB
- 堆和文件映射段的内存是动态分配的
内存分配与回收
- malloc()是C标准库提供的内存分配函数,对应到系统调用上有两种实现方式,即brk()和mmap()
对小块内存(小于 128K),C 标准库使用 brk() 来分配
- 通过移动堆顶的位置来分配内存
这些内存释放后并不会立刻归还系统,而是被缓存起来重复使用
- 可以减少缺页异常的发生,提高内存访问效率
- 在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片
而大块内存(大于 128K),则直接使用内存映射 mmap() 来分配
在文件映射段找一块空闲内存分配
- 每次 mmap 都会发生缺页异常
- 在内存工作繁忙时,频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大
- 当这两种调用发生后,其实并没有真正分配内存。这些内存,都只在首次访问时才分配,也就是通过缺页异常进入内核中,再由内核来分配内存
- 调用 free() 或 unmap() ,来释放不用的内存
内存回收方式
- 回收缓存:比如使用 LRU(Least Recently Used)算法,回收最近使用最少的内存页面
回收不常访问的内存:把不常用的内存通过交换分区SWAP直接写到磁盘中
- Swap 其实就是把一块磁盘空间当成内存来用
杀死进程:内存紧张时系统还会通过 OOM(Out of Memory),直接杀掉占用大量内存的进程
监控进程的内存使用情况,并且使用 oom_score 为每个进程的内存使用情况进行评分
- 一个进程消耗的内存越大,oom_score 就越大
- 一个进程运行占用的 CPU 越多,oom_score 就越小
- 进程的 oom_score 越大,代表消耗的内存越多,也就越容易被 OOM 杀死
可以手动设置进程的 oom_adj ,从而调整进程的 oom_score
- oom_adj 的范围是 [-17, 15],数值越大,表示进程越容易被 OOM 杀死
- echo -16 > /proc/$(pidof sshd)/oom_adj
查看内存使用情况
free
- 第一列,total 是总内存大小
- 第二列,used 是已使用内存的大小,包含了共享内存
- 第三列,free 是未使用内存的大小
- 第四列,shared 是共享内存的大小
- 第五列,buff/cache 是缓存和缓冲区的大小
最后一列,available 是新进程可用内存的大小
- available 不仅包含未使用内存,还包括了可回收的缓存,所以一般会比未使用内存更大
top
- VIRT 是进程虚拟内存的大小,只要是进程申请过的内存,即便还没有真正分配物理内存,也会计算在内
- RES 是常驻内存的大小,也就是进程实际使用的物理内存大小,但不包括 Swap 和共享内存
- SHR 是共享内存的大小,比如与其他进程共同使用的共享内存、加载的动态链接库以及程序的代码段等
- %MEM 是进程使用物理内存占系统总内存的百分比
怎么理解内存中的Buffer和Cache?
Buffers
- Buffers 是对原始磁盘块的临时存储,也就是用来缓存磁盘的数据,通常不会特别大(20MB 左右)
Cached
- Cached 是从磁盘读取文件的页缓存,也就是用来缓存从文件读取的数据
SReclaimable
- Slab 包括两部分, SReclaimable 是可回收部分; SUnreclaim是不可回收部分
- Buffer 是对磁盘数据的缓存,而 Cache 是文件数据的缓存,它们既会用在读请求中,也会用在写请求中
Buffer/Cache用处
- 从写的角度来说,不仅可以优化磁盘和文件的写入,对应用程序也有好处,应用程序可以在数据真正落盘前,就返回去做其他工作。
- 从读的角度来说,既可以加速读取那些需要频繁访问的数据,也降低了频繁 I/O 对磁盘的压力。
磁盘&文件
- 磁盘是一个块设备,可以划分为不同的分区
- 在分区之上再创建文件系统,挂载到某个目录,之后才可以在这个目录中读写文件
- 在读写普通文件时,会经过文件系统,由文件系统负责与磁盘交互;而读写磁盘或者分区时,就会跳过文件系统,也就是所谓的“裸I/O“
【案例】如何利用系统缓存优化程序的运行效率?
缓存命中率
- 命中率越高,表示使用缓存带来的收益越高,应用程序的性能也就越好。
查看系统缓存命中情况的工具:cachestat,cachetop
- 工具安装:yum install bcc-tools,且要求kernel版本在4.1以上
- 安装完成后,手动设置PATH目录:export PATH=$PATH:/usr/share/bcc/tools
cachestat
- 提供了整个操作系统缓存的读写命中情况
- TOTAL ,表示总的 I/O 次数
- MISSES ,表示缓存未命中的次数
- HITS ,表示缓存命中的次数
- DIRTIES, 表示新增到缓存中的脏页数
- BUFFERS_MB 表示 Buffers 的大小,以 MB 为单位
- CACHED_MB 表示 Cache 的大小,以 MB 为单位
cachetop
- 提供了每个进程的缓存命中情况
- READ_HIT 和 WRITE_HIT ,分别表示读和写的缓存命中率
查看文件缓存
使用pcstat工具(前提需要安装go语言)
安装完go之后,执行以下命令:
- export GOPATH=~/go
- export PATH=~/go/bin:$PATH
- go get golang.org/x/sys/unix
- go get github.com/tobert/pcstat/pcstat
- 命令:pcstat <file_url>
【案例】内存泄漏了,我该如何定位和处理?
内存可能出现的问题
- 内存泄漏。没正确回收分配后的内存,导致不仅应用程序自己不能访问,系统也不能把它们再次分配给其他应用。
- 越界访问。访问的是已分配内存边界外的地址,导致程序异常退出
用户空间内存是否会泄漏
- 只读段,包括程序的代码和常量,由于是只读的,不会再去分配新的内存,所以也不会产生内存泄漏。
- 数据段,包括全局变量和静态变量,这些变量在定义时就已经确定了大小,所以也不会产生内存泄漏。
- 内存映射段,包括动态链接库和共享内存,其中共享内存由程序动态分配和管理。所以,如果程序在分配后忘了回收,就会导致跟堆内存类似的泄漏问题。
检测内存泄漏工具:memleak
- 是bcc-tools内的工具之一
- 命令:memleak -p
-a
Swap概念
- 系统内存紧张的处理方式:内存回收、OOM杀死进程
可回收内存的类型:文件页(Buffer和Cache)、匿名页(应用程序动态分配的堆内存)
文件页,可直接回收。若数据暂时还未写入磁盘(脏页),则回收前先写入磁盘再回收
- 应用程序中,通过系统调用 fsync,把脏页同步到磁盘
- 内核线程 pdflush 负责这些脏页的刷新
- 匿名页,不能直接回收。需要使用swap,将数据写入磁盘中,然后释放内存给其它进程使用。当需要使用这些数据时,再从磁盘中读取即可
swap原理
Swap 说白了就是把一块磁盘空间或者一个本地文件(以下讲解以磁盘为例),当成内存来使用。
- 换出,就是把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存
- 换入,则是在进程再次访问这些内存的时候,把它们从磁盘读到内存中来
系统回收内存的两种方式:直接回收、内核线程定期回收
- 直接回收:当有新的大块内存分配请求,但是剩余内存不足。这个时候系统就需要回收一部分内存(Buffer、Cache),进而尽可能地满足新内存请求
内核线程定期回收
- 定期回收内存的内核线程:kswapd0
定义了三个阈值:页最小阈值(pages_min)、页低阈值(pages_low)和页高阈值(pages_high)
- 剩余内存小于页最小阈值,说明进程可用内存都耗尽了,只有内核才可以分配内存
- 剩余内存落在页最小阈值和页低阈值中间,说明内存压力比较大,剩余内存不多了。这时 kswapd0 会执行内存回收,直到剩余内存大于高阈值为止
- 剩余内存落在页低阈值和页高阈值中间,说明内存有一定压力,但还可以满足新内存请求
- 剩余内存大于页高阈值,说明剩余内存比较多,没有内存压力
- 查看页最小阈值:cat /proc/sys/vm/min_free_kbytes
其它阈值由最小阈值通过公式计算得出
- pages_low = pages_min*5/4
- pages_high = pages_min*3/2
NUMA和Swap
- 在 NUMA 架构下,多个处理器被划分到不同 Node 上,且每个 Node 都拥有自己的本地内存空间
- 而同一个 Node 内部的内存空间,实际上又可以进一步分为不同的内存域(Zone),比如直接内存访问区(DMA)、普通内存区(NORMAL)、伪内存区(MOVABLE)等
使用numactl工具,查看每个node的内存使用情况
- 命令:numactl --hardware
查看每个node的各个swap阈值等信息
- 命令:cat /proc/zoneinfo
swappiness
- 直接回收和Swap两种回收方式,系统如何选择?
- Linux 提供了一个 /proc/sys/vm/swappiness 选项,用来调整使用 Swap 的积极程度
- swappiness 的范围是 0-100,数值越大,越积极使用 Swap,也就是更倾向于回收匿名页;数值越小,越消极使用 Swap,也就是更倾向于回收文件页
【总结】如何“快准狠”找到系统内存的问题
内存性能指标
系统维度
- 已使用内存(Used),即已经使用的内存,包含了共享内存
- 未使用内存(free),即未使用的内存
- 可用内存(available),表示进程可以使用的最大内存,其包括未使用内存和可回收缓存
- 共享内存(shared),其允许两个不相关的进程访问同一个逻辑内存。通过 tmpfs 实现,所以它的大小也就是 tmpfs 使用的内存大小
缓存&缓冲区(Cache&Buffer)
- 缓存包括两部分:一部分是磁盘读取文件的页缓存,另一部分,是 Slab 分配器中的可回收内存
- 缓冲区是对原始磁盘块的临时存储,用来缓存将要写入磁盘的数据
进程维度
- 虚拟内存(VIRT),进程申请过的内存,即便还没有真正分配物理内存也会计算在内。包括:只读段、数据段、堆、文件映射段、栈等等。
- 常驻内存(RES),进程实际使用的物理内存大小,不包括 Swap 和共享内存。
- 共享内存(SHR),既包括与其他进程共同使用的真实的共享内存,还包括了加载的动态链接库以及程序的代码段等
- 实际使用的物理内存(PSS),比例分配共享库占用的内存
- 进程独自占用的物理内存(USS),不包含共享库占用的内存
缺页异常
系统调用内存分配请求后,不会立刻为其分配物理内存,而是在请求首次访问时通过缺页异常来分配
- 可以直接从物理内存中分配时,被称为次缺页异常
- 需要磁盘 I/O 介入(比如 Swap)时,被称为主缺页异常
Swap内存
- 已用空间和剩余空间很好理解,即已经使用和没有使用的内存空间
- 换入和换出速度,表示每秒钟换入和换出内存的大小
内存性能工具
系统整体维度
- free
- vmstat
- cachestat
- sar
进程维度
- top
- ps
- pidstat
- cachetop
- memleak
分析思路
- 先用 free 和 top,查看系统整体的内存使用情况
- 再用 vmstat 和 pidstat,查看一段时间的趋势,从而判断出内存问题的类型
- 最后进行详细分析,比如内存分配分析、缓存 / 缓冲区分析、具体进程的内存使用分析等
优化思路
- 最好禁止 Swap。如果必须开启 Swap,降低 swappiness 的值,减少内存回收时 Swap 的使用倾向
- 减少内存的动态分配。比如,可以使用内存池、大页(HugePage)等
- 尽量使用缓存和缓冲区来访问数据。比如,可以使用堆栈明确声明内存空间,来存储需要缓存的数据;或者用 Redis 这类的外部缓存组件,优化数据的访问
- 使用 cgroups 等方式限制进程的内存使用情况。这样,可以确保系统内存不会被异常进程耗尽
- 通过 /proc/pid/oom_adj ,调整核心应用的 oom_score。这样,可以保证即使内存紧张,核心应用也不会被 OOM 杀死