笔者刚入职新公司领导让针对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性能不达标。现有的2核4G单机QPS能达到2000。你重构的只能达到七八百。当时内心数万个草泥马在奔腾。
没办法各种百度寻找优化方案。试过换各种web容器。由tomcat换到jetty再到undertow。试过配置各种参数。然而并没有什么提升。看到一篇文章说可以使用异步请求如图四。先释放容器分配给请求的线程与相关资源,减轻系统负担,释放了容器所分配线程的请求,其响应将被延后,可以在耗时处理完成时再对客户端进行响应。顿时喜出望外,以为找到了解决的办法。然而并没有什么卵用。一度怀疑最初的选型是错误的。但是我想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等等。都多少影响性能。但为了增加必要功能,损失些性能也是可以接受的,有些不必要的损失性能还是要找到根源解决掉,笔者遇到的情况未必适合所有人。不过可以给那些想提升性能的朋友提供一些思路。