最近在生產(chǎn)環(huán)境碰到過多次域名解析失敗的問題,有時候還是客戶windowns環(huán)境報障,是時候深入了解下Vertx內(nèi)部的域名解析機制了。
1、Vertx使用DNS方法
import java.util.Arrays;
import io.vertx.core.Vertx;
import io.vertx.core.VertxOptions;
import io.vertx.core.dns.AddressResolverOptions;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.http.HttpClientRequest;
public class DemoMain {
public static void main(String[] args) throws Exception {
AddressResolverOptions addressResolverOptions = new AddressResolverOptions();
addressResolverOptions.setNdots(1);
addressResolverOptions.setServers(Arrays.asList("8.8.8.8"));
addressResolverOptions.setSearchDomains(Arrays.asList(".com"));
VertxOptions vertxOptions = new VertxOptions();
vertxOptions.setAddressResolverOptions(addressResolverOptions);
Vertx vertx = Vertx.vertx(vertxOptions);
HttpClientOptions clientOp = new HttpClientOptions();
clientOp.setSsl(true);
clientOp.setTrustAll(true);
clientOp.setVerifyHost(false);
HttpClient httpClient = vertx.createHttpClient(clientOp);
HttpClientRequest req = httpClient.get(443, "www.baidu.com", "/index.html", resp -> {
System.out.println(resp.statusCode());
vertx.close();
});
req.end();
}
}
AddressResolverOptions 有幾個重要屬性
servers: 8.8.8.8,8.8.4.4 #對應Linux /etc/resolv.conf的nameserver,DNS服務器地址,支持配置多個,以逗號隔開
ndots: 1 #對應linux /etc/resolv.conf里面的options: ndots, 作用就是如果給的域名里面包含的點的個數(shù)少于該閾值,那么DNS解析的時候就會默認加上searchDomains的值,這個必須和searchDomains搭配使用,Linux默認為1,華為公有云PAAS(包含容器)默認是4
searchDomains: a,b,c #對應linux /etc/resolv.conf里面的search,和ndots搭配使用,如果當前域名的點個數(shù)少于設置值,解析時就會把這些值添加到域名后面一起解析,比如ndots設置的為4,當前域名為servicecomb.cn-north-1.myhwclouds.com,只有三個點,那么解析的時候就會自動加上servicecomb.cn-north-1.myhwclouds.com.a去解析,沒解析出來在用servicecomb.cn-north-1.myhwclouds.com.b,直到能最后解析出來
optResourceEnabled: true #optional record is automatically included in DNS queries
cacheMinTimeToLive: 0 #最小緩存時間
cacheMaxTimeToLive: 10000 #最大緩存時間
cacheNegativeTimeToLive: 0 #DNS解析失敗后,下次重試的等待時間
queryTimeout: 5000 #查詢超時時間
maxQueries: 4 #查詢次數(shù)
rdFlag: true #設置DNS遞歸查詢
rotateServers: true #設置是否支持輪詢,如果有多個域名服務器,輪訓可以加快域名解析速度
2、代碼解析過程
使用Vertx vertx = Vertx.vertx(vertxOptions),在VertxImpl構(gòu)造方法里面會初始化this.addressResolver = new AddressResolver(this, options.getAddressResolverOptions());
2.1 io.vertx.core.impl.AddressResolver里面有個static塊,會讀取/etc/resolv.conf文件,解析得到ndots和rotate默認值,在構(gòu)造方法內(nèi)初始化
public AddressResolver(Vertx vertx, AddressResolverOptions options) {
this.provider = ResolverProvider.factory(vertx, options);
this.resolverGroup = provider.resolver(options);
this.vertx = vertx;
}
ResolverProvider獲取provider的時候,默認得到DnsResolverProvider,這個是Vertx提供的默認域名解析。
2.2 DnsResolverProvider代碼解析
public DnsResolverProvider(VertxImpl vertx, AddressResolverOptions options) {
List<String> dnsServers = options.getServers();
List<InetSocketAddress> serverList = new ArrayList<>();
// 看是否有配置dns服務地址,如果配置了則使用配置的地址
if (dnsServers != null && dnsServers.size() > 0) {
for (String dnsServer : dnsServers) {
// 解析域名地址和端口,默認是53
int sep = dnsServer.indexOf(':');
String ipAddress;
int port;
if (sep != -1) {
ipAddress = dnsServer.substring(0, sep);
port = Integer.parseInt(dnsServer.substring(sep + 1));
} else {
ipAddress = dnsServer;
port = 53;
}
try {
// 檢查地址是否正確
serverList.add(new InetSocketAddress(InetAddress.getByAddress(NetUtil.createByteArrayFromIpAddressString(ipAddress)), port));
} catch (UnknownHostException e) {
throw new VertxException(e);
}
}
} else {
// 如果沒有配置域名地址,則需要讀取服務器上的地址
// 這里直接使用了Netty提供的獲取服務器上默認域名地址,這里最終會調(diào)用DefaultDnsServerAddressStreamProvider,詳細參考2.3
DnsServerAddressStream stream = DnsServerAddresses.defaultAddresses().stream();
Set<InetSocketAddress> all = new HashSet<>();
while (true) {
InetSocketAddress address = stream.next();
if (all.contains(address)) {
break;
}
serverList.add(address);
all.add(address);
}
}
DnsServerAddresses nameServerAddresses = options.isRotateServers() ? DnsServerAddresses.rotational(serverList) : DnsServerAddresses.sequential(serverList);
DnsServerAddressStreamProvider nameServerAddressProvider = hostname -> nameServerAddresses.stream();
// 解析host文件,不需要通過域名服務器解析,比如/etc/hosts,支持配置文件路徑和直接設置。如果都為空,則讀取系統(tǒng)的默認配置,windows讀取C:\Windows\System32\drivers\etc\hosts,Linux讀取/etc/hosts
HostsFileEntries entries;
if (options.getHostsPath() != null) {
File file = vertx.resolveFile(options.getHostsPath()).getAbsoluteFile();
try {
if (!file.exists() || !file.isFile()) {
throw new IOException();
}
entries = HostsFileParser.parse(file);
} catch (IOException e) {
throw new VertxException("Cannot read hosts file " + file.getAbsolutePath());
}
} else if (options.getHostsValue() != null) {
try {
entries = HostsFileParser.parse(new StringReader(options.getHostsValue().toString()));
} catch (IOException e) {
throw new VertxException("Cannot read hosts config ", e);
}
} else {
entries = HostsFileParser.parseSilently();
}
int minTtl = intValue(options.getCacheMinTimeToLive(), 0);
int maxTtl = intValue(options.getCacheMaxTimeToLive(), Integer.MAX_VALUE);
int negativeTtl = intValue(options.getCacheNegativeTimeToLive(), 0);
DnsCache resolveCache = new DefaultDnsCache(minTtl, maxTtl, negativeTtl);
DnsCache authoritativeDnsServerCache = new DefaultDnsCache(minTtl, maxTtl, negativeTtl);
this.vertx = vertx;
// 初始化AddressResolverGroup
this.resolverGroup = new AddressResolverGroup<InetSocketAddress>() {
@Override
protected io.netty.resolver.AddressResolver<InetSocketAddress> newResolver(EventExecutor executor) throws Exception {
ChannelFactory<DatagramChannel> channelFactory = () -> vertx.transport().datagramChannel();
DnsAddressResolverGroup group = new DnsAddressResolverGroup(channelFactory, nameServerAddressProvider) {
@Override
protected NameResolver<InetAddress> newNameResolver(EventLoop eventLoop, ChannelFactory<? extends DatagramChannel> channelFactory, DnsServerAddressStreamProvider nameServerProvider) throws Exception {
DnsNameResolverBuilder builder = new DnsNameResolverBuilder((EventLoop) executor);
builder.hostsFileEntriesResolver(new HostsFileEntriesResolver() {
@Override
public InetAddress address(String inetHost, ResolvedAddressTypes resolvedAddressTypes) {
InetAddress address = lookup(inetHost, resolvedAddressTypes);
if (address == null) {
address = lookup(inetHost.toLowerCase(Locale.ENGLISH), resolvedAddressTypes);
}
return address;
}
InetAddress lookup(String inetHost, ResolvedAddressTypes resolvedAddressTypes) {
switch (resolvedAddressTypes) {
case IPV4_ONLY:
return entries.inet4Entries().get(inetHost);
case IPV6_ONLY:
return entries.inet6Entries().get(inetHost);
case IPV4_PREFERRED:
Inet4Address inet4Address = entries.inet4Entries().get(inetHost);
return inet4Address != null? inet4Address : entries.inet6Entries().get(inetHost);
case IPV6_PREFERRED:
Inet6Address inet6Address = entries.inet6Entries().get(inetHost);
return inet6Address != null? inet6Address : entries.inet4Entries().get(inetHost);
default:
throw new IllegalArgumentException("Unknown ResolvedAddressTypes " + resolvedAddressTypes);
}
}
});
builder.channelFactory(channelFactory);
builder.nameServerProvider(nameServerAddressProvider);
builder.optResourceEnabled(options.isOptResourceEnabled());
builder.resolveCache(resolveCache);
builder.authoritativeDnsServerCache(authoritativeDnsServerCache);
builder.queryTimeoutMillis(options.getQueryTimeout());
builder.maxQueriesPerResolve(options.getMaxQueries());
builder.recursionDesired(options.getRdFlag());
// 必須配置了searchDomains,ndots才起作用
if (options.getSearchDomains() != null) {
builder.searchDomains(options.getSearchDomains());
int ndots = options.getNdots();
if (ndots == -1) {
ndots = AddressResolver.DEFAULT_NDOTS_RESOLV_OPTION;
}
builder.ndots(ndots);
}
return builder.build();
}
};
io.netty.resolver.AddressResolver<InetSocketAddress> resolver = group.getResolver(executor);
resolvers.add(new ResolverRegistration(resolver, (EventLoop) executor));
return resolver;
}
};
}
2.3 DefaultDnsServerAddressStreamProvider 讀取系統(tǒng)默認域名解析地址,使用static塊,利用jndi-DNS獲取域名地址列表
static {
final List<InetSocketAddress> defaultNameServers = new ArrayList<InetSocketAddress>(2);
// Using jndi-dns to obtain the default name servers.
//
// See:
// - http://docs.oracle.com/javase/8/docs/technotes/guides/jndi/jndi-dns.html
// - http://mail.openjdk.java.net/pipermail/net-dev/2017-March/010695.html
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
env.put("java.naming.provider.url", "dns://");
try {
DirContext ctx = new InitialDirContext(env);
// 獲取dnsUrl地址
String dnsUrls = (String) ctx.getEnvironment().get("java.naming.provider.url");
// Only try if not empty as otherwise we will produce an exception
if (dnsUrls != null && !dnsUrls.isEmpty()) {
String[] servers = dnsUrls.split(" ");
for (String server : servers) {
try {
URI uri = new URI(server);
String host = new URI(server).getHost();
if (host == null || host.isEmpty()) {
logger.debug(
"Skipping a nameserver URI as host portion could not be extracted: {}", server);
// If the host portion can not be parsed we should just skip this entry.
continue;
}
int port = uri.getPort();
defaultNameServers.add(SocketUtils.socketAddress(uri.getHost(), port == -1 ? DNS_PORT : port));
} catch (URISyntaxException e) {
logger.debug("Skipping a malformed nameserver URI: {}", server, e);
}
}
}
} catch (NamingException ignore) {
// Will try reflection if this fails.
}
if (defaultNameServers.isEmpty()) {
try {
Class<?> configClass = Class.forName("sun.net.dns.ResolverConfiguration");
Method open = configClass.getMethod("open");
Method nameservers = configClass.getMethod("nameservers");
Object instance = open.invoke(null);
@SuppressWarnings("unchecked")
final List<String> list = (List<String>) nameservers.invoke(instance);
for (String a: list) {
if (a != null) {
defaultNameServers.add(new InetSocketAddress(SocketUtils.addressByName(a), DNS_PORT));
}
}
} catch (Exception ignore) {
// Failed to get the system name server list via reflection.
// Will add the default name servers afterwards.
}
}
if (!defaultNameServers.isEmpty()) {
if (logger.isDebugEnabled()) {
logger.debug(
"Default DNS servers: {} (sun.net.dns.ResolverConfiguration)", defaultNameServers);
}
} else {
// Depending if IPv6 or IPv4 is used choose the correct DNS servers provided by google:
// https://developers.google.com/speed/public-dns/docs/using
// https://docs.oracle.com/javase/7/docs/api/java/net/doc-files/net-properties.html
if (NetUtil.isIpV6AddressesPreferred() ||
(NetUtil.LOCALHOST instanceof Inet6Address && !NetUtil.isIpV4StackPreferred())) {
Collections.addAll(
defaultNameServers,
SocketUtils.socketAddress("2001:4860:4860::8888", DNS_PORT),
SocketUtils.socketAddress("2001:4860:4860::8844", DNS_PORT));
} else {
Collections.addAll(
defaultNameServers,
SocketUtils.socketAddress("8.8.8.8", DNS_PORT),
SocketUtils.socketAddress("8.8.4.4", DNS_PORT));
}
if (logger.isWarnEnabled()) {
logger.warn(
"Default DNS servers: {} (Google Public DNS as a fallback)", defaultNameServers);
}
}
DEFAULT_NAME_SERVER_LIST = Collections.unmodifiableList(defaultNameServers);
DEFAULT_NAME_SERVER_ARRAY = defaultNameServers.toArray(new InetSocketAddress[defaultNameServers.size()]);
DEFAULT_NAME_SERVERS = sequential(DEFAULT_NAME_SERVER_ARRAY);
}
3、潛在的問題
3.1 jndi-DNS獲取的域名解析機制,底層使用native code方法,具體實現(xiàn)不詳。猜測Linux是直接獲取/etc/resolv.conf里面配置的地址,windows獲取網(wǎng)卡配置的域名地址。在windows下面,如果有多個網(wǎng)卡,并且有網(wǎng)卡是不通外網(wǎng)的。
3.2 DNS默認的searchDomain沒有配置,也沒有獲取系統(tǒng)的配置,單獨設置ndots是不生效的。