Dubbo應用遷移到docker的問題
Dubbo是阿里開源的一套服務治理與rpc框架,服務的提供者通過zookeeper把自己的服務發佈上去,然後服務調用方通過zk獲取服務的ip和端口,dubbo客戶端通過自己的軟負載功能自動選擇服務提供者並調用,整個過程牽涉到的三方關係如下圖所示。
在正常的情況下,這三方都在同一個互通的網段,provider提供給zk的就是獲取到的本機地址,consumer能訪問到這個地址。
但是假如服務放在docker容器中,而調用者並不在docker中,它們的網段是不一樣的。
這個時候就出現問題了,consumer無法訪問到provider了。
Dubbo提供的解決方案
新版的Dubbo提供了四個配置來指定與註冊服務相關的地址和端口。
DUBBO_IP_TO_REGISTRY: 要發佈到註冊中心上的地址
DUBBO_PORT_TO_REGISTRY: 要發佈到註冊中心上的端口
DUBBO_IP_TO_BIND: 要綁定的服務地址(監聽的地址)
DUBBO_PORT_TO_BIND: 要綁定的服務端口
以IP地址爲例,Dubbo先找是不是有DUBBO_IP_TO_BIND這個配置,如果有使用配置的地址,如果沒有就取本機地址。然後繼續找DUBBO_IP_TO_REGISTRY,如果有了配置,使用配置,否則就使用DUBBO_IP_TO_BIND。具體代碼如下:
/**
* Register & bind IP address for service provider, can be configured separately.
* Configuration priority: environment variables -> java system properties -> host property in config file ->
* /etc/hosts -> default network address -> first available network address
*
* @param protocolConfig
* @param registryURLs
* @param map
* @return
*/
private static String findConfigedHosts(ServiceConfig<?> sc,
ProtocolConfig protocolConfig,
List<URL> registryURLs,
Map<String, String> map) {
boolean anyhost = false;
String hostToBind = getValueFromConfig(protocolConfig, DUBBO_IP_TO_BIND);
if (hostToBind != null && hostToBind.length() > 0 && isInvalidLocalHost(hostToBind)) {
throw new IllegalArgumentException("Specified invalid bind ip from property:" + DUBBO_IP_TO_BIND + ", value:" + hostToBind);
}
// if bind ip is not found in environment, keep looking up
if (StringUtils.isEmpty(hostToBind)) {
hostToBind = protocolConfig.getHost();
if (sc.getProvider() != null && StringUtils.isEmpty(hostToBind)) {
hostToBind = sc.getProvider().getHost();
}
if (isInvalidLocalHost(hostToBind)) {
anyhost = true;
try {
logger.info("No valid ip found from environment, try to find valid host from DNS.");
hostToBind = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
logger.warn(e.getMessage(), e);
}
if (isInvalidLocalHost(hostToBind)) {
if (CollectionUtils.isNotEmpty(registryURLs)) {
for (URL registryURL : registryURLs) {
if (MULTICAST.equalsIgnoreCase(registryURL.getParameter("registry"))) {
// skip multicast registry since we cannot connect to it via Socket
continue;
}
try (Socket socket = new Socket()) {
SocketAddress addr = new InetSocketAddress(registryURL.getHost(), registryURL.getPort());
socket.connect(addr, 1000);
hostToBind = socket.getLocalAddress().getHostAddress();
break;
} catch (Exception e) {
logger.warn(e.getMessage(), e);
}
}
}
if (isInvalidLocalHost(hostToBind)) {
hostToBind = getLocalHost();
}
}
}
}
map.put(BIND_IP_KEY, hostToBind);
// registry ip is not used for bind ip by default
String hostToRegistry = getValueFromConfig(protocolConfig, DUBBO_IP_TO_REGISTRY);
if (hostToRegistry != null && hostToRegistry.length() > 0 && isInvalidLocalHost(hostToRegistry)) {
throw new IllegalArgumentException("Specified invalid registry ip from property:" + DUBBO_IP_TO_REGISTRY + ", value:" + hostToRegistry);
} else if (StringUtils.isEmpty(hostToRegistry)) {
// bind ip is used as registry ip by default
hostToRegistry = hostToBind;
}
map.put(ANYHOST_KEY, String.valueOf(anyhost));
return hostToRegistry;
}
然後我們看這個getValueFromConfig(),它調用了下面的函數,可以看到,它是先找環境變量,再找properties。
public static String getSystemProperty(String key) {
String value = System.getenv(key);
if (StringUtils.isEmpty(value)) {
value = System.getProperty(key);
}
return value;
}
所以我們通過環境變量,就能修改Dubbo發佈到zookeeper上的地址和端口。假如我們通過docker鏡像啓動了一個dubbo provider,並且它的服務端口是8888,假設主機地址爲192.168.1.10,那麼我們通過下面的命令,
docker run -e DUBBO_IP_TO_REGISTRY=192.168.1.10 -e DUBBO_PORT_TO_REGISTRY=8888 -p 8888:8888 dubbo_image
就能讓內部的服務以192.168.1.10:8888的地址發佈。
我們通過官方的實例來演示一下,因爲官方提供的案例都很久了,所以我自己重新搞了一個示例,代碼在https://github.com/XinliNiu/dubbo-docker-sample.git 。
先啓動一個zookeeper,暴露2181端口。
docker run --name zkserver --rm -p 2181:2181 -d zookeeper:3.4.9
看一下zk起來了
niuxinli@niuxinli-B450M-DS3H:~/dubbo-samples-docker$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5efc1f17fba0 zookeeper:3.4.9 "/docker-entrypoint.…" 4 seconds ago Up 2 seconds 2888/tcp, 3888/tcp, 0.0.0.0:2181->2181/tcp zkserver
把代碼導入IDE,修改dubbo-docker-provide.xml,把地址改成剛發佈到zk的地址和端口,我的地址是192.168.1.8。
運行DubboApplication,這時候可以看到在zk上註冊了服務。
修改dubbo-docker-consumer.xml裏的zk地址,執行單元測試,能正常訪問。
把DubboApplication導出成可以執行的jar包,名字叫app.jar,創建如下Dockerfile
FROM openjdk:8-jdk-alpine
ADD app.jar app.jar
ENV JAVA_OPTS=""
ENTRYPOINT exec java $JAVA_OPTS -jar /app.jar
創建dubbo-demo鏡像,在同樣的目錄裏執行docker build。
docker build --no-cache -t dubbo-demo .
正常啓動鏡像
docker run -p 20880:20880 -it --rm dubbo-demo
發現是172.16.0.3的地址,這個是訪問不了的。
傳入環境變量重新啓動,
docker run -e DUBBO_IP_TO_REGISTRY=192.168.1.8 -e DUBBO_PORT_TO_REGISTRY=20880 -p 20880:20880 -it --rm dubbo-demo
這時候就變成主機地址了。
在Kubernetes中使用Dubbo
當在Kubernetes中啓動多個副本的時候,指定具體的IP和具體的端口,都是不可行的,因爲每個機器的IP都不一樣,不能寫很多個yaml文件,而且一旦指定了具體端口,那這臺主機的這個端口就被佔用了。
我們可以通過創建Service,使用NodePort的方式,把端口固定住,這樣端口的問題就解決了。因爲是對外服務,所以使用ClusterIP肯定是不行了,IP有兩種解決辦法:
(1)使用Kubernetes的downward api動態的傳入主機的ip。
(2)傳固定的loadbalancer的地址,例如在所有的node之外有一個F5。
不管哪種方法,都是一種妥協的辦法,很不“雲原生”,我演示一下使用downward api動態傳入主機地址,並使用nodeport固定端口的方式。
我的kubernetes集羣如下:
角色 | 地址 |
---|---|
master | 192.168.174.50 |
node1 | 192.168.174.51 |
node2 | 192.168.174.52 |
node3 | 192.168.174.53 |
zk的地址是192.168.1.8,它與集羣的主機互通。
我沒有建private鏡像倉庫,把我之前打好的dubbo-demo直接push到docker-hub上了,名字是nxlhero/dubbo-demo。
創建Service,使用的NodePort爲30001,創建4個副本,這樣3臺機器上正好有一臺起兩個pod。
apiVersion: v1
kind: Service
metadata:
name: dubbo-docker
labels:
run: dubbo
spec:
type: NodePort
ports:
- port: 20880
targetPort: 20880
nodePort: 30001
selector:
run: dubbo-docker
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: dubbo-docker
spec:
selector:
matchLabels:
run: dubbo
replicas: 4
template:
metadata:
labels:
run: dubbo
spec:
containers:
- name: dubbo-docker
image: nxlhero/dubbo-demo
env:
- name: DUBBO_IP_TO_REGISTRY
valueFrom:
fieldRef:
fieldPath: status.hostIP
- name: DUBBO_PORT_TO_REGISTRY
value: "30001"
tty: true
ports:
- containerPort: 20880
這個yaml最關鍵的地方就是環境變量,主機IP通過downward apid傳入,端口使用固定的nodeport。
env:
- name: DUBBO_IP_TO_REGISTRY
valueFrom:
fieldRef:
fieldPath: status.hostIP
- name: DUBBO_PORT_TO_REGISTRY
value: "30001"
創建Service,啓動後可以看到zookeeper上的地址都是主機的地址和nodeport。