在 Puppet 運行過程之中,第一個提到的組件就是?agent
?進程。歷史上,這是一個獨立的稱為puppedd
?的可執(zhí)行程序,但在 2.6 中,我們把 Puppet 變成了一個唯一的可執(zhí)行程序,現(xiàn)在,可以使用?puppet agent
?來調(diào)用它,和 Git 的使用方式很類似。這個代理本身的功能不多,主要是用于實現(xiàn)上面圖中客戶端的工作流的配置和代碼。
在 agent 之后的下一個組件是一個稱為 Facter 的外部工具,這是一個用于檢測本機信息的非常簡單的工具。這些信息包括操作系統(tǒng)、IP 地址、主機名等,但 Facter 非常易于擴展,很多機構(gòu)在使用中會增加它們自己的插件來檢測一些個性化信息。Facter 發(fā)現(xiàn)的信息會由 agent 發(fā)送給服務(wù)器,之后,服務(wù)器就接過了接力棒,繼續(xù)工作流。
在服務(wù)端,第一個遇到的組件稱為外部節(jié)點分類器(External Node Classifier),簡稱為 ENC。ENC 接受主機名作為輸入,返回一個包含對應(yīng)主機的高級配置信息的數(shù)據(jù)結(jié)構(gòu)。ENC 通常是一個獨立的服務(wù)或程序:可能是一個其他的開源項目,比如 Puppet Dashboard 或 Foreman,或者是繼承的已有的數(shù)據(jù)存儲,比如 LDAP。ENC 的目的是,確定一臺主機從功能上屬于那些類,以及需要用哪些參數(shù)來配置這些類。比如,一個給定的主機可能屬于?debian
?和?webserver
?類,datacenter
?參數(shù)應(yīng)該設(shè)置為?atlanta
。
注意,在 Puppet 2.7 中,ENC 不是一個必選組件,用戶可以在 Puppet 代碼中直接指定節(jié)點的配置。大約在 Puppet 項目開始兩年之后,ENC 才被添加進來,因為我們開始意識到,對節(jié)點的功能進行劃分和配置節(jié)點從本質(zhì)上說是不同的兩件事,將它們劃分到兩個不同的程序中可能比擴展語言來支持兩種功能要更合理。盡管不是必須,但 ENC 仍然是推薦配置,并且將來某天可能會成為必選組件(到那時,Puppet 會提供一個擁有足夠必備功能的 ENC,不必?fù)?dān)心)。
一旦服務(wù)器收到 ENC 的分類信息,或是來自 Facter (經(jīng)過 agent)的系統(tǒng)信息后,它會將所有信息綁定到一個節(jié)點對象上,并將它送入編譯器。
如前所述,Puppet 有一個自定義的語言,用于指定系統(tǒng)配置。它的編譯起實際上有三個部分:一個 Yacc 風(fēng)格的分析器生成器和一個定制的詞法分析器;一組用于創(chuàng)建我們的抽象語法樹(AST)的類;以及 Compiler 類,它用于處理所有這些類,以及實現(xiàn)編譯器作為系統(tǒng)的一部分所提供的API的函數(shù)之間的交互。
編譯器要處理的最復(fù)雜的事情是,大部分 Puppet 配置代碼是在第一次被引用時才延遲加載的(減少加載時間,同時避免缺少一些實際不必要的依賴資源時產(chǎn)生的無關(guān)日志),這意味著不會有顯式的調(diào)用來加載并分析代碼。
Puppet 的解析器使用了一個使用開源的?Racc?構(gòu)建的正常的?Yacc 風(fēng)格的解析器生成器。可不幸的是,在 Puppet 項目開始時,沒有可用的詞法分析器生成器,所以只好使用了一個定制的詞法分析器。
因為我們在 Puppet 中使用了 AST,所以 Puppet 語法中的每個語句都可以被求值為 Puppet AST 類的一個實例(Puppet::Parser::AST::Statement
),這些 AST 實例不會被直接執(zhí)行操作,而會被放入一個語法樹之中,被一起執(zhí)行。當(dāng)一個服務(wù)器為很多不同節(jié)點服務(wù)時,使用 AST 會帶來一些性能上的收益,因為這樣可以一次解析,多次編譯。同時這也給了我們一個機會,來對 AST 進行一些內(nèi)?。╥ntrospection),讓我們得到一些額外的信息和能力,如果直接解析執(zhí)行是無法得到這些的。
在 Puppet 項目開始時,可參考的 AST 的例子并不多,這部分已經(jīng)經(jīng)過了很多的演化,發(fā)展到現(xiàn)在,我們的形式看起來是比較獨一無二的。我們不會直接針對整個配置生成一個單獨的AST,相反,我們創(chuàng)建很多小的 AST,按照名字切開。比如,如下代碼:
class ssh {
package { ssh: ensure => present }
}
會創(chuàng)建一個新的 AST,包含一個?Puppet::Parser::AST::Resource
?實例,并將這個 AST 命名為 "ssh",存儲在存儲這個特定環(huán)境的所有類的哈希表中。(這里略過了構(gòu)建類的細(xì)節(jié),不過對于這里的討論來說,這些是不必要的。)
給定 AST 和(來自 ENC 的)Node 對象,編譯器取出 node 對象(如果存在的話)指定的類,查找并進行求值。在這個求值的過程中,編譯器構(gòu)建了不同不同的域的樹,每個類有自己的作用域。這意味著 Puppet 是動態(tài)作用域的:如果一個 class include 了另一個類,那么里面的類就可以訪問外面類的變量。這的確是一個噩夢,我們正在著手消除這個問題。
作用域樹是臨時數(shù)據(jù)結(jié)構(gòu),一旦編譯完成就會被釋放,但編譯的輸出也隨著編譯的過程逐漸完成。我們把這個輸出產(chǎn)品稱為 Catalog(目錄),但它實際是一張資源和它們的關(guān)系構(gòu)成的圖。變量、控制結(jié)構(gòu)或是函數(shù)都不會存在在 catalog 之中,catalog 是純數(shù)據(jù),并可以被轉(zhuǎn)化為 JSON、YAML 或其他各種格式。
在編譯過程中,我們會創(chuàng)建一些包含(containment)關(guān)系,一個類"包含(contains)"類中定義的所有資源(比如, 前面的例子中,ssh 類包含 ssh 包)。類可以包含一個定義,而這個定義本身也可以包含一個或多個定義,或其他獨立的資源。一個 catalog 傾向于一個扁平的、彼此無連接的圖:很多類,每個都有少數(shù)的幾個層次。
這種圖的一個別扭的方面是,它還包含了“依賴(dependency)”關(guān)系,比如一個服務(wù)依賴于一個包(可能因為安裝了包才能創(chuàng)建服務(wù)),但這些依賴關(guān)系是由資源的參數(shù)指定的,而非圖結(jié)構(gòu)的邊。我們的圖類(由于歷史原因,稱為?SimpleGraph
)不支持在同一張圖里同時有“包含”邊和“依賴”邊,所以,我們不得不為了不同的需求在它們之間來回轉(zhuǎn)換。
一旦 catalog 完全構(gòu)建好了(假設(shè)沒有失?。?,就會送給 Transaction。在一個區(qū)分客戶機和服務(wù)器的系統(tǒng)中,Transaction 運行在客戶機上,如圖 18.2,它通過 HTTP 協(xié)議下載 Catalog。
Puppet 的 transaction 類提供了實際進行系統(tǒng)修改操作的框架,而我們討論過的其他東西都構(gòu)建于其上或是進行對象的分發(fā)傳遞。和數(shù)據(jù)庫之類的一般系統(tǒng)中的事務(wù)不同 Puppet transaction 的行為并不具有原子性等特征。
transaction 的工作相當(dāng)直接:在圖中按照各種關(guān)系指定的順序進行遍歷,并確保各種資源保持同步。正如上面提到的,它不得不將圖從包含邊(比如?Class[ssh]
?包含?Package[ssh]
)轉(zhuǎn)換為依賴邊(比如?Service[ssh]
?依賴于?Package[ssh]
),然后對圖進行標(biāo)準(zhǔn)的拓?fù)渑判颍错樞蜻x擇每種資源。
對于給定的資源,我們進行簡單的三步操作:獲取資源的當(dāng)前狀態(tài),與期望的狀態(tài)進行比較,進行必要的改動,以滿足期望。比如,有如下代碼:
file { "/etc/motd":
ensure => file,
content => "Welcome to the machine",
mode => 644
}
transaction 會檢查?/etc/motd
?的內(nèi)容,如果和指定的狀態(tài)不匹配的話,會修復(fù)不一致的地方。如果 /etc/motd 是一個目錄,那么它會備份其中的文件,再刪除目錄,然后創(chuàng)建一個內(nèi)容和權(quán)限都符合要求的文件。
進行操作的過程實際上是通過一個簡單的?ResourceHarness
?類來控制的,這個類定義了事務(wù)和資源之間的接口。這樣做可以減少類之間的連接,并可以讓他們互相獨立地進行修改。
事務(wù)類是 Puppet 完成工作的核心,但所有的工作實際都是由資源抽象層(RAL)來完成的,從架構(gòu)上講,這一層也是 Puppet 中最有意思的組件。
RAL 是 Puppet 中創(chuàng)建的第一個組件,與語言部分不同,這部分對用戶能做的事情進行了清晰的定義。RAL 的工作就是定義一個資源究竟是什么,要實現(xiàn)一個資源需要在系統(tǒng)中進行什么操作,而 Puppet 語言正是用來操作由 RAL 建模的資源的。正因如此,RAL 也是系統(tǒng)中最重要和最難改動的組件。我們希望能修改 RAL 中的很多東西,而且也在過去的多年中進行了很多重大改進(最難的莫過于增加 Provider 了),但是在 RAL 中,還是有很多工作需要在日后慢慢修改。
在編譯器子系統(tǒng)中,我們將資源和資源類型分別進行了建模(分別命名為?Puppet::Resource
?和 Puppet::Resource::Type)。我們的目標(biāo)是讓這些類也成為 RAL 的核心,不過,目前這兩種行為(資源和類型)被封裝到了同一個類之中 ——?Puppet::Type
。(這個類的命名十分糟糕,這是因為定義這個類的時間遠(yuǎn)早于我們開始使用“資源”這個名詞的時間,在那時,我們在主機間進行數(shù)據(jù)通信時,是直接對內(nèi)存類型進行序列化的,事到如今,想要再去重新調(diào)整命名已經(jīng)非常困難了。)
當(dāng)?Puppet::Type
?被最早設(shè)計出來的時候,似乎把資源和類型的行為放到同一個類里是有道理的,畢竟資源是資源類型的實例。但隨著時間的推移,越來越發(fā)現(xiàn),資源及其類型不適合于放在一個傳統(tǒng)的繼承關(guān)系構(gòu)成的模型里。比如,資源類型定義了資源可以有哪些參數(shù),但不管資源接受哪些參數(shù)(可能全部接受)。這樣,我們的?Puppet::Type
?就擁有了類級別的行為——規(guī)定資源類型的行為,和實例級別的行為——規(guī)定資源實例如何行為。同時,它還負(fù)責(zé)管理注冊和獲取資源類型的功能,如果你需要 "user" 類型,你可以調(diào)用?Puppet::Type.type(:user)
.
這種混合的行為導(dǎo)致了?Puppet::Type
?不太容易維護。整個類有不到 2000 行代碼,但卻在三個層面上工作 —— 資源、資源類型,和組員類型管理器 —— 這讓它變得難以理解。這就是為什么這個模塊是重構(gòu)的主要目標(biāo)的原因,不過它本身更多的是拼接在一起的代碼,而不是面向用戶的設(shè)計,所以,修正它要比直接根據(jù)功能重寫更困難。
除了?Puppet::Type
?之外,RAL 中還有兩個重要的類,其中最有趣的一個我們稱之為 Provider。在 RAL 剛剛被開發(fā)出來時,每種資源都是由參數(shù)定義和如何管理它們的代碼混在一起構(gòu)成的。比如,我們要定義 "content" 參數(shù),然后提供一個方法來讀取文件的內(nèi)容,以及另一個用于修改內(nèi)容的方法:
Puppet::Type.newtype(:file) do
...
newproperty(:content) do
def retrieve
File.read(@resource[:name])
end
def sync
File.open(@resource[:name], "w") { |f| f.print @resource[:content] }
end
end
end
這是個簡化的例子(比如我們內(nèi)部實際使用的校驗和,而非讀取全部內(nèi)容),但這里可以大致了解處理思路。
這樣就讓事情變得非常難于管理了,因為我們需要為每個資源類型管理很多不同的屬性。目前 Puppet 支持超過 30 種包管理工具,這樣,很難在一個 Package 資源類型中去支持所有這些管理工具了。于是,我們提供了一種資源類型和管理相應(yīng)類型的資源的方法之間的一個清晰接口 —— 實際上,資源類型是指資源類型的名字和它們支持的屬性。 Provider 為所有的資源類型的屬性定義了 getter 和 setter 方法,以清晰直觀的方式命名。例如,這是一個 provider 實現(xiàn)上述屬性的代碼示例:
Puppet::Type.newtype(:file) do
newproperty(:content)
end
Puppet::Type.type(:file).provide(:posix) do
def content
File.read(@resource[:name])
end
def content=(str)
File.open(@resource[:name], "w") { |f| f.print(str) }
end
end
在這個簡單的例子里,似乎還多了一點代碼,但這更易于理解和維護,特別是當(dāng)屬性的數(shù)量或是屬性提供者的數(shù)量變多的時候。
本節(jié)開始處曾經(jīng)提到,Transaction 并不直接改動系統(tǒng),而是通過 RAL 來完成的。現(xiàn)在,我們可以清楚地看到,是 provider 來進行的這想具體工作。事實上,總體上講,provider 是 Puppet 當(dāng)中,唯一真正直接觸及系統(tǒng)的部分。transaction 請求文件內(nèi)容,provider 就會為它讀取,transaction 要求文件的內(nèi)容要被改動,provider 就去改動它。注意,盡管如此,provider 從不決定如何影響系統(tǒng) —— 如何影響系統(tǒng)這個問題是由 Transaction 來決定的,provider 僅僅是執(zhí)行任務(wù)。這種架構(gòu)可以讓 Transaction 能夠完全控制系統(tǒng),卻不需要了解文件、用戶和包這些具體細(xì)節(jié),而且,這個劃分可以讓 Puppet 可以擁有一個完全模擬執(zhí)行的模式,在這種模式下,我們可以保證系統(tǒng)完全不會受到任何影響。
RAL之中的另一個主要的類型負(fù)責(zé)參數(shù)本身。我們支持三種類型的參數(shù): metaparameters, 這種參數(shù)影響所有資源類型(比如,是否在模擬模式中運行);參數(shù),它們是不直接寫入到磁盤上的一些值(比如,是否在查找文件時進入符號鏈接);還有屬性(property),它們規(guī)范了你要修改磁盤上的資源的哪方面的內(nèi)容(比如文件的內(nèi)容,或者一個服務(wù)是否在運行著)。區(qū)分屬性和參數(shù)的不同十分困難,但你可以這么想,屬性是哪些 provider 中有 getter 和 setter 方法的,這樣就易于分辨了。
隨著 transaction 遍歷整張圖,并使用 RAL 來修改系統(tǒng)的配置,Puppet 同時也會同時生成一份報告。這份報告包含了在對系統(tǒng)應(yīng)用修改的過程中發(fā)生的事件。這些事件也完整地飯贏了工作進行的情況:它們會在資源改變時記錄時間戳,已有的值和新的值,以及所有產(chǎn)生的信息,以及變動成功或是失?。ɑ蛘邔嵲谀M運行模式)。
這些事件封裝在?ResourceStatus
?對象之中,映射到相應(yīng)的資源。這樣,對于一個給定的 Transaction,你可以知道其中運行的所有資源,這些改變是否成功,以及你可能希望知道的關(guān)于這些改動的元數(shù)據(jù)。
一旦 transaction 完成了,一些基本的性能參數(shù)也會被計算出來,并存儲到報告中,之后發(fā)送給服務(wù)器(如果配置了服務(wù)器的話)。當(dāng)報告被發(fā)送出去的時候,配置過程就完成了,agent 會回到睡眠模式,或者進程退出。
現(xiàn)在我們已經(jīng)從整體上理解了 Puppet 做了什么,如何做到的,值得再花一點看看其他部分了,這些部分并沒有顯示出什么過人之處,但對于完成工作也是十分必要的。
Puppet 的一個突出優(yōu)點是它非常易于擴展。在 Puppet 里,至少有 12 類擴展,大部分?jǐn)U展都可以被所有人使用。比如,你可以在這些方面寫出你自己的擴展:
不過,Puppet 的分布式本質(zhì)意味著 agent 需要某種方式來取回并加載新的擴展。為此,在每次 Puppet 啟動之前,第一件事請就是找到可用的服務(wù)器,下載所有 plugin。其中可能包括新的資源類型或 provider,新 facts,或者是新的報告處理器。
這意味著我們可以在不改動核心 Puppet 包的同時,升級大部分的 Puppet agent 功能。對于一些自定義的 Puppet 部署,這更是特別有用。
到目前為止,你可能已經(jīng)發(fā)現(xiàn),Puppet 的開發(fā)歷史中有一些壞名字的類,而對于大部分人來說,這一個是最無法容忍的。Indirector 是一個極具擴展性的控制反轉(zhuǎn)(IoC)框架??刂品崔D(zhuǎn)系統(tǒng)允許你講功能的開發(fā)和如何控制使用什么功能獨立開。在 Puppet 的例子中,這允許我們使用很多插件,來提供非常不同的功能,比如可以通過 HTTP 訪問編譯器,也可以直接在進程中加載,這些可以通過一個笑得配置改變而不需要修改代碼就可以實現(xiàn)。換句話說,按照 Wikipedia 的 “控制反轉(zhuǎn)” 頁面的描述,Puppet Indirector 是一個服務(wù)定位器的實現(xiàn)。所有從一個類到另一個類的切換都經(jīng)由 Indirector,通過一個標(biāo)準(zhǔn)的類 REST 接口完成(比如,我們支持 find, search, save 以及 destroy 方法),這樣,講 Puppet 從無服務(wù)器模式切到客戶機/服務(wù)器模式的操作,很大程度上說,是一個配置 agent 使用 HTTP 作為獲取 catalog 的方法,而非直接訪問 compiler 的問題。
因為作為一個控制反轉(zhuǎn)框架,Indirector 的配置必須嚴(yán)格地和代碼的路徑分開,這個類本身非常難于理解,特別是你在 debug 為什么使用給定的代碼路徑時。
Puppet 的原型寫于 2004 年,當(dāng)時的關(guān)于 RPC 的問題是 XMLRPC 與 SOAP 之爭。我們選擇了 XMLRPC,它工作得很好,但是有一個大部分其他方法都有的問題:不鼓勵在模塊間使用標(biāo)準(zhǔn)接口,而且對于獲取簡單的一個結(jié)果這樣的操作有些過于復(fù)雜了。因為 XMLRPC 編碼的需要,導(dǎo)致了幾乎每個對象都在內(nèi)存中至少出現(xiàn)兩次,對于大文件來說代價很高,我們也為此遇到過很嚴(yán)重的內(nèi)存問題。
從 0.25 發(fā)布開始(開始于 2008 年),我們開始了將網(wǎng)絡(luò)通信向類 REST 模型遷移的過程,但我們沒有直接修改網(wǎng)絡(luò),而是選擇了一種更復(fù)雜的方案。我們開發(fā)了 Indirector 作為組件間通信的標(biāo)準(zhǔn)框架,然后構(gòu)建了一個 REST 端點作為一個可選方案。我們用了兩個 Release 來完整支持 REST,目前還沒有完全完成(從使用 YAML)到使用 JSON 作為序列化方案的轉(zhuǎn)換。我們進行 YAML 到 JSON 的轉(zhuǎn)換有兩點主要的原因:首先,Ruby 之中,處理 YAML 非常慢,而 Ruby 處理 JSON 則快很多;其次,大部分 web 應(yīng)用都轉(zhuǎn)向了 JSON,這讓 JSON 看起來更加可移植一些。當(dāng)然,對于 Puppet 的情況來說,使用 YAML 并非是為了語言間可移植性考慮的,,而且 YAML 配置對于不同版本的 Puppet 可能都經(jīng)常不兼容,因為它本質(zhì)上是用來序列化 Ruby 內(nèi)部對象的。
我們的下一個主版本更新將完全移除 XMLRPC 的支持。
從實現(xiàn)的角度講,我們對于 Puppet 中實現(xiàn)的各種解耦非常自豪:描述語言與 RAL 是完全解耦的,Transaction 無法直接訪問系統(tǒng),而 RAL 本身不會做任何策略判斷。這些抽象和解耦讓應(yīng)用的開發(fā)者可以更加專注于工作流的開發(fā),并可以獲得很多關(guān)于發(fā)生了什么、為什么發(fā)生的信息。
Puppet 的可擴展性和可配置性也是它的一個主要優(yōu)點,任何人都可以在 Puppet 的基礎(chǔ)上,輕易地開發(fā)應(yīng)用而無需修改其內(nèi)核。我們也使用提供給用戶的同樣的接口來開發(fā)各種功能。
Puppet 的簡單和易用性一直是它的主要優(yōu)點。雖然讓它跑起來還是有點困難,不過可以強出市場上的其他產(chǎn)品好幾里地了。這些簡單性是以增加了很多工程量為代價的,特別是在維護和更多的設(shè)計工作方面的工作量,但是,如果能讓用戶可以更加集中在他們的問題上,而非工具上的話,這些開銷也是值得的。
Puppet 的可配執(zhí)性是個非常好的特性,不過我們做得好像有點過了。你可以有很多方法來讓 Puppet 工作起來,并且,在 Puppet 上太容易構(gòu)建工作流了,這在有的時候可能讓人趕到困窘。我們的一個短期目標(biāo)是,減少你可以調(diào)整的 Puppet 配置,避免用戶很容易地把 Puppet 調(diào)壞,而且,這樣我們在升級的時候也可以考慮更少的邊界情況了。
我們的改變也有些慢了。有很多重構(gòu)都已經(jīng)等待數(shù)年卻仍然沒有進行。對用戶來說,短期內(nèi)這意味著更穩(wěn)定的系統(tǒng),但卻更難于維護,而且用戶也更難于向社區(qū)回饋代碼。
最后,我們花費了很長時間來認(rèn)識到,描述我們的設(shè)計語言的最恰當(dāng)?shù)脑~就是簡單性。我們現(xiàn)在已經(jīng)開始在考慮簡單性之外的設(shè)計目標(biāo)了,我們開始采用更好的決策框架來決定是否增加或移除特性,開始通過考慮背后的深層次原因來做出決定。
Puppet 是一個簡單的系統(tǒng),也是一個復(fù)雜的系統(tǒng)。它由很多的部分組成,但各個部分之間的耦合非常松,每個部分從2005年至今都發(fā)生了很多變化。它是一個可以用于處理各種配置問題的框架,但作為一個應(yīng)用,它非常簡單易用。
在未來,我們的成功將依賴于更加堅實、更加簡單的框架,并讓應(yīng)用在增強能力的同時,保持易用性。
更多建議: