共计 5590 个字符,预计需要花费 14 分钟才能阅读完成。
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。
: