微服务的伤与痛搭建一个微服务-gRPC篇

Posted 小码农de历险记

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了微服务的伤与痛搭建一个微服务-gRPC篇相关的知识,希望对你有一定的参考价值。

【微服务的伤与痛】搭建一个微服务-gRPC篇

开篇

当越来越多的公司和项目拥抱微服务,总会有人会不自觉的掉入微服务的坑中。微服务虽然是解决大部分服务性能问题的一剂良药,但却不是唯一的选择,有时候或许还不是最佳的选择。例如当甲方是一个除了钱什么都缺,尤其是缺维护人员的公司。那么在给对方提供数据库性能优化方案的时候,那么选择Oracle就比mysql集群更能药到病除。所以微服务只是系统自我演进过程中未来的一种可能发展方向,并不是一种为了炫耀技术的手段,除非炫耀技术本身会为公司带来丰厚的利润。

一个能够支撑产品快速迭代开发并响应市场需求的架构才是一个好架构,如果当一个产品的业务开发人员有一半以上的开发时间都花费在架构的适配上,那么整套架构的价值又体现在什么地方。

一个好的架构不是设计出来的,而是随着业务的增长一步一步演进而来的。如果试图在产品的一开始就采用微服务的架构,那么请自问是否做好接受微服务中的伤与痛。

楔子

微服务有着许多被人们津津乐道的优点,无论是在博客,论坛还是技术沙龙上面都被人们所传唱。就像是再富丽堂皇的宫殿也有污秽的下水道一样,微服务也有着许多的坑等着前赴后继的人们去趟,技术好的人或许能够很快的过去,技术不好的人就只能望而却步。

在本系列中,我们将从小码农的视角来看看微服务的那些坑。现阶段小码农遇到的坑主要有以下几部分

  1. 微服务中的耦合问题

  2. 微服务中的分布式事务问题

  3. 微服务中的统一日志问题

  4. 微服务中的监控问题

  5. 微服务中的服务合并问题

后面我们会一一聊聊这些问题,并探讨一些可能的解决方案。

为了探讨以上问题,就需要我们先搭建一个简单的微服务架构。这里我们使用Zookeeper作为服务的注册中心,负责服务的动态发现。各服务模块之间采用gRPC作为服务的通讯方式。而微服务中的其他组成部分我们会在后续章节中逐步完善。

在本篇中我们首先介绍一下Google所推出的RPC产品gRPC。

正文

简介

gRPC是Google开源的一款高性能RPC框架。采用IDL(Interface Description Language )来定义客户端与服务端进行通信的数据结构和接口,然后再编译成为指定语言版本的数据与接口代码。

gRPC使用Protocol Buffers作为 IDL 和底层的序列化工具。Protocol Buffers 也是非常有名的开源项目,主要用于结构化数据的序列化和反序列化。当前gRPC推荐使用的语法是proto3。

IDL

下面我们将通过一个例子来简单介绍一下proto3的语法,在本文中我们使用的语言为Java,我们定义了一个接口sayHello和两个模型对象HelloRequestHelloResponse

entity.proto

 1// 声明protobuf版本为proto3
2syntax = "proto3";
3
4// 使用package避免命名冲突
5// package默认会作为java的包名
6package org.sydonay.demo.rpc;
7
8// 如果为true时message会生成多个类
9option java_multiple_files = true;
10// 如果使用option java_package的话则会被优先设置为包名
11option java_package = "org.sydonay.demo.model";
12// 指定生成Java的类名,如果没有该字段则根据proto文件名称以驼峰的形式生成类名
13option java_outer_classname = "Hello";
14
15message HelloRequest {  
16    string name  = 1;  
17}
18
19message HelloResponse {  
20    string echo = 1;  
21}

interface.proto

 1syntax = "proto3";
2
3package org.sydonay.demo.rpc;
4
5option java_multiple_files = true;
6option java_package = "org.sydonay.demo.service";
7option java_outer_classname = "HelloInterface";
8
9// 导入其他proto文件中声明的类型
10import "entity.proto";
11
12service HelloService {
13    // protocol buffers 的 idl中不允许无参函数的定义,如果业务有需要可以定义一个空message
14    // 接口不支持基本类型,必须将入参和出参定义为message类型
15    rpc sayHello(HelloRequest) returns (HelloResponse);  
16}

在这里我们可以看到这两个idl文件的后缀名都是proto,为了将这两个文件编译成为代码,我们需要借助两个工具protoc.exeprotoc-gen-grpc-java.exe

1、protoc.exe

功能:用来生成消息对象等代码

编译命令:

1protoc.exe --java_out=./ filename.proto

2、protoc-gen-grpc-java.exe

功能:用来生成rpc通讯相关代码

编译命令:

1protoc.exe --plugin=protoc-gen-grpc-java=protoc-gen-grpc-java-0.13.2-windows-x86_64.exe --grpc-java_out=./ *.proto

对于编译工具而言gRPC就没有Thrift做的好,都没有在一个工具里面就将所有的功能提供出来。为了解决这一问题,Google也提供出一个Gradle的插件com.google.protobuf来一键生成所有代码。

工程结构

为了方便代码的维护,我们将工程分为三部分,工程结构如下:

1Root project 'gRPC-Demo'
2+--- Project ':Demo-Interface'  gRPC生成文件工程
3+--- Project ':Demo-SDK'        client端工程,工程依赖Demo-Interface
4\--- Project ':Demo-Server'     server端工程,工程依赖Demo-Interface

我们分别来看一下这几个工程内部的目录结构

Demo-Interface工程结构

 1│  build.gradle
2└─src
3    └─main
4        ├─java
5        │  └─org
6        │      └─sydonay
7        │          └─demo
8        │              ├─model
9        │              │      Hello.java
10        │              │      HelloRequest.java
11        │              │      HelloRequestOrBuilder.java
12        │              │      HelloResponse.java
13        │              │      HelloResponseOrBuilder.java
14        │              │      
15        │              └─service
16        │                      HelloInterface.java
17        │                      HelloServiceGrpc.java
18        │                      
19        └─proto3
20                entity.proto
21                interface.proto

其中src/main/java路径下的所有文件都是上面proto文件编译后的结果,model路径下面的是数据对象,service里面的是通讯接口。src/main/proto3路径下面都是IDL文件。

Demo-Server工程结构

 1│  build.gradle
2└─src
3    └─main
4        └─java
5            └─org
6                └─sydonay
7                    └─demo
8                        ├─core
9                        │      ServerStart.java
10                        │      
11                        └─impl
12                                HelloServiceImpl.java

这里ServerStart.java的功能是启动服务端进程并监听rpc端口,HelloServiceImpl.javasayHello方法的具体实现逻辑。

Demo-SDK工程结构

1│  build.gradle
2└─src
3    └─main
4        └─java
5            └─org
6                └─sydonay
7                    └─demo
8                        └─client
9                                HelloClient.java

这里HelloClient.javasayHello方法的调用。

实现逻辑

看完工程结构以后我们再来看一下每一个服务代码的具体实现逻辑。

Demo-Interface工程代码

由于该工程中所有的代码部分具由工具编译而成,所以我们这里主要看一下在Google提供的Gradle插件com.google.protobuf如何来一键编译proto文件。

 1apply plugin: 'com.google.protobuf'
2
3jar {
4    baseName = 'gradle-project'
5    version = '0.0.1'
6}
7
8buildscript {
9    repositories {
10        maven { url "http://maven.aliyun.com/nexus/content/groups/public" }
11    }
12    dependencies {
13        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.1'
14    }
15}
16
17/**
18 * gradle generateProto
19 * 自动生成grpc文件&拷贝文件到指定路径
20 */

21protobuf {
22    protoc {
23        artifact = "com.google.protobuf:protoc:${protobufVer}"
24    }
25    plugins {
26        grpc {
27            artifact = "io.grpc:protoc-gen-grpc-java:${grpcVer}"
28        }
29    }
30
31    // 拷贝build/generated/source/proto/main/java的文件到源码路径
32    generatedFilesBaseDir = "src"
33
34    // 拷贝build/generated/source/proto/main/grpc的文件到源码路径
35    generateProtoTasks {
36        all()*.plugins {
37            grpc {
38               outputSubDir = "java"
39            }
40        }
41    }
42}
43
44sourceSets {
45    main {
46        proto {
47            srcDir 'src/main/proto3'
48        }
49        java {
50            srcDir "src/main/java"
51        }
52        resources {
53            srcDir "src/main/resources"
54             srcDir 'src/main/proto3'
55        }
56    }
57    test {
58        java {
59            srcDir "src/test/java"
60        }
61    }
62}
63
64mainClassName = ""

编写完IDL文件后,执行gradle generateProto命令,便能在src/main/java路径下生成编译后的代码。

Demo-Server工程代码

HelloServiceImpl.java

 1package org.sydonay.demo.impl;
2
3import io.grpc.stub.StreamObserver;
4import org.slf4j.Logger;
5import org.slf4j.LoggerFactory;
6import org.sydonay.demo.model.HelloRequest;
7import org.sydonay.demo.model.HelloResponse;
8import org.sydonay.demo.service.HelloServiceGrpc;
9
10public class HelloServiceImpl extends HelloServiceGrpc.HelloServiceImplBase {
11
12    private static final Logger logger = LoggerFactory.getLogger(HelloServiceImpl.class);
13
14    /**
15     * 抽象类HelloServiceImplBase的具体实现
16     * @param request                   请求参数
17     * @param responseObserver          用于处理响应和关闭通道
18     */

19    @Override
20    public void sayHello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
21
22
23        logger.info(String.format("sayHello请求参数为:%s", request.getName()));
24
25        String echoMessage = String.format("hello world, hello %s", request.getName());
26
27        HelloResponse helloResponse = HelloResponse.newBuilder().setEcho(echoMessage).build();
28
29        // 返回响应
30        responseObserver.onNext(helloResponse);
31        // 告诉gRPC写入响应已经完成
32        responseObserver.onCompleted();
33    }
34}

HelloServiceImpl中的逻辑很清晰,就是覆盖在HelloServiceImplBase中定义的接口sayHello。这里值得注意的就是逻辑上定义的响应参数HelloResponse的实例化需要使用newBuilder和build方法。最后这段代码中最重要的StreamObserver作为响应结果的观察者,用来控制响应结果的返回时机和结束本次调用。

ServerStart.java

 1package org.sydonay.demo.core;
2
3import io.grpc.Server;
4import io.grpc.ServerBuilder;
5import org.slf4j.Logger;
6import org.slf4j.LoggerFactory;
7import org.sydonay.demo.impl.HelloServiceImpl;
8
9import java.io.IOException;
10
11public class ServerStart {
12    private static final Logger logger = LoggerFactory.getLogger(ServerStart.class);
13
14    private static final int DEFAULT_PORT = 1217;
15
16    private Server server = null;
17
18    private void start() throws IOException {
19
20        // 将具体的服务对象HelloServiceImpl注册到ServerBuilder & 启动
21        server = ServerBuilder.forPort(DEFAULT_PORT).addService(new HelloServiceImpl()).build().start();
22
23        logger.info(String.format("rpc 服务端启动,监听[%s]端口", DEFAULT_PORT));
24
25        /**
26         * 在jvm中增加一个关闭的钩子,当jvm关闭的时候,会执行系统中已经设置的所有通过方法addShutdownHook添加的钩子。
27         * 当系统执行完这些钩子后,jvm才会关闭。
28         */

29        Runtime.getRuntime().addShutdownHook(new Thread() {
30            @Override
31            public void run() {
32                logger.info("rpc 服务端开始关闭...");
33                ServerStart.this.stop();
34                logger.info("rpc 服务端已关闭");
35            }
36        });
37    }
38
39    private void stop() {
40        if (server != null) {
41            server.shutdown();
42        }
43    }
44
45    private void blockUntilShutdown() throws InterruptedException {
46        if (server != null) {
47            // 等待server 被中断(被调用shutdown命令),在此之前一直将进程阻塞
48            server.awaitTermination();
49        }
50    }
51
52    public static void main(String[] args) throws IOException, InterruptedException {
53        final ServerStart server = new ServerStart();
54        server.start();
55        // 让服务端阻塞一直处于监听状态,也不会执行后面的代码
56        server.blockUntilShutdown();
57    }
58}

本段代码中最为核心的类便是ServerBuilder,它实现了以下3件事:

  1. 指定服务端监听的端口

  2. 注册了具体的服务对象

  3. 实例化server对象并启动

Demo-SDK工程代码

HelloClient.java

 1package org.sydonay.demo.client;
2
3import io.grpc.ManagedChannel;
4import io.grpc.netty.NegotiationType;
5import io.grpc.netty.NettyChannelBuilder;
6import org.slf4j.Logger;
7import org.slf4j.LoggerFactory;
8import org.sydonay.demo.model.HelloRequest;
9import org.sydonay.demo.model.HelloResponse;
10import org.sydonay.demo.service.HelloServiceGrpc;
11
12import java.util.concurrent.TimeUnit;
13
14public class HelloClient {
15    private static final Logger logger = LoggerFactory.getLogger(HelloClient.class);
16
17    // 与服务器之间建立起的一条逻辑上的通道,在底层会建立一条TCP通道
18    private final ManagedChannel channel;
19
20    // 通过stub来调用服务端方法,stub分为阻塞和非阻塞两种,这里建立的是阻塞stub会等待rpc响应结果
21    private final HelloServiceGrpc.HelloServiceBlockingStub blockingStub;
22
23    public HelloClient(String host, int port) {
24
25        channel = NettyChannelBuilder.forAddress(host, port).negotiationType(NegotiationType.PLAINTEXT).build();
26        blockingStub = HelloServiceGrpc.newBlockingStub(channel);
27    }
28
29    public void shutdown() throws InterruptedException {
30        // 等待channel关闭,如果超过5s则放弃关闭
31        channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
32    }
33
34    public void sayHello(String name) {
35        try {
36            logger.info(String.format("请求参数:name=%s", name));
37
38            HelloRequest request = HelloRequest.newBuilder().setName(name).build();
39            HelloResponse response = blockingStub.sayHello(request);
40
41            logger.info(String.format("响应信息为: %s", response.getEcho()));
42        } catch (RuntimeException e) {
43            logger.error(e.getMessage());
44            return;
45        }
46    }
47
48    public static void main(String[] args) throws Exception {
49
50        HelloClient client = new HelloClient("127.0.0.1", 1217);
51
52        try {
53            String name = "Aya";
54            client.sayHello(name);
55        } finally {
56            client.shutdown();
57        }
58    }
59}

为了建立与服务端之间的通信我们必须使用客户桩(stub),而stub的建立又需要使用channel来指定服务端ip和端口。当stub实例化好了以后,我们就能随心所欲的调用服务端的接口了。由于我们在这里定义的stub为同步stub,所以当我们实时等待到服务端的响应结果后整个gRPC-Demo的逻辑就到此结束了。


到此为止,我们已经了解了gRPC的IDL应该如何定义,如何将IDL编译成为指定语言的代码,如何实现是一个简单的服务端程序,并且通过同步调用的方式接受到服务端的响应。

由于需要给公司新人进行Linux相关的培训,所以将本来应该在下一篇文章中继续进行的微服务搭建的教程,改为浅谈Linux中的常用指令。尽请见谅。

以上是关于微服务的伤与痛搭建一个微服务-gRPC篇的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot集成gRPC微服务工程搭建实践

每天进步一点点:go基于grpc构建微服务框架-服务注册与发现

gRPC 使用 protobuf 构建微服务

Grpc微服务架构实现

ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT)

ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT)