这篇解决什么问题
TCP连接追踪的实现原理和eBPF实现细节。
先说结论
连接追踪的核心是状态机,eBPF可以高效实现连接追踪,但需要仔细设计map结构和状态转换逻辑。
现场背景
2024年8月,我们需要一个TCP连接追踪系统。
需求:
- 追踪所有TCP连接
- 记录连接状态变化
- 统计连接时长
- 分析连接失败原因
我最开始的思路
传统方案
考虑的方案:
- conntrack
- iptables -m conntrack
- eBPF实现
为什么不用conntrack
conntrack的优势:
- 功能完善
- 性能好
- 内核原生
但问题:
- 只能追踪内核管理的连接
- 无法获取详细信息
- 无法做复杂分析
查看conntrack信息:
bash
# 查看连接
conntrack -L
# 查看统计
conntrack -S
为什么不用iptables
iptables的优势:
- 配置简单
- 功能丰富
但问题:
- 只能做简单统计
- 无法记录详细信息
- 无法分析连接失败
真正的技术路径
TCP状态机
TCP连接的状态转换:
CLOSED -> SYN_SENT -> SYN_RECEIVED -> ESTABLISHED
ESTABLISHED -> FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT -> CLOSED
ESTABLISHED -> CLOSE_WAIT -> LAST_ACK -> CLOSED
内核中的TCP状态(include/net/tcp_states.h):
c
enum {
TCP_ESTABLISHED = 1,
TCP_SYN_SENT,
TCP_SYN_RECV,
TCP_FIN_WAIT1,
TCP_FIN_WAIT2,
TCP_TIME_WAIT,
TCP_CLOSE,
TCP_CLOSE_WAIT,
TCP_LAST_ACK,
TCP_LISTEN,
TCP_CLOSING,
TCP_NEW_SYN_RECV,
};
eBPF连接追踪实现
设计数据结构:
c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
struct conn_key {
__u32 saddr;
__u32 daddr;
__u16 sport;
__u16 daddr;
};
struct conn_info {
__u8 state;
__u64 start_time;
__u64 last_update;
__u32 packets;
__u64 bytes;
};
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 100000);
__type(key, struct conn_key);
__type(value, struct conn_info);
} conn_map SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 24);
} events SEC(".maps");
追踪连接状态:
c
SEC("tracepoint/sock/inet_sock_set_state")
int trace_sock_set_state(struct trace_event_raw_sys_enter *ctx)
{
struct sock *sk = (struct sock *)ctx->args[0];
__u8 oldstate = ctx->args[1];
__u8 newstate = ctx->args[2];
struct conn_key key = {};
struct conn_info *info;
struct event *e;
key.saddr = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
key.daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr);
key.sport = BPF_CORE_READ(sk, __sk_common.skc_num);
key.dport = bpf_ntohs(BPF_CORE_READ(sk, __sk_common.skc_dport));
info = bpf_map_lookup_elem(&conn_map, &key);
if (!info) {
if (newstate == TCP_SYN_SENT) {
struct conn_info init = {
.state = newstate,
.start_time = bpf_ktime_get_ns(),
.last_update = bpf_ktime_get_ns(),
.packets = 0,
.bytes = 0
};
bpf_map_update_elem(&conn_map, &key, &init, BPF_ANY);
}
return 0;
}
info->state = newstate;
info->last_update = bpf_ktime_get_ns();
if (newstate == TCP_ESTABLISHED && oldstate != TCP_ESTABLISHED) {
e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (e) {
e->type = 1;
e->timestamp = bpf_ktime_get_ns();
e->duration = info->last_update - info->start_time;
bpf_ringbuf_submit(e, 0);
}
}
if (newstate == TCP_CLOSE) {
__u64 duration = bpf_ktime_get_ns() - info->start_time;
e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (e) {
e->type = 2;
e->timestamp = bpf_ktime_get_ns();
e->duration = duration;
bpf_ringbuf_submit(e, 0);
}
bpf_map_delete_elem(&conn_map, &key);
}
return 0;
}
验证过程
测试连接追踪:
bash
# 加载eBPF程序
sudo bpftool prog load conntrack.o /sys/fs/bpf/conntrack
# 测试连接
curl http://192.168.1.100/
# 查看事件
sudo bpftool map dump id 2
输出:
type: 1, timestamp: 1234567890, duration: 100000
type: 2, timestamp: 1234567990, duration: 200000
为什么不用其他方案
为什么不用conntrack
conntrack的优势:
- 内核原生
- 性能好
但问题:
- 无法获取详细信息
- 无法做复杂分析
- 无法自定义逻辑
为什么不用iptables
iptables的优势:
- 配置简单
- 功能丰富
但问题:
- 只能做简单统计
- 无法记录详细信息
- 无法分析连接失败
eBPF的优势
- 性能好(<1% CPU)
- 可以记录详细信息
- 可以做复杂分析
- 可以自定义逻辑
线上注意事项
1. 控制map大小
map大小要合理:
c
// 错误:太大,浪费内存
__uint(max_entries, 1000000);
// 正确:根据实际需求设置
__uint(max_entries, 100000);
2. 定期清理map
map会一直增长,需要定期清理:
c
void cleanup_conn_map(int map_fd)
{
struct conn_key key, next_key;
struct conn_info info;
__u64 current_time = get_current_time_ns();
while (bpf_map_get_next_key(map_fd, &key, &next_key) == 0) {
bpf_map_lookup_elem(map_fd, &next_key, &info);
if (current_time - info.last_update > TIMEOUT) {
bpf_map_delete_elem(map_fd, &next_key);
}
key = next_key;
}
}
3. 处理并发
多CPU并发访问map时要注意:
c
// 错误:没有原子操作
info->packets++;
// 正确:使用原子操作
__sync_fetch_and_add(&info->packets, 1);
4. 监控性能
上线前要做性能测试:
bash
# 测试CPU开销
sudo perf stat -e cycles,instructions,cache-misses -a sleep 10
# 测试延迟
sudo perf record -e cycles:kprobes -a sleep 10
sudo perf report
小结
连接追踪的核心是状态机。
eBPF实现连接追踪的优势:
- 性能好:<1% CPU
- 详细信息:可以记录所有状态变化
- 复杂分析:可以自定义逻辑
- 灵活:可以根据需求定制
实现要点:
- 设计合理的key(五元组)
- 实现状态机
- 定期清理map
- 使用原子操作
工程实践的关键是:理解TCP状态机,设计合理的map结构,处理并发问题。