TGINSIGHT CHAT
Welcome to the Black Parade
@TheB1ackParade
MusicDeath has many faces, I look forward to seeing this one.
Recent posts
Page 2 of 76 · 907 posts
Posted Mar 24
cgroup/sock_release (BPF_PROG_TYPE_CGROUP_SOCK - BPF_CGROUP_INET_SOCK_RELEASE) 这个 bpf hook 实在太坑了,不管怎么读文档 (https://docs.ebpf.io/linux/program-type/BPF_PROG_TYPE_CGROUP_SOCK/) 能应该理解成这是 struct sock 生命周期结束的 hook,参数也是 struct sock 没有问题。 但是!仔细阅读内核发现它的调用路径大致是这样的: close(fd) -> sock_release() -> inet_release() -> BPF_CGROUP_RUN_PROG_INET_SOCK_RELEASE(sk) -> tcp_close(sk, timeout) 这就已经暴露出两个问题了: 1. cgroup sock release hook 是和用户态进程 close(fd) 挂钩的,也就是说如果没有用户态 fd 光有内核态 sk 的时候,并不会触发 cgroup hook。 什么时候没有用户态 fd 只有内核态 sk?太多了,几乎发生在每一次三次握手,因为 3whs 和 accept syscall 是两个步骤,三次握手在内核态完成后就有了 sk,但等到不知何时用户态调用 accept 才会有 fd,在此期间如果进程崩溃,sk 直接从内核态关闭,不会触发 cgroup hook。 2. cgroup hook 运行在前,而 tcp_close(sk) 运行在后,这里有个微小的 race condition,在两者之间如果有 bpf iter 扫描 sk,这些 sk 虽然看起来正常,skc->skc_state == TCP_ESTABLISHED,但实际已经处于关闭路径上,而且已经调用过了 cgroup hook。这个 window 看起来微小,实际上在稍大点的集群上跑起来很容易就遇到了。 反思一下,其实 cgroup sock release 的正确语义是 “struct socket 销毁的 hook” 而不是 "struct sock 销毁的 hook”,两者的微小语义差别(其实挺大的)导致若没有准确理解触发时机就会惊喜连连。内核实在太美好了,不写文档,全靠信息差成为专家,没错一定是这样的。 bonus: tp_btf/tcp_destroy_sock
Posted Mar 23
计算机实在太好玩了,睡前随便看了一点文档让我兴奋得睡不着 🥹 https://man7.org/linux/man-pages/man2/membarrier.2.html (然而三天小长假我爽完了28小时的暗黑2重制版,纯招死灵老大爷逛街逛通了地狱,计算机实在太好玩啦🤬
Posted Mar 20
虽然最近没有太多分享欲,因为下班时间过于充实了,到家先玩 Diablo2(好玩到时光倒流,我在童年流连忘返),然后看 Invincible 漫画(感情细腻,群像精彩,想象力也不错,节奏也很抓人),然后做锻炼(年度计划之一是让胸更大),洗澡后用平板看编程书(虽然智力平平只能慢读慢想慢消化,但我完全不焦虑,每有会意便欣然忘食,感叹计算机的神奇有趣),然后就已经到 1am 之后了,必须立刻睡觉…… 但是还是有很多有趣的发现,比如: 1. cgroup/sock_release (BPF_CGROUP_INET_SOCK_RELEASE) 这个 bpf hook 常常被用来作为 socket 生命周期结束的回调,比方说,比方说哈,我有一个 bpf_iter/tcp 的程序,每分钟扫一次 userspace ipv4 tcp sockets 并加入一个 map,然后在 cgroup/sock_release 里从 map 里删除 socket,理论上已经包圆了 socket lifetime,map 里加入的 socket 应该都会随着 fd close 而删除。结果你猜怎么着,居然 socket lick 了,啊不是,leak 了,原因是在 process 调用 close(fd),触发 cgroup/sock_release bpf 之后,socket (tcp state == ESTABLISHED) 依然有一小段时间是可以被 bpf_iter/tcp 扫到的,在这个 race window 里不幸被加入 map 的 socket 已经错失了 cgroup/sock_release hook,永不释放。 ref: https://elixir.bootlin.com/linux/v6.12.77/source/net/ipv4/af_inet.c#L419 2. 用户态的 bpf map iteration syscall command BPF_MAP_GET_NEXT_KEY,是不保证能够完整 dump 出 map 的,因为在 userspace iter map 的时候很可能内核态 bpf prog 会并发新增 map entry (都不需要删),导致 internal bucket 变化,导致 iter 到重复的 key、漏 key、提前终止、啥都可能。用 BPF_MAP_LOOKUP_BATCH 单一 syscall 全量 dump map 会好很多,因为会锁 bucket,所以不会漏 key。这里也是一个使用 RCU 的完美场景,正好和最近的学习搭上了。 ref: https://elixir.bootlin.com/linux/v6.12.77/source/kernel/bpf/hashtab.c#L1717 3. 内核虽然没有一个 watch cgroup v2 增删 dir 的 syscall,但 tracepoint:cgroup:cgroup_mkdir / rawtracepoint:vmlinux:cgroup_mkdir 系列提供了很好的功能,使用下来也没有槽点,在 k8s 集群里只看 cgroup path basename 就能解出 container ID,然后 socket 在内核里又天然和 cgroup 强绑定的,再借助 cgroup/skb hook,我们可以建立起 skb -> sk -> cgroup -> container -> pod 这一串信息,非常可靠,比用 pid / task 匹配可靠多了,现已成为我最爱的观测数据。 内核的美,我的泪,我情愿和你化作一团火焰,啊啊啊啊~啊啊啊啊~~~ 4. go 内存调优,肉搏了两个星期,现在都还有点想吐,从 150M 降到了 30M,虽然好玩,但每次看到 panel 上看到内存快要爆炸的时候内心都很绝望,顺奸理解了 rust 龙虾人,啊不是,螃蟹人。 虽然这些大部分的东西我已有耳闻,但真正落地变成工程才发现竟有这么多我并不知道的细节,我很享受这种把飘在心中的知识用脚在地上踩实的感觉,对自己的工程师身份深感自豪。
Posted Mar 16
我只是习惯性 PUA 一下 AI,发了一句: › are you sure this doesn't break functionality, and it cuts the cpu use 并非真的质疑,只是想触发一下 gpt 的批判性思考,与此同时我切到另一个终端自己做性能测试去了,结果自己还没测完切回来发现它连 benchmark test 都做完了。 太强大了,有点不能接受 🥹
Posted Mar 13
每个游戏都曾让我茶饭不思夜不能寐,闭上眼睛一回想时间就会倒流。计算机实在带给了我太多快乐,舍不得,这个爱,你是一生一世不会了改。
Posted Mar 7
学会了 spinlock 的正确写法,加一个 while (*xp == 1) 的内部 spin 原地提速 30%👍 --- a/bad.c +++ b/good.c @@ -34,8 +34,10 @@ static inline int read_once(const xchglock_t *p) static inline void xchg_lock(xchglock_t *xp) { - while (xchg(xp, 1) == 1) - ; + while (xchg(xp, 1) == 1) { + while (read_once(xp) == 1) + ; + } } 原因是 cacheline bouncing,perf c2c 检查可知好版本的 HITM 只有坏版本的 1/5。太高级了。 ref: perfbook 7.3.1
Posted Mar 7
发现我最爱的 UNP 关于 tcp socket 如何处理 RST 的描述居然是错的。Linux 处理 tcp reset 是看链接状态的: void tcp_reset(struct sock *sk) { trace_tcp_receive_reset(sk); /* We want the right error as BSD sees it (and indeed as we do). */ switch (sk->sk_state) { case TCP_SYN_SENT: sk->sk_err = ECONNREFUSED; break; case TCP_CLOSE_WAIT: sk->sk_err = EPIPE; break; case TCP_CLOSE: return; default: sk->sk_err = ECONNRESET; } ... 所以有以下三种典型情况: 1. 主动连接方发送 tcp syn,对端并没有监听目标端口,返回 tcp reset,connect syscall 返回 ECONNREFUSED 即 connection refused. 2. 主动关闭方先发一个 fin,被动关闭方收到 fin 之后继续发送 tcp payload,这是合法的 tcp 半关闭单向通道;被动关闭方第一次 send 会成功发送,但主动端如果已经 **全关闭**,会返回 tcp reset;被动方收到 reset 后第二次 send 会返回 EPIPE 并伴随 SIGPIPE。 3. 主动关闭方由于一些原因(linger、 非空 recvbuf on close)直接发送 tcp reset,被动方的 send syscall 会直接返回 ECONNRESET。write syscall 也会 ECONNRESET,但要注意 tcp reset 也是入 recvbuf 的,必须先消费掉 payload。 UNP 只描述了场景2,但是 1 和 3 其实也很常见,而且 2 和 3 很容易混淆,本频道之前讨论过几次 tcp reset / SIGPIPE 就完全稀里糊涂。虽然区分出来好像也没特别大的帮助,但给人一种很专业的样子😎
Posted Mar 6
枪花巡演😭有人和我一起去野裸吗🤬
Posted Feb 25
有趣啊有趣 🤩我觉得 saka 奥老师会喜欢这 AUTOREAP 和 AUTOKILL。我至今回想起 wait 和 SIGCHLD 那些莫名其妙的 kā kā gó gó 总觉得自己沉浸在脚臭的微醺里,房间里人人都在以自己更能吸脚臭而自豪。 https://lwn.net/SubscriberLink/1059673/4c66147b1b92e237/
Posted Feb 24
QC (t.me/QC_Grove) 刚才教我,perf mem record + perf annotate --data-type 可以看到 struct 里面的每一个 field 被采样了多少次,然后就可以把热的凑一块。 我随手拿 https://go.dev/play/p/-hMDKDWXbGE 这个 go 程序试了一下,结果非常雅,请看截图。 perf 实在是太权威了,我还要再细品。
Posted Feb 24
perf report 有时候会解析不了用户态符号,潜在的原因有很多,比如目标进程已死、mnt namespace、奇怪的 buildid cache,总之 perf report 输出是下面这样的: 100.00% 0.00% go_10cpu go_10cpu [.] 0x000000000046f641 | ---0x46f641 0x43a2cb 0x4a2226 我固然精通符号 但在玩了几个小时的 perf buildid-cache、 perf report --symfs、namespace 魔术且听 AI 瞎指挥之后依然无法成功,恼羞成怒,想到一个好😤的解决方法。 我们直接看 perf record 尝试从哪里去读 elf 来解析符号 strace -e trace=file -fTtt -- perf report -g --stdio 过滤一下结果里 buildid / build-id / 二进制名字,比如我本地 ubuntu 2404 能找到 16:43:51.422168 newfstatat(AT_FDCWD, "/root/.debug/.build-id/e8/bdccd3a32295d2545a52e78648834e3f0e3c34/elf", {st_mode=S_IFREG|0755, st_size=2411431, ...}, 0) = ENOENT (No such file or directory) 在 eks 节点 6.1.159-181.297.amzn2023.x86_64 上能看到 08:45:02.012542 newfstatat(AT_FDCWD, "/usr/lib/debug/go_10cpu.debug", {st_mode=S_IFREG|0755, st_size=3707232, ...}, 0) = ENOENT (No such file or directory) 那就把二进制文件(从容器里、镜像里、 /proc/$PID/root/ 里)拷贝过去,比如对于我上述的 eks 节点就是 cp /proc/$(pidof go_10cpu)/root/go_10cpu /usr/lib/debug/go_10cpu.debug 然后 perf report 就正常了 100.00% 100.00% go_10cpu go_10cpu [.] main.burnFor | ---runtime.goexit.abi0 runtime.main main.main main.burnFor 测试发现对 static linked / static-pie linked 都没问题,目标进程死后也能解析。动态链接没测过,但我目测找到 perf-report 要从那里读 libc.so 再手动考虑过去就行了。 这套方法也可以用来做符号分离,比如在生产运行 stripped elf,perf record 不可能解析出符号;但是发版时同步编译了 debug 版本的 elf,那把 .debug 放到 perf report 要读的目录下就可以了。
Posted Feb 22
有什么 metrics 可以让我们知道「B因为A北改动从而在 cache 中实效了」?