1. 从一次线上告警说起为什么要自定义网络层指标去年下半年我负责维护的一个跨区域数据同步服务突然出现同步延迟飙升的问题。应用层的日志一切正常CPU、内存、磁盘IO这些常规指标也都在阈值之内但下游消费方就是收不到数据。排查了将近两个小时最后定位到是某条网络链路的重传率在特定时段飙升到了8%以上而这条链路恰好承载了核心同步流量。问题在于我们当时的监控体系里网络层只有最基础的带宽进出和连接数根本没有重传、RTT分位值、连接建立耗时这些细粒度指标。这件事之后我花了大概三周时间把整个服务网格里的网络层可观测性做了一次系统性补全。这篇文章就是那次实践的完整总结包括我为什么选择自定义指标而不是直接用现成的指标怎么设计采集怎么落地以及踩过的那些坑。如果你也在维护分布式系统、微服务架构或者任何对网络质量敏感的服务这套思路应该能直接复用。不管你是刚接触可观测性的新手还是已经用过一些监控工具的老手我都会尽量把每个决策背后的逻辑讲清楚让你能根据自己的场景做调整。2. 网络层监控的整体设计思路2.1 为什么现成的网络指标不够用大多数监控方案默认提供的网络指标基本停留在网卡级别每秒发送/接收字节数、每秒包量、错误包数、丢包数。这些指标有用但粒度太粗。它们回答不了下面这些问题我的服务调用某个下游时网络往返时间RTT的P99是多少TCP连接建立的成功率如何有没有大量SYN重传某个特定端口的连接队列是否经常溢出重传导致的额外延迟占整体延迟的比例有多大这些问题的答案才是定位网络相关性能瓶颈的关键。网卡级别的指标只能告诉你“网络有问题”但没法告诉你“问题出在哪条链路、哪个方向、哪个时间段”。另一个现实问题是很多监控代理默认采集的指标维度是主机维度而不是服务维度。在容器化和微服务环境下一台主机上可能跑着几十个服务每个服务的网络行为差异巨大。用主机维度的指标去推断某个具体服务的网络质量误差会大到没法用。2.2 自定义指标的核心设计原则我在设计这套指标时定了几个原则后来证明这些原则让整个方案的可维护性好了很多。第一指标要能直接关联到服务身份。每条网络指标都必须带上服务名、实例ID、对端服务名这些标签。没有这些标签指标就是一堆无法归因的数字。第二优先采集“影响用户体验”的指标。不是所有内核暴露的网络计数器都值得采集。我重点关注的是那些能直接解释延迟、错误率、吞吐量变化的指标。比如TCP重传次数、连接建立耗时、RTT分布这些和用户体验直接挂钩。第三采集开销要可控。网络指标采集本身不能成为性能瓶颈。我试过全量采集内核所有TCP统计结果在高流量实例上采集进程的CPU占用直接飙到15%以上。后来改成按需采集关键指标开销降到了2%以内。第四指标要能跨语言、跨框架通用。我们的服务有Go、Java、Python三种语言如果每种语言都单独实现一套采集逻辑维护成本太高。所以最终方案是基于内核暴露的通用接口来采集和具体语言无关。2.3 方案选型的几个关键取舍在具体实现上有几个选择需要提前想清楚。采集点放在哪里可选的位置有三个应用进程内部、Sidecar代理、宿主机内核。应用进程内部采集最精确但需要每种语言都实现Sidecar代理对应用透明但会增加一跳延迟宿主机内核采集最通用但需要处理容器网络命名空间的隔离问题。我最终选择了宿主机内核采集 容器网络命名空间关联的方案兼顾通用性和精确性。用拉模式还是推模式拉模式Prometheus风格实现简单但采集频率受限于拉取间隔推模式StatsD风格可以做到更高频率但需要处理推送失败和重复的问题。考虑到网络指标对实时性要求较高我选择了推模式采集间隔设为5秒。指标粒度怎么定太粗了没用太细了存储成本爆炸。我的经验是按“服务对”聚合即每个服务到每个下游服务的网络指标单独记录。这个粒度既能定位到具体链路又不会产生过多的时间序列。3. 核心指标的定义与采集原理3.1 TCP重传相关指标TCP重传是网络质量问题最直接的信号。内核在/proc/net/snmp和/proc/net/netstat里暴露了丰富的重传统计但直接读这些文件有几个坑。首先是命名空间问题。在容器环境下每个容器有自己的网络命名空间/proc/net/snmp看到的是当前命名空间的数据。要采集某个容器的网络指标需要先进入它的网络命名空间。我用的方法是读取/proc/pid/ns/net拿到命名空间文件描述符然后用setns系统调用切换进去再读。其次是计数器溢出问题。内核的TCP统计是32位无符号整数在高流量场景下会回绕。我遇到过一台机器上TCPRetransSegs在一天内回绕了两次导致计算出的重传率出现负值。解决办法是采集时同时记录原始值和采集时间戳在服务端做差值计算时处理回绕。具体采集的字段包括字段名含义采集来源TCPRetransSegs重传的报文段总数/proc/net/snmpTCPOutSegs发送的报文段总数/proc/net/snmpTCPInSegs接收的报文段总数/proc/net/snmpTCPLostRetransmit丢失的重传报文数/proc/net/netstatTCPFastRetrans快速重传次数/proc/net/netstatTCPTimeouts超时重传次数/proc/net/netstat重传率的计算公式是重传率 (TCPRetransSegs差值 / TCPOutSegs差值) * 100%。这里要注意分母用发送报文段总数而不是接收数因为重传是发送侧的行为。注意TCPLostRetransmit这个指标特别有价值它表示重传的报文也丢了。如果这个值持续增长说明链路质量已经差到重传都救不回来了需要立即介入。3.2 RTT与连接建立耗时指标RTT往返时间是衡量网络延迟的核心指标。内核在/proc/net/tcp里暴露了每个TCP连接的RTT但这是瞬时值而且需要遍历所有连接开销较大。我采用的方案是基于eBPF采集。eBPF可以在内核态挂载探针在TCP发送和接收的关键路径上记录时间戳然后计算RTT。具体来说我在tcp_retransmit_skb和tcp_rcv_established这两个内核函数上挂了kprobe分别记录重传和正常接收的时间戳。eBPF程序的伪代码逻辑大致如下// 在tcp_rcv_established入口记录时间 int kprobe__tcp_rcv_established(struct pt_regs *ctx, struct sock *sk) { u64 ts bpf_ktime_get_ns(); u32 pid bpf_get_current_pid_tgid() 32; // 以socket指针为key存储时间戳 bpf_map_update_elem(rtt_start, sk, ts, BPF_ANY); return 0; } // 在tcp_sendmsg返回时计算RTT int kretprobe__tcp_sendmsg(struct pt_regs *ctx) { struct sock *sk (struct sock *)PT_REGS_PARM1(ctx); u64 *start_ts bpf_map_lookup_elem(rtt_start, sk); if (start_ts) { u64 rtt bpf_ktime_get_ns() - *start_ts; // 将RTT写入perf event或map bpf_map_update_elem(rtt_values, sk, rtt, BPF_ANY); } return 0; }这个方案的好处是精确到微秒级而且可以按连接、按进程维度聚合。缺点是eBPF程序需要内核版本支持4.9以上而且编写和调试有一定门槛。连接建立耗时则是通过记录SYN发送和SYN-ACK接收的时间差来计算的。这个指标能直接反映网络握手阶段的健康度。如果连接建立耗时P99超过500ms通常意味着网络路径上有问题可能是路由抖动、防火墙限速或者对端服务过载。3.3 连接队列与错误指标除了延迟和重传连接队列的状态也很关键。内核在/proc/net/snmp里暴露了ListenOverflows和ListenDrops两个计数器分别表示全连接队列溢出次数和半连接队列丢弃次数。这两个指标如果持续增长说明服务的accept速度跟不上连接到达速度。常见原因有三个一是应用处理连接的速度太慢二是somaxconn参数设置过小三是tcp_max_syn_backlog参数不够用。我遇到过的一个典型案例是某个Go服务在流量高峰期ListenOverflows每分钟增长上千次但应用日志里没有任何错误。原因是Go的net.Listen默认backlog是128而实际并发连接请求远超这个数。后来把backlog调到4096问题就消失了。采集这些指标时我建议同时记录队列长度和溢出速率。队列长度是瞬时值溢出速率是差值。两者结合才能判断问题的严重程度。4. 采集管道的搭建与实操步骤4.1 采集代理的部署与配置采集代理我选择用Go写编译成静态二进制通过DaemonSet部署到每个节点。选择Go的原因是它对eBPF有比较好的支持库cilium/ebpf而且静态编译后部署方便不依赖系统库。代理的配置文件是一个YAML主要包含下面这些字段collector: interval: 5s namespaces: - default - production metrics: - tcp_retrans - tcp_rtt - conn_establish - listen_queue ebpf: enabled: true rtt_sampling_rate: 0.1 output: type: prometheus endpoint: http://monitoring-gateway:9090/api/v1/write batch_size: 1000 flush_interval: 2s这里有几个参数需要解释。interval是采集间隔5秒是一个平衡点再短会增加开销再长会丢失突发问题的细节。rtt_sampling_rate是RTT采样率设为0.1表示只采集10%的连接的RTT这是为了控制eBPF程序的开销。实测下来10%的采样率已经足够反映整体RTT分布而且开销降低了80%以上。batch_size和flush_interval控制推送行为。批量推送可以减少网络开销但会增加延迟。2秒的flush间隔意味着最坏情况下指标会延迟2秒到达监控后端这个延迟在可接受范围内。4.2 命名空间关联的具体实现容器环境下采集代理需要知道每个网络命名空间对应哪个服务。这个关联信息我从Kubernetes的API获取。具体流程是调用Kubernetes API列出所有Pod拿到Pod的IP、所在节点、容器ID。在节点上通过容器运行时containerd或Docker的接口根据容器ID拿到容器的PID。通过/proc/pid/ns/net拿到网络命名空间的文件描述符。将命名空间文件描述符与Pod的标签服务名、实例ID建立映射关系。这个映射关系需要定期刷新因为Pod会重建。我设置的刷新间隔是30秒实测下来既能及时感知Pod变化又不会给API服务器太大压力。提示如果你的集群规模很大超过1000个Pod建议使用Informer机制监听Pod变化事件而不是轮询API。轮询在规模大时会导致API服务器负载过高。4.3 指标推送与后端存储指标推送我走的是Prometheus的remote write协议。选择这个协议的原因是它支持批量推送、有压缩、而且大多数监控后端都兼容。推送的数据格式是Prometheus的时序数据格式每条数据包含指标名、标签集、时间戳、值。标签集里我固定带上service、instance、peer_service、namespace这四个标签。peer_service表示对端服务名这个信息我是通过解析连接的对端IP然后反查Kubernetes的Service来得到的。后端存储我用的是VictoriaMetrics原因是它对高基数标签的支持比Prometheus好很多。网络指标天然是高基数的因为service和peer_service的组合数量是服务数量的平方级。Prometheus在这种场景下容易出现内存问题而VictoriaMetrics的存储引擎对高基数做了专门优化。存储保留策略我设置的是原始数据保留7天5分钟聚合数据保留30天1小时聚合数据保留1年。这样既能满足短期排查需求又能支持长期的容量规划。5. 常见问题与排查技巧实录5.1 采集代理CPU占用过高这是我最开始遇到的最严重的问题。代理刚上线时在流量大的节点上CPU占用直接冲到20%以上影响了同节点上其他服务的性能。排查下来主要开销在三个地方一是eBPF程序在每个包上都触发频率太高二是/proc/net/tcp的遍历太频繁三是指标推送时的序列化开销。优化措施是eBPF程序加上采样率控制只对部分连接做RTT采集/proc/net/tcp的遍历从每5秒一次改成每30秒一次因为连接级别的RTT变化没那么快指标推送改成批量序列化用protobuf替代JSON。优化后CPU占用降到了1.5%左右。这个开销在可接受范围内。5.2 指标断点与数据回绕前面提到过计数器回绕的问题这里展开说一下排查方法。如果你发现重传率突然变成负数或者异常大的值大概率是计数器回绕了。判断方法是检查采集的原始值是否比上一次小。如果小了说明发生了回绕。处理方式是在计算差值时加上2^32。但要注意如果两次采集间隔内回绕了多次这个修正就不准了。所以采集间隔不能太长5秒间隔下即使满速发送32位计数器也需要很长时间才能回绕一次实际中很少遇到多次回绕。另一个数据断点问题是采集代理重启。代理重启后内存里的上一次采集值会丢失导致第一次差值计算失败。我的处理方式是代理启动后第一次采集只记录原始值不计算差值从第二次采集开始才推送指标。这样会丢失一个采集周期的数据但避免了错误数据。5.3 高基数导致的存储问题service和peer_service的组合确实会产生高基数。我们集群有大约200个服务理论上组合数是40000。实际观测到的活跃组合大约是8000个因为不是所有服务都互相调用。即使8000个组合每个组合有10个指标那就是80000个时间序列。每5秒一个点一天就是13.8亿个数据点。这个量级对存储系统是不小的压力。我的应对策略是对非核心链路降低采集频率。核心服务之间的调用保持5秒采集非核心的降到30秒。另外对超过7天的数据做降采样从5秒聚合到5分钟。这样存储成本降低了90%以上而排查问题时通常只需要看最近几天的细粒度数据。5.4 常见问题速查表现象可能原因排查方法解决措施重传率突然飙升链路抖动、对端过载、MTU不匹配检查同时间段RTT和丢包率联系网络团队排查链路或调整MTURTT P99持续偏高路由绕行、跨区域调用、对端处理慢对比不同可用区的RTT优化调用路径或增加超时重试连接建立失败率高全连接队列溢出、防火墙拦截检查ListenOverflows和ListenDrops调大backlog检查防火墙规则指标断点采集代理重启、网络命名空间变化检查代理日志和Pod事件重启代理或等待映射刷新存储写入慢高基数、批量过大检查活跃时间序列数量降低采集频率增加降采样注意排查网络问题时一定要同时看多个指标。单独看重传率可能会误判因为有些重传是正常的比如超时重传在弱网环境下不可避免。结合RTT、连接建立耗时、队列溢出一起看才能准确定位。6. 几个容易被忽略的细节与个人经验6.1 时间同步的重要性网络指标采集对时间同步要求很高。如果采集代理所在节点的时间和其他节点不一致计算出的RTT和连接建立耗时会完全失真。我要求所有节点必须开启NTP同步而且偏差不能超过10ms。实际运维中我遇到过因为NTP服务挂掉导致时间偏差达到几秒的情况那段时间的RTT指标完全不可用。检查时间同步的方法是在采集代理里定期调用clock_gettime获取系统时间同时通过NTP协议查询上游时间服务器计算偏差。如果偏差超过阈值就上报一个专门的指标并在日志里告警。6.2 内核版本差异不同Linux内核版本暴露的网络统计字段不完全一样。比如TCPLostRetransmit这个字段在4.4以下的内核里没有TCPFastRetrans在更早的版本里也没有。采集代理需要做版本适配对于不存在的字段要么跳过要么用0填充。我的做法是在代理启动时读取/proc/version解析出内核版本号然后根据版本号决定采集哪些字段。同时在指标里带上kernel_version标签这样在查询时可以过滤掉不支持的字段。6.3 容器网络模式的差异Kubernetes支持多种网络模式不同模式下网络命名空间的布局不一样。比如hostNetwork模式的Pod和宿主机共享网络命名空间采集时不需要切换命名空间而CNI模式的Pod有独立的命名空间需要切换。我在采集代理里做了自动检测先读取Pod的hostNetwork字段如果是true就直接从宿主机命名空间采集如果是false就通过容器PID切换命名空间。这个逻辑看起来简单但实际实现时要注意有些CNI插件会在Pod里再创建子命名空间这时候需要递归查找。6.4 指标命名的规范指标命名看起来是小事但如果不规范后期查询和维护会很痛苦。我定的规范是domain_subject_measurement_unit。比如network_tcp_retrans_total、network_tcp_rtt_seconds、network_conn_establish_duration_seconds。单位统一用基础单位秒、字节、个数。不要用毫秒、KB这种因为不同来源的指标单位可能不一致统一用基础单位可以避免换算错误。标签命名也要规范service、instance、peer_service、namespace、node。不要用svc、inst这种缩写虽然省了几个字符但可读性差很多。6.5 告警规则的设置指标采集上来之后告警规则怎么设也很关键。我一开始设了很多告警结果每天收到几百条告警根本看不过来。后来精简到只保留最关键的几条重传率超过5%持续2分钟RTT P99超过500ms持续5分钟连接建立失败率超过1%持续1分钟全连接队列溢出速率超过10次/秒持续1分钟这几条规则覆盖了绝大多数网络问题而且误报率很低。其他指标虽然也采集但只用于排查时查看不设告警。告警的阈值需要根据实际业务调整。比如跨区域调用的RTT天然就比同区域高如果统一设500ms跨区域调用会频繁告警。我的做法是按调用路径分组同区域调用设200ms跨区域设800ms。6.6 与链路追踪的联动网络指标和链路追踪Tracing结合使用排查效率会高很多。当链路追踪显示某个Span的耗时异常时可以立即查询对应时间段、对应服务的网络指标看是不是网络问题导致的。我实现联动的方式是在链路追踪的Span里带上service和peer_service标签然后在监控后端里配置跳转链接从Span直接跳到对应的网络指标查询页面。这个联动看起来简单但实际排查时能节省大量时间。7. 后续可以扩展的方向这套方案目前覆盖了TCP层的核心指标但还有一些方向可以继续深入。比如QUIC/HTTP3的指标采集QUIC在用户态实现内核不暴露统计需要从应用层或eBPF的uprobe来采集。再比如网络拓扑的自动发现通过分析连接关系自动构建服务依赖图这样在排查问题时可以快速定位影响范围。另外异常检测也值得做。现在的告警都是基于静态阈值如果能用历史数据做基线自动检测偏离基线的异常可以减少阈值调整的工作量。我试过用简单的移动平均加标准差的方法效果还不错但需要处理周期性波动的问题。这些方向我还在探索中等有成熟经验了再单独写文章分享。如果你也在做类似的事情欢迎交流。