Work

microservices

Microservices
Spring Boot
API Gateway
Spring Cloud
Image 3

Spring Boot 微服務的合理範圍

如果 peoplesystem.tatdvsonorth.com/tymultiverse 這類 HR 系統是以 Spring Boot 打造的微服務架構,我們通常會預期至少三個(甚至更多)獨立的 Spring Boot 應用程式。這是因為微服務的核心精神,就是依據業務職責拆分服務,讓每個服務獨立部署與擴充。

基於業務的最低 Spring Boot 服務

以下三種服務,是以 HR 系統視角最常見的拆分方式:

1. 認證與授權服務(Auth Service)

  • 職責:處理登入、權杖(Token)驗證與用戶權限查詢。
  • 技術重點:高度聚焦於安全性,常搭配 Spring Security、OAuth2、JWT。
  • 獨立理由:安全性是所有服務的共同依賴,因此必須獨立運作、獨立更新。

2. 員工資料與組織服務(Employee/Org Service)

  • 職責:維護員工基本資料、部門結構、職位定義等靜態業務資訊。
  • 技術重點:以大量讀取為主(Read-Heavy),提供單一真實來源(SSOT)。
  • 獨立理由:作為整個系統的基礎資料池,拆分後能確保資料一致性與可用性。

3. 假勤管理服務(Leave/Attendance Service)

  • 職責:處理請假申請、審批流程、出勤記錄等動態業務邏輯。
  • 技術重點:常涉及狀態機與流程引擎,可能整合 Kafka、RabbitMQ 等訊息佇列。
  • 獨立理由:工作負載在月初、月末或節日前後波動大,適合單獨水平擴展。

常見的微服務基礎設施

在實務上,完整的 Spring Cloud 技術棧往往還會納入以下服務,使整體服務數量落在五到七個之間:

  • API 閘道器(API Gateway):以 Spring Cloud Gateway 或 Zuul 為門面,負責路由、限流、基礎安全。前端只需呼叫閘道器的單一網域。
  • 配置服務(Config Service):使用 Spring Cloud Config 集中管理所有服務的設定檔。
  • 服務註冊與發現(Discovery Service):多採用 Eureka 或 Consul,讓服務之間能自動尋址。

REST、OpenAPI 與 gRPC 的協定視角

REST 堅守「資源導向」原則,要求客戶端透過 HTTP 動詞與超連結來操作資源;OpenAPI 延續 RPC 思維但依附在 HTTP URL 結構上,因此客戶端仍需處理實際的路徑與動詞。gRPC 同樣源自 RPC 範式,卻拋開了 HTTP 動詞的限制,在 HTTP/2 之上構建自己的高效傳輸協定,讓呼叫端幾乎感受不到網路存在,只需像呼叫本地方法一樣操作遠端服務。

呼叫行為層面差異(直覺理解)

行為RESTgRPC
請求模式建立 HTTP Request呼叫遠端方法
接收資料解析 JSON String解碼二進制物件
調用方式明確 URL 與動詞介面由 proto 定義,直接呼叫方法
節點間溝通延遲高(HTTP/1.1 + JSON parse)低(HTTP/2 + binary)
Streaming要 WebSocket 實作原生支援
向下相容性靠 API version 管理proto 內建欄位 tag,可平滑演進

非阻塞行為差異

REST 的非阻塞行為

🔹 傳統 REST(Spring MVC / Express)

  • 同步阻塞:每個 HTTP request 都佔用一個線程,直到 response 回來才釋放。

🔹 非阻塞選項:

  • Java:Spring WebFlux(使用 Netty) → Mono / Flux 流式非阻塞。
  • Node.js:本身就是事件循環(event loop),I/O 非阻塞。

特性:

  • 基於 HTTP/1.1,單請求單 TCP 連線(除非用 Keep-Alive)。
  • 非阻塞模式通常依賴框架(Netty, Vert.x, WebFlux)。
  • JSON 序列化與反序列化仍需 CPU 處理,可能成為瓶頸。

gRPC 的非阻塞行為

🔹 gRPC 原生支援非阻塞

  • gRPC 基於 HTTP/2 + Protobuf,內部使用 Netty(Java)或類似事件循環模型。
  • 客戶端可選擇:同步 stub、非同步 stub、Streaming stub。

🔹 Java gRPC 非阻塞示例

OrderServiceGrpc.OrderServiceStub stub = OrderServiceGrpc.newStub(channel);

stub.createOrder(request, new StreamObserver<OrderResponse>() {
    @Override
    public void onNext(OrderResponse value) {
        System.out.println("Order created: " + value.getId());
    }
    @Override
    public void onError(Throwable t) {}
    @Override
    public void onCompleted() {}
});

REST 與 gRPC 在非阻塞上的核心差異

特性REST 非阻塞gRPC 非阻塞
基礎協定HTTP/1.1(需要 WebFlux/Netty 才非阻塞)HTTP/2 + Protobuf(原生支援)
序列化成本JSON(字串解析需要 CPU)Protobuf(二進制,解析快)
多路複用HTTP/1.1 單請求單線程,HTTP/2 可複用連線但框架有限HTTP/2 多路複用原生支援 → 單連線可同時傳多個 request
Streaming 支援需 WebSocket 或 SSE 實作原生支援單向/雙向 stream
客戶端 APIcallback / Future / reactive framework(需要額外框架)async stub / StreamObserver(原生非阻塞)
效能較 REST 傳統阻塞高延遲低延遲、高吞吐、CPU 解析快

Reactive 框架對比

WebFlux (Java) vs ASGI (Python)

特性WebFlux + Mono (Java)ASGI (Python)
非阻塞方式Reactor / Netty event loopasyncio event loop
對外協定HTTP REST, StreamingHTTP REST, WebSocket, Streaming
資料封裝Mono/Flux 封裝資料流async / await coroutine 封裝資料流
Reactive 標準✅ Reactive Streams 規範❌ 不算完整 reactive,只提供 async I/O
背壓支援✅ 原生支援⚠️ 需手動實作 / 框架不保證
使用場景高併發 REST API, Streaming高併發 REST API, WebSocket, async DB/Kafka

gRPC vs ASGI 協定支援

協定ASGI 支援情況
REST / HTTP✅ 直接支援。使用 FastAPI / Starlette / Quart 都可以實作 REST API。
WebSocket / Streaming✅ 原生支援。
gRPC / HTTP/2 + Protobuf⚠️ 不直接支援。需要 grpc-gateway / grpc-web 將 gRPC 轉成 HTTP/JSON 後,再由 ASGI 處理。

速度差異來源分析

gRPC 比傳統 REST 快的核心原因:

1. 資料序列化/傳輸方式

  • REST: JSON(文字)→ 封包大、字串解析耗 CPU
  • gRPC: Protobuf(二進制)→ 封包小、編碼解碼快

2. 連線協定差異

  • REST: HTTP/1.1(短連線,每次 request 都要握手,除非 keep-alive)
  • gRPC: HTTP/2(單連線多路複用,同一連線可同時傳多個 request/response)

3. Streaming 與非阻塞

  • REST: 傳統 HTTP request/response → 需 WebSocket 或 SSE 實作 streaming
  • gRPC: 原生支援 Server/Client/Bi-directional streaming + 非阻塞 async stub

4. 介面契約差異

  • REST: 由程式碼定義 route → JSON schema 可選,易出錯
  • gRPC: .proto 明確定義 service/message,減少錯誤檢查成本

設計選擇建議

使用情境建議
對外 API(給前端、行動裝置)REST / GraphQL(可閱讀、除錯方便)
微服務之間高頻交互(如訂單、報價)gRPC(低延遲、高效能)
事件驅動架構Kafka / MQ(異步、解耦)
要支援流式傳輸(即時報價、影像)gRPC streaming
團隊要快速上手 / DevTools 完善REST(學習成本低)

為什麼前端只需要一個網域

直接讓前端打多個網域(例如 auth.backend.comemployee.backend.com)會遇到 CORS 管理負擔與資安風險。API 閘道器提供單一入口 (https://api.peoplesystem.com),再依路徑將請求轉送給對應的後端服務,實現「前端只打一個 Domain」的最佳實務:

前端請求路徑閘道器轉送目標
POST /api/auth/login認證服務
GET /api/employees/123員工資料服務
POST /api/leave/request假勤管理服務

同時,閘道器可以集中處理 SSL/TLS 憑證與驗證機制。對於 HTTPS 來說,我們常見兩種部署模式:

模式憑證部署位置優點
終止於閘道器憑證僅部署在 API 閘道器或負載平衡器最常見、維護成本低;內部流量可視需求選擇 HTTP 或內網憑證
端到端加密憑證部署於閘道器與各後端服務安全性最高,但管理成本大幅增加

登入後的身分憑證通常使用 JWT。流程如下:

  1. 使用者登入時,Auth Service 發行 JWT。
  2. 前端將 JWT 放在每次請求的 Authorization header。
  3. API 閘道器攔截請求,驗證 JWT(可同步呼叫 Auth Service)。
  4. 後端服務解析 JWT,確認發出請求的身分與權限。

API 閘道器與 HTTP 202 的適用時機

雖然閘道器可以改寫狀態碼,但不建議把所有回應都改成 202 Accepted。HTTP 202 表示「請求已接受,但尚未完成」,適合用於長時間的非同步作業,例如產生大型報表,並搭配查詢進度的端點。一般同步請求應維持原本的 200 OK201 Created4xx/5xx 以符合 RESTful 慣例,避免誤導客戶端。

API 閘道器為什麼不該直連資料庫

閘道器的責任是流量管理與安全控制,而非資料存取。若閘道器直接連 DB,會帶來以下風險:

  • 違反單一職責原則:閘道器不應混入業務邏輯或資料讀寫。
  • 安全性隱患:閘道器位於邊界層,一旦被攻破就等同取得核心資料庫存取權。
  • 耦合度過高:資料庫 schema 變動會迫使閘道器同步調整,破壞微服務自治。
  • 資源競爭:閘道器佔用 DB 連線,影響後端服務的處理能力。

正確的分層是:Client → API Gateway → Microservice(業務邏輯 + Repository)→ Database

閘道器如何回傳正確的狀態碼

閘道器不需直接存取資料庫,也能回傳正確的 HTTP 狀態碼,因為狀態碼來源分為兩類:

  • 後端服務回傳:閘道器收到後端服務的完整 Response(含狀態碼、Header、Body)後,透明轉發給客戶端。業務錯誤或成功(200、201、400、404、500)都由服務決定。
  • 閘道器內部決定:只有在請求未送達後端時,閘道器才自行回應,例如 401(驗證失敗)、429(限流)、503(服務無法連線)、或找不到路由時使用 404。

透過這種責任切分,架構更安全、維護性更高,也符合微服務的分層設計理念。

Protocol Buffers 與程式碼生成

Protocol Buffers(Protobuf)是一種介面描述語言(IDL),用來跨語言定義資料結構與 RPC 服務合約。透過 protoc 編譯器,這些宣告會轉換成目標語言的程式碼:

  • 資料類別:採不可變物件與 Builder 模式,內建二進位序列化/反序列化邏輯,確保跨語言資料一致性。
  • 服務 Stub:封裝 gRPC 在 HTTP/2 上的連線管理、錯誤處理與超時控制,讓呼叫端只需執行看似本地的方法即可觸發遠端服務。

這種自動生成方式,把最容易出錯的網路細節抽象掉,開發者可以專注在業務邏輯與測試。

Proto 文件的雙重角色:Client 與 Server 各取所需

同一個 .proto 文件,在編譯後會為客戶端與伺服器端生成不同用途的程式碼。這種「單一定義、雙向生成」的機制,確保了介面契約的一致性:

角色 (Who)讀取 Proto 的目的 (Why)產生的程式碼 (What)
客戶端 (Client)知道如何呼叫遠程服務Stub(存根):這是客戶端呼叫遠程方法的「代理物件」。它知道如何打包請求數據(序列化)並發送到伺服器。
伺服器端 (Server)知道如何實作遠程服務Service Base Class(服務基類):這是伺服器必須繼承並填寫業務邏輯的抽象介面。它知道如何解包請求數據(反序列化)並將回應封裝。

實際範例:同一個 Proto,兩種用途

假設我們有以下 people_service.proto

service PeopleService {
  rpc GetAllPeople(GetAllPeopleRequest) returns (GetAllPeopleResponse);
}

編譯後產生:

客戶端(Gateway)使用:
├─ PeopleServiceGrpc.PeopleServiceBlockingStub
│  └─ getAllPeople(request) → 發送請求到 Backend

伺服器端(Backend)實作:
└─ PeopleServiceGrpc.PeopleServiceImplBase
   └─ 必須 override getAllPeople() 方法,填入業務邏輯

這就是為什麼:

  • Gateway 需要 proto:生成 Stub(Client 代碼),用來呼叫 Backend
  • Backend 需要 proto:生成 ImplBase(Server 基類),用來實作服務邏輯

兩者雖然使用同一個 proto 文件,但編譯後的角色完全不同,互不干擾。

gRPC 的通訊模式:流式呼叫(Streaming-capable RPC)

gRPC 不是用動詞定義操作,而是用 方法類型(RPC type) 定義資料流方向。 共有 四種呼叫模式

模式說明與 REST 對應資料流方向
① Unary單次請求、單次回應類似 POST / GET➡️ Client → Server → Client
② Server streamingClient 傳一次請求,Server 回多次(stream)REST 無法原生做到(需 SSE/WebSocket 模擬)➡️➡️➡️ Server 多次回傳
③ Client streamingClient 傳多次,Server 收完一次回應REST 無法模擬(需多次 POST 拼接)⬅️⬅️⬅️ Client 多次傳送
④ Bi-directional streaming兩邊可同時連續傳資料(雙向流)REST 完全不支援🔁 雙向即時流

舉例比較(直覺版)

🔹 REST(一次性請求)

Client: POST /order { item: "apple", qty: 1 }
Server: {"orderId": 123, "status": "OK"}

→ 每次都要重發 HTTP 請求、重新建立 request/response。

🔹 gRPC Unary(單次呼叫)

rpc CreateOrder(OrderRequest) returns (OrderResponse);
Client ────▶ Server ────▶ 回傳一次結果

✅ 一模一樣效果,但封包是 Protobuf,效率高。

🔹 gRPC Server Streaming(伺服器端串流)

用於持續回報狀態 / 即時結果推播。

rpc WatchOrderStatus(OrderId) returns (stream OrderStatus);
Client ───▶ Server
           └─▶ status: "queued"
           └─▶ status: "packing"
           └─▶ status: "shipped"
           └─▶ status: "delivered"

→ 類似 WebSocket / SSE,但 HTTP/2 原生支援,效率更高。

🔹 gRPC Client Streaming(客戶端串流)

用於上傳多筆資料 / 感測器資料流 / 合併傳輸。

rpc UploadTelemetry(stream SensorData) returns (SummaryResponse);
Client ─▶ data1
Client ─▶ data2
Client ─▶ data3
Server ◀─── 最後回一個結果(平均值、統計等)

🔹 gRPC Bi-directional Streaming(雙向流)

用於聊天、即時遊戲、交易回報、AI 對話管線等。

rpc Chat(stream ChatMessage) returns (stream ChatMessage);
Client ⇄ Server ⇄ 雙向即時通訊(無需多次連線)

REST 與 gRPC 模式差異總表

項目RESTgRPC
協定HTTP/1.1HTTP/2
呼叫模型Request/Response(一次)4 種 RPC 模式(含 streaming)
傳輸格式JSON(文字)Protobuf(二進制)
雙向通訊❌ 不支援(需 WebSocket)✅ 原生支援(HTTP/2 stream)
長連線限制多單連線多路複用
即時推播❌ 需外掛✅ 內建
適合場景外部 API微服務內部通訊 / 即時服務

Java gRPC 範例

以下示範如何定義 GreetingService 的 Protobuf 介面,並分別實作 gRPC 伺服器與客戶端。伺服器端提供 Greet 方法,客戶端呼叫後即可取得問候訊息。

// greeting.proto
syntax = "proto3";
option java_package = "com.example.grpc.greeting";
option java_multiple_files = true;

service GreetingService {
  rpc Greet (GreetingRequest) returns (GreetingResponse);
}

message GreetingRequest {
  string name = 1;
}

message GreetingResponse {
  string message = 1;
}
// GreetingServer.java
import com.example.grpc.greeting.GreetingRequest;
import com.example.grpc.greeting.GreetingResponse;
import com.example.grpc.greeting.GreetingServiceGrpc;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;

public class GreetingServer extends GreetingServiceGrpc.GreetingServiceImplBase {

    @Override
    public void greet(GreetingRequest request, StreamObserver<GreetingResponse> responseObserver) {
        GreetingResponse response = GreetingResponse.newBuilder()
                .setMessage("Hello, " + request.getName() + "! This is a gRPC response.")
                .build();
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }

    public static void main(String[] args) throws Exception {
        Server server = ServerBuilder.forPort(50051)
                .addService(new GreetingServer())
                .build()
                .start();
        System.out.println("gRPC Server started on port 50051");
        server.awaitTermination();
    }
}
// GreetingClient.java
import com.example.grpc.greeting.GreetingRequest;
import com.example.grpc.greeting.GreetingResponse;
import com.example.grpc.greeting.GreetingServiceGrpc;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

public class GreetingClient {

    public static void main(String[] args) {
        ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
                .usePlaintext()
                .build();

        GreetingServiceGrpc.GreetingServiceBlockingStub stub =
                GreetingServiceGrpc.newBlockingStub(channel);

        GreetingResponse response = stub.greet(GreetingRequest.newBuilder()
                .setName("Charlie")
                .build());

        System.out.println(response.getMessage());
        channel.shutdown();
    }
}

REST 與 gRPC 的協同架構

在真實的部署中,瀏覽器或行動裝置仍以 REST/JSON 呼叫 API 閘道器;閘道器接著改用 gRPC 與內部服務互動,再將結果轉為 REST 回應。非同步流程(例如 Producer 將訊息推送至 MQ、由 Consumer 消費)維持現有拓撲,但可以逐步將 Payload 由 JSON 替換成 Protobuf 以提升效能。

漸進式調整的建議順序如下:

  1. 導入 API 閘道器:對外暴露 REST 端點,處理身份驗證、限流與路由。
  2. Producer 改用 gRPC:保留業務邏輯,改寫為 gRPC 服務實作,移除對外 REST Controller。
  3. MQ 保持既有流程:必要時在 Producer/Consumer 中導入 Protobuf 序列化,但保留既有的訊息佇列與重試機制。

如此一來,只有「前端 → API 閘道器」保留 REST,其餘內部同步呼叫皆可升級為 gRPC,兼顧效益、穩定性與安全性。

TY的智慧庫

你有事?
問前想清楚,機會不是誰都有。

💡 建議主題:

放大圖片