背景

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等资源使用明显降低。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐