設計模式是軟件開發中的重要經驗總結,Gang of Four (GoF) 提出的經典設計模式則被譽爲設計模式中的“聖經”。但是設計模式往往是以抽象和理論化的方式呈現,對於初學者或者沒有太多實戰經驗的開發者來說,直接學習設計模式往往會顯得枯燥乏味。
市面上或者網上也經常有一些書籍或者文章,嘗試以實際的應用場景深入淺出地介紹設計模式。但是這些資料所列舉的樣例或應用實踐,往往都是一些構造的虛擬場景,缺乏生產級軟件的真實應用。而軟件理論最重要的是學以致用,那是否有真實生產級代碼的學習機會呢?
iLogtail 作爲一款阿里雲日誌服務(SLS)團隊自研的可觀測數據採集器,目前已經在 Github 開源,其核心定位是幫助開發者構建統一的數據採集層。iLogtail 在多年的技術演進過程中,也一直在嘗試進行各種設計模式的應用,這些設計模式的應用大大提升了軟件的質量與可維護性。本文我們將結合 iLogtail 項目,從實踐角度探討一些常見設計模式的技術原理。在這裏也要感謝字節跳動多位同學對 iLogtail Golang 部分架構的一些升級優化。
如果你曾經感到學習設計模式枯燥無味,那麼來學習 iLogtail 吧!歡迎參與任何形式的社區討論交流,相信你會發現學習設計模式也可以是一件非常有趣的事情!
創建型模式
創建型模式的作用是提供一個通用的解決方案來創建對象,並隱藏創建的細節創建對象。說到創建一個對象,最熟悉的就是 New 一個對象,然後設置相關屬性。但是,在很多場景下,我們需要給應用方提供更加友好的創建對象的方式,尤其在創建各種複雜類的場景下。
單例模式
模式簡介
單例模式是指在整個系統生命週期內,保證一個類只能產生一個實例,確保該類的唯一性。對於一些資源管理類的場景(例如配置管理),往往需要擁有一個全局對象,這樣有利於協調系統整體的行爲。
iLogtail實踐
在 iLogtail 中,採集配置管理扮演着銜接用戶採集配置和內部採集任務的重要角色,通過加載與解析用戶採集配置,建立具體的採集任務。
作爲一個進程級的管理機制,ConfigManager 非常適合採用單例模式。iLogtail 啓動時會初始加載所有采集配置,並支持運行過程中動態加載變更的採集配置。通過單例模式,可以有效避免多個實例間狀態同步的問題;也提供了統一的全局接口,方便各個模塊進行調用。
class ConfigManager : public ConfigManagerBase {
public:
static ConfigManager* GetInstance() {
static ConfigManager* ptr = new ConfigManager();
return ptr;
}
// 構造、析構、拷貝構造、賦值構造等均爲私有,防止構造多個對象
private:
ConfigManager();
virtual ~ConfigManager();
ConfigManager(const ConfigManager&) = delete;
ConfigManager& operator=(const ConfigManager&) = delete;
ConfigManager(ConfigManager&&) = delete;
ConfigManager& operator=(ConfigManager&&) = delete;
};
GetInstance() 函數是單例模式的關鍵,該函數內使用了靜態變量、靜態函數的方式,以確保在應用程序中只有一個 ConfigManager 類的實例。爲了防止通過拷貝或賦值實例化多個 ConfigManager 對象,將拷貝構造函數和賦值運算符定義爲私有,並將其標記爲刪除。
同時,利用 C++11標準中的Magic Static特性:若變量在初始化時,併發同時進入聲明語句,併發線程將會阻塞等待初始化結束,保證了併發程序中的線程安全。
If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
工廠模式
模式簡介
工廠模式提供了一種創建對象的最佳方式。創建對象時不會對客戶端暴露創建邏輯,客戶端僅需要告訴工廠類要創建的對象,其餘工作由工廠類完成。
iLogtail實踐
爲了應對衆多可觀測數據類型的採集、處理需求,在 iLogtail C++ Pipeline 中定義了 Log、Metric、Trace,並抽象出了 Pipeline Event 作爲 Pipeline 數據流的通用格式。Pipeline Event 作爲 Pipeline 中的數據流轉的基本單元,往往會涉及大量的 Event 申請,因此在core/models中定義了 Pipeline Event 工廠,提供 Log、Metric、Span 等對象的創建,便於數據流靈活調用,降低了業務場景上的耦合,同時提高了數據模型新增時可擴展性。
生成器模式
模式簡介
生成器模式又稱建造者模式,該模式能夠分步驟創建複雜對象,允許使用相同的創建代碼生成不同類型和形式的對象。生成器模式所構建的對象一定是龐大而複雜的,並且一定是按照既定的製造工序將組件組裝起來的,例如汽車生產線等。
生成器模式由四個角色組成:
- Product(產品):複雜對象,它由多個部件組成,每個部件都有自己的構建方法和表示。
- Builder(抽象生成器):負責定義構建複雜對象的抽象接口,包括構建每個部件的方法。
- ConcreteBuilder(具體生成器):實現 Builder 接口,負責實現各個部件的構建方法,並最終組合成一個完整的複雜對象。
- Director(指揮者):負責管理 Builder 對象,調用 Builder 對象的方法來構建複雜對象。它不直接創建複雜對象,而是通過 Builder 對象來構建複雜對象。
iLogtail實踐
iLogtail 的 Go Pipeline 可以視作一個複雜的生產線,是一種典型的生成器模式應用場景。首先,Pipeline 管理器(Director)將 Pipeline 的構建過程分解爲多個插件的構建步驟,並由 PipeBuilder 完成各階段插件的創建和初始化;最後將這些插件組合成一個完整的Pipeline對象(Product)。
通過生成器模式的應用,大大提高 iLogtail 插件機制的可擴展性和可維護性,方便用戶根據實際需求進行擴展各類採集和處理場景。
原型模式
模式簡介
原型模式允許通過複製現有對象來創建新的對象,而不是通過顯式的實例化來創建。
iLogtail實踐
原型模式通常用於創建大量相似對象的場景。在 iLogtail 數據處理過程中,使用原型模式創建多個相似的 PipelineEvent 對象可以有效提高數據處理的效率和可維護性。
總結
創建型模式總體上比較簡單,它們的作用就是爲了產生實例對象。
- 單例模式:保證一個類只有一個實例,並提供一個訪問該實例的全局點。適用於管理一些全局的共享資源,避免多個實例之間的競爭和衝突,但是需要注意實現上的問題。
- 工廠模式:定義一個用於創建對象的接口,但讓子類決定將哪一個類實例化。適用於具有相似性質的對象的創建,更加靈活。
- 生成器模式:將一個複雜對象的構建過程分成多個步驟來完成。適用於創建一些複雜的對象,方便代碼的維護和擴展。
- 原型模式:利用拷貝對象的方法,減少一些複雜的創建過程。
結構型模式
結構型模式的作用是提供一種組織對象的方式,以便實現對象之間的關係和交互。
適配器模式
模式簡介
適配器模式將一種類型的接口轉換成希望的另一類接口,使得原本接口不兼容對象能夠一起配合工作。
iLogtail應用
iLogtail 進程由兩部分組成,一是 C++ 編寫的主體二進制進程,提供了管控、文件採集、C++加速處理、SLS 發送等功能;二是 Golang 編寫的插件部分(libPluginBase.so),通過插件系統實現了處理能力的擴展以及更豐富的上下游生態支持。
在 iLogtail 中,SLS 發送場景主要的實現邏輯在 C++ Sender.cpp,提供了完善的發送可靠性增強能力(異常處理、重試、反壓等)。而對於 Go Pipeline 中 SlsFlusher 也需要將採集、處理後的數據發送到 SLS,如果在 Go 插件側也實現相同的邏輯,會造成代碼的冗餘。因此,Go SlsFlusher 的實現原理是將處理後的數據轉發到 C++ 部分完成最終數據發送。但是跨語言場景必然存在不適配的因素,此時 libPluginAdaptor.so 充當一個適配器層,實現了 Golang 發送接口與 C++ 發送接口之間的銜接。
外觀模式
模式簡介
外觀模式旨在爲程序庫、 框架或其他複雜類提供一個簡單的接口。 外觀類通常會屏蔽一些子系統的複雜交互,提供一個簡單的接口,使得客戶端聚焦在真正關心的功能上。
iLogtail應用
在 K8s 日誌採集到 SLS 場景下,iLogtail 通過支持環境變量( aliyun_logs_{key} )的方式自動完成採集配置,包括創建 Project、Logstore、機器組、採集配置等 SLS 相關資源。整體操作較多,需要考慮配置詳情、容器過濾項、操作順序、失敗等衆多因素。
而對於 iLogtail Env 採集場景來說,僅需關心少數幾個核心的配置項即可。因此,實現了一個封裝所需功能並隱藏代碼細節的外觀類,不僅簡化了當前的調用關係;還能將未來後端 API 升級所造成的影響最小化, 因爲只需修改程序中外觀方法的實現即可。
橋接模式
模式簡介
橋接模式(Bridge Pattern)可將一個大類或一系列緊密相關的類拆分爲抽象和實現兩個獨立的層次結構, 從而能在開發時分別使用。概念比較晦澀,換一種理解方式:一個類存在兩個(或多個)獨立變化的維度,可以通過組合的方式,讓這兩個(或多個)維度可以獨立進行擴展。
iLogtail應用
在 iLogtail 中,使用 flusher_http 發送到不同後端系統時,往往需要支持請求加簽、追加auth header,請求的加簽算法可能因後端平臺而異。爲了實現更好的可擴展性,iLogtail 提供了 extensions 機制,將 flusher_http 插件的實現與具體的發送策略的實現分離,進而實現了Authenticator、FlushInterceptor、RequestInterceptors的可擴展性。
代理模式
模式簡介
代理模式就是使用一個代理類來隱藏具體實現類的實現細節,通常還用於在真實的實現的前後添加一部分邏輯。既然說是代理 ,那就要對客戶端隱藏真實實現,由代理來負責客戶端的所有請求。
iLogtail應用
在 iLogtail 中,最核心的步驟就是保證數據準確地發送到後端服務。在將數據發送到 SLS 場景下,最根本的就是調用 SDK 將打包好的數據發送,整個過程看似簡單卻蘊含着大智慧。因爲後端服務是複雜多變的,往往會存在着這種不確定因素,例如網絡不穩定、後端Quota滿、鑑權失敗、偶爾服務不可用、流控、進程重啓等。如果每個數據發送方獨立處理直接調用 SLS SDK 進行發送,必然導致大量重複代碼,造成代碼複雜度增加。因此,iLogtail 引入了 Sender 代理類,增強了直接 SDK 發送的可靠性。數據發送方僅需要調用 Sender::Instance()->Send 即可認爲已經完成了數據發送,剩下的複雜場景處理全都交給 Sender 類完成,由 Sender 類保證將數據成功發送到後端系統。
總結
代理模式用來做方法的增強;適配器模式實現了類似“把雞包裝成鴨”的接口適配;橋樑模式通過組合,實現系統的解耦;外觀模式可以讓客戶端不需要關心實例化過程,只要調用需要的方法即可。
此外,還有組合模式用於描述具有層次結構的數據;享元模式爲了在特定的場景中緩存已經創建的對象,用於提高性能。
行爲型模式
行爲模式負責對象間的高效溝通和職責委派,它關注的是各個類之間的相互作用,將職責劃分清楚,使得我們的代碼更加地清晰。
觀察者模式
模式簡介
觀察者模式定義了一種對象間的一對多的依賴關係,類似於訂閱和發佈的機制。當可觀察對象的狀態發生改變時, 所有依賴於它的對象都得到通知並自動進行事件處理。通過觀察者模式可以實現靈活的事件處理,使對象間的關係更加鬆散,便於系統的擴展和維護。
iLogtail實踐
文件採集場景可以認爲是觀察者模式比較典型的應用場景。爲了兼顧採集效率以及跨平臺的支持,iLogtail 採用了輪詢(polling)與事件(inotify)並存的模式,既藉助了inotify的低延遲與低性能消耗的特點,也通過輪詢的方式兼顧了運行環境的全面性。
iLogtail 內部以事件的方式觸發日誌讀取行爲。其中,polling 和 inotify 作爲兩個獨立模塊,分別將各自產生的 Create/Modify/Delete 事件,存入 Polling Event Queue和 Inotify Event Queue 中,並最終合併成一個統一的 Event Queue。
- 輪詢模塊由 DirFilePolling 和 ModifyPolling 兩個線程組成,DirFilePolling 負責根據用戶配置定期遍歷文件夾,將符合日誌採集配置的文件加入到 modify cache 中;ModifyPolling 負責定期掃描modify cache 中文件狀態,對比上一次狀態(Dev、Inode、Modify Time、Size),若發現更新則生成modify event。
- inotify 屬於事件監聽方式,根據用戶配置監聽對應的目錄以及子目錄,當監聽目錄存在變化,內核會產生相應的通知事件。
最終,LogInput 模塊完成對 Event Queue 消費的消費,並交由 Event Handler 處理Create/Modify/Delete 等事件,進而進行實際的日誌採集。
責任鏈模式
模式簡介
責任鏈模式允許你將請求沿着處理者鏈進行發送。收到請求後, 每個處理者均可對請求進行處理, 或將其傳遞給鏈上的下個處理者。
責任鏈會將特定行爲轉換爲被稱作處理者的獨立對象。在一個冗長的流程中,每個步驟都可被抽取爲僅有單個方法的類, 並執行操作,請求及其數據則會被作爲參數傳遞給該方法。
iLogtail實踐
iLogtail 中的數據處理 Pipeline,是非常經典的責任鏈模式。插件系統目前的主體由 Input、Processor、Aggregator 和 Flusher 四部分組成,其中 Processor 作爲處理層,可以對輸入的數據進行過濾,比如檢查特定字段是否符合要求或是對字段進行增刪改。每一個配置可以同時配置多個 Processor,它們之間採用串行結構,即上一個 Processor 的輸出作爲下一個 Processor 的輸入,最後一個 Processor 的輸出會傳遞到 Aggregator。
備忘錄模式
模式簡介
備忘錄模式允許在不暴露對象實現細節的情況下,捕獲一個對象的內部狀態,並在該對象之外保存這個狀態,便於後來將該對象恢復到原先保存的狀態。
備忘錄模式主要有以下幾個組成部分:
- 發起人類(Originator):主要記錄當前時刻的內部狀態,並且負責定義哪些是屬於備份範圍的狀態,負責創建和恢復備忘錄數據。
- 備忘錄類(Memento):負責存儲發起人對象的內部狀態,並且在需要的時候向發起人提供需要的內部狀態。
- 管理類(Caretaker):備忘錄的管理類,保存和提供備忘錄。但不能對備忘錄的內容進行訪問與修改。
iLogtail實踐
日誌採集場景下最重要的特性是保證日誌不丟。iLogtail 通過 Checkpoint 機制,及時將文件採集的狀態備份到本地磁盤,保證在極端場景下數據的可靠性。兩個比較典型的應用場景:
- 採集配置更新/進程升級
配置更新或進行升級時需要中斷採集並重新初始化採集上下文,iLogtail需要保證在配置更新/進程升級時,即使日誌發生輪轉也不會丟失日誌。
解決思路:爲保證配置更新/升級過程中日誌數據不丟失,在 iLogtail 在配置重新加載前或進程主動退出前,會將當前所有采集的狀態保存到本地的 checkpoint 文件中;當新配置應用/進程啓動後,會加載上一次保存的 checkpoint,並通過 checkpoint 恢復之前的採集狀態。
- 進程crash、宕機等異常情況
在進程crash或宕機時,iLogtail需要提供容錯機制,不丟數據,儘可能地少重複採集。
解決思路:進程 crash 或宕機沒有退出前記錄 checkpoint 的時機,因此 iLogtail 還會定期將採集進度dump到本地:除了恢復正常日誌文件狀態外,還會查找輪轉後的日誌,儘可能降低日誌丟失風險。
迭代器模式
模式簡介
迭代器模式提供一種在不暴露對象的內部細節的前提下,訪問對象中各個元素的方法。
iLogtail實踐
Golang 插件使用 LevelDB 進行一些上下文資源的備份,並基於迭代器模式恢復數據。
// Iterator iterates over a DB's key/value pairs in key order.
type Iterator interface {
CommonIterator
// Key returns the key of the current key/value pair, or nil if done.
// The caller should not modify the contents of the returned slice, and
// its contents may change on the next call to any 'seeks method'.
Key() []byte
// Value returns the key of the current key/value pair, or nil if done.
// The caller should not modify the contents of the returned slice, and
// its contents may change on the next call to any 'seeks method'.
Value() []byte
}
總結
行爲模式主要關注對象之間的通信和交互的方式和模式。
- 觀察者模式:定義了一種一對多的依賴關係,當一個對象的狀態發生變化時,其所有依賴者都會得到通知並自動更新。
- 職責鏈模式:將請求的發送者和接收者解耦,使多個對象都有機會處理該請求,直到其中一個對象處理成功爲止。
- 備忘錄模式:允許在不暴露對象實現細節的情況下保存和恢復對象之前的狀態。
- 迭代器模式:提供一種統一的方式來訪問聚合對象中的各個元素,而不需要暴露其內部結構。
參考:
C++ 常用設計模式:https://refactoringguru.cn/desi
作者|燁陌
點擊立即免費試用雲產品 開啓雲上實踐之旅!
本文爲阿里雲原創內容,未經允許不得轉載。