blktrace 工具集使用 及其实现原理
文章目錄
- 工具使用
- 原理分析
- 內核I/O棧
- blktrace 代碼做的事情
- 內核調用 ioctl 做的事情
- BLKTRACESETUP
- BLKTRACESTOP
- BLKTRACETEARDOWN
- 內核 調用blk_register_tracepoints 之后做的事情
- 參考
最近使用blktrace 工具集來分析I/O 在磁盤上的一些瓶頸問題,特此做一個簡單的記錄。
工具用起來很簡單,但越向底層看,越復雜。。。。。。越發現自己的無知
工具使用
blktrace 擁有如下幾個工具集合:
安裝的話也很簡單:
sudo yum install blktrace iowatcher -y
-
其中
blktrace工具 主要根據用戶輸入的磁盤設備,收集這個設備上每個IO調度情況,收集的過程是交給當前服務器的每一個core來做的,最后每一個core將各自處理的請求 收集到的結果保存在一個binary文件中。sudo blktrace -d /dev/nvme0n1 -o nvme-trace -w 60收集設備/dev/nvme0n1上的io 情況 60秒,將結果保存到nvme-trace文件中 -
blkparse工具 主要是將之前抓取的多個core的binary文件合并為一個文件blkparse -i nvme-trace -d nvme-trace.bin -o nvme-trace.txt,將nvme-trace開頭的所有文件合并為一個nvme-trace.bin,這個過程中的輸出放在nvme-trace.txt中。 -
btt工具,blkparse 解析的數據文件 雖然已經有了一些匯總信息,但還是不易讀,比如我們想知道磁盤I/O在每一個階段耗時分布,從blkparse的解析中很難看出來的。blkparse 的匯總信息如下:
通過btt工具來進行計算:
btt -i nvme-trace.bin -o btt.txt
其中
btt.txt.avg就是我們想要的請求信息分布情況
也可以通過
btt -A -i nvme-trace.bin | less看到每一個I/O線程各個階段的IO延時情況
計算blktrace工具抓到的分位數指標(p50,p99,p995,p9999 等)腳本如下,輸入的參數是通過btt -i nvme-trace.bin -l d2c_data生成的請求全集文件:#!/bin/bash input=$1num=`cat $input |wc -l` if [ $num -eq 0 ];thenecho "input is null "exit -1 fip50=$(echo "$num * 0.5" | bc) p50=${p50%.*} # to int p99=$(echo "$num * 0.99" | bc) p99=${p99%.*} p995=$(echo "$num * 0.995" | bc) p995=${p995%.*} p9999=$(echo "$num * 0.9999" | bc) p9999=${p9999%.*}echo "lines -- p50: $p50 p99 : $p99 p995: $p995 p9999: $p9999 total: $num "cat "$input" | awk -F. '{print $3}' | sort > buff.txtecho "p50 " sed -n " $p50 p" buff.txt echo "p99" sed -n " $p99 p" buff.txt echo "p995 " sed -n "$p995 p" buff.txt echo "p9999 " sed -n "$p9999 p" buff.txt -
我們有抓取的I/O的歷史數據,那同樣可以用
iowather來將歷史的io變化情況用圖形展示出來,包括磁盤帶寬、延時等
iowatcher -t nvme-trace.bin -o nvme-trace.svg解析blkparse合并的文件,輸出到nvme-trace.svg中
-
如果你僅僅想看看磁盤的I/O塊大小,都是一些什么I/O,不想這么麻煩,可以直接
btrace /dev/nvme0n1這樣,會將打印輸出到標準輸出中
-
如果你想在塊基礎上看看磁盤延時/塊大小的分布,那
blkiomon就比較適用了
blktrace /dev/nvme0n1 -a issue -a complete -w 3600 -o - | blkiomon -I 1 -h test,這里只抓取complete的io,請求的結果分析(延時/塊大小)就以直方圖的形態非常方便得被展示出來。
當然,以上blktrace,blkparse,btrace 都可以僅僅抓單獨類似的io請求,包括只抓取write, read, sync, issue等(可以通過man blktrace查看masks支持的action。),這樣我們就能夠更近一步得區分每一種類型的請求,方便我們從底層排查問題。
關于傳統的btrace, blkparse等解析data文件之后的輸出 含義內容,直接看網友們貼的這張圖就可以了:
主要的幾個Event信息含義如下:
- Q: 即將生成I/O
- G: 生成I/O 請求
- I: I/O 請求進入scheduler 隊列
- D: I/O 請求進入driver
- C: I/O 執行完畢
原理分析
洋洋灑灑,工具如何使用,介紹了一大串,能夠節省一丟丟大家的時間,man手冊已經很通用了,使用上就沒什么需要探索的了。但是能夠真正讓大家看到收獲的其實是工具背后的原理,為什么blktrace能夠實時得追蹤到每一個io請求,它追蹤的請求個數/大小是否準確,是否有請求會被遺漏?這一些請求在操作系統I/O架構中每一個階段處于什么樣的位置,內核在做什么事情?這一些問題如果我們每一個都仔細去探索,背后則是整個操作系統內核I/O棧的龐大調度邏輯,都會讓我們對內核I/O有更為深刻的理解,有了底層架構的知識才能幫助我們更好得設計上層應用。 畢竟,底層架構的每一行代碼,每一個算法都是無數開發者精心雕琢的表現。
不多說,直接進入正題。
內核I/O棧
blktrace 抓取的IO 內核棧的層級如下:
blktrace統計的主要是I/O進入通用塊層 --> I/O 調度層 --> 塊設備驅動層 完成落盤返回的整個過程,上圖并未體現通用塊層,其實是在I/O Scheduler之上的一層I/O封裝。
- 通用塊層 : 接受direct_io/ page_cache flush下來的請求,做一層請求封裝,一般是4k大小。
- I/O 調度層: 將請求加入調度隊列,通過一系列調度算法來調度封裝好的I/O請求 到對應的device-driver(sata/nvme/iscsi等)
- 塊設備驅動層:這里就是每一個物理塊設備封裝好的對接自己物理磁盤空間的內核驅動,請求到這里會按照對應設備的邏輯進入到底層物理磁盤中
知道了大體的I/O棧,也就清楚了大概一個I/O請求從page-cache或者direct_io 到磁盤所經歷的大體層,這個時候也就對blktrace輸出信息的Event的幾個字段有一定的理解了(Q,G,I,D,C),都是對應的請求進入到了I/O棧中的哪一層。
Blktrace 追蹤過程大體可以用如下這張官方的圖來描述:
blktrace 啟動追蹤的時候會讓每一個cpu(每一個請求都是由對應的cpu來調度處理的)綁定一個relay-channel,通過ioctl下發的觸發信息會讓內核將每一個請求的信息通過trace函數添加到relay-channel對應的trace文件,當blktrace停止追蹤時會告知內核將relay-flush 每一個relay-channel,將trace文件信息拷貝到用戶態。
那blktrace 是如何從外部獲取到這一些請求的信息的呢?接著往下看,后面的描述會整體從內核代碼角度告訴你這個外部工具如何在不影響內核I/O性能的情況下拿到這一些I/O 請求的詳細信息的。
blktrace 代碼做的事情
源碼GitHub: https://github.com/efarrer/blktrace
如果不使用blktrace 網絡模式的情況下(是的,blktrace 支持抓取遠端服務器的磁盤請求信息,blktrace -l 啟動server, blktrace -h ip指定抓取的ip),會走如下調用棧邏輯:
main -- blktrace.crun_tracerssetup_buts -- 初始化一些配置start_tracers -- 為每一個cpu 創建一個tracer線程,獲取io信息start_buts -- 開啟記錄,將請求詳細信息記錄到初始化的文件中stop_tracers -- 終止追蹤
其中的主體操作都是通過ioctl來向內核發送觸發信息:
ioctl(dpp->fd, BLKTRACESETUP, &buts) -- 發送 初始化配置
ioctl(dpp->fd, BLKTRACESTART) -- 發送 啟動配置
ioctl(dpp->fd, BLKTRACESTOP) -- 發送終止配置
ioctl(fd, BLKTRACETEARDOWN) -- 發送down 配置,由內核回寫結果到trace-data文件
這個時候,每一個觸發配置 的ioctl系統調用會進入內核來做一些對應的事情。
這一些邏輯也可以通過strace blktrace -d /dev/nvme0n1命令來追蹤:
open("/dev/nvme0n1", O_RDONLY|O_NONBLOCK) = 3
statfs("/sys/kernel/debug", {f_type=DEBUGFS_MAGIC, f_bsize=4096, f_blocks=0, f_bfree=0, f_bavail=0, f_files=0, f_ffree=0, f_fsid={0, 0}, f_namelen=255, f_frsize=4096, f_flags=ST_VALID|ST_RELATIME}) = 0
rt_sigaction(SIGINT, {0x403410, [INT], SA_RESTORER|SA_RESTART, 0x7fa1a4fd0270}, {SIG_DFL, [], 0}, 8) = 0 # strace main函數注冊的信號
rt_sigaction(SIGHUP, {0x403410, [HUP], SA_RESTORER|SA_RESTART, 0x7fa1a4fd0270}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGTERM, {0x403410, [TERM], SA_RESTORER|SA_RESTART, 0x7fa1a4fd0270}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGALRM, {0x403410, [ALRM], SA_RESTORER|SA_RESTART, 0x7fa1a4fd0270}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGPIPE, {SIG_IGN, [PIPE], SA_RESTORER|SA_RESTART, 0x7fa1a4fd0270}, {SIG_DFL, [], 0}, 8) = 0
ioctl(3, BLKTRACESETUP, {act_mask=65535, buf_size=524288, buf_nr=4, start_lba=0, end_lba=0, pid=0, name="nvme0n1"}) = 0
ioctl(3, BLKTRACESTART)
...
內核調用 ioctl 做的事情
這里不討論ioctl整個系統調用的邏輯,細節還是很多的。主要看一下blktrace 調用ioctl發送相應的state后內核做的事情。
內核代碼版本:3.10.1
ioctl 系統調用針對以上state的處理如下:
int blkdev_ioctl(struct block_device *bdev, fmode_t mode, unsigned cmd,unsigned long arg)
{...case BLKTRACESTART:case BLKTRACESTOP:case BLKTRACESETUP:case BLKTRACETEARDOWN:ret = blk_trace_ioctl(bdev, cmd, (char __user *) arg);break;...
}
通過blk_trace_ioctl的邏輯如下:
int blk_trace_ioctl(struct block_device *bdev, unsigned cmd, char __user *arg)
{struct request_queue *q;int ret, start = 0;char b[BDEVNAME_SIZE];q = bdev_get_queue(bdev);if (!q)return -ENXIO;mutex_lock(&bdev->bd_mutex);switch (cmd) {case BLKTRACESETUP:bdevname(bdev, b);// 初始化配置ret = blk_trace_setup(q, b, bdev->bd_dev, bdev, arg);break;
#if defined(CONFIG_COMPAT) && defined(CONFIG_X86_64)case BLKTRACESETUP32:bdevname(bdev, b);ret = compat_blk_trace_setup(q, b, bdev->bd_dev, bdev, arg);break;
#endifcase BLKTRACESTART:start = 1; // 設置啟動追蹤的標記case BLKTRACESTOP:// 結束追蹤ret = blk_trace_startstop(q, start);break;case BLKTRACETEARDOWN:// 將trace文件拷貝到用戶目錄ret = blk_trace_remove(q);break;default:ret = -ENOTTY;break;}mutex_unlock(&bdev->bd_mutex);return ret;
}
BLKTRACESETUP
啟動的時候會進入到這個函數blk_trace_setup,主要創建以下幾個文件:
- 創建
/sys/kernel/debug/block目錄 - 在上面的目錄下創建一個設備目錄
nvme0n1 - 在設備目錄下創建
dropped文件,如果需要relay-channel flush的話會將這個文件置為true - 為每一個cpu綁定一個trace 文件,接受relay-channel 的請求輸出,一般為
traceid - 注冊
/sys/kernel/debug/tracing/events/block下的events,也就是我們前面看到的請求輸出Event(Q,I,D,C等),其實就是這一些events
代碼如下:
int do_blk_trace_setup(struct request_queue *q, char *name, dev_t dev,struct block_device *bdev,struct blk_user_trace_setup *buts)
{struct blk_trace *old_bt, *bt = NULL;struct dentry *dir = NULL;int ret, i;...mutex_lock(&blk_tree_mutex);if (!blk_tree_root) {blk_tree_root = debugfs_create_dir("block", NULL); // 創建/sys/kernel/debug/block目錄if (!blk_tree_root) {mutex_unlock(&blk_tree_mutex);goto err;}}mutex_unlock(&blk_tree_mutex);dir = debugfs_create_dir(buts->name, blk_tree_root); // 創建/sys/kernel/debug/block/nvme0n1目錄if (!dir)goto err;bt->dir = dir;bt->dev = dev;atomic_set(&bt->dropped, 0);ret = -EIO;bt->dropped_file = debugfs_create_file("dropped", 0444, dir, bt, // 在創建好的目錄下創建dropped文件&blk_dropped_fops);if (!bt->dropped_file)goto err;bt->msg_file = debugfs_create_file("msg", 0222, dir, bt, &blk_msg_fops); // 創建msg文件if (!bt->msg_file)goto err;bt->rchan = relay_open("trace", dir, buts->buf_size, // 為每個cpu創建一個trace文件buts->buf_nr, &blk_relay_callbacks, bt);if (!bt->rchan)goto err;bt->act_mask = buts->act_mask;if (!bt->act_mask)bt->act_mask = (u16) -1;blk_trace_setup_lba(bt, bdev);...if (atomic_inc_return(&blk_probes_ref) == 1)blk_register_tracepoints(); // 注冊并追蹤/sys/kernel/debug/tracing/events/block 的events,內核開始追蹤請求return 0;
err:blk_trace_free(bt);return ret;
}
BLKTRACESTOP
blk_trace_startstop執行blktrace的開關操作,停止過后將per cpu的relay chanel強制flush出來。
int blk_trace_startstop(struct request_queue *q, int start)
{int ret;struct blk_trace *bt = q->blk_trace;
...ret = -EINVAL;if (start) { // 這個標記是BLKTRACESTART的時候設置的,如果沒有抓取結束if (bt->trace_state == Blktrace_setup ||bt->trace_state == Blktrace_stopped) {blktrace_seq++;smp_mb();bt->trace_state = Blktrace_running;trace_note_time(bt); // 用戶可能會傳入一個抓取的時間ret = 0;}} else {if (bt->trace_state == Blktrace_running) {bt->trace_state = Blktrace_stopped;relay_flush(bt->rchan); // relay flush 刷數據到trace文件ret = 0;}}return ret;
}
BLKTRACETEARDOWN
釋放blktrace設置創建的buffer、刪除相關文件節點,并去注冊trace events。
static void blk_trace_cleanup(struct blk_trace *bt)
{blk_trace_free(bt);if (atomic_dec_and_test(&blk_probes_ref))blk_unregister_tracepoints();
}int blk_trace_remove(struct request_queue *q)
{struct blk_trace *bt;bt = xchg(&q->blk_trace, NULL);if (!bt)return -EINVAL;if (bt->trace_state != Blktrace_running)blk_trace_cleanup(bt); // 注銷之前注冊的/sys/kernel/debug/tracing/events/block 的eventsreturn 0;
}
到此整個blktrace 通過ioctl 調度起來自己的任務,并能夠取到自己想要的數據。
總結成如下這一張圖來概述整個blktrace的過程:
當然取數據的過程是通過向內核注冊一些block的events。
接下來我們核心看一下這一些events是如何讓內核將數據給出來的?
內核 調用blk_register_tracepoints 之后做的事情
在這個函數內部會逐個注冊每一個/sys/kernel/debug/tracing/events/block下的事件,這里會通過一個宏定義 進入
#define __DECLARE_TRACE(name, proto, args, cond, data_proto, data_args) \extern struct tracepoint __tracepoint_##name; \ // 這里是聲明一些外部的trace point變量static inline void trace_##name(proto) \ // 定義一些trace point用到的公共函數{ \if (static_key_false(&__tracepoint_##name.key)) \ // 如果打開了trace point__DO_TRACE(&__tracepoint_##name, \ // 便利trace point中的樁函數(外部聲明的樁函數)TP_PROTO(data_proto), \TP_ARGS(data_args), \TP_CONDITION(cond),,); \} \__DECLARE_TRACE_RCU(name, PARAMS(proto), PARAMS(args), \ PARAMS(cond), PARAMS(data_proto), PARAMS(data_args)) \static inline int \register_trace_##name(void (*probe)(data_proto), void *data) \ // 注冊trace point{ \return tracepoint_probe_register(#name, (void *)probe, \data); \} \static inline int \unregister_trace_##name(void (*probe)(data_proto), void *data) \{ \return tracepoint_probe_unregister(#name, (void *)probe, \ // 注銷trace pointdata); \} \static inline void \check_trace_callback_type_##name(void (*cb)(data_proto)) \{ \}
而在block.h中已經預定義好了一些列trace io需要的樁函數,類似如下:
TRACE_EVENT(block_bio_complete,TP_PROTO(struct request_queue *q, struct bio *bio, int error),TP_ARGS(q, bio, error),TP_STRUCT__entry(__field( dev_t, dev )__field( sector_t, sector )__field( unsigned, nr_sector )__field( int, error )__array( char, rwbs, RWBS_LEN)),TP_fast_assign(__entry->dev = bio->bi_bdev->bd_dev;__entry->sector = bio->bi_sector;__entry->nr_sector = bio_sectors(bio);__entry->error = error;blk_fill_rwbs(__entry->rwbs, bio->bi_rw, bio->bi_size);),TP_printk("%d,%d %s %llu + %u [%d]",MAJOR(__entry->dev), MINOR(__entry->dev), __entry->rwbs,(unsigned long long)__entry->sector,__entry->nr_sector, __entry->error)
);
而在我們前面說的blk_register_tracepoints函數中會調用:
ret = register_trace_block_bio_complete(blk_add_trace_bio_complete, NULL); 對block_bio_complete進行注冊,注冊之后相當于上面宏定義中打開了針對當前name的trace point,然后block_bio_complete這個trace event函數會被放在對應的I/O連路上(已經在主要的I/O連路上了,只是如果我們注冊了event,那就會在主體鏈路打印它的追蹤信息),而如果不需要開啟的話也就是不注冊事件函數則基本不消耗性能。
// 電梯調度算法的入口
void __elv_add_request(struct request_queue *q, struct request *rq, int where)
{trace_block_rq_insert(q, rq);blk_pm_add_request(q, rq);...
}
說到打印,這也就是以上tracepoint 的核心目的,內核模塊太多,我們想要將內部調試信息打出來到文件肯定不現實,為了方便調試,這里的trace point就是將內核中各個模塊的printk信息 打印到ring_buffer中,這里面的數據只通過debugfs才能夠獲取到。
blktrace 則會通過blk追蹤器將每個cpu 的ring_buffer數據綁定一個trace-data文件,后續完成追蹤之后將這一些文件從debugfs拷出來。
到此我們大體知道了內核如何將I/O請求的信息暴漏出來給用戶讀取,其實就是維護了系列trace-event,用戶注冊之后就開啟追蹤,內核會在trace-event函數中打印每個請求的情況到一個ring-buffer中,用戶通過debug-fs(這里其實是blktrace 自己去debug-fs)將打印的數據取出來。
當然,內核的trace_event整體的宏設計還是比較復雜的,宏的易讀性雖然不是特別好,但人家能夠在編譯時展開,避免了程序運行時的函數入出棧,對程序執行的效率還是有很大的好處的。
參考
https://blog.csdn.net/geshifei/article/details/94360470
總結
以上是生活随笔為你收集整理的blktrace 工具集使用 及其实现原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 上海欢乐谷万圣节夜场期间地铁接驳车最晚到
- 下一篇: cannot find main mod