場景介紹
今天在Docker上部署服務時,啟動都很成功,但是訪問時卻訪問失敗。之前在本地啟動、在測試服k8s上啟動都很正常,為什么同樣的代碼、同樣的docker鏡像在docker上卻有問題呢?真讓人摸不著頭腦。
服務是部署在docker集群上,因為是部署測試,就準備了3臺機器,由3臺機器組成的一個集群。服務是按業務劃分,分別寫在不同的docker-compose.yml文件中,最后通過docker stack 啟動。
dubbo的注冊中心使用的是nacos。
因為在本地測試正常,而且也在k8s上部署且訪問正常,所以在docker環境啟動也很順利。唯一不行的就是docker環境部署后部分接口訪問報錯。
過程排查
在接口訪問失敗后,立刻查看了服務日志,報錯是dubbo接口調用超時。錯誤如下:
org.Apache.dubbo.rpc.RpcException:
Failed to invoke the method getExportedURLs in the service org.apache.dubbo.rpc.service.GenericService.
Tried 1 times of the providers [172.18.0.3:20881] (1/1) from the registry 172.10.36.101:8848 on the consumer 172.19.0.7 using the dubbo version 2.7.17.
。。。。。省略一大堆。。。。。
error message is:Host is unreachable: /172.18.0.3:20881
復制代碼
從報錯日志來看,有個明顯的錯誤是Host is unreachable。現在是服務C調用服務A的dubbo接口調用不到,用我蹩腳的英文理解剛才錯誤信息是主機不可達。
此時,有兩個疑問出現在容量不夠大的腦子里:
- 為什么服務C調用不了服務A?
- 172.18.0.3這個ip是什么東東,哪里來的。
針對問題1,首先想到的是服務A沒有注冊到nacos里去,打開nacos控制臺發現服務A是已經注冊上去了的,并且發現注冊的ip是172.18.0.3,剛好和問題2里的ip一致。
既然服務已經正常注冊,那就剩下172.18.0.3這個ip是哪里來的了。通過docker exec命令進入服務A容器,使用命名ifconfig看下服務A的ip信息:
找到了,是服務A容器的一個網卡地址,不過這個容器怎么網卡?難道是因為多個網卡導致的嗎?那為什么部署在k8s容器里沒有問題?難道k8s里面沒有多個網卡嗎?又接著一連串的問號在腦海里出現了?
先看看,k8s里的ip信息吧。通過kubectl exec登錄容器, 查看ip信息ip addr。嗯?只有兩個,比docker里少了好多。
看來是,dubbo注冊的時候,選擇網卡的時候,是有一定的機制的,選擇的不是我想要的。看看源碼吧,到底是怎么選擇的。
源碼分析
Dubbo獲取網卡地址的邏輯是在
org.apache.dubbo.common.utils.NETUtils類中getLocalAddress0方法。
private static InetAddress getLocalAddress0() {
InetAddress localAddress = null;
// @since 2.7.6, choose the {@link NetworkInterface} first
try {
NetworkInterface networkInterface = findNetworkInterface();
Enumeration<InetAddress> addresses = networkInterface.getInetAddresses();
while (addresses.hasMoreElements()) {
Optional<InetAddress> addressOp = toValidAddress(addresses.nextElement());
if (addressOp.isPresent()) {
try {
if (addressOp.get().isReachable(100)) {
return addressOp.get();
}
} catch (IOException e) {
// ignore
}
}
}
} catch (Throwable e) {
logger.warn(e);
}
try {
localAddress = InetAddress.getLocalHost();
Optional<InetAddress> addressOp = toValidAddress(localAddress);
if (addressOp.isPresent()) {
return addressOp.get();
}
} catch (Throwable e) {
logger.warn(e);
}
return localAddress;
}
復制代碼
這塊代碼的整體邏輯還是很好理解的:
- 查找所有的網卡
- 校驗網卡對應的ip是否合適
- 如果找不到合適的ip,則設置ip為127.0.0.1
再看下Dubbo是如何校驗ip是否合適的呢?對應方法為toValidAddress
private static Optional<InetAddress> toValidAddress(InetAddress address) {
if (address instanceof Inet6Address) {
Inet6Address v6Address = (Inet6Address) address;
if (isPreferIPV6Address()) {
return Optional.ofNullable(normalizeV6Address(v6Address));
}
}
if (isValidV4Address(address)) {
return Optional.of(address);
}
return Optional.empty();
}
復制代碼
先判斷拿到的地址是否為ipv6,再看是否設置了優先選擇ipv6,即有沒有配置
JAVA.net.preferIPv6Addresses=true,我們項目配置的是java.net.preferIPv4Stack=true,所以走的是下面的邏輯。再檢測是否是合法的ipv4地址,拿到ip后,檢查ip的網速,如果響應時間為100ms內,則把這個ip作為注冊ip。
知道了Dubbo是如何選擇網卡的了,但是好像對我們沒有太大幫助,我總不能限制網卡的網速去吧?
最好的辦法還是,我是否可以設置?再看一遍源碼是不是遺漏了什么。看下是如何選擇網卡的:
public static NetworkInterface findNetworkInterface() {
List<NetworkInterface> validNetworkInterfaces = emptyList();
try {
// 尋找合適的網卡
validNetworkInterfaces = getValidNetworkInterfaces();
} catch (Throwable e) {
logger.warn(e);
}
NetworkInterface result = null;
// Try to find the preferred one
for (NetworkInterface networkInterface : validNetworkInterfaces) {
// 是否為優選的網卡
if (isPreferredNetworkInterface(networkInterface)) {
result = networkInterface;
break;
}
}
if (result == null) { // If not found, try to get the first one
for (NetworkInterface networkInterface : validNetworkInterfaces) {
Enumeration<InetAddress> addresses = networkInterface.getInetAddresses();
while (addresses.hasMoreElements()) {
Optional<InetAddress> addressOp = toValidAddress(addresses.nextElement());
if (addressOp.isPresent()) {
try {
if (addressOp.get().isReachable(100)) {
return networkInterface;
}
} catch (IOException e) {
// ignore
}
}
}
}
}
if (result == null) {
result = first(validNetworkInterfaces);
}
return result;
}
復制代碼
這方法也分為幾步,也是很好理解的:
- 查找所有合適的網卡
- 在查找的網卡里遍歷一遍,看是否有優先設置的
- 如果沒有,選擇一個網速100毫秒響應的
- 如果還沒有,則返回第一個網卡
第一步,查找所有合適的網卡,怎么算合適的呢?getValidNetworkInterfaces方法里判斷只要不是被忽略的網卡就是合適的,里面的代碼就不細看了,大致邏輯是,通過參數
dubbo.network.interface.ignored可以設置哪些網卡被忽略,如果忽略多個可以用逗號拼接。例如dubbo.network.interface.ignored=eth0,eth1。這個參數好像可以滿足我們的需求。
第二步,查找網卡是否有被我們優先設置的。
isPreferredNetworkInterface方法我們看下:
public static boolean isPreferredNetworkInterface(NetworkInterface networkInterface) {
// dubbo.network.interface.preferred
String preferredNetworkInterface = System.getProperty(DUBBO_PREFERRED_NETWORK_INTERFACE);
return Objects.equals(networkInterface.getDisplayName(), preferredNetworkInterface);
}
復制代碼
可以看到,我們可以通過
DUBBO_PREFERRED_NETWORK_INTERFACE這個參數,也就dubbo.network.interface.preferred來指定網卡。例如:dubbo.network.interface.preferred=eth0。
看到這里,我們就明白了,至少我們可以通過排除網卡或者設置網卡來讓Dubbo選擇合適的ip去注冊。因為docker容器里,不一定有多少個確定的網卡,還是指定網卡比較保險。
問題解決
好了,知道如何讓Dubbo來選擇網卡了,我們只要找到各個容器里在同一網段的網卡就好了。
于是,登錄到各個docker容器里,查看ip信息。對比了一下,發現eth1這個ip的網段都是10.10.x.x網段的。
配置環境變量信息
dubbo.network.interface.preferred=eth1,重啟服務,然后訪問接口,果然通了。
大功告成!
后記
其實,還有其他辦法來解決問題,比如:protocol配置host信息、設置 DUBBO_IP_TO_REGISTRY和DUBBO_IP_TO_BIND等。這里就不展開說明了,有興趣的小伙伴可以自己試一試。