第二大腦

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

  • 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";
    }

    }

  • article-202102 演化式設計:測試驅動開發與持續重構 -- 心得

    2021/4/17

    日誌
    202102 演化式設計:測試驅動開發與持續重構 -- 心得

    上完演化式設計:測試驅動開發與持續重構,課後兩個月補記心得和筆記。

    專業有價

    早在 2018 左右就聽說 91 的課,當時看到上課時間居然在半年後,而且一堂課要價超過一萬元,就一直處於觀望狀態而沒有報名課程。幸好在這期間我看了《刻意練習》後,就一直心心念念想找到利用「刻意練習」模式教學的課程,再加上我開始認同「專業有價」,花錢就能買到別人累積的功力,超級划算。於是,在剩下最後幾個名額時,趕緊手刀報名,心裡想不管發生什麼事,都不能妨礙我去上課,要是再不做出行動,現在這門課是開課前一年就額滿 ⋯⋯

    從了解需求開始

    • 由實作的人去問出需求
    • 用具體的例子談需求,甚至用錯誤的例子誘導對方講出正確的需求

    說到討論需求,近日有新的感想,我終於可以察覺出「問出需求」和「擬定做法」是兩回事。

    以前很多時候,我只是在覆核 PM 提出的調整可不可行,而沒有去探究背後的需求,現在可以練習再多問『想做出調整的原因?』和問『什麼會想用這個方式調整?』,這樣才能有足夠的資訊選擇最適當的做法。另外,我發現我很習慣用簡短或沒整理過的描述和其他人討論事情,用具體的例子和其他人溝通是我還需要多練習的課題。

    便利貼起手式

    • 拆解會需要的程式碼片段,可使用便利貼或 Mind Map 工具
    • 和團隊成員一起決定命名或 API 規格

    以前我都會一邊設計程式一邊敲鍵盤,因為覺得與其花時間在腦袋一直空想,還不如直接就開始寫程式,邊理解需求邊寫程式還邊重構,自以為我真是會敏捷開發,但是常常發生 A 設計寫到一半覺得不夠好,應該要改成 B 設計,B 設計寫到一半發現有情境沒考慮到,所以一開始才會寫成 A 設計那樣,改過來又改回去,搞到最後沒時間好好梳理程式架構和流程。

    所以二月週末上完課,週一進辦公室,我就乖乖到文具櫃去拿一本便利貼來用,仔細拆解會需要的程式碼片段,再開始敲程式碼,寫程式有明顯變得比較順,思路也比較周全,反而花比較少時間開發,對於 Task 的完成度更有信心。

    安排測試案例,小步重構

    • 測試案例必須是使用情境,命名要描述 Domain,不能出現具體的例子名稱
    • 測試案例從最簡單的案例開始寫,如果要改太多就拆小一點

      最簡單的意思是:加最少的程式碼片段,或複製測試案例改最少的程式碼片段。

    • TDD 只需要列出可以把需要的程式碼片段開發完成即可,不用列出所有案例

    我之前以為用排列組合列出所有測試案例,就是最系統化的方式,完全沒想過測試案例的順序是要刻意安排的,所以一次不會改動太多程式,比較沒有除錯壓力,搭配便利貼起手式一起服用更有效。先做好全局的思考,這時候再開始用 TDD 的方式寫程式:

    1. Baby Step:一次做少一點,然後該做的有做完(一個完整的使用情境)
    2. 紅燈 → 綠燈:無腦通過測試案例
    3. 綠燈 → 重構:從刻意產生重複的程式碼演進成 Production Code 設計

    重構 Legacy Code

    Legacy Code 是實務上最常會遇到的情況,通常 Legacy Code 沒有單元測試保護,所以就沒人敢改。
    利用單元測試和強大的 IDE 工具自動產生程式碼做重構,安全地跳脫惡性循環:

    1. 整理代碼:刪除 comment、調整爛命名、多餘空白行、Temp Variables
    2. 整理流程:消除重複、凸顯意圖

      不要太早抽方法,會不容易看出重複,可用 Inline Method 回復 Method。

    3. 改邏輯要有對應的測試保護
    4. 分職責
      • 物件導向(高內聚):用物件模型模擬真實世界,把資料往物件堆,叫物件做事
      • Design Pattern(低耦合)

    原則上設計心法就是用最簡單的方式滿足使用者情境,並且寫最少的防呆,參數越少越好用。

    Keep it Simple, don’t overdesign your solution.

    結論

    [演化式設計:測試驅動開發與持續重構] 是 [針對遺留代碼加入單元測試的藝術] 和 [極速開發] 兩門課的集大成,上完課後仍需要不斷練習和反思,才能持續進步。原本課前預習就應該要先了解 Code SmellRefactoring 相關知識,但在上課前我沒有確實做到,現在要補下基本功,也藉此督促自己要熟練課堂上的重構練習。