提供:ZStack云計算
本教程為CoreOS上手指南系列九篇中的第六篇。
CoreOS能夠利用一系列工具以集群化與Docker容器化方式簡化服務管理工作。其中etcd負責將各獨立節點聯系起來并提供全局數據平臺,而大部分實際服務管理任務則由fleet守護進程實現。
在上一篇教程中,我們了解了如何利用fleetctl命令操縱服務及集群成員。在今天的教程中,我們將了解如何利用單元文件定義服務。
在接下來的內容中,我們將探討如何構建fleet單元文件,外加在生產環境下提升服務健壯性的可行方法。
要完成本篇教程,大家首先需要擁有一套可用CoreOS集群。
在之前的系列教程如何利用DigitalOcean創建一套CoreOS集群當中,我們已經完成了集群創建工作。
此集群配置包含三個節點。它們能夠利用專有網絡接口實現彼此通信。這三臺節點亦擁有公共接口以運行公共服務。各節點名稱分別為:
coreos-1coreos-2coreos-3雖然本教程中的大部分內容在于介紹單元文件的創建,但上述設備也將在后文說明特定指令的調度作用時被用到。
這里還建議大家參閱如何使用fleetctl一文。只有具備了fleetctl的相關知識,大家才能提交并在集群當中使用這些單元文件。
滿足上述要求后,我們這就開始今天的學習之旅。
由于fleet服務管理機制在很大程度上依靠本地系統的systemd init系統,因此systemd單元文件自然被用于定義各項服務。
除了服務類型之外,還有其它多種單元類型定義,且通常被作為systemd單元文件中的一個子集。每種類型都會通過一段文件后綴進行類型定義,例如example.service:
service: 這是最常見的單元文件類型。其用于定義能夠運行在集群中某臺設備上的服務或者應用程序。socket: 定義關于嵌套或者嵌套類文件。其中包括網絡嵌套、ipC嵌套以及FIFO緩沖。當流量指向該文件時,它們負責調用服務以實現啟動。device: 定義與udev設備樹中可用設備相關的信息。Systemd會根據需求在各設備上根據udev規則創建device單元。其常被用于進行問題排序,從而在實際啟動之前確保設備切實可用。mount: 定義與設備安裝點相關的信息。其名稱位于所引用安裝點之后,且以破折號替代斜杠。automount: 定義掛載點。其與mount單元采用同樣的命名方式,且必須配合相關的mount單元。其用于描述按需與并發安裝。timer: 定義一個與其它單元關聯的計時器。當此文件中設定的時間點被觸發時,該相關單元即開始啟動。path: 定義一條路徑,其可被監控以進行基于路徑的激活操作。我們可以利用它在特定路徑發生變更時啟動其它單元。盡管大家可以隨意選擇上述選項,但service單元仍是最為常用的條目。在本教程中,我們將單純探討service單元的配置。
單元文件為簡單的文本文件,且結尾為“.”加上以上后綴之一。文件內容由多處區段組成。在fleet當中,大部分單元文件將采用以下基本格式:
[Unit]generic_unit_directive_1generic_unit_directive_2[Service]service_specific_directive_1service_specific_directive_2service_specific_directive_3[X-Fleet]fleet_specific_directive區段標題及單元文件中的其它內容皆區分大小寫。其中[Unit]用于定義單元的常規信息。以上與各單元類型相關的選項皆可添加在這里。
[Service]區段用于設定指向各服務單元的指令。大多數(但并非全部)單元類型都與unit-type-specific區段信息相關聯。大家可以參閱常見systemd單元文件man頁面以了解更多與不同單元類型相關的細節。
[X-Fleet]區段用于設定該單元的調度要求以供fleet使用。利用此區段,大家可以要求某特定條件為御前 以在主機上實現單元調度。
在這一區段中,首先對在CoreOS上運行服務中使用過的單元文件進行調整。該文件名為apache.1.service,且內容如下:
[Unit]Description=Apache web server service# RequirementsRequires=etcd.serviceRequires=docker.serviceRequires=apache-discovery.1.service# Dependency orderingAfter=etcd.serviceAfter=docker.serviceBefore=apache-discovery.1.service[Service]# Let PRocesses take awhile to start up (for first run Docker containers)TimeoutStartSec=0# Change killmode from "control-group" to "none" to let Docker remove# work correctly.KillMode=none# Get CoreOS environmental variablesEnvironmentFile=/etc/environment# Pre-start and Start## Directives with "=-" are allowed to fail without consequenceExecStartPre=-/usr/bin/docker kill apacheExecStartPre=-/usr/bin/docker rm apacheExecStartPre=/usr/bin/docker pull username/apacheExecStart=/usr/bin/docker run --name apache -p ${COREOS_PUBLIC_IPV4}:80:80 /username/apache /usr/sbin/apache2ctl -D FOREGROUND# StopExecStop=/usr/bin/docker stop apache[X-Fleet]# Don't schedule on the same machine as other Apache instancesX-Conflicts=apache.*.service我們首先從[Unit]區段入手。在這里,我們的基本思路是描述該單元并添加關聯性信息。首先設定相關要求。在本示例中,我們將使用部分硬性約束條件。如果我們希望fleet嘗試啟動額外服務,但又不會因故障而導致流程中斷,則可使用Wants指令。
之后,我們需要明確列出要求的先后順序。這一點非常重要,因為某些要求需要以特定服務正在運行為前提。另外,我們也可以在這里自動利用etcd聲明我們將要構建的服務。
在[Service]區段中,我們關閉服務啟動超時機制。由于服務首次在主機上運行時,容器需要自Docker注冊表中提取信息,而這往往會造成啟動超時。其默認時長為90秒,一般來說應該是足夠的,但較為復雜的容器可能需要更長的啟動時間。
而后將killmode設置為none。這是因為正常的關閉模式(control-group)有時候會導致容器移除命令失效(特別是Docker的–rmoption)。這有可能在下一次重啟時帶來問題。
我們可以在此環境文件中找到COREOS_PUBLIC_IPV4以及COREOS_PRIVATE_IPV4環境變量(如果創建過程中啟用了專有網絡機制)。我們可以利用其特定主機信息輕松配置Docker容器。
ExecStartPre各行用于清空此前尚未運行的部分以實現執行環境清理。我們可以在前兩行中使用=-以確保出現問題時,systemd會忽略錯誤并繼續執行后續命令。這一點非常重要,因為我們的預啟動操作基本上清除了任何先前正在運行的服務。如果找不到已經在運行的服務,則操作自然會失敗——但由于其僅僅屬于清理流程,因此我們不希望其影響到服務的正常執行。最后的pre-start用于確保容器始終運行最新版本。
這條啟動命令會引導Docker容器并將其與主機設備的公共IPv4接口相綁定。其使用此環境文件中的信息并輕松實現接口與端口交換。這一流程采用前臺運行方式,這是因為該容器會在運行中進程結束后退出。而stop命令則嘗試對容器進行正常關閉。
[X-Fleet]區段包含一條簡單狀態,用于強制fleet在尚未運行其它Apache服務的主機上調度該服務。這是一種簡單的服務高可用性實現方式,即強制要求服務啟動在不同設備上。
在以上示例中,我們已經進行了一些比較基礎的配置。不過還有其它一些需要關注的重點。
下面來看構建主服務中需要注意的幾點:
對關聯性與排序邏輯進行區分:利用Requires=或者Wants=指令進行關聯性排布,具體取決于如果不填寫此關聯性,該單元是否會失敗。排序區分則可使用After=以及Before=行實現,這樣我們就能輕松判斷要求是否出現變更。將關聯性列表與排序區分開來能幫助我們調試關聯性問題。利用獨立進程處理服務注冊:我們的服務應當利用etcd進行注冊以發揮服務發現機制的使用,并借此實現動態配置功能。不過,我們應當使用單獨的“sidekick”容器以保持邏輯獨立性。這樣從外部視角來看,其才能提供更為明確的服務運行狀態報告,并供其它組件加以參考。關注服務超時可能性:考慮調整TimeoutStartSec指令以允許更長的啟動時間。將其設置為“0”則會禁用啟動超時機制。我們建議這樣設定,因為Docker往往需要從注冊表中提取鏡像(在首次運行或者發現更新時),這可能顯著增加服務的初始化時長。如果服務未能徹底停止,請調整KillMode:如果我們的服務或者容器似乎無法徹底停止,請考慮使用KillMode選項。將其設置為“none”有時候能夠解決容器停止后未被移除的問題。特別是在對容器進行命名時,由于Docker會因為存在重名容器而出現錯誤。查閱KillMode說明文檔以獲取更多信息。在啟動前清理運行環境:與上一條相關,請確保在每次啟動前清除環境中的全部既有Docker容器。這些清理命令行需要使用=-標記以保證不需要清理時仍能執行下一步操作。雖然我們一般都會使用docker stop對容器進行正常停止,但也可以使用docker kill以確保其被徹底清除。引入并使用特定主機信息以實現服務可移植性:如果大家需要將服務綁定至特定網絡接口,請引入/etc/environment文件以訪問COREOS_PUBLIC_IPV4以及COREOS_PRIVATE_IPV4。如果我們需要查看當前運行中服務所在設備的主機名稱,則可使用%H系統標記。要了解更多可用標記信息,請參閱systemd標記說明文檔。在[X-Fleet]區段中,只能使用%n、%N、%i與%p。現在我們已經掌握了如何構建主服務,接下來需要了解傳統的“sidekick”服務。這些sidekick服務與主服務相關聯,且可通過etcd作為注冊服務的外部點。
這個被引用于主單元文件中的文件名為apache-discovery.1.service,內容如下:
[Unit]Description=Apache web server etcd registration# RequirementsRequires=etcd.serviceRequires=apache.1.service# Dependency ordering and bindingAfter=etcd.serviceAfter=apache.1.serviceBindsTo=apache.1.service[Service]# Get CoreOS environmental variablesEnvironmentFile=/etc/environment# Start## Test whether service is accessible and then register useful informationExecStart=/bin/bash -c '/while true; do /curl -f ${COREOS_PUBLIC_IPV4}:80; /if [ $? -eq 0 ]; then / etcdctl set /services/apache/${COREOS_PUBLIC_IPV4} /'{"host": "%H", "ipv4_addr": ${COREOS_PUBLIC_IPV4}, "port": 80}/' --ttl 30; /else / etcdctl rm /services/apache/${COREOS_PUBLIC_IPV4}; /fi; /sleep 20; /done'# StopExecStop=/usr/bin/etcdctl rm /services/apache/${COREOS_PUBLIC_IPV4}[X-Fleet]# Schedule on the same machine as the associated Apache serviceX-ConditionMachineOf=apache.1.serviceSidekick服務的實現方式與主服務基本一致。我們首先描述該單元的作用,而后為其提供關聯性信息與排序邏輯。
這里要用到的新命令為BindsTo=。此命令會使該單元逐步執行啟動、停止以及重啟操作?;旧?,這意味著我們能夠在兩個單元被載入至fleet當中后,通過操縱主單元同時管理這兩個單元。這是一種單向機制,所以對sidekick單元進行控制不會影響到主單元。
在[Service]區段中,我們再次查找/etc/environment文件中的變量。此時ExecStart=指令基本上屬于一條簡短的bash腳本。它會嘗試利用已經聲明的接口與端口接入主服務。
如果連接成功,那么etcdctl命令會在etcd內的/services/apache中利用主機設備的公共IP地址設定一個鍵。該鍵的值為JSON對象,其中包含與該服務相關的信息。此鍵的過期時長為30秒,因此如果該單元意外停止,則對應服務信息亦不會駐留在etcd中。如果連接失敗,那么該鍵會被立即移除——這是因為該服務無法被驗證為可用。
此循環中包含一條20秒的sleep命令。這意味著每20秒(早于etcd的30秒鍵超時設定),此單元就會重新檢查其主單元是否可用并重置該鍵。這基本上相當于對鍵上的TTL進行刷新,使其在接下來30秒中繼續生效。
在這種情況下,stop命令只用于手動移除該鍵。意味著該服務注冊將在主單元的stop命令由于BindsTo=指令而被鏡像至此單元時被移除。
在[X-Fleet]區段中,我們需要確保此單元與主單元啟動在同一臺服務器上。盡管這套方案不允許該單元向遠程設備報告服務可用性,但在本示例中,最重要的是確保BindsTo=指令正常起效。
在構建sidekick單元時,我們應當認真考慮以下幾項要求:
檢查主單元的實際可用性:檢查主單元運行狀態非常重要。不要僅靠sidekick能夠正常初始化就認為主單元可用。雖然實際可用性取決于主單元的設計與功能,但檢查機制越強大,注冊狀態的可靠性就越高。這樣的檢查工作應該擁有廣泛的涵蓋面,從檢查/health端點到嘗試利用客戶端接入數據庫。定期對注冊邏輯進行重新檢查:檢查服務可用性當然很重要,但定期進行復查同樣重要。我們可以借此發現意料之外的服務故障,特別是在容器中的某些結果仍在繼續運行時。在不同周期間進行暫停,并借此對主單元上的其它負載進行快速審查。使用TTL標記通過etcd對故障進行自動注銷:sidekick單元中的意外故障可能導致etcd內信息與實際信息間發生沖突。為了避免這種沖突,我們應當允許鍵超時。利用以上循環間隔機制,我們可以在各鍵超時前對其進行刷新,從而確保其永遠不會在sidekick正常運行時過期。這種休眠間隔應被設定為略短于超時間隔,從而確保功能正常起效。利用etcd注冊可用信息,而非單純確認:在對sidekick進行首次迭代時,我們往往只關注單元啟動時是否確切注冊至etcd。然而其中也包含有大量可資其它服務利用的信息。雖然我們目前并不需要這些信息,但其可能會在構建其它組件時發揮作用。Etcd服務屬于一項全局性鍵-值存儲機制,因此別忘記利用它提供鍵信息。以JSON對象方式存儲細節是傳遞多條信息的好辦法。掌握了以上幾條要點,我們就能構建強大的注冊單元,同時確保etcd擁有正常的信息可以使用。
雖然fleet單元文件與其它常見systemd單元文件并沒有太大區別,但其中仍存在著一些值得關注的特性與陷阱。
其中最大的區別就是[X-Fleet]的存在,其可用于指引fleet制定調度決策。具體可用選項包括:
X-ConditionMachineID: 其可用于指定某臺設備以進行單元加載。其提供的值為完整的設備ID。此值可由集群內單一成員通過檢查/etc/machine-id文件進行檢索,或者通過fleetctl的list-machines -l命令查看。其必須使用完整的ID字符串。如果大家是在特定設備上運行一套數據庫及其數據目錄,那么這一要求就非常必要了。當然,在非必要情況下不推薦大家使用它,因為其會嚴重影響到單元的靈活性。X-ConditionMachineOf: 用于將當前單元調度至加載有其它特定單元的設備之上。其可用于調度sidekick單元或者將多個單元聯系在一起。X-Conflicts: 與上一條相反,其限定當前單元文件不可被調度至特定設備上。我們可以利用它輕松實現高可用性配置,即在不同設備上運行同一服務的多個版本。X-ConditionMachineMetadata:用于根據當前可用設備的元數據指定調度要求。在fleetctl list-machines輸出結果中的“METADATA”列當中,大家可以看到每臺主機所設定的元數據。要設置元數據,可在服務器實例初始化時將其添加至cloud-config文件中。Global: 這是一條特殊的指令,會利用一條boolean參數標記目標單元是否應被調度至集群內的全部設備上。我們應當僅使用此指令處理元數據狀態。這些額外指令允許管理員更加靈活且有效地定義服務在可用設備上的運行方式。我們需要對其進行預先評估,而后方可在fleetctl加載階段中將其傳遞至特定設備的systemd實例中。
這就帶來了fleet中需要注意的另一項要點。事實上,fleetctl工具并不會對單元文件內[X-Fleet]區段之外的關聯性要求進行評估。這意味著fleet中的各單元可能在協作時引發某些有趣的問題。
具體來講,當fleetctl工具采取必要步驟以將目標單元操作至所需狀態時,其不會考慮到該單元的關聯性需求。
因此,如果我們分別提交了主與sidekick單元,但尚未在fleet中完成加載,那么輸入fleetctl start main.service將加載并嘗試啟動main.serivce單元。然而,由于sidekick.service單元尚未被加載,而fleetctl又不會在加載與啟動過程中評估關聯性,因此main.service單元會發生錯誤。這是因為一旦設備上的systemd實例開始處理main.service單元,將無法在關聯性評估階段找到sidekick.service。
為了避免這種情況,我們可以同時手動啟動這些服務,而非領先BindsTo=指令將sidekick引入運行狀態:
fleetctl start main.service sidekick.service另一種方式則是確保sidekick單元一定會在主單元運行時進行加載。其載入過程由設備選擇,而該單元文件則被提交至本地systemd實例當中。這樣能夠確保關聯性得到滿意,而BindsTo=指令也能夠正常執行以啟動該輔助單元:
fleetctl load main.service sidekick.servicefleetctl start main.service因此當fleetctl命令報錯時,大家可以從以上幾個角度進行排查。
在使用fleet時,其最為強大的概念之一就是單元模板。單元模板依賴于systemd下一種名為“實例”的特性。它們屬于實例化的單元,通過處理模板單元文件被創建在運行時當中。模板文件在很大程度上類似于常規單元文件,只是其中存在幾處小小的修改。如果得到正確利用,其將發揮巨大作用。
模板文件可通過在文件名中使用@來標記。大多數常規服務的文件名格式為unit.service,而模板文件的名稱格式則為unit@.service。
當某單元利用模板進行實例化時,其實例標記符將位于@與.service后綴之間。此標記符可由管理員任意指定:
unit@instance_id.service此基礎單元名稱可通過%p標記符在單元文件之內進行訪問。同樣的,給定實例標記符則可通過%i進行訪問。
這意味著我們無需像之前那樣一步步創建apache.1.service主單元文件,而可以直接創建一套名為apache@.service的模板:
[Unit]Description=Apache web server service on port %i# RequirementsRequires=etcd.serviceRequires=docker.serviceRequires=apache-discovery@%i.service# Dependency orderingAfter=etcd.serviceAfter=docker.serviceBefore=apache-discovery@%i.service[Service]# Let processes take awhile to start up (for first run Docker containers)TimeoutStartSec=0# Change killmode from "control-group" to "none" to let Docker remove# work correctly.KillMode=none# Get CoreOS environmental variablesEnvironmentFile=/etc/environment# Pre-start and Start## Directives with "=-" are allowed to fail without consequenceExecStartPre=-/usr/bin/docker kill apache.%iExecStartPre=-/usr/bin/docker rm apache.%iExecStartPre=/usr/bin/docker pull username/apacheExecStart=/usr/bin/docker run --name apache.%i -p ${COREOS_PUBLIC_IPV4}:%i:80 /username/apache /usr/sbin/apache2ctl -D FOREGROUND# StopExecStop=/usr/bin/docker stop apache.%i[X-Fleet]# Don't schedule on the same machine as other Apache instancesX-Conflicts=apache@*.service如大家所見,我們將apache-discovery.1.service關聯性修改為apache-discovery@%i.service。這意味著如果我們擁有此單元文件的一個名為apache@8888.service的實例,那么其將需要一個名為apache-discovery@8888.service的sidekick單元。其中的%i已經被替換為實例標記符。在這種情況下,我們使用該標記符代表與當前所運行服務相關的動態信息,特別是Apache服務器的可用端口編號。
為了實現這一目標,我們可以變更docker的運行參數,從而將該容器的端口聲明至主機上的某個端口。在該靜態單元文件中,我們使用的參數為
Docker名稱本身也進行了修改,這樣它也會使用基于實例ID的惟一容器名稱。請注意,Docker容器無法使用@標記,因此我們必須在單元文件中選擇其它名稱。為此,我們修改了全部在Docker容器上運行的指令。
在[X-Fleet]區段中,我們還修改了調度信息以識別這些實例化單元,而非我們之前使用的靜態單元。
我們也可以利用同樣的方法將sidekick單元轉化為模板。
我們的新sidekick單元將被命名為apache-discovery@.service,如下所示:
[Unit]Description=Apache web server on port %i etcd registration# RequirementsRequires=etcd.serviceRequires=apache@%i.service# Dependency ordering and bindingAfter=etcd.serviceAfter=apache@%i.serviceBindsTo=apache@%i.service[Service]# Get CoreOS environmental variablesEnvironmentFile=/etc/environment# Start## Test whether service is accessible and then register useful informationExecStart=/bin/bash -c '/while true; do /curl -f ${COREOS_PUBLIC_IPV4}:%i; /if [ $? -eq 0 ]; then / etcdctl set /services/apache/${COREOS_PUBLIC_IPV4} /'{"host": "%H", "ipv4_addr": ${COREOS_PUBLIC_IPV4}, "port": %i}/' --ttl 30; /else / etcdctl rm /services/apache/${COREOS_PUBLIC_IPV4}; /fi; /sleep 20; /done'# StopExecStop=/usr/bin/etcdctl rm /services/apache/${COREOS_PUBLIC_IPV4}[X-Fleet]# Schedule on the same machine as the associated Apache serviceX-ConditionMachineOf=apache@%i.service我們也用同樣的方式對sidekick單元中的內容進行了調整,從而構建這套實例化版本并保證其與正確的實例化主單元相匹配。
在curl命令中,當我們檢查服務的實際可用性時,我們會利用實例ID替換靜態端口80,從而保證其接入正確的位置。這一點非常重要,因為我們已經在Docker命令中對主單元的端口聲明映射做出了變更。
我們還修改了“port”部分以登錄至etcd,旨在保證其使用同樣的實例ID。變更之后,被設定在etcd中的JSON數據將完全動態。其會提取主機名稱、IP地址以及服務運行所在的端口。
最后,我們再次變更[X-Fleet]區段。我們需要確保此流程與主單元實例運行在同一臺設備上。
要利用模板文件進行單元實例化,我們擁有幾種不同選項。
其中fleet與systemd都能夠處理符號鏈接,這意味著我們可以創建包含完整實例ID且指向模板文件的鏈接,具體如下:
ln -s apache@.service apache@8888.serviceln -s apache-discovery@.service apache-discovery@8888.service這將創建兩條鏈接,分別名為apache@8888.service與apache-discovery@8888.service。二者皆擁有fleet與systemd運行單元所必需的信息。不過,它們會指向回對應模板,因此我們需要再做點調整。
在此之后,我們可以利用以下fleetctl命令實現服務的提交、加載或者啟動:
fleetctl start apache@8888.service apache-discovery@8888.service如果我們不希望利用符號鏈接定義自己的實例,則可使用另一種fleetctl內的模板提交方式:
fleetctl submit apache@.service apache-discovery@.service只需要將實例標記符分配給運行時,我們就能在fleetctl中以模板為基礎實現單元實例化。例如,大家可以使用以下命令:
fleetctl start apache@8888.service apache-discovery@8888.service這就消除了對符號鏈接的需求。部分管理員傾向于使用鏈接機制,因為這能保證實例文件隨時可用。其同時允許我們將目錄傳遞至fleetctl,從而一次性啟動全部組件。
例如,在我們的工作目錄中,大家可以為模板文件建立一個名為templates的子目錄,并為實例化鏈接版本建立名為instances的子目錄。大家甚至可以為非模板單元建立static子目錄。具體命令如下:
mkdir templates instances static而后將靜態文件移動到static子目錄下,而模板文件則移動對templates子目錄下:
mv apache.1.service apache-discovery.1.service staticmv apache@.service apache-discovery@.service templates在這里,大家可以創建自己需要的實例鏈接了。假設我們的服務運行在端口5555、6666與7777上:
cd instancesln -s ../templates/apache@.service apache@5555.serviceln -s ../templates/apache@.service apache@6666.serviceln -s ../templates/apache@.service apache@7777.serviceln -s ../templates/apache-discovery@.service apache-discovery@5555.serviceln -s ../templates/apache-discovery@.service apache-discovery@6666.serviceln -s ../templates/apache-discovery@.service apache-discovery@7777.service而后利用以下命令即可一次性啟動全部實例:
cd ..fleetctl start instances/*非常簡單,也非??旖?。
到這里,大家應該已經掌握了fleet單元文件的構建方法了。利用單元文件帶來的動態特性,我們能夠確保自己的服務始終得到均勻分布、擁有正確的關聯性并利用etcd注冊使用信息。
在下一篇文章中,我們將探討如何配置自己的容器,從而使用通過etcd注冊的信息。如此一來,我們就能夠建立對實際部署環境的認識,并將請求傳遞至后端中的合適容器當中。
本文來源自DigitalOcean Community。英文原文:How to Create Flexible Services for a CoreOS Cluster with Fleet Unit Files By Justin Ellingwood
翻譯:diradw
新聞熱點
疑難解答