前言
因為在做nodejs程序的性能分析的時候,了解到了Perf和FlameGraph這兩個神奇的工具,接著就知道了Brendan D. Gregg這個大神,跪著拜讀了他的博客和他寫的System Performance。從前寫程序和調優只知道從設計的思路去思考,讀完大神的文章,感覺真的給自己打開了一個全新的世界。
把自己的程序看做一個黑盒,它運行的時候到底占多少內存,多少CPU?這個問題看起來不難,那么它多少時間在等待I/O,多少時間在計算,如果CPU是多核,它是否很好的平衡了負載?它訪問文件系統頻率多少,每次的相應時間多少?它運行中的熱點是哪里,瓶頸是哪里?
再復雜點,它在運行時內存是否足夠,page cache和CPU cache命中率如何?有沒有導致系統發生swap等非常耗時的操作?現在如果程序運行不穩定,如何去觀察定位和調優?
目前為止我還在每天跪著閱讀大神的文章中,這篇筆記會按一定順序記錄我的很多理解。
Perf
Perf是一個神奇的工具,主要用于事件監測。
每當linux內核調用某一個函數時,可以視作一個事件,Perf可以記錄這些事件發生的時間和內核調用棧。
基本用法
perf command [options] [execute]
舉個例子:
perf stat -e sched:sched_switch -a sleep 5
統計5s之內,操作系統一共調用了多少個sched_switch。對linux熟悉的朋友應該知道這個表示進程切換。下面我的虛擬機里返回的結果
Performance counter stats for 'system wide':
1,170 sched:sched_switch
5.001593514 seconds time elapsed
也就是5秒鐘之內發生了1170次進程切換。
這里解釋下這個命令,perf stat是統計事件次數,-e sched:sched_switch表示統計sched:sched_switch事件,然后本來應該只統計sleep 5這個process內部的事件的,加上-a表示統計整個系統內的事件,由于sleep 5要在5s后結束,所以這個命令的實際功能就是統計了系統5s內發生的sched:sched_switch事件個數。
再舉個栗子:
perf record -e block:block_rq_complete --filter 'nr_sector > 200'
block_rq_complete是塊設備請求完成的事件,該條命令會統計所有涉及到200扇區以上的設備I/O事件。
命令:
perf record -e page-faults -ag
則會統計所有缺頁中斷。
事件列表
調用perf list (注意root權限可以看到更多)可以查看當前支持的事件列表。
List of pre-defined events (to be used in -e):
alignment-faults [Software event]
bpf-output [Software event]
context-switches OR cs [Software event]
cpu-clock [Software event]
cpu-migrations OR migrations [Software event]
dummy [Software event]
emulation-faults [Software event]
major-faults [Software event]
minor-faults [Software event]
page-faults OR faults [Software event]
task-clock [Software event]
L1-dcache-load-misses [Hardware cache event]
L1-dcache-loads [Hardware cache event]
L1-dcache-stores [Hardware cache event]
L1-icache-load-misses [Hardware cache event]
branch-load-misses [Hardware cache event]
branch-loads [Hardware cache event]
dTLB-load-misses [Hardware cache event]
dTLB-loads [Hardware cache event]
dTLB-store-misses [Hardware cache event]
dTLB-stores [Hardware cache event]
iTLB-load-misses [Hardware cache event]
iTLB-loads [Hardware cache event]
cycles-ct OR cpu/cycles-ct/ [Kernel PMU event]
cycles-t OR cpu/cycles-t/ [Kernel PMU event]
el-abort OR cpu/el-abort/ [Kernel PMU event]
el-capacity OR cpu/el-capacity/ [Kernel PMU event]
Perf可以監控的事件類型很多,甚至連L1 cache miss這種都可以。
Perf 命令
-
perf stat
stat命令用于簡單統計次數- 統計PID進程的事件
perf stat -p PID - 統計整個系統的事件
perf stat -a sleep [seconds] - 統計command內的事件
perf stat [command]
- 統計PID進程的事件
-
perf record & report/script
stat命令只會統計事件發生次數,如果想查看更詳細的信息,比如事件發生時的堆棧,就需要用到record命令了。
perf record把統計結果放到當前目錄內perf.data文件,用perf report/script命令可以解析展示統計結果。- 以固定頻率對程序進行抽樣,并且記錄堆棧
perf record -F [freq] [command] [-p PID] -g -- sleep [sec]
//-g 表示統計stack, sec表示統計時長
例如
perf record -F 99 -a -g -- sleep 2 //統計兩秒內的默認事件 (cpu clock事件) perf script swapper 0 [000] 19822.660852: 10101010 cpu-clock: 7fff810665d6 native_safe_halt ([kernel.kallsyms]) 7fff8103ad7e default_idle ([kernel.kallsyms]) 7fff8103b58f arch_cpu_idle ([kernel.kallsyms]) 7fff810c5faa default_idle_call ([kernel.kallsyms]) 7fff810c6311 cpu_startup_entry ([kernel.kallsyms]) 7fff818239dc rest_init ([kernel.kallsyms]) 7fff81f5d011 start_kernel ([kernel.kallsyms]) 7fff81f5c339 x86_64_start_reservations ([kernel.kallsyms]) 7fff81f5c485 x86_64_start_kernel ([kernel.kallsyms]) swapper 0 [001] 19822.660853: 10101010 cpu-clock: 7fff810665d6 native_safe_halt ([kernel.kallsyms]) 7fff8103ad7e default_idle ([kernel.kallsyms]) 7fff8103b58f arch_cpu_idle ([kernel.kallsyms]) 7fff810c5faa default_idle_call ([kernel.kallsyms]) 7fff810c6311 cpu_startup_entry ([kernel.kallsyms]) 7fff810536e4 start_secondary ([kernel.kallsyms]) swapper 0 [001] 19822.671003: 10101010 cpu-clock: 7fff810665d6 native_safe_halt ([kernel.kallsyms]) 7fff8103ad7e default_idle ([kernel.kallsyms]) 7fff8103b58f arch_cpu_idle ([kernel.kallsyms]) 7fff810c5faa default_idle_call ([kernel.kallsyms]) 7fff810c6311 cpu_startup_entry ([kernel.kallsyms]) 7fff810536e4 start_secondary ([kernel.kallsyms]) swapper 0 [000] 19822.671003: 10101010 cpu-clock: 7fff810665d6 native_safe_halt ([kernel.kallsyms]) 7fff8103ad7e default_idle ([kernel.kallsyms]) 7fff8103b58f arch_cpu_idle ([kernel.kallsyms]) 7fff810c5faa default_idle_call ([kernel.kallsyms])
- 以固定頻率對程序進行抽樣,并且記錄堆棧
由于我統計期間沒有做任何事,所以每次時鐘中斷發生時,CPU都停留在native_safe_halt函數這里。
nodejs對perf的支持
由于nodejs等虛機語言通常采用了JIT技術在運行時改寫函數,其堆棧符號表會在運行時變動。
為了能讓perf命令監測運行過程,nodejs提供了--perf_basic_prof參數,當加上此參數運行時,node會在/tmp目錄下生成perf-PID.map文件,里面給出了地址到函數名稱的映射。
perf record通過-p PID抽樣程序時,會自動去/tmp目錄下找對應的map文件并加載。
FlameGraph
查看perf script的報告仍然不夠直觀,大神給出了一個工具,可以將perf統計結果轉化成SVG圖,并且會將相同的堆棧合并,這樣可以很直觀的看出來程序在那些調用上花費了大量時間。
Markdown好像不支持SVG,只能截個圖表示效果,實際的SVG是可以交互的了解具體細節。
這樣對程序的運行過程會有非常直觀的了解。
用FlameGraph調查nodejs程序內存使用
接下來是一個用FlameGraph來檢查nodejs程序的例子。
- 啟動nodejs程序,帶上 --perf-basic-prof 選項
這里我啟動了一個簡單的deeplearning程序。
node --perf-basic-prof main.js &
進程ID為25586,于是node會在/tmp/目錄下生成/tmp/perf-25586.map文件,其實文件內容就是地址和函數名的映射表。 - 啟動perf監控程序
這里我監控所有的缺頁中斷程序
perf record -e kmem:mm_page_alloc -g -p 25586
監控了一段時間后就Ctrl+c斷開監控,此時目錄下生成了perf.data文件。 - 輸出perf記錄
調用perf script把結果存在一個臨時文件中
perf script > node.tmp - 調用Flamegraph工具將其生成SVG熱點圖
stackcollapse-perf.pl node.tmp | flamegraph.pl > flamegrapsh.svg -
用瀏覽器打開svg就可以看到熱點圖了
flame_graph
可以查看具體細節:
可以看到Rect.junc函數導致了大量的ProcessOldToNewSlot,緊接著導致了大量的缺頁中斷,接下來就可以調研下ProcessOldToNewSlot是什么,以及如何可以避免這種情況了。
ftrace
ftrace是linux提供的一個tracing工具,同perf一樣可以監控很多系統的事件。
ftrace
Dtrace & Systemtap
==========
比起Perf,Dtrace和Systemtap更為強大,它們除了可以檢測事件之外,還可以在事件發生時運行指定的命令去調查更詳細的信息,比如函數參數等。
Dtrace貌似在ubuntu上的支持并不好,接下來我會花些時間去學習Systemtap。
Linux系統提供的監測工具
vmstat
vmstat提供了關于系統虛擬內存的使用統計信息。
用法是
vmstat [options] [delay [count]]
delay表示刷新頻率,count表示統計次數。
重點是options
options
- a
統計active和inactive memory,正被程序引用的page為active。 - m, slabs
統計slab系統信息 - s
展示一些計數信息, - d
統計硬盤信息
vmstat 統計的信息包括
-
進程
- r
狀態為RUNNABLE的進程,運行中或者等待CPU。 - b
狀態為UNINTERRUPTIBLE SLEEP的進程,一般是在等待I/O操作。
- r
-
內存
- swpd
使用的swap內存 - free
空閑內存數量 - buff
被用作buffers的內存數量 - cache
被用作cache的內存數量 - inact
- active
inactive/active內存數量
- swpd
-
Swap
- si
從disk swap進來的內存數量(每秒) - so
換出到disk的內存數量(每秒)
- si
-
I/O
- bi
每秒收到的blocks數量 - bo
每秒發送出去的blocks數量
- bi
-
system
- in
每秒收到的interrupt數量,包括時鐘中斷 - cs
每秒的context switch數量
- in
-
cpu
這里統計的是百分比,按CPU核數平均之后的結果- us
非內核態時間占比 - sy
內核態時間占比 - id
空閑時間占比,注意這里包括了wa時間 - wa
等待I/O時間占比 - st
Time stolen from virtual machine
- us
-
Disk Mode
- Reads
total: 完成的Read總數
merged: 被merge的Read次數
sectors: 完成的Read sector數量
ms: 花費在read上的時間總數 - Writes
total: 完成的Write總數
merged: 被合并的Write次數
sectors: 完成的write扇區總數
ms: 花費在write上的時間總數 - IO
cur: 正在進行的I/O
s: 花費在I/O上的時間
- Reads
使用vmstat的時候,有幾個很重要的概念需要理清楚:
- active/inactive memory
active memory指的是被某個process使用在的memory。
inactive memory指的是被曾經運行的process使用的memory - buffer/cache
有關buffer和cache的區別,我到現在還沒有完全弄清楚,就目前的理解而言,buffer是供給I/O來保存傳輸數據塊的page,而cache是用來做文件內容緩存的page。
iostat
iostat統計了系統運行的一些io數據。
iostat [options] [interval [count]]
重點仍然是options
options
- c
展示CPU統計報告 - d
展示device統計報告 - x
展示擴展統計信息(很有用) - z
忽略不活躍device
iostat展示的信息包括
- CPU報告
- %user
平均后的用戶態時間占比 - %system
平均后的內核態時間占比 - %iowait
io等待時間的占比 - %idle
空閑時間占比
- %user
- Device報告
- Device:
device name - tps:
transfers per second,每秒傳輸請求次數 - Blk_read/s (kB_read/s, MB_read/s):
每秒讀取Block數量或者數據量 - Blk_wrtn/s (kB_wrtn/s, MB_wrtn/s):
每秒寫入的Block數量或者數據量 - rrqm/s:
每秒merged read request數量 - wrqm/s:
每秒merged write request數量 - r/s:
merge之后的每秒read request數量 - w/s:
merge之后的每秒write request數量 - rsec/s (rkB/s, rMB/s):
每秒讀入數據量 - wsec/s (wkB/s, wMB/s):
每秒寫入數據量 - avgrq-sz:
平均每個request的size,一般以sector為單位,每個sector是512字節
所以一般來說:(avgrq-sz * 0.5 * (r/s + w/s) = rkB/s + wkB/s) - avgqu-sz:
device的request queue的平均長度 - await:
平均I/O時間,從發出request到request完成 - r_await: w_await:
r/w 平均等待時間 - %util:
I/O時間占比,也就是整個系統有多少的時間是處于I/O中,當100%時,該device就接近飽和了
- Device:
mpstat
vmstat,iostat給出的都是根據CPU核數平均之后的數據,mpstat可以統計per kernel的數據。
vmstat [options] [delay [count]]
options
- A
顯示每個核的統計信息 - I
顯示中斷統計 - u
顯示CPU統計數據
一般的用法是
mpstat -p ALL [interval]
mpstat報告內容
- CPU統計信息
- CPU
CPU核序號 - %usr
- %sys
- %iowait
- %irq
CPU服務硬件中斷時間占比 - %soft
CPU服務軟中斷占比
- CPU
uptime
uptime主要是以15,5,1分鐘為單位統計了過去這段時間CPU的負荷。
free
free是查看當前內存使用量的工具,它可以查看:
- total
- used
- free
- shared
- buff/cache
- available
total = used + free + buff/cache
ps
ps命令會按進程為單位顯示一些統計數據。事實上ps命令的實現就是去proc文件系統查詢對應的數據。
- %CPU
- %MEM
- VSZ
virtual size - RSS
resident set size
pmap
proc文件系統
proc文件系統是linux提供的內核文件系統,在linux源碼Documentation/filesystems/proc.txt里有詳細介紹。
proc根目錄
proc/[PID]目錄
cmdline
cmdline給出了該process的運行命令。
/usr/lib/at-spi2-core/at-spi2-registryd^@--use-gnome-session^@
以^@(null)分割
cwd
指向當前工作目錄的軟鏈接
environ
環境變量,同cmdline一樣是null分隔的字符串
exe
指向執行程序的軟鏈接
fd
文件描述符目錄,里面是全部file descriptor
maps
maps文件描述了程序的線性地址映射列表,看這個文件可以了解到程序當前的地址空間分布
mem
root
stat
stat文件很有意思,就是一列數字,具體的含義需要去查文檔。
statm
statm提供了進程的內存統計數據,包括:
- total memory
- resident set size
- shared pages
status
status提供了很多進程的監測數據,其中比較有用的有:
- FDSize
當前的file descriptor數量 - VmSize
進程線性空間大小 - VmRss
進程實際占用物理內存大小 - VmPeak
進程線性空間大小峰值
<em>
當進程向linux系統請求一些內存空間的時候,linux系統并不會立刻給進程分配物理頁面,它只是做了一個mark,增加了一下進程的線性空間(vm_area_struct),表示這個進程又多了一塊可訪問地址,這些值會被統計在VmSize里,因此VmSize表示進程邏輯上的內存大小。
當進程真正訪問到請求的地址時,linux才會因為page fault去給進程真正分配物理page,這個實際分配的大小記錄在VmRss里。同樣,當進程內存不夠用時,系統可能將其它進程的物理page斷開然后swap到交換設備上,這時候,其它進程的VmRss是減小的,參見stackoverflow上的回答。
</em>
pagemap
meminfo
meminfo里有內存的很多信息
diskstats
diskstats文件包含了以下數據:
1 - major number
2 - minor mumber
3 - device name
4 - reads completed successfully
5 - reads merged
6 - sectors read
7 - time spent reading (ms)
8 - writes completed
9 - writes merged
10 - sectors written
11 - time spent writing (ms)
12 - I/Os currently in progress
13 - time spent doing I/Os (ms)
14 - weighted time spent doing I/Os (ms)
根據這些數據可以猜想iostat沒準就是通過/proc/diskstats文件來計算監測數據的,strace iostat果然證明了這個猜想
loadavg
看名字也能看出來大概是說啥了
性能分析方法:
CPU
CPU分析太細級別的不一定有特別大的意義,統計工具有:
uptime
觀察CPU load average = number of [runnable, uninterruptable] processesvmstat
vmstat提供了user,system以及id的時間比率,以及r(run queue)的長度mpstat
mpstat -P ALL可以觀測每一個CPU核的統計數據,如果很不均勻,可以考慮多線程來提高CPU利用率pidstat
pidstat根據CPU或者進程來統計使用情況。-
time
/usr/bin/time,注意不是直接的time,加上-v可以顯示一個程序的統計信息。包括- Majo (I/O) page faults
- Minor (reclaiming a frame) page faults
- Swaps
要理解上述參數可以參閱, Minor page faults表示程序訪問了可以復用的page,而Major page faults表示程序訪問了需要通過I/O調入的page。
一個簡單的實驗就是調用兩次/usr/bin/time -v firefox,第一次調用啟動時間較長,400多個major page faults, 35481個minor,第二次調用就快多了,而且是0個major page faults, 34434個minor。因為linux page cache系統緩存了firefox的執行文件內容。 htop
htop也是一個實時監測工具,可以用來初步判斷問題所在。-
getdelay.c
linux源代碼里Document目錄下有一個getdelay.c,編譯后可以通過-p PID的方式來讀取某一個程序的delay信息,包括:- Scheduler Latency
進程花了多少時間等待CPU調度 - Block I/O
進程花了多少時間等待I/O完成 - Swapping
進程花了多少時間等待頁面調入 - Memory reclaim
進程花了多少時間等待cache頁面分配
- Scheduler Latency
profiling
通過perf,systemtap等profiling工具,可以查看CPU熱點,分析CPU耗費在哪里。
perf sched還可以統計scheduler latency,也就是進程切換導致的延時。(這個值我現在讀的有點疑問)
B神提到user:system CPU時間比反應了這個程序的類型,高user time說明是計算密集型。
在B神的業務服務器上(IO密集型,大概100K syscall每秒),一般負載系數在2到8(線程數/cpu核數),user/system時間比大概是60/40。
Memory
Memory監測一般是跟使用語言綁定,不過也有一些從系統層級觀測memory使用情況的工具。
概念
了解Memory工具前最好先了解linux系統里的幾個概念:
- Main Memory
也就是物理內存 - Virtual Memory
進程所看到的線性地址內存 - Resident Memory
有實際物理內存對應的virtual memory - Anonymous Memory
沒有文件系統page對應的內存,一般就是進程的數據部分,包括stack和heap - Paging
主存和存儲設備之間的page轉移 - Anonymous Paging
Anonymous Paging意味著把進程的stack或者heap swap出去,當再次運行的時候又會需要把它們從磁盤上讀回來,這是非常hurting的現象 - Page States
- Unallocated
- Allocated, unmapped
- Allocated, mapped to main memory
- Allocated, mapped to swap device
- Linux Page System
當一個page request來的時候,linux按照以下順序分配內存page- Free List
free page列表 - Page Cache
從文件系統用作cache的page中分配 - Swapping
kswapd系統線程通過swap一些page出去來騰出空間 - OOM Killer
殺死一些進程來空出空間 - Page回收
Linux將page分為以下幾類:- 不可回收頁:
空閑頁,保留頁(PG_reserved),內核分配頁,進程內核態堆棧頁,臨時鎖定頁(PG_locked) - 可交換頁
用戶態anonymous頁(堆棧,堆),回收時將內容保存到交換區 - 可同步頁
用戶態地址空間(映射文件),有對應磁盤文件頁,回收時需要做同步操作
- 不可回收頁:
- Free List
檢測工具及方法
- vmstat
- swpd
總共swap出去的memory - free
當前free memory - si, so
swapped in swapped out memory,這兩個值反應了系統memory壓力
- swpd
- slabtop
slabtop可以了解linux kernel slab系統的內存使用情況 - Systemtap, perf
這些tracer可以監控系統事件了解內存使用情況
File System
同樣,首先需要大致了解一些File system的概念
File system
-
Page Cache
Linux使用統一的Page Cache系統來做磁盤和塊設備的Cache。Page Cache中的page內包含的內容可能有:- 普通文件的內容,page cache通過文件inode中的address_space結構對應起來
- 目錄內容,linux像處理普通文件一樣處理目錄文件
- 從塊設備直接讀出的內容(繞過文件系統)
- 用戶process被swap out的內容,雖然被swap out,但是有些內容會被先cache起來
- 特殊的文件系統文件,比如shm文件系統
-
Linux系統是如何讀取一個文件的(非Direct I/O, Memory Mapping, Asynchronous)
- read調用,最后到generic_file_read(filep, buf, count, ppos),參數分別表示文件句柄,存放內容的數組,讀取內容數量和起始偏移量。
- generic_file_read初始化一個iovec,存放buf和count和一個kiocb,用來控制I/O過程,然后call __generic_file_aio_read:
- 檢查緩沖區有效
- 建立一個read_descriptor_t,表示讀取操作狀態
- do_generic_file_read(filp, ppos, read_descriptor_t, &file_read_actor)
- do_generic_file_read會執行實際的拷貝,過程如下:
- 取得filep->f_mapping,address_space對象
- 將文件看過頁數組,算出起止序號
- 循環讀入頁,(read_page方法):
* 處理預讀頁 * 在頁緩存中尋找頁,找不到則申請空白頁 * 在頁緩存中找到頁的話,讀取結束 * 調用address_space->readpage方法讀取數據到頁緩存 * 調用file_read_actor把數據拷貝到用戶緩存
- 修改文件inode的update_atime,mark this inode dirty
<em>
read_page方法:
普通文件read_page: 首先計算文件在磁盤上的塊號,如果是連續的,發出一個block I/O請求,否則用一次一塊的方法讀取。
塊設備文件read_page: 將page看做塊緩沖區,逐塊讀取。
</em>
-
Linux系統如何寫入一個文件
- generic_file_write(filep, buf, count, ppos)
- 獲取文件inode->i_sem信號量用以控制同步寫入
- 創建kiocb, iovec,調用__generic_file_aio_write_block:
- 首先在page cache中搜索對應頁
- 如果沒有對應頁,新建一個頁框,調用address_space->prepare_write
- 拷貝寫入內容到頁中,調用address_space->commit_write,標記頁dirty
- 釋放inode->i_sem
- 如果需要sync,調用address_space的writepages方法
- 到這一步,write操作已經返回了。標記為dirty的page最終寫到磁盤上,則是延遲執行的,調用address_space->writepages方法
內存映射mmap
內存映射簡單的說,就是直接把文件內容讀入page cache,并通過修改進程的mm_struct中的vm_area_struct來讓一部分線性空間指向page cache中的page的物理地址。這樣進程如果只需要讀取,就相對read調用少了一次往user buffer里拷貝的過程,但如果進程要把數據復制出來的話,其實跟直接調用read區別不大。
同樣,文件內容讀取也是延后的,直到進程訪問地址產生page fault時,操作系統才會去讀入文件內容到對應page frame。
所以修改內存數據會直接修改page cache中的page內容,導致page為dirty,操作系統之后會將臟page寫入磁盤,更新本地文件。Direct I/O
Linux還提供一個Direct I/O,數據直接從設備傳遞到用戶空間,繞過文件緩存系統,好處是少了一次數據從系統內核到用戶空間的拷貝。壞處是用戶空間的頁將會被鎖定不能換出,這是與linux系統內存使用理念相悖的。
檢測工具
Network
- netstat -s
- netstat -i
- tcpdump
- stap/perf