🔍 登录 注册

eBPF 初探 不改内核代码观测 TCP 重传

小 小卒子 2026-10-23 👁 0 ⏱ 6min

eBPF 初探 不改内核代码观测 TCP 重传

排查线上网络抖动时,最有价值的信息往往是"某次 TCP 重传发生在哪个瞬间、当时是谁的连接、耗时多少"。传统手段要么靠 tcpdump 全量抓包(成本高、隐私重),要么靠 nstat 只看计数。eBPF 可以直接在内核里挂载观测点,把重传事件带上下文吐到用户态,开销在微秒级。

一、能观测什么,从哪观测

Linux 提供的挂载方式里,排查 TCP 最常用三类:

  • kprobe / kretprobe:任意内核函数出入口,通用但依赖内核符号稳定;
  • tracepoint:内核显式定义的稳定观测点,如 tcp:tcp_retransmit_skb;
  • perf event / BPF_PROG_TYPE_SK_*:分别用于性能采样与 socket 局部观测。

TCP 的 tracepoint 已经足够覆盖大部分诊断需求,优先用 tracepoint(内核保证 ABI 稳定):

ls /sys/kernel/debug/tracing/events/tcp/
# tcp_retransmit_skb  tcp_retransmit_queue_updated  tcp_send_reset  tcp_destroyed ...
 perf trace -e tcp:tcp_retransmit_skb

二、第一个程序:统计每分钟各进程重传次数

用 bpftrace 一行起量:

sudo bpftrace -e '
tracepoint:tcp:tcp_retransmit_skb /comm != "cpu_idle"/ {
  @retransmits[comm, pid] = count();
}
interval:s:60 { print(@retransmits); clear(@retransmits); }
'

输出形态:

@retransmits[order-svc, 2241]: 41
@retransmits[kube-proxy, 812]: 3

30 秒就能看到"哪个进程在重传"。这类问题过去要么靠抓包,要么靠"猜 + 重启"。

三、带连接四元组的观测

tracepoint 参数里有 socket,用它取四元组。bpftrace 版本:

tracepoint:tcp:tcp_retransmit_skb {
  $skaddr = (struct sock *)arg0;
  $fam = skc_family($skaddr);
  $sport = skc_sport($skaddr);
  $dport = skc_dport($skaddr);
  if ($fam == 2) {                      // AF_INET
    $src = ntop(skc_daddr($skaddr));
    @retrans[$src, $dport] = count();
  }
}

要点:内核结构体字段随版本变化,跨版本脚本要靠 CO-RE(Compile Once Run Everywhere)+ BTF 做字段重定位。生产脚本应该走 libbpf + BTF,而不是硬编码偏移。

四、用 Go 写一个长期采集器

用 cilium/ebpf,比手写 libbpf 省事得多。

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target amd64 retrans ./retrans.c

package main

import (
	"log"
	"time"

	"github.com/cilium/ebpf/link"
	"github.com/cilium/ebpf/rlimit"
	"github.com/cilium/ebpf/perf"
)

type Event struct {
	Sport, Dport uint16
	Saddr, Daddr uint32
	Pid          uint32
	Comm         [16]byte
}

func main() {
	if err := rlimit.RemoveMemlock(); err != nil {
		log.Fatal(err)
	}
	var objs retransObjects
	if err := loadRetransObjects(&objs, nil); err != nil {
		log.Fatal(err)
	}
	defer objs.Close()

	rd, err := perf.NewReader(objs.Events, os.Getpagesize()*64)
	if err != nil {
		log.Fatal(err)
	}
	defer rd.Close()

	go func() {
		for {
			rec, err := rd.Read()
			if err != nil {
				return
			}
			var ev Event
			if err := binary.Unmarshal(rec.RawSample, &ev); err == nil {
				log.Printf("retrans pid=%d comm=%s src=%s:%d -> dst=%s:%d",
					ev.Pid, cstr(ev.Comm[:]), ip(ev.Saddr), ev.Sport, ip(ev.Daddr), ev.Dport)
			}
		}
	}()
	for {
		time.Sleep(time.Hour)
	}
}

内核侧:

//go:build ignore
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

struct { __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); } events SEC(".maps");

SEC("tracepoint/tcp/tcp_retransmit_skb")
int tp_retrans(struct trace_event_raw_tcp_event_sk_skb *ctx) {
    struct event e = {};
    struct sock *sk = (void *)ctx->skaddr;
    e.pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&e.comm, sizeof(e.comm));
    e.sport = BPF_CORE_READ(sk, __sk_common.skc_num);
    e.dport = bpf_ntohs(BPF_CORE_READ(sk, __sk_common.skc_dport));
    e.saddr = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
    e.daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr);
    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &e, sizeof(e));
    return 0;
}
char LICENSE[] SEC("license") = "GPL";

五、必须知道的安全与稳定性边界

eBPF 程序运行在内核态,写错会拖垮整机,Verifier 是唯一的保护栏,理解它的规则:

  1. 循环必须有界(新版内核支持有界循环,但旧内核禁止);
  2. 不可无界访问内存,读用户内存要 bpf_probe_read_user;
  3. 栈 512 字节上限,大数据必须走 map;
  4. map 大小与类型必须静态声明;
  5. license 必须是 GPL 兼容才可用 helper。

运维上三条硬规矩:

  • 任何 eBPF 工具在预发先跑 24 小时,观察 bpf 相关 CPU 开销(/proc/sys/kernel/perf_event_max_stack 与 bpftool prog show 里的 run_time);
  • 高频 tracepoint(tcp_sendmsg、sys_enter_*)加速率限制或按 pid 过滤,否则单核被打满;
  • 生产用带 BTF 的 CO-RE 程序,禁止跨内核版本硬编码结构体偏移。

六、从观测到闭环

eBPF 最有价值的用法不是"抓事件",而是"按业务标签聚合"。落地形态:

tracepoint → BPF map(按 app/pod 聚合计数)→ 用户态 exporter → Prometheus

自定义指标示例:

tcp_retransmits_total{app="order-svc",pod="order-7d9"} 132
tcp_retransmits_total{app="gateway",  pod="gw-3a2"}    4

有了带业务标签的重传时序,配合上层的请求延迟指标与 Trace,"这个接口 P99 抖动是网络重传导致的"就变成一句话可以证明或证伪的事实,而不是靠经验。

小结

eBPF 的价值是把内核观测从"事后抓包"变成"结构化事件流"。入门的正确顺序:先用 bpftrace 拿直觉(一两行看进程级重传计数),再上 libbpf/cilium-ebpf + CO-RE 写生产 collector,最后把 BPF map 聚合成 Prometheus 指标接入既有告警体系。全程记住两件事:优先用 tracepoint 而非 kprobe,高频事件必须限流。守住这两条,eBPF 就是一台稳定、低成本、可解释的观测机器。

读完了?给作者一点支持

本文采用 CC BY 4.0 协议转载 · © 小卒子

评论(0)

登录 后参与评论。

  • 还没有评论,来抢沙发。

🔗 相关文章