# 第3章 示例:極簡環境
一說起生產環境中容器的使用,大家的第一反應是那些在同樣量級的宿主機上部署成千上萬容器的大型公司。但實際上恰恰相反,要發揮容器的作用,并不需要構建如此龐大的系統。小規模的團隊反而能從容器中獲得最大收益,因為容器使構建和部署服務不僅變得簡單,而且可重復、可擴展。
本章描述的就是一家名為PeerSpace的小規模公司構建系統時采取的一種極簡方式。這種極簡方式使他們能在短時間內使用有限的資源開辟一個新市場,并自始至終保持著極高的開發速度。
PeerSpace構建系統時的目標是既要易于開發,又要在生產環境中足夠穩定。這兩個目標通常是相互矛盾的,因為高速開發引起的大量變化反過來會對系統的構建和配置產生很大影響。任何一個有經驗的系統管理員都知道,這樣的變化率必然導致不穩定性。
Docker看起來非常適合用在剛起步的時候,因為它既對開發人員友好,又支持以敏捷的方式構建和運維系統。Docker簡化了開發和系統配置的某些方面,但有時卻過于簡單化了。在易于開發和穩健運維之間取得平衡不是件容易的事。
PeerSpace實現開發速度和穩定的生產環境這兩個目標的方法之一是擁抱簡單。這里所說的簡單是指系統的每個部分——容器——有且只有一個目標。這個目標就是:相同的過程,如日志收集,在任何地方都以相同的方式完成,而各部分連接的方法也是明確、靜態地定義在配置文件中的。
在這種簡單的系統中,開發人員可以同步地、獨立地構建系統的不同部分,并確信構建的容器可組裝在一起。另外,在生產環境出現問題時,簡單性也讓問題的排查與解決變得非常簡單。
要長期保持系統的簡單,需要大量的思考、折中和堅持,但最終這種簡單將物有所值。
PeerSpace的系統由20個零散的微服務組成,其中有部分使用了MongoDB數據庫和/或ElasticSearch搜索引擎。該系統設計遵循下列指導原則。
(1)傾向無狀態服務。這可能是簡化PeerSpace生產環境時最大的決策:大部分服務都是無狀態的。除了用于處理當前進行中的請求的臨時信息,無狀態服務不需要保持任何需要持久化的數據。無狀態服務的優勢在于可以非常容易地對他們進行銷毀、重啟、復制及伸縮,所有這一切都無需考慮任何數據處理方面的邏輯。并且,無狀態服務更易于編寫。
(2)傾向靜態配置。所有宿主機和服務的配置都是靜態的:一旦給服務器推送一項配置,該配置就會一直生效,直至顯式地推送來新配置。與之相對的是那些動態配置的系統,其系統的實際配置是實時生成的,并會根據不同因素(如可用宿主機和即將到達的負載)進行自主修改。盡管動態系統的伸縮性更好,并且具有一些有趣的屬性,如在出現某些故障時自動恢復等,但靜態配置更易于理解和排錯。
(3)傾向靜態的網絡布局。如果在一臺宿主機中找到一項服務,除非新配置被確定并提交,否則總能在那臺宿主機中找到該服務。
(4)區別對待無狀態和有狀態服務。盡管PeerSpace的多數服務是無狀態的,他們還是使用MongoDB和ElasticSearch來持久化數據。這兩種類型的服務在本質上是非常不同的,應該區別處理。例如,將一個無狀態服務從一臺宿主機移動到另一臺上非常簡單,只需要啟動新服務,然后停止舊服務即可。但要對一個數據庫進行移動,數據也要跟著移動。移動數據可能會花費很長時間,要求在遷移過程中停止服務,或通過設備方法進行在線遷移。在開發領域,通常將無狀態服務比做“牲口”,它們沒有名字,很容易被代替和伸縮,而將有狀態服務比做“寵物”,它們是唯一的、具名的,需要維護,并且難以伸縮。幸運的是,PeerSpace正如一個農場一樣,其“牲口”數量要遠遠多于“寵物”。
以上這些設計原則是簡化PeerSpace系統的基礎。將有狀態服務與無狀態服務分離,可以對本質上完全不同的服務進行區別處理(如圖3-1所示),因此可以對每一種情況的處理方式進行優化和盡可能地簡化。使用靜態配置運行無狀態服務使得操作系統的流程變得非常簡單:多數情況下流程被簡化成文件復制和容器重啟,完全不需要考慮其他因素,如對第三方系統的依賴。

圖3-1
上述設計準則能否產生一個簡單的系統,完全取決于系統操作是否同樣簡單。
在設計業務流程時,PeerSpace基于觀察做出了如下假定:在他們的基礎設施中離硬件越近的層變更越少,而越接近終端用戶的層變更越頻繁(如圖3-2所示)。

圖3-2
根據這一觀察,生產環境中的服務器數量很少變更,通常是由于縮放問題或硬件故障。而這些服務器的配置變更頻次可能更高一些,通常是由于性能補丁、系統錯誤修復或安全問題等原因。
在這些服務器上運行的服務數量和類別變更更為頻繁。通常是指移動服務、添加新類型服務或對數據進行操作。這個層級上的其他修改可能與要求重新配置或變更第三方服務的新版本部署有關。不過,這類變更仍然不是很常見。
在這樣的基礎設施中,多數的變更與多個服務的新版本推送有關。每天,PeerSpace都會執行很多次新版服務的部署。多數情況下,新版本的推送只是簡單地將現有版本替換成運行新鏡像的新版本。有時也會使用相同鏡像,但對配置參數進行變更。
PeerSpace的流程建立是為了讓最頻繁的變更最容易也最簡單進行,即便這樣會造成基礎設施更難以變更(實際上并未發生)。
PeerSpace運行著3個類生產環境集群:集成環境、預演環境與生產環境。每個集群包含了相同數量的服務,并使用相同的方式進行配置,唯一不同的是它們的原始性能(CPU、內存等)。開發人員同樣會在自己的電腦上運行全部或部分集群。
每個集群由以下幾個部分組成:
- 幾臺運行著CentOS 7的Docker宿主機,使用[systemd](http://www.freedesktop.org/wiki/Software/systemd)作為系統管理程序;
- 一臺MongoDB服務器或一個復制集合;
- 一臺ElasticSearch服務器或一個集群。
MongoDB和/或ElasticSearch服務器可能在某些環境中是Docker化的,而在其他環境中不是Docker化的(如圖3-3所示)。它們也會在多個環境中共享。在生產環境中,出于運維和性能的原因,這些數據服務是不做Docker化的。

圖3-3
每個Docker宿主機運行著一個服務的靜態集合,所有這些服務都會遵循如下模式進行構建:
- 所有配置都通過環境變量進行設置,包括其他服務的地址(和端口);
- 不將數據寫入磁盤;
- 將日志發送到標準輸出(stdout)中;
- 生命周期由systemd管理,并定義在一個systemd單元文件中。
所有服務都由systemd管理。systemd是一個借鑒了OSX launchd的服務管理程序,此外,systemd使用普通數據文件命名單元來定義每個服務的生命周期(如圖3-4所示),這與其他使用shell腳本完成這類事務的傳統管理程序完全不同。

圖3-4
PeerSpace的服務只將Docker進程當作唯一的運行時的依賴。systemd的依賴管理只用來確保Docker處于運行狀態,但不確保其擁有的服務以正確順序啟動。服務構建時要求它們可以以任何順序啟動。
所有服務都由以下部分組成(如圖3-5所示):
- 一個容器鏡像;
- 一個systemd單元文件;
- 一個該容器專用的環境變量文件;
- 一組用于全局配置參數的共享環境變量文件。

圖3-5
所有單元都遵循相同的結構。在服務啟動之前,一系列包含環境變量的文件將被加載:
```
EnvironmentFile=/usr/etc/service-locations.env
EnvironmentFile=/usr/etc/service-config.env
EnvironmentFile=/usr/etc/cluster.env
EnvironmentFile=/usr/etc/secrets.env
EnvironmentFile=/usr/etc/%n.env
```
這確保了每個服務會加載一系列通用環境文件(`service-locations.env`、`service-config.env`、`cluster.env`及`secrets.env`),外加一個專用于該服務的文件:`%n.env`,此處的`%n`在運行時將被替換成該單元的全稱。例如,一個名為`docker-search`的服務單元將被替換成`docker-search.service`。
接下來的條目是確保在啟動新容器前舊容器被正確刪除的:
```
ExecStartPre=-/bin/docker kill %n
ExecStartPre=-/bin/docker rm -f %n
```
通過使用`%n`,將容器命名為單元的全稱。使用變量進行容器命名能讓單元文件更通用并且可移植。在`docker`程序路徑之前使用“`-`”可防止單元在命令失敗時中止啟動。這里需要忽略潛在的錯誤,因為如果此前不存在該容器,這些命令將執行失敗,而這種情況又是合法的。
單元中主要的條目是`ExecStart`,它將告之systemd如何啟動該容器。這里內容較多,但我們只關注一下其最重要的部分:
```
ExecStart=/bin/docker \
run \
-p "${APP_PORT}:${APP_PORT}" \
-e "APP_PORT=${APP_PORT}" \
-e "SERVICE_C_HOST=${SERVICE_C_HOST}" \
-e "SERVICE_D_HOST=${SERIVCE_D_HOST}" \
-e "SERVICE_M_HOST=${SERVICE_M_HOST}" \
--add-host docker01:${DOCKER01_IP} \
--add-host docker02:${DOCKER02_IP} \
--volume /usr/local/docker-data/%n/db:/data/data \
--volume /usr/local/docker-data/%n/logs:/data/logs \
--name %n \
${IMAGE_NAME}:${IMAGE_TAG}
```
(1)使用`EnvironmentFile`加載的環境變量來配置容器(如通過`-p`公開的端口)。
(2)將集群中的其他宿主機地址添加到容器的`/etc/hosts`文件中(`--add-host`)。
(3)映射用于日志和數據的數據卷。這主要是作為一個“蜜罐”(honey pot[\[1\]](part0009.xhtml#anchor31)),以便檢查這些目錄并確保無人對其進行寫入。
(4)鏡像自身(名稱和版本)來自于從`/usr/etc/%n.evn`中加載的環境變量,在本示例中它將映射到`/usr/etc/docker-search.service.env`中。
最后,是一些定義如何停止容器及其他生命周期要素的條目:
```
ExecStop=-/bin/docker stop %n
Restart=on-failure
RestartSec=1s
TimeoutStartSec=120
TimeoutStopSec=30
```
PeerSpace將集群配置分成兩種類型文件:環境變量文件和systemd單元文件。上面已經講述了單元文件及其加載環境變量文件的方式,接下來看一下環境文件。
將環境變量分解到不同文件中的主要原因在于,這些文件在跨集群時是否需要修改以及如何修改,不過也有其他操作層面的原因。
- `service-locations.env`:集群中所有服務的宿主機名。這個文件在不同集群里通常是一樣,不過也有例外。
- `service-config.env`:與服務自身相關的配置。如果不同集群運行的是服務的兼容性版本,這個文件應該是一樣的。
- `secrets.env`:密鑰信息。因其內容關系,這個文件被處理的方法與其他文件不同,而且在不同集群上也有差異。
- `cluster.env`:包括了集群間的所有不同之處,如所使用的數據庫前綴、是測試還是生產環境、外部地址等。這個文件中最重要的信息是屬于該集群的所有宿主機的IP地址。
下面是某些示例集群中的文件。這是`cluster.env`文件:
```
CLUSTER_ID=alpha
CLUSTER_TYPE="test"
DOCKER01_IP=x.x.x.226
DOCKER02_IP=x.x.x.144
EXTERNAL_ADDRESS=https://somethingorother.com
LOG_STORE_HOST=x.x.x.201
LOG_STORE_PORT=9200
MONGODB_PREFIX=alpha
MONGODB_HOST_01=x.x.x.177
MONGODB_HOST_02=x.x.x.299
MONGODB_REPLICA_SET_ID=rs001
```
這是`service-locations.env`文件:
```
SERVICE_A_HOST=docker01
SERVICE_B_HOST=docker03
CLIENTLOG_HOST=docker02
SERIVCE_D_HOST=docker01
...
SERVICE_Y_HOST=docker03
SERVICE_Z_HOST=docker01
```
每個systemd單元都包含集群中其他宿主機的引用,而這些引用來自于環境變量。包含服務宿主機名的變量會被裝配到Docker命令中,以便容器進程使用。這是通過`-e`參數實現的,如`-e "SERVICE_D_HOST=${SERIVCE_D_HOST}"`。
Docker宿主機的IP地址也同樣通過`--add-host docker01:${DOCKER01_IP}`注入到容器中。這樣,只需要修改這兩個文件并且保持單元文件的完好無損,就可以將容器擴散到不同數量的宿主機中。
容器級別或配置級別的修改通過3個步驟完成:第1步,在配置倉庫(Git)上做修改;第2步,將配置文件復制到宿主機的預演區域(ssh);第3步,運行宿主機上的一個腳本來逐一部署每個服務,使得配置修改生效。這種方法提供了版本化配置,一次只推送一項相關配置,以及讓推送配置生效的一種靈活方式。
如果需要針對一組服務進行修改,首先在Git上做修改并提交。然后運行腳本,將這個配置推送到所有宿主機的預演區域。一旦配置被推送過去,在每臺宿主機上運行一個腳本來部署或重部署該宿主機上的所有容器集合。這個腳本會對在列的所有服務執行如下命令。
(1)將配置文件從預演區域復制到其最終位置:
- systemd單元文件;
- 共享的配置文件;
- 當前服務的配置文件;
- 密鑰文件(解密后的)。
(2)需要的話下載鏡像文件(鏡像定義在服務自身的配置文件中)。
(3)重載systemd的配置,以便讀取新的單元文件。
(4)重啟容器對應的systemd單元。
PeerSpace具有兩個部署工作流,理解這一點有助于闡述其部署流程:一個用于開發環境,另一個用于生產環境,而后者是前者的一個超集。
在開發過程中,他們會通過以下步驟將臨時構建聯署到集成服務器中。
(1)使用最新代碼庫創建一個新的容器鏡像。
(2)將鏡像推送到鏡像倉庫中。
(3)在運行該鏡像的容器宿主機上運行部署腳本。
開發環境的systemd單元會追蹤鏡像的最新版本,所以只要配置不做修改,那我們只需推送鏡像并重新部署即可。
類生產環境的服務器(生產環境和預演環境)與開發環境配置方式大體相同,主要區別在于生產環境中的容器鏡像都打上了版本標簽,而非`latest`。部署發布鏡像到類生產環境容器的流程如下。
(1)在倉庫中為容器鏡像運行發布腳本。該腳本將為Git倉庫打上新版本標簽,然后使用這個版本號構建并推送鏡像。
(2)更新每個服務環境變量文件以引用新鏡像標簽。
(3)將新的配置推送到各宿主機中。
(4)在運行該鏡像的容器宿主機上運行部署腳本。
他們通常會批次地將服務從開發環境轉移到生產環境(一般是兩周一次)。在推送發行版到生產環境時,開發環境中用于該發行版的配置文件會被復制到生產目錄中。多數文件可以完全照搬,因為它們是從集群的具體細節(IP地址、宿主機數量等)抽象出來的,不過`cluster.env`和`secrets.env`文件在各個集群中是不一樣的,在發行時也對其進行更新。一般情況下,會一次性推送所有新版本服務。
PeerSpace使用了一組服務來支撐自己的服務。這些服務包括以下兩個。
- 日志聚合:<a class="calibre5">fluentd+kibana以及docker-gen</a>的組合。docker-gen可根據宿主機中運行的容器創建和重創建一個配置文件。docker-gen為每個運行中的容器生成一個fluentd條目,用于發送日志給kibana。這個服務運行良好,且易于調試。
- 監控:Datadog——一個SaaS監控服務。Datadog代理在容器中運行,用于監控各項性能指標、API使用情況和業務事件。Datadog為標簽提供了豐富的支持,通過fluentd可以使用多種方式對單一事件進行標記。數據收集起來后(如跨集群的相同服務、所有Docker服務、使用某個發行版的所有API端點等),可以利用豐富的標簽對數據進行多種方式的切割。
在系統中,所有宿主機和服務的配置都非常明確,開發人員很容易理解系統的配置,并能不受干擾地工作于系統的不同部分上。每位開發人員都可以在任何時候對集成集群進行推送,并且推送到生產環境所需的協調也很少。
由于每個集群的配置都保存在Git上,很容易追蹤配置的變化,并在出現配置問題時對集群進行排錯。
因為配置推送的方式,一旦新配置設置妥當,該配置將保持不變。靜態配置帶來的是極大的穩定性。
另外,服務編寫的方式,如通過環境變量進行配置、日志寫入控制臺、無狀態等,使得它們之后可原封不動地被Mesos或Kubernetes這類集群管理工具使用。
當然,要得到這些好處是有代價的。一個最明顯的缺點是配置有些繁瑣、重復并且易出錯。我們可以通過大量的自動化的工具來生成這些配置文件。
修改全局配置要求重啟多個容器。目前是由開發人員來重啟正確的容器。在生產環境中,如果推送的修改很多,通常會執行滾動重啟,但這并不是一個很好的解決方法。這絕對是一個薄弱環節,但到目前為止,還是可控的。
PeerSpace正在考慮幾個系統擴展的方式。其中之一是通過反向代理實現零停機時間部署。這將使得PeerSpace有能力對每個服務進行水平擴展。
另外一個方向是從集群的更高層級描述中生成所有的配置文件。這種方法能在配置發生改變后計算哪些容器需要重啟。
在考慮這些未來的方向時,PeerSpace也在權衡使用Mesos或Kubernetes的可能性,因為他們認為,增加部署腳本的任何復雜度勢必造成對簡單模式的過度拉伸。
盡管本章講解了一個極其簡單的Docker使用方式,但我們仍希望它能成為“Docker思想”的基石。不論是使用極簡方式還是集群管理系統,讀者都能利用這種方式在閱讀本書其他部分時獲益。
當然,使用Docker還有很多其他方式,第4章將講述RelateIQ使用Docker運行了一年多的一個真實的Web服務器生產環境。
- - - - - -
[\[1\]](part0009.xhtml#ac31) 用于隱藏宿主機的真實路徑。——譯者注
- 版權信息
- 版權聲明
- 內容提要
- 對本書的贊譽
- 譯者介紹
- 前言
- 本書面向的讀者
- 誰真的在生產環境中使用Docker
- 為什么使用Docker
- 開發環境與生產環境
- 我們所說的“生產環境”
- 功能內置與組合工具
- 哪些東西不要Docker化
- 技術審稿人
- 第1章 入門
- 1.1 術語
- 1.1.1 鏡像與容器
- 1.1.2 容器與虛擬機
- 1.1.3 持續集成/持續交付
- 1.1.4 宿主機管理
- 1.1.5 編排
- 1.1.6 調度
- 1.1.7 發現
- 1.1.8 配置管理
- 1.2 從開發環境到生產環境
- 1.3 使用Docker的多種方式
- 1.4 可預期的情況
- 為什么Docker在生產環境如此困難
- 第2章 技術棧
- 2.1 構建系統
- 2.2 鏡像倉庫
- 2.3 宿主機管理
- 2.4 配置管理
- 2.5 部署
- 2.6 編排
- 第3章 示例:極簡環境
- 3.1 保持各部分的簡單
- 3.2 保持流程的簡單
- 3.3 系統細節
- 利用systemd
- 3.4 集群范圍的配置、通用配置及本地配置
- 3.5 部署服務
- 3.6 支撐服務
- 3.7 討論
- 3.8 未來
- 3.9 小結
- 第4章 示例:Web環境
- 4.1 編排
- 4.1.1 讓服務器上的Docker進入準備運行容器的狀態
- 4.1.2 讓容器運行
- 4.2 連網
- 4.3 數據存儲
- 4.4 日志
- 4.5 監控
- 4.6 無須擔心新依賴
- 4.7 零停機時間
- 4.8 服務回滾
- 4.9 小結
- 第5章 示例:Beanstalk環境
- 5.1 構建容器的過程
- 部署/更新容器的過程
- 5.2 日志
- 5.3 監控
- 5.4 安全
- 5.5 小結
- 第6章 安全
- 6.1 威脅模型
- 6.2 容器與安全性
- 6.3 內核更新
- 6.4 容器更新
- 6.5 suid及guid二進制文件
- 6.6 容器內的root
- 6.7 權能
- 6.8 seccomp
- 6.9 內核安全框架
- 6.10 資源限制及cgroup
- 6.11 ulimit
- 6.12 用戶命名空間
- 6.13 鏡像驗證
- 6.14 安全地運行Docker守護進程
- 6.15 監控
- 6.16 設備
- 6.17 掛載點
- 6.18 ssh
- 6.19 私鑰分發
- 6.20 位置
- 第7章 構建鏡像
- 7.1 此鏡像非彼鏡像
- 7.1.1 寫時復制與高效的鏡像存儲與分發
- 7.1.2 Docker對寫時復制的使用
- 7.2 鏡像構建基本原理
- 7.2.1 分層的文件系統和空間控管
- 7.2.2 保持鏡像小巧
- 7.2.3 讓鏡像可重用
- 7.2.4 在進程無法被配置時,通過環境變量讓鏡像可配置
- 7.2.5 讓鏡像在Docker變化時對自身進行重新配置
- 7.2.6 信任與鏡像
- 7.2.7 讓鏡像不可變
- 7.3 小結
- 第8章 存儲Docker鏡像
- 8.1 啟動并運行存儲的Docker鏡像
- 8.2 自動化構建
- 8.3 私有倉庫
- 8.4 私有registry的擴展
- 8.4.1 S3
- 8.4.2 本地存儲
- 8.4.3 對registry進行負載均衡
- 8.5 維護
- 8.6 對私有倉庫進行加固
- 8.6.1 SSL
- 8.6.2 認證
- 8.7 保存/載入
- 8.8 最大限度地減小鏡像體積
- 8.9 其他鏡像倉庫方案
- 第9章 CI/CD
- 9.1 讓所有人都進行鏡像構建與推送
- 9.2 在一個構建系統中構建所有鏡像
- 9.3 不要使用或禁止使用非標準做法
- 9.4 使用標準基礎鏡像
- 9.5 使用Docker進行集成測試
- 9.6 小結
- 第10章 配置管理
- 10.1 配置管理與容器
- 10.2 面向容器的配置管理
- 10.2.1 Chef
- 10.2.2 Ansible
- 10.2.3 Salt Stack
- 10.2.4 Puppet
- 10.3 小結
- 第11章 Docker存儲引擎
- 11.1 AUFS
- 11.2 DeviceMapper
- 11.3 BTRFS
- 11.4 OverlayFS
- 11.5 VFS
- 11.6 小結
- 第12章 Docker 網絡實現
- 12.1 網絡基礎知識
- 12.2 IP地址的分配
- 端口的分配
- 12.3 域名解析
- 12.4 服務發現
- 12.5 Docker高級網絡
- 12.6 IPv6
- 12.7 小結
- 第13章 調度
- 13.1 什么是調度
- 13.2 調度策略
- 13.3 Mesos
- 13.4 Kubernetes
- 13.5 OpenShift
- Red Hat公司首席工程師Clayton Coleman的想法
- 第14章 服務發現
- 14.1 DNS服務發現
- DNS服務器的重新發明
- 14.2 Zookeeper
- 14.3 基于Zookeeper的服務發現
- 14.4 etcd
- 基于etcd的服務發現
- 14.5 consul
- 14.5.1 基于consul的服務發現
- 14.5.2 registrator
- 14.6 Eureka
- 基于Eureka的服務發現
- 14.7 Smartstack
- 14.7.1 基于Smartstack的服務發現
- 14.7.2 Nerve
- 14.7.3 Synapse
- 14.8 nsqlookupd
- 14.9 小結
- 第15章 日志和監控
- 15.1 日志
- 15.1.1 Docker原生的日志支持
- 15.1.2 連接到Docker容器
- 15.1.3 將日志導出到宿主機
- 15.1.4 發送日志到集中式的日志平臺
- 15.1.5 在其他容器一側收集日志
- 15.2 監控
- 15.2.1 基于宿主機的監控
- 15.2.2 基于Docker守護進程的監控
- 15.2.3 基于容器的監控
- 15.3 小結
- DockOne社區簡介
- 看完了