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服务?
回滚代码应急后发现当时是误判,其实两个服务不是在一个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依赖。
转换一下思路来排查问题就是,SOFA应用部署的过程是什么样的?健康检查等能查出来什么问题?
当前通过查看health.sh和deploy.sh脚本没看出啥可以用的信息,找到的一些排错文档都是部署失败后的排查,还需要进一步看一下部署成功但是调用时失败的问题。
一般应用部署分为下面几步,根据经验编译成功后如果部署不起来,一般都是健康检查出现了问题。
- 镜像启动:即运行镜像中的各类脚本,主要包括准备环境变量、构建启动参数、启动nginx等逻辑。
- 应用启动:镜像脚本成功拉起 SOFABoot 应用后,应用开始执行启动逻辑直至框架完成健康检查阶段。处于本阶段的容器 /actuator/readiness 服务为不可用状态,发起 HTTP 服务将拒绝连接或者返回 500(Not Ready)。
- 健康检测完成后:框架完成健康检查相关逻辑后,/actuator/readiness 服务为可用状态,发起 HTTP 服务将返回 200(可用)或者 503 (不可用);
查找SOFABOOT基础配置,其中有一条关于健康检查的基础配置描述直接正中眉心,直接到代码中查看配置:
com.alipay.sofa.boot.skipJvmReferenceHealthCheck=true,改成false重新部署就部署不起来了,破案(此刻心里暗爽+10086)。
3.写在最后
本次教训/经验:保持代码敬畏之心,修改历史代码务必谨慎,搞清“之所以”再行动。
来源 | 阿里云开发者公众号
作者 | 王谷