SpringBoot集成gRPC微服務工程搭建實踐

前言

本文將使用MavengRPCProtocol buffersDockerEnvoy等工具構建一個簡單微服務工程,筆者所使用的示例工程是之前寫的一個Java後端工程,由於最近都在 學習微服務相關的知識,因此利用起來慢慢的把這個工程作成微服務化應用。在實踐過程踩過不少坑,主要是經驗不足對微服務仍是停留在萌新階段,經過本文 記錄建立微服務工程碰到一些問題,這次實踐主要是解決如下問題:php

  • 如何解決、統一服務工程依賴管理
  • SpringBoot集成gRPC
  • 管理Protocol buffers文件
  • 使用Envoy代理訪問gRPC
  • 部署到Docker

本文假設讀者已經瞭解如下相關知識:html

  • Maven
  • Envoy
  • gRPC
  • Protocol buffers
  • SpringBoot
  • Docker

因爲是初步實現微服務,不會考慮過多的細節,現階段只須要可以使用gRPC正常通訊,後續計劃會發布到k8s中,使用istio實現來服務網格。java

使用Maven

如今比較流行的構建工具備MavenGradle,現階段後端開發大多數都是用的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

  1. 工程主目錄
  2. 單個服務工程目錄(模塊)
  3. docker-compose發佈文件
  4. 存放.proto文件
  5. 發佈、編譯時用到的腳本文件

知道大概的項目工程結構後咱們建立一個父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

使用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服務運行起來後就須要進行調試了,好比使用curlchrome等工具向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 後面會打算慢慢的完善這個應用,這是個簡單的短視屏應用除了服務器還包含了AndroidiOS端,等到將後端微服務化爲開源出來供學習交流使用。

參考引用

相關文章
相關標籤/搜索