本文将使用Maven
、gRPC
、Protocol buffers
、Docker
、Envoy
等工具构建一个简单微服务工程,笔者所使用的示例工程是之前写的一个Java后端工程,由于最近都在 学习微服务相关的知识,因此利用起来慢慢的把这个工程作成微服务化应用。在实践过程踩过不少坑,主要是经验不足对微服务仍是停留在萌新阶段,经过本文 记录建立微服务工程碰到一些问题,这次实践主要是解决如下问题:php
本文假设读者已经了解如下相关知识:html
因为是初步实现微服务,不会考虑过多的细节,现阶段只须要可以使用gRPC正常通讯,后续计划会发布到k8s
中,使用istio
实现来服务网格。java
如今比较流行的构建工具备Maven
和Gradle
,现阶段后端开发大多数都是用的Maven因此本工程也使用Maven来构建项目,固然使用Gradle也能够二者概念大都想通,不一样的地方大可能是实现和配置方式不一致。git
根据Maven
的POM文件继承特性,将工程分不一样的模块,全部的模块都继承父pom.xml
的依赖
、插件
等内容,这样就能够实现统一管理,并方便之后管理、维护。先看一下大概的项目结构:github
AppBubbleBackend (1)
├── AppBubbleCommon
├── AppBubbleSmsService (2)
├── AppBubbleUserService
├── docker-compose.yaml (3)
├── pom.xml
├── protos (4)
│ ├── sms
│ └── user
└── scripts (5)
├── docker
├── envoy
├── gateway
└── sql
复制代码
如下是各个目录的用处简述,详细的用处文章后面都会提到,先在这里列出个大概:spring
知道大概的项目工程结构后咱们建立一个父pom.xml
文件,放在AppBubbleBackend
目录下面:sql
<?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
文件:chrome
<?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
文件,这样能够很方便的更改依赖、配置等信息。docker
Maven提供依赖中心化的管理机制,经过项目继承特性全部对AppBubbleBackend/pom.xml
所作的更改都会对其余模块产生影响,详细的依赖管理 内容可查看官方文档。apache
<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
或其余工具直接进行测试了。
搭建这个工程大概摸索了一周的时间,主要的时间是花在了Protocol buffers
文件的管理与使用Envoy
做为代理调试gRPC服务上。文章中的示例工程已经传到了GitHub: AppBubbleBackend 后面会打算慢慢的完善这个应用,这是个简单的短视屏应用除了服务器还包含了Android
和iOS
端,等到将后端微服务化为开源出来供学习交流使用。