想象一家叫“云客来”的餐厅。每天午市门口排着长队,老板却发现:每来一桌客人,他都要临时去人才市场“摇人”,面试、办工牌、培训再上岗。等新人磨磨蹭蹭走到桌前,客人早就不耐烦了——而且服务完这桌,老板又把服务员辞退,下一桌客人来了再重新“摇人”。在这个故事里,客人就是请求,服务员就是 TCP 连接,而“面试+培训”就是三次握手。在高并发系统里,连接建得越频繁,“招聘流程”的开销就越惊人,TIME_WAIT 这种“离职手续”还会堵住招聘入口。 聪明的老板最终养了一支固定的服务员队伍,并给队伍定下规矩:小单合并送(Nagle 算法)、定期报平安(KeepAlive)、忙时增援闲时轮休(连接池伸缩)。他还在办公室挂了一块仪表盘(监控指标),时刻盯着等位人数与离席人数。这就是高并发系统里“TCP 参数 + 连接池”的全部智慧——听起来朴素,做起来却能带来数倍的性能差距。

岗位关联

对 AI 从业者而言,网络层优化是分布式推理与大模型服务高吞吐、低延迟的必修课。掌握 TCP 参数与连接池调优,相当于为系统性能瓶颈精准开刀。

目录
  1. 可视化演示
  2. 网络连接瓶颈
  3. TCP核心参数
  4. 连接池原理
  5. 连接池参数
  6. 连接池实战
  7. 监控与排障
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示TCP三次握手建立连接的过程、连接池槽位复用与回收机制,以及网络参数调优前后的吞吐对比,可切换三种模式观察。

本节配有交互动效(进入视口后自动加载,约 3KB)。

TCP三次握手 客户端发SYN,服务端回SYN-ACK,客户端再ACK 握手完成后建立双向通道,进入数据传输 SYN → SYN-ACK → ACK 关闭时主动方进入TIME_WAIT持续2MSL。

网络连接瓶颈

想象“云客来”餐厅的午市:后厨做菜只要 30 秒,可每来一桌新客人,老板都要临时“摇人”。面试官问一句(SYN),候选人答一句(SYN-ACK),面试官再确认一句(ACK)——这三轮对话就是 TCP 的三次握手,每一轮都要花一个网络往返(RTT)。一轮“招聘”下来,至少 1.5 个 RTT 就没了。

局域网里 RTT 只有 0.5 毫秒,握手几乎无感;但在跨机房、跨地域的分布式系统中,RTT 动辄 30~80 毫秒,一次握手就是 45~120 毫秒。若每笔请求都新建连接,再乘以每秒上千的请求量,光“招聘”就吞掉了大把响应预算。更糟的是,短连接关闭后进入 TIME_WAIT 状态——像离职员工的工牌还要保留约 4 分钟(2MSL)来处理迟到的挂账,高并发下大量 TIME_WAIT 会耗尽本地端口,最终“无法开工”。

因此,网络优化的第一步是“看见”建连成本:把每次握手的 RTT 开销、TIME_WAIT 占用的端口资源都纳入系统的成本核算。下面的公式把建连总开销量化出来,方便对比“每次新建连接”和“连接池复用”两种模式的差距。

老板把这笔账算清楚后,立刻拍板:老员工一个都不能辞!与其每次花 1.5 个 RTT 重新“摇人”,不如让服务队伍长期稳定。把“招聘成本”从每一次请求中彻底抹掉——这正是连接池登上舞台的原因。

Tbottleneck=N×1.5×RTT+Tservice\displaystyle T_{bottleneck} = N \times 1.5 \times RTT + T_{service}
这个公式说明:N 个请求走“每次新建连接”的路子,建连总耗时等于请求数乘以 1.5 个网络往返,再加上业务处理时间。
  1. 单次建连开销一次三次握手等价于 1.5 个网络往返,这是 TCP 协议固定的成本
    Thandshake=1.5×RTT\displaystyle T_{handshake} = 1.5 \times RTT
  2. 建连累计开销N 个短连接请求重复支付握手成本,总和线性增长
    Tconnect=N×1.5×RTT\displaystyle T_{connect} = N \times 1.5 \times RTT
  3. 总耗时公式建连时间与业务处理时间相加,构成单请求的完整时间账
    Tbottleneck=Tconnect+Tservice\displaystyle T_{bottleneck} = T_{connect} + T_{service}

TCP核心参数

队伍养起来了,但“传菜风格”也得定规矩。TCP 默认开启的 Nagle 算法,就像一位爱省事的传菜员:他手里攒着好几张小纸条(小数据包),要等上一趟菜的确认回执(ACK)回来了,才肯跑一趟厨房。省力气、省路径,却让后面的客人多等。

对交互式业务——RPC 调用、数据库访问、实时推理——这种“攒一攒”是不能忍的。开启 TCP_NODELAY,相当于按下“即时传菜”按钮:每个小包立即发出、不再等待合并,延迟从几十毫秒级压到毫秒级。代价是网络报文变多,带宽利用率下降,所以它只适合低延迟敏感场景。

另一条规矩是 KeepAlive——经理的例行巡逻。TCP 默认在连接空闲 2 小时后才发起第一次探测,之后每隔 75 秒探测一次、共 9 次。这意味着一条“假死”连接(对端崩溃、网络黑洞)默认要等 2 小时 11 分钟才会被发现。生产环境必须调小这些值,或改用应用层心跳(如每 5 秒 ping 一次)。

规矩定好了,“云客来”的服务员既不会因攒单让客人干等,也不会让“失联”员工长期占着编制。TCP 参数看似琐碎,却决定了连接池里每条连接的“性格”。

Tdetect=Tidle+Nprobe×Tinterval\displaystyle T_{detect} = T_{idle} + N_{probe} \times T_{interval}
KeepAlive 发现假死连接的总时间 = 空闲等待时间 + 探测次数乘以探测间隔。默认值算下来超过 2 小时,远非“实时”。
  1. 空闲计时连接空闲 T_idle 秒后,内核才启动 KeepAlive 探测流程
    Tidle=7200 秒\displaystyle T_{idle} = 7200 \text{ 秒}
  2. 探测阶段连续发送 N 次探测包,每次间隔 T_interval 秒确认对端是否存活
    Nprobe×Tinterval=9×75 秒\displaystyle N_{probe} \times T_{interval} = 9 \times 75 \text{ 秒}
  3. 总检测时间空闲等待与探测阶段相加,得到发现假死连接的最坏耗时
    Tdetect=7200+675=7875 秒\displaystyle T_{detect} = 7200 + 675 = 7875 \text{ 秒}

连接池原理

“云客来”的老板决定养一支固定队伍。客人到店,直接指派一位空闲服务员;服务完,服务员回队待命而不是被辞退。这就是“池化复用”:把昂贵的创建开销前置到池中,用“借—还”替代“建—毁”,让每次都新建连接的成本彻底消失。

连接池的核心流程是:请求到达 → 借出一条空闲连接 → 执行业务 → 归还连接。听起来简单,实现却要小心三件事:并发安全——多个线程同时借连接,不能借到同一条;状态标记——借出的连接要打上“忙碌”标记,避免重复分配;生命周期——连接要定期体检,发现失效立即剔除。

池子里的连接是“活”的:健康检查线程周期性发送探活请求,把无响应的坏连接清理出队;空闲过久的连接被回收,释放文件句柄与内核内存。借出时若池已空,请求可以选择排队等待或直接失败,这由下一节的参数决定。

队伍还是那支队伍,但再也没有“摇人”的等待。连接池的复用机制让“云客来”的翻台率翻了一倍——在高并发系统中,这叫“用空间换时间”。

U=λ×TserviceCtotal\displaystyle U = \frac{\lambda \times T_{service}}{C_{total}}
池利用率 = 平均并发占用的连接数除以总连接数。它衡量了连接池的“忙闲程度”,是判断容量是否合理的第一指标。
  1. 平均并发占用单位时间请求数 λ 乘以平均服务时长,得到同时在途的连接数
    Cbusy=λ×Tservice\displaystyle C_{busy} = \lambda \times T_{service}
  2. 池利用率忙碌连接数占总连接数的比例,反映资源是否被有效利用
    U=CbusyCtotal\displaystyle U = \frac{C_{busy}}{C_{total}}

连接池参数

队伍养多少人有讲究。养多了,工位和工资都浪费;养少了,高峰期没人招呼客人。连接池参数就是老板的排班表:maxTotal 是最大编制,maxIdle 是待命人数的上限,minIdle 是保底待命人数,maxWait 则是客人最多能等多久。

最大连接数有一条经验公式:平均每秒请求数(QPS)乘以单请求平均耗时(毫秒),再除以 1000,就是所需的平均并发连接数,最后加上安全余量。比如 QPS=200、平均耗时 50ms,并发需求约 10,加 50% 余量后设置 15~20 比较稳妥。

minIdle 决定低谷期保留多少空闲连接,避免流量突然上涨时临时建连;maxWait 控制请求排队的最长时间,超时就快速失败走降级逻辑;idleTimeout 负责回收闲置过久的连接,防止服务端抢先断开。这四个参数互相配合,构成了连接池的完整行为边界。

好的排班表让“云客来”忙时不缺人、闲时不养闲人。连接池调参的本质,就是用最小的资源占用扛住最大的瞬时压力——这也是高并发设计的核心哲学。

Cmax=⌈QPS×Tavg1000⌉+M\displaystyle C_{max} = \left\lceil \frac{QPS \times T_{avg}}{1000} \right\rceil + M
最大连接数 = 向上取整(每秒请求数 × 平均耗时 ÷ 1000)再加上安全余量 M。它把“并发需求”翻译成“编制定额”。
  1. 并发需求量QPS 乘以平均耗时换算成毫秒级的在途连接数,再除以 1000 得到秒级并发数
    Cdemand=QPS×Tavg1000\displaystyle C_{demand} = \frac{QPS \times T_{avg}}{1000}
  2. 向上取整连接数必须是整数,向上取整保证能力不缩水
    Cceil=⌈Cdemand⌉\displaystyle C_{ceil} = \lceil C_{demand} \rceil
  3. 加安全余量M 是应对流量尖峰和慢请求的冗余,通常取需求量的 30%~100%
    Cmax=Cceil+M\displaystyle C_{max} = C_{ceil} + M

连接池实战

光有编制还不够,真正的考验在午市高峰。第一招是“预热”——午市前 10 分钟就把服务员全部叫到岗,把该建的连接提前建好。应用启动时,用预热代码让连接池达到 minIdle,流量高峰来临时就不用现建连接。

第二招“动态伸缩”:根据实时流量调整连接数,忙时增援、闲时轮休;第三招“故障剔除”:连接心跳多次无响应就直接换掉,不让坏连接占着编制;第四招“避免抖动”:不要在高频波动中反复开、关连接,否则握手与 TIME_WAIT 会把系统资源搅得天翻地覆。

工程落地时,预热、伸缩、剔除、防抖四件事通常由连接池框架(如 HikariCP、Druid)配置完成:initialSize 管预热、maxActive 管上限、testWhileIdle 管健康检查。调好这些参数,连接复用率能稳定在 95% 以上,这是高并发系统稳定的重要基石。

四招齐上,“云客来”在黄金周的午市高峰也从容不迫。系统设计里,决定成败的往往不是宏大的架构,而是这些藏在连接细节里的排兵布阵。

Tsaved=Cpreload×Thandshake\displaystyle T_{saved} = C_{preload} \times T_{handshake}
预热节省的总时间 = 预建连接数 × 单次握手耗时。把握手成本在流量高峰前一次性付清,峰会期间就无需再付。
  1. 预热连接数C_preload 取最小空闲连接数,保证池子在启动后立即被充满
    Cpreload=Cmin\displaystyle C_{preload} = C_{min}
  2. 节省的握手时间每条预建连接都省下一次完整的三次握手开销
    Tsaved=Cpreload×1.5×RTT\displaystyle T_{saved} = C_{preload} \times 1.5 \times RTT

监控与排障

老板的办公室挂着一块仪表盘:等位人数(等待获取连接的请求数)、在岗人数(活跃连接数)、待命人数(空闲连接数)、愤然离席的客人(获取超时次数)。这四组数字,一眼就能看出餐厅的健康状况。

如果等位人数持续上涨,说明池子太小或下游变慢;如果活跃连接长期贴着 maxTotal,说明容量吃紧;如果获取超时频繁,优先排查下游服务是否变慢,而不是一味调大池子——否则只是把请求堆在池里排队,治标不治本。

排障时,Little 定律是一条好用的公式:系统内请求数 L = 到达率 λ × 平均停留时间 W。对连接池而言,停留时间是排队加服务的时间。用它可以反向推算池子的合理容量,也可以验证当前配置是否与流量匹配。

仪表盘上的数字不会说谎。当老板学会读懂这些数字,就能在问题爆发之前把队伍调到最佳状态。监控不是终点,而是持续调优的起点。

L=λ×W\displaystyle L = \lambda \times W
Little 定律描述了系统内平均请求数等于到达率乘以平均停留时间,是连接池容量规划与故障定位的基石。
  1. 入池速率λ 为每秒进入连接池的请求数,即业务 QPS
    λ=QPS\displaystyle \lambda = QPS
  2. 平均停留时长W 是请求从借到还的平均耗时,包含排队与服务两段
    W=Tqueue+Tservice\displaystyle W = T_{queue} + T_{service}
  3. 系统内连接数两者相乘,得到任意时刻停留在池子系统内的平均请求数
    L=λ×W\displaystyle L = \lambda \times W

AI 应用场景

大模型推理服务的 RPC 网关

阶段:模型服务层 / 推理请求入口

解决的问题:大模型应用客户端 SDK 每次推理都新建 gRPC 连接,高并发下三次握手成为瓶颈,P99 延迟飙升。通过连接池复用和开启 TCP_NODELAY,把建连延迟从几十毫秒压到接近 0。

具体案例:某 AI 问答产品接入大模型 API,压测 1000 QPS 时 P99 高达 850ms;开启连接池并预热 50 条连接、关闭 Nagle 算法后,P99 降至 580ms,吞吐提升约 30%。

分布式训练的通信链路

阶段:数据并行训练 / 参数同步

解决的问题:分布式训练中,GPU 节点间高频同步梯度,小包多、频率高,默认 Nagle 与 KeepAlive 参数会放大通信延迟。调优 TCP 参数并复用连接,可显著提升训练吞吐。

具体案例:8 卡训练集群调整 TCP 缓冲区并对参数同步通道启用连接池后,通信等待时间降低约 18%,整体训练耗时缩短约 6%。

在线推荐的特征存储访问

阶段:特征工程 / 在线特征检索

解决的问题:推荐系统每个请求都要查特征数据库,短连接风暴会导致数据库连接数被打满。连接池配合 minIdle 常驻连接,把数据库压力控制在稳定水位。

具体案例:某电商推荐系统将特征库客户端改为连接池后,数据库连接数从峰值的 800 降到 60,接口超时率下降 90%。

Python 演示

python66 行
import queueimport threadingimport time class FakeConn:    """模拟一条需要握手建连的TCP连接"""    def __init__(self, cid):        time.sleep(0.02)  # 模拟三次握手耗时        self.cid = cid     def execute(self):        time.sleep(0.01)  # 模拟业务耗时 class ConnPool:    """一个极简的线程安全连接池"""    def __init__(self, max_size=5):        self.max_size = max_size        self._pool = queue.Queue()        self._made = 0        self._lock = threading.Lock()     def acquire(self, timeout=1.0):        deadline = time.time() + timeout        while True:            try:                return self._pool.get_nowait()            except queue.Empty:                with self._lock:                    if self._made < self.max_size:                        self._made += 1                        return FakeConn(self._made)                if time.time() > deadline:                    raise TimeoutError("等待空闲连接超时")                time.sleep(0.01)     def release(self, conn):        self._pool.put(conn)  def bench(pool, n):    start = time.time()    for _ in range(n):        c = pool.acquire()        c.execute()        pool.release(c)    return time.time() - start  def bench_without_pool(n):    start = time.time()    for _ in range(n):        FakeConn(0).execute()    return time.time() - start  if __name__ == "__main__":    n = 20    pool = ConnPool(max_size=4)    warm = [pool.acquire() for _ in range(4)]    for c in warm:        pool.release(c)    t1 = bench(pool, n)    t2 = bench_without_pool(n)    print("连接池模式总耗时: %.3f s" % t1)    print("每次新建模式总耗时: %.3f s" % t2)    print("性能提升: %.1f 倍" % (t2 / max(t1, 1e-6)))
目的:用最小实现演示连接池的“预热—借—还”机制,并对比“每次新建连接”与“连接池复用”两种模式的总耗时,直观量化三次握手的开销。
  1. time.sleep(0.02) # 模拟三次握手耗时用固定延时模拟网络握手成本,让对比可见
  2. class ConnPool:定义连接池类,管理连接的借出与归还
  3. self._pool = queue.Queue()用队列保存空闲连接,天然具备线程安全特性
  4. self._made = 0统计当前已创建过的连接总数,用于判断是否还可以新建
  5. def acquire(self, timeout=1.0):借连接入口,带超时保护
  6. return self._pool.get_nowait()池中有空闲连接时直接复用,零等待返回
  7. if self._made < self.max_size:池空但未达上限时,才允许“摇人”新建连接
  8. raise TimeoutError("等待空闲连接超时")池空且超过等待时间,立即失败而非无限阻塞
  9. def release(self, conn):归还连接,把服务员“放回队伍”
  10. warm = [pool.acquire() for _ in range(4)]预热:启动阶段提前建满连接,高峰不再建连

常见误区

误区我把连接池的 maxTotal 调到了 500,没想到数据库反而变慢了。连接数越多,并发能力不是越高吗?

正解一条连接要占用文件句柄、内核缓冲区,数据库服务端还要为每条连接分配资源。连接数超过数据库处理能力后,大量连接排队等待,整体吞吐反而下降。正确的做法是基于 QPS 和平均耗时算出并发需求,再加余量。就像“云客来”招 500 个服务员,店面只能坐 100 人,剩下 400 人在走廊里闲逛、堵路。

为何常见:“更多资源等于更高性能”符合直觉,但忽略了连接背后的成本与服务端的并发上限。

误区我把所有服务都开了 TCP_NODELAY,结果传输大文件时吞吐反而变低了,这个参数不是优化延迟的吗?

正解TCP_NODELAY 关闭 Nagle 合并算法,让小包立即发送。它优化的是“交互式延迟”,但对大量小包场景,每个包都独自传输会增加报文头占比、浪费带宽。对吞吐敏感的大数据传输,保留 Nagle 的合并能力反而更高效。选不选它,取决于业务是“等回音”还是“倒数据”。

为何常见:教程强调“低延迟场景开启 NODELAY”,读者容易忽略它的适用边界,把局部经验当成全局真理。

误区我开了 KeepAlive,假死连接应该马上就被发现和回收了吧?

正解Linux 默认 KeepAlive 空闲 2 小时才发起探测,之后 9 次探测、每次间隔 75 秒,最坏要 2 小时 11 分钟才会发现断连。它只是内核的“兜底保险”,不是实时心跳。生产环境要么调小内核参数,要么用应用层心跳(如每 3~5 秒 ping)及时感知对端状态。

为何常见:“开启这个参数功能就立即生效”是常见的认知误区,忽略了默认值的时间尺度。

误区连接池参数在压测时调得很好,上线后就不用再动了。

正解流量是动态的:QPS 翻倍、下游变慢、网络抖动,都会让原来的参数失效。连接池需要持续监控“空闲/活跃/等待/超时”四个指标,定期用压测校准。配置只是起点,调优是持续的过程,就像餐厅的排班表需要随季节和节假日不断调整。

为何常见:把系统看成静态的,忽略了高并发场景的动态性。

面试题

Q1. TCP 三次握手中,客户端最后发送的报文是?

单选题 · easy

  • A.SYN 报文
  • B.ACK 报文← 正确答案
  • C.FIN 报文
  • D.RST 报文

答案:B。三次握手依次为 SYN、SYN+ACK、ACK,客户端最后发送 ACK 确认服务器的 SYN+ACK,连接建立。FIN 用于关闭连接,RST 用于异常复位,都不属于建连流程。就像招聘流程中最后一轮“确认入职”必不可少。

Q2. Nagle 算法的核心思想是什么?

单选题 · easy

  • A.让每次发送都立即传输
  • B.合并小包减少网络报文数← 正确答案
  • C.丢弃超时重传的报文
  • D.对传输内容进行压缩

答案:B。Nagle 算法把多个小数据包合并成一个大包再发送,减少报文数量、降低网络拥塞,代价是延迟上升。选项 A 是 TCP_NODELAY 的行为,C 与重传机制无关,D 是应用层压缩。就像传菜员把多张小单攒成一趟送,省力但慢。

Q3. 开启 TCP_NODELAY 后,会产生什么效果?

单选题 · mid

  • A.关闭 Nagle 立即发小包← 正确答案
  • B.开启延迟确认机制
  • C.增大接收缓冲区的上限
  • D.缩短三次握手过程耗时

答案:A。TCP_NODELAY 关闭 Nagle 算法,允许小包不等待合并立即发送,主要降低交互式场景的延迟。延迟确认与 NODELAY 相互独立;缓冲区大小由系统参数配置;握手时长由 RTT 决定,均不受影响。

Q4. 默认 TCP KeepAlive 机制在连接空闲多久后开始探测?

单选题 · mid

  • A.1 秒
  • B.30 秒
  • C.2 小时← 正确答案
  • D.24 小时

答案:C。Linux 默认 tcp_keepalive_time 为 7200 秒,即空闲 2 小时后才发起第一次探测,随后 9 次探测、每次间隔 75 秒。生产环境通常会调小这些值。把 KeepAlive 当作实时心跳是错误的,应用层心跳更可靠。

Q5. 大量 TIME_WAIT 连接的主要危害是?

单选题 · mid

  • A.占用本地端口导致无法新建连接← 正确答案
  • B.增加服务器的 CPU 计算负载
  • C.导致数据包被加密后无法解密
  • D.使服务器内存容量自动减半

答案:A。TIME_WAIT 持续约 2MSL(约 4 分钟),期间占用本地端口。短连接高并发场景下端口耗尽时,新连接无法建立。缓解手段包括开启 SO_REUSEADDR、使用连接池复用连接。CPU、加密、内存与此无直接关系。

Q6. 连接池最大连接数设置过大,可能引发的后果有?

多选题 · mid

  • A.占用过多的文件句柄与内存← 正确答案
  • B.闲置空闲连接不断空耗资源← 正确答案
  • C.有效提升单条连接的复用率
  • D.服务端连接数超限而排队变慢← 正确答案

答案:ABD。每条连接都占用客户端文件句柄、内核缓冲,空闲连接同样空耗资源;服务端连接数过多会超过其处理能力导致排队变慢。连接过多会摊薄单条连接的复用率而非提升,故 C 错误。

Q7. 估算连接池的最大连接数,较合理做法是?

单选题 · mid

  • A.按 QPS 乘以平均耗时加余量← 正确答案
  • B.直接设置为 CPU 核心数的两倍
  • C.尽量设大以避免出现等待超时
  • D.按前端页面的元素数量来确定

答案:A。连接池大小应由并发需求量决定:QPS 乘以单请求平均耗时得到平均在途连接数,再乘 1.5~2 倍余量。CPU 核心数乘二是线程池的经验公式,不适用于 I/O 型连接池;设太大会浪费资源;页面元素数与连接数无关。

Q8. 关于 KeepAlive 检测假死连接的描述,正确的是?

单选题 · hard

  • A.默认参数下能很快发现断线
  • B.应用层心跳通常比其更及时← 正确答案
  • C.KeepAlive 能检测报文乱序
  • D.开启后连接不再占用任何资源

答案:B。默认 KeepAlive 需空闲 2 小时才探测,最坏超过 2 小时才发现断连,远非“很快”。应用层心跳(如每隔 3~5 秒 ping)检测更及时。KeepAlive 只探测连接死活,与乱序检测、资源释放无关。

Q9. 下列哪些属于连接池带来的好处?

多选题 · hard

  • A.复用已建立好的连接← 正确答案
  • B.减少三次握手的次数← 正确答案
  • C.彻底消除网络带宽消耗
  • D.支持请求排队等待与超时← 正确答案

答案:ABD。连接池通过复用连接避免高频建连,从而大幅减少三次握手次数,同时提供获取超时与排队机制。但网络带宽消耗不会被消除,报文传输本身仍要占用带宽,故 C 错误。

Q10. 关于 Nagle 算法与延迟确认的相互作用,正确的有?

多选题 · hard

  • A.互相等待造成额外网络延迟← 正确答案
  • B.开启 TCP_NODELAY 可规避← 正确答案
  • C.延迟确认减少 ACK 报文数← 正确答案
  • D.Nagle 会增大缓冲区上限

答案:ABC。Nagle 等 ACK 才发新数据,延迟确认等数据才发 ACK,二者互相等待形成约 40ms 的延迟叠加;TCP_NODELAY 关闭 Nagle 后不再等待。延迟确认确实合并多个 ACK、减少报文数。缓冲区上限与 Nagle 无关,D 错误。