前言
本文將使用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: AppBubbleBackend 後面會打算慢慢的完善這個應用,這是個簡單的短視屏應用除了伺服器還包含了Android
和iOS
端,等到將後端微服務化為開源出來供學習交流使用。