踩坑记:一次Nacos客户端偶发connect timed out全链路排查

image

在某天晚上发完版后一些服务连接nacos偶尔打以下异常日志:

nacos [com.alibaba.nacos.client.naming.updater] ERROR com.alibaba.nacos.client.naming -request 192.168.0.11:8848 failed. 
com.alibaba.nacos.api.exception.NacosException: failed to req API:http://192.168.0.1:8848/nacos/v1/ns/instance/list. code:500 msg: 
java.net.SocketTimeoutException: connect timed out at com.alibaba.nacos.client.naming.net.NamingProxy.callServer(NamingProxy.java:340) 
at com.alibaba.nacos.client.naming.net.NamingProxy.reqAPI(NamingProxy.java:367) at 
com.alibaba.nacos.client.naming.net.NamingProxy.reqAPI(NamingProxy.java:304) at 
com.alibaba.nacos.client.naming.net.NamingProxy.queryList(NamingProxy.java:217) at 
com.alibaba.nacos.client.naming.core.HostReactor.updateServiceNow(HostReactor.java:273) at 
com.alibaba.nacos.client.naming.core.HostReactor$UpdateTask.run(HostReactor.java:318) at 
……

第一反应是赶紧看下 Nacos 集群状态,结果三个节点全都是正常,健康检查也正常。服务本身也没挂,功能照常能用,就是日志里隔三差五蹦出来这么一条。服务发现数据也有本地缓存。只是日志看着烦。

不是那种持续性故障,而是偶发的,过一会儿自己就好了。测试环境与压测环境均无这种异常,这种问题其实比那种一挂到底的还头疼,因为你很难复现,也不好说到底是网络抖了一下还是真有问题。

冷静下来先把堆栈仔细看了一遍:

at com.alibaba.nacos.client.naming.net.NamingProxy.callServer(NamingProxy.java:340)
at com.alibaba.nacos.client.naming.net.NamingProxy.reqAPI(NamingProxy.java:367)
at com.alibaba.nacos.client.naming.net.NamingProxy.reqAPI(NamingProxy.java:304)
at com.alibaba.nacos.client.naming.net.NamingProxy.queryList(NamingProxy.java:217)
at com.alibaba.nacos.client.naming.core.HostReactor.updateServiceNow(HostReactor.java:273)
at com.alibaba.nacos.client.naming.core.HostReactor$UpdateTask.run(HostReactor.java:318)

关键信息在 HostReactor$UpdateTask——这是 Nacos Client 内部的定时任务,每隔几秒从 Nacos Server 拉一次服务实例列表。它调 /nacos/v1/ns/instance/list 接口的时候,跟 192.168.0.1 建立 TCP 连接超时了。

注意是 connect timed out,不是 read timed out。也就是说连接都没建起来,不是请求发出去等响应等太久。

查客户端配置。我们项目用的是 spring-cloud-alibaba 0.9.0.RELEASE,对应的 Nacos Client 大约是 1.0.x 到 1.1.1 之间。这个版本相当老了。

翻了一下 bootstrap.yml 里生产环境的配置:

discovery:
    namespace:
    server-addr: 192.168.0.1:8848,192.168.0.2:8848,192.168.0.3:8848
    metadata:
        version: ${project.version}
        description: ${project.description}

很干净,就配了地址和元数据,没有任何超时、重试、缓存相关的参数。

踩了个坑:fail-fast 不存在

一开始想着加个 fail-fast: false,让客户端请求一个节点失败后自动切到下一个。结果翻源码发现——这个属性在 0.9.0.RELEASE 版本里压根就没有

fail-fast 是 Spring Cloud Alibaba 2.2.6 版本才加进去的,我们用的 0.9.0 差了好几个大版本。想当然地照搬网上的方案是会踩坑的。

翻源码找超时参数。既然 Spring 层面没得配,那就往下钻,看 Nacos Client 自己的代码。

NamingHttpClientManager 里找到了这两个常量:

private static final int READ_TIME_OUT_MILLIS = Integer.getInteger(
    "com.alibaba.nacos.client.naming.rtimeout", 50000);

private static final int CON_TIME_OUT_MILLIS = Integer.getInteger(
    "com.alibaba.nacos.client.naming.ctimeout", 3000);

ctimeout——连接超时,默认 3000 毫秒。而且它是通过 Integer.getInteger() 读的,这是个 JVM 系统属性,不是 Spring 配置项,只能在启动参数里用 -D 来设

3 秒,在网络稳定时绰绰有余。但生产环境的网络哪有永远稳定的?稍微抖一下,或者 Nacos Server 那边 GC 卡一下,3 秒就没了。

这就解释了为什么是"偶发"的——大多数时候 3 秒够用,偶尔不够用就报一次。

再看 reqAPI 的重试逻辑。顺着 NamingProxy.reqApi 看了一遍,1.1.1 版本里多节点的请求逻辑是这样的:

// 多节点时,随机选一个起点,遍历所有节点
Random random = new Random(System.currentTimeMillis());
int index = random.nextInt(servers.size());
for (int i = 0; i < servers.size(); i++) {
    String server = servers.get(index);
    try {
        return callServer(api, params, body, server, method);
    } catch (NacosException e) {
        exception = e;
    }
    index = (index + 1) % servers.size();
}

代码上看,3 个节点是会遍历的,一个不行换下一个。但从异常堆栈来看,实际只打了 192.168.0.1 一个地址就抛异常了。这说明走的是另一个分支——nacosDomain 单节点模式,只对同一个节点重试 maxRetry 次,不会切到其他节点。

这大概是老版本客户端的一个坑,多地址配置在某些条件下没有被正确解析成 server list。

再看看nacos服务端

客户端的问题搞清楚了,再看看 Nacos Server 端有没有什么能优化的。

我们的 Nacos Server 是 1.4.2 版本,还是 1.x 系列,走 HTTP 协议,内嵌 Tomcat。

几个值得关注的点:

GC 方面,1.4.2 的启动脚本默认用的是 CMS GC,堆内存给的 512m。CMS 在 Full GC 时会有 STW(Stop-The-World),整个 JVM 停顿,期间 Tomcat 根本 accept 不了新连接。如果恰好赶上客户端的 UpdateTask 来拉数据,3 秒超时很容易打满。

TCP 队列方面,Linux 的 somaxconn 默认值是 128。什么意思呢?Nacos Server 的 Tomcat 就算线程池够大,操作系统层面的全连接队列就只能放 128 个等待中的连接。高并发或者 GC 停顿期间积压的连接超过这个数,新来的连接就被静默丢弃了,客户端表现就是 connect timed out。

Tomcat 线程池,默认 200 个线程。注册的微服务多了,每个服务又有心跳、拉列表等操作,线程可能不够用。

说这么多怎么解决这个日志呢

综合客户端和服务端两头,定下来这么几个动作:

客户端( 启动参数)

-Dcom.alibaba.nacos.client.naming.ctimeout=5000
或配置在配置文件中,不过需要进一步验证

把连接超时从 3 秒提到 5 秒。简单粗暴,但直接对症。注意:不过要对应每个nacos client的版本是不是有同样的参数。

bootstrap.yml 配置

discovery:
    namingLoadCacheAtStart: "true"

启动时先加载本地磁盘缓存,至少保证冷启动阶段不会因为 Server 短暂不可达而完全拿不到服务列表。

降低日志噪音

如果不想改超时时间,可以降低这个 logger 的级别,把 ERROR 改成 WARN:

<!-- ... existing code ... -->
    <logger name="com.alibaba.nacos.client.naming" level="WARN" />
<!-- ... existing code ... -->

这样偶发的超时错误不会再刷屏。

不过长期看来还是升级客户端版本,0.9.0.RELEASE 是 Spring Cloud Alibaba 的孵化版,存在不少已知问题。建议升级到更稳定的版本:

Nacos Server 端(三台机器都要改)

startup.sh 里把 CMS 换成 G1 GC,堆内存加到 2G,开 GC 日志:

JAVA_OPT="${JAVA_OPT} -Xms2g -Xmx2g -Xmn1g"
JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC"
JAVA_OPT="${JAVA_OPT} -XX:MaxGCPauseMillis=200"

当然还有一个教训:不要想当然地照搬网上的解决方案。像 fail-fast 这个参数,如果不翻源码确认版本就往上配,配了也不生效,还浪费时间。老版本有老版本的玩法,先搞清楚自己用的是什么版本、有哪些能力,再决定怎么调,才能少走弯路。

posted @ 2026-09-22 07:59  一个码农的日常  阅读(0)  评论(0)    收藏  举报