背景
Spring Cloud feign是僞RPC方式解決微服務間的調用。翻看FeignCloudFeign源碼,可以看到Feign默認使用HttpUrlConnection; 代碼在DefaultFeignLoadBalancedConfiguration 的Client.Default。
在springboot中HttpMessageConverters 默認使用jackson2方式進行序列化和反序列化,詳見 HttpMessageConvertersAutoConfiguration
正常情況下使用jackson2支持前後端開發基本沒有什麼問題,但是如果是微服務間頻頻通信,使用jackson2序列化和反序列化會佔用不少系統資源,並且效率較差。 這裏有個git地址來對比各種序列化和反序列化框架的性能 https://github.com/eishay/jvm-serializers/wiki,部分內容如下:
- Ser Time+Deser Time (ns)
- Size, Compressed size [light] in bytes
可見jackson在各種測試中都不佔優勢,但我們發現了protobuf性能均比較優益,考慮使用protobuf進行feign序列化和反序列化
客戶端項目添加feign配置支持proto
@Configuration
public class MyProtoFeignConfiguration {
//Autowire the message converters.
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
//add the protobuf http message converter
@Bean
ProtobufHttpMessageConverter protobufHttpMessageConverter() {
return new ProtobufHttpMessageConverter();
}
//override the encoder
@Bean
public Encoder springEncoder(){
return new SpringEncoder(this.messageConverters);
}
//override the encoder
@Bean
public Decoder springDecoder(){
return new ResponseEntityDecoder(new SpringDecoder(this.messageConverters));
}
}
客戶端項目聲明feign client
@FeignClient(name = "FEIGN-SERVICE2-TEST", fallback = FeignTestProtoFallback.class, configuration = MyProtoFeignConfiguration.class)
public interface FeignProtoTestClient {
@RequestMapping(value = "/replyProto", method = POST, consumes = "application/x-protobuf", produces = "application/x-protobuf")
HeaderReply requestMessage(@RequestParam("name") String name);
}
創建proto文件
在srm/main下新建proto文件夾,創建test_grpc.prot文件如下:
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.feign.test.feignservice2test.proto.dto";
//option java_outer_classname = "TestGrpcPerformance";
package com.feign.test.feignservice2test.proto.dto;
import "google/protobuf/timestamp.proto";
service TestPerformance {
rpc SayHello (TestRequest) returns (TestReply) {
}
}
message TestRequest {
string name = 1;
}
message HeaderReply {
repeated LineReply field1 = 1;
int32 field2 = 2;
google.protobuf.Timestamp field3 = 3;
string field4 = 4;
int32 field5 = 5;
string field6 = 6;
int32 field7 = 7;
string field8 = 8;
int32 field9 = 9;
string field10 = 10;
int32 field11 = 11;
string field12 = 12;
int32 field13 = 13;
string field14 = 14;
int32 field15 = 15;
string field16 = 16;
int32 field17 = 17;
string field18 = 18;
}
message LineReply {
repeated DistributionReply field1 = 1;
int32 field2 = 2;
google.protobuf.Timestamp field3 = 3;
string field4 = 4;
int32 field5 = 5;
string field6 = 6;
int32 field7 = 7;
string field8 = 8;
int32 field9 = 9;
string field10 = 10;
int32 field11 = 11;
string field12 = 12;
int32 field13 = 13;
string field14 = 14;
int32 field15 = 15;
string field16 = 16;
int32 field17 = 17;
string field18 = 18;
}
message DistributionReply {
string field1 = 1;
int32 field2 = 2;
google.protobuf.Timestamp field3 = 3;
string field4 = 4;
int32 field5 = 5;
string field6 = 6;
int32 field7 = 7;
string field8 = 8;
int32 field9 = 9;
string field10 = 10;
int32 field11 = 11;
string field12 = 12;
int32 field13 = 13;
string field14 = 14;
int32 field15 = 15;
string field16 = 16;
int32 field17 = 17;
string field18 = 18;
}
message TestReply {
HeaderReply field1 = 1;
int32 field2 = 2;
google.protobuf.Timestamp field3 = 3;
string name = 4;
}
添加POM依賴,以使用maven自動生成proto JAVA文件
添加pom依賴後,執行mvn install
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.M9</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java -->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.5.1</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.5.0</version>
<extensions>true</extensions>
<configuration>
<!--默認值-->
<protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>
<!--默認值-->
<!--<outputDirectory>${project.build.directory}/generated-sources/protobuf/java</outputDirectory>-->
<outputDirectory>${project.build.sourceDirectory}</outputDirectory>
<!--設置是否在生成java文件之前清空outputDirectory的文件,默認值爲true,設置爲false時也會覆蓋同名文件-->
<clearOutputDirectory>false</clearOutputDirectory>
<!--默認值-->
<temporaryProtoFileDirectory>${project.build.directory}/protoc-dependencies</temporaryProtoFileDirectory>
<!--更多配置信息可以查看https://www.xolstice.org/protobuf-maven-plugin/compile-mojo.html-->
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
<!--也可以設置成局部變量,執行compile或test-compile時才執行-->
<!--<configuration>-->
<!--<protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>-->
<!--<outputDirectory>${project.build.directory}/generated-sources/protobuf/java</outputDirectory>-->
<!--<temporaryProtoFileDirectory>${project.build.directory}/protoc-dependencies</temporaryProtoFileDirectory>-->
<!--</configuration>-->
</execution>
</executions>
</plugin>
</plugins>
</build>
客戶端添加controller請求
@RestController
public class FeignProtoController {
public static final int THREAD_NUM = 5;
public static final int CALL_TIMES = 100000;
@Autowired
FeignProtoTestClient feignProtoTestClient;
@RequestMapping("/testProto")
public ResponseEntity testProto() {
HeaderReply headerReply;
Date beginDate = new Date();
System.out.println("begin:" + beginDate);
for (int i = 0; i < CALL_TIMES; i++) {
headerReply = feignProtoTestClient.requestMessage("world:" + i);
//System.out.println("name:" + headerReply.getField4());
}
Date endDate = new Date();
System.out.println("end:" + endDate);
System.out.println("time:" + (endDate.getTime() - beginDate.getTime()) / 1000 + " s");
return ResponseEntity.ok().build();
}
}
服務端添加proto支持
在spring application下添加如下:
@Bean
ProtobufHttpMessageConverter protobufHttpMessageConverter() {
return new ProtobufHttpMessageConverter();
}
服務端響應proto報文內容
需要添加跟客戶端一樣的proto和pom依賴,執行mvn install
service內容如下
@Component
public class FeignReplyProtoServiceImpl {
public HeaderReply reply(String name){
List<LineReply> lineReplyList = new ArrayList<LineReply>();
for (int i = 0; i < 10; i++) {
List<DistributionReply> distributionReplyList = new ArrayList<DistributionReply>();
DistributionReply distributionReply = null;
for (int j = 0; j < 10; j++) {
distributionReply = DistributionReply.newBuilder().setField1("我是第一列").setField2(2).
setField3(Timestamps.fromMillis(System.currentTimeMillis())).setField4("我是第四列").setField5(5).setField6("我是第六列").
setField7(7).setField8("我是第八列").setField9(9).setField10("我是第十列").
setField11(11).setField12("我是第十二列").setField13(13).setField14("我是第十四列").
setField15(15).setField16("我是第十六列").setField17(17).setField18("我是第十八列").
build();
distributionReplyList.add(distributionReply);
}
LineReply lineReply = LineReply.newBuilder().addAllField1(distributionReplyList).setField2(2).
setField3(Timestamps.fromMillis(System.currentTimeMillis())).setField4("我是第四列").setField5(5).setField6("我是第六列").
setField7(7).setField8("我是第八列").setField9(9).setField10("我是第十列").
setField11(11).setField12("我是第十二列").setField13(13).setField14("我是第十四列").
setField15(15).setField16("我是第十六列").setField17(17).setField18("我是第十八列").build();
lineReplyList.add(lineReply);
}
HeaderReply headerReply = HeaderReply.newBuilder().addAllField1(lineReplyList).setField2(2).setField3(Timestamps.fromMillis(System.currentTimeMillis()))
.setField4(name).setField5(5).setField6("我是第六列").
setField7(7).setField8("我是第八列").setField9(9).setField10("我是第十列").
setField11(11).setField12("我是第十二列").setField13(13).setField14("我是第十四列").
setField15(15).setField16("我是第十六列").setField17(17).setField18("我是第十八列").build();
return headerReply;
}
}
controller如下:
@RestController
public class FeignProtoServerController {
@Autowired
FeignReplyProtoServiceImpl feignReplyProtoService;
@RequestMapping("/replyProto")
public com.feign.test.feignservice2test.proto.dto.HeaderReply replyProto(String name) {
return feignReplyProtoService.reply(name);
}
}
執行客戶端請求代碼
使用postman發送請求 localhost:10001/testProto
經過10W次數請求測試,使用proto響應時間比feign用jackson2大概提升10%,但CPU等資源使用明顯降低。