架構
Caddy 是一個單一、獨立、靜態的二進制檔案,不含任何外部依賴,因為它是以 Go 語言編寫的。這些價值觀構成了專案願景的重要部分,因為它們簡化了部署,並減少了生產環境中繁瑣的故障排除。
如果沒有動態連結,那麼它如何擴展?Caddy 採用了一種新穎的插件架構,使其功能遠遠超出任何其他 Web 伺服器,甚至是那些具有外部(動態連結)依賴項的伺服器。
我們「減少活動部件」的哲學最終帶來更可靠、更易管理、成本更低的網站——尤其是在大規模情況下。這份半技術性文件描述了我們如何通過軟體工程實現這個目標。
概觀
Caddy 由命令、核心函式庫和模組組成。
命令提供了您可能熟悉的命令列介面。這是您從作業系統啟動程序的途徑。此處的程式碼和邏輯量相當少,僅包含以使用者期望的方式引導核心所需的內容。我們有意避免使用旗標和環境變數進行配置,除非它們與引導配置有關。
核心函式庫,或 Caddy 的「核心」,主要管理配置。它可以 Run()
新配置或 Stop()
執行中的配置。它還為模組提供了各種實用程式、類型和值以供使用。
模組執行其他所有操作。許多模組內建於 Caddy 中,這些模組稱為標準模組。這些模組被認為對大多數使用者最有用。
Caddy 核心
在其核心,Caddy 僅載入初始配置(「config」),或者,如果沒有配置,則開啟一個 socket 以稍後接受新配置。
Caddy 配置是一個 JSON 文件,在其頂層有一些欄位
{
"admin": {},
"logging": {},
"apps": {•••},
...
}
Caddy 核心知道如何原生處理其中一些欄位
但其他頂層欄位(例如 apps
)對於 Caddy 核心來說是不透明的。事實上,Caddy 對於 apps
中的位元組所知道的就是將它們反序列化為一個介面類型,它可以對其呼叫兩個方法
Start()
Stop()
... 就是這樣。它在載入配置時對每個應用程式呼叫 Start()
,在卸載配置時對每個應用程式呼叫 Stop()
。
當應用程式模組啟動時,它會啟動應用程式模組的生命週期。
模組生命週期
模組有兩種:宿主模組和訪客模組。
宿主模組(或「父」模組)是那些載入其他模組的模組。
訪客模組(或「子」模組)是那些被載入的模組。所有模組都是訪客模組——甚至是應用程式模組。
模組按照以下順序載入、配置和驗證、使用,然後清理
- 已載入
- 已配置和驗證
- 已使用
- 已清理
當載入配置時,Caddy 首先通過初始化所有已配置的應用程式模組來啟動模組生命週期。從那裡開始,就像烏龜疊羅漢一樣,每個應用程式模組都接管剩下的部分。
載入階段
載入模組涉及將其 JSON 位元組反序列化為記憶體中的類型化值。基本上就是這樣。這只是將 JSON 解碼為一個值。
配置階段
此階段是大部分設定工作進行的地方。所有模組在載入後都有機會配置自己。
由於 JSON 編碼中的任何屬性都已經被解碼,因此此處只需要進行額外的設定。配置期間最常見的任務是設定訪客模組。換句話說,配置宿主模組也會導致配置其訪客模組,一直向下。
您可以通過瀏覽我們文件中的 Caddy JSON 結構來了解這一點。您看到的任何 {•••}
都是可以使用訪客模組的地方;當您點擊進入一個時,您可以繼續探索一直向下,直到沒有更多訪客模組為止。
其他常見的配置任務是設定模組生命週期期間將使用的內部值,或標準化輸入。例如,http.matchers.remote_ip
模組使用配置階段從它從 JSON 接收的字串輸入中解析 CIDR 值。這樣,它就不必在每次 HTTP 請求期間都執行此操作,因此效率更高。
驗證也可以在配置階段進行。如果模組的結果配置無效,則可以在此處返回錯誤,從而中止整個配置載入過程。
使用階段
一旦訪客模組被配置和驗證,它就可以被其宿主模組使用。這究竟意味著什麼取決於每個宿主模組。
每個模組都有一個 ID,它由命名空間和該命名空間中的名稱組成。例如,http.handlers.reverse_proxy
是一個 HTTP 處理器,因為它位於 http.handlers
命名空間中,並且其名稱為 reverse_proxy
。http.handlers
命名空間中的所有模組都滿足相同的介面,宿主模組知道該介面。因此,http
應用程式知道如何載入和使用這些模組。
清理階段
當需要停止配置時,所有模組都會被卸載。如果模組分配了任何應該釋放的資源,它有機會在清理階段執行此操作。
插入
模組——或任何 Caddy 插件——通過為模組的套件添加 import
來「插入」Caddy。通過導入套件,模組會在 Caddy 核心中註冊自己,因此當 Caddy 程序啟動時,它會按名稱知道每個模組。它甚至可以在模組值和名稱之間以及反之亦然地進行關聯。
管理配置
更改正在運行的伺服器的活動配置(通常稱為「重新載入」)對於伺服器需要的高併發級別和數千個參數來說可能很棘手。Caddy 使用具有許多優點的設計優雅地解決了這個問題
- 運行中的服務不會中斷
- 可以進行細微的配置更改
- 只需要一個鎖(在背景中)
- 所有重新載入都是原子性、一致性、隔離性和大部分持久性(「ACID」)
- 最小的全域狀態
您可以在此處觀看關於 Caddy 2 設計的影片。
配置重新載入的工作方式是配置新的模組,如果一切成功,則清理舊的模組。在短暫的時間內,兩個配置同時運作。
每個配置都與一個上下文相關聯,該上下文保存所有模組狀態,因此大多數狀態永遠不會逸出配置的範圍。這對於正確性、效能和簡潔性來說都是好消息!
但是,有時真正需要全域狀態。例如,反向代理可能會追蹤其上游的健康狀況;由於每個上游在全域範圍內只有一個,因此如果每次進行小的配置更改時都忘記它們,那將是不好的。幸運的是,Caddy 提供了類似於語言運行時的垃圾收集器的設施,以保持全域狀態整潔。
在線配置更新的一個明顯方法是同步對每個配置參數的訪問,即使在熱路徑中也是如此。這在效能和複雜性方面都是難以置信的糟糕——尤其是在大規模情況下——因此 Caddy 沒有使用這種方法。
相反,配置被視為不可變的原子單元:要么替換整個配置,要么什麼都不會更改。管理 API 端點——允許通過遍歷結構進行細微更改——僅修改配置的記憶體中表示,從中生成並載入全新的配置文件。這種方法在簡潔性、效能和一致性方面具有巨大的優勢。由於只有一個鎖,Caddy 可以輕鬆處理快速重新載入。