Caddy内存占用过高,连续流内存释放异常解决方案与原因分析

本篇文章对Caddy在grpc长连接的场景下, 内存占用过高, 内存释放异常解决方案与原因分析。

问题描述

Caddy服务端内存占用过高, 最初怀疑是内存泄漏所致, 但经过排查发现是由于长连接所对应的内存无法释放导致的。

出现的场景

在使用gRPC长连接传输大量数据时, 此问题尤为显著。XHTTP-stream-up 是较易复现此问题的场景。

  • 配置了flush_interval -1 (低延迟模式流式传输)
  • 以gRPC长连接传输 (请求包含gRPC请求头, 但实际传输内容非gRPC也受影响)
  • 长连接传输 (gRPC基本确认受影响, HTTP/2连接是否被影响尚不明确)

pprof分析

使用go tool pprof命令分析Caddy服务端的内存占用情况。我们可以得到如下信息:

(pprof) top
12959.55kB 52.23% 52.23% 12959.55kB 52.23%  golang.org/x/net/http2.(*clientStream).writeRequestBody

可以看到,golang.org/x/net/http2.(*clientStream).writeRequestBody占用了12959.55kB的内存(后续复现, 实际情况下占用更高)。

复现

commit 238f110 之前的版本中, 可能存在此问题。

解决方案

使用 commit 238f110 提交后的最新源码进行编译, 即可解决此问题。 caddy目前的最新正式版v2.8.4v2.9.0-beta.3均在此提交之前, 均存在此问题。

问题详细分析

以下均为我们对此问题的猜测分析, 不代表caddy官方表态, 仅供参考。

补丁提交信息

commit 238f110

reverseproxy:撤销 #4952 - 不要在流模式中忽略上下文取消 即:撤销提交 f5dce84

两年前, #4952 中的补丁似乎是修复一个问题(某种边缘情况)的必要方法,但它破坏了其他更常见的用例(见 #6666 )。

现在,根据 #6669 ,似乎原始问题无法再被复制,因此我们正在撤销该补丁,因为它本身就是不正确的。

如果原始问题再次出现,更合适的补丁可能在 #6669 中(即使作为未来修复的基线)。一个潜在的未来修复可以是一个可选择的设置。

pull request #4952

如果 FlushInterval 被显式配置为 -1,我们假设用户意图进行低延迟流式传输,并且在这些情况下,客户端有时会在请求与后端的往返完成之前断开连接。

这个补丁在这些情况下忽略客户端断开,因此请求仍将完成而不会出错。

尽管 flushInterval() 是动态计算的,但目前我要求显式配置为 -1,因为我还不完全了解这可能带来的影响。可能没问题,但如果出现问题,我希望它们是可选择的。

我们知道这个解决方案至少是有效的。

issues #6666

在 #4922 中,我们收到请求,希望在客户端在响应之前断开连接时保持与后端的连接开放(而不是立即关闭上游连接),以便后端完成接收客户端发送的数据,当客户端不关心响应时。

这在 #4952 中通过简单地忽略在 flush_interval 为 -1 时连接上游的上下文取消来实现。

显然,事后看来,这种做法是有缺陷的。它不仅以不正确和意外的方式过载了 flush_interval,还留下了未释放的资源。

我们收到赞助商报告,gRPC 连接在客户端断开后未关闭,正是由于这个补丁。(仍在验证中,但似乎很可能。)

在 8c9e87d(非主干分支)中,我将 flush_interval 与一个新选项 ignore_client_gone 分开,该选项显式启用保持与后端连接开放的行为。

然而,正确的修复方法是有一个选项,而不是仅仅让与后端的连接保持开放直到后端决定完成,而是在后端接收完客户端发送的所有数据后关闭与后端的连接。如果我们将更改合并到主干,我更希望这是一个正确的修复。

pull request #6669

修复:低延迟客户端流式传输在客户端关闭后等待数据以关闭连接 #6669

问题原因

  • 2022年8月13日, #4952 被合并,该补丁处理低延迟流式传输时客户端断开连接的问题,确保请求在客户端断开时仍能完成,并要求显式配置 FlushInterval 为 -1 以避免潜在问题。 (但实际上,这导致了更多问题)
  • 2024年10月29日, #6666 被提出, caddy 开始做出响应
  • 2024年11月1日, #6669 被提出, 但未被合并, 留作备选方案
  • 2024年11月13日. commit 238f110 被合并, 解决了此问题, 并撤销了 #4952 这个错误的补丁。

总结

这个问题源于一个目前看来错误的补丁, 这个补丁所解决的问题也已无法复现, 最初的问题或许是相关库和其他部分所导致的, 但目前来看, 这个补丁对grpc长连接的影响可能是致命的。由于错误的补丁,导致内存无法释放, 导致服务端内存占用过高, 最终导致服务端无法正常提供服务。

我们使用最新主线源码进行编译后, 发现此问题被解决, 故写下此文, 希望对大家有所帮助。Caddy目前尚未发布已解决此问题的release, 我们自行编译了caddy以供使用 caddy v24.12.20