延遲載入模式旨在提升應用程式效能,透過延遲資源初始化至實際需要時,減少啟動時間和記憶體消耗。文章以 Python 程式碼示範了兩種延遲載入的實作方式:使用屬性延遲載入和利用快取機制。屬性延遲載入的例子中,透過 @property 修飾符,在首次呼叫屬性時才進行初始化,後續呼叫則直接傳回快取值。快取機制則以階乘計算為例,使用 @lru_cache 儲存已計算結果,避免重複運算,提升效能。節流模式則是用於控制 API 請求速率,防止服務資源過載。文章以 Flask 和 Flask-Limiter 展示瞭如何設定速率限制,並說明瞭不同限制策略的應用。最後,重試模式則關注於提升分散式系統的可靠性,透過在遇到暫時性錯誤時進行重試,避免單點故障導致整個系統當機。文章以 Python 裝飾器實作了重試機制,並以資料函式庫連線為例,示範瞭如何在實際場景中應用重試模式。

延遲載入模式的實作與應用

延遲載入(Lazy Loading)是一種重要的效能最佳化模式,透過延遲資源的初始化直到實際需要時才進行載入,能有效提升應用程式的啟動速度並降低記憶體開銷。在本章中,我們將探討延遲載入模式的原理、實作方法及其在實際應用中的價值。

使用屬性延遲載入

首先,我們來實作一個簡單的延遲載入範例。考慮一個類別 LazyObject,其中包含一個昂貴的屬性 data。我們希望在第一次存取 data 時才進行初始化,而在後續存取中直接使用已快取的值。

class LazyObject:
    def __init__(self):
        self._data = None

    @property
    def data(self):
        if self._data is None:
            print("Loading expensive data...")
            # 模擬昂貴的操作
            self._data = sum(range(333328333350000))
        return self._data

# 測試實作
obj = LazyObject()
print("Object created, expensive attribute not loaded yet.")
print("Accessing expensive attribute:")
print(obj.data)
print("Accessing expensive attribute again, no reloading occurs:")
print(obj.data)

內容解密:

  1. 類別初始化:在 __init__ 方法中,我們將 _data 屬性初始化為 None,表示尚未載入昂貴的資料。
  2. 屬性存取控制:透過 @property 修飾符,我們定義了 data 屬性的 getter 方法。在第一次存取 data 時,若 _dataNone,則進行昂貴資料的載入並將結果存入 _data
  3. 效能最佳化:在第二次及之後的存取中,由於 _data 已被初始化,直接傳回已存的值,避免重複進行昂貴的操作。

執行上述程式碼後,輸出結果如下:

Object created, expensive attribute not loaded yet.
Accessing expensive attribute:
Loading expensive data...
333328333350000
Accessing expensive attribute again, no reloading occurs:
333328333350000

使用快取實作延遲載入

另一個常見的延遲載入實作方式是利用快取機制。考慮一個計算階乘的遞迴函式,由於遞迴運算可能非常耗時,我們可以使用快取來儲存已計算的結果,以避免重複計算。

import time
from datetime import timedelta
from functools import lru_cache

def recursive_factorial(n):
    """遞迴計算階乘"""
    if n == 1:
        return 1
    else:
        return n * recursive_factorial(n - 1)

@lru_cache(maxsize=128)
def cached_factorial(n):
    return recursive_factorial(n)

def main():
    n = 20
    
    # 無快取的計算
    start_time = time.time()
    print(f"Recursive factorial of {n}: {recursive_factorial(n)}")
    duration = timedelta(seconds=time.time() - start_time)
    print(f"Calculation time without caching: {duration}")
    
    # 有快取的計算
    start_time = time.time()
    print(f"Cached factorial of {n}: {cached_factorial(n)}")
    duration = timedelta(seconds=time.time() - start_time)
    print(f"Calculation time with caching: {duration}")
    
    # 重複計算(使用快取)
    start_time = time.time()
    print(f"Cached factorial of {n}, repeated: {cached_factorial(n)}")
    duration = timedelta(seconds=time.time() - start_time)
    print(f"Second calculation time with caching: {duration}")

if __name__ == "__main__":
    main()

內容解密:

  1. lru_cache 使用:透過 @lru_cache 修飾符,我們為 cached_factorial 函式啟用了最近最少使用(LRU)快取機制。這樣,當函式以相同的引數被呼叫時,可以直接從快取中取得結果,而無需重新計算。
  2. 效能比較:在 main 函式中,我們分別測試了無快取和有快取的階乘計算時間。可以明顯看出,使用快取後,首次計算仍需較長時間,但重複計算時能夠顯著提升效能。

輸出結果如下:

Recursive factorial of 20: 2432902008176640000
Calculation time without caching: 0:00:04.840851
Cached factorial of 20: 2432902008176640000
Calculation time with caching: 0:00:00.865173
Cached factorial of 20, repeated: 2432902008176640000
Second calculation time with caching: 0:00:00.000189

節流模式(Throttling Pattern):保護服務資源的限流策略

在現代的應用程式和API中,節流(Throttling)是一種重要的模式,用於控制使用者(或客戶端服務)在給定時間內向特定服務或API傳送請求的速率,以防止服務資源被過度使用。

節流模式的核心概念

節流的核心思想是限制使用者在特定時間內對API的請求次數。例如,我們可以限制使用者每天對某個API的請求次數為1000次。一旦達到這個限制,下一個請求將傳回一個錯誤訊息,包含HTTP狀態碼429,並提示使用者請求過多。

現實生活中的節流例子

節流模式在現實生活中有許多對應的例子,例如:

  • 高速公路交通管理:交通燈或速度限制調節車輛在高速公路上的流動
  • 水龍頭:調整水流的大小
  • 音樂會門票銷售:網站限制每個使用者一次性購買的門票數量,以防止伺服器因突增的需求而當機
  • 用電量管理:一些電力公司根據客戶在尖峰和離峰時段的用電量收取不同的費率
  • 自助餐排隊:自助餐廳可能限制顧客一次只能拿一盤食物,以確保每個人都有公平的機會吃飯並減少食物浪費

軟體實作方面,也有許多工具可以幫助實施節流,例如:

  • django-throttle-requests:一個用於Django專案的應用程式特定速率限制中介軟體框架
  • Flask-Limiter:為Flask路由提供速率限制功能的擴充套件

節流模式的適用場景

當需要確保系統持續提供預期的服務、最佳化服務使用成本或處理活動突增時,建議使用節流模式。實際應用中,可以實施以下規則:

  • 限制API的總請求次數為N/天(例如N=1000)
  • 限制來自特定IP地址或地區的API請求次數為N/天
  • 限制已驗證使用者的讀寫次數

除了速率限制外,節流模式還可以用於資源分配,確保資源在多個客戶端之間公平分配。

實施節流模式

在深入實施範例之前,需要了解有多種型別的節流,例如速率限制(Rate-Limit)、根據IP的限制(IP-level Limit)和平行連線限制(Concurrent Connections Limit)。這裡我們將重點介紹第一種。

使用Flask和Flask-Limiter實作速率限制

首先,匯入必要的模組:

from flask import Flask
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

接著,設定Flask應用程式:

app = Flask(__name__)

然後,建立Limiter例項:

limiter = Limiter(
    get_remote_address,
    app=app,
    default_limits=["100 per day", "10 per hour"],
    storage_uri="memory://",
    strategy="fixed-window",
)

定義路由

定義一個受速率限制的路由/limited

@app.route("/limited")
def limited_api():
    return "Welcome to our API!"

以及另一個具有特定速率限制的路由/more_limited

@app.route("/more_limited")
@limiter.limit("2/minute")
def more_limited_api():
    return "Welcome to our expensive, thus very limited, API!"

最後,新增Flask應用程式的常規程式碼片段:

if __name__ == "__main__":
    app.run(debug=True)

測試範例

執行檔案(ch09/throttling_flaskapp.py)使用命令python ch09/throttling_flaskapp.py。在瀏覽器中存取http://127.0.0.1:5000/limited,並重複重新整理頁面。當達到第10次請求時,將顯示「Too Many Requests」錯誤訊息。

對於/more_limited路由,在1分鐘內重複重新整理頁面超過2次,也將顯示「Too Many Requests」訊息。

程式碼詳細解析

匯入必要的模組

from flask import Flask
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

設定Flask應用程式和Limiter例項

app = Flask(__name__)
limiter = Limiter(
    get_remote_address,
    app=app,
    default_limits=["100 per day", "10 per hour"],
    storage_uri="memory://",
    strategy="fixed-window",
)

內容解密:

  1. get_remote_address函式用於取得客戶端的IP地址。
  2. default_limits引數設定了預設的速率限制。
  3. storage_uri="memory://"表示將速率限制資料儲存在記憶體中。
  4. strategy="fixed-window"指定了速率限制的策略。

定義路由和速率限制

@app.route("/limited")
def limited_api():
    return "Welcome to our API!"

@app.route("/more_limited")
@limiter.limit("2/minute")
def more_limited_api():
    return "Welcome to our expensive, thus very limited, API!"

內容解密:

  1. /limited路由使用預設的速率限制。
  2. /more_limited路由使用特定的速率限制(2次/分鐘)。

圖表說明

此圖示展示了節流模式的工作流程:

  graph LR;
    A[客戶端] -->|請求| B[伺服器端];
    B -->|檢查速率限制|> C{是否超出限制};
    C -->|是| D[傳回429錯誤];
    C -->|否| E[處理請求];
    E --> F[傳回回應];

圖表翻譯:

  1. 客戶端向伺服器端傳送請求。
  2. 伺服器端檢查請求是否超出速率限制。
  3. 如果超出限制,則傳回429錯誤訊息。
  4. 如果未超出限制,則處理請求並傳回回應。

重試模式(The Retry Pattern):在分散式系統中的應用

在現代的分散式系統和雲端基礎設施中,各個元件之間的協作變得越來越複雜。由於元件可能由不同的團隊開發和佈署,因此在系統執行過程中可能會遇到暫時性的故障或錯誤。重試模式(Retry Pattern)提供了一種有效的解決方案,能夠提高系統的穩定性和可靠性。

現實生活中的重試模式範例

在日常生活中,我們經常會遇到需要重試的情況,例如:

  • 打電話給朋友,但由於網路問題或對方忙線而無法接通。這時,我們會在稍作等待後再次撥打電話。
  • 在自動提款機(ATM)取款時,由於網路擁塞或連線問題導致交易失敗。這時,我們會稍作等待後再次嘗試進行交易。

這些日常生活中的例子都體現了重試模式的思想:在遇到暫時性錯誤時,透過重試來提高成功的機會。

軟體開發中的重試模式範例

在軟體開發領域,也有許多工具和技術體現了重試模式的思想。例如:

  • Python 中的 Retrying 函式庫(https://github.com/rholder/retrying)簡化了為函式新增重試行為的任務。
  • Go 語言中的 Pester 函式庫(https://github.com/sethgrid/pester)為開發者提供了類別似的功能。

重試模式的應用場景

重試模式適用於緩解與外部元件或服務通訊時由於網路故障或伺服器過載而導致的暫時性故障。需要注意的是,重試模式不適用於處理由應用程式邏輯本身錯誤引起的內部異常。此外,我們需要分析外部服務的回應。如果應用程式頻繁遇到忙碌故障,通常意味著被存取的服務存在需要解決的擴充套件問題。

在微服務架構中,由於服務經常透過網路進行通訊,重試模式能夠確保暫時性故障不會導致整個系統失敗。另一個應用場景是資料同步。當在兩個系統之間同步資料時,重試可以處理其中一個系統暫時不可用的情況。

重試模式的實作

以下是一個使用 Python 實作重試模式的範例,用於資料函式庫連線的重試機制。我們將使用一個裝飾器(decorator)來處理重試機制。

import logging
import random
import time

# 設定日誌記錄等級
logging.basicConfig(level=logging.DEBUG)

# 定義一個裝飾器,用於自動重試被裝飾的函式
def retry(attempts):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(attempts):
                try:
                    logging.info("正在進行重試")
                    return func(*args, **kwargs)
                except Exception as e:
                    time.sleep(1)
                    logging.debug(e)
            return "所有重試嘗試後仍失敗"
        return wrapper
    return decorator

# 使用 @retry 裝飾器裝飾 connect_to_database 函式,使其最多重試三次
@retry(attempts=3)
def connect_to_database():
    # 模擬資料函式庫連線,可能會引發暫時性錯誤
    if random.randint(0, 1):
        raise Exception("暫時性資料函式庫錯誤")
    return "成功連線到資料函式庫"

# 測試程式碼
if __name__ == "__main__":
    for i in range(1, 6):
        logging.info(f"第 {i} 次連線嘗試")
        print(f"--> {connect_to_database()}")

內容解密:

  1. retry 裝飾器:定義了一個名為 retry 的裝飾器工廠,它接受一個 attempts 引數,表示重試的次數。內部定義了 decorator 函式,用於裝飾目標函式。wrapper 函式是實際執行重試邏輯的地方,它會在指定的 attempts 次數內重複呼叫被裝飾的函式。如果在重試過程中成功執行,則傳回結果;如果所有重試都失敗,則傳回 “所有重試嘗試後仍失敗”。
  2. connect_to_database 函式:這是一個模擬資料函式庫連線的函式,它有 50% 的機率引發一個 “暫時性資料函式庫錯誤” 的異常。該函式被 @retry(attempts=3) 裝飾,表示如果連線失敗,將最多重試三次。
  3. 測試程式碼:在 if __name__ == "__main__": 區塊中,迴圈進行五次連線嘗試,並列印每次嘗試的結果。

重試模式的優點

  • 提高系統可靠性:透過在遇到暫時性錯誤時進行重試,重試模式能夠提高系統的可靠性和穩定性。
  • 降低故障率:在分散式系統和微服務架構中,重試模式能夠有效地降低由於網路故障或服務暫時不可用而導致的故障率。