前言
本文将使用 Maven,gRPC,Protocol buffers,Docker,Envoy 等工具构建一个简单微服务工程, 笔者所使用的示例工程是以前写的一个 Java 后端工程, 因为最近都在 学习微服务相关的知识, 所以利用起来慢慢的把这个工程做成微服务化应用. 在实践过程踩过很多坑, 主要是经验不足对微服务还是停留在萌新阶段, 通过本文 记录创建微服务工程碰到一些问题, 此次实践主要是解决以下问题:
如何解决, 统一服务工程依赖管理
SpringBoot 集成 gRPC
管理 Protocol buffers 文件
使用 Envoy 代理访问 gRPC
部署到 Docker
本文假设读者已经了解以下相关知识:
- Maven
- Envoy
- gRPC
- Protocol buffers
- SpringBoot
- Docker
由于是初步实现微服务, 不会考虑过多的细节, 现阶段只需要能够使用 gRPC 正常通信, 后续计划会发布到 k8s 中, 使用 istio 实现来服务网格.
使用 Maven
现在比较流行的构建工具有 Maven 和 Gradle, 现阶段后端开发大多数都是用的 Maven 所以本工程也使用 Maven 来构建项目, 当然使用 Gradle 也可以两者概念大都想通, 不同的地方大多是实现和配置方式不一致.
使用项目继承
根据 Maven 的 POM 文件继承特性, 将工程分不同的模块, 所有的模块都继承父 pom.xml 的依赖, 插件等内容, 这样就可以实现统一管理, 并方便以后管理, 维护. 先看一下大概的项目结构:
AppBubbleBackend (1)
├── AppBubbleCommon
├── AppBubbleSmsService (2)
├── AppBubbleUserService
├── docker-compose.YAML (3)
├── pom.xml
├── protos (4)
│ ├── sms
│ └── user
└── scripts (5)
├── docker
├── envoy
├── gateway
└── sql
以下是各个目录的用处简述, 详细的用处文章后面都会提到, 先在这里列出个大概:
工程主目录
单个服务工程目录(模块)
docker-compose 发布文件
存放. proto 文件
发布, 编译时用到的脚本文件
知道大概的项目工程结构后我们创建一个父 pom.xml 文件, 放在 AppBubbleBackend 目录下面:
- <?xml version="1.0" encoding="UTF-8"?>
- <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>
- <parent>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-parent</artifactId>
- <version>2.1.2.RELEASE</version>
- <relativePath/> <!-- lookup parent from repository -->
- </parent>
- <groupId>com.bubble</groupId>
- <artifactId>bubble</artifactId>
- <version>0.0.1-SNAPSHOT</version>
- <packaging>pom</packaging>
- <modules>
- <module>AppBubbleSmsService</module>
- <module>AppBubbleCommon</module>
- <module>AppBubbleUserService</module>
- </modules>
- <!-- 省略其他部分 -->
- </project>
因为使用 SpringBoot 构架, 所以主 pom.xml 文件继承自 SpringBoot 的 POM 文件. 有了主 pom.xml 后然后使每个模块的 pom.xml 都继承自 主 pom.xml 文件:
- <?xml version="1.0" encoding="UTF-8"?>
- <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>
- <parent>
- <groupId>com.bubble</groupId>
- <artifactId>bubble</artifactId>
- <version>0.0.1-SNAPSHOT</version>
- </parent>
- <artifactId>sms</artifactId>
- <version>0.0.1-SNAPSHOT</version>
- <!-- 省略其他部分 -->
- </project>
经过上面的配置后, 所有的模块都会继承 AppBubbleBackend 中的 pom.xml 文件, 这样可以很方便的更改依赖, 配置等信息.
依赖管理
Maven 提供依赖中心化的管理机制, 通过项目继承特性所有对 AppBubbleBackend/pom.xml 所做的更改都会对其他模块产生影响, 详细的依赖管理 内容可查看官方文档.
- <dependencyManagement>
- <dependencies>
- <!-- gRPC -->
- <dependency>
- <groupId>io.grpc</groupId>
- <artifactId>grpc-netty-shaded</artifactId>
- <version>${grpc.version}</version>
- </dependency>
- </dependencies>
- </dependencyManagement>
通过 dependencyManagement 标签来配置依赖, 这样可以就可以实现统一依赖的管理, 并且还可以添加公共依赖.
插件管理
使用 pluginManagement 可以非常方便的配置插件, 因为项目中使用了 Protocol buffers 需要集成相应的插件来生成 Java 源文件:
- <pluginManagement>
- <plugins>
- <plugin>
- <groupId>org.xolstice.maven.plugins</groupId>
- <artifactId>protobuf-maven-plugin</artifactId>
- <version>0.5.1</version>
- <executions>
- <execution>
- <goals>
- <goal>compile</goal>
- <goal>compile-custom</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </pluginManagement>
Protocol buffers 插件的完整配置参数, 可以这这里找到.
Profile
使用 Profile 的目的是为了区分生成 Docker 镜像时的一些特殊配置, 示例工程只配置了一个 docker-build 的 profile:
- <profiles>
- <profile>
- <id>docker-build</id>
- <properties>
- <jarName>App</jarName>
- </properties>
- </profile>
- </profiles>
- <properties>
- <jarName>${project.artifactId}-${project.version}</jarName>
- </properties>
- <build>
- <finalName>${jarName}</finalName>
- </build>
如果使用 mvn package -P docker-build 命令生成 jar 包时, 相应的输出文件名是 App.jar 这样可以方便在 Dockerfile 中引用文件, 而不需要使用 ${project.artifactId}-${project.version}的形式来查找输出的 jar 这样可以省去了解析 pom.xml 文件. 如果还需要特殊的参数可以或者不同的行为, 可以添加多个 Profile, 这样配置起来非常灵活.
Protocol buffers 文件管理
因为是使用微服务开发, 而且 RPC 通信框架是使用的 gRPC, 所以每个服务工程都会使用. proto 文件. 服务工程之间又会有使用同一份. proto 文件的需求, 比如在进行 RPC 通信时服务提供方返回的消息 Test 定义在 a.proto 文件中, 那么在使用方在解析消息时也同样需要 a.proto 文件来将接收到的消息转换成 Test 消息, 因此管理. proto 文件也有一些小麻烦. 关于 Protocol buffers 的使用可参考 官方文档.
Protocol buffers 文件管理规约
在我们的示例项目中使用集中管理的方式, 即将所有的. proto 文件放置在同一个目录 (AppBubbleBackend/protos) 下并按服务名称来划分:
├── sms
│ ├── SmsMessage.proto
│ └── SmsService.proto
└── user
└── UserMessage.proto
还可以将整个目录放置在一个单独的 Git 仓库中, 然后在项目中使用 Git subtree 来管理文件.
Protocol buffers 插件配置
有了上面的目录结构后, 就需要配置一下 Protocol buffers 的编译插件来支持这种. proto 文件的组织结构. 在讲解如何配置插件解决. proto 文件的编译问题之前, 推荐读者了解一下插件的配置文档: Xolstice Maven 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.5.1-1:exe:${os.detected.classifier}</protocArtifact>
- <pluginId>grpc-java</pluginId>
- <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.17.1:exe:${os.detected.classifier}</pluginArtifact>
- <additionalProtoPathElements combine.children="append" combine.self="append">
- <additionalProtoPathElement>${GOPATH}/src/GitHub.com/grpc-ecosystem/grpc-gateway/third_party/googleapis</additionalProtoPathElement>
- <additionalProtoPathElement>${GOPATH}/src</additionalProtoPathElement>
- </additionalProtoPathElements>
- <protoSourceRoot>${protos.basedir}</protoSourceRoot>
- <writeDescriptorSet>true</writeDescriptorSet>
- <includeDependenciesInDescriptorSet>true</includeDependenciesInDescriptorSet>
- </configuration>
- <!-- ... -->
- </plugin>
首先上面的插件配置使用 protoSourceRoot 标签将 Protocol buffers 的源文件目录更改成 AppBubbleBackend/protos 目录, 因为工程中使用了 googleapis 来定义服务接口, 所以需要使用添加 additionalProtoPathElement 标签添加额外的依赖文件. 注意这个插件的配置是在 AppBubbleBackend/pom.xml 文件中的, 服务工程都是继承此文件的. 在父 POM 文件配置好以后, 再看一下服务工程的插件配置:
- <plugins>
- <plugin>
- <groupId>org.xolstice.maven.plugins</groupId>
- <artifactId>protobuf-maven-plugin</artifactId>
- <configuration>
- <includes>
- <include>${project.artifactId}/*.proto</include>
- <include>user/*.proto</include>
- </includes>
- </configuration>
- </plugin>
- </plugins>
服务工程主要使用 includes 标签, 将需要的. proto 文件包含在编译脚本中, includes 标签中的 include 只是一个指定匹配. proto 文件的匹配模式,<include>${project.artifactId}/*.proto</include > 意思是 AppBubbleBackend/protos/${project.artifactId}目录下的所有以. proto 文件结尾的文件, 如果服务工程有多个依赖可以将需要依赖的文件也添加到编译服务中, 如上面的 < include>user/*.proto</include > 就将 AppBubbleBackend/protos/user 中的. proto 文件添加进来, 然后进行整体的编译.
gRPC
gRPC 是由 Google 开源的 RPC 通信框架, gRPC 使用 Protocol buffers 定义服务接口并自动生成 gRPC 相关代码, 有了这些代码后就可以非常方便的实现 gRPC 服务端和 gPRC 客户端, 过多的细节就不细说了先看一下如何使用在 SpringBoot 中使用 gRPC.
运行 gRPC 服务
利用 ApplicationRunner 接口, 在 SprintBoot 中运行 gRPC 服非常方便, 只需要像下面代码一样就可以运行一个简单的 gRPC 服务.
- package com.bubble.sms.grpc;
- @Component
- public class GrpcServerInitializer implements ApplicationRunner {
- @Autowired
- private List<BindableService> services;
- @Value("${grpc.server.port:8090}")
- private int port;
- @Override
- public void run(ApplicationArguments args) throws Exception {
- ServerBuilder serverBuilder = ServerBuilder
- .forPort(port);
- if (services != null && !services.isEmpty()) {
- for (BindableService bindableService : services) {
- serverBuilder.addService(bindableService);
- }
- }
- Server server = serverBuilder.build();
- serverBuilder.intercept(TransmitStatusRuntimeExceptionInterceptor.instance());
- server.start();
- startDaemonAwaitThread(server);
- }
- private void startDaemonAwaitThread(Server server) {
- Thread awaitThread = new Thread(() -> {
- try {
- server.awaitTermination();
- } catch (InterruptedException ignore) {
- }
- });
- awaitThread.setDaemon(false);
- awaitThread.start();
- }
- }
Envoy 代理
gRPC 服务运行起来后就需要进行调试了, 比如使用 curl,Chrome 等工具向 gRPC 服务发起 Restful 请求, 实际上 gRPC 的调试并没有那么简单. 一开始的方案是使用了 gRPC-gateway, 为每个服务都启动一个网关将 Http 1.x 请求转换并发送到 gRPC 服务. 然而 gRPC-gateway 只有 go 语言的版本, 并没有 Java 语言的版本, 所有在编译和使用中比较困难, 后来发现了 Envoy 提供了 envoy.grpc_json_transcoder 这个 http 过滤器, 可以很方便的将 RESTful JSON API 转换成 gRPC 请求并发送给 gRPC 服务器.
envoy 的相关配置都放置在 AppBubbleBackend/scripts/envoy 目录中, 里面的 envoy.YAML 是一份简单的配置文件:
- static_resources:
- listeners:
- - name: grpc-8090
- address:
- socket_address: { address: 0.0.0.0, port_value: 8090 }
- filter_chains:
- - filters:
- - name: envoy.http_connection_manager
- config:
- stat_prefix: sms_http
- codec_type: AUTO
- # 省略部分配置
- http_filters:
- - name: envoy.grpc_json_transcoder
- config:
- proto_descriptor: "/app/app.protobin"
- services: ["sms.SmsService"]
- match_incoming_request_route: true
- print_options:
- add_whitespace: true
- always_print_primitive_fields: true
- always_print_enums_as_ints: false
- preserve_proto_field_names: false
- # 省略部分配置
使用 envoy.grpc_json_transcoder 过滤器的主要配置是 proto_descriptor 选项, 该选项指向一个 proto descriptor set 文件. AppBubbleBackend/scripts/envoy/compile-descriptor.sh 是编译 proto descriptor set 的脚本文件, 运行脚本文件会在脚本目录下生成一个 App.protobin 的文件, 将此文件设置到 envoy.grpc_json_transcoder 就可大致完成了 envoy 的代理配置.
使用 Docker 发布
经过上面的一系统准备工作之后, 我们就可以将服务发布到 docker 中了, Docker 相关的文件都放置中 AppBubbleBackend/scripts/docker 和一个 AppBubbleBackend/docker-compose.YAML 文件. 在发布时使用单个 Dockerfile 文件来制作服务镜像:
- FROM rcntech/Ubuntu-grpc:v0.0.5
- EXPOSE 8080
- EXPOSE 8090
- # 将当前目录添加文件到 / bubble
- ARG APP_PROJECT_NAME
- # 复制父 pom.xml
- ADD /pom.xml /App/pom.xml
- ADD /protos /App/protos
- ADD $APP_PROJECT_NAME /App/$APP_PROJECT_NAME
- ADD scripts/gateway /App/gateway
- ADD scripts/docker/entrypoint.sh /App/entrypoint.sh
- RUN chmod u+x /App/entrypoint.sh
- ENTRYPOINT ["/app/entrypoint.sh"]
有了 Dockerfile 文件后, 在 docker-compose.YAML 里面做一些配置就能将服务打包成镜像:
- sms:
- build:
- context: ./
- dockerfile: scripts/docker/Dockerfile
- args:
- APP_PROJECT_NAME: "AppBubbleSmsService"
- environment:
- APOLLO_META: "http://apollo-configservice-dev:8080"
- APP_PROJECT_NAME: "AppBubbleSmsService"
- ENV: dev
同时编写了一个通用的 entrypoint.sh 脚本文件来启动服务器:
- #!/bin/bash
- export GOPATH=${
- HOME
- }/go
- export PATH=$PATH:/usr/local/go/bin:$GOPATH/bin
- rootProjectDir="/app"
- projectDir="${rootProjectDir}/${APP_PROJECT_NAME}"
- cd ${
- rootProjectDir
- }/AppBubbleCommon
- ./mvnw install
- cd $projectDir
- # 打包 App.jar
- ./mvnw package -DskipTests -P docker-build
- # 编译 proto 文件
- ./mvnw protobuf:compile protobuf:compile-custom -P docker-build
- # Run service
- java -jar ${
- projectDir
- }/target/App.jar
entrypoint.sh 脚本中将服务工程编译成 App.jar 包再运行服务. 还有 envoy 代理也要启动起来这样我们就可以使用 curl 或其他工具直接进行测试了.
总结
搭建这个工程大概摸索了一周的时间, 主要的时间是花在了 Protocol buffers 文件的管理与使用 Envoy 作为代理调试 gRPC 服务上. 文章中的示例工程已经传到了 GitHub: https://github.com/yjwfn/AppBubbleBackend 后面会打算慢慢的完善这个应用, 这是个简单的短视屏应用除了服务器还包含了 Android 和 iOS 端, 等到将后端微服务化为开源出来供学习交流使用.
参考引用
gRPC 官方文档 https://grpc.io/
Protocol buffers Maven 插件文档
Protocol buffers 官方文档 https://developers.google.com/protocol-buffers/
gRPC 官方文档中文版
- gRPC-JSON transcoder
- grpc-spring-boot-starter
来源: https://juejin.im/post/5c3ed5a2f265da616f703399