修改一行注解引起的故障

简介: 作者记录了故障发生时的排查思路,再对问题进行详细描述并分析根本原因,最终找到解决方案。

1.事情起因

11.11号接到咨询反馈,有用户在沙箱测试环境的一个上传文件场景遇到异常,原因是其依赖我们团队的应用AxxxCore的一个TR接口报错,通过错误堆栈定位是服务内部依赖的一个SOFA JVM服务找不到。

can not find the corresponding JVM service. Please check if there is a SOFA deployment publish the corresponding JVM service. If this exception occurred when the application starts up, please add Require-Module to SOFA deployment's MANIFEST.MF to indicate the startup dependency of SOFA modules.

2.排查思路

查看代码发现引入服务的地方最近有一次修改,在原先@SofaReference的基础上新增了uniqueId。

@SofaReference(uniqueId = "aftsFileClient")
private AftsFileClient 
aftsFileClient;

SOFA框架有SOFA模块的概念,通常称为Bundle,可以简单理解成成maven项目中的maven module,当然想要被SOFA框架识别为SOFA Bundle还需要配置META-INF/MANIFEST.MF文件、Module-Name等SOFA定制化内容,从官方文档扒出来大致如下。

每个Bundle都有自己独立的Spring上下文,Bundle之间的通信通过SOFA的JVM服务进行,如模块A发布了JVM服务AService、模块B引入该AService即可访问到A模块提供的服务完成Bundle质检的通信,对应上边的代码就是AftsFileClient通过@SofaService注解/配置XML的方式发布服务,问题接口通过@SofaReference的方式引入该服务,简而言之就是:

1、模块A通过@SofaService将模块内的bean发布成一个JVM服务

2、模块B通过@SofaReference将模块A的JVM服务引入

app
├── pom.xml
└── module1
    ├── src
    │   └── main
    │       ├── java
    │       └── resources
    │           └── META-INF
    │               ├── MANIFEST.MF
    │               └── spring
    │                   └── module1.xml
    └── pom.xml



2.1直接问题定位

那么问题就来了,为什么刚开始使用注解进行服务引用没有问题,加上一个uniqueId就报错了?

uniqueId是为了解决一个接口发布多个不同的服务而诞生的,如有一个接口Service对应两个实现AServiceImpl和BServiceImpl,分别发布了AService和BService两个服务,在引入服务的时候就需要通过uniqueId来声明要引入哪一个服务,通过接口名+uniqueId表达服务的唯一性。

直接通过搜索的手段查找报错的原因,在SofaBoot的官方排查文档中是这么描述的:按照从先到后的顺序排查发现问题正好符合第一个解决方法描述,故障代码引入的服务确实共存在同一个Bundle当中,故障应急其实通过代码回滚就已经解决了,那么问题又回来了,为啥没有uniqueId不报错,加了uniqueId就报错?


问题分析-step1

难道是不加uniqueId的时候也会找同Bundle发布的JVM服务或者降级找同Bundle的bean,加了uniqueId之后就严格跨Bundle查找JVM服务?

image.png

回滚代码应急后发现当时是误判,其实两个服务不是在一个Bundle里,不符合第一个解决方法描述,但是这个第一个问题的描述是否成立?因为官方正式文档中并没有对这些细节的非标准设置进行说明,本来想做个实验来验证一下这个同Bundle的服务是否可以引用,突然想起来之前一些历史接口存在同Bundle内通过@SofaReference引入服务依赖,于是乎直接扒皮代码,实验都不用做了,直接可以调通找到服务,上述文档的解决方法1不成立!


com.alipay.xxx.core.service.product.XxxManageServiceImpl

    @SofaReference
    private XxxQueryService       xxxQueryService;
————————————————————————————————————————————————————————————————————
com.alipay.xxx.core.service.product.XxxQueryService

那么这里真正的调用标准到底是什么呢?真的服务匹配/查找逻辑是啥?

这里通过实验进行了探索,在同Bundle中分别通过@SofaReference引用一个发布了JVM服务和只发布了bean的不同服务。


@Service
@SofaService
public class JvmBeanTestServiceImpl implements JvmBeanTestService {
    @Override
    public String test() {
        return "test1";
    }
}


@Service
public class JvmBeanTestV2ServiceImpl implements JvmBeanTestV2Service{
    @Override
    public String testV2() {
        return "testV2";
    }
}


@Service
@SofaService
public class JvmBeanTestRunTimeServiceImpl implements JvmBeanTestRunTimeService {

    @SofaReference
    private JvmBeanTestService jvmBeanTestService;

    @SofaReference
    private JvmBeanTestV2Service jvmBeanTestV2Service;

    @Override
    public String runTest1() {
        return jvmBeanTestService.test();
    }

    @Override
    public String runTest2() {
        return jvmBeanTestV2Service.testV2();
    }
}


实验结果

1、调用jvmBeanTestService成功,那么可以引用同Bundle的JVM服务这个推测结论成立,官方答疑文档有误;


2、调用jvmBeanTestV2Service失败,证明了@SofaReference只能匹配JVM服务不能匹配bean,之前的一个猜想方向是不对的。


问题分析-step2

第二个怀疑的方向是服务的发布方式有问题,依据是服务发布的时候没有指定uniqueId,但是服务引用确指定了uniqueId,和官方文档的描述不一致。


经过和SOFA官方人员核实,使用uniqueId发布和引用服务的时候,查找服务的时候是按照【接口名+uniqueId】进行匹配的,可以理解为如果发布服务的时候指定了uniqueId,那么引用服务的时候也要指定uniqueId,否则会匹配不到服务。我们的这个报错场景就是引用服务的时候指定了uniqueId,但是服务发布的时候并没有指定uniqueId,问题根因就在这里。


<sofa:service ref="aftsFileClient" interface="com.alipay.aaa.client.AftsFileClient"/>


那么当时改代码的同学为什么要在服务引用的时候加上uniqueId呢?通过翻看代码是因为当时的项目需求需要重新配置aftsFileClient服务对应的参数,也就是需要将aftsFileClient发布一个新服务出来。


1、原先有一个aftsFileClient的bean,改造的同学新配置了aftsFileXxxClient这个bean


<bean id="aftsFileXxxClient" class="com.alipay.aaa.client.impl.AftsFileXxxClientImpl" init-method="init">
        <property name="env" value="xxx" />
        <property name="sysName" value="yyy" />
        <property name="appId" value="zzz" />
        <property name="bizKey" value="ttt" />
</bean>

2、原先有一个aftsFileClient的JVM服务,改造的同学又将aftsFileXxxClient这个bean包装成了一个新的JVM服务xxxAftsFileClient


<bean id="xxxAftsFileClient"
          class="com.alipay.xxx.common.service.integration.afts.impl.XxxAftsFileClientImpl"/>
<sofa:service ref="xxxAftsFileClient"
          interface="com.alipay.xxx.common.service.integration.afts.XxxAftsFileClient"/>


那么此时新代码只需要通过@SofaReference直接引用xxxAftsFileClient这个JVM服务即可,因为新JVM服务xxxAftsFileClient和原先的JVM服务aftsFileClient实现的并不是一个接口也没有重名,所以实际上并没有使用uniqueId的需求,无需改动老代码。


这里能想到的有两个优化空间,第一个是不改代码,在官方文档说明不按标准进行配置会存在问题,但我估计还是解决不了问题,出现问题的往往是老司机很少再去看文档;第二个是在@SofaReference进行服务匹配的时候,如果设置了uniqueId可以先维持现状通过【接口名+uniqueId】的方式查找,最后兜底使用接口名查找,但是其实我如果是框架的开发者会觉得不合适,uniqueId本身就是一种进阶用法,不应该为了错误case做这种所谓的兼容。


2.2附加问题定位

依稀记得之前一些新依赖注入时有问题,在系统部署阶段就能发现,但是这次是系统部署成功后才发现,为什么?


这里怀疑应用应该和服务启动的依赖Bundle配置有关系,通过官方排查文档寻找应用启动了哪些模块发现都是外部jar包依赖,和我们要排查的方向不符合。


Spring context initialize success module list(53) >>>>>>> [totalTime = 146001 ms, realTime = 9242 ms]
  ├─aaa-common-service-facade-1.1.0.20211014.jar [1303 ms]
  │   `---aaa-common-service-facade.xml
  ├─bbb-common-service-facade-1.2.0.20230922.jar [1463 ms]
  │   `---common-service-facade.xml
  ├─ccc-common-service-facade-2.0.0.20150526.jar [1456 ms]
  │   `---common-service-facade.xml
  ├─ddd-api-1.0.5.jar [1506 ms]
  │   `---ddd-api.xml
  ├─eee-common-service-facade-1.0.0.20240510.jar [1474 ms]
  │   `---common-service-facade.xml
  ├─fff-common-service-facade-1.0.0.20240430.jar [1501 ms]
  │   `---common-service-facade.xml
   ----省略一万字----
  └─zzz-common-service-client-1.0.0.20220501.jar [846 ms]
      +---common-service-cache.xml
      +---common-service-client.xml
      +---common-service-integration.xml
      `---common-service-xxxcache.xml


继续打开思路,找到一篇CloudEngine部署容器的文档详细的介绍了一些启动日志细节,但查看日志详情没有看到我们想要的应用内自己的Bundle启动的信息,依旧都是外部jar依赖。


image.png


转换一下思路来排查问题就是,SOFA应用部署的过程是什么样的?健康检查等能查出来什么问题?


当前通过查看health.sh和deploy.sh脚本没看出啥可以用的信息,找到的一些排错文档都是部署失败后的排查,还需要进一步看一下部署成功但是调用时失败的问题。


一般应用部署分为下面几步,根据经验编译成功后如果部署不起来,一般都是健康检查出现了问题。

  1. 镜像启动:即运行镜像中的各类脚本,主要包括准备环境变量、构建启动参数、启动nginx等逻辑。
  2. 应用启动:镜像脚本成功拉起 SOFABoot 应用后,应用开始执行启动逻辑直至框架完成健康检查阶段。处于本阶段的容器 /actuator/readiness 服务为不可用状态,发起 HTTP 服务将拒绝连接或者返回 500(Not Ready)。
  3. 健康检测完成后:框架完成健康检查相关逻辑后,/actuator/readiness 服务为可用状态,发起 HTTP 服务将返回 200(可用)或者 503 (不可用);

查找SOFABOOT基础配置,其中有一条关于健康检查的基础配置描述直接正中眉心,直接到代码中查看配置:


com.alipay.sofa.boot.skipJvmReferenceHealthCheck=true,改成false重新部署就部署不起来了,破案(此刻心里暗爽+10086)。


image.png


3.写在最后

本次教训/经验:保持代码敬畏之心,修改历史代码务必谨慎,搞清“之所以”再行动。





来源  |  阿里云开发者公众号

作者  |  王谷




相关文章
|
11月前
|
存储 缓存 Java
写代码原来如此简单:两种常用代码范式
一次项目包含非常多的流程,有需求拆解,业务建模,项目管理,风险识别,代码模块设计等等,如果我们在每次项目中,都将精力大量放在这些过程的思考上面,那我们剩余的,放在业务上思考的精力和时间就会大大减少;这也是为什么我们要 总结经验/方法论/范式 的原因;这篇文章旨在建立代码模块设计上的思路,给出了两种非常常用的设计范式,减少未来在这一块的精力开销。
212 11
|
11月前
|
前端开发 中间件 程序员
如何尽可能快地上手一个业务or项目
本文简单讲述作者对于“怎么尽可能快地上手一个新业务/项目?”这个问题的个人理解。
178 16
|
10月前
|
存储 关系型数据库 MySQL
MySQL索引学习笔记
本文深入探讨了MySQL数据库中慢查询分析的关键概念和技术手段。
699 81
|
10月前
|
存储 设计模式 监控
快速定位并优化CPU 与 JVM 内存性能瓶颈
本文介绍了 Java 应用常见的 CPU & JVM 内存热点原因及优化思路。
1042 166
|
11月前
|
NoSQL 应用服务中间件 API
Redis是如何建立连接和处理命令的
本文主要讲述 Redis 是如何监听客户端发出的set、get等命令的。
1595 160
|
10月前
|
安全 Cloud Native 容灾
海外泼天流量|浅谈全球化技术架构
本文对海外泼天流量现状做了快速整理,旨在抛砖引玉,促进国内企业在出海过程中,交流如何构建全球化技术架构的落地经验,相信会有越来越多资深人士分享更深层次的实践。
488 51
|
9月前
|
缓存 监控 安全
高并发编程知识体系
本文将从线程的基础理论谈起,逐步探究线程的内存模型,线程的交互,线程工具和并发模型的发展。扫除关于并发编程的诸多模糊概念,从新构建并发编程的层次结构。
|
9月前
|
存储 缓存 NoSQL
「缓存」会用很容易,用好才是技术活
本文对比了几种常用缓存的特点,主要介绍了基于Guava的本地缓存和基于Tair的分布式缓存,包含快速入门和深入原理两部分,并在最后提供了使用缓存时需要注意的事项。
|
10月前
|
监控 Java 中间件
8G的容器Java堆才4G怎么就OOM了?
本文记录最近一例Java应用OOM问题的排查过程,希望可以给遇到类似问题的同学提供参考。