概述

在EFK日志查询平台断断续续看到若干个应用的报错信息:
在这里插入图片描述
在这里插入图片描述

排查

上述截图里报错的类(省略掉Import语句后):

@Slf4j
@RestController
public class FilterErrorController extends BasicErrorController {
    public FilterErrorController() {
        super(new DefaultErrorAttributes(), new ErrorProperties());
    }

    @Override
    @RequestMapping(produces = {MediaType.APPLICATION_JSON_VALUE})
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = getStatus(request);
        // 第35行
        log.error(body.get("message").toString());
        Map<String, Object> responseBody = new HashMap<>(16);
        responseBody.put("code", "9000");
        responseBody.put("message", "服务器开小差了");
        return new ResponseEntity<>(responseBody, status);
    }

    @Override
    public String getErrorPath() {
        return "error/error";
    }
}

自定义的FilterErrorController继承BasicErrorController,而BasicErrorController则继承AbstractErrorController,继续跟进源码:

public class BasicErrorController extends AbstractErrorController {
}

找到AbstractErrorController.getErrorAttributes()方法:

public abstract class AbstractErrorController implements ErrorController {
    protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {
       WebRequest webRequest = new ServletWebRequest(request);
       return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
    }
}

进一步定位到DefaultErrorAttributes.addErrorDetails()

@Order(Integer.MIN_VALUE)
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> errorAttributes = new LinkedHashMap();
        errorAttributes.put("timestamp", new Date());
        this.addStatus(errorAttributes, webRequest);
        this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
        this.addPath(errorAttributes, webRequest);
        return errorAttributes;
    }
    
    private void addErrorDetails(Map<String, Object> errorAttributes, WebRequest webRequest, boolean includeStackTrace) {
        Throwable error = this.getError(webRequest);
        if (error != null) {
            while(true) {
                if (!(error instanceof ServletException) || error.getCause() == null) {
                    if (this.includeException) {
                        errorAttributes.put("exception", error.getClass().getName());
                    }    
                    this.addErrorMessage(errorAttributes, error);
                    if (includeStackTrace) {
                        this.addStackTrace(errorAttributes, error);
                    }
                    break;
                }
                error = ((ServletException)error).getCause();
            }
        }
        Object message = this.getAttribute(webRequest, "javax.servlet.error.message");
        if ((!StringUtils.isEmpty(message) || errorAttributes.get("message") == null) && !(error instanceof BindingResult)) {
            errorAttributes.put("message", StringUtils.isEmpty(message) ? "No message available" : message);
        }
    }    
}

报错的errorMsg可以在源码里定位到。

但是为啥呢?排查无果,搁置一阵子。

后续

后来在本地使用Postman调试另一个Debug模式启动的Spring Boot服务cbs的接口,http://localhost:8099/api/cbs/area/getProvinceList,也发生相同的报错:
在这里插入图片描述
控制台打印信息如下:aba-cbs-provider | [http-nio-8099-exec-4] | | ERROR | com.aba.web.controller.FilterErrorController | error | 35 | - No message available

本地可以复现问题,那就好办。

看代码,cbs项目工程里Controller层接口并没有配置前缀/api。为啥在后台管理系统里要加上/api,推测下来是统一化与规范化处理。

Postman请求http://localhost:8099/cbs/area/getProvinceList,正常返回。

也就是说,增加一个/api/,就有No message available报错。Postman里看到code=9000,以及message=服务器开小差了的报错。

Gateway请求转发?

突然想到之前在Apollo看到的几个Gateway相关的配置:

spring.cloud.gateway.routes[24].id = aba-dialog-provider
spring.cloud.gateway.routes[24].uri = lb://aba-dialog-provider
spring.cloud.gateway.routes[24].predicates[0] = Path=/api/dialog/**
spring.cloud.gateway.routes[24].filters[0] = StripPrefix=1

其中有些服务有spring.cloud.gateway.routes[24].filters[0] = StripPrefix=1这个配置项,而有些服务则无此配置。

cbs应用添加spring.cloud.gateway.routes[24].filters[0] = StripPrefix=1配置后,再次通过Postman请求接口http://localhost:8099/api/cbs/area/getProvinceList,报错消失!

结论

也就是说,每次EFK爆出一次No message available报错,就有一次Gateway网关转发到具体的服务后,服务(如smscbs等)找不到对应的@RequestMapping路径,接口匹配失败问题,即一次服务响应失败问题,一次用户界面交互动作失败问题,可能是文章最开始截图对应的短信发送失败问题,也可以是打卡报告失败问题。

可能上面的配置看起来比较简单,增加一个StripPrefix=1就能解决问题。

考虑真实业务情况,Gateway配置可能是这样的:

spring.cloud.gateway.routes[3].id = aba-enduser-provider-dx
spring.cloud.gateway.routes[3].uri = lb://aba-enduser-provider-dx
spring.cloud.gateway.routes[3].predicates[0] = Path=/api/user/admin/**

spring.cloud.gateway.routes[4].id = aba-enduser-provider
spring.cloud.gateway.routes[4].uri = lb://aba-enduser-provider
spring.cloud.gateway.routes[4].predicates[0] = Path=/api/open/user/**
spring.cloud.gateway.routes[4].filters[0] = TokenFilter
spring.cloud.gateway.routes[4].filters[1] = BlockListFilter

spring.cloud.gateway.routes[5].id = aba-enduser-provider
spring.cloud.gateway.routes[5].uri = lb://aba-enduser-provider
spring.cloud.gateway.routes[5].predicates[0] = Path=/api/user/v2/register
spring.cloud.gateway.routes[5].filters[0] = SecurityFilter
spring.cloud.gateway.routes[5].filters[1] = BlockListFilter

spring.cloud.gateway.routes[6].id = aba-enduser-provider
spring.cloud.gateway.routes[6].uri = lb://aba-enduser-provider
spring.cloud.gateway.routes[6].predicates[0] = Path=/api/user/**
spring.cloud.gateway.routes[6].filters[0] = BlockListFilter

spring.cloud.gateway.routes[11].id = aba-enduser-provider
spring.cloud.gateway.routes[11].uri = lb://aba-enduser-provider
spring.cloud.gateway.routes[11].predicates[0] = Path=/api/app/**
spring.cloud.gateway.routes[11].filters[0] = StripPrefix=1

spring.cloud.gateway.routes[13].id = aba-enduser-provider
spring.cloud.gateway.routes[13].uri = lb://aba-enduser-provider
spring.cloud.gateway.routes[13].predicates[0] = Path=/api/unlogin/open/user/**
spring.cloud.gateway.routes[13].filters[0] = JwtTokenFilter

spring.cloud.gateway.routes[18].id = aba-enduser-provider
spring.cloud.gateway.routes[18].uri = lb://aba-enduser-provider
spring.cloud.gateway.routes[18].predicates[0] = Path=/api/user/benefit/getUnionUserBenefit

spring.cloud.gateway.routes[19].id = aba-enduser-provider
spring.cloud.gateway.routes[19].uri = lb://aba-enduser-provider
spring.cloud.gateway.routes[19].predicates[0] = Path=/api/user/**

aba-enduser-provideraba-enduser-provider-dx分别是两个不同的服务,从其命名来看,应该是服务于不同的用户群体。面对不同的用户群体,创建不同的应用。一般而言,这两个应用里会有不少相似的业务逻辑代码(就比如@RequestMapping里配置的路径URL也会以enduser来命名),放在一个应用里,也不是不可以。缺点是可能会有比较复杂的if...else判断逻辑,改动一个用户群体的业务逻辑代码,可能会影响到另一个用户群体的业务规则。这得看不同公司的具体技术架构和业务架构设计。总之,这种情况是存在的。

另外,一个服务里有若干个不同的Controller,AController是一个业务模块,BController是另一个业务模块。设计良好(并且维护良好)的项目工程,AController和BController将是几乎各自独立的业务模块。可以使用ant path通配符,也就是上面代码片段里的user/**

然后,Gateway网关作为流量入口,网关服务里面肯定有各种过滤器,不同的过滤器起到不同的作用。也就是说,哪怕是设计再良好(维护非常良好)的项目工程,也不能确保AController下面的若干个接口都可以使用同样的一个或几个Filter。即,我们可能需要定义精确匹配路径,比如上面的/api/user/benefit/getUnionUserBenefit

另外,作为一条原则,Gateway网关配置有优先级概念,越靠前其优先级更高。如果一个请求与某个路由规则匹配成功,那么该路由规则就被选中,后面的路由规则将不再被考虑。也就是说,如果某个精确匹配路径,如/api/user/benefit/getUnionUserBenefit刚好也满足/api/user/benefit/**,就需要把精确匹配放在前面。

再考虑到代码维护,经过不同的Dev(每个Dev都有他自己编码的思考角度),业务的上线和下线,Controller接口的新增和废弃。

总之,接口定义、@RequestMapping配置URL和Gateway网关配置一定要慎重。尤其是网关配置。

理论知识

StripPrefix

StripPrefix这个Spring Cloud Gateway配置项作用是啥。搜索官方文档,StripPrefix参数表示在将请求发送到下游之前从请求中剥离的路径个数。

spring:
  cloud:
    gateway:
      routes:
        - id: aba-cbs-provider
          uri: http://cbs
          predicates:
          # - Path=/cbs/**
          filters:
            - StripPrefix=1

当通过网关服务向cbs服务发起api/cbs/area/getProvinceList发出请求时,转发到cbs服务的请求变成cbs/area/getProvinceList

StripPrefix=2时,则会从路径URLapi/cbs/area/getProvinceList里去掉2个前缀层级,即得到area/getProvinceList

路由规则优先级

在Spring Cloud Gateway中,路由规则的优先级由路由谓词的匹配顺序和路由规则的定义顺序决定。

路由谓词的匹配顺序

Spring Cloud Gateway会按照以下顺序对路由谓词进行匹配:

  • Cloud Foundry Route Service Route Predicate
  • Weight Route Predicate
  • Method Route Predicate
  • Path Route Predicate
  • Query Route Predicate
  • Header Route Predicate
  • Cookie Route Predicate
  • RemoteAddr Route Predicate
  • Host Route Predicate

这意味着,在路由规则中定义的路由谓词越靠前,它的匹配优先级就越高。

我们应用里,目前只用到Path路由谓词。

路由规则的定义顺序

在路由谓词的匹配顺序相同的情况下,路由规则的定义顺序将决定哪个规则被选中。如果多个路由规则匹配了同一个请求,那么将选择定义在路由规则列表中最前面的那个规则。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐