Traefik网关DNS解析超时问题优化
1、背景
在生产环境使用 Traefik 网关时出现了偶发的 DNS 解析超时导致网关与后端服务建立连接异常的情况。通过调用链埋点数据观察发现,该部署环境中 Traefik 的 DNS 解析性能较差,耗时通常在 4ms 以上(正常应该是 1ms 以内)

初步分析:
Traefik 基于 Go 语言的 net/http 实现连接池,net/http 中仅支持连接复用未实现 DNS 缓存机制,所以每次新建连接时都会进行 DNS 解析。当网络不稳定情况下(DNS 解析使用 UDP 协议,如果网络丢包时只能依赖客户端超时)时就容易出现 DNS 解析超时情况
2、DNS 域名解析优化
1)、DNS 域名解析参数配置
DNS 解析策略、次数和 /etc/resolv.conf
文件中的 ndots、search domain(搜索域) 和 nameserver(DNS 服务器) 配置强相关:
-
默认情况下 DNS 查询会同时尝试解析 IPv4(A 记录) 和 IPv6 地址(AAAA 记录),无论环境是否支持 IPv6
-
ndots 和待解析的域名决定要不要优先使用 search domain,如果你的域名中,点的个数比配置的 ndots 小,则会优先拼接 search domain 后去解析
比如有如下配置时:
search default.svc.cluster.local svc.cluster.local cluster.local options ndots:3
如果要解析的域名是
apportal.rehearsal.com
,ndots 配置的是3,待解析域名中的点数小于 ndots,所以会优先拼接搜索域名去解析,解析顺序如下:apportal.rehearsal.com.default.svc.cluster.local.
apportal.rehearsal.com.svc.cluster.local.
apportal.rehearsal.com.cluster.local.
apportal.rehearsal.com.
如果 ndots 配置的是2(待解析域名中的点数等于ndots),则解析顺序如下:
apportal.rehearsal.com.
apportal.rehearsal.com.default.svc.cluster.local.
apportal.rehearsal.com.svc.cluster.local.
apportal.rehearsal.com.cluster.local.
-
serach domain 和 nameserver 决定了 DNS 最多查询的次数,即 DNS 最多查询的次数等于搜素域的数量+1乘以 dnsserver 的数量
比如有以下配置:
search default.svc.cluster.local svc.cluster.local cluster.local nameserver 169.254.20.10 nameserver 172.16.0.10 options ndots:5
当解析
apportal.rehearsal.com
域名时,解析顺序如下:解析域名 查询类型 dns server apportal.rehearsal.com.default.svc.cluster.local.
A(IPv4) 169.254.20.10 apportal.rehearsal.com.default.svc.cluster.local.
A 172.16.0.10 apportal.rehearsal.com.default.svc.cluster.local.
AAAA(IPv6) 169.254.20.10 apportal.rehearsal.com.default.svc.cluster.local.
AAAA 172.16.0.10 apportal.rehearsal.com.svc.cluster.local.
A 169.254.20.10 apportal.rehearsal.com.svc.cluster.local.
A 172.16.0.10 apportal.rehearsal.com.svc.cluster.local.
AAAA 169.254.20.10 apportal.rehearsal.com.svc.cluster.local.
AAAA 172.16.0.10 apportal.rehearsal.com.cluster.local.
A 169.254.20.10 apportal.rehearsal.com.cluster.local.
A 172.16.0.10 apportal.rehearsal.com.cluster.local.
AAAA 169.254.20.10 apportal.rehearsal.com.cluster.local.
AAAA 172.16.0.10 apportal.rehearsal.com.
A 169.254.20.10 apportal.rehearsal.com.
A 172.16.0.10 apportal.rehearsal.com.
AAAA 169.254.20.10 apportal.rehearsal.com.
AAAA 172.16.0.10
2)、Kubernetes 中搜索域和ndots 默认值
1)搜索域
Kubernetes 为方便 Service 访问,默认配置 3 个搜索域:nsSvcDomain、svcDomain、clusterDomain
default namespace下的搜索域默认为:
search default.svc.cluster.local svc.cluster.local cluster.local
default.svc.cluster.local
:同 namespace 内可直接用 Service 名称访问(比如通过B
访问同命名空间的 Service B)svc.cluster.local
:跨 namespace 可通过${service name}.${namespace name}
访问cluster.local
:支持非 Kubernetes 上的域名访问,例如设置 clusterDomain 为rehearsal.com
,那么对于 apportal.rehearsal.com 域名,可以使用apportal
来访问
2)ndots
Kubernetes 中 ndots 默认值为5(Kubernetes官方解释),官方这样设置的初衷是优先匹配 Kubernetes 集群内域名,但可能对外部域名解析造成冗余
3)、ndots 参数优化
在我们的部署环境下都是通过三级域名(例如:apportal.rehearsal.com)来访问 Service 的,ndots 默认值为5
default namespace Pod 的 /etc/resolv.conf
文件
# cat resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 192.168.0.10
options ndots:5
apportal.rehearsal.com 解析情况如下,需要解析8次才能解析到对应域名:(如果 nameserver 有两个时需要解析16次)
这里我们在 Deployment 中将 ndots 参数调整为2,调整后只需要2次就能解析到对应域名
优化依据:三级域名(含2个点)在 ndots:2
配置下,会直接解析原域名,减少不必要的搜索域拼接步骤,显著降低解析次数和耗时
3、Java DNS 缓存机制分析
在减少 DNS 解析次数后,我们还希望在 Traefik 中添加 DNS 缓存机制来减少每次新建连接时 DNS 解析的耗时,这里我们调研了 Java DNS 缓存机制来作为参考。以下从 JDK 底层缓存和 Http 客户端缓存两方面展开分析:
1)、JDK 中的 DNS 缓存
Java 通过 java.net.InetAddress
类处理域名解析,并将结果缓存以避免重复查询
缓存查询流程:
解析入口方法为 InetAddress.getAllByName(String host)
,其优先从缓存获取结果:
// java.net.InetAddressprivate static InetAddress[] getAllByName0 (String host, InetAddress reqAddr, boolean check)throws UnknownHostException {/* If it gets here it is presumed to be a hostname *//* Cache.get can return: null, unknownAddress, or InetAddress[] *//* make sure the connection to the host is allowed, before we* give out a hostname*/if (check) {SecurityManager security = System.getSecurityManager();if (security != null) {security.checkConnect(host, -1);}}// 先从缓存中获取InetAddress[] addresses = getCachedAddresses(host);/* If no entry in cache, then do the host lookup */if (addresses == null) {// 缓存中数据为空,进行dns lookupaddresses = getAddressesFromNameService(host, reqAddr);}if (addresses == unknown_array)throw new UnknownHostException(host);return addresses.clone();}private static Cache addressCache = new Cache(Cache.Type.Positive);private static InetAddress[] getCachedAddresses(String hostname) {hostname = hostname.toLowerCase();// search both positive & negative cachessynchronized (addressCache) {cacheInitIfNeeded();// 使用addressCache进行缓存CacheEntry entry = addressCache.get(hostname);if (entry == null) {entry = negativeCache.get(hostname);}if (entry != null) {return entry.addresses;}}// not foundreturn null;}
缓存策略控制:
sun.net.InetAddressCachePolicy
中定义了缓存的三个策略值:
FOREVER = -1
:永久缓存(默认值)NEVER = 0
:不缓存- 大于0,缓存多少秒后过期
Cache addressCache = new Cache(Cache.Type.Positive)
这里定义的 type 为 Type.Positive
private int getPolicy() {if (type == Type.Positive) {return InetAddressCachePolicy.get();} else {return InetAddressCachePolicy.getNegative();}}
public final class InetAddressCachePolicy {private static int cachePolicy = -1;public static synchronized int get() {return cachePolicy;}
默认值为永久缓存,不过期,也不会刷新 DNS 数据
2)、Http 客户端缓存实现
在 Java Http 客户端层面,不同的 Http 客户端实现的缓存机制不同,以 Apache HttpClient 4.5.6 版本为例,核心代码如下:
// org.apache.http.impl.conn.DefaultHttpClientConnectionOperator
public class DefaultHttpClientConnectionOperator implements HttpClientConnectionOperator {public DefaultHttpClientConnectionOperator(final Lookup<ConnectionSocketFactory> socketFactoryRegistry,final SchemePortResolver schemePortResolver,final DnsResolver dnsResolver) {super();Args.notNull(socketFactoryRegistry, "Socket factory registry");this.socketFactoryRegistry = socketFactoryRegistry;this.schemePortResolver = schemePortResolver != null ? schemePortResolver :DefaultSchemePortResolver.INSTANCE;// 初始化 dnsResolverthis.dnsResolver = dnsResolver != null ? dnsResolver :SystemDefaultDnsResolver.INSTANCE;}@Overridepublic void connect(final ManagedHttpClientConnection conn,final HttpHost host,final InetSocketAddress localAddress,final int connectTimeout,final SocketConfig socketConfig,final HttpContext context) throws IOException {final Lookup<ConnectionSocketFactory> registry = getSocketFactoryRegistry(context);final ConnectionSocketFactory sf = registry.lookup(host.getSchemeName());if (sf == null) {throw new UnsupportedSchemeException(host.getSchemeName() +" protocol is not supported");}// 使用dnsResolver进行dns解析final InetAddress[] addresses = host.getAddress() != null ?new InetAddress[] { host.getAddress() } : this.dnsResolver.resolve(host.getHostName());...}
Apache HttpClient 中默认 DnsResolver 为 Java DNS 缓存逻辑,也就是 DNS 永久不刷新
//org.apache.http.impl.conn.SystemDefaultDnsResolver
public class SystemDefaultDnsResolver implements DnsResolver {public static final SystemDefaultDnsResolver INSTANCE = new SystemDefaultDnsResolver();@Overridepublic InetAddress[] resolve(final String host) throws UnknownHostException {return InetAddress.getAllByName(host);}}
而对应 OkHttp 来说,OkHttp 内部维护了一个 DNS 缓存,具有以下特点:
- 成功解析缓存:默认缓存时间为5分钟(300 秒),与 DNS 服务器返回的 TTL 无关
- 失败解析缓存:默认缓存10秒,避免频繁重试无效域名
- 缓存实现:通过连接池(
RealConnectionPool
)中的RouteDatabase
关联域名与解析结果,随连接池管理自动失效
4、Traefik 添加 DNS 缓存机制
1)、方案一:net/http替换为fasthttp
可以参考最新的 PR https://github.com/traefik/traefik/pull/11122,将 net/http 替换为 fasthttp,fasthttp 支持 DNS 缓存且性能更好

但目前此功能官方标记为实验性功能,不推荐生产使用,而且这块改动相对较大,所以未采用该方案
2)、方案二:引入 DNS 缓存库
我们选择集成 go-dnscache 库实现 DNS 缓存功能,go-dnscache 库刷新 DNS 缓存有两个额外的配置选项如下:
r := &Resolver{}options := dnscache.ResolverRefreshOptions{ClearUnused: true, // 默认为true,清除未使用的 dns 条目PersistOnFailure: false, // 默认为false,刷新失败时清理旧数据}resolver.RefreshWithOptions(options)
使用的 DNS 缓存策略如下:
- 缓存有效期设置为10分钟,每10分钟异步刷新 DNS 缓存
- 刷新时清除未使用的 DNS 条目(
ClearUnused=true
),缓存刷新失败时自动保留旧数据,(PersistOnFailure=true
,防止因集中刷新时 DNS 解析异常影响后续程序访问)
代码改动示例:
// pkg/server/service/roundtripper.go
func NewRoundTripperManager(spiffeX509Source SpiffeX509Source) *RoundTripperManager {resolver := &dnscache.Resolver{}go func() {logger := log.Ctx(context.Background()).With().Str(logs.ComponentName, "dns-cache-refresher").Logger()t := time.NewTicker(10 * time.Minute)defer t.Stop()for range t.C {// 每隔10分钟刷新一次 dns 缓存start := time.Now()options := dnscache.ResolverRefreshOptions{ClearUnused: false, // 不清除未使用的 dns 条目PersistOnFailure: true, // 刷新失败时保留旧数据}resolver.RefreshWithOptions(options)logger.Info().Dur("duration", time.Since(start)).Msg("DNS cache refreshed successfully")}}()return &RoundTripperManager{roundTrippers: make(map[string]http.RoundTripper),configs: make(map[string]*dynamic.ServersTransport),spiffeX509Source: spiffeX509Source,dnsResolver: resolver,}
}func (r *RoundTripperManager) createRoundTripper(cfg *dynamic.ServersTransport) (http.RoundTripper, error) {...// 创建使用 dns 缓存的 DialContextcustomDialContext := func(ctx context.Context, network, addr string) (net.Conn, error) {host, port, err := net.SplitHostPort(addr)if err != nil {return nil, err}...transport := &http.Transport{Proxy: http.ProxyFromEnvironment,DialContext: utils.CachedDialContext(dialer), // 使用带 dns 缓存的 DialContextMaxIdleConnsPerHost: cfg.MaxIdleConnsPerHost,IdleConnTimeout: 90 * time.Second,TLSHandshakeTimeout: 10 * time.Second,ExpectContinueTimeout: 1 * time.Second,ReadBufferSize: 64 * 1024,WriteBufferSize: 64 * 1024,}
参考:
go dns解析原理及调优
阿里云DNS最佳实践
kubernetes容器中域名解析优化