【HTTP 503】区分连接超时、RST、503:网卡 / 内核 / 应用三层网络故障底层原理

作者介绍

哈喽,我是 CodeStats

一个在底层技术上"考古"了四年的硬核爱好者,也是 WWAIC(全周项目AI编程) 范式的提出者和实践者。我曾手写过一个完整的 Java Web 框架(从 IoC 容器到嵌入式 Tomcat,代码全开源),也喜欢用通俗的语言拆解 CPU、JVM、操作系统的运行本质。

我一直相信,计算机科学没有魔法 。所有看似神奇的效果------无论是 java -jar 一键启动,还是多线程自动切换------底层都是简单的规则层层组合。

本文你能收获什么

线上服务动不动报 503 Service Unavailable,你是不是还在疯狂重启应用、盲目扩容?

很多人把 503 简单理解为"服务器压力大",但压力大在哪个环节?是应用层的线程池满了?是 Socket 的全连接队列溢出了?还是网卡环形队列丢包了?

读完本文,你将彻底搞清楚:

  • 503 到底是应用层返回的状态码 ,还是内核协议栈的"无声拒绝"

  • 全连接队列(Accept Queue)线程池拒绝策略到底什么关系?

  • 数据从网卡到 Socket 缓冲区,完整走过了哪些"关卡"?

  • 为什么网卡环形队列只有几百个槽位 ,却能扛住数十万并发

  • 内核如何靠四元组/五元组把数据精准送到你的应用程序?

点赞收藏不迷路,硬核底层干货持续输出! 👍


目录

  • 提问一:为什么 503 是服务 Socket 队列满了?和线程池有何关系?

  • 提问二:除了应用层 Socket 队列满,网卡和内核五元组找不到服务会报 503 吗?

  • 提问三:数据从网卡环形队列到 Socket 缓冲区,完整流程是什么?

  • 提问四:为什么网卡环形队列只有几百大小,能抗住数十万并发?

  • 提问五:内核如何通过五元组和四元组区分进程和哪个应用程序接收数据?

  • 总结

提问一:为什么 503 是服务 Socket 队列满了?和线程池有何关系?

先给结论:503 是应用层返回的状态码,但它的根因可能扎在应用层,也可能扎在内核层。

场景1:线程池拒绝策略触发(应用层)

Tomcat、Netty、Spring Boot 内嵌容器,底层都是用线程池(ThreadPoolExecutor) 来处理 HTTP 请求的。

当请求量暴增,线程池的处理逻辑是:

text

复制代码
核心线程满了 → 任务放入阻塞队列 → 阻塞队列也满了 → 创建非核心线程 → 最大线程也满了 → 触发拒绝策略

如果拒绝策略是 AbortPolicy(默认) ,线程池直接抛出 RejectedExecutionException。上层容器捕获到这个异常后,往 HttpServletResponse 里写入状态码 503,然后 flush 出去。

这就是你最常见的 503:应用层线程池满了,容器帮你把异常翻译成了 503。

场景2:全连接队列(Accept Queue)满了(内核层)

但这只是冰山一角。更隐蔽的场景发生在内核层

服务端执行 listen(fd, backlog) 时,内核为该 Socket 维护一个全连接队列(Accept Queue) ,存放已完成三次握手、正在等待 accept() 取走的连接。

当这个队列满了(backlog 参数控制大小),而应用层 accept() 又来不及取走连接时,内核的处理策略是:直接丢弃第三次握手的 ACK 包 。应用层压根不知道有这个连接来过,自然也不会触发 RejectedExecutionException

但这时候,Nginx/网关收不到上游响应 ,最终会给客户端返回 503

所以,503 既可能是应用层线程池拒绝,也可能是内核全连接队列溢出。前者你能在日志里看到 RejectedExecutionException,后者你连日志都看不到。

提问二:除了应用层 Socket 队列满,网卡和内核五元组找不到服务会报 503 吗?

网卡和内核"找不到服务"不会报 503,会报更底层的错误。

网卡环形队列(Ring Buffer)满了

网卡通过 DMA 将数据包写入环形队列(Rx Ring Buffer) 。如果这个队列满了,新来的数据包会被网卡硬件直接丢弃(Drop)

操作系统根本收不到这个包,连中断都不会触发。这种场景下,客户端得到的是连接超时(Timeout)或 RST 包,根本轮不到 503。

五元组找不到对应的 Socket

内核收到数据包后,提取五元组(源IP、源端口、目的IP、目的端口、协议) ,去 established 哈希表(ehash)里查找对应的 Socket。

如果找不到(比如连接已经被关闭),内核直接回一个 RST 包 ,连接立即重置。这也不是 503,是"Connection Reset"。

503 只发生在"应用层还在,但处理不过来"的场景。网卡丢包和五元组查不到,根本轮不到应用层插手。

提问三:数据从网卡环形队列到 Socket 缓冲区,完整流程是什么?

这是全链路最硬核的部分,我们按时间线拆开:

第一站:网卡 DMA 写入环形队列

网卡收到数据包后,通过 DMA(直接内存访问) 将数据包写入驱动预分配的 Rx Ring Buffer(环形队列) 。这个队列是网卡驱动级别的,跟 IP、TCP、Socket 还没半毛钱关系。

第二站:硬中断 + 软中断(NAPI)

网卡向 CPU 发起硬中断 ,通知"来活了"。CPU 进入中断处理函数,屏蔽中断,发起软中断(避免 CPU 被频繁中断拖死)。

内核 ksoftirqd 线程 在软中断上下文里,从网卡环形队列批量取出数据包,封装成内核数据结构 sk_buff

第三站:IP 层解析(Netfilter/路由)

内核拿到 sk_buff 后,剥离以太网头,看 IP 头。走 ip_rcv,查路由表,判断是本机接收还是转发。

第四站:传输层解析 + 四元组/五元组查找

确认是本机接收后,进入传输层(TCP/UDP)。内核根据四元组(源IP、源端口、目的IP、目的端口)或五元组(+协议) ,去查找本机的 Socket 哈希表,找到对应的 struct sock 结构体。

第五站:放入 Socket 接收缓冲区

找到 Socket 后,如果该 Socket 处于 ESTABLISHED 状态,内核把 sk_buff 挂到该 Socket 的接收缓冲区链表(sk_receive_queue 上。

应用层调用 read(fd) 时,内核通过 fd 找到 struct sock,从接收缓冲区取下数据,拷贝到用户态内存。

提问四:为什么网卡环形队列只有几百大小,能抗住数十万并发?

这是最容易被误解的地方!几百是"队列深度(容量)",不是"处理能力(吞吐量)"。

关键公式

吞吐量(pps)≠ 队列深度(个数)

吞吐量 = 队列深度 / 单包处理耗时。

网卡 DMA 环形队列默认 256 或 512 个槽位 。这 256 个槽位是流水线工位,不是仓库。

流水线原理

数据包进来触发硬中断 → 内核 NAPI 立刻把这批包从网卡队列批量摘走 ,清空到内存的 sk_buff 池里。网卡队列瞬间又空出来了。

队列大小只决定"突发容忍度",不决定"总吞吐上限"。

真正的天花板是 CPU 处理单包的中断开销和协议栈解析速度,而不是这几百个槽位。设计成几千个反而会增加 DMA 寻址延迟和 Cache 污染。

几百个槽位完全够用,因为它是"高速转盘",不是"停车场"。

提问五:内核如何通过五元组和四元组区分进程和哪个应用程序接收数据?

四元组/五元组是"连接身份证"

一个 TCP 连接由 四元组(源IP、源端口、目的IP、目的端口) 唯一标识。如果算上协议(TCP/UDP),就是 五元组

只要四元组中有一个元素不同,就是两个不同的连接

内核的查找机制:哈希表(O(1))

内核绝不遍历 所有 Socket 去找。它维护了一张 established 哈希表(ehash ,表中存储所有处于 ESTABLISHED 状态的 struct sock 指针。

当数据包到达时,内核提取四元组/五元组,做一次 Hash 计算 ,直接在哈希表里命中目标 Socket。O(1) 复杂度,微秒级定位。

如何关联到应用程序?

  • 内核态 :每个 Socket 对应一个 struct sock 结构体,存着五元组、状态、缓冲区指针等。

  • 用户态 :每个 Socket 对应一个文件描述符(fd) 。当前进程的 fd 数组指向了内核中对应的 struct sock

  • 数据投递 :内核把数据挂到目标 Socket 的接收缓冲区后,唤醒等待在该 Socket 上的进程(或通过 epoll 返回事件,带上 fd)。

  • 应用读取 :应用调用 read(fd),内核通过 fd 反向查出 struct sock,从缓冲区拷走数据。

哈希表负责"收件路由",fd 负责"取件凭证"。这就是内核精准投递数据的底层秘密。

总结

层级 队列/组件 满了会怎样 客户端看到什么
网卡硬件 DMA 环形队列(Ring Buffer) 硬件丢包 连接超时 / RST
内核 TCP 层 全连接队列(Accept Queue) 丢弃 ACK,应用层无感知 网关/代理返回 503
应用层 线程池 + 阻塞队列 抛出 RejectedExecutionException 503 Service Unavailable
内核 Socket 层 接收缓冲区(sk_receive_queue TCP 窗口缩小,流量控制 响应变慢,不会报错

503 的真正含义是:你的请求到达了服务器,但服务器(应用层或内核层)当前没有能力处理它。

排查 503,先看应用日志有没有 RejectedExecutionException------有,就是线程池满了;没有,去看 netstat -ant | grep :端口Recv-Q 列------如果接近 Send-Q,就是全连接队列满了;再往深挖,用 ethtool -S 看网卡丢包统计。

从网卡到内核到应用,每一层都有可能是 503 的根因。搞懂底层,才能精准定位。

如果觉得有帮助,点赞收藏支持一下!有疑问欢迎评论区交流~ 👇