本文记录一次升级Apollo Client组件到1.7.0后遇到的重大生产事故。只想看结论的,可直接快进到文末。实际上,第一句话就是一个结论。
另,本文行文思路事后看起来可行略显思路清晰,实际上排查生产问题时如无头苍蝇,各种猜想各种否定猜想,各种排除各种验证。
另另,回滚服务有时候是一个法宝。
此时已经0点54分,明天醒来再好好整理一下。
某次生产上线一次性发布6个微服务。应用发布后,并没有第一时间收到Prometheus (ERROR类型日志)告警,以为一切正常。
随后在打开某个小程序时,发现小程序打不开。第一时间把问题抛到技术群,随后去生产查看ELK ERROR(再次提到)日志,看到如下日志:
- status 500 reading RemoteOAuthService#smsCodeLogin(SmsCodeLoginDto)
feign.FeignException: status 500 reading RemoteOAuthService#smsCodeLogin(SmsCodeLoginDto)
at feign.FeignException.errorStatus(FeignException.java:78)
at feign.codec.ErrorDecoder$Default.decode(ErrorDecoder.java:93)
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:149)
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:78)
at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:103)
at com.aba.enduser.service.impl.UserLoginServiceImpl.smsCodeLogin(UserLoginServiceImpl.java:181)
at org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstMethodsInter.intercept(InstMethodsInter.java:86)
at com.aba.enduser.controller.LoginController.smsCodeLogin(LoginController.java)
事实上后来的排查证明与上面的报错无关。
与此同时,技术群前端同学排查到如下截图 404 NOT_FOUND异常:
看接口,以及上面报错日志,第一时间怀疑是OAuth服务(在此次发布清单6个服务中)有问题,于是回滚此服务。
但是,在回滚此服务后,小程序依旧有问题,404 NOT_FOUND异常依旧存在。
继续排查剩余5个发布应用清单,将其余4个服务排除后,回滚gateway-open服务。小程序打开正常。
通过GitLab提供的Repository-Compare功能,即代码对比,本次发布版本Tag和发布前线上运行的版本Tag,并没有什么显著性问题(因为此次发布有2个问题,事后结论)看这个版本对比,也看不出啥问题。
为何某些服务需要额外引入apollo-openapi?
因为如下代码:
@SpringBootApplication
@EnableApolloConfig(value = {"commons"})
public class OpenGatewayApplication {
@Value("${apollo.portalUrl:http://172.111.222.33:8070}")
private String portalUrl;
@Value("${apollo.portal.token:717376485f69df0d}")
private String token;
@Bean
public ApolloOpenApiClient apolloOpenApiClient() {
return ApolloOpenApiClient.newBuilder()
.withPortalUrl(portalUrl)
.withToken(token)
.build();
}
}
为什么apollo-core
版本有区别,得看pom文件里先引入apollo-client
还是先引入apollo-openapi
c.ctrip.framework.apollo.internals.AbstractConfigRepository | trySync | 29 | - Sync config failed, will retry. Repository class com.ctrip.framework.apollo.internals.RemoteConfigRepository, reason: 'java.lang.String com.ctrip.framework.foundation.spi.provider.ApplicationProvider.getAccessKeySecret()'
Schedule long polling refresh failed [Cause: com.ctrip.framework.foundation.spi.provider.ApplicationProvider.getAccessKeySecret()Ljava/lang/String;]
Sync config from upstream repository class com.ctrip.framework.apollo.internals.RemoteConfigRepository failed, reason: com.ctrip.framework.foundation.spi.provider.ApplicationProvider.getAccessKeySecret()Ljava/lang/String;
Init Apollo Local Config failed - namespace: commons, reason: Load config from local config failed! [Cause: Cannot read from local cache file /opt/data/App/config-cache/App+default+commons.properties].
Could not load config for namespace commons from Apollo, please check whether the configs are released in Apollo! Return default value now!
借助于开源组件KubernetesFileBrowser,我们可以拿到kubernetes环境下pod里的文件。
@EnableApolloConfig
这个看一下EnableApolloConfig.java
源码就明白:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({ApolloConfigRegistrar.class})
public @interface EnableApolloConfig {
String[] value() default {"application"};
int order() default Integer.MAX_VALUE;
}
也就意味着,除了在本地配置文件里指定需要引用的namespace,我还可以在@注解里指定namespace,像这样:
@EnableApolloConfig(value = {"commons", "gateway-open"})
配置文件:
#apollo
app:
id: App
apollo:
meta: http://apollo.aaabbbccc.com:8080
bootstrap:
enabled: true
namespaces: commons,gateway-open
后端微服务架构体系里将近30个微服务,所有的请求流量转发都是由gateway服务来承载。另外,考虑到我们的产品有不同的用户体系,比如C端用户,三方对接平台或商户等。基于这两点,我们有2个gateway网关服务,一个是gateway-c服务,用于服务C端用户的绝大多数请求。另一个是gateway-open服务,用于服务三方等渠道或商户的请求。
另外,作为一个背景知识,gateway网关服务里面的配置几乎都是路由转发规则配置,类似于:
spring.cloud.gateway.routes[22].id = enduser-provider
spring.cloud.gateway.routes[22].uri = lb://enduser-provider
spring.cloud.gateway.routes[22].predicates[0] = Path=/api/open/user/**
spring.cloud.gateway.routes[22].filters[0] = StripPrefix=1
spring.cloud.gateway.routes[22].filters[1] = RequestLogFilter
spring.cloud.gateway.routes[22].filters[2] = BlockListFilter
spring.cloud.gateway.routes[22].filters[3] = TokenFilter
spring.cloud.gateway.routes[22].filters[4] = SignVerifyFilter
spring.cloud.gateway.routes[22].filters[5] = HeaderCheckNullFilter
当业务场景变得越来越复杂,微服务数量越来越多,或随着一个个接口的上线或下线,或者某个符合predicates断言规则接口前端(含H5、小程序、iOS、Android等,即所谓大前端概念),适配增加各种不同场景的Filter,等等原因,导致gateway网关服务的路由转发规则变得臃肿复杂难以理解。
此为背景。
某次在优化gateway-c服务的配置后,从Sublime Text复制配置到Apollo,准备发布更改时,才发现不对劲,这次调整怎么有这么多啊。立马发现,好家伙,手残点错namespace。把本来应该变更到gateway-c的配置复制到gateway-open。
ok。我要回滚,我当然不可能点发布。但是,当我点击回滚时,弹窗提示的版本号明显不对,一个是目前生产的版本,一个是上一次的版本,类似于下图(非事发时截图):
仔细分析,我想要的其实并不是【回滚】功能,因为我的配置变更还未提交(发布)。再想了想,模模糊糊有了点头绪(虽然此处行文可能看起来显得思路清晰得一逼),此处Apollo版本发布概念实际上就是Git的版本控制概念。
配置有变更,等价于Git里的modified;
配置发布,相当于Git里的commit;
配置回滚,可类比于Git里的revert;
配置撤销,可类比于Git里的restore。
带着Apollo、回滚、撤销等关键词找到官方GitHub issue,看到:
点击Apollo-PR-2952,发现对应着Milestone 1.7.0。
因为之前了解过我们使用的生产环境Apollo UI(配置可视化管理端)版本号为:1.4.0,测试环境Apollo UI版本号为:1.7.0。
版本号正好匹配,于是打开测试环境Apollo UI,发现确实有这个【撤销配置】的入口:
作为对比,放一张生产环境的截图:
另外我们在pom文件里使用的apollo-client版本号为1.5.0,这个当然不区分测试和生产环境。
也就是说,apollo-client-1.5.0版本与apollo server(UI)1.7.0版本可以兼容,即测试环境就是这种情况,没有(发现)问题。
于是:升级生产Apollo Server版本到1.7.0!
PS:Apollo Server端成功升级后,我们应用侧,apollo-client还是1.5.0,两者完美兼容运行一段时间后。
应用pom.xml文件先引入apollo-openapi-1.5.0,后引入apollo-client-1.7.0。先删除本地缓存下来的Apollo配置properties文件,应用启动时会有很多WARN日志,即上面提到的5类WARN日志,不影响应用启动成功。
<dependency>
<groupId>com.ctrip.framework.apollogroupId>
<artifactId>apollo-openapiartifactId>
<version>1.5.0version>
dependency>
<dependency>
<groupId>com.ctrip.framework.apollogroupId>
<artifactId>apollo-clientartifactId>
<version>1.7.0version>
dependency>
事实上,请求http://localhost:8848/health/check
正好对应着健康健康endpoint端点:
@GetMapping({"/health/check"})
public String heathCheck() {
return "OK";
}
但是,后面还是会持续打印WARN日志。
并且!
本地并没有缓存此应用需要的几个Apollo配置文件!!
后面如果要通过此服务做请求转发,拿不到配置路由规则,更谈何URL路径规则转发呢!!