Linux 之《荒島餘生》(四):I/O 篇
我們在cpu篇就提到,iowait高一般代表硬碟到瓶頸了。wait的意思,就是等,就像等正在化妝的女朋友,總是帶著一絲焦躁。本篇是《荒島餘生》系列第四篇,I/O篇,計算機中最慢的那一環。其餘參見:
一點背景
速度差異
I/O
不僅僅是硬碟,還包括外圍的所有裝置,比如鍵盤滑鼠,比如 1.44M
的 3.5
英寸軟盤(還有人記得麼)。但伺服器環境,泛指硬碟。
硬碟有多慢呢?我們不去探究不同裝置的實現細節,直接看它的寫入速度(資料有出入,僅作參考):
可以看到普通磁碟的隨機寫和順序寫相差是非常大的。而 隨機寫
完全和cpu記憶體不在一個數量級。緩衝區依然是解決速度差異的唯一工具,所以在極端情況比如斷電等,就產生了太多的不確定性。這些緩衝區,都容易丟。
我們舉例看一下為了消除這些效能差異,軟體方面都做了哪些權衡。
-
Postgres
通過順序寫WAL
日誌、ES
通過寫translog
等,通過預寫,避免斷電後資料丟失問題 -
Kafka
通過順序寫
來增加效能,但在topic非常多的情況下效能弱化為隨機寫 -
Kafka
通過零拷貝
技術,利用DMA繞過記憶體直接傳送資料 -
Redis
使用記憶體模擬儲存,它流行的主要原因就是和硬碟打交道的傳統DB速度太慢 -
回憶一下記憶體篇的
buffer區
,是用來緩衝寫入硬碟的資料的。linux的sync
命令可以將buffer的資料刷到硬碟上,突然斷電的話,就不好說了
做一個記憶體盤
如果你的記憶體夠大,那麼可以做一個記憶體盤。跑遊戲,做檔案交換什麼的不要太爽。
mkdir /memdisk mount-t tmpfs -o size=1024mtmpfs /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 real0m0.534s user0m0.020s sys0m0.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
的幫助。有時候你是無可奈何的,比如這臺SQL/">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 COMMANDPIDUSERFDTYPEDEVICESIZE/OFFNODE NAME mysqld4050 mysql314uIPv61152306440t0TCP iZ2zeeaoqoxksuhnqbgfjjZ:mysql->10.30.134.8:54943 (ESTABLISHED) mysqld4050 mysql320uREG253,17204844829072 /data/mysql/mysql/user.MYI mysqld4050 mysql321uREG253,17310844829073 /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 FilesystemSizeUsed Avail Use% Mounted on /dev/vda140G8.3G29G23% / /dev/vdb11008G22G935G3% /data tmpfs1.0G782M243M77% /memdisk
使用 du
命令可以檢視某個檔案的大小。
[root@xjj ~]# du -h test.file 782Mtest.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: 819200000Blocks: 1600000IO Block: 4096regular file Device: 26h/38dInode: 3805851Links: 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共行。