ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用NetStat构建高可靠网络可观测性:从TCP状态到生产实践

用NetStat构建高可靠网络可观测性:从TCP状态到生产实践 先说一个我亲手踩过的现场。那是一个周三凌晨两点线上一个核心服务突然大面积超时监控大盘上的CPU、内存、磁盘全是绿的APM曲线也看不出明显异常。折腾了二十分钟没头绪最后我随手敲了一条命令netstat -anp | grep SYN_RECV | wc -l结果数字直接破万。那一刻我才意识到服务的可观测体系再花哨如果漏掉网络连接层这层数据你照样是瞎的。这就是我今天想聊的主题用最朴素的 NetStat 家族命令搭一套高可靠的网络可观测实践。现在市面上聊可观测绕不开日志、链路、指标这三大支柱甚至不少朋友已经开始用 DeerFlow 这类统一可观测平台做事件串联。但落到实际线上你真正缺的往往不是平台能力而是接入平台之前的那个数据源。网络连接层是最容易拿到、也最容易被忽略的信号源端口有没有在监听、半连接堆了多少、TIME_WAIT 是不是多得反常、CLOSE_WAIT 有没有堆积这些光靠业务埋点是埋不出来的只有从内核的网络状态里看。这篇文章适合后端开发、运维、SRE也适合刚接触网络排查的新人。我会按我们实操过的方案把 NetStat 可观测的每个环节拆开讲包括指标怎么定、采集脚本怎么写、怎么避免把采集器本身搞挂以及一堆只有上过生产才看得懂的坑。1. 为什么网络连接层的可观测性常被忽略1.1 一次全绿事故复盘先还原一下那天的经过。服务超时之后第一反应是看JVM线程看数据库慢查询看Redis延迟全都没问题。后来为什么会敲 netstat因为有个老同事说了句看看是不是连接被打满了。我们当时直接看的是ss -lnt里的 Send-Q / Recv-Q 和状态统计发现 SYN_RECV 状态接近一万而该服务平时这个值是几十。紧接着再翻服务端 backlog 参数发现进程监听队列被压爆了内核直接把后续连接丢进黑洞前面已经到达的连接全卡在半握手状态。业务方看起来就是请求没人处理监控上却是一切正常。这个案例不是个例。网络连接层的异常有个特点它很多时候不表现为CPU高、内存高而是表现为请求进不来或者连接处理不完但这些状态并不直接暴露给应用层。而 netstat / ss 这类工具恰好是唯一能直接看到内核TCP状态机的地方。所以我的结论是网络连接层的可观测性不是锦上添花而是必须补上的一层。1.2 NetStat 与 ss一对常被误解的命令很多刚接触运维的朋友会把 netstat 和 ss 当成两个完全不同的东西其实它们解决的是同一个问题只是出身不同。netstat 来自 net-tools 工具包历史非常久几乎每一个Linux发行版都自带即便你装的是精简容器大概率也能用/proc/net/tcp找到状态数据。ss 来自 iproute2 工具包是后来为替代 netstat 推出的速度快得多输出信息也更多比如可以显示 socket 的 inode、定时器、拥塞算法等细节。为什么我这篇文章的标题还要强调 NetStat 可观测因为高可靠这三个字包含了一个现实你在生产环境里不一定装得成 iproute2特别是某些轻量基础镜像、老旧系统、或者甲方严格管控的服务器能用的一定是 netstat。一个高可靠的采集器要同时兼容这两兄弟而不是赌环境里有 ss。别笑我见过不止一个采集脚本上线后发现ss: command not found。1.3 连接状态里的业务信号要理解 NetStat 可观测先得理解 TCP 状态它不只是网络协议知识每个状态背后都直接对应一种业务问题。LISTEN 指的是本机某个端口有进程在监听这个最简单但也是监控巡检里最基本的存活探针。有的服务进程还在、端口却没监听那比进程挂掉更隐蔽健康检查如果只做进程检测根本发现不了。ESTABLISHED 表示当前活跃的 TCP 连接数。对大多数业务来说这个数字不是越高越好也不是越低越好它是容量规划的重要输入。比如一台实例能扛多少并发你就可以用这个状态的历史曲线去压评估。SYN_RECV 是半连接状态服务器已经发出 SYNACK但还没收到对端的 ACK。这个值一旦持续高企十有八九是 accept 队列满了或者有异常流量在打你的端口。上面那起事故就是典型。TIME_WAIT 出现的原因是主动关闭连接的一方在等待 2MSL 时间后才彻底释放 socket。它本身不是故障但如果你是一个高并发短连接服务TIME_WAIT 累计到几万个也是正常的事可如果它持续增长不回收socket 会占用大量内存和端口资源。CLOSE_WAIT 则是个非常有业务味的状态对端已经发起关闭但本端代码里的 socket 没有执行 close。这个状态十次有九次是程序 bug比如读了半截数据没读完或者忘了释放连接对象。它不会立刻让进程 OOM但会慢慢把 fd 耗尽等 fd 到顶服务就彻底躺平了。也就是说netstat 输出的那一张状态表本质上是一张业务故障对应表。你不需要理解 TCP 状态机的全部细节只需要把每个状态的业务含义搞清楚监控就成功了一大半。2. 高可靠 NetStat 可观测方案的整体设计2.1 先定指标把文本变成数字做可观测的第一件事不是写脚本而是定指标。因为 netstat 输出的是文本采集器要做的是把文本变成有业务含义的时序指标。我建议至少先盯以下几组指标。指标采集来源业务含义建议的告警动作监听端口数LISTEN 状态计数服务注册和存活重要端口消失即告警活跃连接数ESTABLISHED 计数并发水位、容量规划超过基线的数倍且持续10分钟半连接数SYN_RECV 计数队列溢出或异常流量连续3次采集超过阈值即告警被动关闭连接数CLOSE_WAIT / FIN_WAIT1代码未释放或对端异常持续增长且不回落TIME_WAIT 数量TIME_WAIT 计数短连接场景资源占用超过可用端口段才告警Recv-Q 堆积LISTEN 行的 Recv-Qaccept 队列已满Recv-Q 持续大于0Remote 连接分布远端IP聚合计数外连异常、被扫描单IP连接数突增看着很简单但这里有个关键的思维转变这些指标不是给你瞄一眼的是给告警系统做时序判断的。所以我后面把它输出成 Prometheus 的文本格式或 JSON让监控平台按时间序列存。换句话说netstat 只负责产生原始信号而高可靠的 NetStat 可观测方案负责信号解释和长期追踪。2.2 采集层架构轮询周期与隔离采集层最忌讳的是业务进程自己调 netstat。你想一下如果服务进程已经处于半死不活的状态连 socket 都卡住了再让它去跑命令采集指标要么采集不到要么把进程搞得更卡。正确做法是把采集器独立成一个小进程或者是独立的 sidecar 脚本只做三件事按固定周期执行 netstat / ss、解析输出、把指标送到存储端。轮询周期也要克制。很多人一上来就做成 1 秒采一次觉得越精细越好。但 netstat 在连接数很多的时候本身就要遍历/proc/net/tcp的整个表这个操作对 CPU 是有开销的。我们在单机 10 万连接的场景实测过纯netstat -tanp一次执行耗时能到 500 毫秒1 秒一轮根本是自杀式监控。最后定的方案是常规指标 15 秒一轮出问题时的细粒度诊断由人工触发。这个周期对绝大多数业务形态已经够了因为连接泄漏和队列堆积都是分钟级以上的趋势15 秒完全可以抓得住。2.3 为什么还要做版本兼容和命令降级这是踩出来的教训。采集脚本第一次上线时代码里写死了ss -tanp结果某天一批老机器上日志全是ss: command not found。后来改成先探测命令是否存在存在就用 ss不存在就回退到 netstat。再后来发现还有个更狠的坑某些发行版的 netstat 输出不带PID/Program name或者把 state 列做了本地化翻译后面问题排查里细说。所以采集器内部要按输出格式做多套解析模板。高可靠的方案设计本质上就是一句话采集器不能比被监控对象更脆弱。如果你监控的是高可用服务那监控脚本本身也得做到高可用。这个高可用不是指冗余部署而是指它在各种脏环境下都不能崩命令不存在要降级、输出列顺序变了要能自适应、采集超时要中断重试、上报失败要本地缓存。下面第三部分就是这套逻辑的具体实现。3. 核心实操一套可直接复用的 NetStat 可观测采集脚本3.1 脚本骨架与核心函数直接上东西。我的采集器主体用 Python 写的因为解析方便且能在异常时做内存缓存。整个脚本的思路是探测系统里可用的是 ss 还是 netstat执行命令并捕获输出设置超时按对应格式解析得到状态维度的指标聚合监听端口的 Recv-Q 数据输出成 Node Exporter 的 textfile 格式方便 Prometheus 直接抓。下面是核心的解析函数兼容 netstat 和 ss 两种列格式。import subprocess import re import tempfile import os NETSTAT_HEADERS {Proto, Recv-Q, Send-Q, Local, Foreign, State, PID/Program} SS_HEADERS {Netid, State, Recv-Q, Send-Q, Local, Peer, Process} def _detect_tool(): for tool in (ss, netstat): try: r subprocess.run([which, tool], capture_outputTrue, timeout3) if r.returncode 0: return tool except Exception: continue return None def parse_netstat_output(output: str): states {} listen_recvq {} remote_ip_count {} lines output.splitlines() for line in lines: line line.strip() if not line or State in line or Netid in line: continue # netstat -tanp 格式示例: # tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 1234/java # ss -tanp 格式示例: # LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:((java,pid1234,fd23)) parts line.split() if len(parts) 6: continue # 判断是 netstat 还是 ss: 检查第一列是否 proto(tcp/udp) if parts[0] in (tcp, tcp6, udp, udp6): state parts[5] if len(parts) 5 else UNKNOWN local parts[3] recv_q int(parts[1]) if parts[1].isdigit() else 0 if state LISTEN: listen_recvq[local] recv_q else: state parts[0] if parts[0] else UNKNOWN local parts[3] if len(parts) 3 else recv_q 0 if len(parts) 2 and parts[2].isdigit(): recv_q int(parts[2]) if LISTEN in state: listen_recvq[local] recv_q states[state] states.get(state, 0) 1 # 远端地址聚合仅保留 IP 部分 if len(parts) 5: peer parts[4] if : in peer: ip peer.rsplit(:, 1)[0].strip([]) remote_ip_count[ip] remote_ip_count.get(ip, 0) 1 return { states: states, listen_recvq: listen_recvq, remote_ip_count: remote_ip_count, }这段代码看着简单但里面有几个细节是实践里磨出来的。第一判断是 netstat 还是 ss不看命令名而是看第一列是协议名还是状态名这样即使你用ss -tan或者netstat -tan解析逻辑也能自动识别。第二解析远端 IP 聚合时先用rsplit(:, 1)切掉端口再做聚合避免因为同一个 IP 的多个端口连接被算成多行。第三所有 field 取值都做了保护判断宁可丢一行也不能让整个采集器因为某一行格式异常而抛异常崩溃。3.2 采集执行层超时、退避与原子写入高可靠的灵魂在采集执行层。很多人写采集脚本只写了执行命令、解析、上报三行根本不管命令卡住、网络抖动、磁盘写一半这些情况。我这边贴一下执行层的设计。def run_capture(tool, timeout10): if tool ss: cmd [ss, -tanp] else: cmd [netstat, -tanp] try: r subprocess.run(cmd, capture_outputTrue, textTrue, timeouttimeout) if r.returncode ! 0: raise RuntimeError(fcapture failed: {r.stderr}) return r.stdout except subprocess.TimeoutExpired: return None def collect_and_write(tool, output_path): output run_capture(tool, timeout10) if output is None: return False data parse_netstat_output(output) # 写临时文件再改名避免 Prometheus 读到写了一半的文件 fd, tmp tempfile.mkstemp(diros.path.dirname(output_path)) try: with os.fdopen(fd, w) as f: f.write(format_metrics(data)) os.replace(tmp, output_path) except Exception: os.unlink(tmp) raise return True原子写入这部分值得说透。如果你把指标直接写进 Nagios / textfile 目录采集进程写到一半崩溃监控端就可能读到一份残缺文件从而造成这一轮指标全部解析失败。os.replace能保证写临时文件完成后一次性替换目标文件这是文件型采集器必须养成的习惯。另外执行超时设 10 秒是底线。如果你定时任务是 15 秒一轮而采集命令卡了 10 秒下一次执行必然重叠。重叠造成的不是一次采集失败而是两个 netstat 进程同时扫描/proc/net/tcpCPU 直接翻倍。所以采集循环里还要加上次未结束就不开启下一次的信号量别用简单的 sleep 循环。3.3 指标输出格式让现成监控平台都能接采集器有了接下来是输出。我推荐一套两用的输出方式本地保留一份 textfile供 Prometheus 体系直接抓同时把 JSON 推给自研 agent 或 DeerFlow 这类统一可观测平台做事件关联。先看 textfile 格式的生成函数def format_metrics(data): lines [] for state, count in sorted(data[states].items()): lines.append(fnetstat_tcp_state{{state\{state}\}} {count}) for local, recvq in sorted(data[listen_recvq].items()): lines.append(fnetstat_listen_recvq{{port\{local}\}} {recvq}) for ip, count in sorted(data[remote_ip_count].items()): lines.append(fnetstat_remote_conn{{remote\{ip}\}} {count}) return \n.join(lines) \n生成的指标带上了 label这样 Grafana 里可以直接 rollup按sum(netstat_tcp_state)聚合所有状态画饼图也可以按具体状态单画曲线。remote_ip_count 这个指标尤其好用当单 IP 打进来的连接数激增在 Grafana 里会非常扎眼直接定位到被扫描或业务被攻击的入口。如果你们内部不是 Prometheus 体系接 JSON 就是一样。把解析结果打包通过 agent 上报。我自己的经验是宁可让采集脚本多几个文件落盘也别把上报逻辑和采集逻辑耦合得太紧。上报失败不应该影响采集所以我会在采集器里维护一个 ring buffer上报失败就把最新 N 轮数据缓存下来下轮补传。3.4 接入监控体系的三种方式结合我们实际用过的方案接入大致有三种方式不同团队可以按自己的基建选。第一种是 Node Exporter textfile 方案。Node Exporter 本身就有--collector.textfile.directory参数你把采集器生成的.prom文件丢到那个目录Prometheus 自动按固定周期抓取。实现成本最低而且整个链路都是标准协议没有任何私有格式。但要注意权限采集器写文件的目录和 Node Exporter 读取的目录必须一致且采集脚本不能直接写入根目录。第二种是 Pushgateway 方案。适合采集器跑在短生命周期任务里的场景比如临时批量任务执行完就把指标 push 给 Pushgateway。但我不建议生产环境长期依赖它因为 Pushgateway 一旦挂了所有指标全都断了而且它缺失了时效性语义旧指标容易误报。我们的用法是只在容器启动阶段用 Pushgateway 上报启动事件其他运行期全部用 Pull 模式。第三种是自研 agent 兼管的方案。如果你已经有统一的 agent 体系比如 DeerFlow 这类支持插件式采集的就把脚本作为外部插件执行agent 负责把结果转成自己的数据模型。这种方案最有弹性也是我现在的首选因为它让网络连接状态不再孤零零躺在指标库里而是可以跟日志、链路事件做关联。举个例子某一轮采集到了 SYN_RECV 突增agent 同时把前 5 分钟的网关日志拉出来自动定位是哪个来源 IP 在打这个能力是普通 node_exporter 给不了的。4. 常见问题与排查技巧实录4.1 netstat vs ss谁在高峰时把 CPU 打满我遇到过最典型的问题采集器上线后单核 CPU 持续 30%。查了半天发现是我们对每条连接都要解析出远端 IP 做聚合导致的。当连接数过万netstat 一次执行本来就要遍历整个 TCP 表再叠加 Python 解析CPU 很容易上去。解决方法是分两级。基础采集只统计各状态计数这部分开销极低15 秒一轮没问题。而 remote_ip_count 这种需要解析远端地址的重指标改成长周期比如 5 分钟或干脆只在告警触发时由诊断脚本补齐。另一个更好的办法是直接用/proc/net/tcp读内核表这个文件的解析性能比 netstat 命令行快 5 到 10 倍几乎没有子进程启动开销适合秒级采样。我在压测环境验证过10 万连接下/proc/net/tcp一次读加解析在 50 毫秒以内而netstat -an在中低负载机器上可能要 300 毫秒以上。生产上我们最终采用的是平时读/proc/net/tcp人工诊断时用 ss的双轨策略。4.2 端口 LISTEN 但业务死活不通这个坑我刚工作时栽过。当时负责一个 Java 应用进程活着端口也 LISTEN可客户端就是连接超时。用netstat -anp看奇怪的是已经建立起来的连接反而能通。问题在哪儿原来这个服务属于单线程 accept 模式业务线程一旦被慢 SQL 拖死就再也没有机会执行 accept内核维护的 accept 队列早就满了新连接只能等在 SYN 队列里表现为 SYN_RECV 上涨。这类问题用脚本怎么发现就是看 LISTEN 行的 Recv-Q。只要 Recv-Q 持续大于 0基本可以判定 accept 消费速度跟不上连接建立速度。不要等到 SYN_RECV 爆炸才发现因为从 Recv-Q 开始涨到用户察觉中间往往有几分钟到十几分钟的时间差这段窗口就是可观测体系该报警的黄金时间。所以我特别强调要把 LISTEN 行的 Recv-Q 拆成指标单存。4.3 数据解析乱套locale、制表符和表头多语言环境是解析脚本的另一大杀手。某些系统 locale 不是 C/POSIXnetstat 输出里的State列可能变成État或者别的单词你用英文关键字做索引当然就穿了。解决翻车思路有两个一是执行命令时强制env LC_ALLC netstat -tanp从源头固定输出语言二是解析时别依赖State这个单词直接按列位置取 state。这两个手段都做了才算高可靠。还有制表符和多空格的问题。netstat 某些版本字段之间用的不是连续空格而是制表符对齐直接str.split()的话看似没问题但个别行的远端地址里可能带着多余空格。我在解析脚本里用正则re.split(r\s, line)并且要求切出来的字段数小于 6 就跳过该行宁可漏一行也不把错数据带进指标。4.4 CLOSE_WAIT 堆积到底是什么问题如果要我从所有状态里选一个最常见的代码级故障那一定是 CLOSE_WAIT 堆积。它的形成机制再强调一遍对端关闭了连接你的进程却没有调用 close。原因千奇百怪读超时后没有释放 socket、数据库连接池回收逻辑 bug、HTTP keep-alive 的超时处理漏了等等。怎么从观测上判断异常正常服务 CLOSE_WAIT 应该在个位数到几十之间波动。如果一个服务 CLOSE_WAIT 持续平稳上升每 15 秒涨几十基本可以坐实是连接泄漏。我们的告警规则不是设绝对值而是设当前值减去基线值且持续 3 个采集周期都在增长。这比单纯设一个 200 的阈值好用得多因为有的高并发服务基线就是 100 多。配合另一条指标netstat_tcp_state{stateCLOSE_WAIT}的增长速率就能在 fd 耗尽之前提醒业务侧去查代码。4.5 外连行为的观测要拉出来单看最后讲一个很多团队容易忽略的视角除了主动监听端口服务器还会主动外连别的服务。你现在的机器有没有在主动连不被允许的地址这种信息藏在 ESTABLISHED 的连接远端里。很多安全事件的第一征兆不是 CPU 飙高而是某台机器突然跟一堆陌生 IP 建立了大量连接。我在采集器里专门加了一个外连监控模式区别于常规的全量聚合。它只统计本地主动发起的连接即 netstat 输出里 Local Address 是本机端口、Foreign Address 是非本机地址的行。然后把远端 IP 聚合并打码只保留前三位比如192.0.2.x。打码不仅仅是为了安全合规也是为了让业务侧关注方向而不是纠结具体 IP。配一条简单的告警如果某个远端网段的连接数在 5 分钟内翻倍就开始通知安全侧介入。这一层有了之后整个 NetStat 可观测才算闭环向内看得到端口健康向外看得到行为异常。最后再分享一个我实践证明很值钱的细节。我们最开始把 NETStat 采集器只在 CPU 有冗余的实例上部署后来发现越是高负载的实例越需要这个观测因为连接堆积常常就是高负载的诱因于是改为所有实例强制部署。高可靠的可观测不是给机器添负担而是在机器出问题前替它说话。另外一个小技巧如果你真的要做秒级采样别用netstat了直接读/proc/net/tcp然后用 awk 按第 4 列状态码聚合比如awk NR1 {print $4} /proc/net/tcp | sort | uniq -c这个命令的并发开销可以忽略不计。网络状态这东西越早看见越省事别等用户先骂街再敲命令。
返回列表