当越来越多的公司和项目拥抱微服务,总会有人会不自觉的掉入微服务的坑中。微服务虽然是解决大部分服务性能问题的一剂良药,但却不是唯一的选择,有时候或许还不是最佳的选择。例如当甲方是一个除了钱什么都缺,尤其是缺维护人员的公司。那么在给对方提供数据库性能优化方案的时候,那么选择Oracle就比MySQL集群更能药到病除。所以微服务只是系统自我演进过程中未来的一种可能发展方向,并不是一种为了炫耀技术的手段,除非炫耀技术本身会为公司带来丰厚的利润。
一个能够支撑产品快速迭代开发并响应市场需求的架构才是一个好架构,如果当一个产品的业务开发人员有一半以上的开发时间都花费在架构的适配上,那么整套架构的价值又体现在什么地方。
一个好的架构不是设计出来的,而是随着业务的增长一步一步演进而来的。如果试图在产品的一开始就采用微服务的架构,那么请自问是否做好接受微服务中的伤与痛。
微服务有着许多被人们津津乐道的优点,无论是在博客,论坛还是技术沙龙上面都被人们所传唱。就像是再富丽堂皇的宫殿也有污秽的下水道一样,微服务也有着许多的坑等着前赴后继的人们去趟,技术好的人或许能够很快的过去,技术不好的人就只能望而却步。
在本系列中,我们将从小码农的视角来看看微服务的那些坑。现阶段小码农遇到的坑主要有以下几部分
- 微服务中的耦合问题
- 微服务中的分布式事务问题
- 微服务中的统一日志问题
- 微服务中的监控问题
- 微服务中的服务合并问题
后面我们会一一聊聊这些问题,并探讨一些可能的解决方案。
为了探讨以上问题,就需要我们先搭建一个简单的微服务架构。这里我们使用Zookeeper作为服务的注册中心,负责服务的动态发现。各服务模块之间采用gRPC作为服务的通讯方式。而微服务中的其他组成部分我们会在后续章节中逐步完善。
在本篇中我们首先介绍一下Google所推出的RPC产品gRPC。
gRPC是Google开源的一款高性能RPC框架。采用IDL(Interface Description Language )来定义客户端与服务端进行通信的数据结构和接口,然后再编译成为指定语言版本的数据与接口代码。
gRPC使用Protocol Buffers作为 IDL 和底层的序列化工具。Protocol Buffers 也是非常有名的开源项目,主要用于结构化数据的序列化和反序列化。当前gRPC推荐使用的语法是proto3。
下面我们将通过一个例子来简单介绍一下proto3的语法,在本文中我们使用的语言为Java,我们定义了一个接口sayHello
和两个模型对象HelloRequest
和HelloResponse
entity.proto
// 声明protobuf版本为proto3
syntax = "proto3";
// 使用package避免命名冲突
// package默认会作为java的包名
package org.sydonay.demo.rpc;
// 如果为true时message会生成多个类
option java_multiple_files = true;
// 如果使用option java_package的话则会被优先设置为包名
option java_package = "org.sydonay.demo.model";
// 指定生成Java的类名,如果没有该字段则根据proto文件名称以驼峰的形式生成类名
option java_outer_classname = "Hello";
message HelloRequest {
string name = 1;
}
message HelloResponse {
string echo = 1;
}
interface.proto
syntax = "proto3";
package org.sydonay.demo.rpc;
option java_multiple_files = true;
option java_package = "org.sydonay.demo.service";
option java_outer_classname = "HelloInterface";
// 导入其他proto文件中声明的类型
import "entity.proto";
service HelloService {
// protocol buffers 的 idl中不允许无参函数的定义,如果业务有需要可以定义一个空message
// 接口不支持基本类型,必须将入参和出参定义为message类型
rpc sayHello(HelloRequest) returns (HelloResponse);
}
在这里我们可以看到这两个idl文件的后缀名都是proto,为了将这两个文件编译成为代码,我们需要借助两个工具protoc.exe
和protoc-gen-grpc-java.exe
。
1、protoc.exe
下载地址:https://github.com/google/protobuf/releases
功能:用来生成消息对象等代码
编译命令:
protoc.exe --java_out=./ filename.proto
2、protoc-gen-grpc-java.exe
下载地址:https://github.com/grpc/grpc-java/tree/master/compiler,这个需要自行进行编译
功能:用来生成rpc通讯相关代码
编译命令:
protoc.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
来一键生成所有代码。
为了方便代码的维护,我们将工程分为三部分,工程结构如下:
Root project 'gRPC-Demo'
+--- Project ':Demo-Interface' gRPC生成文件工程
+--- Project ':Demo-SDK' client端工程,工程依赖Demo-Interface
\--- Project ':Demo-Server' server端工程,工程依赖Demo-Interface
我们分别来看一下这几个工程内部的目录结构
│ build.gradle
└─src
└─main
├─java
│ └─org
│ └─sydonay
│ └─demo
│ ├─model
│ │ Hello.java
│ │ HelloRequest.java
│ │ HelloRequestOrBuilder.java
│ │ HelloResponse.java
│ │ HelloResponseOrBuilder.java
│ │
│ └─service
│ HelloInterface.java
│ HelloServiceGrpc.java
│
└─proto3
entity.proto
interface.proto
其中src/main/java
路径下的所有文件都是上面proto文件编译后的结果,model路径下面的是数据对象,service里面的是通讯接口。src/main/proto3
路径下面都是IDL文件。
│ build.gradle
└─src
└─main
└─java
└─org
└─sydonay
└─demo
├─core
│ ServerStart.java
│
└─impl
HelloServiceImpl.java
这里ServerStart.java
的功能是启动服务端进程并监听rpc端口,HelloServiceImpl.java
是sayHello
方法的具体实现逻辑。
│ build.gradle
└─src
└─main
└─java
└─org
└─sydonay
└─demo
└─client
HelloClient.java
这里HelloClient.java
是sayHello
方法的调用。
看完工程结构以后我们再来看一下每一个服务代码的具体实现逻辑。
由于该工程中所有的代码部分具由工具编译而成,所以我们这里主要看一下在Google提供的Gradle插件com.google.protobuf
如何来一键编译proto文件。
apply plugin: 'com.google.protobuf'
jar {
baseName = 'gradle-project'
version = '0.0.1'
}
buildscript {
repositories {
maven { url "http://maven.aliyun.com/nexus/content/groups/public" }
}
dependencies {
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.1'
}
}
/**
* gradle generateProto
* 自动生成grpc文件&拷贝文件到指定路径
*/
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:${protobufVer}"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${grpcVer}"
}
}
// 拷贝build/generated/source/proto/main/java的文件到源码路径
generatedFilesBaseDir = "src"
// 拷贝build/generated/source/proto/main/grpc的文件到源码路径
generateProtoTasks {
all()*.plugins {
grpc {
outputSubDir = "java"
}
}
}
}
sourceSets {
main {
proto {
srcDir 'src/main/proto3'
}
java {
srcDir "src/main/java"
}
resources {
srcDir "src/main/resources"
srcDir 'src/main/proto3'
}
}
test {
java {
srcDir "src/test/java"
}
}
}
mainClassName = ""
编写完IDL文件后,执行gradle generateProto
命令,便能在src/main/java
路径下生成编译后的代码。
HelloServiceImpl.java
package org.sydonay.demo.impl;
import io.grpc.stub.StreamObserver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sydonay.demo.model.HelloRequest;
import org.sydonay.demo.model.HelloResponse;
import org.sydonay.demo.service.HelloServiceGrpc;
public class HelloServiceImpl extends HelloServiceGrpc.HelloServiceImplBase {
private static final Logger logger = LoggerFactory.getLogger(HelloServiceImpl.class);
/**
* 抽象类HelloServiceImplBase的具体实现
* @param request 请求参数
* @param responseObserver 用于处理响应和关闭通道
*/
@Override
public void sayHello(HelloRequest request, StreamObserver responseObserver) {
logger.info(String.format("sayHello请求参数为:%s", request.getName()));
String echoMessage = String.format("hello world, hello %s", request.getName());
HelloResponse helloResponse = HelloResponse.newBuilder().setEcho(echoMessage).build();
// 返回响应
responseObserver.onNext(helloResponse);
// 告诉gRPC写入响应已经完成
responseObserver.onCompleted();
}
}
HelloServiceImpl中的逻辑很清晰,就是覆盖在HelloServiceImplBase中定义的接口sayHello。这里值得注意的就是逻辑上定义的响应参数HelloResponse的实例化需要使用newBuilder和build方法。最后这段代码中最重要的StreamObserver作为响应结果的观察者,用来控制响应结果的返回时机和结束本次调用。
ServerStart.java
package org.sydonay.demo.core;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sydonay.demo.impl.HelloServiceImpl;
import java.io.IOException;
public class ServerStart {
private static final Logger logger = LoggerFactory.getLogger(ServerStart.class);
private static final int DEFAULT_PORT = 1217;
private Server server = null;
private void start() throws IOException {
// 将具体的服务对象HelloServiceImpl注册到ServerBuilder & 启动
server = ServerBuilder.forPort(DEFAULT_PORT).addService(new HelloServiceImpl()).build().start();
logger.info(String.format("rpc 服务端启动,监听[%s]端口", DEFAULT_PORT));
/**
* 在jvm中增加一个关闭的钩子,当jvm关闭的时候,会执行系统中已经设置的所有通过方法addShutdownHook添加的钩子。
* 当系统执行完这些钩子后,jvm才会关闭。
*/
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
logger.info("rpc 服务端开始关闭...");
ServerStart.this.stop();
logger.info("rpc 服务端已关闭");
}
});
}
private void stop() {
if (server != null) {
server.shutdown();
}
}
private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
// 等待server 被中断(被调用shutdown命令),在此之前一直将进程阻塞
server.awaitTermination();
}
}
public static void main(String[] args) throws IOException, InterruptedException {
final ServerStart server = new ServerStart();
server.start();
// 让服务端阻塞一直处于监听状态,也不会执行后面的代码
server.blockUntilShutdown();
}
}
本段代码中最为核心的类便是ServerBuilder,它实现了以下3件事:
- 指定服务端监听的端口
- 注册了具体的服务对象
- 实例化server对象并启动
HelloClient.java
package org.sydonay.demo.client;
import io.grpc.ManagedChannel;
import io.grpc.netty.NegotiationType;
import io.grpc.netty.NettyChannelBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sydonay.demo.model.HelloRequest;
import org.sydonay.demo.model.HelloResponse;
import org.sydonay.demo.service.HelloServiceGrpc;
import java.util.concurrent.TimeUnit;
public class HelloClient {
private static final Logger logger = LoggerFactory.getLogger(HelloClient.class);
// 与服务器之间建立起的一条逻辑上的通道,在底层会建立一条TCP通道
private final ManagedChannel channel;
// 通过stub来调用服务端方法,stub分为阻塞和非阻塞两种,这里建立的是阻塞stub会等待rpc响应结果
private final HelloServiceGrpc.HelloServiceBlockingStub blockingStub;
public HelloClient(String host, int port) {
channel = NettyChannelBuilder.forAddress(host, port).negotiationType(NegotiationType.PLAINTEXT).build();
blockingStub = HelloServiceGrpc.newBlockingStub(channel);
}
public void shutdown() throws InterruptedException {
// 等待channel关闭,如果超过5s则放弃关闭
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
}
public void sayHello(String name) {
try {
logger.info(String.format("请求参数:name=%s", name));
HelloRequest request = HelloRequest.newBuilder().setName(name).build();
HelloResponse response = blockingStub.sayHello(request);
logger.info(String.format("响应信息为: %s", response.getEcho()));
} catch (RuntimeException e) {
logger.error(e.getMessage());
return;
}
}
public static void main(String[] args) throws Exception {
HelloClient client = new HelloClient("127.0.0.1", 1217);
try {
String name = "Aya";
client.sayHello(name);
} finally {
client.shutdown();
}
}
}
为了建立与服务端之间的通信我们必须使用客户桩
(stub),而stub的建立又需要使用channel来指定服务端ip和端口。当stub实例化好了以后,我们就能随心所欲的调用服务端的接口了。由于我们在这里定义的stub为同步stub,所以当我们实时等待到服务端的响应结果后整个gRPC-Demo的逻辑就到此结束了。
到此为止,我们已经了解了gRPC的IDL应该如何定义,如何将IDL编译成为指定语言的代码,如何实现是一个简单的服务端程序,并且通过同步调用的方式接受到服务端的响应。
由于需要给公司新人进行Linux相关的培训,所以将本来应该在下一篇文章中继续进行的微服务搭建的教程,改为浅谈Linux中的常用指令。尽请见谅。
欢迎关注微信公众号,在这里可以提前看到下一期文章哦~