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

net.Resolver StrictErrors 处理部分解析结果

来源:17golang原创

时间:2026-10-10 18:37:24 457浏览 收藏

net.Resolver.StrictErrors 决定的是:Go 内置 DNS 解析器执行多个子查询时,如果其中一个发生临时错误,是否仍返回其他子查询已经得到的地址。默认值 false 偏向兼容性,例如 A 查询成功而 AAAA 查询超时,调用方仍可能拿到 IPv4 地址;设为 true 后,这类临时错误会中止整个查询,并丢弃已经收集的部分地址。

先理清楚四点
  • StrictErrors 只对 Go 内置解析器生效,使用 cgo 或系统解析器时不能假设它控制结果。
  • 它关注 timeout、套接字错误、SERVFAIL 等临时错误,不是“所有子查询必须都有记录”。
  • false 适合优先保证连接可用的通用客户端,true 适合不能接受单族缺失或部分发现结果的场景。
  • StrictErrors 不是 DNSSEC、重试开关或安全校验;启用后也必须正确处理 *net.DNSError。

背景:一次 LookupIPAddr 可能包含多个子查询

调用 LookupIPAddr 或网络类型为 ip 的 LookupIP 时,Go 内置解析器通常会查询 A 与 AAAA。配置了搜索域时,一个不带结尾点的名称还可能沿搜索列表尝试多个完整域名。对调用方而言是一次方法调用,对解析器而言却可能是多条独立请求。

独立请求意味着结果可以不对称:A 成功返回 IPv4,AAAA 返回 SERVFAIL;AAAA 成功而 A 超时;搜索列表中的一个名字发生套接字错误,另一个名字可以正常解析。StrictErrors 就是为这种“部分成功、部分临时失败”定义处理策略。

A 查询成功、AAAA 临时错误、宽松模式部分结果和严格模式整体错误的语义关系图
图1:同一双栈解析中的临时错误,在宽松模式下可保留部分地址,在严格模式下会中止整体查询。

旧语义的目的:优先返回仍然可用的地址

StrictErrors 默认是 false。这不是忽略所有 DNS 错误,而是在多子查询中允许成功结果继续发挥作用。例如目标服务同时发布 A 和 AAAA,局部 DNS 设备错误处理 AAAA 查询,但 IPv4 路径完全可用;默认模式可以让客户端继续连接。

官方文档明确说明,严格模式默认不启用,是因为它可能影响与错误处理 AAAA 查询的解析器之间的兼容性。对于网页抓取器、普通 HTTP 客户端、软件更新器等“任一可达地址即可工作”的程序,默认行为往往更符合可用性目标。

需要特别区分“没有 AAAA 记录”和“AAAA 查询临时失败”。StrictErrors 控制的是后者。一个子查询正常回答无数据,不应被简单等同于 timeout 或 SERVFAIL;也不要因为启用了严格模式,就假设 A 与 AAAA 都必须返回至少一个地址。

新规则:StrictErrors=true 时临时错误中止整体查询

启用 StrictErrors 后,只要多个子查询中的任一个遇到可识别的临时错误,Go 内置解析器会让整体查询失败。当前实现还会丢弃此前收集的地址,避免网络抖动把一个双栈主机暂时表现成“只有 IPv4”或“只有 IPv6”。

场景StrictErrors=falseStrictErrors=true
A 成功,AAAA 超时可返回 IPv4 部分结果整体返回错误,不保留 IPv4
A 成功,AAAA SERVFAIL可继续使用成功地址整体返回临时错误
A 无记录,AAAA 成功返回 IPv6通常仍返回 IPv6
A 与 AAAA 都无记录返回未找到错误返回未找到错误
调用上下文取消返回取消相关错误返回取消相关错误

表中的“可返回”不是对所有系统解析器的承诺,它描述的是 Go 内置解析器与该字段的设计边界。临时错误是否被准确标记也依赖底层错误信息;官方文档提醒,并非所有真实的超时或临时故障都一定带有对应标志。

代码对比:只改变错误策略,不改变查询接口

下面的构造函数把 PreferGo 与 StrictErrors 放在一起。原因是 StrictErrors 针对 Go 内置解析器;若程序必须依赖这套语义,应显式优先使用内置实现,而不是让不同操作系统自行选择系统解析器。

package main

import "net"

func newResolver(strict bool) *net.Resolver {
    return &net.Resolver{
        // StrictErrors 只约束 Go 内置解析器,显式 PreferGo 便于跨环境保持语义。
        PreferGo:    true,
        StrictErrors: strict,
    }
}

PreferGo 表示在可用的平台上优先使用 Go 内置解析器,并不保证绕过所有操作系统限制。若自定义 Resolver.Dial 指向指定 DNS 服务,还要分别处理 UDP、TCP 回退、超时和网络策略,不能只为了 StrictErrors 随意接管。

宽松模式:通用客户端接受可用的部分地址

宽松模式的重点不是“忽略 err”,而是按返回契约同时检查地址与错误。对 LookupIPAddr 而言,成功得到地址时通常返回 nil 错误;若没有可用地址,才返回对应 DNS 错误。应用仍应为整个查询设置截止时间。

func resolveCompatible(ctx context.Context, host string) ([]net.IPAddr, error) {
    resolver := newResolver(false)

    // 总超时约束全部 A、AAAA 与搜索域子查询,避免解析长期占用请求预算。
    lookupCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()

    addrs, err := resolver.LookupIPAddr(lookupCtx, host)
    if err != nil {
        return nil, fmt.Errorf("解析 %s 失败: %w", host, err)
    }
    if len(addrs) == 0 {
        return nil, fmt.Errorf("解析 %s 未返回可用地址", host)
    }
    return addrs, nil
}

这类策略适合“有一个地址族能连接就继续”的程序。后续应把主机名交给 net.Dialer 处理多地址与双栈回退;如果先解析再只取第一个地址,仍会把 DNS 容错能力浪费掉。

严格模式:服务发现必须保持完整语义

严格模式更适合这样的需求:地址族缺失可能改变流量分配;服务发现结果要写入长生命周期缓存;运维系统需要把单族临时故障显式暴露;安全策略要求在候选集不完整时拒绝继续。它不是默认更安全,而是把“不完整”定义成不可接受。

func resolveStrict(ctx context.Context, host string) ([]net.IPAddr, error) {
    resolver := newResolver(true)

    lookupCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()

    addrs, err := resolver.LookupIPAddr(lookupCtx, host)
    if err != nil {
        var dnsErr *net.DNSError
        if errors.As(err, &dnsErr) {
            // 只记录错误类别,不把内部域名或 DNS 服务器地址无差别写入公共日志。
            return nil, fmt.Errorf(
                "严格解析失败 timeout=%t temporary=%t not_found=%t: %w",
                dnsErr.IsTimeout,
                dnsErr.IsTemporary,
                dnsErr.IsNotFound,
                err,
            )
        }
        return nil, fmt.Errorf("严格解析失败: %w", err)
    }
    return addrs, nil
}

启用严格模式后,重试应由有预算的上层策略控制。不要看到 IsTemporary 就无限重试;应限制次数、加入抖动,并让 context 的截止时间覆盖 DNS 与后续连接。否则局部 DNS 故障会被放大成重试风暴。

兼容注意:这些情况 StrictErrors 管不到

第一,StrictErrors 不控制 cgo 或系统解析器的内部策略。若 PreferGo 没有生效,结果可能仍由 getaddrinfo 等系统接口决定。第二,它不启用 DNSSEC 验证,也不验证返回地址是否符合业务访问控制。第三,它不会增加重试次数,更不会把所有错误都改成临时错误。

第四,查询 ip4 或 ip6 时只有一个地址族子查询,StrictErrors 的“部分结果”价值会明显降低。若业务就是分别处理两个地址族,直接发起两个限族查询并各自记录结果,通常比依赖一次 ip 查询的聚合语义更清楚。

第五,搜索域会引入另一个部分结果维度。短主机名可能按 resolv.conf 的 search 列表组合多个候选名;严格模式中的临时错误可以让整个过程提前失败。服务间通信若能使用完整限定域名,通常更容易解释和观测。

Go 内置解析器、多子查询、临时错误、StrictErrors 与 cgo 系统解析器作用范围图
图2:StrictErrors 的有效范围是 Go 内置解析器中的多子查询临时错误,不覆盖系统解析器和其他 DNS 安全语义。

如何确认当前到底用了哪个解析器

调试时可以使用 GODEBUG=netdns 查看解析器选择。数字 1 输出决策信息;go+1 在强制纯 Go 解析器的同时打印诊断。它适合短期对比,不建议长期写入生产启动参数。

# 查看当前进程选择了 Go 解析器还是系统解析器,排查结束后移除
GODEBUG=netdns=1 ./service

# 临时强制 Go 内置解析器并打印决策,用于验证 StrictErrors 行为
GODEBUG=netdns=go+1 ./service

除了模式,还要记录 Go 版本、操作系统、/etc/resolv.conf 的 search/ndots/options、DNS 服务地址和查询耗时。不要只用“开关前后是否成功”作为结论,因为缓存、网络抖动和 DNS 服务器轮换都可能影响单次实验。

性能与稳定性:严格失败会改变重试压力

宽松模式可能减少整体失败,但会让流量暂时集中到一个地址族;严格模式保持结果完整性,却会把单族临时故障升级为整个请求失败。两者没有脱离业务的统一最优解。

采用前建议观测:A/AAAA 成功率、SERVFAIL 比例、解析延迟、部分结果发生率、严格失败后的重试次数、最终连接地址族和连接成功率。若启用后整体错误率明显上升,应先修复上游 DNS 或 AAAA 兼容问题,而不是无限增加重试。

采用建议

  • 普通客户端、浏览器式访问、任一地址族可用即可:保留 StrictErrors=false。
  • 服务发现结果会长时间缓存,且单族缺失会改变语义:评估 StrictErrors=true。
  • 需要依赖 StrictErrors:同时设置 PreferGo=true,并确认目标平台支持 Go 内置解析器。
  • 只关心一个地址族:使用 LookupIP(ctx, "ip4", host) 或 "ip6" 明确表达。
  • 错误处理:使用 errors.As 提取 *net.DNSError,区分 timeout、temporary 与 not found。
  • 重试:设置总预算、次数与抖动,禁止无上限循环。
  • 观测:记录错误类别、地址族数量和耗时,按数据分级要求保护内部域名与 DNS 地址。
  • 测试:覆盖 A 成功/AAAA SERVFAIL、AAAA 成功/A 超时、单族无记录、上下文取消和搜索域临时错误。

常见问题

StrictErrors=true 是否要求 A 和 AAAA 都必须有记录?

不是。它针对的是多个子查询中的临时错误。一个地址族正常回答无记录,与超时、套接字错误或 SERVFAIL 不同。

为什么设置了 StrictErrors,却看不出行为变化?

可能当前使用了 cgo/系统解析器,也可能查询只有一个子任务,或失败未被标记为临时错误。先确认解析器模式和实际错误类型。

StrictErrors=true 会自动重试失败的 AAAA 查询吗?

不会。它改变的是部分结果处理策略,不是重试策略。重试次数和总超时仍应由应用层控制。

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