容易被忽視的后端服務(wù) chunked 性能問題
- 背景
- spring boot 創(chuàng)建的默認(rèn) spring mvc 項(xiàng)目
- 集成 JAX-RS 規(guī)范框架 Jersey
背景
在之前的一次性能壓測(cè)的時(shí)候我們發(fā)現(xiàn)一個(gè)細(xì)節(jié)問題,我們使用 spring boot 創(chuàng)建的 web rest 項(xiàng)目,使用默認(rèn) spring mvc 作為 web rest 框架。
這在使用上沒有太大問題,但是有一個(gè)影響性能的細(xì)節(jié)問題被發(fā)現(xiàn)了,說實(shí)話這個(gè)問題很難被發(fā)現(xiàn)。
spring boot創(chuàng)建的默認(rèn)spring mvc項(xiàng)目
我們來看一個(gè)簡(jiǎn)單的 demo ,我使用 IDEA 創(chuàng)建一個(gè) spring boot 項(xiàng)目,創(chuàng)建過程中沒有什么特別的選項(xiàng)需要調(diào)整,一路 next 。然后我們創(chuàng)建一個(gè)簡(jiǎn)單的 controller 。
- package springboot.demo.controller;
- import org.springframework.web.bind.annotation.PathVariable;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RestController;
- import springboot.demo.model.User;
- /**
- * Created by plen on 2017/11/25.
- */
- @RestController
- public class SpringMvcController {
- @RequestMapping("/user/{id}")
- public User hello(@PathVariable Long id) {
- User user = new User();
- user.setID(id);
- user.setUserName("mvc.");
- return user;
- }
- }
再創(chuàng)建一個(gè)簡(jiǎn)單的 model 。
- package springboot.demo.model;
- import lombok.Data;
- import lombok.EqualsAndHashCode;
- /**
- * Created by plen on 2017/11/25.
- */
- @Data
- @EqualsAndHashCode
- public class User {
- private Long ID;
- private String userName;
- }
然后啟動(dòng)訪問這個(gè) controller ,注意看下返回的 http 信息里多了一個(gè) Transfer-Encoding:chunked 。 Transfer-Encoding:chunked 在 HTTP 協(xié)議里的意思是無法計(jì)算 Content-Length 長(zhǎng)度,需要分塊傳輸。
這是 spring mvc 的默認(rèn) complex object 傳輸方式,如果我們返回的是一個(gè)簡(jiǎn)單的對(duì)象就不會(huì)有這個(gè)問題。
Transfer-Encoding:chunked帶來的性能問題就是訪問一次數(shù)據(jù)需要不止一次的 http 請(qǐng)求,而一次 http 請(qǐng)求的成本也是比較大的。
(我沒有通過抓包工具來測(cè)試具體哪種對(duì)象大小需要訪問幾次,感興趣的可以自己試下。)
集成JAX-RS規(guī)范框架Jersey
解決這個(gè)問題兩個(gè)層面都可以,一種是采用比較粗暴的方式在 servlet 容器層面解決,但是這個(gè)會(huì)帶來一個(gè)后果就是當(dāng)我們計(jì)算 complex object 大小的時(shí)候會(huì)比較復(fù)雜而且容易出錯(cuò),也會(huì)影響項(xiàng)目未來的分塊傳輸功能,效果不太好。
還有一種就是在應(yīng)用層面解決,比較柔性也易于擴(kuò)展,我們可以集成一個(gè) rest 框架,最好是符合 JAX-RS 規(guī)范,本文我們集成 Jersey 框架。
jersey集成如果通過 __@Component_ _ 方式那么 jersey 會(huì)默認(rèn)接管所有的 web servlet 請(qǐng)求處理,所以就需要我們手動(dòng)的配置專門用來處理 jersey servlet 的容器。
spring boot解決了以前 spring 繁重的配置,提供了 auto config 功能,原來通過 web.xml 配置 servlet 的,現(xiàn)在需要用代碼來配置。 spring boot 提供了讓我們手動(dòng)注冊(cè) servlet bean 的方式。
- org.springframework.boot.web.servlet.ServletRegistrationBean
ServletRegistrationBean 可以讓我們注冊(cè)servlet,我們來看下完整代碼。
- package springboot.demo.config;
- import org.glassfish.jersey.servlet.ServletContainer;
- import org.glassfish.jersey.servlet.ServletProperties;
- import org.springframework.boot.web.servlet.ServletRegistrationBean;
- import org.springframework.context.annotation.Bean;
- import org.springframework.stereotype.Component;
- /**
- * Created by plen on 2017/11/25.
- */
- @Component
- public class JerseyServletBeanConfig {
- @Bean
- public ServletRegistrationBean jerseyServlet() {
- ServletRegistrationBean registrationBean = new ServletRegistrationBean(new ServletContainer(), "/rest/v1/*");
- registrationBean.addInitParameter(ServletProperties.JAXRS_APPLICATION_CLASS, JerseyResourceConfig.class.getName());
- return registrationBean;
- }
- }
這和原來在 web.xml 配置的是一樣的,設(shè)置 routing 地址,設(shè)置 Init 初始化參數(shù),對(duì)應(yīng)的 servlet class name 。
所有的 __"rest/v1/*"__ 請(qǐng)求都將被 ServletContainer jersey servlet 容器接管。
- package springboot.demo.config;
- import org.glassfish.jersey.server.ResourceConfig;
- import org.glassfish.jersey.server.spring.scope.RequestContextFilter;
- import springboot.demo.controller.JerseyController;
- /**
- * Created by plen on 2017/11/25.
- */
- public class JerseyResourceConfig extends ResourceConfig {
- public JerseyResourceConfig() {
- register(JerseyController.class);
- register(RequestContextFilter.class);
- }
- }
ResourceConfig其實(shí)是一個(gè) jersey Application 類型。這是 __jersey 注冊(cè) servlet 時(shí)規(guī)定的。
- package springboot.demo.controller;
- import springboot.demo.model.User;
- import javax.ws.rs.GET;
- import javax.ws.rs.Path;
- import javax.ws.rs.PathParam;
- import javax.ws.rs.Produces;
- import javax.ws.rs.core.MediaType;
- /**
- * Created by plen on 2017/11/25.
- */
- @Path("/user/")
- public class JerseyController {
- @Path("{id}")
- @GET
- @Produces(MediaType.APPLICATION_JSON)
- public User hello(@PathParam("id") Long id) {
- User user = new User();
- user.setID(id);
- user.setUserName("jersey.");
- return user;
- }
- }
這是我們應(yīng)用代碼 Controller ,使用 JAX-RS 規(guī)范的注解進(jìn)行設(shè)置即可。
這樣就解決了 sprng mvc 和 jersey rest 共同存在的問題,我們也不需要將所有的返回 chunked 的接口都改成 JAX-RS 的 rest 服務(wù),只需要將有性能瓶頸的接口改造下即可。