关于ebpf:网络包的内核漂流记-Part-2-BPF-跟踪-epollEnvoy-事件与调度

7次阅读

共计 20795 个字符,预计需要花费 52 分钟才能阅读完成。

注,原文来自 https://blog.mygraphql.com/zh…。如你看到的转载图片不清,请回到原文。

为何

现代人如同都很忙,忙着跟边远的人社交,却很容易漠视眼前的人事,更别提那些不间接体现出价值的根底认知了。要花工夫认真看一编文章前,都要问一个问题:WHY。这才会有 TLDR; 的呈现。一生学习是个口号,但也仅仅是个口号。看看身边的那些放満书的人,有几个真去浏览?社会人大都有事实地认为,继续学习只应该产生在考试前。在社会卷时,就好好做个社会人。

故事是这样的:
话说,在风口上的微服务 (Micro-Service) 很美妙,云原生 (Cloud Native) 很美妙,服务网格(Istio) 很美妙,旧爱非阻塞事件响应编程(epoll) 很美妙。但呈现性能优化需要的时候,性能工程师会把下面的“美妙”会替换为“简单”。是的,没太多人在架构设计或重构前会花太多工夫在性能上和那些开箱即用的基础架构上。直到一天遇到问题要救火……

终于,为救火,咱们还要看基础架构是如何工作的。但从何动手?间接看源码?git clone,开始读源码?很大可能,最初会迷失在源码的陆地中。

YYDS,Linus Torvalds 说过:

https://lwn.net/Articles/193245/
In fact, I’m a huge proponent(支持者) of designing your code around the data, rather than the other way around, and I think it’s one of the reasons git has been fairly successful (*)

(*) I will, in fact, claim that the difference between a bad programmer and a good one is whether he considers his code or his data structures more important. Bad programmers worry about the code. Good programmers worry about data structures and their relationships.

—— Linus Torvalds

可见,理解软件的数据结构是理解软件运行机理的要害。但集体认为,这对于单体架构软件是正确的。但对于古代具备简单组件架构的零碎来说,只理解数据结构不足够,还要理解数据结构是如何在子系统间流转。

上面,咱们关注一下,Istio/Envoy 下,从内核到用户过程,有什么重要数据结构,数据和事件是如何在子系统间合作,最初实现工作的。理解了这些,在零碎须要调优之时,就有了观察点和优化可能点了。而不是齐全看作黑盒,从网上找各种“神奇”配置来自觉碰运气。

目录

  • 为何
  • 目录
  • 《网络包的内核漂流记》系列介绍

    • 重要:格调、款式、本文的交互浏览形式 📖
    • 术语
  • 跟踪目标架构与环境
  • 内核调度点与合作

    • 线程状态

      • ON/OFF CPU
    • 线程的调度与切换
  • 事件链路初探

    • 应用 offwaketime 探视利用唤醒调用链路

      • 收到 downstream 连贯建设申请
      • 收到 downstream 数据

        • kubelet 发送 TCP 数据到 Envoy, 触发 Envoy 端 socket 的 ReadReady 事件
        • ksoftirqd 线程解决接管到的,发向 Envoy socket 的数据, 触发 ReadReady
  • epoll、内核网络栈、内核线程调度的互动

    • BPF 跟踪程序
    • 跟踪输入

      • Downstream Listener(port:15006, fd=36) 新连贯建设事件唤醒
      • 连贯可读(fd=42) 事件唤醒
  • 结尾

《网络包的内核漂流记》系列介绍

大家晓得,网络数据来源于网线、光纤、无线电波上的比特(bit),而后到网卡,到内核,最初到利用过程 socket。事件如同很简略。但如果是 SRE/DevOps 或是 Performance Engineer,须要做粗疏的监控和优化时,这些显然是不够的。援用本文次要参考作者的原话:

Optimizing and monitoring the network stack is impossible unless you carefully read and understand how it works. You cannot monitor code you don’t understand at a deep level.
除非您仔细阅读并理解其工作原理,否则无奈优化和监控网络堆栈。您无奈深刻监控您不了解的代码。
—— Joe Damato

《网络包的内核漂流记》尝试剖析和跟踪一个网络包在内核各子系统间的流转和触发的合作。

开始前先做个预报,《网络包的内核漂流记》零碎(将)包含:

  • Part 1: 图解网络包接管流程
  • Part 2: BPF 跟踪 epoll/Envoy 事件与调度(本文)

    • 我将演示如何用 bpftrace 踪网络包的内核漂流。用例子阐明,内核接管网络包,网络包在协定栈上的解决,epoll 事件期待和线程的调度机制。

为免吓跑人,还是老套路,多图少代码。不过有的图有点点简单。🚜

重要:格调、款式、本文的交互浏览形式 📖

本文图很多,也很有很多细节间接嵌入到图中。其中有到源码的链接、图例等。能够这样说,我写作的大部工夫不是花在文字上,是在图上,所以用电脑去读图,才是本文的正确关上办法。手机与微信号,只是个引流的阳谋。

尽管不是写书,不过还是阐明一下吧,不然浏览体验不太好。:

  1. 我不打算像八股文的源码剖析文章一样,贴一堆源码,让文章看起来内容很饱满但无趣。我用交互 SVG 图片的的办法去援用源码 😎。
  2. https://blog.mygraphql.com/zh… 的原文是 SVG 图片。如果你是在其它中央看到本文,请转回原文。
  3. 正确浏览 SVG 图片的姿态是浏览器中图片处右键,抉择“新 Tab 中关上图片”。大的 SVG 图片,按下鼠标中键,自在滚动 / 拖动。
  4. SVG 图片能够点击链接,间接跳转到相应内核源码网页,准确到源码行。 是的,你不须要 git clone 那大陀源码 🤠,只须要一个浏览器就能够。如果你在电脑前开双屏,联合源码和图看,置信我,源码不是什么天书,你能看懂大部分的。
  5. 浏览内核源码我用 https://elixir.bootlin.com/li…。这个是很好的内核源码浏览网站,内置源码援用跳转和搜寻性能。

术语

开始前简略过一下术语,以缩小前面的误会:

  • upstream: 流量方向中的角色:[downstream] –> envoy –> [upstream]。这里我防止用中文词 上 / 上游,因为概念上没有对立,也容易和英文误会。
  • downstream: 流量方向中的角色:[downstream] –> envoy –> [upstream]

须要留神的是,upstream 与 downstream 是个绝对于观察者的概念。

如场景: service A – 调用 –> service B – 调用 –> service C :

  • 如果站在 service C 上,咱们在把 service B 叫 downstream;
  • 如果站在 service A 上,咱们把 service B 叫 upstream。
  • TID: 线程 ID
  • PID: 过程 ID
  • ENVOY_PID: Envoy 的 PID
  • enqueue: 入队列尾
  • dequeue: 出队列头
  • fd: socket 的文件描述符

以上只是局部术语,其它术语我将在首次应用时作介绍。

跟踪目标架构与环境

被跟踪的架构很简略:

downstream_pod(downstream_app -> downstream_sidecar) ---HTTP---> target_pod(target_sidecar -> target_app)

即有两个 pod,别离叫 downstream_pod、trace_target_pod。他们均有 app 和 sidecar。跟踪开始后,触发 downstream_apptarget_pod 发送 http 申请。

被跟踪的环境地址信息:

downstream_pod IP: 172.30.207.163

target_pod IP: 172.21.206.207
target_sidecar(Envoy) Inbound listen port: 127.0.0.1:15006
target_sidecar(Envoy) 过程 PID: 4182
target_sidecar(Envoy) 主线程 TID: 4182
target_sidecar(Envoy) 主线程名: envoy
target_sidecar(Envoy) 工作线程 0 TID: 4449
target_sidecar(Envoy) 工作线程 0 名: wrk:worker_0
target_sidecar(Envoy) 工作线程 1 TID: 4450
target_sidecar(Envoy) 工作线程 1 名: wrk:worker_1

target_app endpoint: 172.21.206.207:8080 / 127.0.0.1:8080

内核调度点与合作

在开始 事件链路初探 前,先理解一下基础知识:内核调度点与合作。

Linux 内核调度如果要说分明,是一本书一个章节的。因为自己学识无限,本文的篇幅无限,不想深度开展,但说说根本的还是须要的。

线程状态


图:线程状态图(来自:https://idea.popcount.org/201…)

ON/OFF CPU

🛈 留神,本大节是个题外话,不间接和本文相干,本文不波及 Runnable 状态下 ON/OFF CPU 的剖析,不喜可跳过。

光有线程状态其实对性能剖析还是有余的。对于 Runnable 的线程,因为 CPU 资源有余排队、cgroup CPU limit 超限、等状况,能够再分为:

  • Runnable & ON-CPU – 即线程是可运行的,并且曾经在 CPU 上运行。
  • Runnable & OFF-CPU – 线程是可运行的,但因各种资源有余或超限起因,临时未在 CPU 上运行,排队中。

。Brendan Gregg 的 [BPF Performance Tools] 一书中有这个图:


图:ON/OFF CPU 线程状态图(from [BPF Performance Tools] )

介绍几个术语:

  • voluntary switch: 线程自愿地来到 cpu(offcpu,即不运行),个别来到后,状态会变为 TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE
  • involuntary switch: 线程不自愿地来到 cpu(offcpu,即不运行),个别来到后,状态会还是 RUNNABLE 的。

线程的调度与切换

先上个图吧:


图:内核调度点与合作

图中信息量不少,不必放心。本文只关注红点局部。

如果你和我一样,在第一次看到下面的 finish_task_switchtry_to_wake_up 时一面茫然,那么没关系。在看过 Kaiwan N Billimoria 的 [Linux Kernel Programming] 一书后,终于略懂一二。总结如下。

  1. Process Runing ON CPU (正在 CPU 上运行的线程) 在解决定时触发的 timer interrupt soft IRQ TIMER_SOFTIRQ 调用 task_tick_fair() 去计算 Process Runing ON CPU 是否应该被从新调度(即思考抢占。即因为 CPU 资源有余有优先权更高的线程在期待排队、cgroup CPU limit 超限、等状况。如果线程须要被 off-cpu(抢占)那么会标记 TIF_NEED_RESCHED 位。
  2. Process Runing ON CPU 在以下调度点可能触发实在的调度(即 off-cpu)

    • Calling Blocking SYSCALL – 调用阻塞的零碎调用,如 read/write
    • Exit from SYSCALL – 实现零碎调用,返回用户态之前
    • after hardware interrupt handling – 解决硬件中断多后

事件链路初探

对于 BPF 跟踪和剖析问题的启动步骤,集体有一个小教训,也是从 Brendan Gregg 的 [BPF Performance Tools] 一书得来的。

  1. 不要一开始作太细的方向性的假如。因无限的问题解决工夫不应该一开始就花在能够排除的可能性上。
  2. 所有方向性假如都须要用数据证实

    • [BPF Performance Tools] 一书说能够用 stackcount 等工具

因为这次,我关注的是网络事件如果触发 epoll/Envoy 的事件驱动。其中能够想到,要害两头门路是 Linux 的唤醒机制。如果能够剖析到,期待 epoll 事件产生的利用线程(本例中即 Envoy Worker)是如何被唤醒的,那么就能够向上串联利用,向下串联内核网络栈了。

应用 offwaketime 探视利用唤醒调用链路

offwaketime 是 BCC BPF 工具包的一个内置小工具。它能够记录线程 挂起 时的 函数调用堆栈 ,同时也记录相干 唤醒线程 唤醒 挂起线程 时的 函数调用堆栈。这话写得太学究气了,想接地气,细说,还是移步巨匠大作:Brendan Gregg 的 Linux Wakeup and Off-Wake Profiling。

运行:

python3 ./offwaketime -p $ENVOY_PID

输入很多,重要摘录如下:

收到 downstream 连贯建设申请

kubelet 会连贯 pod 端口做健康检查。所以 kubelet 也是 Envoy 的 Downstream。上面看这个 downstream 如何唤醒 Envoy:

1     waker:           kubelet 1172
2     b'[unknown]'
3     b'[unknown]'
4     b'entry_SYSCALL_64_after_hwframe'
5     b'do_syscall_64'
6     b'__x64_sys_connect' <<<<<<<<<<<<<<< kubelet connect Envoy
7     b'__sys_connect'
8     b'inet_stream_connect'
9     b'release_sock'
10     b'__release_sock'
11     b'tcp_v4_do_rcv' <<<<<<<<<<<<<<<< `sk->sk_backlog_rcv` point to
12     b'tcp_rcv_state_process' <<<<<<<<<<<<<<< `sk->sk_state` == TCP_SYN_SENT
13     b'tcp_rcv_synsent_state_process' <<<<<< read `SYN/ACK` from peer(Envoy) at TCP backlog
14     b'tcp_send_ack'  <<<<<<<<<<< send `ACK`, to finish 3 handshake
15     b'__tcp_send_ack.part.0'
16     b'__tcp_transmit_skb'
17     b'ip_queue_xmit'
18     b'__ip_queue_xmit'
19     b'ip_local_out'
20     b'ip_output'
21     b'ip_finish_output'
22     b'__ip_finish_output'
23     b'ip_finish_output2'
24     b'__local_bh_enable_ip' <<<<<<<<<< Task from user process done, kernel try run SoftIRQ by the way.
25     b'do_softirq.part.0'
26     b'do_softirq_own_stack'
27     b'__softirqentry_text_start'
28     b'net_rx_action'
29     b'process_backlog'
30     b'__netif_receive_skb'
31     b'__netif_receive_skb_one_core'
32     b'ip_rcv' <<<<<<<<<<<<< Receive IP packet(TCP SYNC) from `kubelet` to `Envoy`
33     b'ip_rcv_finish'
34     b'ip_local_deliver'
35     b'ip_local_deliver_finish'
36     b'ip_protocol_deliver_rcu'
37     b'tcp_v4_rcv' <<<<<<<<<<< `Envoy` side listener socket sk->sk_state == TCP_LISTEN
38     b'tcp_child_process'
39     b'sock_def_readable' <<<<<<<<<<< `Envoy` side listener trigger Readable event
40     b'__wake_up_sync_key'
41     b'__wake_up_common_lock'
42     b'__wake_up_common'
43     b'ep_poll_callback' <<<<<<<<<<< `Envoy` side epoll wakeup logic
44     b'__wake_up'
45     b'__wake_up_common_lock'
46     b'__wake_up_common'
47     b'autoremove_wake_function' <<<<<<<<<<< wakeup Envoy's wrk:worker_0 thread
48     try_to_wake_up <<<<<<<<<<< this line not output by `offwaketime`, It is the kprobe of `offwaketime`. I add it here manually for easy to understanding.
49     --               --
50     b'finish_task_switch' <<<<<<<<<<< Envoy's wrk:worker_0 thread block waiting, TASK_INTERRUPTIBLE
51     b'schedule'
52     b'schedule_hrtimeout_range_clock'
53     b'schedule_hrtimeout_range'
54     b'ep_poll'
55     b'do_epoll_wait' <<<<<<<<<<< Envoy's wrk:worker_0 thread waiting on epoll event
56     b'__x64_sys_epoll_wait'
57     b'do_syscall_64'
58     b'entry_SYSCALL_64_after_hwframe'
59     b'epoll_wait'
60     b'event_base_loop'
61     b'Envoy::Server::WorkerImpl::threadRoutine(Envoy::Server::GuardDog&, std::__1::function<void ()> const&)'
62     b'Envoy::Thread::ThreadImplPosix::ThreadImplPosix(std::__1::function<void ()>, absl::optional<Envoy::Thread::Options> const&)::{lambda(void*)#1}::__invoke(void*)'
63     b'start_thread'
64     target:          wrk:worker_0 4449
65         201092

产生时序为:

  • 64 -> 50 行是 Envoy 工作线程 wrk:worker_0 (被唤醒者) 进入挂起状态(TASK_INTERRUPTIBLE)前的函数堆栈。
  • 1 -> 48 行为 唤醒者 (kubelet) 发动唤醒时的函数堆栈。倒序的,为不便和 64 -> 50 行的 被唤醒者 函数堆栈串接。

可见,这次唤醒 Envoy wrk:worker_0 线程的正好是它的 downstream 线程。这是一个特例,只是因为 downstream 线程和 Envoy 运行于同一主机上。真实情况是,唤醒线程能够是主机上的所有无相干的 on-cpu 的线程。上面道来。

收到 downstream 数据
kubelet 发送 TCP 数据到 Envoy, 触发 Envoy 端 socket 的 ReadReady 事件
    waker:           kubelet 169240
    b'[unknown]'
    b'[unknown]'
    b'entry_SYSCALL_64_after_hwframe'
    b'do_syscall_64'
    b'__x64_sys_write'
    b'ksys_write'
    b'vfs_write' <<<<<<<< `kubelet` write socket
    b'__vfs_write'
    b'new_sync_write'
    b'sock_write_iter'
    b'sock_sendmsg'
    b'inet_sendmsg'
    b'tcp_sendmsg'
    b'tcp_sendmsg_locked'
    b'tcp_push'
    b'__tcp_push_pending_frames'
    b'tcp_write_xmit'
    b'__tcp_transmit_skb'
    b'ip_queue_xmit'
    b'__ip_queue_xmit'
    b'ip_local_out'
    b'ip_output'
    b'ip_finish_output'
    b'__ip_finish_output'
    b'ip_finish_output2' <<<<<<< ip level sent done
    b'__local_bh_enable_ip' <<<<<<<<<< Task from user process done, kernel try run SoftIRQ by the way.
    b'do_softirq.part.0'
    b'do_softirq_own_stack'
    b'__softirqentry_text_start'
    b'net_rx_action'
    b'process_backlog'
    b'__netif_receive_skb'
    b'__netif_receive_skb_one_core'
    b'ip_rcv' <<<<<<<<<<<<< Receive IP packet(TCP data) from `kubelet` to `Envoy`
    b'ip_rcv_finish'
    b'ip_local_deliver'
    b'ip_local_deliver_finish'
    b'ip_protocol_deliver_rcu'
    b'tcp_v4_rcv' <<<<<<<<<<< `Envoy` side downstream socket event(downstream TCP data segment)
    b'tcp_v4_do_rcv'
    b'tcp_rcv_established'
    b'tcp_data_queue' <<<<<<<<<<< `Envoy` side downstream socket TCP segment enqueue to backlog
    b'tcp_data_ready'
    b'sock_def_readable' <<<<<<<<<<< `Envoy` side downstream socket raise ReadReady event
    b'__wake_up_sync_key'
    b'__wake_up_common_lock'
    b'__wake_up_common'
    b'ep_poll_callback'
    b'__wake_up'
    b'__wake_up_common_lock'
    b'__wake_up_common'
    b'autoremove_wake_function'
    --               --
    b'finish_task_switch'
    b'schedule'
    b'schedule_hrtimeout_range_clock'
    b'schedule_hrtimeout_range'
    b'ep_poll'
    b'do_epoll_wait' <<<<<<<<<<<<<< `Envoy` epoll waiting
    b'__x64_sys_epoll_wait'
    b'do_syscall_64'
    b'entry_SYSCALL_64_after_hwframe'
    b'epoll_wait'
    b'event_base_loop'
    b'Envoy::Server::WorkerImpl::threadRoutine(Envoy::Server::GuardDog&, std::__1::function<void ()> const&)'
    b'Envoy::Thread::ThreadImplPosix::ThreadImplPosix(std::__1::function<void ()>, absl::optional<Envoy::Thread::Options> const&)::{lambda(void*)#1}::__invoke(void*)'
    b'start_thread'
    target:          wrk:worker_0 4449
        197
ksoftirqd 线程解决接管到的,发向 Envoy socket 的数据, 触发 ReadReady
    waker:           ksoftirqd/1 18
    b'ret_from_fork'
    b'kthread'
    b'smpboot_thread_fn'
    b'run_ksoftirqd'
    b'__softirqentry_text_start'
    b'net_rx_action'
    b'process_backlog'
    b'__netif_receive_skb'
    b'__netif_receive_skb_one_core'
    b'ip_rcv'
    b'ip_rcv_finish'
    b'ip_local_deliver'
    b'ip_local_deliver_finish'
    b'ip_protocol_deliver_rcu'
    b'tcp_v4_rcv'
    b'tcp_v4_do_rcv'
    b'tcp_rcv_established'
    b'tcp_data_ready'
    b'sock_def_readable' <-----https://elixir.bootlin.com/linux/v5.4/source/net/core/sock.c#L2791
    b'__wake_up_sync_key'
    b'__wake_up_common_lock'
    b'__wake_up_common'
    b'ep_poll_callback' <----https://elixir.bootlin.com/linux/v5.4/source/fs/eventpoll.c#L1207
    b'__wake_up'
    b'__wake_up_common_lock'
    b'__wake_up_common'
    b'autoremove_wake_function'
    --               --
    b'finish_task_switch'
    b'schedule'
    b'schedule_hrtimeout_range_clock'
    b'schedule_hrtimeout_range'
    b'ep_poll'
    b'do_epoll_wait'
    b'__x64_sys_epoll_wait'
    b'do_syscall_64'
    b'entry_SYSCALL_64_after_hwframe'
    b'epoll_wait'
    b'event_base_loop'
    b'Envoy::Server::WorkerImpl::threadRoutine(Envoy::Server::GuardDog&, std::__1::function<void ()> const&)'
    b'Envoy::Thread::ThreadImplPosix::ThreadImplPosix(std::__1::function<void ()>, absl::optional<Envoy::Thread::Options> const&)::{lambda(void*)#1}::__invoke(void*)'
    b'start_thread'
    target:          wrk:worker_1 4450
        2066849

还记得下面说的:

可见,这次唤醒 Envoy wrk:worker_0 线程的正好是它的 downstream 线程。这是一个特例,只是因为 downstream 线程和 Envoy 运行于同一主机上。真实情况是,唤醒线程能够是主机上的所有无相干的 on-cpu 的线程。上面道来。

是的,这次是 ksoftirqd/1 去唤醒了。

至此,实现了调用链路初探。

epoll、内核网络栈、内核线程调度的互动

像以前文章一样,咱们先看看跟踪剖析的后果,再看跟踪得出的数据和跟踪用的脚本。这样比拟不便直观地了解整个实现。

一图胜千言,先看图吧。前面缓缓道来。下图是我用跟踪收集到的数据,加上一些认知和推理实现的。


图:Envoy 下 epoll 与内核调度合作

看图办法:

  • 图中有图例(Legend)
  • 图中有数据结构和关系、有函数和函数调用关系

先阐明一下图中的红点步骤:

  1. Envoy 调用 do_epoll_wait,线程过程阻塞期待状态 TASK_INTERRUPTABLE_SLEEP

    • 嗯,是一个带 timeout 的,监听多个 socket 的 epoll 阻塞期待状态
  2. 线程放入 waiting_task_queue 队列
  3. 收到网络包,SoftIRQ 解决,TCP 层认为产生了 TCP socket 的事件,触发了事件和线程唤醒机制
  4. 线程唤醒机制回调到 ep_poll_callback
  5. try_to_wake_up把线程从 waiting_task_queue 队列挪动到 runnable_task_queue 队列。即唤醒了线程,线程变为 RUNNABLE

    • 留神,RUNNABLE 不等于 on-cpu线程还须要期待调度器能力上 cpu。
  6. 同上
  7. 调度器把 RUNNABLE 的线程调入 CPU,变为 on-cpu & runnable。这时:

    1. 之前期待在 do_epoll_wait函数上的线程,持续运行,获取唤醒的事件(fd(socket 的文件描述符) 和 事件类型(Read/Writeable))。写用用户态的内存,以便返回后用户态线程能够读取事件。
    2. 函数返回,线程从内核态返回到用户态。
  8. 线程调用 Envoy::FileEventImpl::assignEvents::eventCallback(fd=$fd) 函数,处理事件,accept/ 读取 / 写入相干 socket。

看到这里,你会问:步骤不多,为何图中画那么多货色,是作者要卖弄学问吗?嗯,或者局部起因是。但更重要的起因是,这些内核数据结构图和函数对前面的 BPF 程序剖析至关重要。

BPF 跟踪程序

有了下面的基础知识,相干上面的 bpftrace 程序就不难理解了:

源程序

#!/usr/local/bin/bpftrace

/*
IMPORT-ENV: $ENVOY_PID
args: $1=ENVOY_PID
*/

/*
cd ~/pub-diy/low-tec/network/kernel-net-stack/bpf-trace-net-stack
export SCRIPT_HOME=`pwd`
export BT=ep_poll_interact.bt
export ENVOY_PID=$PID

./warp-bt.sh
*/

#include <linux/sched.h>
#include <linux/wait.h>
#include <linux/fs.h>
#include <linux/net.h>
#include <linux/socket.h>
#include <net/sock.h>

struct epoll_filefd {
    struct file *file;
    int fd;
};

struct epitem {
    union {
        /* RB tree node links this structure to the eventpoll RB tree */
        struct rb_node rbn;
        /* Used to free the struct epitem */
        struct rcu_head rcu;
    };

    /* List header used to link this structure to the eventpoll ready list */
    struct list_head rdllink;

    /*
     * Works together "struct eventpoll"->ovflist in keeping the
     * single linked chain of items.
     */
    struct epitem *next;

    /* The file descriptor information this item refers to */
    struct epoll_filefd ffd;

    /* Number of active wait queue attached to poll operations */
    int nwait;

    /* List containing poll wait queues */
    struct list_head pwqlist;

    /* The "container" of this item */
    struct eventpoll *ep;

    // /* List header used to link this item to the "struct file" items list */
    // struct list_head fllink;

    // /* wakeup_source used when EPOLLWAKEUP is set */
    // struct wakeup_source __rcu *ws;

    // /* The structure that describe the interested events and the source fd */
    // struct epoll_event event;
};


struct eppoll_entry {
    /* List header used to link this structure to the "struct epitem" */
    struct list_head llink;

    /* The "base" pointer is set to the container "struct epitem" */
    struct epitem *base;

    /*
     * Wait queue item that will be linked to the target file wait
     * queue head.
     */
    struct wait_queue_entry wait;

    // /* The wait queue head that linked the "wait" wait queue item */
    // wait_queue_head_t *whead;
};


BEGIN
{printf("Tracing nanosecond time in off-CPU stacks. Ctrl-C to end.\n");

    // See include/net/tcp_states.h:
    @tcp_states[1] = "ESTABLISHED";
    @tcp_states[2] = "SYN_SENT";
    @tcp_states[3] = "SYN_RECV";
    @tcp_states[4] = "FIN_WAIT1";
    @tcp_states[5] = "FIN_WAIT2";
    @tcp_states[6] = "TIME_WAIT";
    @tcp_states[7] = "CLOSE";
    @tcp_states[8] = "CLOSE_WAIT";
    @tcp_states[9] = "LAST_ACK";
    @tcp_states[10] = "LISTEN";
    @tcp_states[11] = "CLOSING";
    @tcp_states[12] = "NEW_SYN_RECV";    
}

kprobe:ep_poll_callback
{
    // record previous thread sleep time
    $wq_entry = (struct wait_queue_entry *)arg0;
    $u = (uint64)$wq_entry;
    $eppoll_entry = (struct eppoll_entry *) ($u-8-sizeof(struct list_head) );
    $base_epitem = $eppoll_entry->base;
    $file = $base_epitem->ffd.file;
    $fd = $base_epitem->ffd.fd;
    // printf("elapsed=%d: tid=%d,comm=%s: ep_poll_callback: fd=%d, file*=%p\n", 
    //     elapsed, tid, comm, 
    //     $fd, $file);

    $ep = ($base_epitem->ep);

    $socket_file_ops = kaddr("socket_file_ops");
    // printf("socket_file_ops=%p\n", $socket_file_ops);
    
    if($file->f_op != $socket_file_ops) {//not socket file
        // printf("not socket_file_ops, file->f_op=%p, ksym=%s\n", $file->f_op, ksym($file->f_op));
        return;
    }

    $private_data = $file->private_data;
    if(((uint64)$private_data ) == 0 ) {// printf("((uint64)$private_data ) == 0s\n");
        return;
    }

    $socket = (struct socket*) $private_data;

    // printf("socket\n");
    $sk = (struct sock *) $socket->sk;

    $inet_family = $sk->__sk_common.skc_family;
    if ($inet_family != AF_INET) {return;}

    // initialize variable type:
    $daddr = ntop(0);
    $saddr = ntop(0);
    $daddr = ntop($sk->__sk_common.skc_daddr);
    $saddr = ntop($sk->__sk_common.skc_rcv_saddr);
    $lport = $sk->__sk_common.skc_num;
    $dport = $sk->__sk_common.skc_dport;

    
    $dport = $sk->__sk_common.skc_dport;
    // Destination port is big endian, it must be flipped
    $dport = ($dport >> 8) | (($dport << 8) & 0x00FF00);

    $tcp_state = $sk->__sk_common.skc_state;

    @scope_ep_poll_callback[tid]=($fd,$saddr,$lport,$daddr,$dport,$tcp_state,$sk);
}

kretprobe:ep_poll_callback
{delete(@scope_ep_poll_callback[tid]);
}

kprobe:try_to_wake_up
/@scope_ep_poll_callback[tid].0/
{$taskToWakeuped = (struct task_struct *)arg0;

    if($taskToWakeuped->tgid == ${ENVOY_PID} ) {$s = @scope_ep_poll_callback[tid];

        printf("\n");
        printf("***waker: elapsed=%-10u tid=%d,comm=%s: ep_poll_callback: fd=%d, %14s:%-6d %14s:%-6d %6s socket=%p\n", 
            elapsed / 1000000, tid, comm, 
            $s.0, $s.1, $s.2, $s.3, $s.4, @tcp_states[$s.5], $s.6);    

        // @wakeupedTaskToWakeupedTid[$taskToWakeuped->pid]=1;
        printf("try_to_wake_up: wakeupedPID=%d, wakeupedTID=%d, wakeupedTIDComm=%s\n", 
            $taskToWakeuped->tgid, $taskToWakeuped->pid, $taskToWakeuped->comm);    
    }

    // delete(@scope_ep_poll_callback[tid]);
}


tracepoint:syscalls:sys_exit_epoll_wait
/pid == ${ENVOY_PID}/
{if( args->ret <= 0) {// printf("do_epoll_wait retval=%d\n", args->ret);
        return;
    }
    // if(@wakeupedTaskToWakeupedTid[tid]) {printf("--------\n");
        printf("***sleeper-wakeup: elapsed=%-10u, pid=%d, tid=%d,comm=%s, event_count=%d\n", elapsed / 1000000, pid, tid, comm, args->ret);
    // }
    // delete(@wakeupedTaskToWakeupedTid[tid]);
}

uprobe:/proc/${ENVOY_PID}/root/usr/local/bin/envoy:*FileEventImpl*assignEvents*
/pid == ${ENVOY_PID}/ 
{
       $fd = arg0;
        $libevent_events = arg1;

        printf("***** elapsed=%-10u: tid=%d,comm=%s: BEGIN:EventFired:FileEventImpl::assignEvents::eventCallback()\n", 
            elapsed / 1000000, tid, comm);
        printf("FileEventImpl*=%p, fd=%d, events=0x%x\n",arg2, $fd, $libevent_events);

        if($libevent_events & (uint16)0x01 /*EV_TIMEOUT*/ ) {printf("libevent: EV_TIMEOUT\n");
        }
        if($libevent_events & (uint16)0x02 /*EV_TIMEOUT*/ ) {printf("libevent: EV_READ\n");
        }
        if($libevent_events & (uint16)0x04 /*EV_TIMEOUT*/ ) {printf("libevent: EV_WRITE\n");
        }
        if($libevent_events & (uint16)0x20 /*EV_TIMEOUT*/ ) {printf("libevent: EV_ET\n");
        }
        if($libevent_events & (uint16)0x80 /*EV_TIMEOUT*/ ) {printf("libevent: EV_CLOSED\n");
        }

}

kretprobe:inet_csk_accept
/pid==${ENVOY_PID} /
{$sk = (struct sock *)retval;
    $inet_family = $sk->__sk_common.skc_family;

    if ($inet_family == AF_INET) {
        // initialize variable type:
        $daddr = ntop(0);
        $saddr = ntop(0);
        $daddr = ntop($sk->__sk_common.skc_daddr);
        $saddr = ntop($sk->__sk_common.skc_rcv_saddr);
        
        $lport = $sk->__sk_common.skc_num;

        $dport = $sk->__sk_common.skc_dport;
        $qlen  = $sk->sk_ack_backlog;
        $qmax  = $sk->sk_max_ack_backlog;

        // Destination port is big endian, it must be flipped
        $dport = ($dport >> 8) | (($dport << 8) & 0x00FF00);

        printf("inet_csk_accept: %-39s %-5d %-39s %-5d", $daddr, $dport, $saddr, $lport);
    }
}

tracepoint:syscalls:sys_exit_accept4
/pid==${ENVOY_PID} /
{
    $fd = args->ret;
    if($fd < 0) {return;}
    printf("sys_exit_accept4 fd=%d\n", $fd);
}

tracepoint:syscalls:sys_enter_close
/pid==${ENVOY_PID} /
{
    $fd = args->fd;
    printf("close fd=%d\n", $fd);
}

END
{clear(@tcp_states);
}

程序不做解释了,双屏,一个看程序,一个看下面的图,就能够了解到了。

跟踪输入

环境阐明:

downstream_pod IP: 172.30.207.163

target_pod IP: 172.21.206.207
target_sidecar(Envoy) Inbound listen port: 0.0.0.0:15006
target_sidecar(Envoy) 过程 PID: 4182
target_sidecar(Envoy) 主线程 TID: 4182
target_sidecar(Envoy) 主线程名: envoy
target_sidecar(Envoy) 工作线程 0 TID: 4449
target_sidecar(Envoy) 工作线程 0 名: wrk:worker_0
target_sidecar(Envoy) 工作线程 1 TID: 4450
target_sidecar(Envoy) 工作线程 1 名: wrk:worker_1

target_app endpoint: 172.21.206.207:8080 / 127.0.0.1:8080

值得注意的是,wrk:worker_0wrk:worker_1 同时监听在同一个 socket(0.0.0.0:15006) 上,但应用了不同的 fd:

  • wrk:worker_0: fd=36
  • wrk:worker_1: fd=40
Downstream Listener(port:15006, fd=36) 新连贯建设事件唤醒
  1. 线程 swapper/0 在 SoftIRQ 中解决网络包,推到 TCP 层
  2. TCP 层解释包后,实现了 TCP 三次握手,建设了连贯,触发到 fd=36, 0.0.0.0:15006 的连贯建设事件
  3. 调用 try_to_wake_up 函数,唤醒了 worker_0worker_1。从跟踪后果看,一个新 TCP 连贯建设唤醒了两个线程。因两个线程均监听了同一 socket,尽管 fd 不雷同。
  4. worker_0 抢夺了连贯的 socket 事件的处理权。worker_1算是做了无用的唤醒(惊群 thundering herd problem?)。
  5. worker_0 accept socket 建设新的 socket,fd=42


***waker: elapsed=20929      tid=0,comm=swapper/0: ep_poll_callback: fd=40,        0.0.0.0:15006         0.0.0.0:0      LISTEN socket=0xffff9f5e53bfbd40
try_to_wake_up: wakeupedPID=4182, wakeupedTID=4450, wakeupedTIDComm=wrk:worker_1

***waker: elapsed=20929      tid=0,comm=swapper/0: ep_poll_callback: fd=36,        0.0.0.0:15006         0.0.0.0:0      LISTEN socket=0xffff9f5e53bfbd40
try_to_wake_up: wakeupedPID=4182, wakeupedTID=4449, wakeupedTIDComm=wrk:worker_0
--------
***sleeper-wakeup: elapsed=20929     , pid=4182, tid=4449,comm=wrk:worker_0, event_count=1
***** elapsed=20929     : tid=4449,comm=wrk:worker_0: BEGIN:EventFired:FileEventImpl::assignEvents::eventCallback()
FileEventImpl*=0x55af6486fea0, fd=36, events=0x2
libevent: EV_READ
inet_csk_accept: 172.30.207.163                          38590 172.21.206.207                          15006 sys_exit_accept4 fd=42
连贯可读(fd=42) 事件唤醒
***waker: elapsed=20986      tid=10,comm=ksoftirqd/0: ep_poll_callback: fd=42, 172.21.206.207:15006  172.30.207.163:38590  ESTABLISHED socket=0xffff9f5e94e00000
try_to_wake_up: wakeupedPID=4182, wakeupedTID=4449, wakeupedTIDComm=wrk:worker_0
--------
***sleeper-wakeup: elapsed=20986     , pid=4182, tid=4449,comm=wrk:worker_0, event_count=1
***** elapsed=20986     : tid=4449,comm=wrk:worker_0: BEGIN:EventFired:FileEventImpl::assignEvents::eventCallback()
FileEventImpl*=0x55af6496d7a0, fd=42, events=0x22
libevent: EV_READ
libevent: EV_ET
  1. 线程 ksoftirqd/0 在 SoftIRQ 中解决网络包,推到 TCP 层
  2. TCP 层解释包后,发现是现有连贯 socket=0xffff9f5e94e00000 fd=42 的数据,把数据写入 socket buffer。
  3. 调用 try_to_wake_up 函数,唤醒了 worker_0

源输入

结尾

没想到,BPF 最近会成为业界的一个热词。也有幸有前年开始了这个学习之旅。当初业界风行将 BPF/eBPF 用于减速网络,如 Cilium、xx Istio 加速器。也有的用于云监控,如 Pixie。而临时,还未有太多的大的胜利遍及。其起因值得深究。

反观 BCC/bpftrace/libbpf,等。通过多年磨难,曾经比拟成熟。它在跟踪利用和内核行为上体现优异。上生产,能够定位问题,在实验室,能够分析简单的程序。真是入得厨房出得厅堂。是居家旅行的必备良药。

学习上,多年一路走来,内核始终是心中的痛。无论读了多少本内核的书,看了多少优良文章,也是很难串起来一些流程和数据结构。而有了 BPF 后,更多的运行期黑盒变得可视化。如果内核的书和文章让你睡着,那么,试试本人手写和运行 BPF 跟踪内核。察看和实际才是学习的最好办法。当初不缺资讯,缺的是粗浅的领会。

本文是在 2022 年 5.1 假期间实现的。很快乐,还能够卷到当初。或者是趣味,或者是恐怖。总之。不晓得能保持到哪天,那么就好好珍惜明天吧。

正文完
 0