Service Communication(微服務通訊)

是什麼?

微服務之間的通訊方式分為兩大類:

ℹ️選擇原則

需要立即得到結果 → 同步(REST/gRPC)。可以稍後處理、需要解耦 → 非同步(Message Queue)。大多數成熟系統會混合使用兩種方式。

核心觀念

三種通訊方式比較

| 面向 | REST | gRPC | Message Queue | |------|------|------|---------------| | 協定 | HTTP/1.1 + JSON | HTTP/2 + Protobuf | AMQP / Kafka Protocol | | 模式 | 同步 Request-Response | 同步 + Streaming | 非同步 Pub/Sub | | 效能 | 中 | 高(binary + 壓縮) | 取決於 broker | | 瀏覽器支援 | 原生支援 | 需要 gRPC-Web | 不直接支援 | | 適用場景 | 對外 API、CRUD | 內部服務間高效能通訊 | 解耦、削峰、事件通知 | | 型別安全 | 弱(靠文件) | 強(.proto 定義) | 取決於 schema registry |

Message Queue 的核心模式

| 模式 | 說明 | 工具 | |------|------|------| | Point-to-Point | 一個訊息只被一個消費者處理 | RabbitMQ Queue | | Pub/Sub | 一個訊息被所有訂閱者處理 | Kafka Topic、RabbitMQ Exchange | | Competing Consumers | 多個消費者競爭處理同一個 Queue | RabbitMQ + 多 Consumer |

Idempotency(冪等性)

在分散式通訊中,訊息可能因為重試而被處理多次。冪等性確保「處理一次和處理多次的結果相同」。實作方式:在每個訊息中加入唯一的 messageId,消費者用它做去重。

常見誤區

⚠️常犯錯誤

  • 所有通訊都用 REST(內部高頻呼叫應考慮 gRPC)
  • 所有通訊都用 Message Queue(簡單的查詢不需要非同步)
  • 忽略 Message Queue 的訊息丟失和重複問題
  • gRPC 直接暴露給瀏覽器(需要 gRPC-Web 或 API Gateway 轉換)

執行流程

1

識別通訊需求

區分哪些呼叫需要同步回應,哪些可以非同步

2

選擇協定

對外用 REST,內部高效能用 gRPC,解耦用 MQ

3

定義契約

REST 用 OpenAPI,gRPC 用 .proto,MQ 用 Schema Registry

4

實作重試與冪等

加入 retry、circuit breaker、idempotency key

5

監控與追蹤

Distributed Tracing 追蹤跨服務的請求鏈

流程解讀:選擇通訊方式的第一步是理解業務需求。使用者下單需要即時回應用同步,發送通知可以稍後處理用非同步。契約定義是服務間合作的基礎,確保雙方對資料格式有共識。在分散式環境中,網路不可靠是常態,重試機制和冪等性設計是必備的。最後透過 Distributed Tracing 確保跨服務的請求可以被追蹤和除錯。

程式碼範例

C# 版本

csharp
// REST Client(使用 HttpClientFactory)
public class InventoryClient
{
    private readonly HttpClient _client;
 
    public InventoryClient(HttpClient client)
    {
        _client = client;
        _client.BaseAddress = new Uri("http://inventory-service");
    }
 
    public async Task<int> GetStock(string productId)
    {
        var response = await _client.GetAsync($"/api/stock/{productId}");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<int>();
    }
}
 
// gRPC Client
var channel = GrpcChannel.ForAddress("http://inventory-service:5001");
var client = new InventoryService.InventoryServiceClient(channel);
var reply = await client.GetStockAsync(
    new StockRequest { ProductId = "SKU-001" });
 
// Message Queue(RabbitMQ + MassTransit)
public class OrderPlacedConsumer : IConsumer<OrderPlacedEvent>
{
    public async Task Consume(ConsumeContext<OrderPlacedEvent> context)
    {
        var orderId = context.Message.OrderId;
        // 冪等性檢查
        if (await IsAlreadyProcessed(orderId)) return;
        await ReserveInventory(orderId);
        await MarkAsProcessed(orderId);
    }
}

TypeScript 版本

typescript
// REST Client(使用 axios + retry)
import axios from "axios";
import axiosRetry from "axios-retry";
 
const inventoryClient = axios.create({
  baseURL: "http://inventory-service",
  timeout: 3000,
});
axiosRetry(inventoryClient, { retries: 3, retryDelay: axiosRetry.exponentialDelay });
 
async function getStock(productId: string): Promise<number> {
  const { data } = await inventoryClient.get(`/api/stock/${productId}`);
  return data.quantity;
}
 
// Message Queue(RabbitMQ + amqplib)
import amqp from "amqplib";
 
async function publishEvent(event: string, data: object) {
  const conn = await amqp.connect("amqp://rabbitmq");
  const channel = await conn.createChannel();
  await channel.assertExchange("events", "topic", { durable: true });
  channel.publish("events", event, Buffer.from(JSON.stringify(data)), {
    persistent: true,
    messageId: crypto.randomUUID(), // 冪等性 key
  });
}
 
// 消費者
async function consumeEvents() {
  const conn = await amqp.connect("amqp://rabbitmq");
  const channel = await conn.createChannel();
  await channel.assertQueue("inventory-queue", { durable: true });
  await channel.bindQueue("inventory-queue", "events", "order.placed");
 
  channel.consume("inventory-queue", async (msg) => {
    if (!msg) return;
    const event = JSON.parse(msg.content.toString());
    await processOrder(event);
    channel.ack(msg); // 手動確認
  });
}

Python 版本

python
# REST Client(使用 httpx + retry)
import httpx
from tenacity import retry, stop_after_attempt, wait_exponential
 
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1))
async def get_stock(product_id: str) -> int:
    async with httpx.AsyncClient(base_url="http://inventory-service") as client:
        response = await client.get(f"/api/stock/{product_id}", timeout=3.0)
        response.raise_for_status()
        return response.json()["quantity"]
 
# Message Queue(Kafka + aiokafka)
from aiokafka import AIOKafkaProducer, AIOKafkaConsumer
import json
 
# Producer
async def publish_order_event(order_id: str, items: list):
    producer = AIOKafkaProducer(bootstrap_servers="kafka:9092")
    await producer.start()
    await producer.send_and_wait(
        "order-events",
        key=order_id.encode(),
        value=json.dumps({"order_id": order_id, "items": items}).encode()
    )
    await producer.stop()
 
# Consumer
async def consume_orders():
    consumer = AIOKafkaConsumer(
        "order-events",
        bootstrap_servers="kafka:9092",
        group_id="inventory-service"
    )
    await consumer.start()
    async for msg in consumer:
        event = json.loads(msg.value)
        await reserve_inventory(event["order_id"])

結構圖

Client
REST
API Gateway
REST/gRPC
Order Service
gRPC (sync)
Payment Service
Inventory Service
Notification Service
Message Queue

圖中展示了混合通訊模式:Client 透過 REST 存取 API Gateway,Gateway 用 REST 或 gRPC 轉發給 Order Service。Order Service 與 Payment Service 之間用 gRPC 同步通訊(下單需要即時知道付款結果)。對於 Inventory 和 Notification 服務,Order Service 透過 Message Queue 發布事件,讓它們非同步處理。

面試常見問題

Q: REST 和 gRPC 各自的優勢?什麼時候選哪個?

A: REST 的優勢是普及度高、瀏覽器原生支援、生態系完整,適合對外 API。gRPC 的優勢是效能高(Protobuf binary 編碼比 JSON 小 60-80%)、型別安全、支援雙向 streaming,適合內部服務間高頻通訊。常見策略是「對外 REST,對內 gRPC」。

Q: 如何確保 Message Queue 的消息不丟失?

A: 三個環節都要保證:Producer 用 confirm mode 確認 broker 已收到;Broker 開啟 persistence 將訊息寫入磁碟;Consumer 用 manual ack,處理完成後才確認。如果需要更強保證,可以用 Outbox Pattern — 將訊息和業務操作寫入同一個 DB transaction,再由背景程序發送到 MQ。

Q: 什麼是 Idempotency?為什麼在微服務中特別重要?

A: 冪等性是指同一個操作執行一次和多次的結果相同。在微服務中,網路超時可能導致 client 重試,message queue 可能重複投遞。如果操作不是冪等的,就會出現重複扣款、重複建立訂單等問題。實作方式是每個請求帶唯一 ID,server 端去重。

理解測驗

🤔 下單時需要即時確認庫存是否足夠,應該用哪種通訊方式?

🤔 以下哪個不是 Message Queue 的優勢?

🤔 gRPC 相較 REST 的主要效能優勢來自哪裡?

重點整理

💡一句話記住

Service Communication = 同步求即時 + 非同步求解耦。 口訣:「對外 REST,對內 gRPC,解耦用 MQ」

| 概念 | 說明 | |------|------| | REST | HTTP + JSON,對外 API 首選 | | gRPC | HTTP/2 + Protobuf,內部高效能通訊 | | Message Queue | 非同步解耦,削峰填谷 | | Idempotency | 確保重複處理不會造成副作用 | | 核心原則 | 混合使用,根據業務需求選擇通訊方式 |

你可能也想看

Microservices FundamentalsAPI Gateway

按 ← → 鍵切換課程