运维干得久了,最怕的其实不是机器宕机,而是在linux排障时碰见“服务端口被打满”这种告警——机器活着,进程活着,CPU内存都正常,但新连接就是进不来。第一次在linux服务器上排这个故障时,我盯着ss和netstat的输出一头雾水,后来逐步摸清了门道才发现,“端口被打满”根本不是单一原因,而是文件描述符、连接队列、端口号池这三层资源叠加出来的现象,每一层的判断思路和解法都不一样。这篇文章把我这些年积累的排查链路、判断方法和参数调整经验完整写出来,不管是刚转运维的新人,还是被类似问题反复折磨的工程师,照着这套思路走,都能少走很多弯路。
1. 端口被打满的三种形态:先分清是“服务进不来”还是“服务出不去”
端口满了,第一步不是急着改参数,而是搞清楚满了的到底是哪一层。TCP连接不是凭空出现的,一条连接从请求到建立,中间要经过内核和应用的层层处理,任何一层饱和,表面上都叫“端口打满”,但本质上完全是两回事。
1.1 从SYN到accept:一次连接要闯的三道关卡
客户端要和服务器的某个端口建立TCP连接,中间有个固定流程。哪怕你平时不深入研究TCP状态机,也得把这三层留在脑子里。
第一层,客户端SYN到达服务器后,先进入内核的半连接队列(SYN队列)。这里的连接还没完成三次握手,一旦队列满,内核会直接丢弃SYN,客户端就会表现为connect超时。第二层,握手完成后,连接进入全连接队列(accept队列),等待应用进程调用accept把它取走。队列满的时候,内核已经完成了握手,但应用来不及处理,新连接照样进不来。第三层,应用accept之后,每个连接都对应一个文件描述符(fd)。fd不够用,accept就返回异常,连接同样建立失败。
这三个关卡分别对应半连接队列、全连接队列、fd资源。很多人在服务器上看到“端口打满”,以为瓶颈一定在监听端口本身,其实真正的瓶颈可能在前面的SYN队列,也可能在后面的fd。不把层次分清楚,后面的排查很容易跑偏。
还要补充一句,本机作为客户端主动发起连接时,需要经过另一道关卡,就是本地端口池。同一个源IP和源端口组合才能唯一标识一条连接,所以主动外呼时,每个连接都需要占用一个临时端口。端口池耗尽的表现就是“服务出不去”,这跟“服务进不来”是完全不同的两个方向,排查思路也不一样,后面我专门展开讲。
1.2 “入口满”和“出口满”:两种被打满的方向
“服务端口被打满”这个说法其实很模糊。我见过很多同事排障,第一反应是看80端口或者8080端口有多少连接,觉得端口“打满”就是监听端口的连接数超了。但实际工作中,我会先在脑子里把问题分成两个方向。
入站方向,外部请求进不来,通常表现为客户端连不上、connect超时,或者服务端日志抛Too many open files。这个方向检查的重点是半连接队列、全连接队列和fd。
出站方向,本机服务作为客户端去调别人的接口,结果发不出去,通常表现为上游调用报错、日志出现Cannot assign requested address。这个方向检查的重点是本地端口池、TIME_WAIT状态堆积。
这个区分非常重要。有一次线上事故,业务方以为是自己服务端口被打满,盯着8080看了半天,结果真正的根因是出口方向的临时端口耗尽。方向搞反了,排查时间至少多花一倍,更麻烦的是,方向错了还会导出错误的调优方案,比如入站队列满了你去调本地端口范围,一点用都没有。
1.3 故障现场的典型表象:日志和用户感知
端口被打满时,不同层次的问题会给出不同的信号,这些信号在日志和监控里非常明显。
如果日志里持续出现Too many open files,那基本可以判定是fd层面的问题。如果一个服务的连接数看着不高,但fd全被占满了,那往往不是连接太多,而是有连接没被正确关闭,造成了fd泄漏,这种问题光调参数解决不了,必须去翻代码。
如果是客户端connect超时、服务端出现大量SYN_RECV状态的连接,那优先怀疑半连接队列被打满,常见诱因是短时间大量请求,或者被人为SYN攻击。如果一条连接已经建立,但应用层始终不响应,响应时间越来越长,那要看accept队列是不是满了,背后的原因通常是应用处理能力跟不上,比如线程池太小,应用accept的速度赶不上建连速度。
如果是主动外呼时报Resource temporarily unavailable或者Cannot assign requested address,那是本地端口池的问题,常见于高并发短连接场景。把这些表象和三层资源对应起来,后面就能按图索骥,不至于两眼一抹黑。
2. 排障第一步:五分钟内定位到瓶颈层的现场采集法
故障发生时别急着改配置,先花五分钟把现场的“证据”采集全。很多初学者一上来就去看某个应用的日志,其实系统层面的指标已经把答案透露了大半。端口类故障尤其如此,内核向外界暴露的状态信息非常丰富,关键是你得知道看哪儿。
2.1 全局快照:ss -s、sockstat、dmesg三板斧
第一板斧是ss -s,它给出整个系统的socket汇总。典型的输出类似:
TCP 10 estab 5 closed 3 orphaned 0 synrecv 0 timewait注意看几个数:estab(已建立的连接数)、timewait(TIME_WAIT数量)、synrecv(半连接队列里的SYN_RECV数量)。如果timewait的数字大到几万,同时系统负载不高,那出站方向的端口池八成有压力。
第二板斧是直接看内核的socket统计文件:
cat /proc/net/sockstat输出里重点看sockets: used、TCP: inuse、tw这几项,其中tw就是TIME_WAIT数量。另外还有/proc/net/sockstat_nf里的conntrack计数,这个在容器环境里尤其关键,后面会讲到。
第三板斧是dmesg。很多内核主动丢包是有记录的,比如最常见的:
possible SYN flooding on port 8080. Sending cookies.看到这句话,基本可以确定半连接队列溢出,内核已经在用SYN Cookie机制兜底了。这一步很多人会漏掉,其实dmesg里的信息对快速定位队列问题非常关键,日志就在眼皮底下,只是很少人会第一时间想起来去看。
如果你用的发行版比较新,ss命令基本可以替代netstat,输出更清晰,而且ss -lnt可以直接看到每个监听端口的Recv-Q和Send-Q,这个后面会用到。
2.2 按连接状态分类:TIME_WAIT、SYN_RECV、CLOSE_WAIT各指向什么问题
全局快照看完了,带上状态过滤条件,用一行命令把连接状态统计出来:
ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn输出类似这样:
51234 TIME-WAIT 1234 ESTAB 89 SYN-RECV 45 CLOSE-WAIT这里的统计数字一下就能把问题指向说清楚。
TIME-WAIT大量堆积,说明系统里有大量主动关闭的连接。TIME_WAIT本身不是错误,它是TCP协议为了保证旧连接的报文不会串到新连接上而设计的等待状态,默认时长60秒。但堆积到几万个,直接后果就是本地端口池被占满,新的主动连接发不出去。
SYN-RECV大量出现,说明半连接队列里积压了没完成握手的连接,要么是请求量远超处理能力,要么是有人在伪造SYN报文。CLOSE-WAIT大量出现,说明对端已经关闭连接,而本机应用没有调用close,这是典型的应用层bug,通常需要查代码而不是调内核参数。CLOSE-WAIT过多时,连接数看着挺高,其实都是半死不活的僵尸连接。
用表格整理一下:
| 连接状态 | 数量激增的常见含义 | 第一优先排查方向 |
|---|---|---|
| TIME_WAIT | 主动关闭连接太多,本地端口池被占用 | 出口连接复用、端口范围 |
| SYN_RECV | 半连接队列积压或SYN攻击 | 队列参数、限流 |
| CLOSE_WAIT | 应用未关闭连接,fd泄漏 | 应用代码、连接管理 |
| ESTABLISHED | 并发连接数超出容量设计 | 应用线程池、全连接队列 |
2.3 进程维度定位:谁在消耗连接与fd
全局的账算完了,再把视角聚焦到进程层面。先找到占用连接的进程,可以用lsof -i:8080查看监听该端口的进程,或者用lsof -nP -iTCP -sTCP:ESTABLISHED统计所有外部连接的归属进程。
重点看两个东西:一是这个进程已经打开了多少fd,二是它的fd限制是多少。
ls /proc/PID/fd | wc -l cat /proc/PID/limits在limits输出里看Max open files一行。如果已用fd数量接近限制值,那基本可以断定瓶颈就在fd。现实中有一个经典场景:nginx的worker_connections配置得很大,但系统对进程的fd限制没同步调大,结果连接还没到设计值,fd先打满了。
到这里,故障的瓶颈层基本已经清楚了,接下来就是按层对号入座找根因。这一步做扎实了,后面的调整就是水到渠成的事。
3. 端口耗尽的三大典型根因,逐个对号入座
现场数据采集完毕,接下来要把问题归类。我实际排障中遇到的端口类故障,九成落在下面三类根因上。每一类都有典型的报错特征和验证手段,对号入座之后,解决方案基本就浮出水面了。
3.1 fd打满:Too many open files不只是“文件”的事
很多人看到Too many open files,觉得是打开的文件太多,跟端口有什么关系?其实在Linux下,socket也是文件,每个TCP连接都要占用一个fd。
fd打满有两种常见场景。第一种是真连接太多,并发量已经超过了进程的fd上限,这种好解决,调大限制就行。第二种是连接总数并不多,但fd被占满了,这是因为连接没有正确关闭,造成了fd泄漏。这种就要去代码里找问题,调参数治标不治本。
怎么区分?先看ss -tan统计出的ESTABLISHED数量,再对比ls /proc/PID/fd | wc -l。如果连接数量只有几百,fd却开了好几万,那肯定不是连接太多,是泄漏。
fd限制涉及三个层面:系统级file-max、用户级ulimit、进程级soft limit。改的时候要三处一起考虑。系统级用fs.file-max设置,用户级在/etc/security/limits.conf里配置nofile,进程限制可以用ulimit -n临时调整。
注意一点:limits.conf里默认只改了登录用户的部分,如果服务是用systemd管理的,还需要在service文件里加LimitNOFILE=65535。这个细节我踩过坑,改了半天limits.conf,重启服务后fd限制还是1024,排查了半天才发现systemd接管了limits设置。
3.2 连接队列打满:SYN洪泛与accept队列溢出
队列问题比fd问题更隐蔽,因为ss -s里看不到“队列满”的直接数字,要从侧面判断。
半连接队列满的表现是SYN_RECV数量异常、dmesg出现possible SYN flooding。半连接队列的长度受tcp_max_syn_backlog控制,同时受内核内存参数影响。实际场景里,业务正常增长导致队列满的情况反而少,更多是突发流量或者恶意SYN。如果内核开启了syncookies(默认开启),队列满时不会彻底拒绝新连接,而是改用SYN Cookie机制,代价是CPU消耗增加。所以很多SYN攻击打过来时,你看到的不一定是连接失败,而是CPU飙升。
全连接队列满的表现更有意思。你应用还在正常响应,但新连接就是进不去。判断方法是用ss -lnt看监听端口的Recv-Q和Send-Q。在LISTEN状态下,Recv-Q表示当前全连接队列里等待应用accept的连接数,Send-Q表示队列的最大长度。如果Recv-Q长期接近或者等于Send-Q,说明应用accept的速度跟不上建连速度。
队列长度受两个参数共同影响:内核的net.core.somaxconn和应用调用listen时传入的backlog,实际取两者中的较小值。所以调大了somaxconn,但如果nginx里没有同步调大listen backlog,队列依然不长。很多人只改内核不改应用配置,结果还怪参数不生效,这是比较典型的误区。
3.3 本地端口池耗尽:TIME_WAIT堆积导致出站连接失败
这是出站方向最典型的故障。本机主动向外发起TCP连接时,内核会从本地端口范围里挑一个空闲端口作为源端口。默认范围是32768到60999,总共28232个端口。如果一条连接主动关闭,它会进入TIME_WAIT状态,默认占用60秒。这意味着,在60秒窗口内创建的短连接数量超过28000个,新连接就发不出去了。
算一笔账:假设你的服务每秒钟创建500条短连接去调下游接口,每条连接在TIME_WAIT里待60秒,那稳定状态下TIME_WAIT数量是500乘以60,等于30000,已经超过了默认端口池的容量。日志里就会出现Cannot assign requested address。
判断方法很简单:统计一下TCP连接里本地端口的使用分布,或者直接看tw数值。前面提到的ss -tan | awk统计,如果TIME-WAIT占到几万,同时报错是出站方向,基本可以锁定这个根因。
解决方向有三个:第一是减少连接创建,用连接池或长连接,这是最根本的解法;第二是扩大ip_local_port_range;第三是让TIME_WAIT快速回收。第三个方向要特别谨慎,tcp_tw_reuse和tcp_tw_recycle的适用条件完全不同,乱开会出大事故,这个在下一节细说。
4. 各层参数怎么调:应用层、内核层、容器层三层组合拳
定位到根因后,调整参数的顺序很关键。我个人的习惯是:先调应用层,再调内核层,最后才考虑容器和虚拟化层的特殊处理。因为应用层改动通常最安全,也最贴近业务本质,内核参数改动影响面大,容易引发次生问题。
4.1 应用层优先:连接池、长连接与容量参数
调内核参数之前,先问自己一个问题:业务的连接模型是不是合理的?如果每个请求都新建连接、用完就关,那再大的端口池也扛不住。最经典的优化是把短连接改成连接池复用,效果立竿见影。
拿Java后端调用MySQL举例,如果每次查询都新建连接,压测到一定QPS必然报连接失败。用上连接池后,整个服务持有几十个连接就够了,端口压力瞬间消失。这是应用层解决端口问题性价比最高的方式。
HTTP服务也一样。nginx默认开启了keepalive,但如果你的后端应用没开keepalive,每来一个请求就新建一条到后端的连接,同样会打满。检查一下后端服务的keepalive配置,往往能省下大量内核参数调整。
应用自身的容量参数也要同步审视。nginx的worker_connections决定了每个worker进程最多能处理多少并发连接,配置太低会限制服务能力,配置太高则要确认系统fd够不够。MySQL的max_connections过高会让每个连接都消耗内存,过低又会拒绝连接,这个值要结合实例规格评估。
4.2 内核层调整:端口范围、超时与队列长度配合
应用层优化完之后,剩下的压力才交给内核参数来扛。以下是我常用的几组调整,按场景整理。
对于出站方向端口耗尽,典型的sysctl配置:
net.ipv4.ip_local_port_range = 1024 65535 net.ipv4.tcp_fin_timeout = 15 net.ipv4.tcp_tw_reuse = 1解释一下各行的作用。ip_local_port_range从默认的32768-60999扩大到1024-65535,能用的端口数从2.8万提升到6.4万,是解决端口池不足最直接的参数。tcp_fin_timeout控制FIN_WAIT_2状态的超时时间,调低到15秒能加快连接回收。tcp_tw_reuse允许内核在安全前提下复用处于TIME_WAIT状态的连接,更适合需要快速发起新连接的场景。
但这里必须提醒一句:tcp_tw_reuse只对主动连接的一方有效,而且它复用的前提是保证序列号安全。如果在NAT出口设备后面,多个内网IP共享一个公网IP时,开启风险很大,可能导致连接错乱。曾经有一个tcp_tw_recycle参数,在NAT环境下引发过大量线上事故,新版内核已经把它移除了。我在生产环境中的原则是:先做连接池复用,再考虑扩大端口范围,不到万不得已不碰tw复用参数。
入站方向的队列参数:
net.core.somaxconn = 1024 net.ipv4.tcp_max_syn_backlog = 1024 net.ipv4.tcp_syncookies = 1somaxconn影响全连接队列长度,tcp_max_syn_backlog影响半连接队列长度,tcp_syncookies建议保持开启,防止SYN洪泛直接打挂服务。调大队列的同时要注意,应用层的listen backlog也要同步调大,比如nginx里在listen指令中加上backlog=1024。否则内核参数调得再大,应用没跟上也白搭。
fd层面:
fs.file-max = 2000000修改文件描述符上限时,系统级和进程级要一并处理,systemd服务注意设置LimitNOFILE。改完执行sysctl -p生效,如果想确认当前生效值,用sysctl net.ipv4.ip_local_port_range验证。
4.3 容器与虚拟化环境的增量问题
如果你的服务跑在容器里,上面这些参数会有一些额外坑。
第一个坑是容器内的limits继承。docker run如果不显式指定ulimit,容器内进程会继承dockerd的默认值,而很多默认值并不高。建议在启动参数里显式加上--ulimit nofile=65535:65535,或者在docker-compose里声明。Kubernetes环境可以在pod spec里通过securityContext设置。
第二个坑是conntrack表。Docker默认使用iptables做NAT和端口映射,每个连接都会占用一条conntrack条目。如果conntrack表满了,即使端口、fd都正常,新连接也会被丢弃,现象跟“端口打满”几乎一模一样,但本质是连接跟踪表溢出。查看方法:
cat /proc/sys/net/netfilter/nf_conntrack_max cat /proc/sys/net/netfilter/nf_conntrack_count如果count接近max,优先考虑调大nf_conntrack_max,同时检查容器网络模型。hostNetwork模式的Pod不经过NAT,conntrack压力会小很多。
第三个坑是虚拟化平台的安全组。云服务器的安全组规则和本地iptables都可能独立限制并发,出现端口问题时要一起排查,不要只盯着系统内部参数。之前遇到过一次,调完所有内核参数后故障依旧,最后发现是安全组规则把某个端口的高并发流量拦截了,这个排查方向很容易被忽略。
5. 真实排障案例:一个回调接口的偶发超时
前面讲了理论和方法,下面用一个我实际处理过的案例,把整个排障链路串起来。你可以直接参照这个思路复现,遇到类似问题的时候,大概率能省下半天排查时间。
5.1 现象:上游超时与调用方报错
出问题的是一个支付回调服务。现象是:某个外部渠道偶尔报回调超时,但监控上看服务本身CPU、内存、磁盘都正常,监听端口也能连上。服务端日志干干净净,没有任何异常堆栈。一开始大家都觉得是网络抖动,但连续几次都集中在业务高峰时段,明显不是偶发。
5.2 排查链路:从监听端口到出口连接
我登录服务器后,先执行ss -s,发现tw值非常高,TIME_WAIT数量稳定在4万以上。再看调用方的连接统计,几乎全是TIME_WAIT,ESTABLISHED反而很少。此时我心里已经有八成把握,是出站方向端口池耗尽。
接着算了一笔账:这个服务的QPS峰值在800左右,每次调用外部接口都需要新建连接,平均每条连接从建连到关闭约400毫秒,TIME_WAIT等待60秒。需要的端口数大约是800乘以60,等于48000,而默认端口池只有28000多个,肯定会饱和。日志里的Cannot assign requested address印证了这一点。
这里有一个关键细节:问题出在调用方,而不是被调方。所以先去查监听端口是不是正常是合理的,但不能只停在这里。端口问题的排查思路一定要双向看,服务端口正常不代表整个链路没有端口问题。
5.3 处理动作与验证结果
当时的处理分两步走。第一步,在代码里引入连接池,把每次调用的新建连接改为复用连接,这是治本的动作。第二步,把ip_local_port_range从默认值扩到1024-65535,同时将tcp_fin_timeout调整为15,给存量短连接场景留出缓冲。
改完后观察了完整的高峰周期,TIME_WAIT数量从4万降到不到1万,上游回调超时消失。一个月后又复查了一次,没有再复发。
这个案例里最值得记住的是:不要把内核参数当万能药,连接池才是真正让端口不会被打满的关键。内核参数只是给不合理的连接模型兜底而已。如果当初只调内核参数,不优化连接模型,短期内可能问题被掩盖了,但端口池终究是有限的,连接数量继续上涨还是会爆。
6. 把“端口打满”扼杀在监控里:指标、阈值与容量规划
最后聊一聊怎么在故障发生前把它拦截下来。端口问题不像CPU、内存那样有直观的使用率曲线,但它的预警信号其实很清晰,关键是是否有人关注这些指标。很多团队的内存、磁盘监控做得非常完善,但端口层面的指标一片空白,等到用户报障才知道出了问题。
6.1 关键监控指标与建议阈值
我梳理一下实际运维中建议纳入监控的指标,以及我常用的告警阈值:
| 指标 | 采集方式 | 建议阈值 |
|---|---|---|
| 进程fd使用率 | ls /proc/PID/fd | wc -l,除以limits中的上限 | 超过80%告警 |
| TIME_WAIT数量 | ss -tan state time-wait | wc -l 或 ss -s | 超过端口池50%告警 |
| SYN_RECV数量 | ss -tan state syn-recv | wc -l | 持续超过100告警 |
| 监听队列积压 | ss -lnt 的Recv-Q | Recv-Q长期高于Send-Q的80% |
| conntrack使用率 | nf_conntrack_count / nf_conntrack_max | 超过80%告警 |
| 本机已用端口数 | 脚本统计本地端口占用总数 | 接近端口池上限时告警 |
这些指标用Prometheus加node_exporter基本都能采到,或者写个简单的定时脚本配合Zabbix也能搞定。关键是不要等到用户反馈才去看。
另外,告警阈值不是随便拍的。比如TIME_WAIT阈值,如果你的实例有多块网卡、多个IP,实际可用的本地端口池还要乘以IP数量,需要先摸清底数再定阈值。fd使用率也是一样,不同服务的fd基线差异很大,最好用动态基线或者观察一段时间后的分位数来确定。
6.2 容量评估方法:一笔账算出安全水位
容量规划的核心公式很简单:并发连接数约等于QPS乘以平均连接持有时间。如果想留出余量,再乘一个1.5到2的系数。
举个例子:QPS是2000,平均连接持有时间是1秒,那么多数时刻并发连接数在2000上下,瞬时峰值可能在3000到4000。如果服务采用的是短连接模型,还要把TIME_WAIT的数量算进去,也就是QPS乘以60秒的TIME_WAIT等待时间,那会是12万个端口,这显然不正常,要么改连接模型,要么接受必须扩容多个实例的事实。
按照这个公式,我建议每个服务上线前都做一次连接模型评估,把预期QPS、连接持有时间、连接复用程度写成文档。这样线上一旦出现端口告警,直接对照文档就能判断是容量不足还是异常增长,排查速度会快很多。
还有一点,平时做压测的时候,除了关注RT和QPS,别忘了观察TIME_WAIT和fd的变化曲线。压测是最便宜的演练,等线上真出问题再临时抱佛脚,代价就大了。
我个人经验来看,端口类故障十次里有七八次都能靠“连接池”和“监控预警”这两件事提前化解。修改内核参数永远只是最后的兜底手段,不要把它当首选。尤其记住,连接队列、fd、端口池这三层瓶颈各有各的解法,先分清方向再动手,排障效率能提升一大截。希望这篇文章能让你下次再遇到服务端口被打满时,少一些手忙脚乱,多一些按图索骥的从容。