
上面提到的Remote、Total和RequestQueue分別表示RemoteTimeMs、TotalTimeMs和RequestQueueTimeMs這3個JMX指標,即副本備份時間(設置了acks=-1)、請求處理總時間以及請求在隊列中的等待時間。具體來說,remote time就是leader broker等待所有follower副本拉取消息的時間。顯然,只有在acks=-1下才會有remote time;RequestQueueTimeMs指客戶端發過來的PRODUCE請求在broker端的請求隊列中待的時間,由於請求隊列是使用阻塞隊列實現的,所以如果該值過大的話,說明broker端負載過大,需要增大請求隊列的大小,即增加參數queued.max.requests的值。
global target_pid = KAFKA_PID global target_dev = DATA_VOLUME probe ioblock.request { if (rw == BIO_READ && pid() == target_pid && devname == target_dev) { t_ms = gettimeofday_ms() + 9 * 3600 * 1000 // timezone adjustment
printf("%s,%03d: tid = %d, device = %s, inode = %d, size = %d\n", ctime(t_ms / 1000), t_ms % 1000, tid(), devname, ino, size) print_backtrace() print_ubacktrace() } }
不了解SystemTap也沒關系,上面的腳本就是打印Kafka進程讀磁盤時內核態和用戶態的棧trace,輸出如下:
Thu Dec 22 17:21:39 2016,209: tid = 126123, device = sdb1, inode = -1, size = 4096 0xffffffff81275050 : generic_make_request+0x0/0x5a0 [kernel]
0xffffffff81275660 : submit_bio+0x70/0x120 [kernel]
0xffffffffa036bcaa : _xfs_buf_ioapply+0x16a/0x200 [xfs]
0xffffffffa036d95f : xfs_buf_iorequest+0x4f/0xe0 [xfs]
0xffffffffa036db46 : _xfs_buf_read+0x36/0x60 [xfs]
0xffffffffa036dc1b : xfs_buf_read+0xab/0x100 [xfs]
0xffffffffa0363477 : xfs_trans_read_buf+0x1f7/0x410 [xfs]
0xffffffffa033014e : xfs_btree_read_buf_block+0x5e/0xd0 [xfs]
0xffffffffa0330854 : xfs_btree_lookup_get_block+0x84/0xf0 [xfs]
0xffffffffa0330edf : xfs_btree_lookup+0xbf/0x470 [xfs]
0xffffffffa032456f : xfs_bmbt_lookup_eq+0x1f/0x30 [xfs]
0xffffffffa032628b : xfs_bmap_del_extent+0x12b/0xac0 [xfs]
0xffffffffa0326f34 : xfs_bunmapi+0x314/0x850 [xfs]
0xffffffffa034ad79 : xfs_itruncate_extents+0xe9/0x280 [xfs]
0xffffffffa0366de5 : xfs_inactive+0x2f5/0x450 [xfs]
0xffffffffa0374620 : xfs_fs_clear_inode+0xa0/0xd0 [xfs]
0xffffffff811affbc : clear_inode+0xac/0x140 [kernel]
0xffffffff811b0776 : generic_delete_inode+0x196/0x1d0 [kernel]
0xffffffff811b0815 : generic_drop_inode+0x65/0x80 [kernel]
0xffffffff811af662 : iput+0x62/0x70 [kernel]
0x37ff2e5347 : munmap+0x7/0x30 [/lib64/libc-2.12.so]
0x7ff169ba5d47 : Java_sun_nio_ch_FileChannelImpl_unmap0+0x17/0x50 [/usr/jdk1.8.0_66/jre/lib/amd64/libnio.so]
關於這段輸出,Yuto並沒有給出過多的解釋。不過我特意標紅了第一行和最后兩行:
- 第一行中最重要的就是tid信息,即線程ID。我們需要記下這個ID:126123
- 最后這兩行告訴我們目前Kafka進程下某個讀磁盤的線程正在執行munmap系統調用試圖刪除某段地址空間的內存映射。雖然進程結束時該映射會被自動取消,但如果只是在程序中關閉了文件部署符(file descriptor)——比如調用File.close(),那么這段映射是不會自動被移除的。被標紅的第二行顯示出了是哪段Java代碼執行的這個系統調用。從輸出中我們已知是由Java的FileChannelImpl的unmap方法發出的。
$ grep 0x1ecab /tmp/jstack.dump
"Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007ff278d0c800 nid=0x1ecab in Object.wait() [0x00007ff17da11000]
$ grep --text 'Total time for which application threads were stopped' kafkaServer-gc.log | grep --text '11T01:4' | perl -ne '/^(.*T\d{2}:\d{2}).*stopped: ([0-9\.]+)/; $h{$1} += $2 * 1000; END { print "$_ = $h{$_}\n" for sort keys %h }'
2017-01-11T01:40 = 332.0601
2017-01-11T01:41 = 315.1805
2017-01-11T01:42 = 318.4317
2017-01-11T01:43 = 317.8821
2017-01-11T01:44 = 302.1132
2017-01-11T01:45 = 950.5807 # << 這里非常耗時
2017-01-11T01:46 = 344.9449
2017-01-11T01:47 = 328.936
2017-01-11T01:48 = 296.3204
2017-01-11T01:49 = 299.2834
- 索引文件僅僅被VFS(虛擬文件系統)標記為“已刪除”。由於對應的映射對象沒有被清除,所以依然有引用指向該文件,故物理文件塊仍然在磁盤上
- Kafka的索引對象OffsetIndex在JVM中變為不可達,此時可以被GC收集器回收
- GC線程開始工作清除MappedByteBuffer對象,並執行對應的刪除映射操作
- OffsetIndex對象通常都會在堆上保留很長時間(因為日志段文件通常都會保留很長時間),所以該對象應該早就被提升到老年代了
- OffsetIndex.delete()操作將文件標記為“已刪除”,VFS稍后會更新inode緩存
- 由於G1收集器是以region為單位收集的,且它通常都會挑選包含了最多“垃圾”的region進行收集,所以包含mmap對象的region通常都不屬於這類region,從而有可能很長時間都不會被收集
- 在沒有被收集的這段時間內,inode緩存被更新,正式把標記為“已刪除”的那個索引文件元數據信息“踢出”緩存
- 最終當GC開始回收mmap對象並調用munmap時,內核就需要執行物理刪除IO操作。但此時,該索引文件的元數據信息已經從緩存中被剔除,因此文件系統就必須要從磁盤中讀取這些信息。而同一時刻Kafka磁盤正在用於高速寫操作(producer還在飛速運行着),所以讀磁盤操作就遭遇競爭從而拉長整體GC時間。對Kafka broker來說,表現為所有Kafka線程全部暫停,極大地拉長了請求的平均響應時間

那條粗線代表bug的修復點。如圖所示,在那之后請求的平均響應時間非常穩定,再也沒有毛刺的現象出現,這足以說明這次修復是有效的。縱觀整個bug的修復過程,Yuto對於Kafka索引文件機制、JVM GC和操作系統VFS都有很深的造詣,我自己也在這個過程中學到了很多東西。今總結出來以供大家討論。最后貼一份Kafka committer Ijuma關於此bug的評論,足見此問題的分量:
