TGTGInsighttelegram intelligenceLIVE / telegram public index
Back to channels
Welcome to the Black Parade avatar

TGINSIGHT CHAT

Welcome to the Black Parade

@TheB1ackParade

Music

Death has many faces, I look forward to seeing this one.

Subscribers787Current channel subscribers
Tracked posts907Indexed post count
Recent reach9,180Sum of recent post views
Recent posts

Recent posts

Page 1 of 76 · 907 posts

Posted Apr 17

把自己用过的网络编程方面的 bpf 梳理了一下(tx 方向的 tcp only),太多细节完全无文档,这就是💻。 [1] cgroup/sock_create 虽然是很好的 hook 用来关联用户态进程和 socket,但是它只能用在主动连接的 tcp socket,因为 passive establish tcp socket 创建时刻是三次握手完成时的内核态,强行读取 struct task 读到的可能是软中断侵入的用户态进程而非最终 accept 的用户态进程。 bpf programming 心智负担高,这是第一条:同一个 bpf prog 可能在不同上下文被触发。 [2] cgroup/connect4 是很好的 per-socket hook 点,此处可以修改 dst ip (LB DNAT 逻辑)、修改 SO_MARK 保证之后的网络包都有特殊的 mark、注入 setsockopt 强绑 ifindex 做路由强选……在之后还会有很多时刻可以用来“重定向流量”,比方用作透明代理流量劫持,但之后的做法都叫做 per-packet,性能优劣一目了然。 [3] skops 除了可以注入 tcp toa 等 optional header 之外,在高版本内核里的还有不少很好的观测性 hook 弥补了传统 tcp info (ss) 的局限,比如 BPF_SOCK_OPS_TSTAMP_SND_SW_CB 用来观测 tcp 在 kernel stack 的 latency。 [4] sk_msg bpf 的 bpf_msg_redirect_hash 虽然实现了 local -> local 的 tcp 流量在三次握手后直接像 pipe 一样发送字节流而完全 bypass tcp stack,看起来美好,但由于 sockmap 的海量恶性 bug,目前慎用,kernel panic。 [5] cgroup_skb/egress 肯定不是唯一可以同时拿到 sk 和 skb 的 bpf,比方说我们可以在 tc/egress 里读取 skb->sk,但它是唯一的单次 attach (cgroup v2 root) 就能全局触发的 bpf,而 tc/egress 需要逐 netdev attach,也要注意 net namespace;我们也可以在任何一个 skb context 的 tracepoint 里 CORE_READ(skb, sk),但是这些 hook 很难高性能读全 GSO-ed skb payload(非线性区),而 cgroup_skb 可以通过 bpf_load_skb_bytes 读到所有的非线性区。这是一个被低估的 bpf hook,有很多想象空间。 [6] lwt_xmit 是另一个可以用来做流量重定向的 hook,但它相比其他 hook 来说已经比较晚了,而且它是 attach 在 routing 上,很难用,我的看法是不要用。 关于顺序和 context 的额外注解: 1. 顺序隐含了很多信息,比方说 nf postrouting > cgroup_skb/egress > arp,这个顺序暗含了: 1.a. cgroup_skb bpf 看到的是 nf NAT 过的 skb,在此时收集观测性数据时用 sk 的 tuple 还是 skb 的 tuple,不同需求有不同设计。 1.b. 还没发生二层解析,skb->data 里不可能有 mac 地址。 1.c. 这个 hook 在 tcp retrans 之后,意味着可能看到重试的 tcp segment,如果要做 seg/ack 统计分析就要重组的准备。 1.d. 这个 hook 只是网络包在网络栈的一个中间步骤,在这里看到一个 tcp 请求不代表它发出去了。 2. user context vs kernel context 我们想要在 tc/egress 上抓 “指定 pid 进程的流量”,第一反应应该是在其中调用 bpf_get_current_task() 或者 bpf_get_current_pid_tgid(),但是这只有在 tc bpf 运行在用户态上下文时才成立,而 tc bpf 如果处理的是 tcp retrans 重试流量,一定处于软中断内核态上下文,那 bpf_get_current_pid_tgid 读到的就是被侵入的 pid。 3. irqsoft enable vs disable (local_bh_disable) 我们想要在在 BPF_PROG_TYPE_NETFILTER bpf 里做一些简单的统计,决定用 percpu_map 避免 CAS 原子操作的性能问题,这在 xdp 和 tc bpf 里很常见,因为 tc bpf 关闭了软中断 local_bh_disable,所以直接非原子地加减 percpu 变量不会被抢占和重入,但在 BPF_PROG_TYPE_NETFILTER 等大部分没有关闭软中断的 bpf 就不行了。 其实 rx 方向还要更复杂一些,因为有 xdp, sk_lookup, sk_reuseport 等更多的 bpf 类型,心智负担更高,但我已经打开了 Diablo2 继续我的专家模式圣骑士了。老板都辞职了我上个屁班 😀

217 views

Posted Apr 14

+1 提交辞职了,把我拉进厕所雅间问我想不想接替他的位置管理团队,顺便安利了一波针灸、正骨和术后调理。 我: 🤯 -> 😭 -> 🙅‍♂️

484 views

Posted Apr 13

人生首个 Java 小震撼: native Java 既拿不到 tcp conn 的 fd (java.net.Socket),也拿不到 OS level 的 thread id (Thread.threadId() 返回的是 Java 线程 ID),我观测性观测个屁。 这就是 Java 的魅力吗,太细腻了 🙏

518 views

Posted Apr 10

考虑下面的简单 go 程序,其中 bpf.o 编译自任意一个简单的需要 CO-RE relocation 的 kprobe 此处就不展示了: func main() { spec, _ := ebpf.LoadCollectionSpec("bpf.o") coll, _ := ebpf.NewCollection(spec) coll.Close() coll = nil spec = nil runtime.GC() debug.FreeOSMemory() log.Fatal(http.ListenAndServe("localhost:6060", nil)) } 用 ps -e -o pid,rss,comm 很惊讶地发现这个进程在强制 gc 后依然占用了 50+M 的 RSS,泄!明明所有 stack var 都被我 close + nil 了! 用 go tool pprof http://127.0.0.1:6060/debug/pprof/heap?gc=1 看一下前五 heap objects: (pprof) top 5 flat flat% sum% cum cum% 18.17MB 38.00% 38.00% 22.68MB 47.42% github.com/cilium/ebpf/btf.readAndInflateTypes 17MB 35.55% 73.55% 17MB 35.55% github.com/cilium/ebpf/btf.indexTypes 4.50MB 9.42% 82.97% 4.50MB 9.42% github.com/cilium/ebpf/btf.readAndInflateTypes.func2 3MB 6.27% 89.24% 3MB 6.27% bufio.(*Scanner).Text (inline) 2.64MB 5.52% 94.76% 5.64MB 11.79% github.com/cilium/ebpf/btf.readStringTable 😅虽然不知道怎么回事,但貌似是 BTF 读进内存后就没 gc 回去,真是谢谢您了。 虽然 pprof/heap 提供这层信息已经很屌了,然而接下来的路依然不好走,我能想到的办法是用 tree 命令看调用栈,然后对每一层调用都用 list/web 仔细检查源码。 比如对于泄露了 20M 的 github.com/cilium/ebpf/btf.readAndInflateTypes, tree 可以看到其中一层栈是: (pprof) tree github.com/cilium/ebpf/btf.readAndInflateTypes [...] 22.68MB 100% | github.com/cilium/ebpf/btf.LoadKernelSpec 0 0% 47.42% 22.68MB 47.42% | github.com/cilium/ebpf/btf.loadKernelSpec 22.68MB 100% | github.com/cilium/ebpf/btf.loadRawSpec 然后用 weblist 检查 LoadKernelSpec 的源码 (pprof) weblist github.com/cilium/ebpf/btf.LoadKernelSpec 看到源码里 35 . . func LoadKernelSpec() (*Spec, error) { 36 . . kernelBTF.RLock() 37 . . spec := kernelBTF.kernel 38 . . kernelBTF.RUnlock() 39 . . 40 . . if spec == nil { 41 . . kernelBTF.Lock() 42 . . defer kernelBTF.Unlock() 43 . . 44 . . spec = kernelBTF.kernel 45 . . } 46 . . 47 . . if spec != nil { 48 . . return spec.Copy(), nil 49 . . } 50 . . 51 . 45.31MB spec, _, err := loadKernelSpec() 52 . . if err != nil { 53 . . return nil, err 54 . . } 55 . . 56 . . kernelBTF.kernel = spec 57 . . return spec.Copy(), nil 58 . . } 注意到 spec := loadKernelSpec() 这一步 alloc 了 45M 内存并返回,但是在返回之前给全局变量 kernelBTF.kernel = spec 设置了一个引用,正是这个引用导致了之后无法 gc 一大坨内存。喜欢吗 😀 以上步骤还是太苦了,尤其是还要人肉找到全局变量的引用,但凡瞎一点就错过了,任劳任怨的 LLM 此时就站了出来。 如果思路再打开一点,相信 golang gc 没有这么恶性的 bug 😀,一定是 lib 自己内部有全局变量拿了一份引用导致无法 gc,那么可以直接去看 elf 里的 bss/data segment 里长得像 cilium/ebpf/*btf* 的符号 $ objdump -t ./main | grep '\.bss\|\.data' | grep -i 'cilium/ebpf.*btf' [...] 0000000000afda20 g O .bss 0000000000000028 github.com/cilium/ebpf/btf.kernelBTF 也能直接找到这个全局变量。 但总得来说我还是觉得有点蛋疼,不知道有没有更好的方法和工具,请各位 golang experts 再次伸出⚪️手交交我

600 views

Posted Apr 9

继续 golang 数码世界大冒险,上次说到 https://go.dev/play/p/kfAeED2Q2mJ 这个程序里的 callback 版本和 direct 版本有巨大的性能差距,群友 Bo 指出这是由于 literal func 导致的,把闭包移动到循环外避免每次循环都创建就可以了: @@ -65,12 +65,13 @@ func consumeCallback(ring eventRing) (int, uint64) { ops int digest uint64 ) + f := func(chunk []byte) error { + digest += binary.LittleEndian.Uint64(chunk[:8]) + ops++ + return nil + } for range totalItems { - err := readFunc(ring, func(chunk []byte) error { - digest += binary.LittleEndian.Uint64(chunk[:8]) - ops++ - return nil - }) + err := readFunc(ring, f) if err != nil { panic(err) } That's a very sharp observation! 📱 但是喜欢随地乱拉闭包的我也很想知道如何在没有参照物 baseline 的情况下单纯通过 perf/pprof 发现这样的性能问题。不要想得太复杂,用常识。 随便做一次 perf record -g + perf report --stdio,发现热点栈是这样的: 99.62% 0.00% purego_callback purego_callback_repro [.] runtime.goexit.abi0 | ---runtime.goexit.abi0 runtime.main main.main | --98.72%--main.consumeCallback2 | |--79.03%--runtime.newobject 稍微敏锐一点应该都能注意到那个 80% runtime.newobject 不对劲,用 gdb disas/m 一下 main.consumeCallback 可以看到: 0x000000000049cb68 <+104>: call 0x413f00 <runtime.newobject> 0x000000000049cb6d <+109>: lea 0x14c(%rip),%rcx # 0x49ccc0 <main.consumeCallback.func1> 说明确实是每次循环都创建了闭包。 但是把闭包移出循环之外,再做一次 perf record,发现依然有 80% 的 runtime.newobject👻 再用 gdb 看一次,发现循环里还有另一个闭包: 0x000000000049cbc0 <+192>: call 0x413f00 <runtime.newobject> 0x000000000049cbc5 <+197>: lea 0x94(%rip),%rcx # 0x49cc60 <main.consumeCallback.readFunc.readWithPoll.consumeCallback.readFunc.func2.func3> 这个闭包看名字就知道是来自: func readFunc(ring eventRing, f func(chunk []byte) error) error { return readWithPoll(func() error { return ring.readRecordFunc(func(chunk []byte) error { return f(chunk) }) }) } 想办法把这个闭包初始化 hack 掉,发现把 ring 显式传入而不是 capture、并且直接 forward closure 而不新创建 closure 就可以了: @@ -24,12 +24,12 @@ func (r *Ring) readRecordFunc(f func(chunk []byte) error) error { return f(r.data[:chunkBytes]) } -func readWithPoll(read func() error) error { +func readWithPoll(ring eventRing, read func() error) error { return read() } func readInto(ring eventRing, rec *[chunkBytes]byte) error { - return readWithPoll(func() error { + return readWithPoll(ring, func() error { return ring.readRecordFunc(func(chunk []byte) error { copy(rec[:], chunk) return nil @@ -38,10 +38,8 @@ func readInto(ring eventRing, rec *[chunkBytes]byte) error { } func readFunc(ring eventRing, f func(chunk []byte) error) error { - return readWithPoll(func() error { - return ring.readRecordFunc(func(chunk []byte) error { - return f(chunk) - }) + return readWithPoll(ring, func() error { + return ring.readRecordFunc(f) }) } 这下性能直接提升了十倍,从 40 Mops/s -> 400+ Mops/s,看 perf record 里也不再有 runtime.newobject,全是干净的业务代码栈: |--97.11%--main.main | | | |--94.43%--main.consumeCallback | | | | | |--48.08%--main.(*Ring).readRecordFunc | | | | | | | --33.64%--main.consumeCallback.func1 我觉得教训还是比较深刻的,我之前确实不知道 go 闭包有这么悲痛的性能,真就随地大小 closure。但 perf 方法论依然可以让我们在不具备相关知识储备的情况下重新发现这个问题,perf 好!

1,570 views

Posted Apr 8

虽然我完全不理解其中的内涵,但是这个算法用来判断两个正则表达式等价性简直让我🐯躯一震。图论的魅力 😭

520 views

Posted Apr 7

我们考虑从某个 fd 读消息数据,我们可能会有这样的 API: func ReadInto(msg) { for { epoll_wait(fd) buf = read(fd) copy(buf, msg) } } 如果我们只是为了从几百字节的消息中读几个字节,那么那 copy 是低效的,我们可以实现一个接受回调函数的 ReadFunc,传入的函数只读我们关心的几个字节: func ReadFunc(f func(buf)) { for { epoll_wait(fd) buf = read(fd) f(buf) } } 注意到 ReadInto 和 ReadFunc 都用一套相同的处理 epoll 的代码,我们也是性成熟的工程师了,必须 DRY,于是抽出了一个 readWithPoll,这样两个函数就写成了 func ReadInto(msg) { readWithPoll(func(buf) { copy(buf, msg) }) } func ReadFunc(f) { readWithPoll(func(buf) { f(buf) }) } 由于各种原因,修改 buf 的部分有个 interface 抽象,所以实际上代码是这样的: func ReadInto(msg) { readWithPoll(func(buf) { Interface.Copy(buf, msg) }) } func ReadFunc(f) { readWithPoll(func(buf) { Interface.ReadFunc(f) }) } 我们注意到 interface.Copy 的逻辑其实也可以用 interface.ReadFunc 来实现,出于降低代码复杂度、减少 interface 的目的,我们决定把 interface.Copy 删掉复用 interface.ReadFunc,这样两个代码的调用链、抽象层级、闭包程度也完全一致: func ReadInto(msg) { readWithPoll(func(buf) { Interface.ReadFunc(func() { copy(buf, msg) }) }) } func ReadFunc(f) { readWithPoll(func(buf) { Interface.ReadFunc(func() { f(buf) }) }) } 以上都是软件工程常规,我们先注意到业务代码里只从几百字节的消息里读几个字节,决定做零拷贝优化,实现了 ReadFunc 传入回调函数;然后做 DRY,抽出共用的 readWithPoll 函数;然后降低代码复杂度,删掉了一个接口函数 interface.Copy 复用新增的 interface.ReadFunc,最后代码结构也很一致。 正片开始,问下面哪个调用的性能更好? A. var msg Msg var acc uint64 for { ReadInto(&msg) acc += msg.Count } B. var acc uint64 for { ReadFunc(func(buf) { acc += uint64(buf[:8]) }) } 在 go1.24.4 x86_64 linux6.17 上,结果大吃一精,零拷贝的性能只有全拷贝的一半 28.87 Mops/s -> 55.22 Mops/s,我们优化了半天零拷贝优化了个寂寞。 go playground 请在本地 taskset 锁单核运行: https://go.dev/play/p/kfAeED2Q2mJ 这个问题比我想象中还要棘手,因为它没有明显异常的指标: 1. 两个版本的 perf stat 显示 IPC 高达 4+,不需要做 micro-bench 2. 两个版本的 perf record 对比,cpu 热点函数占比没有明显区别 3. go build -gcflags='-m=2' 对比虽然有一些差异,但直觉上感受这些差异并不应该造成如此严重的性能回退 唯一比较明显的指标是执行的总指令数,零拷贝版本比全拷贝多了一倍,可以断定性能回退正是来自这里;我也大概可以猜测是闭包 capturing、各种 callback escape 导致的,但缺少直观的观测性证据。 这个性能回退是由于我在做某种优化所以一路都在 benchmark 才发现的,如果一个大型项目可能也正在遭遇相似的腰斩性能,但 perf stat 的 IPC 报告和 perf record 的热点报告都无明显异常,我不知道应该如何观测这种性能回归。如果哪位 golang 专家有相关经验,请伸出圆手帮帮男同🥹

1,320 views

Posted Apr 6

新学了一个词 dryhump。这种词太清水了以至于我阅片无数精通 bareback bukkake gangbang 但直到今天才认识这词。

488 views

Posted Apr 4

白天在湾区之眼逛了一天,居然连一本《习近平谈治国理政》都没看到,有问题🤨 一问湾区之眼:深圳作为改革开放的窗口和排头兵,如此大型书店竟无《习近平谈治国理政》,人民群众迫切需要学习领袖思想,你却拒之门外,仁义何在? 二问湾区之眼:全国上下都在深入学习贯彻习近平新时代中国特色社会主义思想,你作为深圳这座改革开放最前沿城市最大的书店,却独缺此书,击碎读者学习热情,大义何在? 三问湾区之眼:你作为传播先进文化的重要阵地,只为商业私利或别样考量不予上架,礼义何在? 四问湾区之眼:深圳是改革开放的最大受益者,享受中央政策最大红利,党和国家大力推动此书普及,你却视若无睹,不知感恩回报,忠义何在? 五问湾区之眼:读者专程前来寻书却空手而归,你心安理得置之不理,良心何在? 此五问,望速答!

570 views

Posted Apr 2

频道主野外露出

608 views

Posted Mar 31

想象之中的假期: 1. 我悟了,gc 语言原生就支持 rcu,只要 hack 一下 rcu pubsub APIs 就行了,看我 hack 一下 golang 暴打一下 RWMutex. 2. 我不信,toy rcu 没准也能暴打 RWLock,光靠并发读写不阻塞就很炸裂了吧,也快让我试一下。 3. 我好像懂了为何 smp_mb + smp_mb 和 membarrier + barrier 的 order 是一致的,但仔细想想好像又没懂,让我再想想。 4. nogil CPython 终于步入现代 CPU 编程体系了,让我看看有没有什么好玩的,比如:1️⃣ 实际上的假期: 1. I like the heat! 取得了大菠萝2R的 "Complete the game with each class on Hell" 成就,法师是最无聊的职业,刷得我脑壳痛,不如我德。 2. 刘震云的小说虽然有趣地揭示了微小角色在巨大社会系统中的深刻联系,但相比莫言来说缺少时代和命运的震撼交织,我忍不住要用 “经典物理里的箱中刚体粒子” vs "量子世界观" 这个不恰当的比喻。 3. 柏林警察局长将此案报告给宣传部长戈培尔,后者虽然对腐败并不陌生,但还是大为震惊,最后戈培尔亲自向希特勒报告了此事。在日记中,戈培尔写道:“我对此绝不会高兴,腐败如此严重,长此以往必然要危害战争的进程。”希特勒听到宣传部长对此案的揭露,虽然”相当震惊“,却告诫说”不要大惊小怪“,要维护“国家利益”。希特勒在1943年4月2日高速党中央办公室主任,这种诉讼“绝不会发生”。——《纳粹德国的腐败和反腐》 4. 计算的本质——深入剖析程序和计算机,Tom Stuart.

645 views

Posted Mar 27

继昨晚爆出20大(掉率最低的20件装备)“祖父”(300mf 地狱巴尔掉率 1/15792 )之后,今早又掉落20大“风暴尖塔“(300mf 地狱巴尔掉率 1/15989 ),虽然实用性是完全没有的,但刷子游戏的魅力正是在于小概率事件在漫长时空里随机落地的小幸运🥰

1,640 views
123•••5•••10•••15•••20•••25•••30•••35•••40•••45•••50•••55•••60•••65•••70•••7576
PreviousPage 1 of 76Next