作者:亚瑟、文远

1. 微服务框架 -- 从系统怪物到服务小怪兽

一个小巧的单体应用会随着公司业务的扩张而慢慢成长,逐渐演化成一个庞大且复杂的系统怪物,系统任何一处的问题都将影响整个怪物的表现,很少有单独的开发者能理清系统怪物所有的肌理脉络,导致bug的定位和新功能的扩展都变得越来越困难,对系统的任一改动都要求整个怪物一起回归测试并重新部署,效率必然不高。所以公司发展到了一定阶段,总会需要从架构上寻找解决系统怪物之道,而微服务就是目前最流行的架构方案之一,它将系统怪物拆分成多个独立自治的服务小怪兽,让我们有能力分而治之。
[插画:无数小怪兽组成一座大怪兽形状的山,小怪兽正一个个从山上滚下来]

2. RPC框架 -- 小怪兽的电报员

一旦系统怪物被拆分成了多个服务小怪兽,小怪兽们如何沟通协作就成了我们最关心的问题。服务小怪兽间的通信就好像发电报一样,涉及到数据序列化、反序列化、连接管理、收发线程、超时处理等多个问题,RPC框架的出现解决了这些问题,就好像通过电报员发电报一样,使用RPC框架让小怪兽们不必关心通信的底层细节。
[插画:小怪兽在请电报员发电报]

RPC调用细节

  1. 服务消费方(小怪兽A)以本地调用方式调用服务
  2. client stub(小怪兽A的电报员)接受到调用后负责将方法、参数等编码成能够进行网络传输的消息体(电报)
  3. client stub(小怪兽A的电报员)找到服务地址,并将消息发送到服务端
  4. server stub(小怪兽B的电报员)收到消息(电报)后进行解码
  5. server stub(小怪兽B的电报员)根据解码结果调用本地的服务(小怪兽B)
  6. 本地服务(小怪兽B)执行并将结果返回给server stub(小怪兽B的电报员)
  7. server stub(小怪兽B的电报员)将结果编码成消息(电报)并发送至客户端
  8. client stub(小怪兽A的电报员)接受到消息(电报)并进行解码
  9. 服务消费方(小怪兽A)得到最终的结果

3. gRPC -- 这位电报员是语言天才

如果通信的小怪兽们语言不通,那么我们需要对电报员(亦即RPC框架)的人选提出更高的要求,无论小怪兽们用的是什么语言,协助通信的两位电报员都必须把它们的话翻译成电报员彼此能理解的同一种语言,亦即IDL(Interface Description Language),是的,电报员在这种情况下还必须承担翻译的角色,而gRPC就是一位如此优秀的电报员。
[插画:小怪兽说"今晚的月色真美",电报员写”I Love you“]

4. gPRC Demo

实现Node客户端小怪兽发送"今晚的月色真美",Java服务端小怪兽收到电报内容,并回复"I love you too"。

  1. 通过Spring Boot创建Java项目,pom.xml中加入如下依赖

        <dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-test</artifactId>            <scope>test</scope>        </dependency>        <dependency>            <groupId>io.grpc</groupId>            <artifactId>grpc-netty-shaded</artifactId>            <version>1.21.0</version>        </dependency>        <dependency>            <groupId>io.grpc</groupId>            <artifactId>grpc-protobuf</artifactId>            <version>1.21.0</version>        </dependency>        <dependency>            <groupId>io.grpc</groupId>            <artifactId>grpc-stub</artifactId>            <version>1.21.0</version>        </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.xolstice.maven.plugins</groupId>                <artifactId>protobuf-maven-plugin</artifactId>                <version>0.5.1</version>                <configuration>                    <protocArtifact>com.google.protobuf:protoc:3.7.1:exe:${os.detected.classifier}</protocArtifact>                    <pluginId>grpc-java</pluginId>                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.21.0:exe:${os.detected.classifier}</pluginArtifact>                    <!--指定生成文件目录-->                    <outputDirectory>src/main/java</outputDirectory>                    <!--重新生成文件时不清除 原有src/main/java下的内容-->                    <clearOutputDirectory>false</clearOutputDirectory>                </configuration>                <executions>                    <execution>                        <goals>                            <goal>compile</goal>                            <goal>compile-custom</goal>                        </goals>                    </execution>                </executions>            </plugin>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>            </plugin>        </plugins>    </build>
  2. 定义IDL文件

    syntax = "proto3";option java_multiple_files = true;option java_package = "net.changjinglu.proto";option java_outer_classname = "TelegraphProto";package telegraph;// The greeting service definition.service TelegraphService {    // Sends a greeting    rpc SayLove (LoveRequest) returns (LoveReply) {}}// The request message containing the user's name.message LoveRequest {    string message = 1;}// The response message containing the greetingsmessage LoveReply {    string message = 1;}
  3. 编译生成IDL定义的Java服务接口

    mvn clean install
  4. 实现IDL定义的Java服务接口

    public class TelegraphGreeterImpl extends TelegraphServiceGrpc.TelegraphServiceImplBase {    @Override    public void sayLove(LoveRequest request, StreamObserver<LoveReply> responseObserver) {        System.out.println("收到Node小怪兽的消息:"+request.getMessage());        responseObserver.onNext(LoveReply.newBuilder().setMessage("I Love U Too").build());        //结束        responseObserver.onCompleted();    }}
  5. 编写并启动Java服务端

    public class GrpcServer {    /** GRPC 服务端 */    private Server server;    public static void main(String[] args) throws IOException, InterruptedException {        GrpcServer grpcService = new GrpcServer();        grpcService.start();        System.out.println("GRPC 服务端启动成功");        //GRPC 服务端需要手动阻塞线程        grpcService.waitTermination();    }    private void start() throws IOException {        //绑定接口、启动服务        this.server = ServerBuilder.forPort(8899)                .addService(new TelegraphGreeterImpl())                .build()                .start();        System.out.println("server start!");        //这里是为了防止jvm关闭了,但是tcp还没有关闭的情况        Runtime.getRuntime().addShutdownHook(new Thread(()->{            System.out.println("关闭jvm");            GrpcServer.this.stop();        }));    }    private void stop() {        if (this.server != null) {            this.server.shutdown();        }    }    private void waitTermination() throws InterruptedException {        if (this.server != null) {            server.awaitTermination();        }    }}


     

  6. 编写并启动Nodejs客户端,客户端使用相同的IDL

    var PROTO_FILE_PATH = '/Users/wenyuan/Nodejs/grpc/proto/telegraph.proto';var grpc = require('grpc');var grpcService = grpc.load(PROTO_FILE_PATH).telegraph;function main() {    var stub = new grpcService.TelegraphService('localhost:8899',grpc.credentials.createInsecure());    stub.sayLove({message:'今晚的月色真美'},function (error, result) {        console.log('收到Java小怪兽的消息: ' + result.message);    });}main();
  7. Java服务端收到消息并回复


     

  8. Nodejs客户端收到Java服务端的回复