1. 程式人生 > >如何零基礎搭建一套微服務框架(Spring Boot + Dubbo + Docker + Jenkins)

如何零基礎搭建一套微服務框架(Spring Boot + Dubbo + Docker + Jenkins)

本文將以原理+實戰的方式,首先對“微服務”相關的概念進行知識點掃盲,然後開始手把手教你搭建這一整套的微服務系統。

這套微服務框架能幹啥?

這套系統搭建完之後,那可就厲害了:

  • 微服務架構

    你的整個應用程式將會被拆分成一個個功能獨立的子系統,獨立執行,系統與系統之間通過RPC介面通訊。這樣這些系統之間的耦合度大大降低,你的系統將非常容易擴充套件,團隊協作效率提升了N個檔次。這種架構通過眼下流行的SpringBoot和阿里巴巴吊炸天的Dubbo框架來實現。

  • 容器化部署

    你的各個微服務將採用目前處於浪潮之巔的Docker來實現容器化部署,避免一切因環境引起的各種問題,讓你們團隊的全部精力集中在業務開發上。

  • 自動化構建

    專案被微服務化後,各個服務之間的關係錯中複雜,打包構建的工作量相當可怕。不過沒關係,本文將藉助Jenkins,幫助你一鍵自動化部署,從此你便告別了加班。

知識點掃盲篇

咳咳,敲黑板啦!筆記趕緊記起來,課後我要檢查的!

知識點1:微服務

微服務一詞近幾年相當火,成為程式猿飯前便後裝逼熱門詞彙,你不對它有所瞭解如何在程式猿裝逼圈子裡混?下面我用最為通俗易懂的語言介紹它。

要講清楚微服務,我先要從一個系統架構的演進過程講起。

單機結構

我想大家最最最熟悉的就是單機結構,一個系統業務量很小的時候所有的程式碼都放在一個專案中就好了,然後這個專案部署在一臺伺服器上就好了。整個專案所有的服務都由這臺伺服器提供。這就是單機結構。

那麼,單機結構有啥缺點呢?我想缺點是顯而易見的,單機的處理能力畢竟是有限的,當你的業務增長到一定程度的時候,單機的硬體資源將無法滿足你的業務需求。此時便出現了叢集模式,往下接著看。

叢集結構

叢集模式在程式猿界有各種裝逼解釋,有的讓你根本無法理解,其實就是一個很簡單的玩意兒,且聽我一一道來。

單機處理到達瓶頸的時候,你就把單機複製幾份,這樣就構成了一個“叢集”。叢集中每臺伺服器就叫做這個叢集的一個“節點”,所有節點構成了一個叢集。每個節點都提供相同的服務,那麼這樣系統的處理能力就相當於提升了好幾倍(有幾個節點就相當於提升了這麼多倍)。

但問題是使用者的請求究竟由哪個節點來處理呢?最好能夠讓此時此刻負載較小的節點來處理,這樣使得每個節點的壓力都比較平均。要實現這個功能,就需要在所有節點之前增加一個“排程者”的角色,使用者的所有請求都先交給它,然後它根據當前所有節點的負載情況,決定將這個請求交給哪個節點處理。這個“排程者”有個牛逼了名字——負載均衡伺服器。

叢集結構的好處就是系統擴充套件非常容易。如果隨著你們系統業務的發展,當前的系統又支撐不住了,那麼給這個叢集再增加節點就行了。但是,當你的業務發展到一定程度的時候,你會發現一個問題——無論怎麼增加節點,貌似整個叢集效能的提升效果並不明顯了。這時候,你就需要使用微服務結構了。

微服務結構

先來對前面的知識點做個總結。

從單機結構到叢集結構,你的程式碼基本無需要作任何修改,你要做的僅僅是多部署幾臺伺服器,每臺伺服器上執行相同的程式碼就行了。但是,當你要從叢集結構演進到微服務結構的時候,之前的那套程式碼就需要發生較大的改動了。所以對於新系統我們建議,系統設計之初就採用微服務架構,這樣後期運維的成本更低。但如果一套老系統需要升級成微服務結構的話,那就得對程式碼大動干戈了。所以,對於老系統而言,究竟是繼續保持叢集模式,還是升級成微服務架構,這需要你們的架構師深思熟慮、權衡投入產出比。

OK,下面開始介紹所謂的微服務。

微服務就是將一個完整的系統,按照業務功能,拆分成一個個獨立的子系統,在微服務結構中,每個子系統就被稱為“服務”。這些子系統能夠獨立執行在web容器中,它們之間通過RPC方式通訊。

舉個例子,假設需要開發一個線上商城。按照微服務的思想,我們需要按照功能模組拆分成多個獨立的服務,如:使用者服務、產品服務、訂單服務、後臺管理服務、資料分析服務等等。這一個個服務都是一個個獨立的專案,可以獨立執行。如果服務之間有依賴關係,那麼通過RPC方式呼叫。

這樣的好處有很多:

  1. 系統之間的耦合度大大降低,可以獨立開發、獨立部署、獨立測試,系統與系統之間的邊界非常明確,排錯也變得相當容易,開發效率大大提升。

  2. 系統之間的耦合度降低,從而系統更易於擴充套件。我們可以針對性地擴充套件某些服務。假設這個商城要搞一次大促,下單量可能會大大提升,因此我們可以針對性地提升訂單系統、產品系統的節點數量,而對於後臺管理系統、資料分析系統而言,節點數量維持原有水平即可。

  3. 服務的複用性更高。比如,當我們將使用者系統作為單獨的服務後,該公司所有的產品都可以使用該系統作為使用者系統,無需重複開發。

那麼問題來了,當採用微服務結構後,一個完整的系統可能有很多獨立的子系統組成,當業務量漸漸發展起來之後,而這些子系統之間的關係將錯綜複雜,而且為了能夠針對性地增加某些服務的處理能力,某些服務的背後可能是一個叢集模式,由多個節點構成,這無疑大大增加了運維的難度。微服務的想法好是好,但開發、運維的複雜度實在是太高。為了解決這些問題,阿里巴巴的Dubbo就橫空出世了。

知識點2:Dubbo

Dubbo是一套微服務系統的協調者,在它這套體系中,一共有三種角色,分別是:服務提供者(下面簡稱提供者)、服務消費者(下面簡稱消費者)、註冊中心。

你在使用的時候需要將Dubbo的jar包引入到你的專案中,也就是每個服務都要引入Dubbo的jar包。然後當這些服務初始化的時候,Dubbo就會將當前系統需要釋出的服務、以及當前系統的IP和埠號傳送給註冊中心,註冊中心便會將其記錄下來。這就是服務釋出的過程。與此同時,也是在系統初始化的時候,Dubbo還會掃描一下當前系統所需要引用的服務,然後向註冊中心請求這些服務所在的IP和埠號。接下來系統就可以正常運行了。當系統A需要呼叫系統B的服務的時候,A就會與B建立起一條RPC通道,然後再呼叫B系統上相應的服務。

這,就是Dubbo的作用。

知識點3:容器化部署

當我們使用了微服務架構後,我們將一個原本完整的系統,按照業務邏輯拆分成一個個可獨立執行的子系統。為了降低系統間的耦合度,我們希望這些子系統能夠執行在獨立的環境中,這些環境之間能夠相互隔離。

在Docker出現之前,若使用虛擬機器來實現執行環境的相互隔離的話成本較高,虛擬機器會消耗較多的計算機硬體/軟體資源。Docker不僅能夠實現執行環境的隔離,而且能極大程度的節約計算機資源,它成為一種輕量級的“虛擬機器”。

知識點4:自動化構建

當我們使用微服務架構後,隨著業務的逐漸發展,系統之間的依賴關係會日益複雜,而且各個模組的構建順序都有所講究。對於一個小型系統來說,也許只有幾個模組,那麼你每次採用人肉構建的方式也許並不感覺麻煩。但隨著系統業務的發展,你的系統之間的依賴關係日益複雜,子系統也逐漸增多,每次構建一下你都要非常小心謹慎,稍有不慎整個服務都無法正常啟動。而且這些構建的工作很low,但卻需要消耗大量的精力,這無疑降低了開發的效率。不過沒關係,Jenkins就是來幫助你解決這個問題的。

我們只需在Jenkins中配置好程式碼倉庫、各個模組的構建順序和構建命令,在以後的構建中,只需要點選“立即構建”按鈕,Jenkins就會自動到你的程式碼倉庫中拉取最新的程式碼,然後根據你事先配置的構建命令進行構建,最後釋出到指定的容器中執行。你也可以讓Jenkins定時檢查程式碼倉庫版本的變化,一旦發現變動就自動地開始構建過程,並且讓Jenkins在構建成功後給你發一封郵件。這樣你連“立即構建”的按鈕也不需要按,就能全自動地完成這一切構建過程。

實戰動手篇

1. 學習目標

接下來我會帶著大家,以一個線上商城為例,搭建一套能夠自動化部署的微服務框架。這個框架能做如下幾件事情:

  1. 基於SpringBoot快速開發

    我們將選擇目前熱度很高的SpringBoot,最大限度地降低配置複雜度,把大量的精力投入到我們的業務開發中來。

  2. 基於Dubbo的微服務化

    我們會使用阿里巴巴的開源框架Dubbo,將我們的系統拆分成多個獨立的微服務,然後用Dubbo來管理所有服務的釋出和引用。有了Dubbo之後,呼叫遠端服務就像呼叫一個本地函式一樣簡單,Dubbo會幫我們完成遠端呼叫背後所需要的一切。

  3. 基於Docker的容器化部署

    由於使用了微服務架構後,我們的系統將會由很多子系統構成。為了達到多個系統之間環境隔離的目的,我們可以將它們部署在多臺伺服器上,可這樣的成本會比較高,而且每臺伺服器的效能可能都沒有充分利用起來。所以我們很自然地想到了虛擬機器,在同一臺伺服器上執行多個虛擬機器,從而實現環境的隔離,每個虛擬機器上執行獨立的服務。然而虛擬機器的隔離成本依舊很高,因為它需要佔用伺服器較多的硬體資源和軟體資源。所以,在微服務結構下,要實現服務環境的隔離,Docker是最佳選擇。它比虛擬機器更加輕量級,佔用資源較少,而且能夠實現快速部署。

  4. 基於Jenkins的自動化構建

    當我們採用了微服務架構後,我們會發現這樣一個問題。整個系統由許許多多的服務構成,這些服務都需要執行在單獨的容器中,那麼每次釋出的複雜度將非常高。首先你要搞清楚這些服務之間的依賴關係、啟動的先後順序,然後再將多個子系統挨個編譯、打包、釋出。這些操作技術難度低,卻又容易出錯。那麼有什麼工具能夠幫助我們解決這些問題呢?答案就是——Jenkins。 它是一款自動化構建的工具,簡單的來說,就是我們只需要在它的介面上按一個按鈕,就可以實現上述一系列複雜的過程。

2. 專案背景介紹

本文我以一個大家都非常熟悉的線上商城作為例子,一步步教大家如何搭建微服務框架,它有如下功能:

  • 產品管理 產品的增刪改查。
  • 訂單管理 訂單的增刪改查、購物車功能。
  • 使用者管理 使用者的登入、註冊、許可權管理、收貨地址等等。
  • 資料分析 提供對本系統資料分析的功能。

注意:本文的IDE使用的是intelliJ IDEA,推薦大家也用這個,用了都說好,用了你就會愛上它。

3. 建立專案的組織結構

在動手之前,我先來說一說這一步的目標:

  • 建立一個Maven Project,命名為“Gaoxi”

    這個Project由多個Module構成,每個Module對應著“微服務”的一個子系統,可獨立執行,是一個獨立的專案。這也是目前主流的專案組織形式,即多模組專案。

  • 在Gaoxi這個專案下建立各個子模組,每個自模組都是一個獨立的SpringBoot專案:

    • Gaoxi-User 使用者服務
    • Gaoxi-Order 訂單服務
    • Gaoxi-Product 產品服務
    • Gaoxi-Analysis 資料分析服務
    • Gaoxi-Controller 本系統的控制層,和以往三層結構中的Controller層的作用一樣,都是用作請求排程,只不過在微服務架構中,我們將它抽象成一個單獨的系統,可以獨立執行。
    • Gaoxi-Common-Service-Facade 它處於本系統的最底層,被所有模組依賴,一些公用的類庫都放在這裡。
    • Gaoxi-Redis 我們將Redis封裝成一個單獨的服務,執行在獨立的容器中,當哪一個模組需要使用Redis的時候,僅需要引入該服務即可,就免去了各種繁瑣的、重複的配置。而這些配置均在Gaoxi-Redis系統中完成了。

下面開始動手。

3.1 建立Project

  • New一個Project

    建立Project

  • 選擇Spring Initializr

    title

  • 設定groupId、artifactId、version

<groupId>com.gaoxi</groupId>
<artifactId>gaoxi</artifactId>
<version>0.0.1-SNAPSHOT</version>
  • Project建立完畢!接下來在Project下面建立Module

    3.2 建立Module

  • 在Project上New Module

    title

  • 和剛才一樣,選擇Spring Initializr,設定groupId、artifactId、version

  • 依次建立好所有的Module,如下圖所示:

    title

3.3 構建模組的依賴關係

目前為止,模組之間沒有任何聯絡,下面我們要通過pom檔案來指定它們之間的依賴關係,依賴關係如下圖所示:

title

Gaoxi-User、Gaoxi-Analysis、Gaoxi-Product、Gaoxi-Order這四個系統相當於以往三層結構的Service層,提供系統的業務邏輯,只不過在微服務結構中,Service層的各個模組都被抽象成一個個單獨的子系統,它們提供RPC介面供上面的Gaoxi-Controller呼叫。它們之間的呼叫由Dubbo來完成,所以它們的pom檔案中並不需要作任何配置。而這些模組和Gaoxi-Common-Service-Facade之間是本地呼叫,因此需要將Gaoxi-Common-Service-Facade打成jar包,並讓這些模組依賴這個jar,因此就需要在所有模組的pom中配置和Gaoxi-Common-Service-Facade的依賴關係。

此外,為了簡化各個模組的配置,我們將所有模組的通用依賴放在Project的pom檔案中,然後讓所有模組作為Project的子模組。這樣子模組就可以從父模組中繼承所有的依賴,而不需要自己再配置了。

下面開始動手:

  • 首先將Common-Service-Facade的打包方式設成jar

    當打包這個模組的時候,Maven會將它打包成jar,並安裝在本地倉庫中。這樣其他模組打包的時候就可以引用這個jar。

<groupId>com.gaoxi</groupId>
<artifactId>gaoxi-common-service-facade</artifactId>
<version>0.0.1</version>
<packaging>jar</packaging>
  • 將其他模組的打包方式設為war

    除了Gaoxi-Common-Service-Facade外,其他模組都是一個個可獨立執行的子系統,需要在web容器中執行,所以我們需要將這些模組的打包方式設成war

<groupId>com.gaoxi</groupId>
<artifactId>gaoxi-user</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
  • 在總pom中指定子模組

    modules標籤指定了當前模組的子模組是誰,但是僅在父模組的pom檔案中指定子模組還不夠,還需要在子模組的pom檔案中指定父模組是誰。

<modules>
    <module>Gaoxi-Analysis</module>
    <module>Gaoxi-Order</module>
    <module>Gaoxi-Product</module>
    <module>Gaoxi-User</module>
    <module>Gaoxi-Redis</module>
    <module>Gaoxi-Controller</module>
    <module>Gaoxi-Common-Service-Facade</module>
</modules>
  • 在子模組中指定父模組
<parent>
    <groupId>com.gaoxi</groupId>
    <artifactId>gaoxi</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <relativePath>../pom.xml</relativePath>
</parent>

到此為止,模組的依賴關係配置完畢!但要注意模組打包的順序。由於所有模組都依賴於Gaoxi-Common-Servie-Facade模組,因此在構建模組時,首先需要編譯、打包、安裝Gaoxi-Common-Servie-Facade,將它打包進本地倉庫中,這樣上層模組才能引用到。當該模組安裝完畢後,再構建上層模組。否則在構建上層模組的時候會出現找不到Gaoxi-Common-Servie-Facade中類庫的問題。

3.4 在父模組的pom中新增所有子模組公用的依賴

<dependencies>
    <!-- Spring Boot -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <!-- Spring MVC -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Boot Test -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- MyBatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.1</version>
    </dependency>

    <!-- Mysql -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- Dubbo -->
    <dependency>
        <groupId>io.dubbo.springboot</groupId>
        <artifactId>spring-boot-starter-dubbo</artifactId>
        <version>1.0.0</version>
    </dependency>

    <!-- gaoxi-common-service-facade -->
    <dependency>
        <groupId>com.gaoxi</groupId>
        <artifactId>gaoxi-common-service-facade</artifactId>
        <version>0.0.1</version>
    </dependency>

    <!-- AOP -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>

    <!-- guava -->
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>23.3-jre</version>
    </dependency>
</dependencies>

當父模組的pom中配置了公用依賴後,子模組的pom檔案將非常簡潔,如下所示:

<groupId>com.gaoxi</groupId>
<artifactId>gaoxi-user</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>

<name>gaoxi-user</name>

<parent>
    <groupId>com.gaoxi</groupId>
    <artifactId>gaoxi</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <relativePath>../pom.xml</relativePath>
</parent>

當專案的結構搭建完成之後,接下來你需要配置Docker環境,並將這些專案打包進容器中,驗證下是否能正常啟動。

4. 建立Docker容器

4.1 安裝Docker

在使用Docker之前,你當然先要安裝Docker,安裝過程較為簡單,基本上就是傻瓜式操作,這裡就不作過多介紹了,你可以在Docker的官網下載相應系統的安裝包。

4.2 獲取Tomcat映象

在微服務架構中,一個完整的系統被拆分成了多個被稱為“微服務”的子系統,這些子系統可以獨立執行在Web容器中。所以我們需要為這些系統提供執行的Web容器,這裡我們選擇大家較為熟悉的Tomcat。

我們知道,Tomcat依賴於Java環境,安裝Tomcat之前要進行一系列環境的配置:安裝Java、配置環境變數、安裝Tomcat等等。這些操作還是有些繁瑣的。不過沒關係,當使用了Docker之後,這些過程都可以輕而易舉地完成。

我們只需從Docker Hub上找到Tomcat的映象資源,然後從上面拉取下來就可以使用。你可以使用Tomcat官方的映象,也可以使用我釋出在Docker Hub上的Tomcat映象。

注意點:推薦使用我的Tomcat映象資源chaimm/tomcat,因為這個映象中除了配置Tomcat的安裝環境以外,還有一些本專案中要用到的Jenkins相關的配置。

採用如下命令從Docker Hub上拉取映象:

docker pull chaimm/tomcat:1.1

簡單解釋下,docker pull是從從Docker Hub上拉取映象的命令,後面的chaimm/tomcat是映象的名稱,:1.1是映象的版本號。目前這個映象的最新版本號是1.1,推薦大家拉取這個。

4.3 建立Tomcat容器

這裡再簡單介紹下“映象”和“容器”的關係。

“映象”就好比是面向物件中的“類”,“容器”就好比“類”建立的“物件”。在面向物件中,“類”定義了各種屬性,“類”可以例項化出多個“物件”;而在Docker中,“映象”定義了各種配置資訊,它可以例項化出多個“容器”。“容器”就是一臺可以執行的“虛擬機器”。

接下來我們需要為所有的微服務建立各自的容器:

  • gaoxi-user
  • gaoxi-product
  • gaoxi-order
  • gaoxi-analysis
  • gaoxi-controller
  • gaoxi-redis

以建立gaoxi-user容器為例,採用如下命令建立容器:

docker run --name gaoxi-user-1 -p 8082:8080 -v /usr/web/gaoxi-log:/opt/tomcat/gaoxi-log chaimm/tomcat:1.1
  • --name:指定容器的名字

  • -p:指定容器的埠對映

    -p 8082:8080 表示將容器的8080埠對映到宿主機的8082埠上

  • -v:指定容器資料卷的對映

    xxx:yyy 表示將容器yyy目錄對映到宿主機的xxx目錄上,從而訪問宿主機的xxx目錄就相當於訪問容器的yyy目錄。

  • chaimm/tomcat:1.1:表示容器所對應的映象。

這條命令執行成功後,你就可以通過你的IP:8082 訪問到gaoxi-user-1容器的tomcat了。如果你看到了那隻眼熟了貓,那就說明容器啟動成功了!

title

接下來,你需要按照上面的方法,給剩下幾個系統建立好Tomcat容器。

注意點:這裡要注意的是,你需要給這些Tomcat容器指定不同的埠號,防止埠號衝突。當然,在實際開發中,你並不需要將容器的8080埠對映到宿主機上,這裡僅僅是為了驗證容器是否啟動成功才這麼做的。

5. 整合Dubbo

5.1 建立zookeeper容器

Dubbo一共定義了三種角色,分別是:服務提供者、服務消費者、註冊中心。註冊中心是服務提供者和服務消費者的橋樑,服務消費者會在初始化的時候將自己的IP和埠號傳送給註冊中心,而服務消費者通過註冊中心知道服務提供者的IP和埠號。

在Dubbo中,註冊中心有多種選擇,Dubbo最為推薦的即為ZooKeeper,本文采用ZooKeepeer作為Dubbo的註冊中心。

建立ZooKeeper容器也較為簡單,大家可以直接使用我建立的ZooKeeper映象,通過如下命令即可下載映象:

docker pull chaimm/zookeeper-dubbo:1.0

該映象中不僅運行了一個zookeeper,還運行了一個擁有dubbo-admin專案的tomcat。dubbo-admin是Dubbo的一個視覺化管理工具,可以檢視服務的釋出和引用的情況。

使用如下命令啟動容器:

docker run --name zookeeper-debug -p 2182:2181 -p 10000:8080 chaimm/zookeeper-dubbo:1.0
  • -p 2182:2181:將容器的2181埠對映到宿主機的2182埠上,該埠是ZooKeeper的埠號。

  • -p 10000:8080:將容器的8080埠對映到宿主機的10000埠上,該埠是Dubbo-Admin所在Tomcat的埠號。

啟動成功後,你就可以通過你的IP:10000/dubbo-admin-2.8.4/訪問到Dubbo-Admin,如下圖所示:

title

5.2 父pom檔案中引入dubbo依賴

<!-- Spring Boot Dubbo 依賴 -->
<dependency>
    <groupId>io.dubbo.springboot</groupId>
    <artifactId>spring-boot-starter-dubbo</artifactId>
    <version>1.0.0</version>
</dependency>

5.3 釋出服務

假設,我們需要將Gaoxi-User專案中的UserService釋出成一項RPC服務,供其他系統遠端呼叫,那麼我們究竟該如何藉助Dubbo來實現這一功能呢?

  • 在Gaoxi-Common-Service-Facade中定義UserService的介面

    由於服務的釋出和引用都依賴於介面,但服務的釋出方和引用方在微服務架構中往往不在同一個系統中,所以需要將需要釋出和引用的介面放在公共類庫中,從而雙方都能夠引用。介面如下所示:

public interface UserService {

    public UserEntity login(LoginReq loginReq);
}
  • 在Gaoxi-User中定義介面的實現

    在實現類上需要加上Dubbo的@Service註解,從而Dubbo會在專案啟動的時候掃描到該註解,將它釋出成一項RPC服務。

@Service(version = "1.0.0")
public class UserServiceImpl implements UserService {

    @Override
    public UserEntity login(LoginReq loginReq) {
        // 具體的實現程式碼
    }
}
  • 在Gaoxi-User的application.properties中配置服務提供者的資訊
spring.dubbo.application.name=user-provider # 本服務的名稱
spring.dubbo.registry.address=zookeeper://IP:2182 # ZooKeeper所在伺服器的IP和埠號
spring.dubbo.protocol.name=dubbo # RPC通訊所採用的協議
spring.dubbo.protocol.port=20883 # 本服務對外暴露的埠號
spring.dubbo.scan=com.gaoxi.user.service # 服務實現類所在的路徑

按照上面配置完成後,當Gaoxi-User系統初始化的時候,就會掃描spring.dubbo.scan所指定的路徑下的@Service註解,該註解標識了需要釋出成RPC服務的類。Dubbo會將這些類的介面資訊+本伺服器的IP+spring.dubbo.protocol.port所指定的埠號傳送給Zookeeper,Zookeeper會將這些資訊儲存起來。這就是服務釋出的過程,下面來看如何引用一項RPC服務。

5.4 引用服務

假設,Gaoxi-Controller需要呼叫Gaoxi-User 提供的登入功能,此時它就需要引用UserService這項遠端服務。下面來介紹服務引用的方法。

  • 宣告需要引用的服務

    引用服務非常簡單,你只需要在引用的類中宣告一項服務,然後用@Reference標識,如下所示:

@RestController
public class UserControllerImpl implements UserController {

    @Reference(version = "1.0.0")
    private UserService userService;

    @Override
    public Result login(LoginReq loginReq, HttpServletResponse httpRsp) {

        // 登入鑑權
        UserEntity userEntity = userService.login(loginReq);
    }
}
  • 在Gaoxi-Controller的application.properties中配置服務消費者的資訊
spring.dubbo.application.name=controller-consumer # 本服務的名稱
spring.dubbo.registry.address=zookeeper://IP:2182 # zookeeper所在伺服器的IP和埠號
spring.dubbo.scan=com.gaoxi # 引用服務的路徑

上述操作完成後,當Gaoxi-Controller初始化的時候,Dubbo就會掃描spring.dubbo.scan所指定的路徑,並找到所有被@Reference修飾的成員變數;然後向Zookeeper請求該服務所在的IP和埠號。當呼叫userService.login()的時候,Dubbo就會向Gaoxi-User發起請求,完成呼叫的過程。這個呼叫過程是一次RPC呼叫,但作為程式猿來說,這和呼叫一個本地函式沒有任何區別,遠端呼叫的一切都由Dubbo來幫你完成。這就是Dubbo的作用。

6. 自動化構建

Jenkins是一個自動化構建工具,它可以幫助我們擺脫繁瑣的部署過程,我們只需要在一開始配置好構建策略,以後部署只需要一鍵完成。

6.1 建立Jenkins容器

Jenkins採用Java開發,也需要Java環境,但我們使用Docker後,一切都採用容器化部署,Jenkins也不例外。

  • 拉取映象

    這裡我們使用Jenkins官方提供的映象,大家只需執行如下命令拉取即可:

docker pull docker.io/jenkins/jenkins
  • 啟動容器 由於Jenkins執行在Tomcat容器中,因此我們將容器的8080埠對映到宿主機的10080埠上:
docker run --name jenkins -p 10080:8080 docker.io/jenkins/jenkins
  • 初始化Jenkins

    然後你需要訪問IP:10080,Jenkins會帶著你進行一系列的初始化設定,你只要跟著它一步步走就行了,比較傻瓜式。

6.2 在Jenkins中建立專案

接下來我們要做的是,在Jenkins中為每一個服務建立一個專案,每個專案中定義了構建的具體流程。由於我們將整個專案分成了6個微服務,所以我們需要在Jenkins中分別為這6個服務建立專案。那句開始吧~

  • 點選頁面左側的“新建”按鈕:

    title

  • 輸入專案名稱gaoxi-user,選擇“構建一個Maven專案”,然後點選“OK”:

    title

  • 配置Git倉庫

    選擇Git,然後輸入本專案Git倉庫的URL,並在Credentials中輸入Git的使用者名稱和密碼,如下圖所示:

    title

  • 構建觸發器

    選擇第一項,如下圖所示:

    title

  • Pre Step

    Pre Step會在正式構建前執行,由於所有專案都依賴於Gaoxi-Common-Service—Facade,因此在專案構建前,需要將它安裝到本地倉庫,然後才能被當前專案正確依賴。 因此,在Pre Step中填寫如下資訊:

    title

  • Build

    然後就是正式構建的過程,填寫如下資訊即可:

    title

OK,Gaoxi-User的構建過程就配置完成了。當我們點選“立即構建”按鈕時,Jenkins首先會從我們指定的Git倉庫中拉取程式碼,然後執行Pre Step中的Maven命令,將Gaoxi-Common-Serivce-Facade打包安裝到本地倉庫。然後執行Build過程,將Gaoxi-User進行編譯打包。 但此時Gaoxi-User仍然只是一個本地war包,並沒有部署到Tomcat容器中,而我們採用了容器化部署後,Jenkins服務和Gaoxi-User服務並不在同一個Docker容器中,那麼究竟該如何才能將Jenkins本地編譯好的war包傳送到Gaoxi-User容器中呢?這就需要使用Jenkins的一個外掛——Deploy Plugin。

6.3 遠端部署

  • 安裝外掛

    在系統管理–>外掛管理–>高階上傳deploy.hpi進行安裝。

  • 在父專案的pom檔案中增加遠端部署外掛:

<plugin>
    <groupId>org.codehaus.cargo</groupId>
    <artifactId>cargo-maven2-plugin</artifactId>
    <version>1.6.5</version>
    <configuration>
        <container>
            <!-- 指明使用的tomcat伺服器版本 -->
            <containerId>tomcat8x</containerId>
            <type>remote</type>
        </container>
        <configuration>
            <type>runtime</type>
            <cargo.remote.username>Tomcat的使用者名稱</cargo.remote.username>
            <cargo.remote.password>Tomcat的密碼</cargo.remote.password>
        </configuration>
    </configuration>
    <executions>
        <execution>
            <phase>deploy</phase>
            <goals>
                <goal>redeploy</goal>
            </goals>
        </execution>
    </executions>
</plugin>
  • 為Tomcat設定使用者名稱和密碼

    修改gaoxi-user容器中tomcat的tomcat-users.xml檔案,增加tomcat的manager使用者。

title

注意:如果你使用了chaimm/tomcat映象,那麼其中Tomcat配置都已經完成,預設使用者名稱:admin、預設密碼:jishimen2019。強烈建議修改使用者名稱和密碼。

  • 修改Jenkins中gaoxi-user的配置

    在“構建後操作”中增加如下配置:

    title

    • WAR/EAR files:表示你需要釋出的war包
    • Containers:配置目標Tomcat的使用者名稱和密碼

7. Maven的profile功能

在實際開發中,我們的系統往往有多套環境構成,如:開發環境、測試環境、預發環境、生產環境。而不同環境的配置各不相同。如果我們只有一套配置,那麼當系統從一個環境遷移到另一個環境的時候,就需要通過修改程式碼來更換配置,這樣無疑增加了工作的複雜度,而且易於出錯。但好在Maven提供了profile功能,能幫助我們解決這一個問題。

  • 父專案的pom中新增profile元素

    首先,我們需要在總pom的中新增多套環境的資訊,如下所示:

<profiles>
    <profile>
        <id>dev</id>
        <properties>
            <profileActive>dev</profileActive>
        </properties>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
    </profile>
    <profile>
        <id>test</id>
        <properties>
            <profileActive>test</profileActive>
        </properties>
    </profile>
    <profile>
        <id>prod</id>
        <properties>
            <profileActive>prod</profileActive>
        </properties>
    </profile>
</profiles>
  • 父專案的pom中新增resource元素

    resource標識了不同環境下需要打包哪些配置檔案。

<resources>
    <resource>
        <!-- 標識配置檔案所在的目錄 -->
        <directory>src/main/resources</directory>
        <filtering>true</filtering>
        <!-- 構建時將這些配置檔案全都排除掉 -->
        <excludes>
            <exclude>application.properties</exclude>
            <exclude>application-dev.properties</exclude>
            <exclude>application-test.properties</exclude>
            <exclude>application-prod.properties</exclude>
        </excludes>
    </resource>
    <resource>
        <directory>src/main/resources</directory>
        <filtering>true</filtering>
        <!-- 標識構建時所需要的配置檔案 -->
        <includes>
            <include>application.properties</include>
            <!-- ${profileActive}這個值會在maven構建時傳入 -->
            <include>application-${profileActive}.properties</include>
        </includes>
    </resource>
</resources>
  • 父專案的pom中新增外掛maven-resources-plugin

    該外掛用來在Maven構建時引數替換。

<plugin>
    <artifactId>maven-resources-plugin</artifactId>
    <version>3.0.2</version>
    <configuration>
        <delimiters>
            <delimiter>@</delimiter>
        </delimiters>
        <useDefaultDelimiters>false</useDefaultDelimiters>
    </configuration>
</plugin>
  • 在子專案中建立配置

    分別為dev環境、test環境、prod環境建立三套配置,application.proerpties中存放公用的配置。

    title

  • 在application.properties中新增[email protected]@

[email protected]@
  • 修改Jenkins的配置

    在所有Jenkins中所有Maven命令的末尾新增-P test,在打包的時候-P後面的引數將會作為@[email protected]的值傳入系統中,從而根據該值打包相應的application-{profileActive}.properties檔案。

8. 實戰演練

到此為止,所有準備工作都已經完成,接下來就可以進入程式碼開發階段。下面我以一個例子,帶著大家感受下有了這套微服務框架後,我們的開發流程究竟有了哪些改變?下面以開發一個使用者登入功能為例,介紹下使用本框架之後開發的流程。

8.1 演練目標

  • 在Gaoxi-User系統中實現登入的業務邏輯,併發布成RPC服務
  • 在Gaoxi-Controller中遠端呼叫登入服務,並向前端提供登入的REST介面

title

8.2 開發登入服務

首先需要在Gaoxi-Common-Service-Facade中建立UserService介面,並在其中宣告登入的抽象函式。

public interface UserService {

    public UserEntity login(LoginReq loginReq);
}

PS:為什麼要將UserService放在Gaoxi-Common-Service-Facade中?

在這個專案中,Gaoxi-User是UserService服務的提供方,Gaoxi-Controller是UserService服務的引用方。由於二者並不在同一個系統中,所以必須要藉助於Dubbo來實現遠端方法呼叫。而Dubbo釋出服務和引用服務的時候,都是根據服務的介面標識服務的,即服務引用方和釋出方都需要使用服務的介面,因此需要將服務的介面放在所有專案共同依賴的基礎模組——Gaoxi-Common-Service-Facade中。

然後在Gaoxi-User中開發UserService的實現——UserServiceImpl。UserServiceImpl上必須要加上Dubbo的@Service註解,從而告訴Dubbo,在本專案初始化的時候需要將這個類釋出成一項服務,供其他系統呼叫。

@Service(version = "1.0.0")
@org.springframework.stereotype.Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDAO userDAO;

    @Override
    public UserEntity login(LoginReq loginReq) {

        // 校驗引數
        checkParam(loginReq);

        // 建立使用者查詢請求
        UserQueryReq userQueryReq = buildUserQueryReq(loginReq);

        // 查詢使用者
        List<UserEntity> userEntityList = userDAO.findUsers(userQueryReq);

        // 查詢失敗
        if (CollectionUtils.isEmpty(userEntityList)) {
            throw new CommonBizException(ExpCodeEnum.LOGIN_FAIL);
        }

        // 查詢成功
        return userEntityList.get(0);
    }
}

8.3 引用登入服務

當UserService開發完畢後,接下來Gaoxi-Controller需要引用該服務,並向前端提供一個登入的REST介面。

若要使用userService中的函式,僅需要在userService上新增@Reference註解,然後就像呼叫本地函式一樣使用userService即可。Dubbo會幫你找到UserService服務所在的IP和埠號,併發送呼叫請求。但這一切對於程式猿來說是完全透明的。

@RestController
public class UserControllerImpl implements UserController {
    @Reference(version = "1.0.0")
    private UserService userService;

    @Override
    public Result login(LoginReq loginReq, HttpServletResponse httpRsp) {

        // 登入鑑權
        UserEntity userEntity = userService.login(loginReq);

        // 登入成功
        doLoginSuccess(userEntity, httpRsp);
        return Result.newSuccessResult();
    }
}

8.4 自動構建服務

上面的程式碼完成後,接下來你需要將程式碼提交至你的Git倉庫。接下來就是自動化部署的過程了。

你需要進入Jenkins,由於剛才修改了Gaoxi-User和Gaoxi-Controller的程式碼,因此你需要分別構建這兩個專案。

接下來Jenkins會自動從你的Git倉庫中拉取最新的程式碼,然後依次執行Pre Step、Build、構建後操作的過程。由於我們在Pre Step中設定了編譯Gaoxi-Common-Service-Facade,因此Jenkins首先會將其安裝到本地倉庫;然後再執行Build過程,構建Gaoxi-User,並將其打包成war包。最後將執行“構建後操作”,將war包釋出到相應的tomcat容器中。

至此,整個釋出流程完畢!

8.5 檢視服務的狀態

當Jenkins構建完成後,我們可以登入Dubbo-Admin檢視服務釋出和引用的狀態。

當我們搜尋UserService服務後,可以看到,該服務的提供者已經成功釋出了服務:

title

點選“消費者”我們可以看到,該服務已經被controller-consumer成功訂閱:

title

9. 總結

總結一下,這套框架有如下優勢:

  1. 微服務架構

    我們藉助於SpringBoot和Dubbo實現了微服務架構。微服務架構的理念就是將一個原本龐大、複雜的系統,按照業務功能拆分成一個個具有獨立功能、可以獨立執行的子系統,系統之間若有依賴,則通過RPC介面通訊。從而最大限度地降低了系統之間的耦合度,從而更加易於擴充套件、更加易於維護。

  2. 容器化部署

    我們藉助於Docker實現了容器化部署。容器能夠幫助我們遮蔽不同環境下的配置問題,使得我們只需要有一個Dockerfile檔案,就可以處處執行。和虛擬機器一樣,Docker也擁有環境隔離的能力,但比虛擬機器更加輕量級,由於每個容器僅僅是一條程序,因此它可以達到秒級的啟動速度。

  3. 自動化構建

    我們藉助於Jenkins實現了所有專案的自動化構建與部署。我們只需要點選“立即構建”這個按鈕,Jenkins就可以幫助我們梳理好錯綜複雜的專案依賴關係,準確無誤地完成構建,並將war包傳送到相應的web容器中。在啟動的過程中,Dubbo會掃描當前專案所需要釋出和引用的服務,將所需要釋出的服務釋出到ZooKeeper上,並向ZooKeeper訂閱所需的服務。

    有了Jenkins之後,這一切都是自動化完成。也許你並沒有太強烈地感受到Jenkins所帶來的便利。但是你想一想,對於一個具有錯綜複雜的依賴關係的微服務系統而言,如果每個服務的構建都需要你手動完成的話,你很快就會崩潰,你大把的時間將會投入在無聊但又容易出錯的服務構建上。而Jenkins的出現能讓這一切自動化完成。