Spring Cloud 元件實作 (下)

Spring Cloud 元件實作 (下)

微服務已經徹底改變了我們設計和開發現代應用程式的方式。憑藉其對可擴展性、模組化和彈性的關注,微服務為工程師提供了一種強大的方法來構建堅固且適應性強的系統。在本教程中,我們將深入探討微服務的世界,探索其關鍵概念、優點和最佳實踐。無論您是一位經驗豐富的工程師想要提升技能,還是一位初學者渴望學習,本指南將為您提供必要的知識,讓您踏上微服務之旅。讓我們一起深入並發掘微服務的潛力吧!

本筆記參考以下課程:

其他針對此門課程找到的筆記:

課程架構圖概覽:

此為系列筆記的第二部。

建立專案

請參考 Github 專案目錄:Click

根據教學影片指引建立的練習專案,但是使用 Docker 建立微服務。

服務設定:Spring Cloud Config

由於微服務數量越來越多,各種服務若需要做設定非常費工,故有此組件出現,Cloud Config 也是一個微服務。 其功能有以下所列:

  1. 集中管理設定
  2. 拆分各種環境設定:DEV/PROD 等等
  3. 每個微服務可以統一向 Config Server 拿取自己的設定
  4. 執行時可以動態調整設定,不需要停止服務再更新
  5. 設定會以 RESTFUL 方式提供

整個 Cloud Config 有分成 Server 端與 Client 端,並搭配版本控制系統,工作架構圖如下:

流程為:將設定推送到 GIT,Config Server 拉取設定,再由 Config Client (各個微服務) 向 Server 獲取後設定。

Gitlab 的建立

這邊不直接使用 Github 來示範,改自建 Gitlab。

首先抓取 Gitlab 的 Docker image 後啟動,建立一個 Repository 來儲存要統一的各種設定檔案:

servies:
  gitlab-server:
    image: gitlab/gitlab-ce:16.0.1-ce.0
    hostname: gitlab.me
    container_name: gitlab-16
    environment:
      # 不設定加密
      GITLAB_OMNIBUS_CONFIG: |
        external_url "http://gitlab.me"
        letsencrypt['enable'] = false
    volumes: # local 儲存路徑從 .env 檔案中讀取
      - "${gitlab_data_location}/config:/etc/gitlab"
      - "${gitlab_data_location}/logs:/var/log/gitlab"
      - "${gitlab_data_location}/data:/var/opt/gitlab"
    ports:
      - 443:443
      - 80:80
    networks:
      - gitlab-network

networks:
  gitlab-network:
  • 為了方便設定 Gitlab,將資料、設定等資料夾指定掛到本機的特定目錄中 (Volume),有三個目錄:
    • config 資料夾會有初始 root 密碼的檔案,啟動完成後首次登入需要用到,24 小時候自動刪除
    • 其他兩個暫時不用管

Gitlab 一開始啟動的時候真的很慢,需要耐心等待。另外創建的專案直接 Public 就好了。

  • 創建出來的 URI 大概這樣的形式:http://gitlab.me/name/test.git
    • branch 預設是 main
  • 先直接在上面建立兩個檔案供之後測試用
config:
  version: 1.0
  enviroment: dev
config:
  version: 2.0
  enviroment: prod

Config Server 的建立

Config Server 也會是一個微服務,只有這個微服務會連線到 Git 上。

使用 Config Server 只要加以下 library:

// Spring Cloud Config Server
implementation "org.springframework.cloud:spring-cloud-config-server:${springCloudConfigServerVersion}"
  • Config Server 一樣會連到 Eureka
spring:
  application:
    name: config-center
  mvc:
    converters:
      preferred-json-mapper: gson
  cloud:
    config:
      server:
        git:
          # Git 位置
          uri: ${CONFIG_SERVER_GIT_URI:http://gitlab.me/enixlin/springcloudtest.git}
          # 設定檔的目錄位置
          search-paths: ${GIT_SEARCH_PATHS:springcloudtest}
      # 讀取的分支
      label: ${GIT_LABEL:main}
  • 最主要的設定為 Git 位置、Branch 名稱、目錄位置

Springboot 啟動類別需要再加一個 EnableConfigServer Annotation:

@SpringBootApplication
@EnableConfigServer
class ConfigCenterApplication

最後只要輸入特定的 URL 樣式就可以透過 ConfigServer 取得 Gitlab 上面的 yml 設定檔案:

  1. http://{config-server-domain}/{label}/{application}-{profile}.yml
    • 最好用這種,因為比較清楚
    • 例如:http://localhost:3344/main/config-dev.yml
  2. http://{config-server-domain}/{application}-{profile}.yml
  3. http://{config-server-domain}/{application}-{profile}[/{label}]

如果輸入的 URI 沒有對應的設定檔,則會得到一個空的 Json 內容:{}

bootstrap.yml 與application.yml

重要:Spring Cloud Config Client has changed and technically bootstrap.properties and bootstrap.yml files are deprecated. 以下了解即可。

這裡要先說明 bootstrap.yml 這個檔案,本質上與 application.yml 一樣是設定檔,但是其載入的順序是不一樣的。

  • bootstrap.yml 用來在程式引導時執行,用於更加前期配置資訊讀取,如可以使用來設定 application.yml 中使用到參數等
  • bootstrap.yml 先於 application.yml 載入

當使用 Spring Cloud Config Server 的時候,會先在 bootstrap.yml 裡面指定 spring.application.name 和 spring.cloud.config.server.git.uri 和一些加密/解密的資訊。 因為有些啟動時必須要知道的設定被放在 Git 上了,所以有必要有讀取的優先順位。

Config Client 建立

Client 的部分加上以下 library:

// Spring cloud config
implementation "org.springframework.cloud:spring-cloud-starter-config:${springCloudConfigServerVersion}"

再來需要設定要從那個位置讀取設定檔,所以需要於 application.yml 中設定:

spring:
  application:
    name: config-client
  mvc:
    converters:
      preferred-json-mapper: gson

  config:
    # 這個是 Springboot 2.4 之後需要增加的設定值,否則會錯誤
#    import: "optional:configserver:"
    import: "configserver:"
  cloud:
    config:
      # 以下會組成 URI => http://config-3344.com:3344/main/config/dev
      # Branch 名稱
      label: ${CONFIG_LABEL:main}
      # 設定檔名稱
      name: ${CONFIG_NAME:config}
      # 後綴環境名稱
      profile: ${CONFIG_PROFILE:dev}
      # Config Server 位置
      uri: ${CONFIG_URI:http://gitlab.me}
  • spring.config.import 一定要加
  • 其他的 ConfigCenter 位置、設定檔版本、Branch、環境等則對應 Config Center 設定即可。

再來就可以寫一個 Controller 回應這些設定值來確定是否有讀取到數值:

@RequestMapping("/info")
@RestController
@RefreshScope
class ClientController {

    @Value("\${config.version}")
    lateinit var version: String

    @Value("\${config.enviroment}")
    lateinit var enviroment: String

    @GetMapping("/config")
    fun getConfigInfo(): ResponseJson = ResponseJson()
        .setStatus(CodeMessage.SUCCESS)
        .addData<JsonObject> {
            addProperty("enviroment", enviroment)
            addProperty("version", version)
        }
}

監控與重新取得設定值

有了 Config Center 與 Client 後,如果 Git 有任何修改,僅 Center 的部分會立即取到新的設定值,而 Client 的部分是會需要額外設定的。

為此須對 Client 增加以下設定方能於設定值被更改時,能通知其進行更新而不用重新啟動服務:

  1. 針對需要能更新的 Bean 加上 RefreshScope Annotation。
@RequestMapping("/info")
@RestController
@RefreshScope
class ClientController {
   
    @Value("\${config.version}")
    lateinit var version: String
    
    //...
}
  1. 增加監控用的 Library:
implementation 'org.springframework.boot:spring-boot-starter-actuator'
  1. 開啟對外暴露的控資訊:application.yml 設定
management:
  endpoints:
    web:
      exposure:
        # 這邊簡單點,直接全開
        include: "*"
#        include: health,info,beans,conditions
#        exclude:

關於 actuator 監控的部分設定,詳細可參考以下文件:

  1. 最後要要更新的話,直接對要更新的微服務打 Post Request 通知就能生效。
curl -X POST "http://your-service/actuator/refresh"

如果打了 refresh 但是沒有新的數值變動,那就只會返回一個空的 Json Array。

Spring Cloud Bus

詳細的官方文件:Spring Cloud Bus

在前面一章,如果微服務有非常多,而每個都要更新的話,那要分開打非常多的 POST Request,故還需要有一個機制能夠通知更新一個服務,就能全部服務都更新。

以更新的機制來說,可以有兩種實踐概念:

  1. 先通知其中一個 Client,再由該 Client 通知其他 Client。
    • 這種作法會讓其中一個 Client 承擔額外的通知職責,並不好

  1. 直接讓 Config Center 去通知所有的 Client,也就是只通知一次 Config Center。
    • 這個是此節要說明的方法

SpringCloud Bus 可以處理微服務群集中訊息的傳播,可以用來廣播狀態更改、訊息推送、亦或是訊息通道。

這邊會讓所有的微服務都訂閱同一個設定檔 Topic,當 Config Server 有新的數值變化時,可以讓所有訂閱該 Topic 的微服務 Client 都能夠及時更改。

而作為其配套的 MessageQueue ,Spring 僅支援 RabbitMQ 與 Kafka。本節以 RabbitMQ 來操作。

RabbitMQ 建立

可以先拉取 management tag 的版本,因為個有預先開啟 management plugin,可以使用 Web 去查看監控。

docker pull rabbitmq:3.9.29-management-alpine
  • 如果需要其他的外掛,則需要再寫 dockerfile 來設定
  • 監控頁面預設的 port 是 15672,帳號密碼都是 guest
  • API 功能的使用則是用到 5672 port

RabbitMQ 等 Message Queue 的知識,需要另外再補足,這邊不說明。

增加 SpringCloudBus 並通知更新

有了 RabbitMQ 後,需要於 Client、Center 兩邊都加上以下 Library:

// 監控用
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// Spring cloud Bus
implementation "org.springframework.cloud:spring-cloud-starter-bus-amqp:${springCloudBusVersion}"
  • 監控用 actuator 如果不加上去,你所有打的 actuator 開頭 API 都只會回應你 Method not allowed.

於 application.yml 中亦需要將 RabbitMQ 的設定放上,不管是 Config Center 還是 Client 都要:

spring:
  rabbitmq:
    host: ${RABBITMQ_HOST:rabbit-mq}
    port: ${RABBITMQ_PORT:5672}
    username: ${RABBITMQ_USERNAME:guest}
    password: ${RABBITMQ_PASSWORD:guest}

最後我們只要通知 Config Center 去通知所有其他有訂閱的微服務,就可以達到『減少通知動作至一次,便可全部更新』的效果:

curl -X POST "http://localhost:3344/actuator/busrefresh"
  • 根據 Spring cloud 2020.0 的文件, API 已經從原本的 /bus-refresh 改成了 /busrefresh

最後我們可以有以下的流程總結圖:

Spring Cloud Stream

Stream 主要是 Spring 把 Message Queue 做更一層的封裝,在同時使用多種 MQ 的時候,可以利用一致性的 API 來調用各種 MQ,達到 Code 統一、方便使用的效果。甚至可以做到在各種 Message Queue 的訊息內容轉換。

目前 4.0.3 版本,其支援的 Message Queue 主要是 RabbitMQ、Apache Kafka。

  • RabbitMQ 有 Exchange 的概念
  • Kafka 有 Topic、Partition 的概念
  • 各種 MQ 有其不同的概念…

詳細的官方文件:

一般 Message Queue 的運作流程如下:

  1. 生產者端建立 Message 物件
  2. Message 物件透過 MessageChannel 物件作為通道傳遞
  3. 消費者端的 MessageChannel 其 Subclass SubscriberChannel 使用 MessageHandler 做訂閱的處理

而 Stream 引入後,其關鍵的物件為 Binder 物件。

Stream 使用的物件與其 Annotation

在 Spring Cloud Stream version 4 後,Annotation 的做法已經不可用了,需要轉為 Functional Programing 的作法。

操作 Stream 會有以下三介面:

  1. Binder 介面
  2. MessageChannel 介面:主要會注入使用的物件,進行訊息傳遞與接收
  3. Sink、Source 介面:用於定義要綁定的 Type 為接收或者是傳送

Annotation:Deprecated

  1. @Input:標註為傳送用
  2. @Output:標註為接收用
  3. @EnableBinding:標註將 Channel 與 RabbitMQ 的 Exchange 綁定,連帶要定義是接收還是傳送
  4. @StreamListener:Kafka 用的訊息消費者使用
    • 3.1.0 後已被廢棄,推薦用 functional programming 設定

Functional Programing 的做法

Annotation 作法廢棄後,可以直接建立 Java 8 的 Consumer/Function/Producer 來替換原有的 Source/Sink 等等物件。

作法參考以下文章:

  1. Spring Cloud Streams With Functional Programming Model
  2. spring cloud stream详解

application.yml 的相關設定可參考文件:

發送方設置

首先需要加上 Stream – RabbitMQ 的 Library。

// Stream with Rabbit MQ
implementation "org.springframework.cloud:spring-cloud-starter-stream-rabbit:${springCloudStreamRabbitMQVersion}"

application.yml 也設定好 RabbitMQ 的參數:

spring:
  rabbitmq:
    host: ${RABBITMQ_HOST:localhost}
    port: ${RABBITMQ_PORT:5672}
    username: ${RABBITMQ_USERNAME:guest}
    password: ${RABBITMQ_PASSWORD:guest}
    connection-timeout: 15s

如果需要手動發送訊息,則會需要名為 StreamBridge 的物件,並使用其 send() 方法:

@RestController
@RequestMapping("/message")
class MessageController(
    val streamBridge: StreamBridge
) {

    @Value("\${server.port:Unknown}")
    lateinit var port: String

    @Value("\${custom.binding-name:my-message}")
    lateinit var bindingName: String

    @GetMapping("/send/{message}")
    fun sendMessage(
        @PathVariable("message") message: String?
    ): ResponseJson = ResponseJson()
        .setStatus(
            when (streamBridge.send(bindingName, message)) {
                true -> CodeMessage.SUCCESS
                false -> CodeMessage.FAILURE
            }
        )
        .addData<JsonObject> {
            addProperty("port", port.toInt())
            addProperty("message", message)
        }
}
  • send 方法參數第一個的 bingname,這會是與 RabbitMQ 中看到的 Exchange name 一樣。

消費方設置

若使用 Consumer 物件,Spring 會根據設定進行綁定並消費。

import java.util.function.Consumer

@Configuration
class MessageService {

    private val log = LoggerUtil.getLogger<MessageService>()

    @Bean
    fun messageConsumer() = Consumer<String> {
        log.info { "Got message : $it" }
    }
}
  • 只要引入 Stream,創建這些 Consumer 等等的 Bean 就會被 Stream 作為處理訊息消費/處理的物件。

再來於 application.yml 中設定綁定的屬性,要配合上面的 Bean 名稱:

spring:
  application:
    name: message-consumer
  cloud:
    function:
      # 需要啟用的 Method 名稱
      definition: messageConsumer #;messageSupplier
    stream:
      function:
        bindings:
          # 自訂對應表:[方法名]-[in/out]-[Number]
          messageConsumer-in-0: customConsumer1
      bindings:
        # 自訂名稱
        customConsumer1:
          # 綁定指定的 Exchange
          destination: ${RECEIVE_DESTINATION:my-message}
          group: ${RECEIVE_GROUP:my-group-1}
  • binding 名稱 Spring 有其欲設定命名方式:[方法名]-[in/out]-[Number]
    • spring.cloud.stream.function.bindings 可以自訂對應表,簡化原有的預設寫法。
  • spring.cloud.stream.binding 可以針對 Binder 做綁定設定。
    • destination 設定 Exchange anme
    • group 為組別 ( 重要 )

分組、重複訊息、持久化訊息

最關鍵的設定就是 group。組別內的消費者會是競爭關係,同一訊息僅有一個消費者能處理。 RabbitMQ 如果兩個 Consumer 沒有設定同一個群組,就會有多次消費的狀況產生。

例如一個訂單訊息,應該只要被處理一次的消費流程,不同有重複訂單出現。

組別可以登入 RabbitMQ 管理頁面的 Queue 頁籤看到,如果沒有設定組別,則會是一個隨機組別名,相當於每個 Consumer 都有自己的組別。

設定 Group 的好處還不只防止重複消費。 只有設定好 Group,當消費方服務掛了然後重新連回,於斷線期間沒有收到的訊息是可以再次接收的,有防範訊息遺失的效果。

Spring Cloud Sleuth

根據官方描述:

Spring Cloud Sleuth will not work with Spring Boot 3.x onward. The last major version of Spring Boot that Sleuth will support is 2.x.

主要功能將遷移到 Micrometer Tracing 去了。

概念

由於微服務越來越多,各種的調用關係越來越複雜,故有必要有一個機制去做追蹤與定位問題。

Request 的整條調用鍊會有一個 TraceID。 每個微服務間的調用則會有一個 SpanID 以及上一級的 ParentID。

Micrometer Tracing

參考以下文章解說:

此節採用 Brave 串接,因為 opentelemetry 一直嘗試失敗。

整個 API 使用的連接順序為:

  1. micrometer-tracing
  2. micrometer-tracing-bridge-brave
    • Brave 可以想為類似 SLF4J 的介面,摒除各式追蹤系統,提供一個統一調用
  3. zipkin-reporter-brave
    • 將追蹤的各式資訊傳給 Zipkin Server 儲存
  4. zipkin
    • 利用其提供的 UI 介面可以查看到整個鏈路調用的流程

如果需要與 OpenFeign 搭配,還需要額外的 Library

// 監控用
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// Micrometer Tracing
implementation "io.micrometer:micrometer-tracing:${micrometerVersion}"
implementation "io.micrometer:micrometer-tracing-bridge-brave:${micrometerVersion}"
implementation "io.zipkin.reporter2:zipkin-reporter-brave:${zipkinReporterBraveVersion}"
implementation "io.github.openfeign:feign-micrometer:${feignMicrometerVersion}"

再來設定 application.yml 的監控相關設定就可以完成設定。

# 監控
management:
  endpoints:
    web:
      exposure:
        include: '*'
  tracing:
    sampling:
      # 取樣率 0 ~ 1 , 1 最大 (最耗性能),預設 0.1
      probability: 1
  zipkin:
    tracing:
      endpoint:  "${ZIPKIN_URL:http://localhost:9411}/api/v2/spans"

以下可以看到調用鍊的圖形化介面:

Spring Cloud Alibaba

此篇筆記不打算針對這一塊進行詳細了解。

Seata 這個部分是說明微服務下,如何保證交易完整性,也就是在多個微服務加上多個資料庫的架構時該如何處理。

這一塊前面沒有說明,可以了解。

發佈留言