环境:dubbo-2.6.5(公司内部版本),注册中心:zookeeper
生产服务有两个提供者,突然发现目前仅剩下了一个节点,登录丢失的节点,查看服务是正常的,而且没有任何error日志。
查看服务的应用日志,发现服务没有任何实际的调用,也就是说,消费者已经真的看不到该提供者,故不成立。
查看日志定位到服务丢失的时间点。发现当天出现zk出现了大量的超时,原因是当天的zk主节点宕机了。
查看zk下的提供者节点确实仅剩下了一个提供者节点数据。问题已定位那么原因是什么呢?
问题是否出现在了dubbo对zk重连恢复数据这块,开始查源码。注册中心源码ZookeeperRegistry。
public class ZookeeperRegistry extends FailbackRegistry {
public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
...
// 1. 连接zk
zkClient = zookeeperTransporter.connect(url);
// 2. 添加zk状态监听
zkClient.addStateListener(new StateListener() {
@Override
public void stateChanged(int state) {
// 3. 重新连接后恢复动作,将当前的注册服务于订阅任务添加至重试列表中等待重试
if (state == RECONNECTED) {
try {
recover();
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
});
}
}
public class CuratorZookeeperClient extends AbstractZookeeperClient<CuratorWatcher> {
public CuratorZookeeperClient(URL url) {
...
client = builder.build();
// dubbo对接zk连接状态监听器
client.getConnectionStateListenable().addListener(new ConnectionStateListener() {
@Override
public void stateChanged(CuratorFramework client, ConnectionState state) {
if (state == ConnectionState.LOST) {
CuratorZookeeperClient.this.stateChanged(StateListener.DISCONNECTED);
} else if (state == ConnectionState.CONNECTED) {
CuratorZookeeperClient.this.stateChanged(StateListener.CONNECTED);
} else if (state == ConnectionState.RECONNECTED) {
CuratorZookeeperClient.this.stateChanged(StateListener.RECONNECTED);
}
}
});
client.start();
...
}
}
public FailbackRegistry(URL url) {
super(url);
this.retryPeriod = url.getParameter(Constants.REGISTRY_RETRY_PERIOD_KEY, Constants.DEFAULT_REGISTRY_RETRY_PERIOD);
this.retryFuture = retryExecutor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
// Check and connect to the registry
try {
// failedRegistered失败注册重试,failedUnregistered失败注销重试,failedSubscribed失败订阅重试,failedUnsubscribed失败取消订阅重试,failedNotified失败通知重试
retry();
} catch (Throwable t) { // Defensive fault tolerance
logger.error("Unexpected error occur at failed retry, cause: " + t.getMessage(), t);
}
}
}, retryPeriod, retryPeriod, TimeUnit.MILLISECONDS);
}
通过三部分的代码我们可以推断,如果zk状态监听与恢复部分出现问题可能会导致数据丢失问题。于是查看相关的api并且尝试查看dubbo社区的问题与bug,果然发现了类似问题的修改与原因分析:https://github.com/apache/dubbo/pull/5135
问题的原因已经在代码的注释中说明,大体含义:如果ZNode数据已经存在,在会话超时期间,此时我们将重建一个数据节点,这个重复的异常原因可能是由于zk server中老的超时会话依然持有节点导致该节点的delete删除事件延迟,并且zk server还没有来得及去执行删除,可能由这种场景引起。在这个情景下,我们可以本地删除节点后再创建恢复节点数据。
个人理解是:如果会话断开连接又重新连接成功。断开连接发出的删除节点事件,因为延迟原因走在了重新连接恢复节点事件的后面。导致重新连接后没能成功恢复节点。也就是我么见到的,provider有一个节点服务正常,但是zk注册中心中的提供者节点数据丢失,导致出现该节点对其他订阅者不可见的现象
@Override
protected void createEphemeral(String path, String data) {
byte[] dataBytes = data.getBytes(CHARSET);
try {
client.create().withMode(CreateMode.EPHEMERAL).forPath(path, dataBytes);
} catch (NodeExistsException e) {
logger.warn("ZNode " + path + " already exists, since we will only try to recreate a node on a session expiration" +
", this duplication might be caused by a delete delay from the zk server, which means the old expired session" +
" may still holds this ZNode and the server just hasn't got time to do the deletion. In this case, " +
"we can just try to delete and create again.", e);
deletePath(path);
createEphemeral(path, data);
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}