迁移后 Connection Refused:被遗漏的 localhost 依赖
前一篇写了 MySQL + Redis 的持续同步方案,实际迁移中遇到一个意料之外的故障:告警服务迁走后,另一个还在老集群运行的服务报 Connection refused。
排查半小时,根因一句话:代码里有一个 HTTP client 直接调 localhost:8097,但我们只检查了 Nginx 和 yml 配置,没搜代码。
故障回放
迁移步骤中,告警管理服务(alert-manager)是第一批迁走的。之前它和其他服务运行在同一台机器上,提供 localhost:8097 的 HTTP 接口。
迁走当天,另一个服务(trade-executor)开始疯狂报错:
ERROR: Connection refused: localhost:8097
第一反应:Nginx 反代配置没改?查了,改了。
第二反应:application.yml 里的 URL 没改?查了,也改了。
第三反应:搜代码。发现 DcAlertClient.java 里直接 hardcode 了 http://localhost:8097——不走 Nginx,不走配置中心,就是个本地直连。
为什么之前没问题
因为告警服务之前就在同一台机器上,localhost:8097 天然可达。运行时拓扑是这样的:
同一台机器 (老集群):
trade-executor ──localhost:8097──► alert-manager
迁走后:
新集群: alert-manager :8097
老集群: trade-executor ──localhost:8097──► ❌ Connection refused
中间跨了机房,localhost 不再指向同一个东西了。
修复
两步修复:
临时止损:把 localhost:8097 改为 localhost:18097,走之前建好的 SSH 反向隧道:
老集群 trade-executor ──localhost:18097──► 隧道 ──► 新集群 alert-manager:8097
长期方案:添加 application-conoha.yml 覆盖,等全部服务迁到新集群后,改回 localhost:8097 同机直连。
漏了什么
迁移检查清单里有三层依赖要查:
| 层级 | 检查方式 | 这次查了吗 |
|---|---|---|
| Nginx 反代 | grep nginx 配置 | ✅ 查了 |
| yml 配置 | rg yml 里的 URL | ✅ 查了 |
| 代码 HTTP client | rg Java/Go/Python 代码中的 URL | ❌ 漏了 |
第三层是最容易被忽略的。硬编码的 localhost 调用在微服务同机部署时完全合理——因为就是本机调用。但一旦服务拆分到不同机器,这些隐藏依赖就会爆炸。
元结构映射:广告系统的索引依赖
这和广告系统的一个经典事故一模一样:
某广告系统把正排索引拆到了独立服务(之前是进程内加载),迁移结束后发现召回模块疯狂超时。排查发现召回模块里有一段历史代码,直接通过本地文件路径读正排索引——没有走正排 RPC 服务。当正排数据不再落地到本地磁盘后,那段代码读到的是 3 天前的旧文件。
教训相同:迁移服务前,不是检查”你记着的依赖”,而是搜出”所有依赖”。 你不知道的事情才是真正会炸的。
迁移依赖检查清单
基于这次教训,补充一条检查项:
# 搜代码中所有 localhost / 127.0.0.1 引用
rg -n 'localhost|127\.0\.0\.1' --type java --type py --type go
# 搜代码中所有 HTTP URL(不只是 yml)
rg -n 'https?://' --type java --type py --type go | grep -v '^.*\.yml'
两个命令的区别:第一个搜所有 localhost(包括非 HTTP 的,比如 DB 连接),第二个搜所有 HTTP URL(包括远程域名)。两个都要跑。
教训
迁移检查清单的正确顺序:Nginx → yml → 代码。漏掉哪一层,哪一层就会在凌晨三点把你叫醒。
一句话:服务迁走后 Connection Refused 不是网络问题,是依赖没搜干净——local 调用的另一头已经不是 local 了。