如何使用 perf 分析 splice 中 pipe 的容量變化

如何使用 perf 分析 splice 中 pipe 的容量變化

這個文章爲了填上一篇文章的坑的,跟蹤內核函數本來是準備使用 ebpf 的,但是涉及到了低內核版本,只能使用 kprobe 了。

恰好,在搜索東西的時候又看到了 perf,可以使用 perf probe 來完成對內核函數的跟蹤,使用相對寫內核模塊簡單很多,對於排查問題如何能解決就應該儘量挑簡單的方案,所以就它了。

提到 perf 那麼 Brendan Gregg 是繞不過去的,這裏對 perf 只記一些本文使用到的一些東西。

perf 的一些東西

需要先添加探測點,探測點可以通過 /proc/kallsyms 進行查詢,以 splice_to_pipe 爲例

perf probe --add 'splice_to_pipe'

# 如何系統內有 kernel-debuginfo 那麼就可以直接檢測變量的值
perf probe --add 'splice_to_pipe pipe->nrbufs pipe->buffers spd->nr_pages'

在添加探測點後,進行記錄。可以指定對應的 pid 和記錄的時間 30s(等待的過程可以中斷,並且不影響結果)

perf record -e 'probe:splice_to_pipe' -p $(pidof a.out) -gR sleep 30

# 也可以記錄多個事件
perf record -e 'probe:tcp_splice_data_recv,probe:kill_fasync,probe:pipe_wait,probe:sock_spd_release,probe:splice_to_pipe' -p $(pidof a.out) -gR sleep 30

在完成記錄後,將結果展示在命令行中

perf report --stdio

其它的可能用到的

# 查詢已經添加過的探測點
perf probe --list
probe:splice_to_pipe (on splice_to_pipe@fs/splice.c with nrbufs buffers nr_pages)
probe:tcp_splice_data_recv (on tcp_splice_data_recv@net/ipv4/tcp.c with count len)
probe:tcp_splice_data_recv__return (on tcp_splice_data_recv%return@net/ipv4/tcp.c with arg1)

# 刪除已添加的探測點,從 perf probe --list 中獲取
perf probe --del probe:splice_to_pipe

# 查看準確的探測點(顏色區分)
perf probe -L splice_to_pipe

探測點要捕獲變量,需要安裝 kernel-debuginfo,Centos7.9 可以直接從阿里雲下載,速度非常快(有的鏡像源沒有debuginfo,官方的速度太慢)

問題背景

在數據在 24k 字節左右時,低版本內核 3.10.0 調用 splice 會被阻塞,但是在高版本內核 6.1 可以直接返回。
這個問題只需要對 3.10.0版本內核的 splice_to_pipe 做分析(6.1 不會被阻塞),確認 24k 字節數據下 skbuff 的 PAGE 數量

以及引出來的一個問題,調用 splice 只做 fd -> pipe 而不做 pipe -> fd,這個情況都會發生阻塞,但是阻塞觸發的大小不相同

  • 3.10.0 大概在 24k 字節就發生阻塞
  • 6.1.0 大概 200k 字節才發生阻塞,遠大於 65536

這個問題聚焦點在

  • 3.10.0 下和上面那個問題相同,判斷 PAGE 數量,是否大於了 pipe size
  • 6.1.0 需要判斷阻塞之前的兩個點
    • splice 入口的 wait_for_space 是否滿足
    • splice_to_pipe 判斷 PAGE 數量,觀察掛載了幾個頁的數據

分析

問題在 3.10.0 的內核上體現明顯,先對 3.10.0 進行分析。

本機環境

  • 宿主機 Debian12 (6.1.0-10-amd64), CPU i7-12700
  • 虛擬機 CentOS7.9 (3.10.0-1160.62.1.el7.x86_64)
  • QEMU 7.2.4 virt-io

分析 splice 3.10.0內核上阻塞的情況

先對 3.10.0內核入手,大概分析一下 splice_to_pipe 的源碼

// fs/splice.c splice_to_pipe
186 ssize_t splice_to_pipe(struct pipe_inode_info *pipe,
187                        struct splice_pipe_desc *spd)
188 {
198         for (;;) {
206                 if (pipe->nrbufs < pipe->buffers) {
218                         pipe->nrbufs++;
219                         page_nr++;
220                         ret += buf->len;
221
222                         if (pipe->files)
223                                 do_wakeup = 1;
224
225                         if (!--spd->nr_pages)
226                                 break;
227                         if (pipe->nrbufs < pipe->buffers)
228                                 continue;
229
230                         break;
231                 }
232
233                 if (spd->flags & SPLICE_F_NONBLOCK) {
234                         if (!ret)
235                                 ret = -EAGAIN;
236                         break;
237                 }
244
245                 if (do_wakeup) {
246                         smp_mb();
247                         if (waitqueue_active(&pipe->wait))
248                                 wake_up_interruptible_sync(&pipe->wait);
249                         kill_fasync(&pipe->fasync_readers, SIGIO, POLL_IN);
250                         do_wakeup = 0;
251                 }
252
253                 pipe->waiting_writers++;
254                 pipe_wait(pipe);
255                 pipe->waiting_writers--;
256         }
257
260         if (do_wakeup)
261                 wakeup_pipe_readers(pipe);
262
263         while (page_nr < spd_pages)
264                 spd->spd_release(spd, page_nr++);
265
266         return ret;
267 }

之前是懷疑 if (pipe->nrbufs < pipe->buffers) 不滿足而又不滿足 if (spd->flags & SPLICE_F_NONBLOCK),在 pipe_wait(pipe) 中被阻塞。

所以要看的就是

  • pipe->nrbufs, pipe 中已使用的 buffer 數量
  • pipe->buffers, pipe 中總的 buffer 數量
  • spd->nr_pages, socket 中讀取出來數據頁的數量

perf 追蹤單次 splice 24k 字節數據的調用情況

調整測試數據的大小,生成 24k 字節的數據

$ dd if=/dev/zero of=/tmp/1.txt bs=1k count=24
$ ncat -nv 192.168.32.245 10022 < /tmp/1.txt

開始 perf 記錄

[root@localhost ~]# perf probe --add 'splice_to_pipe pipe->nrbufs pipe->buffers spd->nr_pages'
Added new event:
  probe:splice_to_pipe (on splice_to_pipe with nrbufs=pipe->nrbufs buffers=pipe->buffers nr_pages=spd->nr_pages)

[root@localhost ~]# perf record -e 'probe:splice_to_pipe' -p $(pidof a.out) -gR sleep 30
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.017 MB perf.data (1 samples) ]

[root@localhost ~]# perf report --stdio
# Samples: 2  of event 'probe:splice_to_pipe'
# Event count (approx.): 2
# Children      Self  Trace output
# ........  ........  ......................................................
    50.00%    50.00%  (ffffffffa9a811e0) nrbufs=0x0 buffers=0x10 nr_pages=17
		...
    50.00%    50.00%  (ffffffffa9a811e0) nrbufs=0x10 buffers=0x10 nr_pages=2

通過 perf 觀察到 splice_to_pipe 調用了兩次,從 nrbufs 看第一次調用後 pipe 就沒有空間了,再看一次代碼,第一次調用在在 L230 返回,沒有執行後續的邏輯。

// fs/splice.c splice_to_pipe
227                         if (pipe->nrbufs < pipe->buffers)
228                                 continue;
229
230                         break;

並且在 L263 while (page_nr < spd_pages) 這個條件是滿足的,我們完整的追蹤一下這個調用的鏈路,主要跟蹤可能出現循環的邏輯,包括 tcp_read_sock, tcp_splice_data_recv, sock_spd_release 以及阻塞的邏輯 pull_wait

---splice
	system_call_fastpath
	sys_splice
	do_splice_to
	sock_splice_read
	tcp_splice_read
	tcp_read_sock
	tcp_splice_data_recv
	skb_splice_bits
	skb_socket_splice
	splice_to_pipe
	kill_fasync

通過增加觀測點來進行驗證,

perf probe --add 'tcp_read_sock desc->count'
perf probe --add 'tcp_read_sock%return $retval'

perf probe --add 'tcp_splice_data_recv rd_desc->count len offset'
perf probe --add 'tcp_splice_data_recv%return $retval'

perf probe --add 'splice_to_pipe pipe->nrbufs pipe->buffers spd->nr_pages pipe->files pipe->waiting_writers pipe->readers'
perf probe --add 'splice_to_pipe%return $retval'

perf probe --add 'pipe_wait pipe->nrbufs pipe->buffers pipe->files pipe->waiting_writers pipe->readers'
perf probe --add 'sock_spd_release spd->nr_pages i'

perf record -e "$(perf probe --list | awk '{print $1}' | sed ':a;N;$!ba;s/\n/,/g')" -p $(pidof a.out) -gR sleep 30

輸出結果爲:

# Samples: 1  of event 'probe:pipe_wait'
# Children      Self  Trace output
# ........  ........  .....................................................................................
   100.00%   100.00%  (ffffffffa9a57760) nrbufs=0x10 buffers=0x10 files=0x2 waiting_writers=0x1 readers=0x1

# Samples: 1  of event 'probe:sock_spd_release'
# Children      Self  Trace output
# ........  ........  ....................................
   100.00%   100.00%  (ffffffffa9e418a0) nr_pages=1 i=0x10

# Samples: 2  of event 'probe:splice_to_pipe'
# Children      Self  Trace output
# ........  ........  ................................................................................................
    50.00%    50.00%  (ffffffffa9a811e0) nrbufs=0x0 buffers=0x10 nr_pages=17 files=0x2 waiting_writers=0x0 readers=0x1
    50.00%    50.00%  (ffffffffa9a811e0) nrbufs=0x10 buffers=0x10 nr_pages=2 files=0x2 waiting_writers=0x0 readers=0x1

# Samples: 1  of event 'probe:tcp_read_sock'
# Children      Self  Trace output
# ........  ........  .................................
   100.00%   100.00%  (ffffffffa9eb2e50) count=0x100000

# Samples: 2  of event 'probe:tcp_splice_data_recv'
# Children      Self  Trace output
# ........  ........  ........................................................
    50.00%    50.00%  (ffffffffa9eb2a10) count=0x100000 len=0x6000 offset=0x0
    50.00%    50.00%  (ffffffffa9eb2a10) count=0xfa770 len=0x770 offset=0x5890

# Samples: 1  of event 'probe:splice_to_pipe__return'
# Children      Self  Trace output
# ........  ........  ..................................................
   100.00%   100.00%  (ffffffffa9a811e0 <- ffffffffa9e481b7) arg1=0x5890

# Samples: 0  of event 'probe:tcp_read_sock__return'
# Children      Self  Trace output
# ........  ........  ............

# Samples: 1  of event 'probe:tcp_splice_data_recv__return'
# Children      Self  Trace output
# ........  ........  ..................................................
   100.00%   100.00%  (ffffffffa9eb2a10 <- ffffffffa9eb2efb) arg1=0x5890

通過測試結果分析代碼

splice_to_pipe

splice_to_pipe 被調用兩次,返回(splice_to_pipe__return)一次,poll_wait 調用一次,sock_spd_release 調用一次

  • 第一次調用的時候在 fs/splice.c L230 break 返回,沒有進入 poll_wait 邏輯,但是由於數據沒有全部寫入 pipe 中,fs/splice.c L263 while (page_nr < spd_pages) 被調用,觀察 nr_pages=1 i=0x10 到寫入了 16 頁,剩餘 1 頁。觀察 tcp_splice_data_recv__return 寫入 pipe 的數據爲 0x5890.

  • 然後出現了第二次調用,由於沒有空間(nrbufs=0x10 buffers=0x10)再進行寫入 fs/splice.c 206 if (pipe->nrbufs < pipe->buffers) 條件不滿足,直接進入了阻塞邏輯 pull_wait.

  • 第二次調用是第一次剩餘的頁數,重試導致阻塞,觀察代碼發現只要寫入數據至 pipe 中,就會跳出循環不進入阻塞中

    225       if (!--spd->nr_pages)
    226               break;
    227       if (pipe->nrbufs < pipe->buffers)
    228               continue;
    230       break;
    
tcp_splice_data_recv

tcp_splice_data_recv 出現在 tcp_read_sock 的循環中,我們對其調用參數進行分析。

// net/ipv4/tcp.c tcp_splice_data_recv
634 static int tcp_splice_data_recv(read_descriptor_t *rd_desc, struct sk_buff *skb,
635                                 unsigned int offset, size_t len)
636 {                       
637         struct tcp_splice_state *tss = rd_desc->arg.data;
638         int ret;                        
639                                 
640         ret = skb_splice_bits(skb, offset, tss->pipe, min(rd_desc->count, len),
641                               tss->flags);
642         if (ret > 0)            
643                 rd_desc->count -= ret; 
644         return ret;     
645 }

// net/ipv4/tcp.c tcp_read_sock
1458 int tcp_read_sock(struct sock *sk, read_descriptor_t *desc,
1459                   sk_read_actor_t recv_actor)
1460 {
1469         while ((skb = tcp_recv_skb(sk, seq, &offset)) != NULL) {
1470                 if (offset < skb->len) {
1471                         int used;
1472                         size_t len;
1473 
1474                         len = skb->len - offset;
1475                         /* Stop reading if we hit a patch of urgent data */
1476                         if (tp->urg_data) {
1477                                 u32 urg_offset = tp->urg_seq - seq;
1478                                 if (urg_offset < len)
1479                                         len = urg_offset;
1480                                 if (!len)
1481                                         break;
1482                         }
1483                         used = recv_actor(desc, skb, offset, len);
1484                         if (used <= 0) {
1485                                 if (!copied)
1486                                         copied = used;
1487                                 break;
1488                         } else if (used <= len) {
1489                                 seq += used;
1490                                 copied += used;
1491                                 offset += used;
1492                         }

//  50.00%    50.00%  (ffffffffa9eb2a10) count=0x100000 len=0x6000 offset=0x0
//  50.00%    50.00%  (ffffffffa9eb2a10) count=0xfa770 len=0x770 offset=0x5890

第一次調用爲 count 爲 0x100000,是 splice 的 max 參數,從套接字讀出來的字節爲 0x6000,一次性從套接字把數據讀完了,寫入 pipe 的長度爲 0x5890,剩餘 0x770
看起來第二次調用 splice 的情況下,0x770 的數據佔用了兩個 PAGE(nr_pages=2

看起來是 tcp_recv_skb 從套接字讀取的數據沒有把每個 PAGE 佔滿,24576 字節的數據佔用 PAGE 數量爲 18,直接寫入 pipe 就發生了阻塞。

perf 追蹤多次 splice 4k 字節數據的調用情況

這種情況的阻塞是正常的,是爲了觀測 splice 持續可以寫多少數據至 pipe 中

測試數據量保持不變,修改 splice 最大的長度爲 4096,並且不再從 pipe 消費數據。得到的結果如下

ssize_t n = splice(fd, NULL, pipefd, NULL, 1<<20, 0);
調整爲 ->
ssize_t n = splice(fd, NULL, pipefd, NULL, 1<<12, 0);


ssize_t n = splice_pump(pipefd[0], dstfd, in_pipe);
if (n > 0) {
  remain -= n;
  written += n;
}
調整爲 ->
// ssize_t n = splice_pump(pipefd[0], dstfd, in_pipe);
// if (n > 0) {
//   remain -= n;
//   written += n;
// }

使用 perf 跟蹤得到的結果如下:

[root@localhost ~]# perf report --stdio
# Samples: 1  of event 'probe:pipe_wait'
# Children      Self  Trace output                                                                         
# ........  ........  .....................................................................................
   100.00%   100.00%  (ffffffffa9a57760) nrbufs=0x10 buffers=0x10 files=0x2 waiting_writers=0x1 readers=0x1

# Samples: 2  of event 'probe:sock_spd_release'
# Children      Self  Trace output                       
# ........  ........  ...................................
    50.00%    50.00%  (ffffffffa9e418a0) nr_pages=2 i=0x2
    50.00%    50.00%  (ffffffffa9e418a0) nr_pages=2 i=0x3

# Samples: 6  of event 'probe:splice_to_pipe'
# Children      Self  Trace output                                                                                    
# ........  ........  ................................................................................................
    16.67%    16.67%  (ffffffffa9a811e0) nrbufs=0x0 buffers=0x10 nr_pages=3 files=0x2 waiting_writers=0x0 readers=0x1
    16.67%    16.67%  (ffffffffa9a811e0) nrbufs=0x10 buffers=0x10 nr_pages=2 files=0x2 waiting_writers=0x0 readers=0x1
    16.67%    16.67%  (ffffffffa9a811e0) nrbufs=0x3 buffers=0x10 nr_pages=4 files=0x2 waiting_writers=0x0 readers=0x1
    16.67%    16.67%  (ffffffffa9a811e0) nrbufs=0x7 buffers=0x10 nr_pages=3 files=0x2 waiting_writers=0x0 readers=0x1
    16.67%    16.67%  (ffffffffa9a811e0) nrbufs=0xa buffers=0x10 nr_pages=4 files=0x2 waiting_writers=0x0 readers=0x1
    16.67%    16.67%  (ffffffffa9a811e0) nrbufs=0xe buffers=0x10 nr_pages=4 files=0x2 waiting_writers=0x0 readers=0x1

# Samples: 5  of event 'probe:tcp_read_sock'
# Children      Self  Trace output                   
# ........  ........  ...............................
   100.00%   100.00%  (ffffffffa9eb2e50) count=0x1000

# Samples: 10  of event 'probe:tcp_splice_data_recv'
# Children      Self  Trace output                                            
# ........  ........  ........................................................
    10.00%    10.00%  (ffffffffa9eb2a10) count=0x0 len=0x2000 offset=0x4000
    10.00%    10.00%  (ffffffffa9eb2a10) count=0x0 len=0x3000 offset=0x3000
    10.00%    10.00%  (ffffffffa9eb2a10) count=0x0 len=0x4000 offset=0x2000
    10.00%    10.00%  (ffffffffa9eb2a10) count=0x0 len=0x5000 offset=0x1000
    10.00%    10.00%  (ffffffffa9eb2a10) count=0x1000 len=0x2000 offset=0x4000
    10.00%    10.00%  (ffffffffa9eb2a10) count=0x1000 len=0x3000 offset=0x3000
    10.00%    10.00%  (ffffffffa9eb2a10) count=0x1000 len=0x4000 offset=0x2000
    10.00%    10.00%  (ffffffffa9eb2a10) count=0x1000 len=0x5000 offset=0x1000
    10.00%    10.00%  (ffffffffa9eb2a10) count=0x1000 len=0x6000 offset=0x0
    10.00%    10.00%  (ffffffffa9eb2a10) count=0x868 len=0x1868 offset=0x4798

# Samples: 5  of event 'probe:splice_to_pipe__return'
# Children      Self  Trace output                                      
# ........  ........  ..................................................
    80.00%    80.00%  (ffffffffa9a811e0 <- ffffffffa9e481b7) arg1=0x1000
    20.00%    20.00%  (ffffffffa9a811e0 <- ffffffffa9e481b7) arg1=0x798

# Samples: 4  of event 'probe:tcp_read_sock__return'
# Children      Self  Trace output                                      
# ........  ........  ..................................................
   100.00%   100.00%  (ffffffffa9eb2e50 <- ffffffffa9eb3128) arg1=0x1000

# Samples: 9  of event 'probe:tcp_splice_data_recv__return'
# Children      Self  Trace output                                      
# ........  ........  ..................................................
    44.44%    44.44%  (ffffffffa9eb2a10 <- ffffffffa9eb2efb) arg1=0x0
    44.44%    44.44%  (ffffffffa9eb2a10 <- ffffffffa9eb2efb) arg1=0x1000
    11.11%    11.11%  (ffffffffa9eb2a10 <- ffffffffa9eb2efb) arg1=0x798

總共 24k 的數據,splice 被調用了5次,4次返回,阻塞了1次。觀察 pipe 的變化,同樣是最後 nrbufs=0x10 buffers=0x10 nr_pages=2 pipe 已滿導致被阻塞,PAGE 數量也是 18.

自頂向下分析的話,每次調用 splice 會調用一次 tcp_read_sock,然後調用兩次 tcp_splice_data_recv(觀察 probe:tcp_splice_data_recv__returnprobe:tcp_splice_data_recv 裏面 count 的變化),最後一次在 L1487 之前就被阻塞了。

// net/ipv4/tcp.c tcp_read_sock
1484                         if (used <= 0) {
1485                                 if (!copied)
1486                                         copied = used;
1487                                 break;

結論

3.10.0 在數據遠小於 65536 的情況下被阻塞的原因就是 tcp_read_sock 用於讀取數據的頁沒有寫滿 4096 字節,導致佔用的頁數大於 pipe 的容量(16)

TODO

由於 debian12 沒有找到對應的 debuginfo(ubuntu 的 dbgsyms),這裏再挖個坑,後面準備用 fedora39 再跟蹤一波

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章