当前位置: 首页 > news >正文

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 服务器) 配置强相关:

  1. 默认情况下 DNS 查询会同时尝试解析 IPv4(A 记录) 和 IPv6 地址(AAAA 记录),无论环境是否支持 IPv6

  2. 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.
  3. 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.A172.16.0.10
    apportal.rehearsal.com.default.svc.cluster.local.AAAA(IPv6)169.254.20.10
    apportal.rehearsal.com.default.svc.cluster.local.AAAA172.16.0.10
    apportal.rehearsal.com.svc.cluster.local.A169.254.20.10
    apportal.rehearsal.com.svc.cluster.local.A172.16.0.10
    apportal.rehearsal.com.svc.cluster.local.AAAA169.254.20.10
    apportal.rehearsal.com.svc.cluster.local.AAAA172.16.0.10
    apportal.rehearsal.com.cluster.local.A169.254.20.10
    apportal.rehearsal.com.cluster.local.A172.16.0.10
    apportal.rehearsal.com.cluster.local.AAAA169.254.20.10
    apportal.rehearsal.com.cluster.local.AAAA172.16.0.10
    apportal.rehearsal.com.A169.254.20.10
    apportal.rehearsal.com.A172.16.0.10
    apportal.rehearsal.com.AAAA169.254.20.10
    apportal.rehearsal.com.AAAA172.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容器中域名解析优化

http://www.dtcms.com/a/336056.html

相关文章:

  • Agent开发进阶路线:从基础响应到自主决策的架构演进
  • C++类型转换详解:从C风格到C++风格
  • 如何理解事件循环和JS的异步?
  • LintCode第137-克隆图
  • PostgreSQL导入mimic4
  • SQL详细语法教程(四)约束和多表查询
  • C语言相关简单数据结构:双向链表
  • Rust Async 异步编程(五):执行器和系统 I/O
  • Effective C++ 条款47: 使用traits classes表现类型信息
  • 基于强化学习的柔性机器人控制研究
  • 【大模型微调系列-07】Qwen3全参数微调实战
  • 关于虾的智能养殖系统的开发与实现(LW+源码+讲解+部署)
  • 【LeetCode题解】LeetCode 33. 搜索旋转排序数组
  • 详解flink java基础(一)
  • 嵌入式软件--->任务间通信
  • 【C++知识杂记1】智能指针及其分类
  • 05-实施任务控制
  • open Stack及VM虚拟机和其他平台虚拟机迁移至 VMware vSphere(esxi)虚拟化平台骨灰级后台磁盘替换法迁移方式
  • Maven依赖范围
  • C11期作业18(07.12)
  • 跨越南北的养老对话:为培养“银发中国”人才注入新动能
  • Linux——一些常用的其他命令
  • 学习Python中Selenium模块的基本用法(5:程序基本步骤)
  • MySQL数据库备份与恢复
  • 《棒球百科》奥运会取消了棒球·野球1号位
  • 旋钮键盘项目---foc讲解(闭环位置控制)
  • Redis-plus-plus API使用指南:通用操作与数据类型接口介绍
  • TensorFlow|张量流
  • C/C++复习(四)
  • 【LeetCode】单链表经典算法:移除元素,反转链表,约瑟夫环问题,找中间节点,分割链表