返回文章列表

微服務重構實踐與領域驅動設計

本文以 MADE.com 的案例,講解如何從單體架構遷移至微服務架構,並探討領域驅動設計和事件驅動架構的實踐挑戰。文章涵蓋了識別系統瓶頸、引入事件驅動架構、CQRS 模式實作、領域建模技巧,以及使用 Docker Compose 進行容器化組態和管理等內容。同時,也分享了技術審閱者 David Seddon

系統設計 Web 開發

從單體架構轉型到微服務架構是許多電商平台的必經之路,本文以 MADE.com 為例,闡述瞭如何透過事件驅動架構解決系統瓶頸,並提升系統效能和可維護性。文章首先分析了 MADE.com 原有的單體架構及其面臨的效能問題,接著介紹瞭如何透過識別瓶頸,引入事件驅動架構和 CQRS 模式來解決庫存同步緩慢的問題。同時,文章也強調了領域建模的重要性,並以使用者模型的重構為例,說明如何透過領域建模來解決業務和技術的溝通問題。最後,文章還介紹了 Docker Compose 和環境變陣列態等現代軟體開發技術,展示如何在實踐中結合領域驅動設計和事件驅動架構,構建高可擴充套件性和高可靠性的系統。

從單體架構到微服務:以MADE.com為例的系統重構實踐

前言

MADE.com最初由兩個單體架構組成:一個用於前端電子商務應用,另一個用於後端履行系統。這兩個系統透過XML-RPC進行通訊。後端系統會定期喚醒並查詢前端系統以取得新訂單資訊。當它匯入所有新訂單後,會傳送RPC命令來更新庫存水平。

問題的根源

隨著時間的推移,這種同步過程變得越來越慢,直到有一年聖誕節,匯入一天的訂單竟然花了超過24小時。為瞭解決這個問題,Bob被聘請將系統拆分成一組事件驅動的服務。

識別瓶頸

首先,我們發現了計算和同步可用庫存是最慢的部分。我們需要一個能夠監聽外部事件並保持可用庫存總量的系統。我們透過API暴露了這些資訊,以便使用者的瀏覽器可以查詢每個產品的可用庫存以及送貨到他們地址所需的時間。

事件驅動架構的引入

每當產品完全缺貨時,我們會引發一個新的事件,電子商務平台可以使用該事件將產品下架。由於我們不知道需要處理多少負載,因此我們使用CQRS模式編寫了系統。每當庫存數量發生變化時,我們都會使用快取檢視模型更新Redis資料函式庫。我們的Flask API查詢這些檢視模型,而不是執行複雜的領域模型。

程式碼範例:CQRS模式實作

from dataclasses import dataclass
from typing import List
import redis

# 定義事件類別
@dataclass
class StockChangedEvent:
    product_id: int
    new_stock_level: int

# CQRS模式中的命令處理器
class StockCommandHandler:
    def __init__(self, redis_client: redis.Redis):
        self.redis_client = redis_client

    def handle(self, event: StockChangedEvent):
        # 更新Redis中的庫存檢視模型
        self.redis_client.hset("stock_levels", event.product_id, event.new_stock_level)

# 初始化Redis客戶端和命令處理器
redis_client = redis.Redis(host='localhost', port=6379, db=0)
command_handler = StockCommandHandler(redis_client)

# 處理庫存變更事件
event = StockChangedEvent(product_id=1, new_stock_level=10)
command_handler.handle(event)

內容解密:

  1. 事件類別定義StockChangedEvent類別代表庫存變更事件,包含產品ID和新的庫存水平。
  2. 命令處理器實作StockCommandHandler類別負責處理庫存變更事件,並更新Redis中的庫存檢視模型。
  3. Redis客戶端初始化:初始化Redis客戶端,以便與Redis資料函式庫進行互動。
  4. 事件處理:建立一個庫存變更事件例項,並透過命令處理器進行處理,更新Redis中的庫存水平。

成果與效益

結果,我們可以在2到3毫秒內回答“有多少庫存可用?”的問題,現在API經常在一秒鐘內處理數百個請求並持續數秒。

說服利益相關者嘗試新方法

如果你正在考慮從一個大泥球中剝離出一個新系統,你可能正在遭受可靠性、效能、可維護性或同時三者的問題。深層、棘手的問題需要採取激進的措施!

領域建模的重要性

我們建議將領域建模作為第一步。在許多過度成長的系統中,工程師、產品負責人和客戶不再使用相同的語言。透過領域建模,我們可以使用相同的通用語言來討論系統,從而達成對複雜性的共識。

使用者模型的案例研究

在我們的初始系統中,帳戶和使用者模型被一個“奇怪的規則”繫結在一起。這是工程和業務利益相關者之間產生分歧的典型例子。透過重寫整個使用者管理功能作為一個全新的系統,我們解決了多年來積累的技術債。

領域建模技巧

我們喜歡使用互動式技術,如事件風暴和CRC建模,因為人類擅長透過遊戲進行協作。事件建模是另一種技術,它使工程師和產品負責人能夠根據命令、查詢和事件來理解系統。

實踐 Domain-Driven Design:David Seddon 的經驗分享

從理論到實踐的轉變

David Seddon,一位技術審閱者,在閱讀了本文中介紹的模式後,感到非常興奮。因為他已經在較小的專案中使用過其中的一些技術,但現在他有了一個藍圖,可以將這些技術應用於更大型的、資料函式庫驅動的系統。然而,當他開始在自己的組織中實施這些模式時,卻遇到了意想不到的問題。

逐步實踐的重要性

David 最初試圖將一個複雜的問題領域建模為一個使用案例,但他很快發現自己面臨著許多未曾考慮到的問題。這些問題包括:使用案例是否可以與多個聚合根互動?一個使用案例是否可以呼叫另一個使用案例?以及如何在遵循不同架構原則的系統中共存而不產生混亂?

David 發現,他可以從小處著手,不必追求完美,可以進行實驗,找出適合自己的方法。他成功地在幾個地方應用了書中的一些想法,並建立了新的功能,使得業務邏輯可以在沒有資料函式庫或模擬的情況下進行測試。

給讀者的建議

David 建議讀者在嘗試應用這些模式時,首先關注特定的問題,並思考如何以有限和不完美的方式應用相關的想法。不要試圖一次改變太多,也不要害怕犯錯。這將是一個學習的過程,你可以放心,你正朝著別人已經發現有用的方向前進。

技術審閱者提出的問題

在編寫本文的過程中,技術審閱者提出了許多問題,以下是其中一些問題的解答:

是否需要一次性完成所有更改?

不需要,可以逐步採用這些技術。如果現有的系統已經存在,建議建立一個服務層,以保持協調在一個地方。一旦有了服務層,就更容易將邏輯推入模型,並將邊緣問題(如驗證或錯誤處理)推到入口點。

提取使用案例是否會破壞現有的程式碼?

是的,但這沒關係。可以先複製和貼上現有的程式碼到新的地方,然後清理新的程式碼。之後,可以用新的程式碼替換舊的程式碼,最後刪除舊的程式碼。修復大型程式碼函式庫是一個混亂和痛苦的過程,不要期望事情會立即變好。

是否需要使用 CQRS?

不需要,可以使用儲存函式庫。這些技術旨在使生活變得更容易,而不是某種苦行僧式的紀律。在第一個案例研究系統中,使用了許多 View Builder 物件,它們使用儲存函式庫來取得資料,然後執行一些轉換以傳回簡單的讀取模型。

使用案例如何在更大的系統中互動?

這可能是一個過渡步驟。在第一個案例研究中,有一些處理程式需要呼叫其他處理程式,但這會變得很混亂。更好的方法是使用訊息匯流排來分離這些關注點。

使用多個儲存函式庫/聚合根是否是一種程式碼異味?

如果使用案例需要原子地更新兩個聚合根,那麼嚴格來說,這是聚合根設計的問題。理想情況下,應該考慮移動到一個新的聚合根,它包裝了所有需要同時更改的事物。如果只是為了讀取其他聚合根,那麼這是可以的,但也可以考慮建立一個讀取/檢視模型。

技術深度探討:事件驅動架構與領域驅動設計的實踐挑戰

在探討現代軟體架構時,事件驅動架構(Event-Driven Architecture, EDA)與領域驅動設計(Domain-Driven Design, DDD)已成為備受關注的技術趨勢。本篇文章將深入分析這兩種技術的結合實踐,並探討其在實際應用中面臨的挑戰及解決方案。

事件驅動架構的核心概念

事件驅動架構是一種透過事件來觸發系統行為的設計模式。這種架構允許系統元件之間透過發布和訂閱事件來進行非同步通訊,從而提高系統的可擴充套件性和彈性。

事件驅動架構的優點

  1. 鬆耦合:系統元件之間透過事件進行通訊,降低了直接依賴關係。
  2. 可擴充套件性:便於新增處理事件的元件,而無需修改現有程式碼。
  3. 非同步處理:提高系統的回應速度和吞吐量。

領域驅動設計的核心概念

領域驅動設計是一種軟體開發方法論,強調以業務領域為中心進行系統設計。它透過定義領域模型、聚合根、值物件等概念來確保業務邏輯的正確性和一致性。

領域驅動設計的關鍵元素

  1. 領域模型:反映業務領域核心概念和規則的軟體模型。
  2. 聚合根:定義了一致性邊界,確保領域模型的一致性。
  3. 領域事件:表示領域中發生的重要事件,用於觸發後續業務流程。

事件驅動架構與領域驅動設計的結合

將事件驅動架構與領域驅動設計結合,可以充分發揮兩者的優勢,構建出既具備業務邏輯正確性,又具有高度可擴充套件性的系統。

結合實踐中的挑戰

  1. 事件版本管理:隨著業務需求的變化,事件結構可能需要調整,如何管理不同版本的事件是一個挑戰。

    • 解決方案:使用JSON Schema等工具來定義和版本化事件結構。
  2. 事件處理的冪等性:確保重複處理同一事件不會導致錯誤或不一致的狀態。

    • 解決方案:設計事件處理器時考慮冪等性,例如透過記錄已處理事件的ID來避免重複處理。
  3. 可靠的訊息傳遞:確保事件能夠可靠地傳遞到所有相關的處理器。

    • 解決方案:選擇可靠的訊息代理(如Event Store、RabbitMQ),並實作重試機制和死信佇列處理。

程式碼實踐範例

以下是一個簡單的事件發布和處理範例,使用Python實作:

from dataclasses import dataclass
from typing import List
import json

#### 事件類別定義
@dataclass
class OrderCreatedEvent:
    order_id: str
    customer_id: str

#### 事件處理器介面
class EventHandler:
    def handle(self, event):
        raise NotImplementedError

#### 具體事件處理器實作
class OrderCreatedEventHandler(EventHandler):
    def handle(self, event: OrderCreatedEvent):
        print(f"Handling OrderCreatedEvent: {event.order_id}")
        # 在此實作具體的業務邏輯

#### 事件匯流排實作
class EventBus:
    def __init__(self):
        self.handlers = {}

    def register(self, event_type, handler):
        if event_type not in self.handlers:
            self.handlers[event_type] = []
        self.handlers[event_type].append(handler)

    def publish(self, event):
        event_type = type(event)
        if event_type in self.handlers:
            for handler in self.handlers[event_type]:
                handler.handle(event)

# 初始化事件匯流排並註冊事件處理器
event_bus = EventBus()
event_bus.register(OrderCreatedEvent, OrderCreatedEventHandler())

# 發布事件
event = OrderCreatedEvent(order_id="123", customer_id="456")
event_bus.publish(event)

內容解密:

  1. 事件類別定義:使用dataclass定義了OrderCreatedEvent,包含了訂單ID和客戶ID等資訊。
  2. 事件處理器介面:定義了EventHandler介面,要求所有事件處理器實作handle方法。
  3. 具體事件處理器實作OrderCreatedEventHandler是針對OrderCreatedEvent的具體處理器,實作了業務邏輯。
  4. 事件匯流排實作EventBus負責管理事件處理器的註冊和事件的發布。它維護了一個事件型別到處理器列表的對映,確保每個事件被正確地派發到對應的處理器。

現代軟體開發中的容器化組態與管理

在現代軟體開發中,容器化技術已成為提升開發效率、確保環境一致性以及簡化佈署流程的重要工具。本文將探討如何利用 Docker Compose 進行容器協調,以及如何在 Python 專案中實作根據環境變數的組態管理。

專案結構與容器組態

一個典型的 Python 專案結構如下所示:

src/
├── allocation
│   ├── adapters
│   │   ├── __init__.py
│   │   └── model.py
│   ├── entrypoints
│   │   ├── __init__.py
│   │   └── flask_app.py
│   └── service_layer
│       ├── __init__.py
│       └── services.py
└── setup.py
tests/
├── conftest.py
├── e2e
│   └── test_api.py
├── integration
│   ├── test_orm.py
│   └── test_repository.py
├── pytest.ini
└── unit
    ├── test_allocate.py
    ├── test_batches.py
    └── test_services.py

Docker Compose 組態檔案

Docker Compose 是管理多容器 Docker 應用程式的強大工具。以下是一個典型的 docker-compose.yml 組態檔案範例:

version: "3"
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    depends_on:
      - postgres
    environment:
      - DB_HOST=postgres
      - DB_PASSWORD=abc123
      - API_HOST=app
      - PYTHONDONTWRITEBYTECODE=1
    volumes:
      - ./src:/src
      - ./tests:/tests
    ports:
      - "5005:80"
  postgres:
    image: postgres:9.6
    environment:
      - POSTGRES_USER=allocation
      - POSTGRES_PASSWORD=abc123
    ports:
      - "54321:5432"

此組態檔案定義了兩個服務:apppostgresapp 服務根據當前目錄下的 Dockerfile 構建,而 postgres 服務則直接使用官方的 PostgreSQL 映象。

根據環境變數的組態管理

為了在不同環境(開發、測試、生產)中靈活組態應用程式,我們採用根據環境變數的組態策略。以下是一個 config.py 範例:

import os

def get_postgres_uri():
    host = os.environ.get('DB_HOST', 'localhost')
    port = 54321 if host == 'localhost' else 5432
    password = os.environ.get('DB_PASSWORD', 'abc123')
    user, db_name = 'allocation', 'allocation'
    return f"postgresql://{user}:{password}@{host}:{port}/{db_name}"

def get_api_url():
    host = os.environ.get('API_HOST', 'localhost')
    port = 5005 if host == 'localhost' else 80
    return f"http://{host}:{port}"

這些函式根據環境變數動態生成組態值,確保了應用程式在不同環境下的靈活性。

環境變陣列態的優點

  1. 靈活性:無需修改程式碼即可更改組態。
  2. 安全性:敏感資訊(如資料函式庫密碼)不會硬編碼在程式碼中。
  3. 一致性:確保開發、測試和生產環境的一致性。

將原始碼安裝為套件

為了方便開發和測試,我們將原始碼組織成一個 Python 套件,並使用 pip install -e 命令安裝。這種方式使得在開發過程中無需重新構建容器即可更新程式碼。

Makefile 與自動化工作流程

使用 Makefile 可以簡化常見的開發和 CI 工作流程。例如:

build:
    docker-compose build

test:
    docker-compose run app pytest .

這樣,只需執行 make buildmake test 即可構建或測試應用程式。

此圖示說明瞭容器化組態的核心元件及其相互關係。
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title 微服務重構實踐與領域驅動設計

package "Docker 架構" {
    actor "開發者" as dev

    package "Docker Engine" {
        component [Docker Daemon] as daemon
        component [Docker CLI] as cli
        component [REST API] as api
    }

    package "容器運行時" {
        component [containerd] as containerd
        component [runc] as runc
    }

    package "儲存" {
        database [Images] as images
        database [Volumes] as volumes
        database [Networks] as networks
    }

    cloud "Registry" as registry
}

dev --> cli : 命令操作
cli --> api : API 呼叫
api --> daemon : 處理請求
daemon --> containerd : 容器管理
containerd --> runc : 執行容器
daemon --> images : 映像檔管理
daemon --> registry : 拉取/推送
daemon --> volumes : 資料持久化
daemon --> networks : 網路配置

@enduml

此圖示呈現了 Docker Compose 如何協調多個容器服務,以及環境變陣列態和原始碼管理在整個系統中的作用。