服务器入坑指北:连不上但服务是好的

举报
Tulpartner 发表于 2026/10/21 08:16:18 2026/10/21
【摘要】 进程活着、端口在听、日志安静,客户端却一直超时——这未必是网络问题,更可能是 accept 队列满了:连接完成握手后堆在队列里等应用取走,应用根本没看到它,日志自然安静。本文按“先分清超时还是被拒绝 → 同屏看队列深度与上限 → 用溢出计数器差值取证 → 最后再决定改什么”的顺序,讲清半开队列与已完成队列、应用 backlog 与 somaxconn 取小、以及“先让应用取走速度跟上,再评估上限”

进程活着、端口在听,外面却连不上;高峰期必现,过了峰自己就好了;翻遍访问日志和错误日志,什么都没有——客户端只说超时,不给原因。于是排查从网络链路开始,一路查到安全策略,最后才怀疑服务本身,而时间已经过去半天。
这种"看着是好的"最容易被误判。连接建立其实分两段:握手完成后,连接要先堆进队列,等应用把它取走。堆在队列里的时候,应用根本还没看到它——所以日志是安静的,而客户端已经在超时了。这就是"服务是好的"这句错觉的来源。
这篇的顺序是:先看客户端是超时还是被拒绝 → 再看队列深度与上限 → 再看溢出计数器的差值 → 最后才决定改什么。文中数值都是示例,请以现读为准。

先分清:是超时,还是被拒绝

这个分岔决定了后面往哪个方向查,必须先问清楚:

  • 被拒绝:客户端收到重置(连接被直接拒绝)。常见原因是没有监听,或者被安全策略/拒绝规则挡掉。
  • 超时:客户端发出去之后什么回应都没有,重试到超时。更可能是队列满,或者链路层面把包丢了。
    队列满为什么会表现成超时?因为满了之后内核丢弃的是握手的起始包——客户端收不到任何回应,既没有同步确认,也没有重置,只能按自己的重试策略等到超时。所以现场看起来就是:客户端在超时,服务端一路安静。"超时"和"被拒绝"指向的层完全不同,这一步判错,后面全是在错误的地方找。

握手的两个队列:错误为什么没出现在日志里

监听套接字背后有两条队列:

  • 半开连接队列:收到了握手的起始包、但整个握手还没完成;
  • 已完成连接队列:握手已经完成,等着应用调用接受把它取走。
    应用侧的日志、错误计数、业务指标,都是在"取走之后"才会产生的。连接停在第二条队列里时,应用压根不知道有它——日志自然安静。应用侧看不到错误,正是队列被填满的典型特征。
    由此得到一条要反复用的判断:进程活着、CPU 在跑、端口在听,都不等于它在取连接。真实案例里出现过这样的组合:进程持续占着 CPU、句柄远没到上限、连接确实到达并且完成了握手,但队列始终没人取走。这时候"服务是好的"就是错觉——它在忙别的,或者在等一个被阻塞的环节。

队列深度与上限:同屏看两个数

看监听套接字时,有两个数要一起看:

ss -lnt

在监听(LISTEN)状态下:Recv-Q 是当前已完成队列的深度(等着被取走的连接数),Send-Q 是该队列的上限。判据就是这两个数的关系——Recv-Q 接近或贴住 Send-Q,并且持续如此,说明应用取走的速度跟不上进来的速度。
但要立刻补一句:瞬时值不是证据。队列深度会随负载抖动,抓一次读数看到一个"很大"或"贴顶"的值,并不能证明发生过溢出——高峰期本来就难免堆积,堆完能及时清掉就是正常的。拿一次瞬时读数当结论,是这条排查路上最常见的错误顺序。

真正的证据是计数器差值

溢出要用计数器证明,而不是用瞬时深度推测。内核为监听队列维护了溢出与丢弃的计数:队列溢出次数与被丢弃的握手包数。做法是间隔取两次,看是否增长,而不是只看一次的总量(总量从开机累计到现在,本来就很大)。

nstat -az | grep -E 'TcpExtListen(Overflows|Drops)'    # 第一次取值
# 间隔一个高峰窗口(或至少几分钟的同负载时段)再取一次,比较两个数字
cat /proc/sys/net/core/somaxconn                        # 内核侧的队列上限现值
ss -lnt                                                 # 复核队列深度与上限
grep -i backlog <应用配置中的监听位置>                   # 应用侧申请的值(按你的服务与配置位置)
cat /proc/net/sockstat                                  # 在用套接字总数(含监听与已建立),不是「已建立连接数」
ss -Htan state established | wc -l                       # 已建立连接数的趋势

判据:两次取值之间,任一计数器在增长(尤其在高峰期),才算拿到了溢出的证据;如果两个计数纹丝不动,那这个方向可以暂时放下,回去查别的层。计数器的名字在不同内核版本上可能不同,以本机 nstat 能列出来的对应项为准。另外,应用侧的申请值在不同技术栈里写法不同——配置项、命令行开关、代码里的常量都可能是它,找不到 backlog 这个字眼不等于没有设置,按你的服务实际形态去找。

上限是"取小":为什么"我在应用里调大了"没用

accept 队列的最大长度不是"你申请多少就多少",而是申请值与内核上限取较小者。也就是说:

  • 应用在监听时申请一个值(配置里常叫 backlog);
  • 内核侧另有一个上限(net.core.somaxconn);
  • 两者取小,才是真正的队列上限。
    这解释了一个常见困境:在应用配置里把 backlog 翻了几倍,现象毫无变化——因为内核那个值更小,一直是它在限制;反过来,只把内核参数调大、应用仍申请一个小值,同样没有效果。两处都要看,而且要拿小的那个当结论。

现象到动作的判定表

你看到的现象 该查哪条队列 证据 动作
客户端超时,服务端日志安静 已完成连接队列 Recv-Q 贴住 Send-Q,且溢出计数器在增长 先让应用更快取走;再评估上限
客户端收到连接被拒绝 通常不是队列 服务端有没有在监听、策略是否拒绝 查监听与访问策略
只有高峰期出现,过峰自愈 两条队列都要看 高峰期取两次计数器差值 治本在前;再看半开队列的上限
怀疑句柄快用完 — 打开句柄数与上限的对比 那是另一条线(句柄不足)

修复顺序:先让取走速度跟上

顺序本身就是判断,三步走:

  1. 让接收速度跟上(治本)。应用取走慢通常有明确根因:事件循环被阻塞(把耗时操作放在主循环里)、工作进程被打满、或者并发量本来就超出了这台机器的处理能力。对应动作:把阻塞操作移出主循环、异步化、增加工作进程;如果是突发流量打满,先限流保护自己。
  2. 再评估队列上限(治标)。放宽上限的作用只有一个——把"立刻失败"换成"先排队、后失败",等于买了一段时间。代价是每条排队的连接都要占内核内存,队列越深,内存占用越高,而且排队时间的增加对客户端来说仍然是延迟。
  3. 最后才考虑扩容或拆分。当单机的接收能力确实到顶,把负载分到多个实例才是解法。
    改参数只给形态与顺序,落地前先想好怎么回去:
# 内核侧(评估之后再做;两处要一起核对,取小者才是上限)
# sysctl -w net.core.somaxconn=<按评估值>                                  # 临时生效
# echo 'net.core.somaxconn = <按评估值>' > /etc/sysctl.d/99-backlog.conf   # 持久化
# 回滚:删除该文件,然后 sysctl --system 重新加载

改完怎么验证:每项一条判据

  • 溢出计数器不再增长:跨一个高峰窗口取两次差值,两个计数都保持不动。
  • 队列深度不再长期贴近上限:ss -lnt 多次采样,Recv-Q 与 Send-Q 之间留出了余量。
  • 客户端侧超时消失:在原来的高峰时段、用原来的并发复现一次,不再超时。
  • 高峰期复现窗口内观察一轮:验证必须落在"原来会出问题的那段时间",低峰期跑通不算数。
  • 旁证正常:已建立连接数的趋势平稳;如果"应用取走慢"本来就是根因,应用侧的 CPU 与阻塞迹象应当在改动后改善。
  • 验证方式固定成一句话:在出问题的那个时间窗口内复现原操作,不超时、计数器不涨,才算通过。

这篇不解决什么

  • 不重复连接状态与端口池:《TIME_WAIT 多不等于出故障》那篇讲的是"连接状态计数高是不是故障",并把"入站被拒"分流到队列与资源限制——本篇接住的正是这条指路。
  • 不解决句柄不足:《文件描述符与 inotify 上限》那篇解决的是"句柄不够"。它和"队列满了"都会表现为连不上,判定入口是报错文本与正在增长的计数器:是资源用尽的那类报错,还是队列溢出计数在动。
  • 不涉及本机服务起不来:那是约束视图与权限那篇的范围(《服务写不进去与沙箱边界》),与本篇没有交集。

踩坑清单

  • 只看一次瞬时队列深度就下结论:队列会抖,证据要看计数器的差值。
  • 只调应用申请的 backlog、不管内核上限(或反过来):两者的较小者才是实际上限。
  • 把超时当成"网络问题"直接去查链路:先分清楚是超时还是被拒绝,再决定查哪层。
  • 把"进程活着、端口在听"当成服务正常:它不等于应用在取连接。
  • 只用一次读数判断溢出:总量从开机累计,必须看一段时间内的增量。
  • 不区分"被拒绝"与"超时":前者多半是没有监听或被策略拒绝,后者更像队列满或丢包。
  • 盲目放大队列上限:应用取走慢时,这只是把立刻失败换成排队后失败,还推高内存。
  • 忽略事件循环被阻塞这个根因:上限调得再大,主循环卡住照样堆积。
  • 改完不看计数器差值就宣布修好:没有跨高峰窗口复现,等于没验证。
  • 只改了一处就以为上限变了:另一处更小的话,实际限制一直没动。
  • 把队列深度大当成故障:高峰期堆积但能及时清掉,属于正常。
  • 在低峰期验证高发问题:验证窗口必须落在原来出问题的那段时间。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。