SystemTapを学習します
Google Cloudが技術的な問題をどのように解決していったかということを「DNS パケット欠落のケース: Google Cloud サポート ストーリー」として記事にして公開してくれていましたので
それを題材にSystemTapを勉強してみました。
こちらの記事では net.core.rmem_default の整数オーバーフローが原因でDNS パケットが欠落する問題を解決するために収集した情報やどのように解決に至ったかについて
詳しく説明されています。
cloud.google.com
あらかじめ v5.4のカーネルとdropwatchを用意しておきます。
v5.4のkernelで起動した直後のUDPの統計情報です。
# netstat -su Udp: 0 packets received 0 packets to unknown port received 0 packet receive errors 0 packets sent 0 receive buffer errors 0 send buffer errors IgnoredMulti: 1 UdpLite: IpExt: InBcastPkts: 1 InOctets: 18613 OutOctets: 16002 InBcastOctets: 272 InNoECTPkts: 98 InECT0Pkts: 82 # nstat -a |grep -i udp UdpInDatagrams 3 0.0 UdpOutDatagrams 3 0.0 UdpIgnoredMulti 1 0.0
sysctl -w net.core.rmem_default=$((2**31-1))
を設定し
DNS パケットが欠落する状況を用意してからdig www.google.com
したdropwatchの結果です。
# dropwatch -lkas Initializing kallsyms db dropwatch> start Enabling monitoring... Kernel monitoring activated. Issue Ctrl-C to stop monitoring 16 drops at netlink_broadcast_filtered+257 (0xffffffffadd8e7c7) [software] 1 drops at udp_queue_rcv_one_skb+391 (0xffffffffaddd7061) [software] 1 drops at udpv6_queue_rcv_one_skb+138 (0xffffffffade4f718) [software] 8 drops at netlink_broadcast_filtered+257 (0xffffffffadd8e7c7) [software] 1 drops at udp_queue_rcv_one_skb+391 (0xffffffffaddd7061) [software] 8 drops at netlink_broadcast_filtered+257 (0xffffffffadd8e7c7) [software] 2 drops at udp_queue_rcv_one_skb+391 (0xffffffffaddd7061) [software] 1 drops at udp_queue_rcv_one_skb+391 (0xffffffffaddd7061) [software] 1 drops at __udp4_lib_rcv+ae5 (0xffffffffaddd9185) [software] ^CGot a stop message dropwatch> exit Shutting down ...
receive buffer errorsとpacket receive errorsが増加しているのが分かります。
# netstat -su Udp: 3 packets received 0 packets to unknown port received 6 packet receive errors 9 packets sent 6 receive buffer errors 0 send buffer errors IgnoredMulti: 3 UdpLite: IpExt: InBcastPkts: 3 InOctets: 34027 OutOctets: 27396 InBcastOctets: 816 InNoECTPkts: 223 InECT0Pkts: 175 # nstat -a |grep -i udp UdpInDatagrams 3 0.0 UdpInErrors 6 0.0 UdpOutDatagrams 9 0.0 UdpRcvbufErrors 6 0.0 UdpIgnoredMulti 4 0.0 Udp6InErrors 1 0.0 Udp6OutDatagrams 1 0.0 Udp6RcvbufErrors 1 0.0
dropwatchの結果から udp_queue_rcv_one_skb() を見てみることにします。https://elixir.bootlin.com/linux/v5.4/source/net/ipv4/udp.c#L1993
udp_queue_rcv_one_skb() の実行結果を確認するために __udp_queue_rcv_skb() 及びラベル csum_error と drop にプローブポイントを設定して
別ターミナルでdig www.google.com
してみます。
$ cat stp0.stp #!/usr/bin/stap probe kernel.statement("udp_queue_rcv_one_skb@net/ipv4/udp.c:2085") { printf ("TIME=%s,ProbePoint=%s,func=%s\n",tz_ctime(gettimeofday_s()), pp(),probefunc()) } probe kernel.statement("udp_queue_rcv_one_skb@net/ipv4/udp.c:2088") { printf ("TIME=%s,ProbePoint=%s,func=%s\n",tz_ctime(gettimeofday_s()), pp(),probefunc()) } probe kernel.statement("udp_queue_rcv_one_skb@net/ipv4/udp.c:2090") { printf ("TIME=%s,ProbePoint=%s,func=%s\n",tz_ctime(gettimeofday_s()), pp(),probefunc()) } $ sudo stap -v ./stp0.stp Pass 1: parsed user script and 476 library scripts using 104260virt/90828res/7396shr/83300data kb, in 130usr/30sys/170real ms. Pass 2: analyzed script: 3 probes, 13 functions, 1 embed, 0 globals using 247624virt/226380res/8432shr/226664data kb, in 1120usr/150sys/1268real ms. Pass 3: translated to C into "/tmp/stapfN7K19/stap_91e495a5ab0883939c22b5dffb5db63b_8460_src.c" using 247624virt/226716res/8688shr/226664data kb, in 130usr/90sys/214real ms. Pass 4: compiled C into "stap_91e495a5ab0883939c22b5dffb5db63b_8460.ko" in 4030usr/430sys/4412real ms. Pass 5: starting run. TIME=Thu Dec 10 15:18:44 2020 JST,ProbePoint=kernel.statement("udp_queue_rcv_one_skb@/usr/src/linux-stable/net/ipv4/udp.c:2085"),func=udp_queue_rcv_one_skb TIME=Thu Dec 10 15:18:44 2020 JST,ProbePoint=kernel.statement("udp_queue_rcv_one_skb@/usr/src/linux-stable/net/ipv4/udp.c:2085"),func=udp_queue_rcv_one_skb TIME=Thu Dec 10 15:18:49 2020 JST,ProbePoint=kernel.statement("udp_queue_rcv_one_skb@/usr/src/linux-stable/net/ipv4/udp.c:2085"),func=udp_queue_rcv_one_skb TIME=Thu Dec 10 15:18:54 2020 JST,ProbePoint=kernel.statement("udp_queue_rcv_one_skb@/usr/src/linux-stable/net/ipv4/udp.c:2085"),func=udp_queue_rcv_one_skb ^CPass 5: run completed in 10usr/60sys/27537real ms.
こちらの出力から2085行目 : return __udp_queue_rcv_skb() してることがわかりました。
次に1956行目 : __udp_queue_rcv_skb() を確認してみます。https://elixir.bootlin.com/linux/v5.4/source/net/ipv4/udp.c#L1956
確認すると __udp_queue_rcv_skb() は __udp_enqueue_schedule_skb() を呼び出して結果をrcに代入していますので
1446行目 : __udp_enqueue_schedule_skb() のreturnを確認してみます。https://elixir.bootlin.com/linux/v5.4/source/net/ipv4/udp.c#L1446
$ sudo stap -v -e 'probe kernel.function("__udp_enqueue_schedule_skb").return { printf("%s\n", $$return) }' Pass 1: parsed user script and 476 library scripts using 104260virt/90888res/7456shr/83300data kb, in 150usr/30sys/171real ms. Pass 2: analyzed script: 1 probe, 1 function, 0 embeds, 0 globals using 172580virt/159936res/8580shr/151620data kb, in 830usr/210sys/1092real ms. Pass 3: translated to C into "/tmp/stapsiCjUW/stap_460d03025583944fff84ca26db9eda2b_1217_src.c" using 172580virt/160064res/8708shr/151620data kb, in 30usr/70sys/99real ms. Pass 4: compiled C into "stap_460d03025583944fff84ca26db9eda2b_1217.ko" in 2090usr/500sys/2536real ms. Pass 5: starting run. return=0xfffffffffffffff4 return=0xfffffffffffffff4 return=0xfffffffffffffff4 return=0xfffffffffffffff4 return=0xfffffffffffffff4
0xfffffffffffffff4を返しているので-ENOMEMのようです。
1446行目 : __udp_enqueue_schedule_skb() を詳しくみて行きます。
この関数にはラベル drop と uncharge_drop がありますので関数を使ってプローブポイントをセットしてみます。
$ sudo stap -L 'kernel.function("__udp_enqueue_schedule_skb").label("*")' kernel.function("__udp_enqueue_schedule_skb@/usr/src/linux-stable/net/ipv4/udp.c:1510").label("uncharge_drop") $sk:struct sock* $skb:struct sk_buff* $delta:int $err:int $busy:spinlock_t* $size:int kernel.function("__udp_enqueue_schedule_skb@/usr/src/linux-stable/net/ipv4/udp.c:1513").label("drop") $sk:struct sock* $skb:struct sk_buff* $delta:int $err:int $busy:spinlock_t* $size:int $ cat stp1.stp #!/usr/bin/env stap probe kernel.function("__udp_enqueue_schedule_skb").label("drop") { printf("drop: TIME=%s\n", tz_ctime(gettimeofday_s())) } probe kernel.function("__udp_enqueue_schedule_skb").label("uncharge_drop") { printf("uncharge_drop: TIME=%s\n", tz_ctime(gettimeofday_s())) } $ sudo stap -v ./stp1.stp Pass 1: parsed user script and 476 library scripts using 104256virt/90892res/7464shr/83296data kb, in 180usr/20sys/209real ms. Pass 2: analyzed script: 2 probes, 6 functions, 1 embed, 0 globals using 172576virt/159780res/8388shr/151616data kb, in 860usr/160sys/1013real ms. Pass 3: translated to C into "/tmp/stapJJI9Hg/stap_279afa261f7ee3edcfb4050fbc405982_5786_src.c" using 172576virt/160100res/8708shr/151616data kb, in 50usr/60sys/115real ms. Pass 4: compiled C into "stap_279afa261f7ee3edcfb4050fbc405982_5786.ko" in 2160usr/380sys/2493real ms. Pass 5: starting run. uncharge_drop: TIME=Tue Dec 15 17:57:38 2020 JST drop: TIME=Tue Dec 15 17:57:38 2020 JST uncharge_drop: TIME=Tue Dec 15 17:57:38 2020 JST drop: TIME=Tue Dec 15 17:57:38 2020 JST uncharge_drop: TIME=Tue Dec 15 17:57:38 2020 JST drop: TIME=Tue Dec 15 17:57:38 2020 JST uncharge_drop: TIME=Tue Dec 15 17:57:43 2020 JST drop: TIME=Tue Dec 15 17:57:43 2020 JST uncharge_drop: TIME=Tue Dec 15 17:57:48 2020 JST drop: TIME=Tue Dec 15 17:57:48 2020 JST
uncharge_drop -> drop の順に実行されているのが分かりました。
uncharge_drop にジャンプする条件は以下になります。
1456 rmem = atomic_add_return(size, &sk->sk_rmem_alloc); 1457 if (rmem > (size + sk->sk_rcvbuf)) 1458 goto uncharge_drop;
rmem の値と (size + sk->sk_rcvbuf) の値を確認します。
$ cat stp2.stp %{ #include <net/sock.h> #include <asm/atomic.h> %} function get_rmem:long(sk:long, skb:long) %{ int rmem, size; struct sock *sk = (struct sock *)((long)STAP_ARG_sk); struct sk_buff *skb = (struct sk_buff *)((long)STAP_ARG_skb); size = skb->truesize; rmem = atomic_add_return(size, &sk->sk_rmem_alloc); STAP_RETVALUE = rmem; %} function calc:long(sk:long, skb:long) %{ int size, total; struct sock *sk = (struct sock *)((long)STAP_ARG_sk); struct sk_buff *skb = (struct sk_buff *)((long)STAP_ARG_skb); size = skb->truesize; STAP_RETVALUE = size + sk->sk_rcvbuf; %} probe kernel.statement("__udp_enqueue_schedule_skb") { printf("rmem=%d total=%d\n", get_rmem($sk, $skb), calc($sk, $skb)) } $ sudo stap -v -g ./stp2.stp Pass 1: parsed user script and 476 library scripts using 104260virt/90880res/7448shr/83300data kb, in 130usr/40sys/178real ms. Pass 2: analyzed script: 1 probe, 4 functions, 1 embed, 0 globals using 176940virt/164372res/8624shr/155980data kb, in 890usr/100sys/985real ms. Pass 3: translated to C into "/tmp/stap0FmEiy/stap_424f4d030d8e4bcc70ca1d070427c068_2061_src.c" using 176940virt/164500res/8752shr/155980data kb, in 30usr/70sys/107real ms. Pass 4: compiled C into "stap_424f4d030d8e4bcc70ca1d070427c068_2061.ko" in 2210usr/360sys/2513real ms. Pass 5: starting run. rmem=768 total=-2147482881 rmem=768 total=-2147482881 rmem=768 total=-2147482881 rmem=1536 total=-2147482881 rmem=2304 total=-2147482881 ^CPass 5: run completed in 30usr/60sys/45548real ms.
size + sk->sk_rcvbufの計算結果が-2147482881となっているのでオーバーフローしていそうです。
sizeとsk->sk_rcvbufの値をそれぞれ確認してみます。
$ cat stp3.stp %{ #include <net/sock.h> #include <asm/atomic.h> %} function get_rmem:long(sk:long, skb:long) %{ int rmem, size; struct sock *sk = (struct sock *)((long)STAP_ARG_sk); struct sk_buff *skb = (struct sk_buff *)((long)STAP_ARG_skb); size = skb->truesize; rmem = atomic_add_return(size, &sk->sk_rmem_alloc); STAP_RETVALUE = rmem; %} function calc:long(sk:long, skb:long) %{ int size, total; struct sock *sk = (struct sock *)((long)STAP_ARG_sk); struct sk_buff *skb = (struct sk_buff *)((long)STAP_ARG_skb); size = skb->truesize;; STAP_RETVALUE = size + sk->sk_rcvbuf; %} function get_size:long(sk:long, skb:long) %{ struct sock *sk = (struct sock *)((long)STAP_ARG_sk); struct sk_buff *skb = (struct sk_buff *)((long)STAP_ARG_skb); STAP_RETVALUE = skb->truesize; %} probe kernel.statement("__udp_enqueue_schedule_skb") { printf("rmem=%d %d=%d+%d\n", get_rmem($sk, $skb), calc($sk, $skb), get_size($sk, $skb), $sk->sk_rcvbuf) } $ sudo stap -g -v ./stp3.stp Pass 1: parsed user script and 476 library scripts using 104264virt/90936res/7504shr/83304data kb, in 170usr/20sys/185real ms. Pass 2: analyzed script: 1 probe, 6 functions, 1 embed, 0 globals using 176948virt/164364res/8608shr/155988data kb, in 870usr/130sys/999real ms. Pass 3: using cached /root/.systemtap/cache/2b/stap_2b032db9bfd3b47319c8ecdca4d96dda_2476.c Pass 4: using cached /root/.systemtap/cache/2b/stap_2b032db9bfd3b47319c8ecdca4d96dda_2476.ko Pass 5: starting run. rmem=768 -2147482881=768+2147483647 rmem=768 -2147482881=768+2147483647 rmem=768 -2147482881=768+2147483647 rmem=1536 -2147482881=768+2147483647 rmem=2304 -2147482881=768+2147483647 ^CPass 5: run completed in 20usr/50sys/22017real ms.
sizeとsk->sk_rcvbuf は int なので整数オーバーフローが起き計算結果が -2147482881 となりDNS パケット欠落の原因がオーバーフローによるものであることが確認できました。
まとめ
SystemTapの学習のためGoogle Cloudのサポートブログを参考にスクリプトを作成しながら カーネルの動作を追ってみました。
- ソースコードを変更しなくても良いので敷居は低そうです。
- printkデバッグのようにカーネルを再構築をする必要がないので時間の節減になります。
- 大変有益な記事を公開してくれたGoogle Cloudさんありがとう。
参考にさせてもらいました。
Linuxカーネルのデバッグでprintk(9)デバッグを卒業したいっ! - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモ
第585回 SystemTapを活用してネットワークの動作を確認する:Ubuntu Weekly Recipe|gihyo.jp … 技術評論社