昨天晚上,线上出了一个Spring Cloud Alibaba的Bug,我排查了一夜,终于,找到,了,原因,也,解决,了,问题。今天,就,来,分享,一下,这个,Bug,的,排查,过程,和,解决方案,希望,能,给,正在,使用,Spring Cloud Alibaba,的,朋友,一些,参考,和,帮助,避免,踩,同样,的,坑。

我们,的,项目,用,的,是,Spring Cloud Alibaba,的,技术栈,包括,Nacos,做,服务,注册,和,配置,中心,Sentinel,做,限流,和,熔断,Dubbo,做,RPC,调用,等等,用,了,一段时间,一直,都,比较,稳定,但是,昨天,晚上,突然,出,了,一个,奇怪,的,问题,服务,调用,偶尔,会,失败,而且,没有,明显,的,报错,日志,排查,起来,很,麻烦,我,排查,了,一夜,才,找到,原因。

今天,这篇,文章,就,来,详细,聊聊,这个,Bug,的,排查,过程,和,解决方案。

一、问题是怎么发现的

先说说,问题,是,怎么,发现,的。

昨天,晚上,大概,8点,多,我们,的,运维,同事,突然,在,群里,说,线上,的,订单,服务,调用,商品,服务,偶尔,会,失败,报错,是,"No provider available for the service",也就是,找不到,服务,提供者,但是,过,一会儿,再,试,又,好,了,是,偶发,的,不是,必现,的。

我,当时,正在,吃饭,看到,消息,赶紧,放下,筷子,打开,电脑,开始,排查。因为,订单,服务,是,核心,服务,调用,商品,服务,失败,会,影响,用户,下单,影响,业务,所以,必须,尽快,解决。

我,先,看,了,一下,监控,发现,商品,服务,的,实例,都,是,健康,的,在,Nacos,控制台,上,也,能,看到,商品,服务,的,实例,都,在,注册,列表,里,状态,是,健康,的,没有,下线,的,实例,而且,大部分,调用,都,是,成功,的,只有,很少,一部分,调用,失败,报错,"No provider available for the service"。

这个,问题,很,奇怪,因为,服务,提供者,明明,都,在,也,都,健康,为什么,会,偶尔,找不到,提供者,呢?而且,是,偶发,的,不是,必现,的,这,就,更,难,排查,了。

我,先,想,是不是,网络,的,问题,比如,偶尔,网络,抖动,导致,调用,失败,但是,看,了,一下,网络,监控,没有,发现,明显,的,网络,问题,而且,报错,是,"No provider available for the service",不是,网络,超时,或者,连接,拒绝,所以,应该,不是,网络,的,问题。

我,又,想,是不是,Nacos,的,问题,比如,Nacos,服务,列表,同步,有,延迟,导致,消费者,拿到,的,服务,列表,不,完整,但是,看,了,一下,Nacos,的,监控,和,日志,没有,发现,明显,的,问题,而且,Nacos,我们,用,了,很久,一直,都,很,稳定,不,太,可能,突然,出,这种,问题。

排查,了,半天,没有,找到,原因,问题,还,在,偶尔,发生,虽然,概率,不,高,但是,影响,用户,下单,必须,尽快,解决,于是,我,决定,深入,排查,从,代码,和,日志,入手,一步步,找,原因。

二、排查过程

再说说,详细,的,排查,过程。

第一步:开启详细日志,看调用过程

首先,我,开启,了,Dubbo,的,详细,日志,把,日志,级别,调,到,DEBUG,这样,能,看到,Dubbo,调用,的,详细,过程,包括,服务,发现,路由,负载均衡,调用,等等,这样,能,更,清楚,地,看到,调用,失败,的,时候,到底,发生,了,什么。

开启,日志,之后,我,等,了,一会儿,终于,又,出现,了,一次,调用,失败,我,赶紧,看,日志,发现,调用,失败,的,时候,Dubbo,的,日志,显示,"No provider available for the service xxx from registry xxx on consumer xxx using dubbo version xxx, may be providers disabled or not registered ?",也就是说,Dubbo,在,调用,的,时候,从,本地,的,服务,列表,里,没有,找到,可用,的,提供者。

但是,我,看,了,一下,Nacos,控制台,商品,服务,的,实例,都,在,也,都,健康,为什么,消费者,本地,的,服务,列表,里,没有,呢?

我,又,看,了,一下,消费者,本地,的,服务,列表,发现,消费者,本地,的,服务,列表,里,商品,服务,的,实例,确实,少,了,几个,但是,过,一会儿,又,恢复,了,也就是说,消费者,本地,的,服务,列表,偶尔,会,少,几个,实例,导致,调用,失败。

这,就,奇怪,了,为什么,消费者,本地,的,服务,列表,会,偶尔,少,几个,实例,呢?是,Nacos,推送,的,问题,还是,消费者,本地,缓存,的,问题?

第二步:检查Nacos推送日志

接下来,我,检查,了,Nacos,的,推送,日志,看,Nacos,有没有,正确,地,把,服务,列表,推送,给,消费者。

我,在,Nacos,控制台,上,看,了,商品,服务,的,订阅者,列表,发现,订单,服务,的,实例,都,在,订阅,列表,里,说明,订阅,是,正常,的。

我,又,看,了,Nacos,服务端,的,日志,发现,Nacos,确实,有,推送,服务,列表,给,消费者,而且,推送,的,服务,列表,是,完整,的,没有,少,实例,所以,Nacos,服务端,的,推送,是,正常,的,问题,应该,出,在,消费者,端。

第三步:检查消费者端的服务列表更新逻辑

既然,Nacos,服务端,推送,是,正常,的,那么,问题,应该,出,在,消费者,端,的,服务,列表,更新,逻辑,上。

我,开始,研究,Spring Cloud Alibaba,Nacos,的,服务,发现,源码,看,消费者,是,怎么,更新,本地,的,服务,列表,的。

看,了,源码,之后,我,发现,Spring Cloud Alibaba,Nacos,的,服务,发现,是,通过,NacosWatch,这个,类,来,实现,的,NacosWatch,会,定时,从,Nacos,拉取,服务,列表,然后,更新,本地,的,缓存,而且,也,会,监听,Nacos,的,推送,实时,更新,本地,的,服务,列表。

我,又,看,了,一下,NacosWatch,的,源码,发现,NacosWatch,在,更新,本地,服务,列表,的,时候,是,先,清空,本地,的,服务,列表,然后,再,把,新,的,服务,列表,放,进去,这,就,有,问题,了!

因为,清空,和,放入,不是,原子,操作,中间,有,一个,时间,窗口,本地,的,服务,列表,是,空,的,或者,不,完整,的,如果,这,时候,有,调用,进来,就,会,找不到,服务,提供者,报错,"No provider available for the service"。

而且,NacosWatch,默认,的,定时,拉取,间隔,是,30秒,也就是说,每,30秒,就,会,更新,一次,本地,的,服务,列表,每次,更新,都,有,一个,时间,窗口,服务,列表,不,完整,所以,就,会,偶尔,出现,调用,失败,的,情况,这,正好,和,我们,遇到,的,问题,吻合!

我,又,看,了,一下,我们,用,的,Spring Cloud Alibaba,的,版本,是,2.1.0.RELEASE,这个,版本,的,NacosWatch,确实,有,这个,问题,更新,服务,列表,的,时候,不是,原子,操作,有,时间,窗口,服务,列表,不,完整。

我,又,去,GitHub,上,看,了,一下,Spring Cloud Alibaba,的,issue,和,commit,发现,这个,问题,确实,有人,提过,而且,在,后面,的,版本,里,已经,修复,了,修复,的,方法,就是,更新,服务,列表,的,时候,用,新,的,服务,列表,直接,替换,旧,的,而,不是,先,清空,再,放入,这样,就是,原子,操作,了,不会,有,时间,窗口,服务,列表,不,完整。

终于,找到,原因,了!就是,Spring Cloud Alibaba 2.1.0.RELEASE,版本,的,NacosWatch,更新,服务,列表,的,逻辑,有,bug,不是,原子,操作,导致,偶尔,服务,列表,不,完整,调用,失败。

三、解决方案

找到,原因,之后,就,好,解决,了,有,几个,解决方案,下面,说说。

方案一:升级Spring Cloud Alibaba版本

最,简单,的,解决方案,就是,升级,Spring Cloud Alibaba,的,版本,升级,到,修复,了,这个,bug,的,版本,比如,2.1.1.RELEASE,或者,更高,的,版本,这些,版本,已经,修复,了,NacosWatch,的,这个,bug,更新,服务,列表,是,原子,操作,不会,有,时间,窗口,服务,列表,不,完整。

但是,升级,版本,有,风险,因为,版本,升级,可能,会,带来,其他,的,兼容性,问题,需要,充分,测试,而且,我们,的,项目,比较,大,依赖,比较,多,升级,版本,需要,测试,很多,东西,比较,麻烦,而且,当时,是,晚上,线上,有,问题,需要,尽快,解决,没有,时间,做,充分,的,测试,所以,这个,方案,暂时,不,太,合适。

方案二:自定义NacosWatch,修复bug

第二个,方案,就是,自定义,NacosWatch,覆盖,默认,的,实现,修复,更新,服务,列表,的,逻辑,改成,原子,操作,这样,不用,升级,版本,也,能,解决,问题。

这个,方案,比较,灵活,也,比较,快,不用,改,其他,的,东西,只,需要,写,一个,自定义,的,NacosWatch,替换,默认,的,就行,而且,风险,比较,小,因为,只,改,了,更新,服务,列表,的,逻辑,其他,的,都,不变。

我,当时,就是,用,的,这个,方案,写,了,一个,自定义,的,NacosWatch,修复,了,更新,服务,列表,的,逻辑,然后,替换,了,默认,的,NacosWatch,发布,到,线上,问题,就,解决,了。

下面,说说,具体,怎么,实现。

首先,我们,需要,看,一下,默认,的,NacosWatch,的,源码,找到,更新,服务,列表,的,方法,然后,自定义,一个,类,继承,NacosWatch,重写,更新,服务,列表,的,方法,改成,原子,操作。

默认,的,NacosWatch,更新,服务,列表,的,方法,大概,是,这样,的:

public void nacosServicesWatch() {
    // ...
    for (String service : services) {
        // 获取服务列表
        List<NacosServiceInstance> instances = nacosDiscoveryProperties.namingServiceInstance()
            .selectInstances(service, group, true);
        // 先清空旧的服务列表
        cache.put(service, instances);
    }
    // ...
}

问题,就,出,在,cache.put(service, instances),这里,虽然,put,本身,是,原子,的,但是,在,获取,服务,列表,和,put,之间,有,时间,差,而且,如果,服务,列表,获取,失败,或者,为空,就,会,把,空,的,列表,放,进去,导致,服务,列表,为空。

而且,默认,的,实现,在,处理,多个,服务,的,时候,是,循环,处理,每个,服务,依次,更新,所以,在,更新,的,过程,中,有些,服务,的,列表,已经,更新,了,有些,还,没有,也,可能,导致,问题。

修复,的,方法,就是,先,把,所有,服务,的,新,列表,都,获取,好,放到,一个,临时,的,Map,里,然后,再,一次性,替换,整个,cache,这样,就是,原子,操作,了,不会,有,时间,窗口,服务,列表,不,完整。

自定义,的,NacosWatch,大概,是,这样,的:

public class CustomNacosWatch extends NacosWatch {
    
    private final Map<String, List<NacosServiceInstance>> cache = new ConcurrentHashMap<>();
    
    // ... 构造方法等
    
    @Override
    public void nacosServicesWatch() {
        // 先获取所有服务
        List<String> services = null;
        try {
            services = nacosDiscoveryProperties.namingServiceInstance()
                .getServicesOfServer(1, Integer.MAX_VALUE, group).getData();
        } catch (NacosException e) {
            log.error("get services from nacos error", e);
            return;
        }
        
        if (services == null || services.isEmpty()) {
            return;
        }
        
        // 先把所有服务的新列表都获取好,放到临时Map里
        Map<String, List<NacosServiceInstance>> tempCache = new HashMap<>();
        for (String service : services) {
            try {
                List<NacosServiceInstance> instances = nacosDiscoveryProperties.namingServiceInstance()
                    .selectInstances(service, group, true);
                tempCache.put(service, instances);
            } catch (NacosException e) {
                log.error("get instances for service {} error", service, e);
                // 如果获取失败,就用旧的列表,不要清空
                if (cache.containsKey(service)) {
                    tempCache.put(service, cache.get(service));
                }
            }
        }
        
        // 一次性替换整个cache,原子操作
        cache.clear();
        cache.putAll(tempCache);
    }
    
    // ... 其他方法
}

这样,就,修复,了,更新,服务,列表,的,逻辑,先,把,所有,服务,的,新,列表,都,获取,好,放到,临时,Map,里,然后,再,一次性,替换,整个,cache,这样,就是,原子,操作,了,不会,有,时间,窗口,服务,列表,不,完整。

而且,如果,某个,服务,的,列表,获取,失败,就,用,旧,的,列表,不要,清空,这样,就算,Nacos,偶尔,有,问题,也,不会,导致,服务,列表,为空,调用,失败。

然后,需要,把,自定义,的,NacosWatch,注册,到,Spring,容器,里,替换,默认,的,NacosWatch,可以,用,@Primary,注解,或者,在,配置,类,里,手动,注册。

这样,就,不用,升级,版本,也,能,解决,问题,了,而且,风险,比较,小,只,改,了,更新,服务,列表,的,逻辑。

方案三:调整NacosWatch的定时拉取间隔

第三个,方案,就是,调整,NacosWatch,的,定时,拉取,间隔,把,间隔,调,大,一些,比如,从,默认,的,30秒,调,到,5分钟,或者,10分钟,这样,更新,服务,列表,的,频率,降低,了,出现,问题,的,概率,也,降低,了。

但是,这个,方案,只是,降低,了,问题,的,概率,没有,根本,解决,问题,而且,间隔,调,大,了,服务,上下线,的,感知,会,变慢,服务,下线,了,消费者,要,过,很久,才能,感知,到,可能,会,调用,已经,下线,的,服务,导致,失败,所以,这个,方案,只是,临时,的,缓解,方案,不是,根本,的,解决方案。

我,当时,在,修复,之前,临时,把,间隔,调,大,了,一些,先,缓解,一下,问题,然后,再,用,方案二,根本,解决,问题。

四、最终的解决过程

再说说,最终,的,解决,过程。

我,找到,原因,之后,已经,是,凌晨,2点,多,了,我,先,用,方案三,把,NacosWatch,的,定时,拉取,间隔,从,30秒,调,到,5分钟,先,缓解,一下,问题,发布,到,线上,问题,的,发生,概率,明显,降低,了,很少,再,出现,调用,失败,的,情况。

然后,我,开始,写,自定义,的,NacosWatch,修复,更新,服务,列表,的,逻辑,写完,之后,在,测试,环境,测试,了,一下,确认,没有,问题,然后,发布,到,线上,把,定时,拉取,间隔,又,调,回,了,默认,的,30秒,观察,了,一会儿,没有,再,出现,调用,失败,的,情况,问题,彻底,解决,了。

这,时候,已经,是,凌晨,5点,多,了,天,快,亮,了,我,终于,松,了,一口气,排查,了,一夜,终于,解决,了,问题。

虽然,很,累,但是,也,很,有,成就感,而且,通过,这次,排查,我,对,Spring Cloud Alibaba,的,服务,发现,机制,有,了,更,深入,的,理解,也,积累,了,线上,问题,排查,的,经验,这些,都是,很,宝贵,的,财富。

五、排查线上问题的一些经验

最后,总结,一下,排查,线上,问题,的,一些,经验,希望,能,给,大家,一些,参考。

1. 先稳住,不要慌

线上,出,了,问题,首先,要,稳住,不要,慌,慌,了,就,容易,乱,乱,了,就,容易,出错,甚至,可能,会,把,问题,搞,得,更,大。所以,一定要,先,稳住,冷静,分析,问题,一步步,排查。

2. 先缓解,再根治

排查,问题,的,时候,要,先,想,办法,缓解,问题,降低,影响,比如,降级,限流,切流量,调整,配置,等等,先,让,业务,恢复,正常,或者,降低,影响,然后,再,慢慢,排查,根本,原因,根治,问题。不要,一上来,就,想,根治,问题,结果,问题,还,没,解决,业务,已经,受,了,很大,影响。

3. 充分利用日志和监控

排查,问题,的,时候,要,充分,利用,日志,和,监控,日志,能,告诉,我们,问题,发生,的,时候,到底,发生,了,什么,监控,能,告诉,我们,系统,的,状态,变化,这些,都是,排查,问题,的,重要,依据。

如果,默认,的,日志,不够,详细,可以,临时,开启,更,详细,的,日志,比如,DEBUG,级别,的,日志,这样,能,看到,更,详细,的,信息,帮助,排查,问题,但是,要,注意,DEBUG,日志,量,大,可能,会,影响,性能,排查,完,之后,要,及时,关掉。

4. 学会看源码

很多,问题,尤其是,框架,层面,的,问题,光,看,日志,和,监控,可能,找,不到,原因,这,时候,就,需要,去,看,源码,理解,框架,的,实现,原理,这样,才能,找到,根本,原因。

所以,平时,要,养成,看,源码,的,习惯,理解,常用,框架,的,实现,原理,这样,出,了,问题,才能,快速,定位,原因。

5. 多搜索,多查资料

排查,问题,的,时候,要,多,搜索,多,查,资料,比如,GitHub,的,issue,Stack Overflow,技术,博客,等等,很多,问题,可能,别人,已经,遇到,过,也,解决,过,搜索,一下,可能,就,能,找到,解决方案,不用,自己,从零,开始,排查,节省,时间。

6. 做好记录,总结经验

排查,完,问题,之后,要,做好,记录,总结,经验,把,问题,的,现象,原因,解决方案,都,记录,下来,这样,以后,再,遇到,类似,的,问题,就能,快速,解决,也,能,分享,给,团队,的,其他,同学,帮助,大家,一起,成长。

六、写在最后

这次,线上,Spring Cloud Alibaba,的,Bug,排查,了,一夜,终于,解决,了,虽然,很,累,但是,也,很,有,收获,不仅,解决,了,问题,还,对,Spring Cloud Alibaba,的,服务,发现,机制,有,了,更,深入,的,理解,也,积累,了,线上,问题,排查,的,经验。

Spring Cloud Alibaba,是,一个,很,好,的,微服务,技术栈,功能,丰富,使用,方便,但是,毕竟,是,开源,项目,可能,会,有,一些,bug,我们,在,使用,的,过程,中,要,多,留意,多,总结,遇到,问题,不要,慌,一步步,排查,总能,找到,原因,解决,问题。

希望,这篇,文章,能,给,正在,使用,Spring Cloud Alibaba,的,朋友,一些,参考,和,帮助,避免,踩,同样,的,坑,也,希望,大家,都,能,少,遇到,线上,问题,系统,都,能,稳定,运行。

也,欢迎,大家,在,评论,区,分享,你们,遇到,的,Spring Cloud Alibaba,的,坑,和,解决方案,一起,交流,一起,进步。

最后,提醒,大家,线上,问题,排查,要,注意,安全,不要,随便,在,生产,环境,调试,要,先,在,测试,环境,验证,没问题,再,发布,到,线上,避免,造成,更大,的,影响。

愿,我们,都,能,在,踩坑,中,成长,在,解决,问题,中,进步,成为,更好,的,工程师。