
“keep-alive 这个词面试背过、联调翻过车、线上被坑过的人不在少数。在我看过的简历和面过的人里十个有八个能说出‘HTTP 长连接’这句话但再追问一句‘长连接是谁在维持、维持多久、断了之后谁来发现’一半人就卡住了。这个现象很有意思它明明是最常被当八股文记的知识点却在真出故障时最容易让人懵。这篇东西我不打算列一堆背诵口诀而是想从 HTTP、TCP 再到前端框架把 keep-alive 彻底拆开说说它背后的机制为什么值得理解以及你实际调参、排查问题时到底该怎么用。”1. 先聊清楚keep-alive 到底是不是一个东西1.1 一个单词三层含义很多人一听到 keep-alive脑子里直接弹出“HTTP 长连接”然后关闭话题。但 keep-alive 这个词在计算机体系里起码有三层完全不同的含义第一层是 HTTP 协议里的Connection: keep-alive它的作用是复用 TCP 连接避免每个请求都重新握手这是面试八股文的重灾区。第二层是 TCP 协议栈里的 KeepAlive 机制它是一个内核级的心跳探测开关用来发现对端是不是已经挂了。它跟 HTTP 层那个“保持连接不断”完全是两码事。第三层是前端框架层面的组件缓存比如 Vue 的keep-alive它帮你在切换页面时保留组件状态避免反复渲染。这三层共用同一个单词却分别工作在应用层、传输层和组件层。我见过不少人在面试时把“TCP KeepAlive 是用来保持长连接的”这句话说出口面试官没有当场纠正但评价已经打了折扣。因为 TCP KeepAlive 的核心职责不是保持而是探测。1.2 面试官想挖的“下一层”我后来复盘过自己在面试中喜欢怎么问 keep-alive。面对回答“这是 HTTP 长连接”的候选人我通常会追问下面几个问题HTTP/1.1 默认就开启了 keep-alive那Connection头还有必要写吗server 返回的Keep-Alive: timeout5到底是“5 秒断开”还是“5 秒内没有请求就断开”一个 TCP 连接上发了 10 个 HTTP 请求这 10 个请求里哪些算同一个 keep-alive 连接如果 Nginx 在 60 秒后关掉空闲连接而下游客户端还在用它发请求会发生什么这些问题看起来是在考协议细节其实都是在验证你有没有真正理解“连接复用”这件事。很多在应用层写代码的人从来没关心过底层连接是怎么建立的所以一旦出现线上连接数暴涨或CLOSE_WAIT堆积就只能重启大法。这也是我为什么想把这一层知识重新翻出来讲的原因它不冷门但很容易被忽略成“背过的句子”。2. HTTP Keep-Alive连接复用这件事2.1 Connection 头HTTP/1.0 留下来的开关HTTP 刚问世的时候每个请求都是独立的 TCP 连接请求完就把连接关掉。那时候网页简单问题不大。后来页面资源越来越多一个页面几百个请求每次都握手成本和延迟都肉眼可见地涨。于是 HTTP/1.0 引入了Connection: keep-alive这个非标准头意思是“这个 TCP 连接先别关我可能马上还要用”。到了 HTTP/1.1keep-alive 直接转正为默认行为。也就是说只要不主动写Connection: close连接默认就是复用的。这带来一个很常见的面试坑现在很多人写代码时仍然在响应头里加Connection: keep-alive其实在 HTTP/1.1 里这是多余的。它不会报错但它暴露了写的人对协议版本演进不熟。真正需要关注的反而是Connection: close的场景。比如服务端希望尽快回收连接、或者某个接口是一次性请求不需要复用这时候显式地关掉 keep-alive 才是有意义的操作。我见过一些误配置把Connection: close写进静态资源服务器的响应头导致每个文件请求都新建连接页面加载速度直接掉一截。这类问题如果只盯业务代码永远发现不了根因。2.2 Keep-Alive 的超时与最大请求数怎么设才合理HTTP 层的 keep-alive 不是无限期的。服务端通常会设置一个空闲超时时间在这个时间内如果有新请求进来就继续复用这个 TCP 连接如果一直没有请求超时后服务端就会主动关闭连接。这个时间由Keep-Alive响应头里的timeout字段声明单位是秒。举个例子Nginx 默认配置里常见的写法是keepalive_timeout 65; keepalive_requests 100;意思是空闲 65 秒后关闭连接一个连接上最多处理 100 个请求超过就关掉。这两个参数非常值得认真调。在设keepalive_timeout时不只考虑自己的服务还要看客户端和中间设备的限制。比如你设了 120 秒但客户端或负载均衡器设的是 60 秒那么到 60 秒左右连接就会被更中间的一方断开。这时候如果客户端不感知重连就会报“Connection reset”。我在线上调过一些场景最后的经验是超时时间尽量和各层对齐不追求长只追求预期一致。一般 30 到 75 秒都是常见区间。keepalive_requests也容易被忽略。早期协议实现里一个连接理论上可以无限复用到超时为止。但现实是连接挂得越久中间经过的 NAT 设备越容易把它的状态丢掉。定期用最大请求数来切断连接反而是降低故障率的手段。如果你做的是流媒体或大文件传输这类请求耗时特别长的服务可以把请求数调小一些避免一条老连接占着资源不动。2.3 HTTP/2 出现以后Keep-Alive 还重要吗面试里还有一个进阶问题HTTP/2 都已经多路复用了keep-alive 是不是该淘汰了事实恰恰相反。HTTP/2 本身就是在一条 TCP 连接上跑多个并发流它比 HTTP/1.1 更依赖这条底层连接的稳定性。连接一旦断开所有并发流都会受影响。所以 HTTP/2 里的连接保活、空闲超时、连接迁移这些问题仍然属于 keep-alive 思路的延续只是不叫这个名字了。而且生产环境里 H2 的协商是带 TLS 的也就是 ALPN 协议在握手时选出来的。一个连接如果握手之后因为超时被关掉重新协商的成本比 HTTP/1.1 还高。这也意味着服务端和客户端对空闲超时的设定必须更谨慎否则一条连接被中间层悄悄断开客户端可能完全不知道直到下一个请求发出去才发现“连接已经被重置”。所以我的结论很朴素HTTP keep-alive 没有过时只是从显式配置变成了默认策略同时责任更重了。你不需要记住每个参数但要清楚连接生命周期是由谁决定的以及断开后客户端会有什么表现。3. TCP KeepAlive真正干“心跳”的哥们3.1 它解决的问题和你以为的不太一样TCP 层也有 KeepAlive翻译成“保活”其实容易误解。它要解决的问题不是“怎么让连接不断”而是“一个看起来还在的、但可能已经死掉的连接怎么被及时发现”。假设你有一条 TCP 连接客户端断电了没有发 FIN也没有发 RST。服务端这边的连接不会立刻消失内核里它还是 ESTABLISHED 状态占着文件描述符和内存。如果不做任何处理这个假连接可能一直挂到某个超时才被清理期间服务端的连接数会缓慢上涨最终逼近上限。TCP KeepAlive 的机制是连接空闲超过一定时间后内核主动发一个很小的探测包看看对端还活着没有。如果对端正常回 ACK说明连接还在继续保持如果没回应就隔一段时间再发几次直到确认对端不可达才真正关闭连接。注意这个机制默认在多数 Linux 系统里是关闭的。也就是说那些假连接在一段时间内确实没法被自动发现除非你的应用层自己实现了心跳。3.2 三个内核参数别一上来就改 60 秒Linux 下 TCP KeepAlive 主要受下面三个参数控制net.ipv4.tcp_keepalive_time 7200 net.ipv4.tcp_keepalive_intvl 75 net.ipv4.tcp_keepalive_probes 9默认逻辑是连接空闲 7200 秒2 小时后开始探测每 75 秒探一次连续 9 次没响应就断开。也就是说从闲置到真正断开可能要两个多小时。在多数场景下这个节奏太慢了所以做长连接服务的同学通常会把它调短。但调整之前要想清楚一个代价探测包也是流量而且可能被中间的防火墙或负载均衡器误判。比如你把tcp_keepalive_time调到 60 秒那每 60 秒就有一次探测包网络路径上任何防火墙如果对包频率敏感反而会带来新问题。我个人的建议如果是内部服务可以调到 300 秒到 600 秒之间既能在几十分钟内发现故障又不会产生太多冗余探测如果是面向公网的服务要事先评估中间设备的行为别只盯着自己这台机器的参数。有些人会问应用层已经做了心跳还需要 TCP KeepAlive 吗答案是可以同时开。应用层心跳能表达更丰富的业务状态比如“我还活着但很忙”TCP KeepAlive 则用来兜底防止对端进程彻底无响应时连接一直挂着。两者不冲突只是级别不同。3.3 和 HTTP 长连接串联起来看很多人把 HTTP keep-alive 和 TCP KeepAlive 混在一起这里用一个时间线就能理清客户端发出 HTTP 请求TCP 连接建立成功后这条连接默认被 HTTP 层保留用来发后续请求。如果 70 秒内没有新请求HTTP 层的keepalive_timeout到期服务端主动发送 FIN正常关闭连接。在整个空闲期内TCP 内核随时可能执行自己的 KeepAlive 探测它跟 HTTP 层那个 65 秒超时是并行运行的互不冲突。我把这条时间线解释给不少同事听之后他们终于明白一个线上现象为什么 HTTP 超时设了 65 秒但 netstat 里还能看到 70 秒、80 秒的连接因为还有一层 TCP KeepAlive 在后台运行或者恰好有请求在超时前一两秒打进来把连接又续命了。这两个保活机制的关系可以类比为HTTP keep-alive 是“应用层的排队规则”TCP KeepAlive 是“系统级的体检项目”。排队规则决定谁可以继续用这个窗口体检项目决定窗口是不是早就坏了。只看其中一个都容易得出错误结论。4. 前端框架里的 keep-alive业务里最常用的一层4.1 Vue 的 keep-alive缓存谁不缓存谁如果说 HTTP 和 TCP 层的 keep-alive 偏向后端和运维那前端框架里的keep-alive就更贴近日常业务。Vue 的keep-alive是一个内置组件它不会渲染成 DOM而是把它包裹的动态组件缓存起来。当一个组件被v-if或路由切换隐藏时默认会被销毁下次再进来重新创建状态全部丢失。用keep-alive包裹后组件实例和它的 DOM 状态会被保留在缓存里下次展示时直接复用。基本的用法是将路由出口或动态组件包起来template keep-alive :include[Home, List] :max10 component :iscurrentComponent / /keep-alive /template这里面的include和exclude可以按组件 name 决定哪些组件需要缓存哪些不需要。一个常见的需求是列表页需要缓存滚动位置详情页每次都要重新拉数据。那么就把列表页加进include详情页排除掉。真正的细节在于生命周期。用了keep-alive后组件不会走标准的destroyed流程而是多出两个钩子activated和deactivated。一个常见错误是把“每次进入页面都要重新请求数据”的逻辑写在mounted里结果第一次进入会请求第二次从缓存恢复时mounted不再触发数据就变成了旧的。解决办法是把这类逻辑从mounted挪到activated。同时保留deactivated来做离开页面前的状态清理比如停止轮询、取消定时器。这个陷阱我让不少前端同事踩过也是面试中比较好用的一道追问。4.2 max 与 LRU隐藏考点其实在这里Vue 的keep-alive还支持一个max属性用来限制最多缓存多少个组件实例。一旦超出限制最久没有被访问的实例会被销毁。这个策略就是 LRULeast Recently Used缓存淘汰算法。很多面试者能背出“LRU”但说不清它在 Vue 里是怎么触发的。实际上Vue 在缓存组件时会用键值对保存组件 vnode内部维护一个命中顺序。每次命中缓存都会把这个组件调整到最近使用的位置当缓存数量超过max就从最久未使用的位置开始清理。了解这一点能帮你在业务上做取舍如果页面很多每个页面状态又很大把所有页面都放进keep-alive是不现实的。正确做法是设置一个合理的max比如 5 到 10让最常用的页面留在缓存不常用的自动淘汰。顺带一提keep-alive只是缓存了组件树和 DOM 状态它不会缓存接口数据。组件恢复到缓存状态时如果依赖的数据变成了旧数据还是得你自己用activated去刷新。这个“缓存了状态但没缓存数据”的区别是面试里很容易深挖的点。4.3 React 没有 keep-alive我们是怎么做的React 官方生态里没有直接叫 keep-alive 的组件但业务需求是存在的管理后台的表格页填了一堆筛选条件切到别的页面再回来条件全部还原非常折磨人。我常用的方案有三个第一种是在父组件里用状态提升。把筛选条件和列表数据放到一个容器组件里子页面切换时不卸载容器只切换内容区域这样状态天然还在。这个方案最干净也最容易维护但对组件结构有要求。第二种是保留 DOM 隐藏而不是销毁。页面切换时用display: none隐藏上一次页面而不是条件渲染销毁它。这个做法能保留 DOM 状态但内存占用高而且每个隐藏页面还残留在 DOM 树里对页面性能和事件绑定都有影响。我只在组件简单、数量少的时候用。第三种是借助第三方库比如react-activation它模拟了类似 Vue 的行为通过一个KeepAlive组件包裹页面实现按需缓存和回收。这类库核心思路还是维护一个缓存池配合生命周期回调来恢复状态。如果你在面试中回答“React 没有 keep-alive”我建议补一句“但我们有几种替代方案分别适用不同场景”。这样比背一个标准答案更能体现经验也能把话题引向自己擅长的部分。5. 一个线上案例CLOSE_WAIT 堆积把服务拖垮5.1 现象连接数爆了CPU 却不高前几年我处理过一个比较典型的故障。一个内部 API 服务平时连接数稳定在几百某天突然涨到一万多。从监控面板看CPU、内存都不高但请求延迟明显上升而且时不时出现“Connection reset by peer”。用ss -tan一看大量连接停在CLOSE_WAIT状态。这个状态的意思是对端已经发来 FIN主动要求关闭连接但本地应用没有调用 close 来关闭 socket。换句话说服务端自己侧没有关连接导致半开连接不断堆积最终把文件描述符耗尽了。当时的直觉是“连接没被正确回收”。进一步查代码发现服务端用了一个比较老的 HTTP 客户端库它默认会在收到响应后把连接放回连接池但如果服务端返回的响应头里带着Connection: close这个库的处理逻辑里有一个分支没有正确释放底层的 socket。5.2 定位谁开着连接不走排查过程分了几步第一步确认CLOSE_WAIT的对端地址和端口判断是来自网关还是上游服务。如果来自网关说明是网关主动断开的问题大概率在本服务的连接管理。第二步检查应用日志里有没有“连接被关闭但资源未释放”之类的警告以及抓 TCP 包看 FIN 的发送方。我们很快确认FIN 是从网关侧发出的但本服务进程没有及时回 FIN于是连接停在 CLOSE_WAIT。第三步对照 HTTP keep-alive 配置。网关那边的空闲超时设置比较短比如 30 秒。我们的服务却把keepalive_timeout设成了 120 秒。结果网关认为这条连接空闲太久了主动断开而我们的服务还在等这个连接上的下一个请求没意识到连接已经结束。从根因看这是典型的“各层超时不一致”问题。HTTP 空闲超时设置成 120 秒的初衷是减少握手次数但没有考虑到中间网关的 30 秒限制。于是空闲连接会在 30 秒时被网关先“杀死”应用层却一直等。5.3 修复调整 keep-alive 超时和连接池修复动作并不复杂但很能说明问题。第一把服务的 HTTPkeepalive_timeout从 120 秒调到 60 秒以内比网关的 30 秒长一点但不至于让中间层断开后还要等很久才感知。这里需要留一点时间余量避免客户端在超时临界点重用连接。第二在 HTTP 客户端连接池上做健康检查凡是池里存留的连接在取出使用前先判断是否已经失效。很多语言的标准库和流行库都提供了evict或validate机制目的就是防止用到“假活”的连接。第三调整 TCP 层参数把tcp_keepalive_time从默认的 7200 秒缩短到 600 秒让内核能更早发现假连接配合应用层一起做回收。这个参数调完观察了一周没有再出现 CLOSE_WAIT 堆积。这类故障的根因其实在八股文里就有答案HTTP keep-alive 负责复用连接TCP KeepAlive 负责兜底但超时设置不一致会导致连接被某一方提前关闭。如果不理解这两层机制排障时很容易在业务代码里翻来覆去找 bug。6. 保留自查常见误区与速查清单6.1 三个典型的错误认知在带人和面试过程中我总结出了三个反复出现的错误认知分享出来你可以对照自己有没有中招。第一个误区“keep-alive 是保持连接永远不断。”实际上HTTP 层和 TCP 层都只有超时和探测机制没有“永远保活”这种说法。连接终归会关闭区别只是由谁来关、什么时候关。把预期设定为“不断”就是给故障埋雷。第二个误区“服务端设置了 keepalive_timeout客户端就会严格按这个时间断开。”服务器下发的Keep-Alive头本质上是一个超时建议客户端可以执行也可以忽略。很多客户端库使用自己的默认超时因此你在服务端调参时真正的服务端主动断开时间要以实际抓包为准不能只看响应头。第三个误区“TCP KeepAlive 能保证应用层及时发现故障。”TCP KeepAlive 的探测周期在默认配置下可能长达 2 小时加上探测重试感知故障的时间非常慢。对于实时性要求高的业务必须靠应用层心跳或连接池健康检查不能把宝押在内核参数上。6.2 配置速查表与排查清单下面这张表是我平时排查连接问题时习惯对照的速查表包含三层 keep-alive 的关键维度层级核心标识作用默认行为常用调优点HTTP 层Connection: keep-alive/close是否复用 TCP 连接HTTP/1.1 默认复用keepalive_timeout、keepalive_requestsTCP 层内核参数tcp_keepalive_*探测死连接默认关闭或 7200 秒后开始time、intvl、probes前端组件Vuekeep-alive缓存组件状态不包裹则不缓存include、exclude、max遇到线上连接异常时我习惯按下面的顺序排查先看ss -tan或netstat -an统计当前连接状态。如果TIME_WAIT多通常是主动关闭方在反复断开要检查空闲超时或连接池回收策略。如果CLOSE_WAIT多说明本地服务没有正确关 socket重点查应用层资源释放逻辑。如果ESTABLISHED多但请求量不大要怀疑连接池是否过度预建或者 TCP KeepAlive 没开假连接无法被清理。然后再对比各层的超时时间。整个链路中任何一个环节的超时设置不一致都会造成连接先被某一方关闭。调参时不要单独看服务端要把 Nginx、网关、负载均衡、客户端连接池都拉出来对齐。最后再看业务代码。HTTP 客户端库如果缓存了失效连接往往表现为“间歇性 Connection reset”。这时候检查连接池的验证机制确保每次拿出的连接真正可用。这几个步骤靠的是对 keep-alive 机制本身的理解而不是背下来的哪一条命令。命令会变工具会换但“连接是谁建立、谁维护、谁关闭”这个思路不会变。最后再说一个我个人常年养成的习惯每改一次 keep-alive 相关配置我都会在灰度环境观察至少一个完整生命周期用ss -tan记录连接状态曲线而不是只盯着监控面板的平均值。因为连接问题往往是间歇性的平均值看起来正常实际已经积累了足够多的异常连接。等看到曲线突然抬头的时候通常已经晚了。保持警惕、分层对齐、及时清理这三点比背下任何一条面试答案都管用。