第二大腦

人生,是靠輸出達到轉變。

  • article-使用 Go Workspaces 管理系統服務:從單體架構到分散式架構的彈性實現

    2024/9/15

    技術
    使用 Go Workspaces 管理系統服務:從單體架構到分散式架構的彈性實現

    在這篇文章,我要跟大家分享一下我是怎麼使用 Go workspaces 來管理多個系統服務,並且實現單體架構和微服務架構之間的彈性切換。這其實是我這幾年在做支付和金融系統時的一些實戰經驗,當系統越來越大,很多架構上的決策就變得非常重要。這篇文章會幫大家解開一些疑惑,還會分享怎麼設計專案的檔案目錄結構,讓系統在擴展時變得更靈活好維護。

    1. 為什麼用 Go workspaces?

    大家可能都有過類似的經驗,當系統變得複雜的時候,不同服務之間的依賴關係和版本管理會讓人頭大。特別是當你把系統拆成很多微服務後,怎麼有效管理這些模組之間的依賴,不讓它們互相影響,就變得非常棘手。

    Go 在 1.18 推出的 workspaces 功能,真的可以幫助解決這個問題。透過這個工具,我們可以把每個服務獨立出來成為一個 Go module,這樣每個模組的依賴關係都可以獨立管理,而且還可以更輕鬆地在單體架構和微服務之間切換。

    2. 如何規劃檔案目錄結構

    說到如何規劃一個專案的目錄結構,這其實是整個系統維護的核心。特別是當你要在單體和微服務架構間保持彈性時,規劃得當的目錄結構可以讓開發、測試和部署都變得更順利。

    這是我常用的目錄結構範例,適合同時管理多個微服務或模組:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    project-root/

    ├── go.work # Go workspace 設定檔
    ├── go.work.sum # Go workspace 依賴管理檔
    ├── services/ # 所有系統服務的目錄
    │ ├── user-service/ # 使用者服務
    │ │ ├── cmd/
    │ │ │ └── main.go
    │ │ ├── internal/
    │ │ ├── pkg/
    │ │ └── go.mod
    │ ├── order-service/ # 訂單服務
    │ │ ├── cmd/
    │ │ │ └── main.go
    │ │ ├── internal/
    │ │ ├── pkg/
    │ │ └── go.mod
    │ ├── payment-service/ # 支付服務
    │ │ ├── cmd/
    │ │ │ └── main.go
    │ │ ├── internal/
    │ │ ├── pkg/
    │ │ └── go.mod
    │ └── shared/ # 各服務共用的程式碼(例如工具函式)
    │ ├── internal/
    │ └── pkg/
    ├── Makefile # 管理建置和部署的腳本
    └── README.md # 專案說明文件

    這個結構讓我們可以把系統中的每個業務模組分別放在獨立的資料夾裡,每個模組都有自己的 go.mod 檔案,這樣每個模組的依賴就能各自管理,而且如果將來要把某個服務獨立出來運行或擴展,也會非常方便。

    3. Go Workspace 的設定

    要開始使用 Go Workspace,可以利用 Go 提供的 CLI 工具進行自動化的設定。

    首先,進入專案的根目錄,並透過以下指令來初始化一個新的 workspace:

    1
    go work init

    這條命令會在專案根目錄下建立一個 go.work 檔案,表示這是我們的 Go workspace 的起點。

    接下來,我們可以使用 go work use 來將現有的模組(例如 user-serviceorder-servicepayment-service)加入到 workspace 裡面:

    1
    go work use ./services/user-service ./services/order-service ./services/payment-service

    這會自動將這三個模組加入到 go.work 檔案中,這樣你的 workspace 檔案就會像這樣:

    1
    2
    3
    4
    5
    6
    7
    go 1.18

    use (
    ./services/user-service
    ./services/order-service
    ./services/payment-service
    )

    這個流程讓我們可以快速管理多個模組,並確保這些模組在同一個 workspace 中協作開發,非常方便。

    4. 實現單體和微服務架構的彈性切換

    在實際的專案開發中,我們經常需要在 單體架構微服務架構 之間靈活切換,特別是系統在初期和後期的需求會有所不同。

    單體架構的情境

    在專案初期,如果你的團隊規模不大,或是系統還在快速迭代的階段,單體架構往往是個不錯的選擇。這樣的架構部署起來比較簡單,所有服務都會打包在一個應用內運行。

    你可以在專案的 main.go 中把所有模組都匯入,像這樣:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package main

    import (
    "services/user-service"
    "services/order-service"
    "services/payment-service"
    )

    func main() {
    go user-service.Start()
    go order-service.Start()
    go payment-service.Start()

    select {} // 避免 main goroutine 結束
    }

    然後,你只需要執行 go build,就能把整個應用打包成一個單一的可執行檔,部署也會變得很簡單。

    微服務架構的情境

    當系統越來越大,某些服務的負載可能會不斷增加,這時就需要把這些服務拆分成獨立的微服務。透過 Go workspace,我們可以輕鬆地把每個模組分開,變成獨立的服務來部署。

    你只需要進到每個模組的資料夾,然後各自建置它們的可執行檔:

    1
    2
    3
    4
    5
    6
    7
    8
    cd services/user-service
    go build -o user-service

    cd services/order-service
    go build -o order-service

    cd services/payment-service
    go build -o payment-service

    這樣一來,每個微服務可以分別部署在不同的服務器或容器裡,達到系統的彈性擴展。

    5. 結語

    透過 Go workspaces 和合理的檔案目錄結構設計,我們能夠輕鬆管理多個系統服務,並且可以在單體架構和微服務架構之間靈活切換。這不僅提升了開發效率,也讓我們在面對系統擴展時更加從容。希望這篇文章能夠讓你在處理多服務管理的時候,也能夠輕鬆應對不同的需求變化。

    如果你剛好在做類似的專案,或是考慮將系統進行服務拆分,試試 Go workspaces,或許會讓你感到意外地好用!

  • article-在 Spring Boot 使用 AOP 印日誌

    2021/6/26

    技術
    在 Spring Boot 使用 AOP 印日誌

    使用 AOP (Aspect Oriented Programming) 的方式印出日誌,會比在各處程式中寫印日誌來的簡潔,集中管理印日誌的程式,避免影響閱讀業務邏輯。

    建置 Log4j2

    參考這篇建置 Log4j2 文章。

    加入 Dependency

    build.gradle 加入 Spring Boot AOP dependencies:spring-boot-starter-aop

    1
    2
    3
    4
    5
    6
    7
    8
    dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    compileOnly 'org.projectlombok:lombok:1.18.18'
    annotationProcessor 'org.projectlombok:lombok:1.18.18'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    }

    定義 TimeLogAspect

    想像各個業務邏輯是縱向的流程,而 AOP 就是將流程橫剖後織入程式,藉此達到關注點分離。

    元件上標註 @Aspect,即可定義為一個切面,注意也要將物件加註 @Component,Spring Boot 框架才得以管理這個元件。在方法上標註 @Around 指的是在切面的前、後織入程式,參數 ProceedingJoinPoint 是相對於橫切面的縱向資料流,可以由此參數取得資料流中的方法簽章和傳入參數等資訊。而 @Pointcut 定義切面的切點,例如,切點可以是有標註自定義的 Annotation,或是某個 Controller Package 下的所有 method。

    以下是定義在所有標註 @TimeLog 或 Controller 的 Aspect 範例,計算執行這些方法需要多少時間,並將執行時間於方法回傳後印到日誌中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @Log4j2
    @Aspect
    @Component
    public class TimeLogAspect {

    @Around("logTime() || controller()")
    public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
    long startMillis = System.currentTimeMillis();
    Object proceed = pjp.proceed();
    long executionTime = System.currentTimeMillis() - startMillis;
    log.info(String.format("Completed %s in %d ms", pjp.getSignature().toShortString(), executionTime));
    return proceed;
    }

    @Pointcut("@annotation(cc.secondbrain.demo.annotation.TimeLog)")
    public void logTime() {
    }

    @Pointcut("execution(* cc.secondbrain.demo.controller.*.*(..))")
    public void controller() {
    }

    }

    自定義 Annotation @TimeLog 如下,

    1
    2
    3
    4
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface TimeLog {
    }

    使用自定義 Annotation @TimeLog,可以自由地在想要記錄執行時間的方法上註記,但只限於 public 方法。
    值得特別注意的是,假如在 Controller 註記 @TimeLog,只會印出一行執行時間的日誌,不會重複印成兩行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @RestController
    public class HelloController {

    @TimeLog
    @RequestMapping("/")
    public String hello() {
    return "Hello Spring Boot";
    }

    }

  • article-建置 Log4j2

    2021/6/26

    技術
    建置 Log4j2

    Log4j2 是一套執行效能不錯的日誌工具,Lombok 將 Log4j2 日誌工具整合其中,Lombok 也讓 Log4j2 使用起來更簡潔。

    建置 Log4j2

    build.gradle 加入 Lombok dependencies,

    1
    2
    3
    4
    5
    6
    7
    dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok:1.18.18'
    annotationProcessor 'org.projectlombok:lombok:1.18.18'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    }

    參考 Log4j2 的文件中的 Console Appender 建立 log4j2.xml 設定檔,其他用途的日誌設定,可以參考文件的 Appenders 章節;客製化調整日誌樣式,可以參考文件的 Pattern Layout 章節

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration>
    <Appenders>
    <Console name="STDOUT" target="SYSTEM_OUT">
    <PatternLayout pattern="%m%n"/>
    </Console>
    </Appenders>
    <Loggers>
    <Root level="info">
    <AppenderRef ref="STDOUT"/>
    </Root>
    </Loggers>
    </Configuration>

    使用日誌

    在要印出日誌的 class 宣告上標注 @Log4j2,就可以用 log 印出不同等級的日誌。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Log4j2
    @RestController
    public class HelloController {

    @RequestMapping("/")
    public String hello() {
    log.info("hello log4j2");
    return "Hello Spring Boot";
    }

    }