Photo @ Jude Beck
public void register(URL url) {
super.register(url);
failedRegistered.remove(url);
failedUnregistered.remove(url);
try {
// Sending a registration request to the server side
doRegister(url);
} catch (Exception e) {
Throwable t = e;
// If the startup detection is opened, the Exception is thrown directly.
boolean check = getUrl().getParameter(Constants.CHECK_KEY, true)
&& url.getParameter(Constants.CHECK_KEY, true)
&& !Constants.CONSUMER_PROTOCOL.equals(url.getProtocol());
boolean skipFailback = t instanceof SkipFailbackWrapperException;
if (check || skipFailback) {
if (skipFailback) {
t = t.getCause();
}
throw new IllegalStateException("Failed to register " + url + " to registry " + getUrl().getAddress() + ", cause: " + t.getMessage(), t);
} else {
logger.error("Failed to register " + url + ", waiting for retry, cause: " + t.getMessage(), t);
}
// Record a failed registration request to a failed list, retry regularly
failedRegistered.add(url);
}
}
在继续排查问题前,我们先普及下这些概念:Dubbo 默认使用 curator 作为ZooKeeper 的客户端, curator 与 ZooKeeper 是通过 session 维持连接的。当 curator 重连 ZooKeeper 时,若 session 未过期,则继续使用原 session 进行连接;若 session 已过期,则创建新 session 重新连接。而 Ephemeral 节点与 session 是绑定的关系,在 session 过期后,会删除此 session 下的 Ephemeral 节点。
public void createEphemeral(String path) {
try {
client.create().withMode(CreateMode.EPHEMERAL).forPath(path);
} catch (NodeExistsException e) {
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
但是实际上还有一种极端场景,ZooKeeper 的 Session 过期与删除 Ephemeral 节点不是原子性的,也就是说客户端在得到 Session 过期的消息时, Session 对应的 Ephemeral 节点可能还未被 ZooKeeper删除。此时 Dubbo 去创建 Ephemeral 节点,发现原节点仍存在,故不重新创建。待 Ephemeral 节点被 ZooKeeper 删除后,便会出现 Dubbo 认为重新注册成功,但实际未成功的情况,也就是我们在生产环境遇到的问题。
此时,问题的根源已被定位。定位问题之后,经我们与 Dubbo 社区交流,发现考拉的同学也遇到过同样的问题,更确定了这个原因。
定位到问题之后,我们便开始尝试本地复现。由于 ZooKeeper 的 Session 过期但Ephemeral节点未被删除的场景直接模拟比较困难,我们通过修改 zookeeper 源码,在 Session 过期与删除 Ephemeral 节点的逻辑中增加了一段休眠时间,间接模拟出这种极端场景,并在本地复现了此问题。
在排查问题的过程中,我们发现 kafka 的旧版本在使用 ZooKeeper 时也遇到过类似的问题,并参考 Kafka 关于此问题的修复方案,确定了 Dubbo 的修复方案。在创建 Ephemeral 节点捕获到 NodeExistsException 时进行判断,若 Ephemeral 节点的 SessionId 与当前客户端的 SessionId 不同,则删除并重建 Ephemeral 节点。在内部修复并验证通过后,我们向社区提交了 issues 及 pr 。
Kafka 类似问题 issues :
https://issues.apache.org/jira/browse/KAFKA-1387
Dubbo 注册恢复问题 issues:
https://github.com/apache/dubbo/issues/5125
1、统一 Dubbo 版本后,我们可以在此版本上内部紧急修复一些 Dubbo 问题(如上文的 Dubbo 注册故障恢复失效问题)。
2、瓜子目前正在进行第二机房的建设,部分 Dubbo 服务也在逐渐往第二机房迁移。统一 Dubbo 版本,也是为 Dubbo 的多机房做铺垫。
3、有利于我们后续对 Dubbo 服务的统一管控。
4、Dubbo 社区目前的发展方向与我们公司现阶段对Dubbo 的一些诉求相吻合,如支持 gRPC 、云原生等。
1、我们了解到,在我们之前携程已经与 Dubbo 社区合作进行了深度合作,携程内部已全量升级为 2.7.3 的社区版本,并在协助社区修复了 2.7.3 版本的一些兼容性问题。感谢携程的同学帮我们踩坑~
2、Dubbo2.7.3 版本在当时虽然是最新的版本,但已经发布了 2 个月的时间,从社区issues反馈来看,Dubbo2.7.3 相对 Dubbo2.7之前的几个版本,在兼容性方面要好很多。
3、我们也咨询了 Dubbo 社区的同学,推荐升级版本为 2.7.3 。
基于社区 Dubbo2.7.3 版本开发的 Dubbo 内部版本属于过渡性质的版本,目的是为了修复线上 provider 不能恢复注册的问题,以及一些社区 Dubbo2.7.3 的兼容性问题。瓜子的 Dubbo 最终还是要跟随社区的版本,而不是开发自已的内部功能。因此我们在 Dubbo 内部版本中修复的所有问题均与社区保持了同步,以保证后续可以兼容升级到社区 Dubbo 的更高版本。
兼容性验证与升级过程
我们在向 Dubbo 社区的同学咨询了版本升级方面的相关经验后,于 9 月下旬开始了 Dubbo 版本的升级工作。
1、初步兼容性验证
首先,我们梳理了一些需要验证的兼容性 case ,针对公司内部使用较多的dubbo版本,与 Dubbo2.7.3 一一进行了兼容性验证。经验证,除 DubboX 外, Dubbo2.7.3 与其他 Dubbo 版本均兼容。DubboX 由于对 Dubbo 协议进行了更改,与 Dubbo2.7.3 不兼容。
2、生产环境兼容性验证
在初步验证兼容性通过后,我们与业务线合作,挑选了一些重要程度较低的项目,在生产环境对 Dubbo2.7.3 与其他版本的兼容性进行了进一步验证。并在内部版本修复了一些兼容性问题。
3、推动公司 Dubbo 版本升级
在 10 月初,完成了 Dubbo 兼容性验证后,我们开始在各个业务线推动 Dubbo 的升级工作。截止到 12 月初,已经有 30% 的 Dubbo 服务的完成了版本升级。按照排期,预计于 2020 年 3 月底前完成公司 Dubbo 版本的统一升级。
兼容性问题汇总
在推动升级 Dubbo2.7.3 版本的过程整体上比较顺利,当然也遇到了一些兼容性问题:
1、创建 ZooKeeper 节点时提示没有权限
Dubbo 配置文件中已经配置了 ZooKeeper 的用户名密码,但在创建 ZooKeeper 节点时却抛出 KeeperErrorCode = NoAuth 的异常,这种情况分别对应两个兼容性问题:
a. issues:
https://github.com/apache/dubbo/issues/5076
dubbo在未配置配置中心时,默认使用注册中心作为配置中心。通过注册中心的配置信息初始化配置中心配置时,由于遗漏了用户名密码,导致此问题。
b. issues:
https://github.com/apache/dubbo/issues/4991
Dubbo 在建立与 ZooKeeper 的连接时会根据 ZooKeeper 的 address 复用之前已建立的连接。当多个注册中心使用同一个 address ,但权限不同时,就会出现 NoAuth 的问题。参考社区的 PR ,我们在内部版本进行了修复。
2、curator 版本兼容性问题
a. Dubbo2.7.3 与低版本的 curator 不兼容,因此我们默认将 curator 版本升级至4.2.0
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
b. 分布式调度框架 elastic-job-lite 强依赖低版本的 curator ,与 Dubbo2.7.3 使用的 curator 版本不兼容,这给 Dubbo 版本升级工作带来了一定阻塞。考虑到 elastic-job-lite 已经很久没有人进行维护,目前一些业务线计划将 elastic-job-lite 替换为其他的调度框架。
3、 OpenFeign 与 Dubbo 兼容性问题
issues:
https://github.com/apache/dubbo/issues/3990
Dubbo 的 ServiceBean 监听 Spring 的 ContextRefreshedEvent ,进行服务暴露。OpenFeign提前触发了 ContextRefreshedEvent ,此时 ServiceBean 还未完成初始化,于是就导致了应用启动异常。
参考社区的pr,我们在内部版本修复了此问题。
4、RpcException兼容性问题
Dubbo低版本consumer不能识别dubbo2.7版本provider抛出的org.apache.dubbo.rpc.RpcException。因此,在consumer全部升级到2.7之前,不建议将provider的com.alibaba.dubbo.rpc.RpcException改为org.apache.dubbo.rpc.RpcException
5、QoS 端口占用
Dubbo2.7.3 默认开启 QoS 功能,导致一些混部在物理机的 Dubbo 服务升级时出现 qos 端口占用问题。关闭 QoS 功能后恢复。
6、自定义扩展兼容性问题
业务线对于 Dubbo 的自定义扩展比较少,因此在自定义扩展的兼容性方面暂时还没有遇到比较难处理的问题,基本上都是变更 package 导致的问题,由业务线自行修复。
7、Skywalking agent 兼容性问题
我们项目中一般使 Skywalking进行链路追踪,由于Skywalking agent6.0 的 plugin 不支持 Dubbo2.7 ,因此统一升级 Skywalking agent 到 6.1 。
瓜子目前正在进行第二机房的建设工作,Dubbo 多机房是第二机房建设中比较重要的一个话题。在 Dubbo 版本统一的前提下,我们就能够更顺利的开展 Dubbo 多机房相关的调研与开发工作。
我们咨询了 Dubbo 社区的建议,并结合瓜子云平台的现状,初步确定了 Dubbo 多机房的方案。
1、在每个机房内,部署一套独立的 ZooKeeper 集群。集群间信息不同步。这样就没有了 ZooKeeper 集群跨机房延迟与数据不同步的问题。
2、Dubbo 服务注册时,仅注册到本机房的 ZooKeeper 集群;订阅时,同时订阅两个机房的 ZooKeeper 集群。
3、实现同机房优先调用的路由逻辑。以减少跨机房调用导致的不必要网络延迟。
Dubbo 同机房优先调用的实现比较简单,相关逻辑如下:
1、瓜子云平台默认将机房的标志信息注入容器的环境变量中。
2、 provider 暴露服务时,读取环境变量中的机房标志信息,追加到待暴露服务的url中。
3、 consumer 调用 provider 时,读取环境变量中的机房标志信息,根据路由策略优先调用具有相同标志信息的 provider 。
针对以上逻辑,我们简单实现了 Dubbo 通过环境变量进行路由的功能,并向社区提交了 PR 。
Dubbo 通过环境变量路由 PR :
https://github.com/apache/dubbo/pull/5348
本文缩略图:icon by 泉子Zq_222
Tips:
# 点下“在看”❤️
# 然后,公众号对话框内发送“家书”,试试手气?😆
# 本期奖品是阿里家书一套。