Dubbo介绍 dubbo.apache.org/zh/docs/int…
Dubbo是一款高性能、轻量级的开源RPC框架、深受大部分互联网企业青睐,我司Dubbo使用ZooKeeper作为注册中心,在生产实践过程中,因ZooKeeper服务的稳定性导致RPC服务出现异常问题,今天来分析为什么ZooKeeper宕机会导致Dubbo异常。
问题现象
ZooKeeper因OOM发生了一次异常宕机,宕机后出现大面积服务不可用,Consumer在调用Provider时出现NoProvider异常,异常信息如下:
rpcInvokeFailed error: com.alibaba.dubbo.rpc.RpcException: No provider available from registry xxx:2181 for service com.xxx.interface:PROD on consumer 172.27.53.211 use dubbo version 2.6.2, please check status of providers(disabled, not registered or in blacklist).
原因分析
我们知道Dubbo的Consumer是有服务端地址的缓存列表的,ZooKeeper宕机后,按理说本地内存有服务端列表信息,为什么会出现无服务可用呢?Dubbo本身有推空保护机制为何不起作用?
dubbo的服务注册发现原理
服务注册比较简单,直接向ZooKeeper创建临时节点
public class ZookeeperRegistry extends FailbackRegistry {
@Override
protected void doRegister(URL url) {
try {
zkClient.create(toUrlPath(url), url.getParameter(Constants.DYNAMIC_KEY, true));//true标识临时节点
} catch (Throwable e) {
throw new RpcException("Failed to register " + url + " to zookeeper " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
}
Consumer在Watch到Provider节点发生变化后处理逻辑
这里的client.getChildren().usingWatcher(this).forPath(path)方法是根据url的父节点为path如/dubbo/xxxxIntefrace/providers,获取其下面的所有子节点,只要子节点变化都会重新获取一遍,当一个应用的dubbo接口数量多,Provider和Consumer实例多很多的情况,这对ZooKeeper的压力也比较大,这个大家要注意。 下图是一个应用服务和实例较多在发布的情况下,ZooKeeper某个节点的网络IO骤升
原因一
如果Provider与ZooKeeper先断开连接,异常断开非正常关闭的情况下,此时ZooKeeper的临时节点不会立即删除,而是在Session过期后删除临时节点,Consumer在接收到服务端节点变更信息后会重新获取注册列表,从客户端视角来看,不知道是Provider主动下线还是异常下线,所以直接从ZooKeeper重新拉取服务节点后更新缓存。
原因二
设Provider和ZooKeeper的初始连接为Connection1,在Connection1异常断开连接后,ZooKeeper的临时节点不会立即删除,直到Connection1的Session过期才会删除。若在Connection1的Session过期之前,Provider会与ZooKeeper重新连接为Connection2,连接成功后向ZooKeeper注册临时节点,因为Connection1创建的url还存在,所以此时会报NodeExistException异常,导致Connection2连接成功,但未能真正创建临时节点,最终现象是Provider与ZooKeeper连接正常,但是ZooKeeper却没有Provider的注册信息。
为什么Dubbo推空保护未起作用?首先看一下Consumer订阅逻辑
Consumer在监听到变化后拉取所有服务的子节点并更新缓存。
@Override
protected void doSubscribe(final URL url, final NotifyListener listener) {
//...
listeners.putIfAbsent(listener, new ChildListener() {
@Override
public void childChanged(String parentPath, List<String> currentChilds) {
ZookeeperRegistry.this.notify(url, listener, toUrlsWithEmpty(url, parentPath, currentChilds));
}
});
//...
}
推空保护逻辑
public abstract class AbstractRegistry implements Registry {
protected void notify(URL url, NotifyListener listener, List<URL> urls) {
//...
Map<String, List<URL>> result = new HashMap<String, List<URL>>();
for (URL u : urls) {
if (UrlUtils.isMatch(url, u)) {
String category = u.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
List<URL> categoryList = result.get(category);
if (categoryList == null) {
categoryList = new ArrayList<URL>();
result.put(category, categoryList);
}
categoryList.add(u);
}
}
if (result.size() == 0) {//推空保护处理
return;
}
Map<String, List<URL>> categoryNotified = notified.get(url);
if (categoryNotified == null) {
notified.putIfAbsent(url, new ConcurrentHashMap<String, List<URL>>());
categoryNotified = notified.get(url);
}
for (Map.Entry<String, List<URL>> entry : result.entrySet()) {
String category = entry.getKey();
List<URL> categoryList = entry.getValue();
categoryNotified.put(category, categoryList);//更新缓存
saveProperties(url);
listener.notify(categoryList);
}
}
}
重点提一下toUrlsWithEmpty方法,这个方法在处理url转换的时候,如果Provider列表为空,则默认会加一个Empty的URL,因此上面的result.size()永远不会为0,这也是为什么推空保护未生效的直接原因。
解决方法
解决方法很简单,直接重启ZooKeeper所有节点即可,让Provider和ZooKeeper主动断开连接,因为ZooKeeper正常关闭连接,Provider会接进行重新连接,重新连接后会向ZooKeeper重新注册节点信息。
public class ZookeeperRegistry extends FailbackRegistry {
public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
//...
zkClient.addStateListener(new StateListener() {
@Override
public void stateChanged(int state) {
if (state == RECONNECTED) {
try {
recover();//zk重新连接成功后,恢复注册和订阅
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
});
}
}
@Override
protected void recover() throws Exception {
// register 重新注册
Set<URL> recoverRegistered = new HashSet<URL>(getRegistered());
if (!recoverRegistered.isEmpty()) {
if (logger.isInfoEnabled()) {
logger.info("Recover register url " + recoverRegistered);
}
for (URL url : recoverRegistered) {
failedRegistered.add(url);
}
}
}