《SpringBoot2从入门到工程实战》第十四篇:gRPC的接入

最近在改造公司的业务过程中,之前一直使用的是HTTP接口,接口性能基本在10-12 ms之间,对于短连接调用来说,性能其实已经算非常高的了,但是实际中发现,业务端的代码,写的实在是让人崩溃,部分业务接口,对于基础接口的调用,超过了20次+,如果每一个接口都是10-12 ms, 光是这些基础接口的调用,都是一件非常消耗时间的事情,没有办法,只好引入RPC框架了。

因为业务端是PHP 7.1,需要达到跨语言调用,所以目前合适的,比较成熟的也就是thrift、grpc两种了,其实dubbo也有一个基于swoole的php实现,但是经过测试,发现官方的框架,实在是太过随意了,怕有大坑,不敢使用。

经过权衡,还是最终选用了gRPC,所以本文还是以gRPC作为实践。关于gRPC与thrift之间的功能、性能差异,可以在网上找一找其他博主的文章。

本章示例工程名称:springboot_worker_grpc

代码地址:https://github.com/stamhe/SpringBoot-Work-Example

目录结构如下:


pom.xml内容:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.stamhe</groupId>
  <artifactId>springboot_worker_grpc</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>springboot_worker_grpc</name>
  <url>http://maven.apache.org</url>

  <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
        
        <grpc.version>1.0.0</grpc.version>
        <protobuf.plugin.version>0.5.0</protobuf.plugin.version>
        <protoc.version>3.7.1</protoc.version>
  </properties>
  
  
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

  <dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-thymeleaf</artifactId>  
    </dependency> 
    
    <dependency>
  		<groupId>net.devh</groupId>
  		<artifactId>grpc-spring-boot-starter</artifactId>
  		<version>2.3.0.RELEASE</version>
	</dependency>
	
	
    <dependency>
  		<groupId>com.alibaba</groupId>
  		<artifactId>fastjson</artifactId>
  		<version>1.2.56</version>
	</dependency>
	
	
    <dependency>
    	<groupId>de.codecentric</groupId>
    	<artifactId>spring-boot-admin-starter-client</artifactId>
    	<version>2.1.4</version>
	</dependency>
	
	<dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-security</artifactId>
	</dependency>
    
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>
  
  <build>
  		<extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.5.0.Final</version>
            </extension>
        </extensions>
        
        <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>${protobuf.plugin.version}</version>
                <configuration>
                	<protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>
                    <protocArtifact>com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}</protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
                    
                    <outputDirectory>${project.basedir}/src/main/java</outputDirectory>
                    <!-- 
                    巨坑的地方,一定要设置为false,否则上面设置的 src/main/java 目录的代码会全部被删除.
                    https://github.com/xolstice/protobuf-maven-plugin/issues/16
                    -->
                     <clearOutputDirectory>false</clearOutputDirectory>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

注意:

1、使用了grpc-spring-boot-starter,免得自己手动集成很多东西

2、proto定义文件放置在 src/main/proto目录, 并在pom.xml添加自动生成命令即执行mvn compile 的时候,自动在 src/main/java目录生成protobuf和stub文件

3、clearOutputDirectory一定要配置为 false, 否则 src/main/java 目录代码会被删除。。。

App.java 内容:

package com.stamhe.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App 
{
    public static void main( String[] args )
    {
    	SpringApplication.run(App.class, args);
    }
}

UserModel.java内容:

package com.stamhe.springboot.user.model;

import java.io.Serializable;

public class UserModel implements Serializable {
	private Long user_id;
	private String name;
	private String createTime;
	public Long getUser_id() {
		return user_id;
	}
	public void setUser_id(Long user_id) {
		this.user_id = user_id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getCreateTime() {
		return createTime;
	}
	public void setCreateTime(String createTime) {
		this.createTime = createTime;
	}
	@Override
	public String toString() {
		return "UserModel [user_id=" + user_id + ", name=" + name + ", createTime=" + createTime + "]";
	}
}

UserService.java内容:

package com.stamhe.springboot.user.service;

import com.alibaba.fastjson.JSON;
import com.stamhe.springboot.common.proto.CommonReply;
import com.stamhe.springboot.user.model.UserModel;
import com.stamhe.springboot.user.proto.UserGrpc;
import com.stamhe.springboot.user.proto.UserRequest;

import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;

@GrpcService
public class UserService extends UserGrpc.UserImplBase {
	@Override
	public void userInfo(UserRequest request, StreamObserver<CommonReply> responseObserver) {

		UserModel userModel = new UserModel();
		userModel.setUser_id(request.getUserId());
		userModel.setName("Stam He");
		userModel.setCreateTime("2019-04-17 12:00:00");
		
		String json_data = JSON.toJSONString(userModel);
		
		CommonReply reply = CommonReply.newBuilder().setCode(0).setMessage("Success From UserService")
				.setData(json_data).build();
		
        responseObserver.onNext(reply);
        responseObserver.onCompleted();
	}
}

application.properties

grpc.server.port=9090
grpc.server.address=0.0.0.0

server.port=8080

我们自定义了grpc的端口为 9090

com.stamhe.springboot.*.proto 包下面的文件,都是自动生成的 protobuf和stub文件,此处不再列出。

到此,java相关的gRPC部分已经完全开发完成了。

从proto文件生成PHP代码,需要依赖 protoc、grpc_php_plugin这两个工具

安装方式如下:

git clone https://github.com/grpc/grpc.git
cd grpc
git submodule update --init
make grpc_php_plugin -j4

cp bins/opt/grpc_php_plugin   /usr/bin/
cp bins/opt/protobuf/protoc   /usr/bin/

grpc代码库比较大, 比较耗时,如果是在国内,最好开稳定的vpn.

PHP使用gRPC,需要依赖 2 个扩展,一个是grpc扩展,一个是protobuf扩展,编译安装方式如下(假设php安装在/opt/php7目录下):

wget -c "http://pecl.php.net/get/grpc-1.20.0RC3.tgz"
tar xvf grpc-1.20.0RC3.tgz
cd grpc-1.20.0RC3
/opt/php7/bin/phpize
./configure --with-php-config=/opt/php7/bin/php-config && make -j4 && make install

wget -c "http://pecl.php.net/get/protobuf-3.7.1.tgz"
tar xvf protobuf-3.7.1.tgz
cd protobuf-3.7.1
/opt/php7/bin/phpize
./configure --with-php-config=/opt/php7/bin/php-config && make -j4 && make install

然后在/opt/php7/etc/php.ini 中,添加上下面两个配置即可:
extension=grpc.so
extension=protobuf.so

配置好后,下面命令就可以看到两个扩展了
/opt/php7/bin/php -m |grep -E "grpc|protobuf"

使用proto文件,生成php使用的protobuf和stub文件,命令如下:

cd /data/lib
protoc --php_out=/data/lib   --grpc_out=/data/lib   --plugin=protoc-gen-grpc=/usr/bin/grpc_php_plugin   User.proto

则在 /data/lib就有我们需要的protobuf和stub php相关文件了。

为了使用方便,我们引入 composer 的autoload功能来管理所有php文件的加载

composer的安装命令如下:

wget https://dl.laravel-china.org/composer.phar -O /usr/local/bin/composer && chmod a+x /usr/local/bin/composer

composer.json 配置如下:

{
        "name" : "grpc-java/php",
        "require" : {
                "grpc/grpc" : "^v1.3.0",
                "google/protobuf" : "^v3.3.0"
        },
        "autoload" : {
                "classmap" : [
                        "lib/"
                ]
        }
}

放置此 composer.json 文件到 /data 目录, 执行下面的命令更新 autoload 文件列表

composer update

在 /data 目录,新建 main-autoload.php 文件, 内容如下:

<?php
require_once  __DIR__ . '/vendor/autoload.php';

use \Com\Stamhe\Springboot\User\Proto\UserClient;
use \Com\Stamhe\Springboot\User\Proto\UserRequest;

use \Com\Stamhe\Springboot\Common\Proto\CommonReply;

$start_time = microtime(true);

$obj = new UserClient('127.0.0.1:9090', [
    'credentials' => \Grpc\ChannelCredentials::createInsecure(),
    'timeout' => 1000,
]);


$req = new UserRequest();
$req->setUserId(10000);
$rsp = $obj->UserInfo($req)->wait();

$end_time = microtime(true);
printf("start_time = %s end_time = %s 【diff = %s】 ms\n", $start_time, $end_time, ($end_time - $start_time) * 1000.0);

list($rsp_data, $rsp_status) = $rsp;
var_dump($rsp_status);
var_dump($rsp_data->getCode());
var_dump($rsp_data->getMessage());
var_dump($rsp_data->getData());

启动 springboot 项目, 然后去 /data 目录执行 php main-autoload.php , 可得到如下结果:

start_time = 1555487705.4756 end_time = 1555487705.4959 【diff = 20.330905914307】 ms
object(stdClass)#12 (3) {
  ["metadata"]=>
  array(0) {
  }
  ["code"]=>
  int(0)
  ["details"]=>
  string(0) ""
}
int(0)
string(24) "Success From UserService"
string(69) "{"createTime":"2019-04-17 12:00:00","name":"Stam He","user_id":10000}"

说明:

1、rpc调用的请求成功与否,使用$rsp_status->code == 0 来确认

2、业务端的请求是否有问题,使用$rsp_data->getCode() 来确认

gRPC 的错误码说明文档:

https://github.com/grpc/grpc/blob/master/doc/statuscodes.md

完毕。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据