• SpringBoot Admin升级boot等组件版本后无法监控微服务


    最近,安全中心报告了spring-boot-admin服务由于集成security版本过低导致了安全问题,Spring Security RegexRequestMatcher 认证绕过漏洞(CVE-2022-22978),建议升级security版本

    5.5.x 版本使用者建议升级至5.5.7及其以上

    5.6.x 版本使用者建议升级至5.6.4及其以上

    排查服务发现当前服务集成security版本为5.3.9,于是决定动手升级。

    一、升级版本

    由于项目通过集成

    
       org.springframework.boot
       spring-boot-starter-security
    

    接入security,因此security版本由springboot统一管理。

    工程中各依赖版本如下:

    1. <spring-boot.version>2.3.12.RELEASEspring-boot.version>
    2. <spring-cloud.version>Hoxton.SR12spring-cloud.version>
    3. <spring-cloud-alibaba.version>2.2.6.RELEASEspring-cloud-alibaba.version>

    于是为了满足上述升级需求,我们对该服务进行版本调整。

    1. <spring-boot.version>2.6.8spring-boot.version>
    2. <spring-cloud.version>2021.0.1spring-cloud.version>
    3. <spring-cloud-alibaba.version>2021.0.1.0spring-cloud-alibaba.version>

    springboot升级到2.4+之后,不再支持bootstrap版本,因此服务此时需要调整配置由bootstrap.yml转变为application.yml。

    1. spring:
    2. application:
    3. name: xxx
    4. cloud:
    5. nacos:
    6. config:
    7. username: xxx
    8. password: xxx
    9. server-addr: xxx:8848
    10. namespace: xxx
    11. group: DEFAULT_GROUP
    12. refresh-enabled: false
    13. config:
    14. import:
    15. - optional:nacos:xxx.properties?group=${spring.cloud.nacos.config.group}&refreshEnabled=false
    16. - optional:nacos:xxx.properties?group=${spring.cloud.nacos.config.group}&refreshEnabled=false

    工程可以正常启动,但访问监控页面,监控服务健康检查处于DOWN状态,且无元数据。

    二、排查原因

    排查源码发现spring-boot-admin-server-cloud源码包中

    InstanceDiscoveryListener通过监听event事件动态更新监控服务状态。

    服务启动后通过监听事件调用

    InstanceDiscoveryListener.discover()方法。
    1. protected void discover() {
    2. log.debug("Discovering new instances from DiscoveryClient");
    3. Flux.fromIterable(discoveryClient.getServices()).filter(this::shouldRegisterService)
    4. .flatMapIterable(discoveryClient::getInstances).filter(this::shouldRegisterInstanceBasedOnMetadata)
    5. .flatMap(this::registerInstance).collect(Collectors.toSet()).flatMap(this::removeStaleInstances)
    6. .subscribe((v) -> {
    7. }, (ex) -> log.error("Unexpected error.", ex));
    8. }

    方法首先会从注册中心获取注册服务,之后屏蔽不需要监控的服务,再进行bean转换。

    此处需要注意discoveryClient::getInstances方法,即bean转换方法。

    由于服务注册中心为consul,因此getInstance会调用

    ConsulDiscoveryClient.getInstances()。
    1. public List getInstances(final String serviceId, final QueryParams queryParams) {
    2. List instances = new ArrayList();
    3. this.addInstancesToList(instances, serviceId, queryParams);
    4. return instances;
    5. }
    6. private void addInstancesToList(List instances, String serviceId, QueryParams queryParams) {
    7. Builder requestBuilder = HealthServicesRequest.newBuilder().setPassing(this.properties.isQueryPassing()).setQueryParams(queryParams).setToken(this.properties.getAclToken());
    8. String[] queryTags = this.properties.getQueryTagsForService(serviceId);
    9. if (queryTags != null) {
    10. requestBuilder.setTags(queryTags);
    11. }
    12. HealthServicesRequest request = requestBuilder.build();
    13. Response> services = this.client.getHealthServices(serviceId, request);
    14. Iterator var8 = ((List)services.getValue()).iterator();
    15. while(var8.hasNext()) {
    16. HealthService service = (HealthService)var8.next();
    17. instances.add(new ConsulServiceInstance(service, serviceId));
    18. }
    19. }

    方法中真正的bean转换在于new ConsulServiceInstance(service, serviceId)。

    该构造器方法如下:

    1. public ConsulServiceInstance(HealthService healthService, String serviceId) {
    2. this(healthService.getService().getId(), serviceId, ConsulServerUtils.findHost(healthService), healthService.getService().getPort(), getSecure(healthService), getMetadata(healthService), healthService.getService().getTags());
    3. this.healthService = healthService;
    4. }

    可见metadata数据通过getMetadata()方法获取。

    1. private static Map<String, String> getMetadata(HealthService healthService) {
    2. Map<String, String> metadata = healthService.getService().getMeta();
    3. if (metadata == null) {
    4. metadata = new LinkedHashMap();
    5. }
    6. return (Map)metadata;
    7. }

    从代码看出,metadata数据从service实例的meta属性中获取。而由于监控服务统一注册tag标签而非meta到注册中心,因此此处数据获取为空,从而导致spring-boot-admin 元数据及监控路径拼接异常,进而服务监控失败,状态DOWN。

    那么为什么之前可以正常监控,升级版本之后便监控异常呢?

    通过回退之前版本我们发现,老版本中meta数据获取方法并非当前这样。

    1. private void addInstancesToList(List instances, String serviceId, QueryParams queryParams) {
    2. Builder requestBuilder = HealthServicesRequest.newBuilder().setPassing(this.properties.isQueryPassing()).setQueryParams(queryParams).setToken(this.properties.getAclToken());
    3. String queryTag = this.properties.getQueryTagForService(serviceId);
    4. if (queryTag != null) {
    5. requestBuilder.setTag(queryTag);
    6. }
    7. HealthServicesRequest request = requestBuilder.build();
    8. Response<List<HealthService>> services = this.client.getHealthServices(serviceId, request);
    9. HealthService service;
    10. String host;
    11. Map metadata;
    12. boolean secure;
    13. for(Iterator var8 = ((List)services.getValue()).iterator(); var8.hasNext(); instances.add(new DefaultServiceInstance(service.getService().getId(), serviceId, host, service.getService().getPort(), secure, metadata))) {
    14. service = (HealthService)var8.next();
    15. host = ConsulServerUtils.findHost(service);
    16. metadata = ConsulServerUtils.getMetadata(service, this.properties.isTagsAsMetadata());
    17. secure = false;
    18. if (metadata != null && metadata.containsKey("secure")) {
    19. secure = Boolean.parseBoolean((String)metadata.get("secure"));
    20. }
    21. }
    22. }

    老版本通过调用ConsulServerUtils.getMetadata(service, this.properties.isTagsAsMetadata());获取meta数据。

    1. @Deprecated
    2. public static Map<String, String> getMetadata(HealthService healthService, boolean tagsAsMetadata) {
    3. return tagsAsMetadata ? getMetadata(healthService.getService().getTags()) : healthService.getService().getMeta();
    4. }

    此版本中metadata通过tagsAsMetadata 属性标识判断是否可从tags中获取metadata,由于该属性默认为true,因此我们之前注册到tags的属性可以被admin标记为metadata,从而参与到

    DefaultServiceInstanceConverter.convert()实现对Registration对象的组装。
    1. protected Mono registerInstance(ServiceInstance instance) {
    2. try {
    3. Registration registration = this.converter.convert(instance).toBuilder().source("discovery").build();
    4. log.debug("Registering discovered instance {}", registration);
    5. return this.registry.register(registration);
    6. } catch (Exception var3) {
    7. log.error("Couldn't register instance for discovered instance ({})", this.toString(instance), var3);
    8. return Mono.empty();
    9. }
    10. }

    此时,我们只需要让监控服务注册meta到consul注册中心即可。

    根据spring-cloud-consul官方文档Spring Cloud Consul, metadata使用实例如下(也可使用properties文件,自行转换即可)

    此处有特别说明:

     

    参考DefaultServiceInstanceConverter所定义的key属性,于是我们修改配置文件,再次重新启动微服务

    此时,服务再次报错。

    OperationException{statusCode=400, statusMessage='Bad Request', statusContent='Invalid Service Meta: Couldn't load metadata pair ('management.context-path', '/xxx/actuator'): Key contains invalid characters'}

    根据异常栈,我们服务该异常由调用consul服务端rpc之后response响应体返回。

     由于是远端服务返回,考虑版本兼容性问题,直接升级最高版本consul,升级之后发现异常仍然存在。

    为了验证该问题,我们切换注册中心到nacos,发现meta注册正常,监控正常。

    到此,该问题排查基本结束,定义为spring-boot-admin与spring-cloud-consul版本兼容性问题。

    spring-cloud-consul在v3.0.0(不包含M1等版本)及之上,meta数据获取不再兼容tags,因此使用该版本的cloud在于admin集成时,若监控服务存在context-path或者自定义的健康检查路径等,由于admin定义meata属性key均包含"."字符,因此大概率均会出现监控异常问题(推测结论,未完整验证所有版本)。

    目前,已提issue到github上spring-boot-admin中,参见https://github.com/codecentric/spring-boot-admin/issues/2076,等待官方解答中。

  • 相关阅读:
    JAVA宝典----容器(理解记忆)
    RxJava/RxAndroid的操作符使用(二)
    MySQL基础篇【第五篇】| union、limit、DDL、DML、约束
    wpf devexpress 自定义统计
    基于单片机的空调的温度控制系统设计
    为什么会有流处理?
    【德哥说库系列】-Clickhouse集群部署(3分片2副本)
    Leetcode 剑指 Offer II 001. 整数除法
    设计分享—国外UI设计作品赏析
    LeetCode第203题—移除链表元素
  • 原文地址:https://blog.csdn.net/weixin_39195030/article/details/126044833