为什么ZooKeeper宕机会引发Dubbo服务出现NoProvider异常?

1,198 阅读4分钟

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节点发生变化后处理逻辑

image.png

这里的client.getChildren().usingWatcher(this).forPath(path)方法是根据url的父节点为path如/dubbo/xxxxIntefrace/providers,获取其下面的所有子节点,只要子节点变化都会重新获取一遍,当一个应用的dubbo接口数量多,Provider和Consumer实例多很多的情况,这对ZooKeeper的压力也比较大,这个大家要注意。 下图是一个应用服务和实例较多在发布的情况下,ZooKeeper某个节点的网络IO骤升

image.png

原因一

如果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的注册信息。

image.png

为什么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,这也是为什么推空保护未生效的直接原因。

image.png

解决方法

解决方法很简单,直接重启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);
        }
    }
}