Spring Cloud Feign支持protobuf

背景

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) image
  • Size, Compressed size [light] in bytes image

可見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>
                        <!--也可以設置成局部變量,執行compiletest-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等資源使用明顯降低。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章