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容器中域名解析優化