1 測(cè)試工具
$ while true; do curl http://192.168.0.10:10000/products/geektime; sleep 5; done
echo 1>/proc/sys/vm/drop_caches表示釋放pagecache,也就是文件緩存,而mysql讀書(shū)的數(shù)據(jù)就是文件緩存,dataservice不停的釋放文件緩存,就導(dǎo)致MySQL都無(wú)法利用磁盤(pán)緩存,也就慢了~
2 指標(biāo)
使用率、飽和度、IOPS、吞吐量以及響應(yīng)時(shí)間等。這五個(gè)指標(biāo),是衡量磁盤(pán)性能的基本指標(biāo)
使用率,是指磁盤(pán)處理 I/O 的時(shí)間百分比。過(guò)高的使用率(比如超過(guò) 60%),通常意味著磁盤(pán) I/O 存在性能瓶頸。
飽和度,是指磁盤(pán)處理 I/O 的繁忙程度。過(guò)高的飽和度,意味著磁盤(pán)存在嚴(yán)重的性能瓶頸。當(dāng)飽和度為 100% 時(shí),磁盤(pán)無(wú)法接受新的 I/O 請(qǐng)求。
IOPS(Input/Output Per Second),是指每秒的 I/O 請(qǐng)求數(shù)。
吞吐量,是指每秒的 I/O 請(qǐng)求大小。
響應(yīng)時(shí)間,是指 I/O 請(qǐng)求從發(fā)出到收到響應(yīng)的間隔時(shí)間。
最容易想到的是存儲(chǔ)空間的使用情況,包括容量、使用量以及剩余空間等。我們通常也稱這些為磁盤(pán)空間的使用量,因?yàn)槲募到y(tǒng)的數(shù)據(jù)最終還是存儲(chǔ)在磁盤(pán)上。不過(guò)要注意,這些只是文件系統(tǒng)向外展示的空間使用,而非在磁盤(pán)空間的真實(shí)用量,因?yàn)槲募到y(tǒng)的元數(shù)據(jù)也會(huì)占用磁盤(pán)空間。而且,如果你配置了 RAID,從文件系統(tǒng)看到的使用量跟實(shí)際磁盤(pán)的占用空間,也會(huì)因?yàn)?RAID 級(jí)別的不同而不一樣。比方說(shuō),配置 RAID10 后,你從文件系統(tǒng)最多也只能看到所有磁盤(pán)容量的一半。
除了數(shù)據(jù)本身的存儲(chǔ)空間,還有一個(gè)容易忽略的是索引節(jié)點(diǎn)的使用情況,它也包括容量、使用量以及剩余量等三個(gè)指標(biāo)。如果文件系統(tǒng)中存儲(chǔ)過(guò)多的小文件,就可能碰到索引節(jié)點(diǎn)容量已滿的問(wèn)題
其次,你應(yīng)該想到的是前面多次提到過(guò)的緩存使用情況,包括頁(yè)緩存、目錄項(xiàng)緩存、索引節(jié)點(diǎn)緩存以及各個(gè)具體文件系統(tǒng)(如 ext4、XFS 等)的緩存。這些緩存會(huì)使用速度更快的內(nèi)存,用來(lái)臨時(shí)存儲(chǔ)文件數(shù)據(jù)或者文件系統(tǒng)的元數(shù)據(jù),從而可以減少訪問(wèn)慢速磁盤(pán)的次數(shù)
除了以上這兩點(diǎn),文件 I/O 也是很重要的性能指標(biāo),包括 IOPS(包括 r/s 和 w/s)、響應(yīng)時(shí)間(延遲)以及吞吐量(B/s)等。在考察這類指標(biāo)時(shí),通常還要考慮實(shí)際文件的讀寫(xiě)情況。比如,結(jié)合文件大小、文件數(shù)量、I/O 類型等,綜合分析文件 I/O 的性能。誠(chéng)然,這些性能指標(biāo)非常重要,但不幸的是,Linux 文件系統(tǒng)并沒(méi)提供,直接查看這些指標(biāo)的方法。我們只能通過(guò)系統(tǒng)調(diào)用、動(dòng)態(tài)跟蹤或者基準(zhǔn)測(cè)試等方法,間接進(jìn)行觀察、評(píng)估。不過(guò),實(shí)際上,這些指標(biāo)在我們考察磁盤(pán)性能時(shí)更容易見(jiàn)到,因?yàn)?Linux 為磁盤(pán)性能提供了更詳細(xì)的數(shù)據(jù)。

3 工具
iostat 是最常用的磁盤(pán) I/O 性能觀測(cè)工具,它提供了每個(gè)磁盤(pán)的使用率、IOPS、吞吐量等各種常見(jiàn)的性能指標(biāo),當(dāng)然,這些指標(biāo)實(shí)際上來(lái)自 /proc/diskstats
pidstat 給它加上 -d 參數(shù),你就可以看到進(jìn)程的 I/O 情況
iotop 根據(jù) I/O 大小對(duì)進(jìn)程排序,也是性能分析中一個(gè)常用的方法。這一點(diǎn),我推薦另一個(gè)工具, iotop。它是一個(gè)類似于 top 的工具,你可以按照 I/O 大小對(duì)進(jìn)程排序,然后找到 I/O 較大的那些進(jìn)程
lsof 它專門(mén)用來(lái)查看進(jìn)程打開(kāi)文件列表,不過(guò),這里的“文件”不只有普通文件,還包括了目錄、塊設(shè)備、動(dòng)態(tài)庫(kù)、網(wǎng)絡(luò)套接字等
filetop 它是 bcc 軟件包的一部分,基于 Linux 內(nèi)核的 eBPF(extended Berkeley Packet Filters)機(jī)制,主要跟蹤內(nèi)核中文件的讀寫(xiě)情況,并輸出線程 ID(TID)、讀寫(xiě)大小、讀寫(xiě)類型以及文件名稱
opensnoop 。它同屬于 bcc 軟件包,可以動(dòng)態(tài)跟蹤內(nèi)核中的 open 系統(tǒng)調(diào)用。這樣,我們就可以找出這些 txt 文件的路徑
掌握文件系統(tǒng)和磁盤(pán) I/O 的性能指標(biāo)后,我們還要知道,怎樣去獲取這些指標(biāo),也就是搞明白工具的使用問(wèn)題。你還記得前面的基礎(chǔ)篇和案例篇中,都分別用了哪些工具嗎?
我們一起回顧下這些內(nèi)容。第一,在文件系統(tǒng)的原理中,我介紹了查看文件系統(tǒng)容量的工具 df。它既可以查看文件系統(tǒng)數(shù)據(jù)的空間容量,也可以查看索引節(jié)點(diǎn)的容量。至于文件系統(tǒng)緩存,我們通過(guò) /proc/meminfo、/proc/slabinfo 以及 slabtop 等各種來(lái)源,觀察頁(yè)緩存、目錄項(xiàng)緩存、索引節(jié)點(diǎn)緩存以及具體文件系統(tǒng)的緩存情況。
第二,在磁盤(pán) I/O 的原理中,我們分別用 iostat 和 pidstat 觀察了磁盤(pán)和進(jìn)程的 I/O 情況。它們都是最常用的 I/O 性能分析工具。通過(guò) iostat ,我們可以得到磁盤(pán)的 I/O 使用率、吞吐量、響應(yīng)時(shí)間以及 IOPS 等性能指標(biāo);而通過(guò) pidstat ,則可以觀察到進(jìn)程的 I/O 吞吐量以及塊設(shè)備 I/O 的延遲等。
第三,在狂打日志的案例中,我們先用 top 查看系統(tǒng)的 CPU 使用情況,發(fā)現(xiàn) iowait 比較高;然后,又用 iostat 發(fā)現(xiàn)了磁盤(pán)的 I/O 使用率瓶頸,并用 pidstat 找出了大量 I/O 的進(jìn)程;最后,通過(guò) strace 和 lsof,我們找出了問(wèn)題進(jìn)程正在讀寫(xiě)的文件,并最終鎖定性能問(wèn)題的來(lái)源——原來(lái)是進(jìn)程在狂打日志。
第四,在磁盤(pán) I/O 延遲的單詞熱度案例中,我們同樣先用 top、iostat ,發(fā)現(xiàn)磁盤(pán)有 I/O 瓶頸,并用 pidstat 找出了大量 I/O 的進(jìn)程??山酉聛?lái),想要照搬上次操作的我們失敗了。在隨后的 strace 命令中,我們居然沒(méi)看到 write 系統(tǒng)調(diào)用。于是,我們換了一個(gè)思路,用新工具 filetop 和 opensnoop ,從內(nèi)核中跟蹤系統(tǒng)調(diào)用,最終找出瓶頸的來(lái)源。
最后,在 MySQL 和 Redis 的案例中,同樣的思路,我們先用 top、iostat 以及 pidstat ,確定并找出 I/O 性能問(wèn)題的瓶頸來(lái)源,它們正是 mysqld 和 redis-server。隨后,我們又用 strace+lsof 找出了它們正在讀寫(xiě)的文件。關(guān)于 MySQL 案例,根據(jù) mysqld 正在讀寫(xiě)的文件路徑,再結(jié)合 MySQL 數(shù)據(jù)庫(kù)引擎的原理,我們不僅找出了數(shù)據(jù)庫(kù)和數(shù)據(jù)表的名稱,還進(jìn)一步發(fā)現(xiàn)了慢查詢的問(wèn)題,
性能指標(biāo)和工具的聯(lián)系
第一個(gè)維度,從文件系統(tǒng)和磁盤(pán) I/O 的性能指標(biāo)出發(fā)。換句話說(shuō),當(dāng)你想查看某個(gè)性能指標(biāo)時(shí),要清楚知道,哪些工具可以做到

第二個(gè)維度,從工具出發(fā)。也就是當(dāng)你已經(jīng)安裝了某個(gè)工具后,要知道這個(gè)工具能提供哪些指標(biāo)

4 命令
iostat——磁盤(pán)IO觀測(cè)
# -d -x表示顯示所有磁盤(pán)I/O的指標(biāo)
$ iostat -d -x 1
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
loop0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
loop1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
sda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
sdb 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
iostat 提供了非常豐富的性能指標(biāo)。第一列的 Device 表示磁盤(pán)設(shè)備的名字,其他各列指標(biāo),雖然數(shù)量較多,但是每個(gè)指標(biāo)的含義都很重要。為了方便你理解,我把它們總結(jié)成了一個(gè)表格

這些指標(biāo)中,你要注意:
- %util ,就是我們前面提到的磁盤(pán) I/O 使用率;
- r/s+ w/s ,就是 IOPS;
- rkB/s+wkB/s ,就是吞吐量;
- r_await+w_await ,就是響應(yīng)時(shí)間。
pidstat 和 iotop——進(jìn)程IO觀測(cè)
pidstat
$ pidstat -d 1
13:39:51 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
13:39:52 102 916 0.00 4.00 0.00 0 rsyslogd
從 pidstat 的輸出你能看到,它可以實(shí)時(shí)查看每個(gè)進(jìn)程的 I/O 情況,包括下面這些內(nèi)容。
用戶 ID(UID)和進(jìn)程 ID(PID) 。
每秒讀取的數(shù)據(jù)大小(kB_rd/s) ,單位是 KB。每秒發(fā)出的寫(xiě)請(qǐng)求數(shù)據(jù)大?。╧B_wr/s) ,單位是 KB。
每秒取消的寫(xiě)請(qǐng)求數(shù)據(jù)大小(kB_ccwr/s) ,單位是 KB。
塊 I/O 延遲(iodelay),包括等待同步塊 I/O 和換入塊 I/O 結(jié)束的時(shí)間,單位是時(shí)鐘周期。
iotop
yum -y install iotop
根據(jù) I/O 大小對(duì)進(jìn)程排序,也是性能分析中一個(gè)常用的方法。這一點(diǎn),我推薦另一個(gè)工具, iotop。它是一個(gè)類似于 top 的工具,你可以按照 I/O 大小對(duì)進(jìn)程排序,然后找到 I/O 較大的那些進(jìn)程
$ iotop
Total DISK READ : 0.00 B/s | Total DISK WRITE : 7.85 K/s
Actual DISK READ: 0.00 B/s | Actual DISK WRITE: 0.00 B/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
15055 be/3 root 0.00 B/s 7.85 K/s 0.00 % 0.00 % systemd-journald
個(gè)輸出,你可以看到,前兩行分別表示,進(jìn)程的磁盤(pán)讀寫(xiě)大小總數(shù)和磁盤(pán)真實(shí)的讀寫(xiě)大小總數(shù)。因?yàn)榫彺?、緩沖區(qū)、I/O 合并等因素的影響,它們可能并不相等。剩下的部分,則是從各個(gè)角度來(lái)分別表示進(jìn)程的 I/O 情況,包括線程 ID、I/O 優(yōu)先級(jí)、每秒讀磁盤(pán)的大小、每秒寫(xiě)磁盤(pán)的大小、換入和等待 I/O 的時(shí)鐘百分比等
strace
MySQL 是一個(gè)多線程的數(shù)據(jù)庫(kù)應(yīng)用,為了不漏掉這些線程的數(shù)據(jù)讀取情況,你要記得在執(zhí)行 stace 命令時(shí),加上 -f 參數(shù)
$ strace -f -p 27458
[pid 28014] read(38, "934EiwT363aak7VtqF1mHGa4LL4Dhbks"..., 131072) = 131072
[pid 28014] read(38, "hSs7KBDepBqA6m4ce6i6iUfFTeG9Ot9z"..., 20480) = 20480
[pid 28014] read(38, "NRhRjCSsLLBjTfdqiBRLvN9K6FRfqqLm"..., 131072) = 131072
[pid 28014] read(38, "AKgsik4BilLb7y6OkwQUjjqGeCTQTaRl"..., 24576) = 24576
[pid 28014] read(38, "hFMHx7FzUSqfFI22fQxWCpSnDmRjamaW"..., 131072) = 131072
[pid 28014] read(38, "ajUzLmKqivcDJSkiw7QWf2ETLgvQIpfC"..., 20480) = 20480
# -f表示跟蹤子進(jìn)程和子線程,-T表示顯示系統(tǒng)調(diào)用的時(shí)長(zhǎng),-tt表示顯示跟蹤時(shí)間
$ strace -f -T -tt -p 9085
[pid 9085] 14:20:16.826131 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 65, NULL, 8) = 1 <0.000055>
[pid 9085] 14:20:16.826301 read(8, "*2\r\n$3\r\nGET\r\n$41\r\nuuid:5b2e76cc-"..., 16384) = 61 <0.000071>
[pid 9085] 14:20:16.826477 read(3, 0x7fff366a5747, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000063>
[pid 9085] 14:20:16.826645 write(8, "$3\r\nbad\r\n", 9) = 9 <0.000173>
[pid 9085] 14:20:16.826907 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 65, NULL, 8) = 1 <0.000032>
[pid 9085] 14:20:16.827030 read(8, "*2\r\n$3\r\nGET\r\n$41\r\nuuid:55862ada-"..., 16384) = 61 <0.000044>
[pid 9085] 14:20:16.827149 read(3, 0x7fff366a5747, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000043>
[pid 9085] 14:20:16.827285 write(8, "$3\r\nbad\r\n", 9) = 9 <0.000141>
[pid 9085] 14:20:16.827514 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 64, NULL, 8) = 1 <0.000049>
[pid 9085] 14:20:16.827641 read(8, "*2\r\n$3\r\nGET\r\n$41\r\nuuid:53522908-"..., 16384) = 61 <0.000043>
[pid 9085] 14:20:16.827784 read(3, 0x7fff366a5747, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000034>
[pid 9085] 14:20:16.827945 write(8, "$4\r\ngood\r\n", 10) = 10 <0.000288>
[pid 9085] 14:20:16.828339 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 63, NULL, 8) = 1 <0.000057>
[pid 9085] 14:20:16.828486 read(8, "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n535"..., 16384) = 67 <0.000040>
[pid 9085] 14:20:16.828623 read(3, 0x7fff366a5747, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000052>
[pid 9085] 14:20:16.828760 write(7, "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n535"..., 67) = 67 <0.000060>
[pid 9085] 14:20:16.828970 fdatasync(7) = 0 <0.005415>
[pid 9085] 14:20:16.834493 write(8, ":1\r\n", 4) = 4 <0.000250>
pstree
# -t表示顯示線程,-a表示顯示命令行參數(shù)
$ pstree -t -a -p 27458
mysqld,27458 --log_bin=on --sync_binlog=1
...
├─{mysqld},27922
├─{mysqld},27923
└─{mysqld},28014
lsof
lsof后面接進(jìn)程號(hào)不是線程號(hào)
$ lsof -p 27458
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
...
?mysqld 27458 999 38u REG 8,1 512440000 2601895 /var/lib/mysql/test/products.MYD
這次我們得到了 lsof 的輸出。從輸出中可以看到, mysqld 進(jìn)程確實(shí)打開(kāi)了大量文件,而根據(jù)文件描述符(FD)的編號(hào),我們知道,描述符為 38 的是一個(gè)路徑為 /var/lib/mysql/test/products.MYD 的文件。這里注意, 38 后面的 u 表示, mysqld 以讀寫(xiě)的方式訪問(wèn)文件??吹竭@個(gè)文件,熟悉 MySQL 的你可能笑了:MYD 文件,是 MyISAM 引擎用來(lái)存儲(chǔ)表數(shù)據(jù)的文件;文件名就是數(shù)據(jù)表的名字;而這個(gè)文件的父目錄,也就是數(shù)據(jù)庫(kù)的名字。換句話說(shuō),這個(gè)文件告訴我們,mysqld 在讀取數(shù)據(jù)庫(kù) test 中的 products 表。
5 場(chǎng)景
有時(shí)候,明明你碰到了空間不足的問(wèn)題,可是用 df 查看磁盤(pán)空間后,卻發(fā)現(xiàn)剩余空間還有很多
剛才我強(qiáng)調(diào)的一個(gè)細(xì)節(jié)。除了文件數(shù)據(jù),索引節(jié)點(diǎn)也占用磁盤(pán)空間。你可以給 df 命令加上 -i 參數(shù),查看索引節(jié)點(diǎn)的使用情況,如下所示
$ df -i /dev/sda1
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sda1 3870720 157460 3713260 5% /
索引節(jié)點(diǎn)的容量,(也就是 Inode 個(gè)數(shù))是在格式化磁盤(pán)時(shí)設(shè)定好的,一般由格式化工具自動(dòng)生成。
當(dāng)你發(fā)現(xiàn)索引節(jié)點(diǎn)空間不足,但磁盤(pán)空間充足時(shí),很可能就是過(guò)多小文件導(dǎo)致的。
所以,一般來(lái)說(shuō),刪除這些小文件,或者把它們移動(dòng)到索引節(jié)點(diǎn)充足的其他磁盤(pán)中,就可以解決這個(gè)問(wèn)題
6 IO高——瘋狂打log案例
可以先用 top ,來(lái)觀察 CPU 和內(nèi)存的使用情況;然后再用 iostat ,來(lái)觀察磁盤(pán)的 I/O 情況
# 按1切換到每個(gè)CPU的使用情況
$ top
top - 14:43:43 up 1 day, 1:39, 2 users, load average: 2.48, 1.09, 0.63
Tasks: 130 total, 2 running, 74 sleeping, 0 stopped, 0 zombie
%Cpu0 : 0.7 us, 6.0 sy, 0.0 ni, 0.7 id, 92.7 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 : 0.0 us, 0.3 sy, 0.0 ni, 92.3 id, 7.3 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 8169308 total, 747684 free, 741336 used, 6680288 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 7113124 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
18940 root 20 0 656108 355740 5236 R 6.3 4.4 0:12.56 python
1312 root 20 0 236532 24116 9648 S 0.3 0.3 9:29.80 python3
iowait 超過(guò)了 90%。這說(shuō)明 CPU0 上,可能正在運(yùn)行 I/O 密集型的進(jìn)程
運(yùn)行 iostat 命令,觀察 I/O 的使用情況
# -d表示顯示I/O性能指標(biāo),-x表示顯示擴(kuò)展統(tǒng)計(jì)(即所有I/O指標(biāo))
$ iostat -x -d 1
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
loop0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
sdb 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
sda 0.00 64.00 0.00 32768.00 0.00 0.00 0.00 0.00 0.00 7270.44 1102.18 0.00 512.00 15.50 99.20
磁盤(pán) sda 的 I/O 使用率已經(jīng)高達(dá) 99%,很可能已經(jīng)接近 I/O 飽和。
再看前面的各個(gè)指標(biāo),每秒寫(xiě)磁盤(pán)請(qǐng)求數(shù)是 64 ,寫(xiě)大小是 32 MB,寫(xiě)請(qǐng)求的響應(yīng)時(shí)間為 7 秒,而請(qǐng)求隊(duì)列長(zhǎng)度則達(dá)到了 1100。
超慢的響應(yīng)時(shí)間和特長(zhǎng)的請(qǐng)求隊(duì)列長(zhǎng)度,進(jìn)一步驗(yàn)證了 I/O 已經(jīng)飽和的猜想。此時(shí),sda 磁盤(pán)已經(jīng)遇到了嚴(yán)重的性能瓶頸
到這里,也就可以理解,為什么前面看到的 iowait 高達(dá) 90% 了,這正是磁盤(pán) sda 的 I/O 瓶頸導(dǎo)致的。接下來(lái)的重點(diǎn)就是分析 I/O 性能瓶頸的根源了。那要怎么知道,這些 I/O 請(qǐng)求相關(guān)的進(jìn)程呢?
可以用 pidstat 或者 iotop ,觀察進(jìn)程的 I/O 情況。這里,我就用 pidstat 來(lái)看一下。使用 pidstat 加上 -d 參數(shù),就可以顯示每個(gè)進(jìn)程的 I/O 情況。所以,你可以在終端中運(yùn)行
$ pidstat -d 1
15:08:35 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
15:08:36 0 18940 0.00 45816.00 0.00 96 python
15:08:36 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
15:08:37 0 354 0.00 0.00 0.00 350 jbd2/sda1-8
15:08:37 0 18940 0.00 46000.00 0.00 96 python
15:08:37 0 20065 0.00 0.00 0.00 1503 kworker/u4:2
從 pidstat 的輸出,你可以發(fā)現(xiàn),只有 python 進(jìn)程的寫(xiě)比較大,而且每秒寫(xiě)的數(shù)據(jù)超過(guò) 45 MB,比上面 iostat 發(fā)現(xiàn)的 32MB 的結(jié)果還要大。很明顯,正是 python 進(jìn)程導(dǎo)致了 I/O 瓶頸
察系統(tǒng)調(diào)用情況,就可以知道進(jìn)程正在寫(xiě)的文件。想起 strace 了嗎,它正是我們分析系統(tǒng)調(diào)用時(shí)最常用的工具
$ strace -p 18940
strace: Process 18940 attached
...
mmap(NULL, 314576896, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0f7aee9000
mmap(NULL, 314576896, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0f682e8000
write(3, "2018-12-05 15:23:01,709 - __main"..., 314572844
) = 314572844
munmap(0x7f0f682e8000, 314576896) = 0
write(3, "\n", 1) = 1
munmap(0x7f0f7aee9000, 314576896) = 0
close(3) = 0
stat("/tmp/logtest.txt.1", {st_mode=S_IFREG|0644, st_size=943718535, ...}) = 0
從 write() 系統(tǒng)調(diào)用上,我們可以看到,進(jìn)程向文件描述符編號(hào)為 3 的文件中,寫(xiě)入了 300MB 的數(shù)據(jù)??磥?lái),它應(yīng)該是我們要找的文件。不過(guò),write() 調(diào)用中只能看到文件的描述符編號(hào),文件名和路徑還是未知的。再觀察后面的 stat() 調(diào)用,你可以看到,它正在獲取 /tmp/logtest.txt.1 的狀態(tài)。 這種“點(diǎn) + 數(shù)字格式”的文件,在日志回滾中非常常見(jiàn)。我們可以猜測(cè),這是第一個(gè)日志回滾文件,而正在寫(xiě)的日志文件路徑,則是 /tmp/logtest.txt。當(dāng)然,這只是我們的猜測(cè),自然還需要驗(yàn)證
在終端中運(yùn)行下面的 lsof 命令,看看進(jìn)程 18940 都打開(kāi)了哪些文件
$ lsof -p 18940
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
python 18940 root cwd DIR 0,50 4096 1549389 /
python 18940 root rtd DIR 0,50 4096 1549389 /
…
python 18940 root 2u CHR 136,0 0t0 3 /dev/pts/0
python 18940 root 3w REG 8,1 117944320 303 /tmp/logtest.txt
這個(gè)輸出界面中,有幾列我簡(jiǎn)單介紹一下,F(xiàn)D 表示文件描述符號(hào),TYPE 表示文件類型,NAME 表示文件路徑。這也是我們需要關(guān)注的重點(diǎn)。再看最后一行,這說(shuō)明,這個(gè)進(jìn)程打開(kāi)了文件 /tmp/logtest.txt,并且它的文件描述符是 3 號(hào),而 3 后面的 w ,表示以寫(xiě)的方式打開(kāi)。這跟剛才 strace 完我們猜測(cè)的結(jié)果一致,看來(lái)這就是問(wèn)題的根源:進(jìn)程 18940 以每次 300MB 的速度,在“瘋狂”寫(xiě)日志,而日志文件的路徑是 /tmp/logtest.txt。既然找出了問(wèn)題根源,接下來(lái)按照慣例,就該查看源代碼,然后分析為什么這個(gè)進(jìn)程會(huì)狂打日志了。
7 IO高——響應(yīng)過(guò)慢案例
用 iostat、strace、lsof 無(wú)法定位時(shí),還可以用其它工具查看
filetop
# 切換到工具目錄
$ cd /usr/share/bcc/tools
# -C 選項(xiàng)表示輸出新內(nèi)容時(shí)不清空屏幕
$ ./filetop -C
TID COMM READS WRITES R_Kb W_Kb T FILE
514 python 0 1 0 2832 R 669.txt
514 python 0 1 0 2490 R 667.txt
514 python 0 1 0 2685 R 671.txt
514 python 0 1 0 2392 R 670.txt
514 python 0 1 0 2050 R 672.txt
...
TID COMM READS WRITES R_Kb W_Kb T FILE
514 python 2 0 5957 0 R 651.txt
514 python 2 0 5371 0 R 112.txt
514 python 2 0 4785 0 R 861.txt
514 python 2 0 4736 0 R 213.txt
514 python 2 0 4443 0 R 45.txt
你會(huì)看到,filetop 輸出了 8 列內(nèi)容,分別是線程 ID、線程命令行、讀寫(xiě)次數(shù)、讀寫(xiě)的大?。▎挝?KB)、文件類型以及讀寫(xiě)的文件名稱。這些內(nèi)容里,你可能會(huì)看到很多動(dòng)態(tài)鏈接庫(kù),不過(guò)這不是我們的重點(diǎn),暫且忽略即可。我們的重點(diǎn),是一個(gè) python 應(yīng)用,所以要特別關(guān)注 python 相關(guān)的內(nèi)容。多觀察一會(huì)兒,你就會(huì)發(fā)現(xiàn),每隔一段時(shí)間,線程號(hào)為 514 的 python 應(yīng)用就會(huì)先寫(xiě)入大量的 txt 文件,再大量地讀。線程號(hào)為 514 的線程,屬于哪個(gè)進(jìn)程呢?我們可以用 ps 命令查看。先在終端一中,按下 Ctrl+C ,停止 filetop ;然后,運(yùn)行下面的 ps 命令。這個(gè)輸出的第二列內(nèi)容,就是我們想知道的進(jìn)程號(hào):
$ ps -efT | grep 514
root 12280 514 14626 33 14:47 pts/0 00:00:05 /usr/local/bin/python /app.py
opensnoop
它同屬于 bcc 軟件包,可以動(dòng)態(tài)跟蹤內(nèi)核中的 open 系統(tǒng)調(diào)用。這樣,我們就可以找出這些 txt 文件的路徑
$ opensnoop
12280 python 6 0 /tmp/9046db9e-fe25-11e8-b13f-0242ac110002/650.txt
12280 python 6 0 /tmp/9046db9e-fe25-11e8-b13f-0242ac110002/651.txt
12280 python 6 0 /tmp/9046db9e-fe25-11e8-b13f-0242ac110002/652.txt
這次,通過(guò) opensnoop 的輸出,你可以看到,這些 txt 路徑位于 /tmp 目錄下。你還能看到,它打開(kāi)的文件數(shù)量,按照數(shù)字編號(hào),從 0.txt 依次增大到 999.txt,這可遠(yuǎn)多于前面用 filetop 看到的數(shù)量。綜合 filetop 和 opensnoop ,我們就可以進(jìn)一步分析了。我們可以大膽猜測(cè),案例應(yīng)用在寫(xiě)入 1000 個(gè) txt 文件后,又把這些內(nèi)容讀到內(nèi)存中進(jìn)行處理。
結(jié)合前面的所有分析,我們基本可以判斷,案例應(yīng)用會(huì)動(dòng)態(tài)生成一批文件,用來(lái)臨時(shí)存儲(chǔ)數(shù)據(jù),用完就會(huì)刪除它們。但不幸的是,正是這些文件讀寫(xiě),引發(fā)了 I/O 的性能瓶頸,導(dǎo)致整個(gè)處理過(guò)程非常慢。
這是一種常見(jiàn)的利用磁盤(pán)空間處理大量數(shù)據(jù)的技巧,不過(guò),本次案例中的 I/O 請(qǐng)求太重,導(dǎo)致磁盤(pán) I/O 利用率過(guò)高
8 IO高——mysql慢查詢案例
通過(guò)對(duì)系統(tǒng) CPU、內(nèi)存和磁盤(pán) I/O 等資源使用情況的分析,我們發(fā)現(xiàn)這時(shí)出現(xiàn)了磁盤(pán)的 I/O 瓶頸,并且正是案例應(yīng)用導(dǎo)致的。接著,我們借助 pidstat,發(fā)現(xiàn)罪魁禍?zhǔn)资?mysqld 進(jìn)程。我們又通過(guò) strace、lsof,找出了 mysqld 正在讀的文件。根據(jù)文件的名字和路徑,我們找出了 mysqld 正在操作的數(shù)據(jù)庫(kù)和數(shù)據(jù)表。綜合這些信息,我們猜測(cè)這是一個(gè)沒(méi)利用索引導(dǎo)致的慢查詢問(wèn)題
為了驗(yàn)證猜測(cè),我們到 MySQL 命令行終端,使用數(shù)據(jù)庫(kù)分析工具發(fā)現(xiàn),案例應(yīng)用訪問(wèn)的字段果然沒(méi)有索引。既然猜測(cè)是正確的,那增加索引后,問(wèn)題就自然解決了。從這個(gè)案例你會(huì)發(fā)現(xiàn),MySQL 的 MyISAM 引擎,主要依賴系統(tǒng)緩存加速磁盤(pán) I/O 的訪問(wèn)。可如果系統(tǒng)中還有其他應(yīng)用同時(shí)運(yùn)行, MyISAM 引擎很難充分利用系統(tǒng)緩存。緩存可能會(huì)被其他應(yīng)用程序占用,甚至被清理掉。所以,一般我并不建議,把應(yīng)用程序的性能優(yōu)化完全建立在系統(tǒng)緩存上。最好能在應(yīng)用程序的內(nèi)部分配內(nèi)存,構(gòu)建完全自主控制的緩存;或者使用第三方的緩存應(yīng)用,比如 Memcached、Redis 等。
9 IO高——redis響應(yīng)嚴(yán)重延遲案例
Redis 是最常用的鍵值存儲(chǔ)系統(tǒng)之一,常用作數(shù)據(jù)庫(kù)、高速緩存和消息隊(duì)列代理等。Redis 基于內(nèi)存來(lái)存儲(chǔ)數(shù)據(jù),不過(guò),為了保證在服務(wù)器異常時(shí)數(shù)據(jù)不丟失,很多情況下,我們要為它配置持久化,而這就可能會(huì)引發(fā)磁盤(pán) I/O 的性能問(wèn)題。今天,我就帶你一起來(lái)分析一個(gè)利用 Redis 作為緩存的案例。這同樣是一個(gè)基于 Python Flask 的應(yīng)用程序,它提供了一個(gè) 查詢緩存的接口,但接口的響應(yīng)時(shí)間比較長(zhǎng),并不能滿足線上系統(tǒng)的要求。
# -f表示跟蹤子進(jìn)程和子線程,-T表示顯示系統(tǒng)調(diào)用的時(shí)長(zhǎng),-tt表示顯示跟蹤時(shí)間
$ strace -f -T -tt -p 9085
[pid 9085] 14:20:16.826131 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 65, NULL, 8) = 1 <0.000055>
[pid 9085] 14:20:16.826301 read(8, "*2\r\n$3\r\nGET\r\n$41\r\nuuid:5b2e76cc-"..., 16384) = 61 <0.000071>
[pid 9085] 14:20:16.826477 read(3, 0x7fff366a5747, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000063>
[pid 9085] 14:20:16.826645 write(8, "$3\r\nbad\r\n", 9) = 9 <0.000173>
[pid 9085] 14:20:16.826907 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 65, NULL, 8) = 1 <0.000032>
[pid 9085] 14:20:16.827030 read(8, "*2\r\n$3\r\nGET\r\n$41\r\nuuid:55862ada-"..., 16384) = 61 <0.000044>
[pid 9085] 14:20:16.827149 read(3, 0x7fff366a5747, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000043>
[pid 9085] 14:20:16.827285 write(8, "$3\r\nbad\r\n", 9) = 9 <0.000141>
[pid 9085] 14:20:16.827514 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 64, NULL, 8) = 1 <0.000049>
[pid 9085] 14:20:16.827641 read(8, "*2\r\n$3\r\nGET\r\n$41\r\nuuid:53522908-"..., 16384) = 61 <0.000043>
[pid 9085] 14:20:16.827784 read(3, 0x7fff366a5747, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000034>
[pid 9085] 14:20:16.827945 write(8, "$4\r\ngood\r\n", 10) = 10 <0.000288>
[pid 9085] 14:20:16.828339 epoll_pwait(5, [{EPOLLIN, {u32=8, u64=8}}], 10128, 63, NULL, 8) = 1 <0.000057>
[pid 9085] 14:20:16.828486 read(8, "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n535"..., 16384) = 67 <0.000040>
[pid 9085] 14:20:16.828623 read(3, 0x7fff366a5747, 1) = -1 EAGAIN (Resource temporarily unavailable) <0.000052>
[pid 9085] 14:20:16.828760 write(7, "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n535"..., 67) = 67 <0.000060>
[pid 9085] 14:20:16.828970 fdatasync(7) = 0 <0.005415>
[pid 9085] 14:20:16.834493 write(8, ":1\r\n", 4) = 4 <0.000250>
觀察一會(huì)兒,有沒(méi)有發(fā)現(xiàn)什么有趣的現(xiàn)象呢?事實(shí)上,從系統(tǒng)調(diào)用來(lái)看, epoll_pwait、read、write、fdatasync 這些系統(tǒng)調(diào)用都比較頻繁。那么,剛才觀察到的寫(xiě)磁盤(pán),應(yīng)該就是 write 或者 fdatasync 導(dǎo)致的了。接著再來(lái)運(yùn)行 lsof 命令,找出這些系統(tǒng)調(diào)用的操作對(duì)象:
$ lsof -p 9085
redis-ser 9085 systemd-network 3r FIFO 0,12 0t0 15447970 pipe
redis-ser 9085 systemd-network 4w FIFO 0,12 0t0 15447970 pipe
redis-ser 9085 systemd-network 5u a_inode 0,13 0 10179 [eventpoll]
redis-ser 9085 systemd-network 6u sock 0,9 0t0 15447972 protocol: TCP
redis-ser 9085 systemd-network 7w REG 8,1 8830146 2838532 /data/appendonly.aof
redis-ser 9085 systemd-network 8u sock 0,9 0t0 15448709 protocol: TCP
現(xiàn)在你會(huì)發(fā)現(xiàn),描述符編號(hào)為 3 的是一個(gè) pipe 管道,5 號(hào)是 eventpoll,7 號(hào)是一個(gè)普通文件,而 8 號(hào)是一個(gè) TCP socket。結(jié)合磁盤(pán)寫(xiě)的現(xiàn)象,我們知道,只有 7 號(hào)普通文件才會(huì)產(chǎn)生磁盤(pán)寫(xiě),而它操作的文件路徑是 /data/appendonly.aof,相應(yīng)的系統(tǒng)調(diào)用包括 write 和 fdatasync。如果你對(duì) Redis 的持久化配置比較熟,看到這個(gè)文件路徑以及 fdatasync 的系統(tǒng)調(diào)用,你應(yīng)該能想到,這對(duì)應(yīng)著正是 Redis 持久化配置中的 appendonly 和 appendfsync 選項(xiàng)。很可能是因?yàn)樗鼈兊呐渲貌缓侠恚瑢?dǎo)致磁盤(pán)寫(xiě)比較多
接下來(lái)就驗(yàn)證一下這個(gè)猜測(cè),我們可以通過(guò) Redis 的命令行工具,查詢這兩個(gè)選項(xiàng)的配置。繼續(xù)在終端一中,運(yùn)行下面的命令,查詢 appendonly 和 appendfsync 的配置
$ docker exec -it redis redis-cli config get 'append*'
1) "appendfsync"
2) "always"
3) "appendonly"
4) "yes"
從這個(gè)結(jié)果你可以發(fā)現(xiàn),
appendfsync 配置的是 always,
而 appendonly 配置的是 yes。這兩個(gè)選項(xiàng)的詳細(xì)含義,你可以從 Redis Persistence 的文檔中查到,這里我做一下簡(jiǎn)單介紹。
Redis 提供了兩種數(shù)據(jù)持久化的方式,分別是快照和追加文件。
快照方式,會(huì)按照指定的時(shí)間間隔,生成數(shù)據(jù)的快照,并且保存到磁盤(pán)文件中。為了避免阻塞主進(jìn)程,Redis 還會(huì) fork 出一個(gè)子進(jìn)程,來(lái)負(fù)責(zé)快照的保存。這種方式的性能好,無(wú)論是備份還是恢復(fù),都比追加文件好很多。不過(guò),它的缺點(diǎn)也很明顯。在數(shù)據(jù)量大時(shí),fork 子進(jìn)程需要用到比較大的內(nèi)存,保存數(shù)據(jù)也很耗時(shí)。所以,你需要設(shè)置一個(gè)比較長(zhǎng)的時(shí)間間隔來(lái)應(yīng)對(duì),比如至少 5 分鐘。這樣,如果發(fā)生故障,你丟失的就是幾分鐘的數(shù)據(jù)。
追加文件,則是用在文件末尾追加記錄的方式,對(duì) Redis 寫(xiě)入的數(shù)據(jù),依次進(jìn)行持久化,所以它的持久化也更安全。此外,它還提供了一個(gè)用 appendfsync 選項(xiàng)設(shè)置 fsync 的策略,確保寫(xiě)入的數(shù)據(jù)都落到磁盤(pán)中,具體選項(xiàng)包括 always、everysec、no 等。always 表示,每個(gè)操作都會(huì)執(zhí)行一次 fsync,是最為安全的方式;everysec 表示,每秒鐘調(diào)用一次 fsync ,這樣可以保證即使是最壞情況下,也只丟失 1 秒的數(shù)據(jù);而 no 表示交給操作系統(tǒng)來(lái)處理。
回憶一下我們剛剛看到的配置,appendfsync 配置的是 always,意味著每次寫(xiě)數(shù)據(jù)時(shí),都會(huì)調(diào)用一次 fsync,從而造成比較大的磁盤(pán) I/O 壓力。當(dāng)然,你還可以用 strace ,觀察這個(gè)系統(tǒng)調(diào)用的執(zhí)行情況。比如通過(guò) -e 選項(xiàng)指定 fdatasync 后,你就會(huì)得到下面的結(jié)果:
$ strace -f -p 9085 -T -tt -e fdatasync
strace: Process 9085 attached with 4 threads
[pid 9085] 14:22:52.013547 fdatasync(7) = 0 <0.007112>
[pid 9085] 14:22:52.022467 fdatasync(7) = 0 <0.008572>
[pid 9085] 14:22:52.032223 fdatasync(7) = 0 <0.006769>
...
[pid 9085] 14:22:52.139629 fdatasync(7) = 0 <0.008183>
從這里你可以看到,每隔 10ms 左右,就會(huì)有一次 fdatasync 調(diào)用,并且每次調(diào)用本身也要消耗 7~8ms。不管哪種方式,都可以驗(yàn)證我們的猜想,配置確實(shí)不合理。這樣,我們就找出了 Redis 正在進(jìn)行寫(xiě)入的文件,也知道了產(chǎn)生大量 I/O 的原因。
回到最初的疑問(wèn),為什么查詢時(shí)會(huì)有磁盤(pán)寫(xiě)呢?按理來(lái)說(shuō)不應(yīng)該只有數(shù)據(jù)的讀取嗎?這就需要我們?cè)賮?lái)審查一下 strace -f -T -tt -p 9085 的結(jié)果
read(8, "*2\r\n$3\r\nGET\r\n$41\r\nuuid:53522908-"..., 16384)
write(8, "$4\r\ngood\r\n", 10)
read(8, "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n535"..., 16384)
write(7, "*3\r\n$4\r\nSADD\r\n$4\r\ngood\r\n$36\r\n535"..., 67)
write(8, ":1\r\n", 4)
細(xì)心的你應(yīng)該記得,根據(jù) lsof 的分析,文件描述符編號(hào)為 7 的是一個(gè)普通文件 /data/appendonly.aof,而編號(hào)為 8 的是 TCP socket。而觀察上面的內(nèi)容,8 號(hào)對(duì)應(yīng)的 TCP 讀寫(xiě),是一個(gè)標(biāo)準(zhǔn)的“請(qǐng)求 - 響應(yīng)”格式,即:從 socket 讀取 GET uuid:53522908-… 后,響應(yīng) good;再?gòu)?socket 讀取 SADD good 535… 后,響應(yīng) 1。對(duì) Redis 來(lái)說(shuō),SADD 是一個(gè)寫(xiě)操作,所以 Redis 還會(huì)把它保存到用于持久化的 appendonly.aof 文件中。觀察更多的 strace 結(jié)果,你會(huì)發(fā)現(xiàn),每當(dāng) GET 返回 good 時(shí),隨后都會(huì)有一個(gè) SADD 操作,這也就導(dǎo)致了,明明是查詢接口,Redis 卻有大量的磁盤(pán)寫(xiě)
通過(guò)今天的案例你會(huì)發(fā)現(xiàn),為了進(jìn)一步分析,就需要你對(duì)系統(tǒng)和應(yīng)用程序的工作原理有一定的了解。比如,今天的案例中,雖然磁盤(pán) I/O 并沒(méi)有出現(xiàn)瓶頸,但從 Redis 的原理來(lái)說(shuō),查詢緩存時(shí)不應(yīng)該出現(xiàn)大量的磁盤(pán) I/O 寫(xiě)操作。順著這個(gè)思路,我們繼續(xù)借助 pidstat、strace、lsof、nsenter 等一系列的工具,找出了兩個(gè)潛在問(wèn)題,一個(gè)是 Redis 的不合理配置,另一個(gè)是 Python 應(yīng)用對(duì) Redis 的濫用。找到瓶頸后,相應(yīng)的優(yōu)化工作自然就比較輕松了
10 小結(jié)
想弄清楚性能指標(biāo)的關(guān)聯(lián)性,就要通曉每種性能指標(biāo)的工作原理
從 I/O 角度來(lái)分析,最開(kāi)始的分析思路基本上類似,都是:先用 iostat 發(fā)現(xiàn)磁盤(pán) I/O 性能瓶頸;再借助 pidstat ,定位出導(dǎo)致瓶頸的進(jìn)程;隨后分析進(jìn)程的 I/O 行為;最后,結(jié)合應(yīng)用程序的原理,分析這些 I/O 的來(lái)源
為了縮小排查范圍,我通常會(huì)先運(yùn)行那幾個(gè)支持指標(biāo)較多的工具,如 iostat、vmstat、pidstat 等。然后再根據(jù)觀察到的現(xiàn)象,結(jié)合系統(tǒng)和應(yīng)用程序的原理,尋找下一步的分析方向。我把這個(gè)過(guò)程畫(huà)成了一張圖,你可以保存下來(lái)參考使用

圖中列出了最常用的幾個(gè)文件系統(tǒng)和磁盤(pán) I/O 性能分析工具,以及相應(yīng)的分析流程,箭頭則表示分析方向。這其中,iostat、vmstat、pidstat 是最核心的幾個(gè)性能工具,它們也提供了最重要的 I/O 性能指標(biāo)。舉幾個(gè)例子你可能更容易理解。例如,在前面講過(guò)的 MySQL 和 Redis 案例中,我們就是通過(guò) iostat 確認(rèn)磁盤(pán)出現(xiàn) I/O 性能瓶頸,然后用 pidstat 找出 I/O 最大的進(jìn)程,接著借助 strace 找出該進(jìn)程正在讀寫(xiě)的文件,最后結(jié)合應(yīng)用程序的原理,找出大量 I/O 的原因。再如,當(dāng)你用 iostat 發(fā)現(xiàn)磁盤(pán)有 I/O 性能瓶頸后,再用 pidstat 和 vmstat 檢查,可能會(huì)發(fā)現(xiàn) I/O 來(lái)自內(nèi)核線程,如 Swap 使用大量升高。這種情況下,你就得進(jìn)行內(nèi)存分析了,先找出占用大量?jī)?nèi)存的進(jìn)程,再設(shè)法減少內(nèi)存的使用。另外注意,我在這個(gè)圖中只列出了最核心的幾個(gè)性能工具,并沒(méi)有列出前面表格中的所有工具。這么做,一方面是不想用大量的工具列表嚇到你。在學(xué)習(xí)之初就接觸所有核心或小眾的工具,不見(jiàn)得是好事。另一方面,也是希望你能先把重心放在核心工具上,畢竟熟練掌握它們,就可以解決大多數(shù)問(wèn)題。
方法:
1> 用iostat看磁盤(pán)的await,utils,iops,bandwidth
2>用smartctl看磁盤(pán)的health status
3>用iotop/pidstat找出持續(xù)讀寫(xiě)的進(jìn)程做優(yōu)化
4>在碰到這種“狂打日志”的場(chǎng)景時(shí),你可以用 iostat、strace、lsof 等工具來(lái)定位狂打日志的進(jìn)程,找出相應(yīng)的日志文件,再通過(guò)應(yīng)用程序的接口,調(diào)整日志級(jí)別來(lái)解決問(wèn)題。如果應(yīng)用程序不能動(dòng)態(tài)調(diào)整日志級(jí)別,你可能還需要修改應(yīng)用的配置,并重啟應(yīng)用讓配置生效。如果無(wú)法找出問(wèn)題,可以借助動(dòng)態(tài)追蹤工具包 bcc 中的 filetop 和 opensnoop ,發(fā)現(xiàn)這個(gè)根源是大量讀寫(xiě)臨時(shí)文件
在 Linux 中一切皆文件。不僅普通的文件和目錄,就連塊設(shè)備、套接字、管道等,也都要通過(guò)統(tǒng)一的文件系統(tǒng)來(lái)管理
5>pcstat(page cache stat)這個(gè)可以查看目標(biāo)log文件在cache中的大小
例子
一次io性能問(wèn)題
數(shù)據(jù)寫(xiě)es,運(yùn)行一段時(shí)間后,發(fā)現(xiàn)寫(xiě)入很慢,查io時(shí)發(fā)現(xiàn),讀的io很高,寫(xiě)的io很少,很奇怪只寫(xiě)數(shù)據(jù)還沒(méi)查詢,讀的io使用率基本接近100%。
用iotop定位到es一些寫(xiě)的線程,將線程id轉(zhuǎn)成16進(jìn)制,用jstack打印出es的堆棧信息,查出16進(jìn)制的線程號(hào)的堆棧。發(fā)現(xiàn)原來(lái)是es會(huì)跟據(jù)doc id查數(shù)據(jù),然后選擇更新或新插入。es數(shù)據(jù)量大時(shí),會(huì)占用了很多讀的io.
后面寫(xiě)es就不傳id,讓es自動(dòng)生成。解決了問(wèn)題
應(yīng)用程序優(yōu)化
首先,我們來(lái)看一下,從應(yīng)用程序的角度有哪些優(yōu)化 I/O 的思路。應(yīng)用程序處于整個(gè) I/O 棧的最上端,它可以通過(guò)系統(tǒng)調(diào)用,來(lái)調(diào)整 I/O 模式(如順序還是隨機(jī)、同步還是異步), 同時(shí),它也是 I/O 數(shù)據(jù)的最終來(lái)源。在我看來(lái),可以有這么幾種方式來(lái)優(yōu)化應(yīng)用程序的 I/O 性能。第一,可以用追加寫(xiě)代替隨機(jī)寫(xiě),減少尋址開(kāi)銷,加快 I/O 寫(xiě)的速度。第二,可以借助緩存 I/O ,充分利用系統(tǒng)緩存,降低實(shí)際 I/O 的次數(shù)。第三,可以在應(yīng)用程序內(nèi)部構(gòu)建自己的緩存,或者用 Redis 這類外部緩存系統(tǒng)。這樣,一方面,能在應(yīng)用程序內(nèi)部,控制緩存的數(shù)據(jù)和生命周期;另一方面,也能降低其他應(yīng)用程序使用緩存對(duì)自身的影響。比如,在前面的 MySQL 案例中,我們已經(jīng)見(jiàn)識(shí)過(guò),只是因?yàn)橐粋€(gè)干擾應(yīng)用清理了系統(tǒng)緩存,就會(huì)導(dǎo)致 MySQL 查詢有數(shù)百倍的性能差距(0.1s vs 15s)。再如, C 標(biāo)準(zhǔn)庫(kù)提供的 fopen、fread 等庫(kù)函數(shù),都會(huì)利用標(biāo)準(zhǔn)庫(kù)的緩存,減少磁盤(pán)的操作。而你直接使用 open、read 等系統(tǒng)調(diào)用時(shí),就只能利用操作系統(tǒng)提供的頁(yè)緩存和緩沖區(qū)等,而沒(méi)有庫(kù)函數(shù)的緩存可用。第四,在需要頻繁讀寫(xiě)同一塊磁盤(pán)空間時(shí),可以用 mmap 代替 read/write,減少內(nèi)存的拷貝次數(shù)。第五,在需要同步寫(xiě)的場(chǎng)景中,盡量將寫(xiě)請(qǐng)求合并,而不是讓每個(gè)請(qǐng)求都同步寫(xiě)入磁盤(pán),即可以用 fsync() 取代 O_SYNC。第六,在多個(gè)應(yīng)用程序共享相同磁盤(pán)時(shí),為了保證 I/O 不被某個(gè)應(yīng)用完全占用,推薦你使用 cgroups 的 I/O 子系統(tǒng),來(lái)限制進(jìn)程 / 進(jìn)程組的 IOPS 以及吞吐量。最后,在使用 CFQ 調(diào)度器時(shí),可以用 ionice 來(lái)調(diào)整進(jìn)程的 I/O 調(diào)度優(yōu)先級(jí),特別是提高核心應(yīng)用的 I/O 優(yōu)先級(jí)。ionice 支持三個(gè)優(yōu)先級(jí)類:Idle、Best-effort 和 Realtime。其中, Best-effort 和 Realtime 還分別支持 0-7 的級(jí)別,數(shù)值越小,則表示優(yōu)先級(jí)別越高。
文件系統(tǒng)優(yōu)化
應(yīng)用程序訪問(wèn)普通文件時(shí),實(shí)際是由文件系統(tǒng)間接負(fù)責(zé),文件在磁盤(pán)中的讀寫(xiě)。所以,跟文件系統(tǒng)中相關(guān)的也有很多優(yōu)化 I/O 性能的方式。第一,你可以根據(jù)實(shí)際負(fù)載場(chǎng)景的不同,選擇最適合的文件系統(tǒng)。比如 Ubuntu 默認(rèn)使用 ext4 文件系統(tǒng),而 CentOS 7 默認(rèn)使用 xfs 文件系統(tǒng)。相比于 ext4 ,xfs 支持更大的磁盤(pán)分區(qū)和更大的文件數(shù)量,如 xfs 支持大于 16TB 的磁盤(pán)。但是 xfs 文件系統(tǒng)的缺點(diǎn)在于無(wú)法收縮,而 ext4 則可以。第二,在選好文件系統(tǒng)后,還可以進(jìn)一步優(yōu)化文件系統(tǒng)的配置選項(xiàng),包括文件系統(tǒng)的特性(如 ext_attr、dir_index)、日志模式(如 journal、ordered、writeback)、掛載選項(xiàng)(如 noatime)等等。比如, 使用 tune2fs 這個(gè)工具,可以調(diào)整文件系統(tǒng)的特性(tune2fs 也常用來(lái)查看文件系統(tǒng)超級(jí)塊的內(nèi)容)。 而通過(guò) /etc/fstab ,或者 mount 命令行參數(shù),我們可以調(diào)整文件系統(tǒng)的日志模式和掛載選項(xiàng)等。第三,可以優(yōu)化文件系統(tǒng)的緩存。比如,你可以優(yōu)化 pdflush 臟頁(yè)的刷新頻率(比如設(shè)置 dirty_expire_centisecs 和 dirty_writeback_centisecs)以及臟頁(yè)的限額(比如調(diào)整 dirty_background_ratio 和 dirty_ratio 等)。再如,你還可以優(yōu)化內(nèi)核回收目錄項(xiàng)緩存和索引節(jié)點(diǎn)緩存的傾向,即調(diào)整 vfs_cache_pressure(/proc/sys/vm/vfs_cache_pressure,默認(rèn)值 100),數(shù)值越大,就表示越容易回收。最后,在不需要持久化時(shí),你還可以用內(nèi)存文件系統(tǒng) tmpfs,以獲得更好的 I/O 性能 。tmpfs 把數(shù)據(jù)直接保存在內(nèi)存中,而不是磁盤(pán)中。比如 /dev/shm/ ,就是大多數(shù) Linux 默認(rèn)配置的一個(gè)內(nèi)存文件系統(tǒng),它的大小默認(rèn)為總內(nèi)存的一半。
磁盤(pán)優(yōu)化
數(shù)據(jù)的持久化存儲(chǔ),最終還是要落到具體的物理磁盤(pán)中,同時(shí),磁盤(pán)也是整個(gè) I/O 棧的最底層。從磁盤(pán)角度出發(fā),自然也有很多有效的性能優(yōu)化方法。第一,最簡(jiǎn)單有效的優(yōu)化方法,就是換用性能更好的磁盤(pán),比如用 SSD 替代 HDD。第二,我們可以使用 RAID ,把多塊磁盤(pán)組合成一個(gè)邏輯磁盤(pán),構(gòu)成冗余獨(dú)立磁盤(pán)陣列。這樣做既可以提高數(shù)據(jù)的可靠性,又可以提升數(shù)據(jù)的訪問(wèn)性能。第三,針對(duì)磁盤(pán)和應(yīng)用程序 I/O 模式的特征,我們可以選擇最適合的 I/O 調(diào)度算法。比方說(shuō),SSD 和虛擬機(jī)中的磁盤(pán),通常用的是 noop 調(diào)度算法。而數(shù)據(jù)庫(kù)應(yīng)用,我更推薦使用 deadline 算法。第四,我們可以對(duì)應(yīng)用程序的數(shù)據(jù),進(jìn)行磁盤(pán)級(jí)別的隔離。比如,我們可以為日志、數(shù)據(jù)庫(kù)等 I/O 壓力比較重的應(yīng)用,配置單獨(dú)的磁盤(pán)。第五,在順序讀比較多的場(chǎng)景中,我們可以增大磁盤(pán)的預(yù)讀數(shù)據(jù),比如,你可以通過(guò)下面兩種方法,調(diào)整 /dev/sdb 的預(yù)讀大小。調(diào)整內(nèi)核選項(xiàng) /sys/block/sdb/queue/read_ahead_kb,默認(rèn)大小是 128 KB,單位為 KB。使用 blockdev 工具設(shè)置,比如 blockdev --setra 8192 /dev/sdb,注意這里的單位是 512B(0.5KB),所以它的數(shù)值總是 read_ahead_kb 的兩倍。第六,我們可以優(yōu)化內(nèi)核塊設(shè)備 I/O 的選項(xiàng)。比如,可以調(diào)整磁盤(pán)隊(duì)列的長(zhǎng)度 /sys/block/sdb/queue/nr_requests,適當(dāng)增大隊(duì)列長(zhǎng)度,可以提升磁盤(pán)的吞吐量(當(dāng)然也會(huì)導(dǎo)致 I/O 延遲增大)。最后,要注意,磁盤(pán)本身出現(xiàn)硬件錯(cuò)誤,也會(huì)導(dǎo)致 I/O 性能急劇下降,所以發(fā)現(xiàn)磁盤(pán)性能急劇下降時(shí),你還需要確認(rèn),磁盤(pán)本身是不是出現(xiàn)了硬件錯(cuò)誤。比如,你可以查看 dmesg 中是否有硬件 I/O 故障的日志。 還可以使用 badblocks、smartctl 等工具,檢測(cè)磁盤(pán)的硬件問(wèn)題,或用 e2fsck 等來(lái)檢測(cè)文件系統(tǒng)的錯(cuò)誤。如果發(fā)現(xiàn)問(wèn)題,你可以使用 fsck 等工具來(lái)修復(fù)。
附:
(Flexible I/O Tester)正是最常用的文件系統(tǒng)和磁盤(pán) I/O 性能基準(zhǔn)測(cè)試工具。它提供了大量的可定制化選項(xiàng),可以用來(lái)測(cè)試,裸盤(pán)或者文件系統(tǒng)在各種場(chǎng)景下的 I/O 性能,包括了不同塊大小、不同 I/O 引擎以及是否使用緩存等場(chǎng)景
yum install -y fio
# 隨機(jī)讀
fio -name=randread -direct=1 -iodepth=64 -rw=randread -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sdb
# 隨機(jī)寫(xiě)
fio -name=randwrite -direct=1 -iodepth=64 -rw=randwrite -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sdb
# 順序讀
fio -name=read -direct=1 -iodepth=64 -rw=read -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sdb
# 順序?qū)?fio -name=write -direct=1 -iodepth=64 -rw=write -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sdb
在這其中,有幾個(gè)參數(shù)需要你重點(diǎn)關(guān)注一下。direct,表示是否跳過(guò)系統(tǒng)緩存。上面示例中,我設(shè)置的 1 ,就表示跳過(guò)系統(tǒng)緩存。iodepth,表示使用異步 I/O(asynchronous I/O,簡(jiǎn)稱 AIO)時(shí),同時(shí)發(fā)出的 I/O 請(qǐng)求上限。在上面的示例中,我設(shè)置的是 64。rw,表示 I/O 模式。我的示例中, read/write 分別表示順序讀 / 寫(xiě),而 randread/randwrite 則分別表示隨機(jī)讀 / 寫(xiě)。ioengine,表示 I/O 引擎,它支持同步(sync)、異步(libaio)、內(nèi)存映射(mmap)、網(wǎng)絡(luò)(net)等各種 I/O 引擎。上面示例中,我設(shè)置的 libaio 表示使用異步 I/O。bs,表示 I/O 的大小。示例中,我設(shè)置成了 4K(這也是默認(rèn)值)。filename,表示文件路徑,當(dāng)然,它可以是磁盤(pán)路徑(測(cè)試磁盤(pán)性能),也可以是文件路徑(測(cè)試文件系統(tǒng)性能)。示例中,我把它設(shè)置成了磁盤(pán) /dev/sdb。不過(guò)注意,用磁盤(pán)路徑測(cè)試寫(xiě),會(huì)破壞這個(gè)磁盤(pán)中的文件系統(tǒng),所以在使用前,你一定要事先做好數(shù)據(jù)備份。
下面就是我使用 fio 測(cè)試順序讀的一個(gè)報(bào)告示例
read: (g=0): rw=read, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=64
fio-3.1
Starting 1 process
Jobs: 1 (f=1): [R(1)][100.0%][r=16.7MiB/s,w=0KiB/s][r=4280,w=0 IOPS][eta 00m:00s]
read: (groupid=0, jobs=1): err= 0: pid=17966: Sun Dec 30 08:31:48 2018
read: IOPS=4257, BW=16.6MiB/s (17.4MB/s)(1024MiB/61568msec)
slat (usec): min=2, max=2566, avg= 4.29, stdev=21.76
clat (usec): min=228, max=407360, avg=15024.30, stdev=20524.39
lat (usec): min=243, max=407363, avg=15029.12, stdev=20524.26
clat percentiles (usec):
| 1.00th=[ 498], 5.00th=[ 1020], 10.00th=[ 1319], 20.00th=[ 1713],
| 30.00th=[ 1991], 40.00th=[ 2212], 50.00th=[ 2540], 60.00th=[ 2933],
| 70.00th=[ 5407], 80.00th=[ 44303], 90.00th=[ 45351], 95.00th=[ 45876],
| 99.00th=[ 46924], 99.50th=[ 46924], 99.90th=[ 48497], 99.95th=[ 49021],
| 99.99th=[404751]
bw ( KiB/s): min= 8208, max=18832, per=99.85%, avg=17005.35, stdev=998.94, samples=123
iops : min= 2052, max= 4708, avg=4251.30, stdev=249.74, samples=123
lat (usec) : 250=0.01%, 500=1.03%, 750=1.69%, 1000=2.07%
lat (msec) : 2=25.64%, 4=37.58%, 10=2.08%, 20=0.02%, 50=29.86%
lat (msec) : 100=0.01%, 500=0.02%
cpu : usr=1.02%, sys=2.97%, ctx=33312, majf=0, minf=75
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=0.1%, >=64=100.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.1%, >=64=0.0%
issued rwt: total=262144,0,0, short=0,0,0, dropped=0,0,0
latency : target=0, window=0, percentile=100.00%, depth=64
Run status group 0 (all jobs):
READ: bw=16.6MiB/s (17.4MB/s), 16.6MiB/s-16.6MiB/s (17.4MB/s-17.4MB/s), io=1024MiB (1074MB), run=61568-61568msec
Disk stats (read/write):
sdb: ios=261897/0, merge=0/0, ticks=3912108/0, in_queue=3474336, util=90.09%
這個(gè)報(bào)告中,需要我們重點(diǎn)關(guān)注的是, slat、clat、lat ,以及 bw 和 iops 這幾行。
先來(lái)看剛剛提到的前三個(gè)參數(shù)。事實(shí)上,slat、clat、lat 都是指 I/O 延遲(latency)。
不同之處在于:slat ,是指從 I/O 提交到實(shí)際執(zhí)行 I/O 的時(shí)長(zhǎng)(Submission latency);clat ,是指從 I/O 提交到 I/O 完成的時(shí)長(zhǎng)(Completion latency);
而 lat ,指的是從 fio 創(chuàng)建 I/O 到 I/O 完成的總時(shí)長(zhǎng)。
這里需要注意的是,對(duì)同步 I/O 來(lái)說(shuō),由于 I/O 提交和 I/O 完成是一個(gè)動(dòng)作,所以 slat 實(shí)際上就是 I/O 完成的時(shí)間,而 clat 是 0。而從示例可以看到,使用異步 I/O(libaio)時(shí),lat 近似等于 slat + clat 之和。
再來(lái)看 bw ,它代表吞吐量。在我上面的示例中,你可以看到,平均吞吐量大約是 16 MB(17005 KiB/1024)。最后的 iops ,其實(shí)就是每秒 I/O 的次數(shù),上面示例中的平均 IOPS 為 4250。通常情況下,應(yīng)用程序的 I/O 都是讀寫(xiě)并行的,而且每次的 I/O 大小也不一定相同。所以,剛剛說(shuō)的這幾種場(chǎng)景,并不能精確模擬應(yīng)用程序的 I/O 模式。那怎么才能精確模擬應(yīng)用程序的 I/O 模式呢?幸運(yùn)的是,fio 支持 I/O 的重放。借助前面提到過(guò)的 blktrace,再配合上 fio,就可以實(shí)現(xiàn)對(duì)應(yīng)用程序 I/O 模式的基準(zhǔn)測(cè)試。你需要先用 blktrace ,記錄磁盤(pán)設(shè)備的 I/O 訪問(wèn)情況;然后使用 fio ,重放 blktrace 的記錄。比如你可以運(yùn)行下面的命令來(lái)操作:
# 使用blktrace跟蹤磁盤(pán)I/O,注意指定應(yīng)用程序正在操作的磁盤(pán)
$ blktrace /dev/sdb
# 查看blktrace記錄的結(jié)果
# ls
sdb.blktrace.0 sdb.blktrace.1
# 將結(jié)果轉(zhuǎn)化為二進(jìn)制文件
$ blkparse sdb -d sdb.bin
# 使用fio重放日志
$ fio --name=replay --filename=/dev/sdb --direct=1 --read_iolog=sdb.bin
這樣,我們就通過(guò) blktrace+fio 的組合使用,得到了應(yīng)用程序 I/O 模式的基準(zhǔn)測(cè)試報(bào)告
舉例:
1 df -h 顯示占用100%,而關(guān)閉應(yīng)用程序后,再次df -h是85%,這一般是因?yàn)樵搼?yīng)用程序還有指向已刪除文件的文件指針沒(méi)有關(guān)閉,典型的比如日志文件,雖然在操作系統(tǒng)中用rm命令刪除了,在相應(yīng)的目錄中已經(jīng)沒(méi)有該文件了,但如果應(yīng)用中還有對(duì)應(yīng)的文件指針沒(méi)有關(guān)閉,則實(shí)際硬盤(pán)空間還不會(huì)釋放,而應(yīng)用程序被關(guān)閉時(shí),實(shí)際空間才會(huì)釋放。問(wèn)題中更像是有些apk文件或處理后文件的文件指針沒(méi)有釋放。這種情況也可以通過(guò) lsof | grep deleted 來(lái)找到這些文件。
2 out of memory的問(wèn)題,可以先用free或top看一下可用內(nèi)存是否確實(shí)沒(méi)有了,如果確實(shí)是沒(méi)有內(nèi)存了,那再去研究?jī)?nèi)存的問(wèn)題;還有一種常見(jiàn)情況,內(nèi)存是充足的,文件描述符的個(gè)數(shù)或進(jìn)程數(shù)達(dá)到上限了,那就得調(diào)整 ulimit,可以通過(guò) ulimit -a (注意要用php的用戶)來(lái)查看,關(guān)注open files和max user processes,這兩個(gè)默認(rèn)很小,1k和4k,建議調(diào)整到加兩個(gè)0
3 執(zhí)行 find 命令時(shí),會(huì)不會(huì)導(dǎo)致系統(tǒng)的緩存升高呢?如果會(huì)導(dǎo)致,升高的又是哪種類型的緩存呢
這個(gè)命令,會(huì)不會(huì)導(dǎo)致系統(tǒng)的緩存升高呢?
--> 會(huì)的
如果有影響,又會(huì)導(dǎo)致哪種類型的緩存升高呢?
--> /xfs_inode/ proc_inode_cache/dentry/inode_cache
實(shí)驗(yàn)步驟:
1. 清空緩存:echo 3 > /proc/sys/vm/drop_caches ; sync
2. 執(zhí)行find : find / -name test
3. 發(fā)現(xiàn)更新top 4 項(xiàng)是:
OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
37400 37400 100% 0.94K 2200 17 35200K xfs_inode
36588 36113 98% 0.64K 3049 12 24392K proc_inode_cache
104979 104979 100% 0.19K 4999 21 19996K dentry
18057 18057 100% 0.58K 1389 13 11112K inode_cache
find / -name 這個(gè)命令是全盤(pán)掃描(既包括內(nèi)存文件系統(tǒng)又包含本地的xfs【我的環(huán)境沒(méi)有mount 網(wǎng)絡(luò)文件系統(tǒng)】),所以 inode cache & dentry & proc inode cache 會(huì)升高。
另外,執(zhí)行過(guò)了一次后再次執(zhí)行find 就機(jī)會(huì)沒(méi)有變化了,執(zhí)行速度也快了很多,也就是下次的find大部分是依賴cache的結(jié)果。