欢迎光临
我们一直在努力

一个Listener泄漏干掉了32G内存:Nacos配置管理你不该碰的默认值

一个Listener泄漏干掉了32G内存:Nacos配置管理你不该碰的默认值


32G 内存的机器,Nacos 客户端吃了 18G

周一早上。巡检脚本报了一条我以为是误报的数字。

那台跑着订单服务的机器,物理内存 32G,Nacos 客户端的 Java 进程占了 18G。不是 Nacos 服务端——是客户端。一个本该用 200MB 的 SDK。

jmap -histo:live 拉出来一看,堆里躺了 24,731 个 ConfigService 实例。每个实例内部挂着一个独立的 HTTP 连接池、一个线程池、一个本地缓存。24,731 个。

代码里是这么写的:

@Bean
public void initConfigs() {
// 每次配置变更,创建一个新的 ConfigService
// 没关过旧的
configs.forEach(config -> {
ConfigService cs = NacosFactory.createConfigService(properties);
cs.addListener(config.getDataId(), config.getGroup(), new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
refreshConfig(configInfo);
}
});
});
}

addListener 方法内部每调用一次就注册一个新 Listener,没人去 removeListener。配置变更越频繁,堆里的 Listener 越多。半年跑了 2 万多个,内存一路涨到 18G。

这就是 Nacos 配置管理最常见的三个慢性病:监听器泄漏、长轮询堆积、缓存策略缺失。它们不致命——不会立即炸出堆栈——但会让你的系统像一只慢慢漏气的轮胎,直到某个深夜彻底瘪掉。


监听器:注册了就得注销

Nacos 的配置监听有两种写法。第一种是 Spring Cloud Alibaba 的注解式,第二种是 SDK 的原生 API。

注解式不会泄漏。Spring 容器帮你管生命周期,Bean 销毁时自动注销 Listener。

// 写法一:Spring 托管,不会泄漏
@NacosConfigListener(dataId = "order-service-prod.yml")
public void onConfigChange(String config) {
// Bean 销毁时 Spring 自动 removeListener
}

原生 API 泄漏,是因为你忘了关。

// 写法二:原生 SDK,容易泄漏
ConfigService configService = NacosFactory.createConfigService(properties);
configService.addListener("order-service-prod.yml", "ORDER_GROUP", listener);
// … 业务代码 …
// 忘了调 configService.removeListener() 和 configService.shutDown()

Nacos SDK 的 ConfigService 内部架构长这样:

#mermaid-svg-YG1i7WpcrcF5wnAw{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YG1i7WpcrcF5wnAw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YG1i7WpcrcF5wnAw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YG1i7WpcrcF5wnAw .error-icon{fill:#552222;}#mermaid-svg-YG1i7WpcrcF5wnAw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YG1i7WpcrcF5wnAw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YG1i7WpcrcF5wnAw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YG1i7WpcrcF5wnAw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YG1i7WpcrcF5wnAw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YG1i7WpcrcF5wnAw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YG1i7WpcrcF5wnAw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YG1i7WpcrcF5wnAw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YG1i7WpcrcF5wnAw .marker.cross{stroke:#333333;}#mermaid-svg-YG1i7WpcrcF5wnAw svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YG1i7WpcrcF5wnAw p{margin:0;}#mermaid-svg-YG1i7WpcrcF5wnAw .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-YG1i7WpcrcF5wnAw .cluster-label text{fill:#333;}#mermaid-svg-YG1i7WpcrcF5wnAw .cluster-label span{color:#333;}#mermaid-svg-YG1i7WpcrcF5wnAw .cluster-label span p{background-color:transparent;}#mermaid-svg-YG1i7WpcrcF5wnAw .label text,#mermaid-svg-YG1i7WpcrcF5wnAw span{fill:#333;color:#333;}#mermaid-svg-YG1i7WpcrcF5wnAw .node rect,#mermaid-svg-YG1i7WpcrcF5wnAw .node circle,#mermaid-svg-YG1i7WpcrcF5wnAw .node ellipse,#mermaid-svg-YG1i7WpcrcF5wnAw .node polygon,#mermaid-svg-YG1i7WpcrcF5wnAw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YG1i7WpcrcF5wnAw .rough-node .label text,#mermaid-svg-YG1i7WpcrcF5wnAw .node .label text,#mermaid-svg-YG1i7WpcrcF5wnAw .image-shape .label,#mermaid-svg-YG1i7WpcrcF5wnAw .icon-shape .label{text-anchor:middle;}#mermaid-svg-YG1i7WpcrcF5wnAw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YG1i7WpcrcF5wnAw .rough-node .label,#mermaid-svg-YG1i7WpcrcF5wnAw .node .label,#mermaid-svg-YG1i7WpcrcF5wnAw .image-shape .label,#mermaid-svg-YG1i7WpcrcF5wnAw .icon-shape .label{text-align:center;}#mermaid-svg-YG1i7WpcrcF5wnAw .node.clickable{cursor:pointer;}#mermaid-svg-YG1i7WpcrcF5wnAw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YG1i7WpcrcF5wnAw .arrowheadPath{fill:#333333;}#mermaid-svg-YG1i7WpcrcF5wnAw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YG1i7WpcrcF5wnAw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YG1i7WpcrcF5wnAw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YG1i7WpcrcF5wnAw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YG1i7WpcrcF5wnAw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YG1i7WpcrcF5wnAw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YG1i7WpcrcF5wnAw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YG1i7WpcrcF5wnAw .cluster text{fill:#333;}#mermaid-svg-YG1i7WpcrcF5wnAw .cluster span{color:#333;}#mermaid-svg-YG1i7WpcrcF5wnAw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YG1i7WpcrcF5wnAw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YG1i7WpcrcF5wnAw rect.text{fill:none;stroke-width:0;}#mermaid-svg-YG1i7WpcrcF5wnAw .icon-shape,#mermaid-svg-YG1i7WpcrcF5wnAw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YG1i7WpcrcF5wnAw .icon-shape p,#mermaid-svg-YG1i7WpcrcF5wnAw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YG1i7WpcrcF5wnAw .icon-shape .label rect,#mermaid-svg-YG1i7WpcrcF5wnAw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YG1i7WpcrcF5wnAw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YG1i7WpcrcF5wnAw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YG1i7WpcrcF5wnAw :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

ConfigService

ClientWorker线程池

CacheMap本地缓存

HttpAgentHTTP 连接池

Listener1

Listener2

Listener3… 2万个

长轮询线程1

长轮询线程2

长轮询线程3

一个 ConfigService 实例 = 一个线程池 + 一个连接池 + 一个本地缓存。2 万个实例 = 2 万个线程池 + 2 万个连接池。

怎么检查有没有泄漏

# 第一步:看有多少 ConfigService 实例
jmap -histo:live <pid> | grep ConfigService

# 返回示例(如果数字很大,就是泄漏了):
# 24731 …ConfigService → 24731 个实例
# 24731 …ClientWorker → 每个实例带一个线程池

# 第二步:看内存占比
jmap -heap <pid> | grep -A 5 "Heap Usage"

怎么修

@Component
public class ConfigManager implements DisposableBean {

private final List<ConfigService> services = new ArrayList<>();

public void addConfig(String dataId, String group, Listener listener) {
try {
Properties props = new Properties();
props.put("serverAddr", "127.0.0.1:8848");
ConfigService cs = NacosFactory.createConfigService(props);
cs.addListener(dataId, group, listener);
services.add(cs);
} catch (NacosException e) {
log.error("注册配置监听失败: {}", dataId, e);
}
}

@Override
public void destroy() {
for (ConfigService cs : services) {
try {
cs.shutDown(); // 关键:注销所有 Listener,关闭连接池
} catch (NacosException e) {
log.warn("关闭 ConfigService 失败", e);
}
}
services.clear();
}
}

一个 shutDown() 就够了——它会注销这个实例下的所有 Listener,释放线程池和连接池。


长轮询:Hold 得越久越好吗

Nacos 1.x 的配置推送走 HTTP 长轮询。客户端发一个 GET 请求,服务端 Hold 住,有变更才返回。没有变更就一直 Hold 到超时,超时后客户端再发一次。

Nacos Server客户端Nacos Server客户端#mermaid-svg-aINYuiSPZ045YlxS{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aINYuiSPZ045YlxS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aINYuiSPZ045YlxS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aINYuiSPZ045YlxS .error-icon{fill:#552222;}#mermaid-svg-aINYuiSPZ045YlxS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aINYuiSPZ045YlxS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aINYuiSPZ045YlxS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aINYuiSPZ045YlxS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aINYuiSPZ045YlxS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aINYuiSPZ045YlxS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aINYuiSPZ045YlxS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aINYuiSPZ045YlxS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aINYuiSPZ045YlxS .marker.cross{stroke:#333333;}#mermaid-svg-aINYuiSPZ045YlxS svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aINYuiSPZ045YlxS p{margin:0;}#mermaid-svg-aINYuiSPZ045YlxS .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-aINYuiSPZ045YlxS text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-aINYuiSPZ045YlxS .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-aINYuiSPZ045YlxS .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-aINYuiSPZ045YlxS .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-aINYuiSPZ045YlxS .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-aINYuiSPZ045YlxS #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-aINYuiSPZ045YlxS .sequenceNumber{fill:white;}#mermaid-svg-aINYuiSPZ045YlxS #sequencenumber{fill:#333;}#mermaid-svg-aINYuiSPZ045YlxS #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-aINYuiSPZ045YlxS .messageText{fill:#333;stroke:none;}#mermaid-svg-aINYuiSPZ045YlxS .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-aINYuiSPZ045YlxS .labelText,#mermaid-svg-aINYuiSPZ045YlxS .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-aINYuiSPZ045YlxS .loopText,#mermaid-svg-aINYuiSPZ045YlxS .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-aINYuiSPZ045YlxS .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-aINYuiSPZ045YlxS .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-aINYuiSPZ045YlxS .noteText,#mermaid-svg-aINYuiSPZ045YlxS .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-aINYuiSPZ045YlxS .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-aINYuiSPZ045YlxS .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-aINYuiSPZ045YlxS .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-aINYuiSPZ045YlxS .actorPopupMenu{position:absolute;}#mermaid-svg-aINYuiSPZ045YlxS .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-aINYuiSPZ045YlxS .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-aINYuiSPZ045YlxS .actor-man circle,#mermaid-svg-aINYuiSPZ045YlxS line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-aINYuiSPZ045YlxS :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}alt[30秒内有变更][30秒内无变更]GET /nacos/v1/cs/configs/listener(长轮询,请求头: Long-Pulling-Timeout=30000)立即返回: "有变更,dataId=order-service.yml"拉取新配置新配置内容超时返回: "无变更"等待 1 秒发起下一次长轮询

默认超时是 30 秒。这个数字影响三件事:

超时时间配置变更延迟服务端连接数适用场景
10 秒 最多 10 秒 高(每 10 秒重建连接) 配置频繁变动的核心业务
30 秒(默认) 最多 30 秒 常规场景
60 秒 最多 60 秒 配置很少变的晚间任务

改超时时间不会改变配置变更的感知延迟——因为变更是 Push 的,不是等到超时才通知。 超时只影响"没有变更时多久重建一次连接"。改短了无意义地增加连接开销,改长了没有实际收益。

# Nacos 服务端(1.x 或兼容模式)
nacos.longpolling.timeout=30000

# 客户端侧(bootstrap.yml)
spring:
cloud:
nacos:
config:
timeout: 30000 # 长轮询超时,默认 30000ms

除非你的服务端 CPU 非常紧张、连接数是个瓶颈,否则默认 30 秒别动。


缓存:Nacos 服务端崩了你还剩什么

Nacos 客户端在本地文件系统存了两样东西:

  • 配置内容:$HOME/nacos/config/{namespace}/{group}/{dataId}
  • 上一次拉到的快照:$HOME/nacos/config/snapshot-tenant/{namespace}/{group}/{dataId}
  • # 默认缓存目录
    ls ~/nacos/config/

    # 结构:
    # nacos/config/
    # ├── fixed-localhost_8848_nacos/
    # │ └── snapshot-tenant/
    # │ └── public/
    # │ └── ORDER_GROUP/
    # │ └── order-service-prod.yml

    启动时如果连不上 Nacos,客户端会读这个本地缓存。

    spring:
    cloud:
    nacos:
    config:
    # 修改缓存目录
    file-extension: yml
    namespace: prod
    group: ORDER_GROUP
    # 本地缓存策略
    naming-load-cache-at-start: true # 启动时加载本地缓存

    但这里有个容易忽略的点:如果服务端正常但配置被删了,本地缓存不会自动失效。 客户端以为自己拿到的是最新配置,其实是上一次成功拉取的历史版本。

    两种场景对比:

    场景服务端状态客户端取的配置安全吗
    服务端正常 配置存在 从服务端拉取最新
    服务端正常 配置被误删 上次缓存的版本 ⚠️ 可能是错的
    服务端全挂 不可达 上次缓存的版本 ✅ 兜底有用
    服务端恢复 配置已回滚 等下一次长轮询刷新 ✅ 自动修复

    也就是说,缓存是兜底,不是永久缓存。服务端恢复后,下一次长轮询会自动拉回正确配置。


    配置太多:当你把 Nacos 当成文件服务器

    一家 SaaS 公司把每个租户的自定义配置都存进了一个 Data ID:

    # tenant-config.yml —— 一个文件 12MB
    tenant_001:
    features: { 1500 行 }
    whitelist: { 800 行 }
    tenant_002:
    features: { 1500 行 }
    whitelist: { 800 行 }
    # … 200 个租户

    每次随便改一个租户的白名单,200 个租户的客户端全部触发 receiveConfigInfo,重新解析这 12MB 的 YAML。

    两个问题:

  • 网络传输:12MB × 200 个客户端 = 2.4GB 带宽
  • YAML 解析:单个 12MB 的 YAML 解析耗时 200~400ms,期间阻塞配置刷新线程
  • 拆分的收益远大于"统一管理"的便利:

    # 拆成按租户粒度
    tenant001config.yml # 3KB
    tenant002config.yml # 3KB
    # … 200 个 Data ID

    每个客户端只订阅自己租户的 Data ID。改租户 001 的配置,只推送给租户 001,其他 199 个不受影响。

    拆分 vs 不拆分

    维度一个巨型文件按粒度拆 N 个文件
    配置变更影响面 全体客户端 只影响订阅了该文件的客户端
    网络带宽 N × 12MB 只有订阅者收到 ~3KB
    YAML 解析耗时 200~400ms <5ms
    代码可维护性 一个人改全公司提心吊胆 隔离,改自己的

    三个参数的最终建议

    别在配置文件里动太多参数。Nacos 配置管理的默认值经过了生产验证,大多数场景不需要改。

    参数默认值什么时候才需要改
    长轮询超时 30000ms 服务端连接数确实是瓶颈时
    客户端缓存 默认开启 确认了服务端全部宕机后客户端无法启动
    Data ID 粒度 没限制 单个文件超过 500KB 时考虑拆分

    真正需要你关注的不是调参,是这三件事:

  • 每一个 addListener 都有对应的 shutDown——或者交给 Spring 管。
  • 一个 Data ID 一个服务一个功能维度——别把 200 个租户塞进一个文件。
  • 升级到 Nacos 2.x——gRPC 双向流原生解决长轮询的超时问题和连接数问题。1.x 调再多参数也不如升级本身。

  • 你们遇到过一次配置变更影响所有服务的场景吗?评论区留个数字:1=改一个配置全公司抖三抖 2=拆分得好好的互不影响 3=还没上生产配置中心。顺便说一个你最想吐槽的 Nacos 配置管理问题。

    赞(0)
    未经允许不得转载:171主机测评 » 一个Listener泄漏干掉了32G内存:Nacos配置管理你不该碰的默认值
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址