我們在cpu篇就提到,iowait高一般代表硬碟到瓶頸了。wait的意思,就是等,就像等正在化妝的女朋友,總是帶著一絲焦躁。本篇是《荒島餘生》系列第四篇,I/O篇,計算機中最慢的那一環。其餘參見:
一點背景
速度差異
I/O
不僅僅是硬碟,還包括外圍的所有裝置,比如鍵盤滑鼠,比如1.44M
的3.5
英寸軟盤(還有人記得麼)。但伺服器環境,泛指硬碟。
硬碟有多慢呢?我們不去探究不同裝置的實現細節,直接看它的寫入速度(資料有出入,僅作參考):
可以看到普通磁碟的隨機寫和順序寫相差是非常大的。而隨機寫
完全和cpu記憶體不在一個數量級。緩衝區依然是解決速度差異的唯一工具,所以在極端情況比如斷電等,就產生了太多的不確定性。這些緩衝區,都容易丟。
我們舉例看一下為了消除這些效能差異,軟體方面都做了哪些權衡。
-
資料庫設計,採用
BTree
結構組織資料,通過減少對磁碟的訪問和隨機讀取,來提高效能 -
Postgres
通過順序寫WAL
日誌、ES
通過寫translog
等,通過預寫,避免斷電後資料丟失問題 -
Kafka
通過順序寫
來增加效能,但在topic非常多的情況下效能弱化為隨機寫 -
Kafka
通過零拷貝
技術,利用DMA繞過記憶體直接傳送資料 -
Redis
使用記憶體模擬儲存,它流行的主要原因就是和硬碟打交道的傳統DB速度太慢 -
回憶一下記憶體篇的
buffer區
,是用來緩衝寫入硬碟的資料的。linux的sync
命令可以將buffer的資料刷到硬碟上,突然斷電的話,就不好說了
做一個記憶體盤
如果你的記憶體夠大,那麼可以做一個記憶體盤。跑遊戲,做檔案交換什麼的不要太爽。
mkdir /memdisk
mount -t tmpfs -o size=1024m tmpfs /memdisk/
複製程式碼
以上命令劃出1GB
記憶體,掛載到/memdisk
目錄,然後就可以像使用普通資料夾一樣使用它了。只是,速度不可同日而語。
使用dd命令測試寫入速度
[root@xjj memdisk]# time dd if=/dev/zero of=test.file bs=4k count=200000
200000+0 records in
200000+0 records out
819200000 bytes (819 MB) copied, 0.533173 s, 1.5 GB/s
real 0m0.534s
user 0m0.020s
sys 0m0.510s
複製程式碼
你見過這麼快的硬碟麼?
排查I/O問題的一般思路
判斷I/O
問題的命令其實並不多,大體有下面幾個。
#檢視wa
top
#檢視wa和io(bi、bo)
vmstat 1
#檢視效能相關i/o詳情
sar -b 1 2
# 檢視問題相關i/o詳情
iostat -x 1
# 檢視使用i/o最多的程式
iotop
複製程式碼
驚鴻一瞥
首先是我們的老面孔。top、vmstat、sar命令,可以初步判斷io情況。
bi、bo等在你瞭解磁碟的型別後才有判斷價值。我們有更專業的判斷工具,所以這些資訊一瞥即可。在本例中,wa
已經達到30%
,證明cpu耗費在上面的時間太多。
定位問題
如何判斷還需要結合iostat
的幫助。有時候你是無可奈何的,比如這臺MySQL的宿主機。你可能會更換更牛X的磁碟,或者整治耗I/O的慢SQL,再或者去改引數。
iostat
命令就夠了!我們對一些重要結果進行說明:
-
%util 最重要的判斷引數。一般地,如果該引數是100%表示裝置已經接近滿負荷執行了
-
Device 表示發生在哪塊硬碟。如果你有多快,則會顯示多行
-
avgqu-sz 還記得準備篇裡提到的麼?這個值是請求佇列的飽和度,也就是平均請求佇列的長度。毫無疑問,佇列長度越短越好
-
await 響應時間應該低於5ms,如果大於10ms就比較大了。這個時間包括了佇列時間和服務時間
-
svctm 表示平均每次裝置
I/O
操作的服務時間。如果svctm
的值與await
很接近,表示幾乎沒有I/O
等待,磁碟效能很好,如果await
的值遠高於svctm
的值,則表示I/O
佇列等待太長,系統上執行的應用程式將變慢
總體來說,%util
代表了硬碟的繁忙程度,是你進行擴容增加配置的指標。而await
、avgqu-sz
、svctm
等是硬碟的效能指標,如果%util
正常的情況下反應異常則代表你的磁碟可能存在問題。
iostat列印出的第1個報告,數值是基於最後一次系統啟動的時間統計的;基於這個原因,在大部份情況下,iostat列印出的第1個報告應該被忽略。
另外一種方式就是通過ps命令或者top命令得到狀態為D的程式。比如下面命令,迴圈10次進行狀態抓取。
for x in `seq 1 1 10`; do ps -eo state,pid,cmd | grep "^D"; echo "----"$x; sleep 5; done
複製程式碼
找到I/O大戶
iostat
檢視的是硬碟整體的狀況,但我們想知道到底是哪個應用引起的。top系列有一個iotop
,能夠像top一樣,看到佔用I/O
最多的應用。iotop
的本質是一個python指令碼,從proc
目錄中獲取thread的IO資訊,進行彙總。比如
[root@xjj ~]# cat /proc/5178/io
rchar: 628
wchar: 461
syscr: 2
syscw: 8
read_bytes: 0
write_bytes: 0
cancelled_write_bytes: 0
複製程式碼
如圖,顯示了當前系統硬碟的讀寫速度和應用的I/O使用佔比。
那麼怎麼看應用所關聯的所有檔案資訊呢?可以使用lsof命令,列出了所有的引用控制程式碼。
[root@xjj ~]# lsof -p 4050
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
mysqld 4050 mysql 314u IPv6 115230644 0t0 TCP iZ2zeeaoqoxksuhnqbgfjjZ:mysql->10.30.134.8:54943 (ESTABLISHED)
mysqld 4050 mysql 320u REG 253,17 2048 44829072 /data/mysql/mysql/user.MYI
mysqld 4050 mysql 321u REG 253,17 3108 44829073 /data/mysql/mysql/user.MYD
...
複製程式碼
更深層的資訊,可以通過類似Percona-Toolkit
這種工具去深入排查,比如pt-ioprofile
,在此不做詳解。
幾個特殊程式說明*
kswapd0
這依然是由於swap虛擬記憶體引起的,證明虛擬記憶體正在大量使用
jbd2
全稱是journaling block driver。這個程式實現的是檔案系統的日誌功能,磁碟使用日誌功能來保證資料的完整性。
可以通過以下方法將其關掉,但一定要權衡
dumpe2fs /dev/sda1
tune2fs -o journal_data_writeback /dev/sda1
tune2fs -O "^has_journal" /dev/sda1
e2fsck -f /dev/sda1
複製程式碼
同時在fstab下重新設定一下,在defaults之後增加
defaults,data=writeback,noatime,nodiratime
複製程式碼
你可能會有一個數量級的效能提升。
其他
硬碟快滿了
使用df
命令可以看到磁碟的使用情況。一般,使用達到90%就需要重點關注,然後人工介入刪除檔案了。
[root@xjj ~]# df -h
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 40G 8.3G 29G 23% /
/dev/vdb1 1008G 22G 935G 3% /data
tmpfs 1.0G 782M 243M 77% /memdisk
複製程式碼
使用du
命令可以檢視某個檔案的大小。
[root@xjj ~]# du -h test.file
782M test.file
複製程式碼
如果想把一個檔案置空,千萬不要直接rm。其他應用可能保持著它的引用,經常發上檔案刪除但空間不釋放的問題。比如tomcat的calatina.out檔案,如果你想清空裡面的內容,不要rm,可以執行下面的命令進行檔案內容清空
cat /dev/null > calatina.out
複製程式碼
這非常安全。
zero copy
kafka比較快的一個原因就是使用了zero copy。所謂的Zero copy,就是在運算元據時, 不需要將資料buffer從一個記憶體區域拷貝到另一個記憶體區域。因為少了一次記憶體的拷貝, CPU的效率就得到提升。
我們來看一下它們之間的區別:
要想將一個檔案的內容通過socket傳送出去,傳統的方式需要經過以下步驟: =>將檔案內容拷貝到核心空間 =>將核心空間的內容拷貝到使用者空間記憶體,比如java應用 =>使用者空間將內容寫入到核心空間的快取中 =>socket讀取核心快取中的內容,傳送出去 如上圖,zero copy在核心的支援下,少了一個步驟,那就是核心快取向使用者空間的拷貝。即節省了記憶體,也節省了cpu的排程時間,效率很高。值得注意的是,java中的zero copy,指的其實是DirectBuffer;而Netty的zero copy是在使用者空間中進行的優化。兩者並不是一個概念。
Linux通用I/O模型
面向介面程式設計?linux從誕生開始就有了。在linux下,一切都是檔案,比如裝置、指令碼、可執行檔案、目錄等。操作它們,都有公用的介面。所以,編寫一個裝置驅動,就是實現這些介面而已。
-
fd = open(pathname, flags, mode)
-
rlen = read(fd, buf, count)
-
wlen = write(fd, buf, count)
-
status = close(fd)
使用stat
命令可以看到檔案的一些狀態。
[root@xjj ~]# stat test.file
File: ‘test.file’
Size: 819200000 Blocks: 1600000 IO Block: 4096 regular file
Device: 26h/38d Inode: 3805851 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2018-11-29 12:56:34.801412100 +0800
Modify: 2018-11-29 12:56:35.334415131 +0800
Change: 2018-11-29 12:56:35.334415131 +0800
複製程式碼
而使用file
命令,能得到檔案的型別資訊
[root@xjj ~]# file /bin/bash
/bin/bash: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=ab347e897f002d8e3836479e2430d75305fe6a94, stripped
複製程式碼
I/O模型
談完了I/O
問題的定位方法,就不得不提一下Linux下的5
種I/O
模型。等等,這其實是一個網路問題。
-
同步阻塞IO(Blocking IO) 傳統的IO模型
-
同步非阻塞IO(Non-blocking IO) 非阻塞IO要求socket被設定為NONBLOCK
-
IO多路複用(IO Multiplexing) 即經典的Reactor設計模式
-
非同步IO(Asynchronous IO) 即經典的Proactor設計模式
java中nio
使用的就是多路複用功能,也就是使用的Linux的epoll
庫。一般手擼nio的比較少了,大都是直接使用netty
進行開發。它們用到的,就是經典的reactor模式。
我們能得到什麼
除了能夠幫助我們評價I/O瓶頸,一個非常重要的點就是:業務研發要合理輸出日誌,日誌檔案不僅僅是影響磁碟那麼簡單,它還會耗佔大量的CPU。
對於我們平常的優化思路,也有章可循。像mysql、es、postgresql等,在寫真正的資料庫檔案之前,會有很多層緩衝。如果你對資料可靠性要求並不是那麼嚴重,調整這些緩衝引數的閾值和執行間隔,通常會得到較大的效能提升。
當然,瞭解I/O還能幫助我們更好的理解一些軟體的設計理念。比如leveldb是如何通過LSM來組織資料;ES為什麼會存在那麼多的段合併;甚至Redis為何存在。
當然,你可能再也無法忍受單機硬碟的這些特性,轉而尋求像ceph這樣的解決方案。但無論如何,我們都該向所有的資料庫研發工作者致敬,在很長一段時間裡,我們依然需要和緩慢的I/O共行。