- 语言无关(这点对于我们Python+Java的后台系统很关键)
- 开发效率高、调试方便
- 接口的语义明确然而缺点也显而易见:基于HTTP的RPC在效率上不如传统的RPC。
在ModelService中,我们使用SpringMVC框架来实现RESTful接口。但是,在最近一次对ModelService的更新中我们发现SpringMVC的RESTful接口性能存在问题。
SpringMVC RESTful 性能优化
使用RESTful风格的接口有如下优势:
RESTful:
@RequestMapping(path = "/list/cityId/{cityId}", method = RequestMethod.GET)
@ResponseBody
public String getJsonByCityId(@PathVariable Integer cityId)
非RESTful:
@RequestMapping(path = "/list/cityId", method = RequestMethod.GET)
@ResponseBody
public String getJsonByCityId(@RequestParam Integer cityId)
我们使用Apache JMeter对SpringMVC RESTful接口与非RESTful接口进行了性能测试:
*并发量为200
*测试在同一台机器上进行,执行业务逻辑相同,仅接口不同。
*为了证明的确是SpringMVC造成的问题,我们使用了最简单的业务逻辑,直接返回字符串。
由结果可见,非RESTful接口的性能是RESTful接口的两倍,且请求的最大响应时间是35毫秒,有99%的请求在20毫秒内完成。相比之下,RESTful接口的最大响应时间是436毫秒。
由于ModelService是一个对并发性能要求极高的系统,且被多个上层业务系统所依赖,所有请求需在50ms内返回,若超时则会引起上层系统的read timeout,进而导致502。所以需要对这一情况进行优化。
方案一:将所有的url修改为非RESTful风格(不使用@PathVariable)
这是最直接的方式,也是最能保证效果的方式。但是这么做需要修改的是ModelService中已有的全部100+个接口,同时也要修改客户端相应的调用。修改量太大,而且极有可能由于写错URL导致404。更令人不爽的是这种修改会导致接口没有了RESTful风格。故该方案只能作为备选。
方案二:对SpringMVC进行改造
根据实际现象以及测试的结果,几乎可以确定的是问题出在SpringMVC的RESTful路径查找中。所以我们对SpringMVC中的相关代码进行了调查。
org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod
(spring-webmvc-4.2.3.RELEASE)
路径匹配的过程中有如下代码:
List matches = new ArrayList();
List directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
// No choice but to go through all mappings...
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
SpringMVC首先对HTTP请求中的path与已注册的RequestMappingInfo(经解析的@RequestMapping)中的path进行一个完全匹配来查找对应的HandlerMethod,即处理该请求的方法,这个匹配就是一个Map#get方法。若找不到则会遍历所有的RequestMappingInfo进行查找。
这个查找是不会提前停止的,直到遍历完全部的RequestMappingInfo。在遍历过程中,SpringMVC首先会根据@RequestMapping中的headers, params, produces, consumes, methods与实际的HttpServletRequest中的信息对比,剔除掉一些明显不合格的RequestMapping。如果以上信息都能够匹配上,那么SpringMVC会对RequestMapping中的path进行正则匹配,剔除不合格的。
接下来会对所有留下来的候选@RequestMapping进行评分并排序。最后选择分数最高的那个作为结果。所以使用非RESTful风格的URL时,SpringMVC可以立刻找到对应的HandlerMethod来处理请求。但是当在URL中存在变量时,即使用了@PathVariable时,SpringMVC就会进行上述的复杂流程。
从结果可见,这段匹配逻辑对性能的影响很大,URL数量越多,SpringMVC的性能越差,初步验证了我们从源码中得出的结论。在最近一次ModelService的更新中,接口数量翻了一倍,导致性能下降了一半,这也符合我们的结论。考虑到未来ModelService的接口必定会持续增加,我们肯定不能容忍在请求压力不断增加的情况下ModelService的性能反而不断下降的情况。所以现在我们要做的就是防止SpringMVC执行这种复杂的匹配逻辑,找到一种方式可以绕过它。
通过继承
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
我们可以实现自己的匹配逻辑。由于ModelService已经服务化,所以每个接口都有一个服务名,通过这个服务名即可直接找到对应的方法,并不需要通过@RequestMapping匹配的方式。而在服务消费端,由于服务消费端是通过服务名进行的方法调用,所以在服务消费端可以很直接地获取到服务名,把服务名加到HTTP请求的header中并不需要对代码进行大量的修改。
最终方案:
服务端:
- 在每个@RequestMapping中添加接口对应服务名的信息。
- 实现自己定义的HandlerMethod查询逻辑,在HandlerMethod注册时记录与之对应的服务名,在查询时通过HTTP请求头中的服务名查表获得HandlerMethod。
客户端:
- 调用服务时将服务名加入到HTTP请求头中
分析:
- 这样的查询时间复杂度是O(1)的,典型的空间换时间。理论上使用这样的查找逻辑的效率和非RESTful接口的效率是一样的。
- 由于HandlerMethod的注册是在服务启动阶段完成的,且在运行时不会发生改变,所以不用考虑注册的效率以及并发问题。
- SpringMVC提供了一系列的方法可以让我们替换它的组件,所以该方案的可行性很高。
实现细节:
我们要建立一个HandlerMethod与服务名的映射,保存在一个Map中。注意到在@RequestMapping中有一个name属性,这个属性并没有被SpringMVC用在匹配逻辑中。该属性是用来在JSP中直接生成接口对应的URL的,但是在AbstractHandlerMethodMapping.MappingRegistry中已经提供了一个name与Handler Method的映射,直接拿来用即可。所以我们只需要在每个接口的@RequestMapping中添加name属性,值为接口的服务名。在SpringMVC启动时会自动帮我们建立起一个服务名与Handler Method的映射。我们只要在匹配时从HTTP请求头中获取请求的服务名,然后从该Map中查询到对应的HandlerMethod返回。如果没有查询到则调用父类中的原匹配逻辑,这样可以保证不会对现有的系统造成问题。
*小细节:
因为RESTful接口存在@PathVariable,我们还需要调用handleMatch方法来将HTTP请求的path解析成参数。然而这个方法需要的参数是RequestMappingInfo,并不是HandlerMethod,SpringMVC也没有提供任何映射,所以我们还是要自己实现一个HandlerMethod => RequestMappingInfo的反向查询表。重写AbstractHandlerMethodMapping#registerMapping方法即可在@RequestMapping的注册阶段完成映射的建立。
1:自定义的MappingHandlerMapping
public class actMappingHandlerMapping extends RequestMappingHandlerMapping {
private static Map NAME_HANDLER_MAP = new HashMap();
private static Map MAPPING_HANDLER_MAP = new HashMap();
@Override
protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
RequestMapping rMapping = AnnotationUtils.getAnnotation(method, RequestMapping.class);
NAME_HANDLER_MAP.put(rMapping.name(), handlerMethod);
MAPPING_HANDLER_MAP.put(handlerMethod, mapping);
System.out.println("======================name=" + rMapping.name() + "=handlerMethod="
+ handlerMethod.toString());
super.registerHandlerMethod(handler, method, mapping);
}
@Override
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
String api = request.getHeader("api");
HandlerMethod handlerMethod = NAME_HANDLER_MAP.get(api);
if (StringUtils.isNotBlank(api) && handlerMethod != null) {
handleMatch(MAPPING_HANDLER_MAP.get(handlerMethod), lookupPath, request);
return handlerMethod;
}
return super.lookupHandlerMethod(lookupPath, request);
}
}
2:Spring-servlet.xml配置添加
3:对ajax支持可能需要这个拦截器
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException,
ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", "*");
httpResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
httpResponse.setHeader("Access-Control-Allow-Headers", "api");
if ("OPTIONS".equals(httpRequest.getMethod())) {
httpResponse.setStatus(204);
httpResponse.setHeader("Cache-Control", "no-cache");
}
filterChain.doFilter(request, response);
}
感谢达达技术分析