Spring boot性能优化

笔者刚入职新公司领导让针对api项目进行重构,由于当前系统用play框架写的加上历史遗留原因,造成当前的api项目难以维护以及部署。重构便成了迫在眉睫的事。由于公司的业务性质,要求单台机器api的吞吐量很高,大家都知道springboot的好处,可以快速搭建起web服务。所以在选型时笔者只是写了个无业务逻辑的接口然后简单的用ab命令对这个接口进行了性能压测。因为笔者认为吞吐量问题springboot可以完全胜任。没有过多的考虑性能不达标的问题。

于是笔者便开开心心的按照老系统的逻辑进行重构。根据需求接口返回类型需要根据请求后缀是json还是xml提供相应的返回数据格式。其他后缀结尾的或者没有后缀的返回错误码。笔者当时想到两种方案。一种是直接在@RequestMapping注解中通过value设置支持的后缀格式。如:@RequestMapping(value = {"/ping.json", "/ping.xml"}, method = RequestMethod.GET)
另一种是在@RequestMapping中不设置后缀。通过实现WebMvcConfigurer配置类。实现configurePathMatch方法开启后缀匹配。实现configureContentNegotiation方法根据后缀进行返回格式设置。然后再写个拦截器对非json和xml结尾的请求进行拦截。

@Api(tags = "ping接口")
@RestController
@Timed(percentiles = {0.9, 0.95, 0.99})
public class Ping {

    @ApiOperation(value = "Ping接口")
    @RequestMapping(value = {"/ping"}, method = {RequestMethod.POST, RequestMethod.GET})
    public BaseResponse ping() {
        BaseResponse baseResponse = new BaseResponse();
        baseResponse.setCode(Constants.HTTP_SUCCESS_CODE);
        return ResultWrap.ok(baseResponse);
    }
}
@Configuration
public class WebConfiguration implements WebMvcConfigurer {

@Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        // 开启匹配后缀
        configurer.setUseSuffixPatternMatch(true);
        // 设置 path 大小写不敏感
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        antPathMatcher.setCaseSensitive(false);
        configurer.setPathMatcher(antPathMatcher);
    }

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.favorPathExtension(true)
            .parameterName("mediaType")
            .defaultContentType(MediaType.APPLICATION_JSON)
            .mediaType("xml", MediaType.APPLICATION_XML)   //当后缀名称为xml的时候返回xml数据(暂时不支持)
            .mediaType("json", MediaType.APPLICATION_JSON);//当后缀名称是json的时候返回json数据
    }

}
@Component
public class RequestSuffixInterceptor extends BaseInterceptor {

    public static final String SUFFIX_JSON = ".json";
    public static final String SUFFIX_XML = ".xml";
    private final Logger logger = LoggerFactory.getLogger(RequestSuffixInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
        Object handler) throws Exception {
 
        String uri = request.getRequestURI();
        if (!StringUtils.endsWithIgnoreCase(uri, SUFFIX_JSON) && !StringUtils.endsWithIgnoreCase(uri, SUFFIX_XML)) {
            logger.error("非法请求后缀 uri:[{}]", uri);
            HttpResponseUtil.sendStringMessage(response, uri);
            return false;
        }
        return true;
    }
}

为了简单少写代码。笔者选择了第二种方式实现。然后就开启了撸代码的模式。在完成所有开发任务,进入测试阶段时。测试小朋友跑过来跟我说:少年你重构的api性能不达标。现有的2核4G单机QPS能达到2000。你重构的只能达到七八百。当时内心数万个草泥马在奔腾。

没办法各种百度寻找优化方案。试过换各种web容器。由tomcat换到jetty再到undertow。试过配置各种参数。然而并没有什么提升。看到一篇文章说可以使用异步请求。先释放容器分配给请求的线程与相关资源,减轻系统负担,释放了容器所分配线程的请求,其响应将被延后,可以在耗时处理完成时再对客户端进行响应。

@Api(tags = "ping接口")
@RestController
@Timed(percentiles = {0.9, 0.95, 0.99})
public class Ping {

    @ApiOperation(value = "异步Ping接口")
    @RequestMapping(value = {"/async/ping"}, method = {RequestMethod.POST, RequestMethod.GET})
    public Callable<BaseResponse> asyncPing() {
        return () -> {
            BaseResponse baseResponse = new BaseResponse();
            baseResponse.setCode(Constants.HTTP_SUCCESS_CODE);
            return ResultWrap.ok(baseResponse);
        };
    }
}

顿时喜出望外,以为找到了解决的办法。然而并没有什么卵用。一度怀疑最初的选型是错误的。但是我想springboot的性能应该不能这么不堪吧。于是便开始查找自己的代码。跟踪线程耗时方法。
有过性能调优的同学应该都熟悉 jvisualvm,jdk自带监控程序。可以监控本地或远端cpu、内存、线程等实时动态信息。以及对线程进行快照。对线程内方法调用耗时统计等功能。非常强大。笔者用的是undertow做为web容器。可以看到它有跟netty类似的IO模型,IO线程负责接收请求,然后把请求放到任务池中,由后面的任务线程进行处理。这也解释了为什么我之前用异步请求没有提升性能的原因。因为本身undertow已经是异步的了。自己再进行异步操作毫无意义。tomcat也是同样的道理。tomcat7以上默认支持NIO,所以自己再实现异步请求操作没有什么意义。

图一

图二
然后我用wrk命令进行压测,看下任务线程中哪些操作是比较耗时的,wrk -t 10 -c 500 -d 15s --latency -s http://127.0.0.1:2551/ping.json。10个线程500个连接,持续15秒。可以看到没有任何业务逻辑的接口QPS只有1715。

图七
对任务线程抽样进行快照。
图八
展开其中一个线程任务。查看耗时的调用方法。
图九
如图中DispatcherServlet在调用doDispatch方法占用了64.2%的时间。一个doDispatch怎么会用这么多的时间呢?继续追踪方法内调用getHander,最后耗时在getMatchingCondition中。
图十
查看源码从doDispatch开始跟踪,发现当程序启动时会把@RequestMapping注解的path放到map集合中,当有请求时,先去map中获取对应的路径,如果有则返回方法,没有则根据设置的后缀匹配规则进行遍历匹配。其中画框的属性是不是很熟悉。对,它就是实现WebMvcConfigurer时设置的配置。 如写的是@RequestMapping(value = {"/ping"}, method = {RequestMethod.GET}) ,但请求的是/ping.json,第一次查找在集合中没有以/ping.json为path的方法,就会遍历所有路径集合进行拆分后缀匹配。直到匹配到为止。笔者的项目中有300个接口,500多个路径。如果不显示的给出后缀,每次请求都会遍历一遍这500多个路径,造成耗时。

图十一
图十二
图十三
最后猜想是匹配路径耗时导致吞吐量变低。于是把注解中路径后缀显示给出 ,@RequestMapping(value = {"/ping.json", "/ping.xml"}, method = {RequestMethod.GET}) 再进行一次压测。结果QPS为9384,翻了4倍多。到此为止才算把性能提升上来。符合上线标准。

图十四

此次调优过程中发现还有好多需要优化的地方,比如日志,集成的swagger,actuator等等。都多少影响性能。但为了增加必要功能,损失些性能也是可以接受的,有些不必要的损失性能还是要找到根源解决掉,笔者遇到的情况未必适合所有人。不过可以给那些想提升性能的朋友提供一些思路。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值