随着瓜子业务的不断发展,系统规模在逐渐扩大,目前在瓜子的私有云上已经运行着数百个 Dubbo 应用,上千个 Dubbo 实例。瓜子各部门业务迅速发展,版本没有来得及统一,各个部门都有本身的用法。随着第二机房的建设,Dubbo 版本统一的需求变得愈加迫切。几个月前,公司发生了一次与 Dubbo 相关的生产事故,成为了公司 基于社区 Dubbo 2.7.3 版本升级的诱因。git
接下来,我会从此次线上事故开始,讲讲咱们这段时间所作的 Dubbo 版本升级的历程以及咱们规划的 Dubbo 后续多机房的方案。github
事故背景spring
在生产环境,瓜子内部各业务线共用一套zookeeper集群做为dubbo的注册中心。2019年9月份,机房的一台交换机发生故障,致使zookeeper集群出现了几分钟的网络波动。在zookeeper集群恢复后,正常状况下dubbo的provider应该会很快从新注册到zookeeper上,但有一小部分的provider很长一段时间没有从新注册到zookeeper上,直到手动重启应用后才恢复注册。apache
排查过程网络
首先,咱们统计了出现这种现象的dubbo服务的版本分布状况,发如今大多数的dubbo版本中都存在这种问题,且发生问题的服务比例相对较低,在github中咱们也未找到相关问题的issues。所以,推断这是一个还没有修复的且在网络波动状况的场景下偶现的问题。session
接着,咱们便将出现问题的应用日志、zookeeper日志与dubbo代码逻辑进行相互印证。在应用日志中,应用重连zookeeper成功后provider马上进行了从新注册,以后便没有任何日志打印。而在zookeeper日志中,注册节点被删除后,并无从新建立注册节点。对应到dubbo的代码中,只有在FailbackRegistry.register(url)
的doRegister(url)
执行成功或线程被挂起的状况下,才能与日志中的状况相吻合。架构
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节点。app
继续对doRegister(url)
的代码进行进一步排查,咱们发如今CuratorZookeeperClient.createEphemeral(path)
方法中有这么一段逻辑:在createEphemeral(path)
捕获了NodeExistsException
,建立ephemeral节点时,若此节点已存在,则认为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
上文中的问题修复方案已经肯定,但咱们显然不可能在每个dubbo版本上都进行修复。在咨询了社区dubbo的推荐版本后,咱们决定在dubbo2.7.3版本的基础上,开发内部版本修复来这个问题。并借这个机会,开始推进公司dubbo版本的统一升级工做。
为何要统一dubbo版本
为何选择dubbo2.7.3
内部版本定位
基于社区dubbo2.7.3版本开发的dubbo内部版本属于过渡性质的版本,目的是为了修复线上provider不能恢复注册的问题,以及一些社区dubbo2.7.3的兼容性问题。瓜子的dubbo最终仍是要跟随社区的版本,而不是开发自已的内部功能。所以咱们在dubbo内部版本中修复的全部问题均与社区保持了同步,以保证后续能够兼容升级到社区dubbo的更高版本。
兼容性验证与升级过程
咱们在向dubbo社区的同窗咨询了版本升级方面的相关经验后,于9月下旬开始了dubbo版本的升级工做。
兼容性问题汇总
在推进升级dubbo2.7.3版本的过程总体上比较顺利,固然也遇到了一些兼容性问题:
建立zookeeper节点时提示没有权限
dubbo配置文件中已经配置了zookeeper的用户名密码,但在建立zookeeper节点时却抛出KeeperErrorCode = NoAuth
的异常,这种状况分别对应两个兼容性问题:
dubbo在创建与zookeeper的链接时会根据zookeeper的address复用以前已创建的链接。当多个注册中心使用同一个address,但权限不一样时,就会出现NoAuth
的问题。
参考社区的pr,咱们在内部版本进行了修复。
curator版本兼容性问题
<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>
* 分布式调度框架elastic-job-lite强依赖低版本的curator,与dubbo2.7.3使用的curator版本不兼容,这给dubbo版本升级工做带来了必定阻塞。考虑到elastic-job-lite已经好久没有人进行维护,目前一些业务线计划将elastic-job-lite替换为其余的调度框架。
dubbo的ServiceBean监听spring的ContextRefreshedEvent,进行服务暴露。openFeign提早触发了ContextRefreshedEvent,此时ServiceBean还未完成初始化,因而就致使了应用启动异常。
参考社区的pr,咱们在内部版本修复了此问题。
org.apache.dubbo.rpc.RpcException
。所以,在consumer所有升级到2.7以前,不建议将provider的com.alibaba.dubbo.rpc.RpcException
改成org.apache.dubbo.rpc.RpcException
瓜子目前正在进行第二机房的建设工做,dubbo多机房是第二机房建设中比较重要的一个话题。在dubbo版本统一的前提下,咱们就可以更顺利的开展dubbo多机房相关的调研与开发工做。
初步方案
咱们咨询了dubbo社区的建议,并结合瓜子云平台的现状,初步肯定了dubbo多机房的方案。
同机房优先调用
dubbo同机房优先调用的实现比较简单,相关逻辑以下:
针对以上逻辑,咱们简单实现了dubbo经过环境变量进行路由的功能,并向社区提交了pr。
dubbo经过环境变量路由pr: https://github.com/apache/dubbo/pull/5348
本文做者:李锦涛,任职于瓜子二手车基础架构部门,负责瓜子微服务架构相关工做。目前主要负责公司内 Dubbo 版本升级与推广、 Skywalking 推广工做。
本文做者:李锦涛
本文为阿里云内容,未经容许不得转载。