服务启动即崩:InetAddress.getLocalHost() 抛 UnknownHostException 排查记

服务启动即崩:InetAddress.getLocalHost() 抛 UnknownHostException 排查记

摘要 :新环境机器上服务启动第一秒就崩,堆栈指向 InetAddress.getLocalHost(),报 UnknownHostException: 主机名: Name or service not known。代码一行没改,同样的包在别的机器跑得好好的。本文讲清这行代码的真实机制------它是"取主机名 + 解析主机名"两步,任何一步失败都会崩;给出三类高发场景、运维侧与代码侧的根治方案,最后补一个更隐蔽的姊妹坑:不抛错、但静默拿错 IP。

适合读者:在 Linux 服务器/容器上部署 Java 服务的开发和运维。这个报错的搜索量常年居高不下,而大部分答案只给一句"改 hosts",不讲为什么。


一、报错现场

服务初始化时拿本机 IP,塞进系统属性供日志格式、监控上报和服务注册使用------非常常见的写法:

java 复制代码
// 启动初始化代码(示意)
System.setProperty("ip", InetAddress.getLocalHost().getHostAddress());

新机器上启动即崩:

text 复制代码
java.net.UnknownHostException: game_review-02: game_review-02: Name or service not known
        at java.net.InetAddress.getLocalHost(InetAddress.java:1506)
        at com.example.server.ServerBootstrap.initConfig(ServerBootstrap.java:148)
        at com.example.server.ServerBootstrap.init(ServerBootstrap.java:59)
        at com.example.server.GameMain.main(GameMain.java:21)

特定机器必现,代码与其他机器完全相同。报错信息里有两个细节:主机名出现了两次 ,是异常被包了两层(内层查找失败的消息已含主机名,外层 getLocalHost() 捕获后又拼一次重抛);Name or service not known 是 glibc EAI_NONAME 的标准输出,意思是这个名字在所有解析来源里都找不到------JVM 只是转述了操作系统的判决。

启动即崩某种意义上是好事------后文会讲"不崩"的版本更危险。

二、getLocalHost() 到底做了什么

很多人以为它就是"读一下本机 IP",不会失败。实际是两步操作 (堆栈行号 1506 正对应 JDK 8 getLocalHost() 里的重抛代码块,不同 8u 小版本会浮动一两行):

text 复制代码
第一步  gethostname()   取内核维护的主机名(启动时从 /etc/hostname 加载)

第二步  解析这个名字     ← 本次事故的失败点
                        按 /etc/nsswitch.conf 的顺序(通常 files → dns):
                        查 /etc/hosts → 查不到问 DNS → 都没有 = UnknownHostException
                        成功的结果被 JVM 缓存 5 秒(见 §4.3)

关键认知:getLocalHost() 要求"本机主机名是可解析的",而操作系统只保证主机名存在,不保证它能被解析。 hostnamectl set-hostname 会更新内核主机名和 /etc/hostname,但不会 往 /etc/hosts 里写任何东西;DNS 上也未必有这台机器的记录。主机名与解析记录之间这条"没人担保的缝隙",就是本类事故的全部生存空间。

对照本案:主机名是部署时自定义的(game_review-02),改了名但没动 hosts,DNS 里更不会有它------第二步必然失败。

三、为什么解析会失败:三类高发场景

场景一:自定义主机名,但没有对应的解析记录(本案)

云上机器、测试/审核环境最常见:运维用 hostnamectl set-hostname xxx 起了个好认的名字,然后忘了 /etc/hosts。此后这台机器上所有 调用 getLocalHost() 的 Java 进程都活不了。同类还有:镜像克隆带来的模板主机名、改名后没同步 hosts。

顺带一提,本案主机名里有下划线 (..._review-02):内核层面合法,但不符合 RFC 952/1123。DNS 协议本身(RFC 2181)不限制标签字符集,_dmarc、_acme-challenge 遍地都是------但那些是 TXT 记录,不在校验范围;BIND 的 check-names 只管"按规范应当是主机名"的位置(A/AAAA/MX 的 owner name,以及 NS/SOA/MX/SRV 记录里的目标名),所以主机名想建 A 记录默认就会被拒。再加上云厂商校验器、TLS 证书与 SNI、Kerberos 的 host/FQDN 全都对不上,给机器起名请只用字母、数字和连字符。

场景二:容器环境(Docker / K8s)

  • 默认桥接网络 :Docker 会把"容器 IP + 容器主机名"写进容器的 /etc/hosts,一般不出事;
  • --network=host + --hostname=自定义名 :--hostname 只改容器的 UTS 主机名,而 Docker 生成的那份 hosts 文件是宿主机 /etc/hosts 的拷贝 (libnetwork 的 OptionOriginHostsPath("/etc/hosts")),里面没有这个新名字 → 必然查不到。因为是创建时的拷贝而非挂载,事后改宿主机 hosts 不会同步进已存在的容器,只能修好后重建;
  • 想用 --add-host "$(hostname):127.0.0.1" 补一条?注意副作用 :host 网络下一旦带了 --add-host,libnetwork 就不再拷贝宿主机 hosts,改走自己生成的路径------里面只剩 Docker 内置的几行 localhost 加你显式传的条目,宿主机 hosts 上的其他映射(内部服务、数据库)会在容器里静默消失;
  • K8s hostNetwork: true :kubelet 给这类 Pod 生成的 hosts 文件是节点 /etc/hosts 的拷贝 (首行带 # Kubernetes-managed hosts file (host network). 标记),而 Pod 的主机名是节点名 ------节点名不在节点自己的 hosts 里、集群 DNS 又没记录 → 同样炸。这里 hostAliases 是生效的(API 注释里那句 "only valid for non-hostNetwork pods" 本来就是错的,已从源码删掉),可以直接拿它补这条映射。

容器化改造后"原来好好的服务突然起不来",优先怀疑这一类。

场景三:镜像/环境裁剪过度

坑不在"镜像里的 /etc/hosts 被删了"------运行时会重新生成它,改镜像无效。真正的坑是解析链路缺件 :Alpine/musl 压根不读 /etc/nsswitch.conf(固定 files → dns),glibc 上验证过的方案搬过来行为可能不一致;distroless 连 getent/dig 都没有,现场难取证;CI 动态生成 hosts 漏了本机。

一个重要推论:不用这个 API 不代表安全

即便业务代码从不调 getLocalHost(),很多基础库会在内部调它:日志框架取主机名、注册中心客户端取 IP、监控 agent、Hadoop/Kafka 生态组件......所以这个报错经常出现在你从没写过的调用栈里。修机器(让主机名可解析)是治本,改自己代码只是治标。

四、修复:把主机名放回解析链里

4.1 应急修复:补一行 hosts(本案的解法)

text 复制代码
# /etc/hosts
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
127.0.0.1   localhost localhost.localdomain
10.0.24.15  game_review-02          ← 新增:本机IP + 主机名

映射到 127.0.0.1 还是真实网卡 IP?看用途:

用途 映射到 原因
只是让 getLocalHost() 不抛错、日志里显示 IP 127.0.0.1 最稳,不依赖网卡状态
取到的 IP 要注册给外部(注册中心、对端回连) 真实网卡 IP 别人要能连通这个地址

本案的 IP 只用于日志和监控标记,按上表选即可。注意同一主机名别在 hosts 里写多行 :glibc 返回全部匹配项,getLocalHost() 取第一个,而顺序由 RFC 6724 地址排序规则决定、不是你写的行序。

4.2 运维侧根治:让这条规则"必然存在"

手工修一台没问题,机器多了就是概率问题。把规则固化进镜像模板 / 云主机初始化脚本 / Ansible:

bash 复制代码
#!/bin/bash
# 幂等(需 root)。用法:ensure_hosts.sh <本机IP>
# 要注册给外部的 IP 必须传真实网卡地址;传 127.0.0.1 就是在亲手制造 §6 那个故障。
set -euo pipefail
hn="$(hostname)"
ip="${1:?必须显式传入要写入 /etc/hosts 的本机 IP}"

# 按字段精确匹配:主机名不当正则用,容忍行尾注释与 CRLF,跳过注释行,
# 也不会把 web1 误判成 web10 / web1.example.com
if awk -v hn="$hn" '{ sub(/\r$/, "") } /^[[:space:]]*#/ { next }
      { for (i = 2; i <= NF; i++) if ($i == hn) { found = 1; exit } }
      END { exit !found }' /etc/hosts
then exit 0; fi

printf '%s\t%s\n' "$ip" "$hn" >> /etc/hosts

配套两条规范:新机器命名只用字母数字连字符(见 §3 场景一);改主机名的流程里,"同步 /etc/hosts"作为必选项写进 SOP。

4.3 生效与缓存

改完没立刻生效,按顺序排这三层:

  1. glibc:改完立即生效,这层不用管;
  2. 缓存服务(最容易漏) :跑了 nscd / systemd-resolved 时,失败结果也会被缓存 ,有 nscd 的老 CentOS 上是必现而非"极端情况"------nscd -i hosts(或重启服务),systemd-resolved 用 resolvectl flush-caches;
  3. JVM :getLocalHost() 有一个它独有的 5 秒缓存 (从 JDK 8 GA 到 21 都是 5 秒),刻意绕过通用 DNS 缓存、失败也不进缓存。所以运行中的进程等 5 秒就能拿到新结果,不必重启;崩在启动阶段的,重启即生效。

别和通用 DNS 缓存(networkaddress.cache.ttl / negative.ttl)搞混,那套管的是 getByName() 和建连接。"hosts 明明改对了、进程还在报同一个错",卡住的多半是第 2 层。

五、代码侧防御:不要再裸调 getLocalHost()

"机器修好了"不等于"下次新机器不会再犯"。把"获取本机 IP"封装成防御性工具,三层递进:

java 复制代码
// import 省略:java.net.{Inet4Address,InetAddress,NetworkInterface,SocketException,UnknownHostException}
//             java.util.{Collections,Enumeration}  java.util.logging.{Level,Logger}
public final class LocalIpUtil {

    // 换成你项目自己的日志框架
    private static final Logger LOG = Logger.getLogger(LocalIpUtil.class.getName());
    private static final String FALLBACK = "127.0.0.1";

    public static String getLocalIp() {
        // 一、显式配置优先:IP 是部署信息,不是探测出来的
        String configured = System.getProperty("app.ip");
        if (configured != null && !configured.isEmpty()) return configured;

        // 二、常规路径,但只信"非回环、非通配"的结果
        try {
            InetAddress addr = InetAddress.getLocalHost();
            if (!addr.isLoopbackAddress() && !addr.isAnyLocalAddress()) return addr.getHostAddress();
            LOG.warning("getLocalHost() 返回回环/通配地址 " + addr.getHostAddress() + ",降级到网卡遍历");
        } catch (UnknownHostException | SecurityException e) {
            // 绝不向上抛------拿 IP 这件事不该有资格搞崩服务
            LOG.log(Level.WARNING, "getLocalHost() 失败,降级到网卡遍历", e);
        }

        // 三、遍历网卡兜底:先内网 IPv4,再放宽到任意可用 IPv4
        try {
            String ip = pickIpv4(true);
            if (ip == null) ip = pickIpv4(false);
            if (ip != null) return ip;
        } catch (SocketException e) {
            LOG.log(Level.WARNING, "遍历网卡失败", e);
        }

        LOG.severe("无法确定本机 IP,降级到 " + FALLBACK);
        return FALLBACK;
    }

    /** 取一个可用 IPv4;siteLocalOnly 时只认内网段(10/172.16-31/192.168) */
    private static String pickIpv4(boolean siteLocalOnly) throws SocketException {
        Enumeration<NetworkInterface> nis = NetworkInterface.getNetworkInterfaces();
        if (nis == null) return null;                 // 无网络栈时返回 null
        for (NetworkInterface ni : Collections.list(nis)) {
            if (!ni.isUp() || ni.isLoopback() || isVirtualByName(ni.getName())) continue;
            for (InetAddress addr : Collections.list(ni.getInetAddresses())) {
                if (!(addr instanceof Inet4Address)) continue;
                // 回环、通配、链路本地(169.254/16)三类无条件排除
                if (addr.isLoopbackAddress() || addr.isAnyLocalAddress()
                        || addr.isLinkLocalAddress()) continue;
                if (siteLocalOnly && !addr.isSiteLocalAddress()) continue;
                return addr.getHostAddress();
            }
        }
        return null;
    }

    /** 按名字挡虚拟网卡是打不完的地鼠,新增一种 CNI 就得补一条,只作辅助手段 */
    private static boolean isVirtualByName(String name) {
        return name.startsWith("docker") || name.startsWith("veth") || name.startsWith("br-")
            || name.startsWith("virbr")  || name.startsWith("cali") || name.startsWith("flannel")
            || name.equals("cni0");     // 按需再补 lxc/vnet/tap/tunl/kube-* ...
    }
}

三个设计要点:

  1. 显式配置永远优先。多网卡机器上"本机 IP"本身就是歧义问题(内网卡还是公网卡?),启发式探测只能兜底;迁移时记得把 §1 那行遗留代码的读取方一起改掉。
  2. 失败要降级,但不要静默。它是辅助信息,降级到回环地址比炸掉启动流程好得多;日志这一步别省,否则故障会在几小时后换个面目出现。
  3. 判断回环别用字符串比较 。"127.0.0.1".equals(ip) 挡不住 ::1,保留 InetAddress 对象调 isLoopbackAddress() 才能一次覆盖两种栈。

六、姊妹坑:不抛错,但静默拿错 IP

UnknownHostException 是这个 API 最"体面"的失败方式------启动即崩,一眼定位。更贵的是它成功返回、但返回了错误的 IP,而且不止一条路:

  • 选中虚拟网卡 :宿主机装了 Docker,出现 docker0 网桥(172.17.0.1)。hosts 里主机名恰好映射到它,或兜底逻辑没过滤虚拟网卡 → 服务拿 172.17.0.1 注册到 ZooKeeper/Nacos/Eureka,其他服务按这个地址回连全部失败。服务"看起来启动成功",故障在半小时后以"服务间调用超时"的面目出现,排查路径完全想不到是 IP 问题;
  • 拿到链路本地地址 :云主机 DHCP 失败、网卡降级时接口上挂着 169.254.x.x。比 172.17.0.1 更难查------它看起来完全"像个正常内网 IP",注册进注册中心也不会有人觉得不对;
  • 拿到回环还不报错 :getLocalHost() 里有一句 catch (SecurityException e) { return impl.loopbackAddress(); }------装了 SecurityManager 而缺 SocketPermission "<主机名>", "resolve"(checkConnect(host, -1) 校验的正是 resolve 动作)时,它不抛异常,直接返回 127.0.0.1。hosts 只有 ::1 主机名 映射而 JVM 走 IPv6 栈时同理。

防御手段:

  • 注册地址入库前做合法性校验:非回环、非通配、非 169.254.0.0/16、非 172.17.0.0/16(自家内网真用了 172.17/16 的就去掉这条),(可选)再确认本机确实持有该地址;
  • 明确期望 IPv4 的场景加 -Djava.net.preferIPv4Stack=true,并在解析代码里过滤 Inet4Address。

一句话:这个 API 的两种失败------崩溃和说谎------说谎的那种更贵。

七、排查手册

拿到 UnknownHostException: <主机名> 后的五分钟定位流程:

步骤 命令 看什么
1 hostname / hostnamectl 确认报错里的名字 = 系统主机名(通常是)
2 getent hosts $(hostname) 关键一步:与 JVM 同走 glibc NSS,结论可互推;无输出 = 复现故障,问题在机器不在代码
3 cat /etc/nsswitch.conf 确认 hosts: files dns 顺序(files 优先,改 hosts 才有效)。文件不存在也别意外------Alpine/musl 压根不读它
4 cat /etc/hosts 有没有主机名映射、localhost 条目是否完整、主机名里有没有下划线
5 nslookup <主机名> / dig 验证 DNS 路径(这两个工具绕过 hosts 直连 DNS;容器/裁剪环境常见两条路都断)

修复后同样用 getent hosts $(hostname) 验证:有输出,Java 就能起来。开发机上,Windows 看 C:\Windows\System32\drivers\etc\hosts,macOS 改名后常发同类问题,解法相同。


一句话总结 :getLocalHost() 表面上在问"我的 IP 是什么",实际在问"我的主机名解析得出来吗"------前者天经地义,后者取决于 /etc/hosts 里有没有那一行,而这行字,只有你自己会写。

相关推荐
格物致胜工业数智技术1 小时前
皮尔磁纸板进料安全方案运维实录:O300传感器用了三年的个人体会
运维·安全
IT研究室1 小时前
最新计算机毕业设计选题推荐-基于spring boot的食品安全信息管理系统-网站-文档指导-Java-springboot
java·spring boot·课程设计
可涵不会debug1 小时前
实测ToDesk新终端:跨端会话恢复+多终端并行,远程Vibe Coding的完整体验报告
运维·电脑
Sayai1 小时前
MCP Server 开发实战:Spring AI 把后端日志查询暴露给 AI Agent(7 条设计原则与踩坑实录)
java·人工智能·ai agent·spring ai·mcp
大牧师2 小时前
Mybatis 框架教程
java·数据库·mybatis·数据持久化·orm
SunnyDays10112 小时前
Java 合并 PDF 文件:完整合并、指定页面合并与流合并
java·开发语言·合并pdf
金銀銅鐵2 小时前
[Java] 用GUI展示class文件顶层的 access_flags
后端·python·ai编程
资深电气设计2 小时前
TPO卷材研发常见问题:强度与柔韧性为何难兼顾?埃克森美孚威达美™6102的技术参考
运维·物联网·创业创新·业界资讯