各種IO監視工具在Linux IO 體系結構中的位置

源自 Linux Performance and Tuning Guidelines.pdf

1 系統級IO監控

iostat

iostat -xdm 1 # 個人習慣

%util 代表磁盤繁忙程度。100% 表示磁盤繁忙, 0%表示磁盤空閒。但是注意,磁盤繁忙不代表磁盤(帶寬)利用率高

argrq-sz 提交給驅動層的IO請求大小,一般不小於4K,不大於max(readahead_kb, max_sectors_kb)

可用於判斷當前的IO模式,一般情況下,尤其是磁盤繁忙時, 越大代表順序,越小代表隨機

svctm 一次IO請求的服務時間,對於單塊盤,完全隨機讀時,基本在7ms左右,既尋道+旋轉延遲時間

注: 各統計量之間關係

=======================================

%util = ( r/s + w/s) * svctm / 1000 # 隊列長度 = 到達率 * 平均服務時間

avgrq-sz = ( rMB/s + wMB/s) * 2048 / (r/s + w/s) # 2048 爲 1M / 512

=======================================

總結:

iostat 統計的是通用塊層經過合併(rrqm/s, wrqm/s)後,直接向設備提交的IO數據,可以反映系統整體的IO狀況,但是有以下2個缺點:

1 距離業務層比較遙遠,跟代碼中的write,read不對應(由於系統預讀 + pagecache + IO調度算法等因素, 也很難對應)

2 是系統級,沒辦法精確到進程,比如只能告訴你現在磁盤很忙,但是沒辦法告訴你是誰在忙,在忙什麼?

2 進程級IO監控

iotop 和 pidstat (僅rhel6u系列)

iotop 顧名思義, io版的top

pidstat 顧名思義, 統計進程(pid)的stat,進程的stat自然包括進程的IO狀況

這兩個命令,都可以按進程統計IO狀況,因此可以回答你以下二個問題

當前系統哪些進程在佔用IO,百分比是多少?佔用IO的進程是在讀?還是在寫?讀寫量是多少?

pidstat 參數很多,僅給出幾個個人習慣

pidstat -d 1 #只顯示IO

pidstat -u -r -d -t 1 # -d IO 信息,

# -r 缺頁及內存信息

# -u CPU使用率

# -t 以線程爲統計單位

# 1 1秒統計一次

iotop, 很簡單,直接敲命令

block_dump, iodump

iotop 和 pidstat 用着很爽,但兩者都依賴於/proc/pid/io文件導出的統計信息, 這個對於老一些的內核是沒有的,比如rhel5u2

因此只好用以上2個窮人版命令來替代:

echo 1 > /proc/sys/vm/block_dump # 開啓block_dump,此時會把io信息輸入到dmesg中

# 源碼: submit_bio@ll_rw_blk.c:3213

watch -n 1 "dmesg -c | grep -oP \"\w+\(\d+\): (WRITE|READ)\" | sort | uniq -c"

# 不停的dmesg -c

echo 0 > /proc/sys/vm/block_dump # 不用時關閉

也可以使用現成的腳本 iodump, 具體參見 http://code.google.com/p/maatkit/source/browse/trunk/util/iodump?r=5389

iotop.stp

systemtap腳本,一看就知道是iotop命令的窮人複製版,需要安裝Systemtap, 默認每隔5秒輸出一次信息

stap iotop.stp # examples/io/iotop.stp

總結

進程級IO監控 ,

可以回答系統級IO監控不能回答的2個問題距離業務層相對較近(例如,可以統計進程的讀寫量)

但是也沒有辦法跟業務層的read,write聯繫在一起,同時顆粒度較粗,沒有辦法告訴你,當前進程讀寫了哪些文件? 耗時? 大小 ?

3 業務級IO監控

ioprofile

ioprofile 命令本質上是 lsof + strace, 具體下載可見 http://code.google.com/p/maatkit/

ioprofile 可以回答你以下三個問題:

1 當前進程某時間內,在業務層面讀寫了哪些文件(read, write)?

2 讀寫次數是多少?(read, write的調用次數)

3 讀寫數據量多少?(read, write的byte數)

假設某個行爲會觸發程序一次IO動作,例如: "一個頁面點擊,導致後臺讀取A,B,C文件"

============================================

./io_event # 假設模擬一次IO行爲,讀取A文件一次, B文件500次, C文件500次

ioprofile -p `pidof io_event` -c count # 讀寫次數

ioprofile -p `pidof io_event` -c times # 讀寫耗時

ioprofile -p `pidof io_event` -c sizes # 讀寫大小

注: ioprofile 僅支持多線程程序,對單線程程序不支持. 對於單線程程序的IO業務級分析,strace足以。

總結:

ioprofile本質上是strace,因此可以看到read,write的調用軌跡,可以做業務層的io分析(mmap方式無能爲力)

4 文件級IO監控

文件級IO監控可以配合/補充"業務級和進程級"IO分析

文件級IO分析,主要針對單個文件, 回答當前哪些進程正在對某個文件進行讀寫操作.

1 lsof 或者 ls /proc/pid/fd

2 inodewatch.stp

lsof 告訴你 當前文件由哪些進程打開

lsof ../io # io目錄 當前由 bash 和 lsof 兩個進程打開

lsof 命令 只能回答靜態的信息, 並且"打開" 並不一定"讀取", 對於 cat ,echo這樣的命令, 打開和讀取都是瞬間的,lsof很難捕捉

可以用 inodewatch.stp 來彌補

stap inodewatch.stp major minor inode # 主設備號, 輔設備號, 文件inode節點號

stap inodewatch.stp 0xfd 0x00 523170 # 主設備號, 輔設備號, inode號,可以通過 stat 命令獲得

[root@server-mysql ~]# stat test.c File: `test.c' Size: 375 Blocks: 8 IO Block: 4096 regular fileDevice: 803h/2051d Inode: 1208533 Links: 1Access: (0755/-rwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)Access: 2016-06-28 23:27:56.327543866 +0800Modify: 2015-12-27 22:13:27.654476214 +0800Change: 2015-12-27 22:13:27.823852491 +0800

5 IO模擬器

iotest.py # 見附錄

開發人員可以 利用 ioprofile (或者 strace) 做詳細分析系統的IO路徑,然後在程序層面做相應的優化。

但是一般情況下調整程序,代價比較大,尤其是當不確定修改方案到底能不能有效時,最好有某種模擬途徑以快速驗證。

以爲我們的業務爲例,發現某次查詢時,系統的IO訪問模式如下:

訪問了A文件一次

訪問了B文件500次, 每次16字節, 平均間隔 502K

訪問了C文件500次, 每次200字節, 平均間隔 4M

這裏 B,C文件是交錯訪問的, 既

1 先訪問B,讀16字節,

2 再訪問C,讀200字節,

3 回到B,跳502K後再讀16字節,

4 回到C,跳4M後,再讀200字節

5 重複500次

strace 文件如下:

一個簡單樸素的想法, 將B,C交錯讀,改成先批量讀B , 再批量讀C,因此調整strace 文件如下:

將調整後的strace文件, 作爲輸入交給 iotest.py, iotest.py 按照 strace 文件中的訪問模式, 模擬相應的IO

iotest.py -s io.strace -f fmap

fmap 爲映射文件,將strace中的222,333等fd,映射到實際的文件中

===========================

111 = /opt/work/io/A.data

222 = /opt/work/io/B.data

333 = /opt/work/io/C.data

===========================

6 磁盤碎片整理

一句話: 只要磁盤容量不常年保持80%以上,基本上不用擔心碎片問題。

如果實在擔心,可以用 defrag 腳本

7 其他IO相關命令

blockdev 系列

=======================================

blockdev --getbsz /dev/sdc1 # 查看sdc1盤的塊大小

block blockdev --getra /dev/sdc1 # 查看sdc1盤的預讀(readahead_kb)大小

blockdev --setra 256 /dev/sdc1 # 設置sdc1盤的預讀(readahead_kb)大小,低版的內核通過/sys設置,有時會失敗,不如blockdev靠譜

=======================================

附錄 iotest.py

#! /usr/bin/env python# -*- coding: gbk -*-import osimport reimport timeit from ctypes import CDLL, create_string_buffer, c_ulong, c_longlongfrom optparse import OptionParserusage = '''%prog -s strace.log -f fileno.map '''_glibc = None_glibc_pread = None_c_char_buf = None_open_file = []def getlines(filename): _lines = [] with open(filename,'r') as _f: for line in _f: if line.strip() != "": _lines.append(line.strip()) return _lines def parsecmdline(): parser = OptionParser(usage) parser.add_option("-s", "--strace", dest="strace_filename", help="strace file", metavar="FILE") parser.add_option("-f", "--fileno", dest="fileno_filename", help="fileno file", metavar="FILE") (options, args) = parser.parse_args() if options.strace_filename is None: parser.error("strace is not specified.") ifnot os.path.exists(options.strace_filename): parser.error("strace file does not exist.") if options.fileno_filename is None: parser.error("fileno is not specified.") ifnot os.path.exists(options.strace_filename): parser.error("fileno file does not exist.") return options.strace_filename, options.fileno_filename # [type, ...]# [pread, fno, count, offset]# pread(15, "", 4348, 140156928)def parse_strace(filename): lines = getlines(filename) action = [] _regex_str = r'(pread|pread64)[^\d]*(\d+),\s*[^,]*,\s*([\dkKmM*+\-. ]*),\s*([\dkKmM*+\-. ]*)'for i in lines: _match = re.match(_regex_str, i) if _match is None: continue# 跳過無效行 _type, _fn, _count, _off = _match.group(1), _match.group(2), _match.group(3), _match.group(4) _off = _off.replace('k', " * 1024 ").replace('K', " * 1024 ").replace('m', " * 1048576 ").replace('M', " * 1048576 ") _count = _count.replace('k', " * 1024 ").replace('K', " * 1024 ").replace('m', " * 1048576 ").replace('M', " * 1048576 ") #print _off action.append([_type, _fn, str(int(eval(_count))), str(int(eval(_off))) ]) return action def parse_fileno(filename): lines = getlines(filename) fmap = {} for i in lines: if i.strip().startswith("#"): continue# 註釋行 _split = [j.strip() for j in i.split("=")] if len(_split) != 2: continue# 無效行 fno, fname = _split[0], _split[1] fmap[fno] = fname return fmap def simulate_before(strace, fmap): global _open_file, _c_char_buf rfmap = {} for i in fmap.values(): _f = open(i, "r+b") #print "open {0}:{1}".format(_f.fileno(), i) _open_file.append(_f) rfmap[i] = str(_f.fileno()) # 反向映射 to_read = 4 * 1024 # 默認4K buffor i in strace: i[1] = rfmap[fmap[i[1]]] # fid -> fname -> fid 映射轉換 to_read = max(to_read, int(i[2])) #print "read buffer len: %d Byte" % to_read _c_char_buf = create_string_buffer(to_read) def simulate_after(): global _open_file for _f in _open_file: _f.close() def simulate(actions): #timeit.time.sleep(10) # 休息2秒鐘, 以便IO間隔 start = timeit.time.time() for act in actions: __simulate__(act) finish = timeit.time.time() return finish - start def__simulate__(act): global _glibc, _glibc_pread, _c_char_buf if"pread"in act[0]: _fno = int(act[1]) _buf = _c_char_buf _count = c_ulong(int(act[2])) _off = c_longlong(int(act[3])) _glibc_pread(_fno, _buf, _count, _off) #print _glibc.time(None)else: passpassdef loadlibc(): global _glibc, _glibc_pread _glibc = CDLL("libc.so.6") _glibc_pread = _glibc.pread64 if__name__ == "__main__": _strace, _fileno = parsecmdline() # 解析命令行參數 loadlibc() # 加載動態庫 _action = parse_strace(_strace) # 解析 action 文件 _fmap = parse_fileno(_fileno) # 解析 文件名映射 文件 simulate_before(_action, _fmap) # 預處理#print "total io operate: %d" % (len(_action))#for act in _action: print " ".join(act)print"%f" % simulate(_action)

查看原文 >>
相關文章