Codetopia 的週一早晨,總是充滿了咖啡香與……新的災難。城市地圖 City Map 2.0
計畫才剛慶祝完用 Flyweight 模式大幅降低了記憶體壓力,都還沒來得及開香檳,警報就響徹了整個監控中心。
「報告!公共景點的圖資熱點被不明爬蟲刷爆了!」
「等等!合作廠商的測試機器怎麼直接把我們內部的圖示原檔,拉到他們外網的 CDN 上了?!」
「天啊,還有未授權的下載請求正大量湧入!」
地圖前端工程師 Dylan 額頭冒著冷汗,眼前的儀表板一片血紅。前兩天,我們才剛用 Facade 清理了邊界,用 Flyweight 共享了內蘊,今天,我們的大門口就直接被攻破了。
砰! 一聲巨響,門禁代理隊長 Iris|門禁代理隊長 用力拍在會議桌上。「夠了!各自為政的時代結束了。」她的眼神像掃描器一樣掃過在場的每一個人,「從現在起,所有對資產庫(AssetRepo)的請求,一律不准直連!全部給我先過代理層!」
她迅速在白板上劃下三條鐵律:
公有資產:可以匿名讀取,但必須加上速率限制與快取,擋住惡意爬蟲。
私有資產:必須憑藉有效的權杖(Token)與細粒度的存取策略(ACL)才能核發。
大檔案或跨區請求:全部交給「遠端代理」在邊緣節點(Edge Node)代為傳輸,不准再讓這些流量把市中心的網路給拖垮!
(旁白:「看來,繼『門面要清爽』、『內存要共享』之後,我們 Codetopia 的第三條生存法則是——『大門要有腦』。」)
驗收的軍令狀也隨之而來,而且這次附上了明確的服務等級目標(SLO):
Given:一個 /assets/icons/{key}
請求,帶著 partner:read
的角色,來自 APAC
區域,但同時正遭受每秒超過 1000 次的 QPS 攻擊。
When:客戶端(例如 Dylan 的地圖程式)的程式碼維持不變,僅調整其依賴注入(DI)綁定,將 IAssetRepo
的實作從 RealAssetRepo
指向 GatekeeperProxy
。
Then:未授權請求 → 403
;超速請求 → 429
(黑名單 1h);合法請求命中快取或由最近 Edge 代傳。目標:核心倉 QPS 峰值下降 ≥ 50%、P95 延遲 ≤ 400ms、公共資產誤封率 ≤ 0.1%。
Proxy(代理):一個控制對象(Object)存取的「替身」。它和真實對象實作相同的介面,讓客戶端感覺不到它的存在,但在中間卻能執行額外的任務,像是門禁(Protection Proxy)、延遲載入(Virtual Proxy)、遠端代打(Remote Proxy),或是增加快取/計數等功能的智慧代理(Smart Proxy)。
讓我們把時間倒轉回 Iris 拍桌前的三十分鐘。當時,我們焦頭爛額的英雄 Dylan 是這樣寫他的前端讀取邏輯的:
# ❌ 反例:Dylan 直連真實資產庫,既曝密又拖垮中心
def get_asset_directly(key):
# 風險1:將管理員權杖硬編碼在客戶端,形同虛設。
token = "hardcoded-super-secret-admin-token"
# 風險2:直接暴露內部核心服務的 URL。
url = f"https://core.assets.codetopia/raw/{key}"
# 風險3:所有請求都用同一最高權限,缺乏分級。
blob = http.get(url, headers={"Authorization": f"Bearer {token}"}).content
# 風險4:客戶端自行實作快取,但缺乏一致性與清理機制。
local_cache.write(key, blob)
# 風險5:同步下載大檔案,導致 UI 嚴重卡頓。
render_map_tile(blob)
# 後果:
# 1. 管理員權杖外洩,整個資產庫形同虛設。
# 2. QPS 雪崩直接衝擊核心資料庫,沒有任何緩衝。
# 3. UI 延遲飆升,市民怨聲載道。
# 4. 任何合作夥伴或內部人員都可以輕易繞過所有治理規則。
Proxy 模式就像是在你的豪宅門口,雇用了一位訓練有素、十八般武藝樣樣精通的警衛。客人按的門鈴(介面)是一樣的,但警衛(代理)會根據訪客身分、時間、甚至訪客的意圖,決定是直接開門、請他稍後、或是叫他滾蛋。
需要存取控制:當你想對一個物件的存取加上權限檢查、審計日誌、或是流量限制,但又不希望污染原始物件的商業邏輯,更不想改動所有呼叫端的程式碼時。
隱藏遠端呼叫的複雜性:當你的真實物件在另一台機器,你可以用一個 Remote Proxy
假裝它就在本地。代理會幫你處理網路通訊、序列化,甚至是熔斷與回退(fallback)機制。
延遲載入昂貴資源:當一個物件的初始化成本極高,你可以先給客戶端一個 Virtual Proxy
,等到真正需要使用它時,代理才去建立真實的物件。
智慧操作與加值服務:在呼叫前後增加快取、引用計數、執行緒安全鎖等。此外,針對大檔案下載,Remote Proxy
可選擇「代理傳輸」或「僅簽發附帶短時效(TTL)的簽名 URL」兩條路徑,依成本與延遲需求動態切換。
只是介面長得不一樣:如果只是想把一個介面轉換成另一個,那用 Adapter
(轉接站)模式更單純、語意也更清晰。
只想動態疊加功能,且不需要攔截:如果你只是想為物件動態地增加新功能,且不打算控制或拒絕存取,那 Decorator
(裝修工)模式是更好的選擇。Decorator 讓請求一定會穿透到真實物件,而 Proxy 則可能會攔截它。
代理變成萬能上帝:如果你的 Proxy 開始做太多不相干的事情,比如處理複雜的商業規則、流程編排,那它就變成了「上帝代理」,這是一種壞味道。這些邏輯應該回歸到服務內部,或是由 Facade
來負責編排。
導播,鏡頭拉一下!讓我們從三個不同的尺度,看看 Proxy 是如何扮演好它的守門員角色的。
視角 | 觀念/模式 | 在 Codetopia 的說法 |
---|---|---|
微觀 (GoF) | Proxy 實作 Subject 介面,控制對 RealSubject 的存取 |
GatekeeperProxy (門禁) 和 EdgeRemoteProxy (遠端) 都偽裝成 RealAssetRepo ,站在客戶端前面。 |
中觀 (EIP/EDA) | Ambassador /Sidecar , Throttler , Cache |
代理就像一個邊車或大使,在服務邊界執行節流、快取和路由策略。 |
宏觀 (MAS) | GatekeeperAgent (門禁), EdgeRelayAgent (邊緣代傳) |
分工明確的代理人:一個負責審計與配額,另一個負責跨區資料中繼。 |
微觀 GoF 結構圖:
中觀資訊流時序圖:
現在,讓我們看看 Iris 隊長提出的正解。注意,這個版本將易變的邏輯(如限速、快取 TTL)都抽離成可注入的「策略」,並加入了優雅的失敗路徑處理。
# --- 簡易退避重試 helper(jitter 防止同步尖峰) ---
import random, time
def retry_with_jitter(fn, *, max_attempts=3, base=0.05):
attempt = 0
while True:
try:
return fn()
except Exception as e:
attempt += 1
if attempt >= max_attempts:
raise e
time.sleep(base * (2 ** (attempt - 1)) + random.random() * base)
# --- 自定義錯誤型別,便於上層映射為 HTTP 狀態碼 ---
class ForbiddenError(Exception): pass # --> 對應 403 Forbidden
class RateLimitedError(Exception): pass # --> 對應 429 Too Many Requests
class EdgeUnavailableError(Exception): pass # --> 對應 503 Service Unavailable
# --- 介面定義 (Subject) ---
class IAssetRepo:
def get(self, key, *, cred=None):
raise NotImplementedError("Subclasses must implement this method")
# --- 真實的物件 (RealSubject) ---
class RealAssetRepo(IAssetRepo):
def get(self, key, *, cred=None):
print(f"CORE: Reading '{key}' from deep storage...")
return f"BLOB_DATA_FOR_{key}"
# --- 代理物件 (Proxy),採用策略注入 ---
class GatekeeperProxy(IAssetRepo):
def __init__(self, real_repo, *,
acl, limiter, cache, auditor, circuit_breaker,
edge_proxy=None,
large_file_strategy,
cache_ttl_strategy):
self._real_repo = real_repo
self._acl = acl
self._limiter = limiter
self._cache = cache
self._auditor = auditor
self._circuit = circuit_breaker
self._edge_proxy = edge_proxy
self._large_file_strategy = large_file_strategy
self._cache_ttl_strategy = cache_ttl_strategy
def get(self, key, *, cred=None):
# ... (權限檢查與速率限制如前) ...
if not self._acl.is_allowed(cred, key): raise ForbiddenError("Access Denied")
if not self._limiter.try_acquire(cred["id"], key): raise RateLimitedError("Too Many Requests")
if (blob := self._cache.get(key)) is not None:
self._auditor.log("CACHE_HIT", key, cred)
return blob
self._auditor.log("CACHE_MISS", key, cred)
# 關卡 4: 以策略決定是否走遠端代理,並加入熔斷與回退
if self._edge_proxy and self._large_file_strategy.is_large(key):
try:
# 只有在熔斷器關閉時才嘗試遠端呼叫
if self._circuit.is_closed():
# 帶有抖動的退避重試
blob = retry_with_jitter(lambda: self._edge_proxy.get(key, cred=cred), max_attempts=3)
else:
raise EdgeUnavailableError("Circuit is open")
except EdgeUnavailableError:
self._circuit.open(ttl_seconds=60) # 熔斷 60 秒
self._auditor.log("EDGE_FAIL_FALLBACK", key, cred)
blob = self._real_repo.get(key, cred=cred) # 回退到本地核心庫
else:
blob = self._real_repo.get(key, cred=cred)
ttl = self._cache_ttl_strategy.ttl_for(key)
self._cache.set(key, blob, ttl=ttl)
return blob
# --- 審計治理建議(示例,用於實際記錄點附近) ---
# 建議在呼叫 self._auditor.log(...) 的地方,搭配下列策略設定:
# self._auditor.set_sampling(rate=0.1) # 高 QPS 場景取樣 10%
# self._auditor.set_retention(days=30) # 保存 30 天
# # 例:去識別化(示意,於實際 log 時計算)
# actor_hash = hash(str(cred.get("id"))) % 10000 if cred else None
# self._auditor.log("FORBIDDEN", key, {"actor_hash": actor_hash})
當你在城市中巡邏時,看到以下場景,請立刻亮起紅旗,這通常是 Proxy 被誤用的跡象:
🚩 客戶端的秘密判斷:把權杖、角色判斷邏輯寫死在客戶端。這等於把警衛的工作交給了訪客自己,一換就全部外洩。
🚩 紙老虎代理:一個 Proxy
只是單純地將請求轉發給真實物件,沒有做任何權限控制、快取或審計。這不是警衛,這是傳聲筒。
🚩 洩漏的實作細節:代理應該完美偽裝成真實物件。如果它回傳了內部的錯誤碼、資料型別,或是讓客戶端能以某種方式繞過它直接聯繫到真實物件,那它的抽象就被破壞了。
🚩 萬能的上帝代理:代理的職責應該是「跨領域關注點」(Cross-cutting Concerns),如安全、快取、日誌。如果它開始處理起「核心商業邏輯」,那它就越權了,正在變成一個難以維護的「微型神物件」。
將 Proxy 的概念放大,它在更宏觀的架構中扮演著關鍵角色。
EIP/EDA/Actor 架構:在事件驅動或微服務架構中,GatekeeperProxy
的角色很像 Ambassador
或 Sidecar
模式。它作為一個獨立的進程部署在服務旁邊,專門處理服務邊界的網路流量、安全認證、熔斷、節流和監控,而核心服務則可以專注於商業邏輯。任何異常都可以發布為事件,通知監控中心。
MAS (多代理系統):在多代理的世界裡,我們可以將其具現化為兩個高度分工的代理人:GatekeeperAgent
負責權杖校驗、配額管理和審計,而 EdgeRelayAgent
則負責跨網路邊界的資料拉取與中繼。這些代理會將自己的服務註冊到 DF (黃頁) 上,讓其他代理知道可以從哪裡找到最近的邊緣節點。
讓我們回到故事的開頭。當 Dylan 團隊的部署流程更新,僅調整了依賴注入的設定,將 IAssetRepo
的實作從 RealAssetRepo
指向 GatekeeperProxy
後,監控中心的警報聲戛然而止,取而代之的是一片代表「正常」的綠燈。
軍令狀上的驗收項目,逐一通過:
未授權→403:那些不明爬蟲收到了冰冷的 403 Forbidden
回應。
超速→429:QPS 攻擊在速率限制器前撞得頭破血流,收到了 429 Too Many Requests
,並被暫時封鎖一小時。
已授權→就近服務:合法的合作夥伴請求大檔案時,流量被無縫導向最近的 APAC 邊緣節點,P95
延遲從數秒降至幾百毫秒,市中心核心庫的 QPS 壓力也應聲下降。
最美妙的是,Dylan 的地圖渲染程式碼,一行都沒改。這完美呼應了前兩日的設計精神:「門面不變、共享不變」,今天則是「門禁升級,呼叫端無感」。
Iris 隊長在交付 GatekeeperProxy
時,附上了一份嚴謹的測試指南,確保這道大門固若金湯:
授權矩陣測試:驗證不同的使用者角色(subject)、對不同的資源(resource)、執行不同的操作(action)時,是否能得到預期的成功回應或 ForbiddenError
。
限速/配額測試:在一個時間窗內連續發動 M 次請求,驗證第 M+1 次請求是否確實拋出 RateLimitedError
;並驗證等待時間窗過去後,請求是否能恢復正常。
快取一致性測試:驗證第一次請求是 MISS
,第二次是 HIT
。可透過比對 ETag
或版本號來設計快取失效策略(Cache Invalidation),確保資料更新後能正確地再次 MISS
。
遠端代理路徑測試:透過模擬網路故障,驗證當遠端節點不可用時,代理是否能觸發熔斷機制,並優雅地回退(fallback)到本地核心庫,或回傳適當的錯誤訊息。
契約穩定性測試:確保 Proxy
和 RealSubject
的方法簽名完全一致,且回傳值的語義是等價的,不能破壞客戶端的預期。
實作題:請為 GatekeeperProxy
加上一個更具體的速率限制器。例如,一個基於滑動時間窗(Sliding Window)的限速邏輯,並能區分不同角色的配額(如 partner
每分鐘 100 次,employee
每分鐘 1000 次,public
每分鐘 10 次)。撰寫測試案例來驗證窗口過期後速率恢復,以及配額耗盡時的回應。
思辨題(二選一):假設最近 Codetopia 的邊緣節點(Edge)維護成本急劇上升。你作為總設計師,會選擇:
A: 保留 Remote Proxy
的架構,但將大檔案的策略從「代理傳輸」改為「代理只生成帶有簽名的 CDN URL」,讓客戶端自己去 CDN 下載。
B: 徹底關閉 Remote Proxy
,所有流量都回到市中心的核心庫,但強制要求客戶端必須使用分段續傳(Chunked Transfer)來下載大檔案。
請選擇 A 或 B,並簡要說明你在總成本、延遲體驗、治理權限這三者之間的權衡。
Proxy 模式為我們在混亂的邊界上,建立了一道智慧的防線。它讓我們在不驚動內部居民(核心邏輯)和外部訪客(客戶端)的前提下,完成了城市安全的重大升級。
本日摘要:同介面攔放行,權控限速擋濫用;遠端代打降延遲,門內安全門外順。
明日預告:Day 14|Observer:城市廣播、訂閱更新——一呼百應的事件之城即將登場!
為了確保在不支援 Mermaid 渲染的環境中也能正常閱讀,以下提供文中圖表的 ASCII 替代版本:
┌─────────────────┐
│ <<Subject>> │
│ Interface │
│ │
│ + get(key): Blob│
└─────────┬───────┘
│
┌──────────────┼──────────────┐
│ │ │
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌──────────────┐ ┌──────────────────┐
│ RealAssetRepo │ │GatekeeperProxy│ │ EdgeRemoteProxy │
│ │ │ │ │ │
│ + get(key): Blob│ │ + get(key): │ │ + get(key): Blob │
│ │ │ Blob │ │ │
│ [核心資產庫] │ │ │ │ [邊緣代理] │
│ │ │ ┌──────────┐ │ │ │
└─────────────────┘ │ │ AuthZ │ │ └──────────────────┘
▲ │ │ RateLimit│ │ │
│ │ │ Cache │ │ │
│ │ │ Audit │ │ │
└─────────┤ └──────────┘ │ │
│ │ │
└──────────────┘ │
│ │
└─────────────────┘
(遠端呼叫/中繼)
Dylan GatekeeperProxy EdgeRemoteProxy RealAssetRepo
│ │ │ │
│ get(key, │ │ │
│ cred) ──────► │ │
│ │ │ │
│ │ [AuthZ Check] │ │
│ │ [Rate Limit] │ │
│ │ [Cache Lookup] │ │
│ │ │ │
│ ├─ Cache Hit? ──────┤ │
│ │ │ │
│◄─── blob ───┤ (if cached) │ │
│ │ │ │
│ │ (else: cache miss)│ │
│ │ │ │
│ │ fetchNear(key) ───► │
│ │ │ │
│ │ │ get(key) ──────►
│ │ │ │
│ │ │◄──── blob ─────│
│ │ │ │
│ │◄──── blob ────────│ │
│ │ │ │
│ │ [Audit Log] │ │
│ │ [Cache Store] │ │
│ │ │ │
│◄─── blob ───│ │ │
│ │ │ │
🌐 外部網路
│
┌────┴────┐
│ 🛡️ 防火牆 │
└────┬────┘
│
┌───────────────┴────────────────┐
│ Codetopia 大門 │
│ ┌──────────────────┐ │
│ │ GatekeeperProxy │ │
│ │ │ │
│ │ ⚔️ AuthZ Check │ │
│ │ 🚦 Rate Limiter │ │
│ │ 💾 Cache Layer │ │
│ │ 📊 Audit Logger │ │
│ │ ⚡ Circuit Break │ │
│ └─────────┬─────────┘ │
└───────────────┼─────────────────┘
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌────────┐ ┌─────────────┐
│EdgeRemoteProxy│ │ Cache │ │RealAssetRepo│
│ │ │ Store │ │ │
│ APAC Node │ │ │ │ 核心資產庫 │
│ 🌏 就近服務 │ │ Redis │ │ │
└───────────────┘ └────────┘ └─────────────┘
[請求進入]
│
▼
┌──────────┐ ❌ 無權限 ┌─────────┐
│ 權限檢查 │ ──────────────────► │ 403錯誤 │
└─────┬────┘ └─────────┘
│ ✅ 有權限
▼
┌──────────┐ ❌ 超過速率 ┌─────────┐
│ 速率檢查 │ ──────────────────► │ 429錯誤 │
└─────┬────┘ └─────────┘
│ ✅ 未超速
▼
┌──────────┐ ✅ 快取命中 ┌─────────┐
│ 快取查詢 │ ──────────────────►│ 回傳資料 │
└─────┬────┘ └─────────┘
│ ❌ 快取未命中
▼
┌──────────┐ ✅ 大檔案 ┌────────────┐
│ 檔案類型 │ ──────────────────► │ 邊緣代理處理 │
│ 判斷 │ └─────┬──────┘
└─────┬────┘ │
│ ❌ 小檔案 │
▼ │
┌──────────┐ │
│核心庫處理 │ │
└─────┬────┘ │
│ │
└──────────┬──────────────────────┘
▼
┌──────────┐
│ 更新快取 │
└─────┬────┘
▼
┌──────────┐
│ 回傳資料 │
└──────────┘