作者:亚瑟、文远
1. 微服务框架 — 从系统怪物到服务小怪兽
一个小巧的单体应用会随着公司业务的扩张而慢慢成长,逐渐演化成一个庞大且复杂的系统怪物,系统任何一处的问题都将影响整个怪物的表现,很少有单独的开发者能理清系统怪物所有的肌理脉络,导致 bug 的定位和新功能的扩展都变得越来越困难,对系统的任一改动都要求整个怪物一起回归测试并重新部署,效率必然不高。所以公司发展到了一定阶段,总会需要从架构上寻找解决系统怪物之道,而微服务就是目前最流行的架构方案之一,它将系统怪物拆分成多个独立自治的服务小怪兽,让我们有能力分而治之。
[插画:无数小怪兽组成一座大怪兽形状的山,小怪兽正一个个从山上滚下来]
2. RPC 框架 — 小怪兽的电报员
一旦系统怪物被拆分成了多个服务小怪兽,小怪兽们如何沟通协作就成了我们最关心的问题。服务小怪兽间的通信就好像发电报一样,涉及到数据序列化、反序列化、连接管理、收发线程、超时处理等多个问题,RPC 框架的出现解决了这些问题,就好像通过电报员发电报一样,使用 RPC 框架让小怪兽们不必关心通信的底层细节。
[插画:小怪兽在请电报员发电报]
RPC 调用细节
- 服务消费方(小怪兽 A)以本地调用方式调用服务
- client stub(小怪兽 A 的电报员)接受到调用后负责将方法、参数等编码成能够进行网络传输的消息体(电报)
- client stub(小怪兽 A 的电报员)找到服务地址,并将消息发送到服务端
- server stub(小怪兽 B 的电报员)收到消息(电报)后进行解码
- server stub(小怪兽 B 的电报员)根据解码结果调用本地的服务(小怪兽 B)
- 本地服务 (小怪兽 B) 执行并将结果返回给 server stub(小怪兽 B 的电报员)
- server stub(小怪兽 B 的电报员)将结果编码成消息(电报)并发送至客户端
- client stub(小怪兽 A 的电报员)接受到消息(电报)并进行解码
- 服务消费方(小怪兽 A)得到最终的结果
3. gRPC — 这位电报员是语言天才
如果通信的小怪兽们语言不通,那么我们需要对电报员(亦即 RPC 框架)的人选提出更高的要求,无论小怪兽们用的是什么语言,协助通信的两位电报员都必须把它们的话翻译成电报员彼此能理解的同一种语言,亦即 IDL(Interface Description Language),是的,电报员在这种情况下还必须承担翻译的角色,而 gRPC 就是一位如此优秀的电报员。
[插画:小怪兽说 ” 今晚的月色真美 ”,电报员写”I Love you“]
4. gPRC Demo
实现 Node 客户端小怪兽发送 ” 今晚的月色真美 ”,Java 服务端小怪兽收到电报内容,并回复 ”I love you too”。
-
通过 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>
-
定义 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 greetings message LoveReply {string message = 1;}
-
编译生成 IDL 定义的 Java 服务接口
mvn clean install
-
实现 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();} }
-
编写并启动 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(); } } }
-
编写并启动 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();
- Java 服务端收到消息并回复
- Nodejs 客户端收到 Java 服务端的回复