本文将使用 Maven
、 gRPC
、 Protocol buffers
、 Docker
、 Envoy
等工具构建一个简单微服务工程,笔者所使用的示例工程是以前写的一个Java后端工程,因为最近都在
学习微服务相关的知识,所以利用起来慢慢的把这个工程做成微服务化应用。在实践过程踩过很多坑,主要是经验不足对微服务还是停留在萌新阶段,通过本文
记录创建微服务工程碰到一些问题,此次实践主要是解决以下问题:
本文假设读者已经了解以下相关知识:
由于是初步实现微服务,不会考虑过多的细节,现阶段只需要能够使用gRPC正常通信,后续计划会发布到 k8s
中,使用 istio
实现来服务网格。
现在比较流行的构建工具有 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 复制代码
以下是各个目录的用处简述,详细的用处文章后面都会提到,先在这里列出个大概:
知道大概的项目工程结构后我们创建一个父 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
的目的是为了区分生成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,这样配置起来非常灵活。
因为是使用微服务开发,而且RPC通信框架是使用的gRPC,所以每个服务工程都会使用 .proto
文件。服务工程之间又会有使用同一份 .proto
文件的需求,比如在进行RPC通信时服务提供方返回的消息 Test
定义在 a.proto
文件中,那么在使用方在解析消息时也同样需要 a.proto
文件来将接收到的消息转换成 Test
消息,因此管理 .proto
文件也有一些小麻烦。关于 Protocol buffers
的使用可参考官方文档。
在我们的示例项目中使用集中管理的方式,即将所有的.proto文件放置在同一个目录(AppBubbleBackend/protos)下并按服务名称来划分:
├── sms │ ├── SmsMessage.proto │ └── SmsService.proto └── user └── UserMessage.proto 复制代码
还可以将整个目录放置在一个单独的git仓库中,然后在项目中使用 git subtree
来管理文件。
有了上面的目录结构后,就需要配置一下 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是由Google开源的RPC通信框架,gRPC使用 Protocol buffers
定义服务接口并自动生成gRPC相关代码,有了这些代码后就可以非常方便的实现gRPC服务端和gPRC客户端,过多的细节就不细说了先看一下如何使用在 SpringBoot
中使用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(); } } 复制代码
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相关的文件都放置中 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
或其他工具直接进行测试了。