登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go DNS 在本机与容器不同的 Resolver 配置

来源:17golang原创

时间:2026-10-28 06:58:04 324浏览 收藏

我遇到过这样的部署差异:同一段 Go 代码在开发机上能访问内部域名,放进容器后却返回 no such host。这时先不要把问题归咎于 HTTP 客户端。Go 的默认解析路径会读取运行环境提供的系统 DNS 配置,而本机与容器的 /etc/resolv.conf、hosts 文件、搜索域和网络可达范围可能都不同。更稳妥的做法是把 Resolver 当成依赖注入到真正发起连接的 net.Dialer 中,并为它设置明确的超时边界。

如果只是某个容器环境需要使用指定 DNS,应为该客户端创建一个独立的 net.Resolver,通过 PreferGo 和 Dial 指向容器可达的 DNS 服务,再把它交给 net.Dialer.Resolver。不要只创建 Resolver 却继续使用默认的 http.Transport,那样配置不会进入目标请求的拨号路径。
要点速览
  • 本机和容器的 DNS 差异首先是运行环境配置差异,不一定是业务域名本身有问题。
  • net.Resolver.Dial 控制 DNS 查询连接,net.Dialer.Resolver 决定实际拨号时使用哪个 Resolver。
  • DNS 服务器地址应来自容器网络配置,不能把本机回环地址或某个环境的固定 IP 当成通用值。

先把本机与容器的差异归到正确一层

net.LookupHost、net.LookupIP 等包级函数使用默认 Resolver。默认 Resolver 会根据平台和构建条件选择 Go resolver 或系统 resolver,并读取当前进程所在环境的解析配置。容器里的 /etc/resolv.conf 往往由运行时注入,里面的 nameserver、search 和 options 不必与宿主机相同;容器也可能只能访问自己的 DNS stub 或集群 DNS 服务。

因此,下面三种现象要分开判断:

现象优先检查说明
所有域名都解析失败容器的 resolv.conf、DNS 地址和网络策略更像运行环境或网络路径问题
只有内部域名失败search 域、split DNS 和容器可见的 nameserver本机的企业 DNS 不一定对容器可达
解析成功但 HTTP 失败端口、TLS、代理和目标服务Resolver 已经完成了名称解析,继续查连接层
Go 默认 Resolver、容器 resolv.conf、hosts 文件与应用自定义 Resolver 的静态边界关系说明图
图1:本机与容器 DNS 配置边界的结构说明图,展示配置来源与应用 Resolver 的关系,不是运行截图。

用 net.Resolver 为单个客户端指定 DNS

net.Resolver 的 PreferGo 可以要求优先使用 Go 内置解析器;Dial 则用于建立 DNS 查询连接。回调收到的 network 通常是 udp 或 tcp,address 是 DNS 服务地址。这里的 DNS 地址应由部署环境提供,例如通过环境变量传入一个容器确实能访问的 10.0.0.53:53,不要直接照搬宿主机的回环地址。

package dnsclient

import (
    "context"
    "fmt"
    "net"
    "time"
)

func newResolver(server string) *net.Resolver {
    return &net.Resolver{
        PreferGo: true,
        // DNS 查询也必须有自己的截止时间,不能无限等待网络重试。
        Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
            dialer := net.Dialer{Timeout: 2 * time.Second}
            // 使用部署传入的 DNS 地址,不依赖本机 resolv.conf 中的地址。
            return dialer.DialContext(ctx, network, server)
        },
    }
}

func lookup(ctx context.Context, resolver *net.Resolver, host string) ([]net.IPAddr, error) {
    // Resolver 只负责解析;调用方仍要把整体请求的 context 传进去。
    ips, err := resolver.LookupIPAddr(ctx, host)
    if err != nil {
        return nil, fmt.Errorf("lookup %s: %w", host, err)
    }
    return ips, nil
}

这个 Resolver 只影响显式调用它的查询。它不会改变进程里所有包级 DNS 调用,也不会自动改变已有 HTTP 客户端的拨号行为。把 Resolver 的作用域限制在需要特殊 DNS 的客户端,通常比修改全局环境变量更容易维护。

把 Resolver 接到 HTTP 的真实拨号路径

我第一次修这类问题时,最容易漏掉的是“配置对象没有被使用”。http.Transport 没有直接接收 net.Resolver 的字段;正确连接关系是 Resolver -> net.Dialer.Resolver -> Transport.DialContext -> http.Client。如果只调用了 resolver.LookupIPAddr,后续 http.Client.Do 仍可能使用另一个默认拨号器。

package dnsclient

import (
    "context"
    "net"
    "net/http"
    "time"
)

func newHTTPClient(dnsServer string) *http.Client {
    resolver := newResolver(dnsServer)
    dialer := &net.Dialer{
        Timeout:   5 * time.Second,
        KeepAlive: 30 * time.Second,
        Resolver:  resolver, // 让实际 TCP 拨号使用同一个自定义 Resolver。
    }
    transport := &http.Transport{
        DialContext:         dialer.DialContext,
        TLSHandshakeTimeout: 5 * time.Second,
        // 保留 Transport 复用能力,避免每次请求都新建连接池。
        MaxIdleConnsPerHost: 8,
    }
    return &http.Client{
        Transport: transport,
        Timeout:   15 * time.Second, // 给 DNS、建连、TLS 和响应读取设置总上限。
    }
}

func request(ctx context.Context, client *http.Client, url string) (*http.Response, error) {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return nil, err
    }
    // 使用注入了 Resolver 的 client,不能在这里改回 http.DefaultClient。
    return client.Do(req)
}

如果业务同时访问公网和内网域名,这个客户端仍然可以共用同一个 Transport;前提是指定的 DNS 服务能够解析这些域名。若 DNS 服务只覆盖内部区域,应该为不同解析域创建不同客户端,而不是在请求失败后盲目切换 nameserver。

Go net.Resolver 通过 net.Dialer Resolver 注入 http.Transport DialContext 的静态调用关系说明图
图2:Resolver 注入 HTTP 拨号路径的结构说明图,展示 DNS 查询、连接器和客户端的静态关系,不是运行证据。

超时、udp/tcp 和连接复用要一起考虑

Resolver.Dial 中的连接超时和 http.Client.Timeout 不是一回事。前者约束一次 DNS 查询连接,后者约束整个 HTTP 交换;请求上下文还可以提供更短的调用级截止时间。三者叠加时,实际生效的是最早到达的截止时间。不要只在 DNS 回调里设置超时,却让上层请求无限等待。

Dial 回调的 network 参数不应被忽略后固定写死为另一种协议。Go resolver 可能按解析过程请求 UDP 或 TCP;回调要把它传给底层拨号器。DNS 响应被截断、网络策略只允许 TCP 或特定解析服务要求 TCP 时,保留这个参数才能让 Resolver 按自己的策略工作。

自定义 Resolver 不等于每次请求都重新创建连接。真正的 HTTP 连接复用仍由同一个 Transport 管理;生产代码应把 http.Client 作为长期对象复用,并在应用关闭时按需要关闭空闲连接。

用四个证据点排查容器里的 no such host

  1. 看配置来源:确认容器实际看到的 /etc/resolv.conf、/etc/hosts 和搜索域,不要只看宿主机文件。
  2. 看 DNS 可达性:检查配置里的 nameserver 是否能从容器网络访问,尤其注意本机回环地址在容器中代表容器自己。
  3. 看注入链路:确认请求使用的是带 Resolver 的 net.Dialer,并且 Transport.DialContext 绑定了这个 Dialer。
  4. 看错误边界:把 *net.DNSError、连接超时、TLS 错误和 HTTP 状态码分开记录,避免把解析失败与服务拒绝混成一个“网络问题”。

GODEBUG=netdns=go 可以作为进程级解析器选择的诊断手段,但它是全局运行时设置,不是按客户端隔离的 Resolver 配置。需要不同容器或不同目标域使用不同 DNS 时,优先采用显式的 net.Resolver 注入。

常见问题

为什么本机能解析,容器里却返回 no such host?

最常见的原因是两边读取到的 DNS 服务、搜索域或 hosts 内容不同。先对照容器自己的解析配置,再确认 DNS 地址从容器网络可达。

设置了 net.Resolver,http.Client 为什么还是解析失败?

通常是 Resolver 没有进入 HTTP 的拨号路径。应将它赋给 net.Dialer.Resolver,再把 dialer.DialContext 交给 http.Transport。

PreferGo 是不是固定 DNS 服务器?

不是。PreferGo 影响解析器实现选择;要指定服务器,还需要在 Dial 中连接部署环境提供的 DNS 地址。

可以把 DNS 地址写成 127.0.0.1:53 吗?

只有在 DNS 服务确实运行在同一个网络命名空间的回环地址上时才可以。容器里的 127.0.0.1 通常指向容器自身,不能默认等同于宿主机。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>