bpf map 的数据 expiration 一直很困扰我.
比如我用 XDP + TC 实现了一个 ICMP SNAT, 其中最重要的一步就是维护状态, icmp echo出去的时候记录 (icmp id, dst ipv4) -> (src ipv4) 这条映射, icmp reply 回来的时候查一下 bpf hash 才能找到原始 src ipv4. 那么问题来了, 记录在 map 里的数据多久 expire?
像我这么懒得思考的直接照抄内核参数 net.netfilter.nf_conntrack_tcp_timeout_established = 432000, 默认 5 天, 我知道这是 TCP SNAT 的, 不重要, 重要的是, 怎么实现计时器?
在用户态轮询 map 当然可以, 但是太愚蠢了, 能不能远离中世纪, 30 多年前的人类都已经不再用轮询的方式等事件了啊. 那就用一个额外的 ring buf 事件通知用户态 start / refresh 计时器来避免轮询, 但是这个额外的 daemon 也非常不美好. 你能想象 TCP 四大计时器跑在用户态吗? 我连想都不敢想. 更何况 ring buf 事件的丢失也总是带来惊喜.
搜了一下 bpf timer, 有几个 patch 都是试图在内核态注册 timer 和回调, 正是我想要的, 但是似乎都没有合并. 也不知道当前状态下有没有什么更好的办法.
比如我用 XDP + TC 实现了一个 ICMP SNAT, 其中最重要的一步就是维护状态, icmp echo出去的时候记录 (icmp id, dst ipv4) -> (src ipv4) 这条映射, icmp reply 回来的时候查一下 bpf hash 才能找到原始 src ipv4. 那么问题来了, 记录在 map 里的数据多久 expire?
像我这么懒得思考的直接照抄内核参数 net.netfilter.nf_conntrack_tcp_timeout_established = 432000, 默认 5 天, 我知道这是 TCP SNAT 的, 不重要, 重要的是, 怎么实现计时器?
在用户态轮询 map 当然可以, 但是太愚蠢了, 能不能远离中世纪, 30 多年前的人类都已经不再用轮询的方式等事件了啊. 那就用一个额外的 ring buf 事件通知用户态 start / refresh 计时器来避免轮询, 但是这个额外的 daemon 也非常不美好. 你能想象 TCP 四大计时器跑在用户态吗? 我连想都不敢想. 更何况 ring buf 事件的丢失也总是带来惊喜.
搜了一下 bpf timer, 有几个 patch 都是试图在内核态注册 timer 和回调, 正是我想要的, 但是似乎都没有合并. 也不知道当前状态下有没有什么更好的办法.
我可能想出了 业界第 一个对 overlay upstream 彻底透明的透明代理方案, 容器和虚机都通用, 比 NGINX Plus / mmproxy 这些要侵入到 overlay 内部的方案高到不知道哪里去了.
然后过两天我发现了方案纰漏, 默默删掉了这条 - -
然后过两天我发现了方案纰漏, 默默删掉了这条 - -
ICMP payload 可以随便塞东西, 震惊吧! 我本来还打算发 UDP 来着, 这倒是省事了.
BPF_PROG_TYPE_SCHED_CLS 类型的 bpf 程序其实灵活性比 XDP 大多了, 然而 bcc 貌似只实现了 bpf_redirect_map, 只有 XDP 能用, 所以是残废的.
bpf for network 这块迟早还是要用 libbpf, 和 uprobe-based debugger 不同, bcc 没有带来什么显著的好处, 却不能用众多的 bpf helpers.
我怎么觉得 hping3 是个垃圾软件, ping 能工作的时候它不能, 毕竟是给自己发 SIGALRM 来做进程内事件回调呢, 一种近亲结婚导致的低能感扑面而来.
BPF_PROG_TYPE_SCHED_CLS 类型的 bpf 程序其实灵活性比 XDP 大多了, 然而 bcc 貌似只实现了 bpf_redirect_map, 只有 XDP 能用, 所以是残废的.
bpf for network 这块迟早还是要用 libbpf, 和 uprobe-based debugger 不同, bcc 没有带来什么显著的好处, 却不能用众多的 bpf helpers.
我怎么觉得 hping3 是个垃圾软件, ping 能工作的时候它不能, 毕竟是给自己发 SIGALRM 来做进程内事件回调呢, 一种近亲结婚导致的低能感扑面而来.
今天围观了同事查问题, 大家还是喜欢用猜测式 debug, 猜一个可能的问题, 然后做点改动试试看, 然后继续猜.
比如网络问题, 顺着网线爬就行了. 实例内 ping 不同目标, 先看 arp 能不能解出来, 路由对不对, 然后一路顺着设备抓包, tap -> br-int -> ovs -> genev -> eth0, 如果是 br-int 流表不符合预期, 先用 oftrace 模拟流量, 然后查数据库元数据. 本应该是逐步收敛的过程, 但是很多人都喜欢猜. 排错次数多了, 遇到过类似的问题, 以后猜得越来越快, 美其名曰经验, 其实遇到新问题还是无头苍蝇, 我看就是狗屎.
不要盲目猜, 去搜集线索和证据, 排除可能性, 佐证假设.
比如网络问题, 顺着网线爬就行了. 实例内 ping 不同目标, 先看 arp 能不能解出来, 路由对不对, 然后一路顺着设备抓包, tap -> br-int -> ovs -> genev -> eth0, 如果是 br-int 流表不符合预期, 先用 oftrace 模拟流量, 然后查数据库元数据. 本应该是逐步收敛的过程, 但是很多人都喜欢猜. 排错次数多了, 遇到过类似的问题, 以后猜得越来越快, 美其名曰经验, 其实遇到新问题还是无头苍蝇, 我看就是狗屎.
不要盲目猜, 去搜集线索和证据, 排除可能性, 佐证假设.
我有个不太健康的习惯, 就是相比阅读代码, 我更相信 trace 的结果.
给两个例子.
第一个是 dockerd, 如果你去看代码研究 docker 容器在什么时候生成 veth 的话, 你也许能找到它调用 CNM 的链路, 但是你很可能不会注意到, 这条链路的调用起点不是 dockerd 不是客户端, 而是 runc prehook 用一个命令行回调.
第二个是 tcp time wait 的状态转化, 如果你直接在 torvalds/linux 里搜 ag '\s+=\s+TCP_TIME_WAIT', 会得到如图的结果. 不知道啥是 minisock 不过第二个搜索结果看起来靠谱多了, 去深入研究了一会儿就心满意足地离开了评论区, 但是如果你用 ftrace(1) 或者 kprobe 追溯一下就会发现, 其实 localhost 的连接调用的基本是第一处代码.
四年多以前侯老师批评我 debug 就知道打断点偷懒, 但我还是觉得, 用 debugger / tracing tools 来探索未知世界, 是更有效率更不容易出错的现代做法.
(侯老师永远是我老师, 非黑)
(第二题的更现代解法是用 BPF_PROG_TYPE_SOCK_OPS 类型的 bpf 程序追踪 socket 状态变化, 这样连 kprobe 都不用找)
给两个例子.
第一个是 dockerd, 如果你去看代码研究 docker 容器在什么时候生成 veth 的话, 你也许能找到它调用 CNM 的链路, 但是你很可能不会注意到, 这条链路的调用起点不是 dockerd 不是客户端, 而是 runc prehook 用一个命令行回调.
第二个是 tcp time wait 的状态转化, 如果你直接在 torvalds/linux 里搜 ag '\s+=\s+TCP_TIME_WAIT', 会得到如图的结果. 不知道啥是 minisock 不过第二个搜索结果看起来靠谱多了, 去深入研究了一会儿就心满意足地离开了评论区, 但是如果你用 ftrace(1) 或者 kprobe 追溯一下就会发现, 其实 localhost 的连接调用的基本是第一处代码.
四年多以前侯老师批评我 debug 就知道打断点偷懒, 但我还是觉得, 用 debugger / tracing tools 来探索未知世界, 是更有效率更不容易出错的现代做法.
(侯老师永远是我老师, 非黑)
(第二题的更现代解法是用 BPF_PROG_TYPE_SOCK_OPS 类型的 bpf 程序追踪 socket 状态变化, 这样连 kprobe 都不用找)
产生了一些疯狂且不成熟的想法:
1. 用 XDP + BPF_HASH 在内核做 cache service. 但是 TCP 连接状态是个大问题, 也许可以用 UDP
2. 用 ECMP 做透明三层 LB, 用 BGP 动态更新. 路由是天然的透明"代理"
3. 内核 iptables/netfilter/route 也许可以全部被 XDP + TC + cgroup 级别的 SOCK BPF 代替. 前两者 attach 设备, 后者 attach 进程.
1. 用 XDP + BPF_HASH 在内核做 cache service. 但是 TCP 连接状态是个大问题, 也许可以用 UDP
2. 用 ECMP 做透明三层 LB, 用 BGP 动态更新. 路由是天然的透明"代理"
3. 内核 iptables/netfilter/route 也许可以全部被 XDP + TC + cgroup 级别的 SOCK BPF 代替. 前两者 attach 设备, 后者 attach 进程.
veth 太迷惑了,默认的 veth 都是打开 vlan offload 的,以至于 xdp on veth 根本看不到 vlan tag,而 tcpdump 明明看得一清二楚,简直见鬼了!
好好的 veth 又不是真的网卡你搞这些干嘛啊!
看起来要用 tc 子系统的 bpf 屁事更少,但是 tc 有 tc 的问题,看看这两周能不能解决。
好好的 veth 又不是真的网卡你搞这些干嘛啊!
看起来要用 tc 子系统的 bpf 屁事更少,但是 tc 有 tc 的问题,看看这两周能不能解决。
bpf-helpers(7) 里的 bpf_csum_diff 好像是有 bug 的?它的算法一言以蔽之就是 sum' = ~(~sum + ~from + to), 然而在原始 sum 是通过进位计算的情况下这样计算是不正确的。不是特别确定,毕竟是内核的函数,总不能让我发现内核的 bug 吧,不合理。再多做一点测试。
Welcome to the Black Parade
bpf-helpers(7) 里的 bpf_csum_diff 好像是有 bug 的?它的算法一言以蔽之就是 sum' = ~(~sum + ~from + to), 然而在原始 sum 是通过进位计算的情况下这样计算是不正确的。不是特别确定,毕竟是内核的函数,总不能让我发现内核的 bug 吧,不合理。再多做一点测试。
破案了,果然是我太菜了。
中间种种误导让我越走越远,核心是因为用程序算 checksum 的时候用的小端序,而 wireshare 显示的大端序,更可恶的是:
checksum 的计算与端序无关!
所以用任意端序都可以,但是不能混用。因为 from / to 参数都是 u16, 所以端序决定了是传 0x0800 还是 0x0008,差别很大的!
此外我还忘了两次进位: sum = (csum»16)+(csum&0xffff) 之后还要再 sum += sum»16。
此外进位问题对简便算法 ~(~sum - from + to) 的影响,其实可以通过微操处理好,如果 ~sum - from 变成 0 的话多做一次 1s‘ complement 应该就好。
中间种种误导让我越走越远,核心是因为用程序算 checksum 的时候用的小端序,而 wireshare 显示的大端序,更可恶的是:
checksum 的计算与端序无关!
所以用任意端序都可以,但是不能混用。因为 from / to 参数都是 u16, 所以端序决定了是传 0x0800 还是 0x0008,差别很大的!
此外我还忘了两次进位: sum = (csum»16)+(csum&0xffff) 之后还要再 sum += sum»16。
此外进位问题对简便算法 ~(~sum - from + to) 的影响,其实可以通过微操处理好,如果 ~sum - from 变成 0 的话多做一次 1s‘ complement 应该就好。
这样和波波在之前讨论的 Calico 黑洞问题就实操搞定了:用 XDP 在每台宿主机的 ingress 设备上反弹 ICMP,,用 BPF HASH 通过用户态 daemon 传递数据告知哪些 IP 是管辖的 CIDR 里但是进入了黑洞路由。不影响 BGP 也不改 iptables,非常完美。
https://github.com/jschwinger233/xdp-tutorial/tree/zc/bcc
我花了几天断断续续把 xdp-tutorial 里代码用 bcc 实现了一遍,除了 AF_XDP 和 fib_lookup 两道题,前者感觉繁琐难用不如 redirect 到设备,后者倒是可以用来连接两个二层网络,但我觉得干嘛不直接用 bpf map。
xdp-tutorial 这个项目是开源社区“燃烧自己照亮智障”的又一力作,是真正的共产主义,治病救人,天下为公,人民万岁。唯一的问题是通过填空的方式来教学失去了大量细节,比如 if (__builtin_memcmp() == 0) __builtin_memcpy() 在使用全局 static 变量时是编译不过的,这些很细的点只有自己亲自摸过一遍才能知道,所以我用 bcc 自己从头撸了一遍,少了 libbpf 的细节,但是 xdp 的核心还能保留,感觉是个不错的入门方式,推荐大家也试试看。
我花了几天断断续续把 xdp-tutorial 里代码用 bcc 实现了一遍,除了 AF_XDP 和 fib_lookup 两道题,前者感觉繁琐难用不如 redirect 到设备,后者倒是可以用来连接两个二层网络,但我觉得干嘛不直接用 bpf map。
xdp-tutorial 这个项目是开源社区“燃烧自己照亮智障”的又一力作,是真正的共产主义,治病救人,天下为公,人民万岁。唯一的问题是通过填空的方式来教学失去了大量细节,比如 if (__builtin_memcmp() == 0) __builtin_memcpy() 在使用全局 static 变量时是编译不过的,这些很细的点只有自己亲自摸过一遍才能知道,所以我用 bcc 自己从头撸了一遍,少了 libbpf 的细节,但是 xdp 的核心还能保留,感觉是个不错的入门方式,推荐大家也试试看。
GitHub
GitHub - jschwinger233/xdp-tutorial at zc/bcc
XDP tutorial. Contribute to jschwinger233/xdp-tutorial development by creating an account on GitHub.