微服务使用RESTful时遇到的坑

RESTfulAPI序列化和反序列化遇到的坑

Posted by Yezhiwei on July 16, 2020

微服务架构

概念

一个大型复杂的业务系统由一个或多个微服务组成。系统中的各个微服务可被独立部署,各个微服务之间是松耦合的。每个微服务仅关注于完成一件任务并很好地完成该任务。每个任务代表着一个小的业务能力,组合或者复用这些微服务的能力,支撑上层大型复杂的业务系统。

解决方案

目前流行的微服务架构是基于开源的 Spring Cloud 实现,它提供了一整套的解决方案——服务注册与发现,服务消费,服务保护与熔断,网关,分布式调用追踪,分布式配置管理等。

Spring Boot 是 Spring 的一套快速配置脚手架,使用默认大于配置的理念,用于快速开发单个微服务。提供 HTTP 协议的 RESTful 接口。

Spring Cloud 组件架构

流程:

  1. 请求统一通过 API 网关(Zuul)来访问内部服务。
  2. 网关接收到请求后,从注册中心(Eureka)获取可用服务。
  3. 由 Ribbon 进行均衡负载后,分发到后端具体实例。
  4. 微服务之间通过 Feign 进行通信处理业务。
  5. Hystrix 负责处理服务超时熔断。
  6. Turbine 监控服务间的调用和熔断相关指标。

微服务间通信

基于 HTTP 的 REST 方式

Spring Cloud 采用的是基于 HTTP 的 REST 方式,相比于 Dubbo RPC,更加轻量化和灵活,有利于跨语言服务的实现,以及服务的发布部署,但是 REST 服务调用性能会比 RPC 低一些。

HTTP 序列化和反序列化

上篇文章中学习到,当我们需要将内存中的对象持久化到磁盘,数据库中时,当我们需要与浏览器进行交互时,当我们需要实现 RPC 时,这个时候就需要序列化和反序列化了。

在使用 Spring Boot 开发微服务时,通过 @RestController 注解进行 Json 的序列化和反序列化操作。这里实际上已经体现了 HTTP 序列化和反序列化的过程,而这里其实就是 HttpMessageConverter 发挥着作用。在报文到达 SpringMVC / SpringBoot 和从 SpringMVC / SpringBoot 出去,都存在一个字符串和 Java 对象间转化的问题。这一过程,在 SpringMVC / SpringBoot 中,是通过 HttpMessageConverter 来解决的。

FastJson 和 Jackson 的序列化和反序列化交叉使用

发现接口访问很慢

经过排查发现服务A的接口调用另一个服务B的接口,接口调用关系如下图

分别查询方法请求时间

1、在服务A中检测到的searchNotices()方法的请求慢时间(511.2513ms)如下:

com.gemantic.semantic.datacenter.controller.CompanyProceedingController:searchNotices()

---[1452.6121ms] com.gemantic.semantic.datacenter.controller.CompanyProceedingController:searchNotices()

   +---[50.4082ms] com.gemantic.semantic.datacenter.repository.TqOaStcodeRepository:findBySetypeAndEnddateAndSymbol() #93

   +---[461.2263ms] com.gemantic.semantic.datacenter.repository.CompanyProceedingRepository:findAll() #148

   +---[251.5718ms] com.gemantic.semantic.datacenter.repository.ComProceedingDetailRepository:findAllByIdIn() #158

   +---[511.2513ms] com.gemantic.semantic.datacenter.rest.repository.GreatWisdomDocRepository:existByCaseNumbers() #226

2、在服务B中检测到被调用的方法existByCaseNumbersPost()方法的请求时间(77.0756ms)如下:


 ---[77.0756ms] com.gemantic.greatwisdom.controller.LegalProceedingDetailController:existByCaseNumbersPost()

        +---[0.1424ms] org.apache.commons.logging.Log:info() #73

        +---[71.7306ms] com.gemantic.greatwisdom.repository.LegalProceedingDetailRepository:findAllByCrIdIn() #74

        `---[0.4626ms] com.gemantic.springcloud.model.Responses:ok() #76

问题的原因分析

  1. 网络的问题可以基本排除,因为在同一台机器上进行联调测试的

  2. 服务之间的额外处理(序列化和反序列化),如下图

  • 在 Spring 的处理过程中,一次请求报文和一次响应报文,分别被抽象为一个请求消息 HttpInputMessage 和一个响应消息 HttpOutputMessage
  • 处理请求时,由合适的消息转换器将请求报文绑定为方法中的形参对象,在这里同一个对象就有可能出现多种不同的消息形式,如json、xml。同样响应请求也是同样道理。
  • 在 Spring 中,针对不同的消息形式,有不同的 HttpMessageConverter 实现类来处理各种消息形式,至于各种消息解析实现的不同,则在不同的 HttpMessageConverter 实现类中。

观察项目的配置发现两个服务配置的不一样

服务 A 配置如下:

@Bean
public 
FastJsonHttpMessageConverter fastJsonpHttpMessageConverter() {
    FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
    FastJsonConfig fastJsonConfig = new FastJsonConfig();
    fastJsonConfig.setSerializerFeatures(
            SerializerFeature.DisableCircularReferenceDetect,
            SerializerFeature.BrowserSecure);
    fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
 
    List<MediaType> supportedMediaTypes = new ArrayList<>();
    supportedMediaTypes.add(MediaType.APPLICATION_JSON);
    supportedMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
    supportedMediaTypes.add(MediaType.APPLICATION_ATOM_XML);
    supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
    supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
    supportedMediaTypes.add(MediaType.APPLICATION_PDF);
 
    supportedMediaTypes.add(MediaType.APPLICATION_XHTML_XML);
    supportedMediaTypes.add(MediaType.APPLICATION_XML);
    supportedMediaTypes.add(MediaType.IMAGE_GIF);
    supportedMediaTypes.add(MediaType.IMAGE_JPEG);
    supportedMediaTypes.add(MediaType.IMAGE_PNG);
 
    supportedMediaTypes.add(MediaType.TEXT_HTML);
    supportedMediaTypes.add(MediaType.TEXT_MARKDOWN);
    supportedMediaTypes.add(MediaType.TEXT_PLAIN);
    supportedMediaTypes.add(MediaType.TEXT_XML);
    fastJsonHttpMessageConverter.setSupportedMediaTypes(supportedMediaTypes);
 
    return fastJsonHttpMessageConverter;
}

服务 B 配置如下:

@Bean
public HttpMessageConverters jsonHttpMessageConverters() {
   return new HttpMessageConverters(false, Collections
         .singleton(new MappingJackson2HttpMessageConverter()));
}

实验和结论

将两个服务的 Json 序列化和反序列化进行统一配置再次测试,请求的时间大幅缩减了,所以可以将大部分的时间消耗原因确定为序列化的问题

推荐阅读:

Java的POJO类为什么要实现Serializable接口

Java几种常用JSON库性能比较

如果觉得还有帮助的话,你的关注和转发是对我最大的支持,O(∩_∩)O: