# 第4章 示例:Web環境
我們所知的大多數公司都曾以一個很低的容器和宿主機比例(1~2個容器對應1臺宿主機)成功地使用過Docker。也就是說,要在生產環境中成功運行Docker,并不是必須要運行Apache Mesos或Kubernates。在本示例中,將對RelateIQ公司[\[1\]](part0010.xhtml#anchor41)使用Docker運行了一年多的一個真實Web服務器生產環境做詳細的說明。這個環境在運行Ubuntu的標準亞馬遜云服務(AWS)實例上,使用Docker支撐其CRM Web應用。當初使用Docker的原因有三:一是Docker能快速生成和銷毀容器,從而為客戶提供零停機時間部署;二是因為Docker為不同Web版本提供依賴隔離;三是Docker支持即時回滾。圖4-1所示為該環境的高層次示圖。

圖4-1
相信嗎?這個Web環境提供了如下功能:穩定的零停機時間部署、回滾、集中式日志、監控及分析JVM的一種方式。所有這些都是通過bash腳本編排Docker鏡像獲得的。圖4-2所示為主機的詳細情況。

圖4-2
這臺Web服務器運行于單臺AWS服務器上,并通過Docker運行著4個容器。部分容器被鏈接在一起,以便與Docker網橋上的其他容器進行通信。它給宿主機公開了多個端口,用于為性能分析提供HTTP服務和JVM監控。它使用了亞馬遜ELB負載均衡器(健康檢查在其上進行)。所有容器都將它們的日志保存在宿主機上,這樣現有的日志方案(SumoLogic)依舊適用,同時有一個簡單的bash編排腳本用于部署和設置新版本Web服務。
為了便于理解很多公司在生產環境中運行Docker時會遇到的問題,我們來看一些具體細節。
編排歸根到底就是做兩件事:一是獲取已安裝Docker的服務器,并且使之準備好運行容器的服務器;二是在服務器上啟動并運行容器。
該服務器使用標準的基本Ubuntu AMI(亞馬遜機器鏡像)在AWS上部署,并通過[Chef](http://www.chef.io/)的標準配置管理系統對宿主機進行設置。其設置過程與當下的多數環境完全相同。服務器啟動之后,Chef就會運行并設置ssh用戶、ssh密鑰,然后通過其包安裝器安裝基礎包(如iostat),安裝并配置監控代理(本例中是Datadog),集合一些臨時磁盤空間用于數據或日志存儲,安裝并配置日志代理(SumoLogic),安裝最新版Docker,最后創建bash設置腳本,并配置一個cron任務來運行它。
Chef在服務器上運行之后,宿主機就準備好在其上運行機器所需的任何容器了。Chef還配置了監控和日志軟件,用于未來的調試。這個環境可以運行任何類型的容器服務,與當下運行的大多數服務器環境,甚至是物理環境也一般無二。現在,Docker已經安裝完畢,宿主機也準備好核心操作工具,下面就可以讓宿主機上的容器開始運行Web應用了。
早期運行Docker的大多數公司一般都是使用bash腳本來設置容器的,這個環境也不例外。這個環境使用一個cron任務,每5分鐘運行一個bash腳本來進行容器的所有編排工作。腳本的核心功能是正確地設置容器并拉取最新版的Web服務器鏡像。我們來深入看一下所使用的腳本片段。
這個腳本完成以下操作。
(1)檢查容器是否正在運行(通常是的,這主要用于新機器的情況)。
(2)如果容器未運行,則部署Hipache和Redis容器并將它們鏈接在一起。
(3)拉取最新版的Web服務器鏡像并運行。
(4)等待Web服務器健康檢查通過,然后再將其添加到負載均衡器中。
(5)一旦上述操作成功,給服務器上的迷你負載均衡器`hipache`發送一條消息(本例中是使用netcat運行一個`redis-cli`命令),告知Docker為之分配的隨機端口和IP地址。
(6)保持舊容器運行,以便在需要時進行回滾。
(7)清除舊鏡像。
下面是腳本的一些片斷(為適合閱讀,刪除了部分代碼):
```
#!/bin/bash
# 檢查Hipache容器
STATE=$(docker inspect hipache | jq ".[0].State.Running")
if [[ "$STATE" != "true" ]]; then
set +e
docker rm hipache >/dev/null 2>&1
set -e
mkdir -p /logs/hipache/
docker run -p 80:80 -p 6379:6379 --name hipache -v /logs/hipache:/logs -d repo.com/hipache
echo "$(date +"%Y-%m-%d %H:%M:%S %Z") lpush frontend:* default"
sleep 5
(echo -en "lpush frontend:* default\r\n"; sleep 1) | nc localhost 6379
fi
# 拉取最新鏡像
IMAGE_ID=$(docker images | grep ${IMAGE_NAME} | grep $REMOTE_VERSION | head -n 1 | awk '{print $3}')
if [ -z $IMAGE_ID ]; then
docker pull $DOCKER_IMAGE_NAME
fi
echo $REMOTE_VERSION >$VERSION_FILE
# 啟動新容器
echo "$(date +"%Y-%m-%d %H:%M:%S %Z") launching $DOCKER_IMAGE_NAME, logging to $LOG_DIR"
mkdir -p $LOG_DIR
NEW_WEBAPP_ID="abcdefghijklmnopqrstuvwxyz"
MAX_TIMEOUT=5
set +e
until [ $MAX_TIMEOUT -le 0 ] || NEW_WEBAPP_ID=$(docker run -P -h $(hostname) --link hipache:hipache $(dockerParameters $BRANCH) -d -v $LOG_DIR:/logs $DOCKER_IMAGE_NAME); do
echo -n "."
sleep 1
let MAX_TIMEOUT-=1
done
set -e
# 檢查Web應用容器是否已啟動
NEW_WEBAPP_IP_ADDR=$(docker inspect $NEW_WEBAPP_ID | jq '.[0].NetworkSettings.IPAddress' -r)
if [ -z "$NEW_WEBAPP_IP_ADDR" -o "$NEW_WEBAPP_IP_ADDR" = "null" ]; then
echo "$(date +"%Y-%m-%d %H:%M:%S %Z") no new webapp ip, failed to start"
# send_deploy_message $HOSTNAME $BRANCH $IMAGE_NAME "error"
send_webhook $HOSTNAME $BRANCH $BUILD_ID $BUILD_NUMBER "failure"
exit 1
fi
echo -n "$(date +"%Y-%m-%d %H:%M:%S %Z") new instance $NEW_WEBAPP_ID starting, on ip $NEW_WEBAPP_IP_ADDR"
# 5分鐘
MAX_TIMEOUT=300
HEALTH_RC=1
set +e
until [ $HEALTH_RC == 0 ]; do
if [ $MAX_TIMEOUT -le 0 ]; then
echo "$(date +"%Y-%m-%d %H:%M:%S %Z") failed to be healthy within 5 minutes, killing and exiting..."
docker kill $NEW_WEBAPP_ID
docker rm $NEW_WEBAPP_ID
# send_deploy_message $HOSTNAME $BRANCH $IMAGE_NAME "error"
send_webhook $HOSTNAME $BRANCH $BUILD_ID $BUILD_NUMBER "failure"
exit 1
fi
${SCRIPT_HOME}/health.sh $NEW_WEBAPP_IP_ADDR
HEALTH_RC=$?
echo -n "."
sleep 5
let MAX_TIMEOUT-=5
done
set -e
echo
# 將自身作為后端添加到Redis中
(echo -en "rpush frontend:* http://${NEW_WEBAPP_IP_ADDR}:${WEBAPP_PORT}\r\n"; sleep 1) | nc localhost 6379
# 確保自己是Redis的第一個后端
(echo -en "lset frontend:* 1 http://${NEW_WEBAPP_IP_ADDR}:${WEBAPP_PORT}\r\n"; sleep 1) | nc localhost 6379
# 將Redis中所有其他后端移除
(echo -en "ltrim frontend:* 0 1\r\n"; sleep 1) | nc localhost 6379
```
如我們所見,這段腳本大部分都是一些很基礎的bash指令。只要有一些bash腳本的經驗,任何系統管理員或運維工程師都能完成此類編排。容器的編排可以很簡單,但必須經過幾次迭代,過一段時間腳本就會變得更強壯。即便是在出現失敗的情況下,這個腳本也能正確工作,不會將未通過健康檢查的新容器上線。隨著與Docker相關的新技術不斷出現,類似Apache Mesos和Kubernetes這樣的系統將取代bash腳本來完成編排。下面來看這個環境在其他方面是如何工作的。
只要掌握竅門,運行Docker和單一容器的宿主機網絡就很容易理解。Docker通過`docker run`命令將容器的端口公開給宿主機。服務器公開的端口包括負載均衡器監聽的80端口(ssl只傳遞到負載均衡器)、用于Java優化的優化端口、Redis用于切換負載均衡器后端的端口,以及Web服務器自身的一個端口(后續章節詳述)。服務器之外的負載均衡器只監控80端口。宿主機中的Web服務器會啟動一個隨機端口,來自80端口的請求會被Hipache代理轉發到這個端口上。
由于這是一個Web服務,存儲的需求不會太多。有時需要存儲日志、文件的緩存或加載靜態內容。本例中,使用的是宿主機的而非容器的存儲。將數據保存在宿主機上的理由很簡單。如果容器宕機了,我們仍然需要排查出現的問題。服務一般是將日志寫入到某個文件路徑中。本例中我們將Docker容器映射到宿主機文件系統中,并將持久化的日志文件從容器里重定向到宿主機上,以便未來進行日志分析。這通過`docker run`的`-v`卷參數很容易實現。
容器日志根據服務進行分類。例如,使用`/logs/Redis`、`/logs/hipache`和`/logs/webserver/`(如圖4-3所示)。這里需要特別注意,Web服務器會根據請求的日期時間戳來記錄錯誤和請求日志。容器記錄日志時,其文件名類似這樣:`/logs/webserver/2015-03-01.request.log`。如果文件存在,日志會自動追加到同一文件中。如果有另外一個或多個容器啟動,日志同樣會被追加到同一個文件中。通過Chef安裝logrotate,可防止日志無限制地增長。

圖4-3
在生產環境中,通常會有一個集中式日志服務器,因此服務器上的日志只是在被收集器取走前做臨時保存。由于所有的容器都將日志寫入宿主機中,所以無須為Docker采用一項全新的日志技術。非運行Docker的環境極可能也會采用相同的日志操作方式,以保持現存監控框架的不變。在這個環境中,很容易將創建或追加的日志文件發送到中央日志服務器(Splunk、Sumologic和Loggly)上以便分析。
這里需要特別注意的是,負載均衡器監控服務器的負荷,并根據需要自動將下一個請求發送給其他可用Web服務。宿主機是通過帶有Docker插件(這里是Datadog)的宿主機上的監控代理來監控的。本示例中的監控是一個全棧監視器。這個代理監控著宿主機的使用情況,如CPU、內存、磁盤IO、JVM監控以及運行的容器數量。這個環境里的應用程序指標通過StatsD發送給中央收集器。這里的指標包括了:網頁點擊量、應用程序查詢速度以及特定功能的延遲指標。
在這個環境中,使用了一個名為Yourkit的JVM優化工具來監控堆的運行情況。這樣,運維團隊或開發人員可以將自己的優化工具連接到宿主機上,從而通過調用棧發現應用程序的深層問題。其缺點之一是,每個容器都需要有單獨的端口,如果宿主機上同時運行著兩個容器,它們的端口也不能一樣。所以需要通過一個快速的SSH或工具來檢查這個端口。類似New Relic和Sysdig(在生態系統中提到過)的這類新技術可以對其進行監控。
由于所有的應用程序依賴都存儲在容器鏡像中,運維團隊只需要管理服務器管理方面的依賴即可。這簡化了Chef配置管理框架以及用于保持環境更新的腳本數量。
這個Web服務環境可以提供零停機時間部署。零停機時間部署通過Hipache以及一個由Redis支撐的實時Web查詢引擎實現,由于Redis是單線程的,在此作為數據庫非常完美。Hipache會將HTTP會話重定向給數據庫列表中最頂部的服務器。新容器上線時,將發送一條更新列表的命令給Redis,然后這個新容器就能接收所有新的點擊。會話狀態保存于后臺數據庫中,因此容器可以短暫存活,并且不會造成客戶端狀態的丟失。
因為Docker鏡像存儲于服務器上且容器啟動速度非常快,這個環境可以非常容易地在需要時回滾舊代碼。由于宿主機上保存著多個容器,很容易使用類似腳本或其他編排工具來啟動舊容器并取代新(錯誤部署的)容器。
RelateIQ已經在生產環境中運行本章所描述的設置一年多了,取得了巨大的成功。他們的團隊將標準的運維工具應用到Docker中,創造出一個功能完整的Web編排層。使得他們無須進行重大的基礎性變更即可嘗試新技術。他們也能將Docker和當前的基礎設施監控及日志方案相結合,使其易于在生產環境中運行。對這個環境有興趣的讀者,可以閱讀有關該環境的[博客文章](https://www.salesforceiq.com/blog/zero-downtime-pushes-say-goodbye-to-the-workout-robot/)和[訪談](http://blog.heavybit.com/blog/2015/3/23/dockermeetup)。
在第5章中,我們將講述RelateIQ如何使用AWS Beanstalk通過Docker為每個分支完整編排一個Web環境。
- - - - - -
[\[1\]](part0010.xhtml#ac41) 現已被Salesforce收購并更名為SalesforceIQ。——譯者注
- 版權信息
- 版權聲明
- 內容提要
- 對本書的贊譽
- 譯者介紹
- 前言
- 本書面向的讀者
- 誰真的在生產環境中使用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社區簡介
- 看完了