本文将以個人(開發)的角度,講述如何使用Docker技術線上上單機模式下部署一個Web應用,如有錯誤歡迎指出。
上次在這篇文章提到了Docker,這次打算把這個坑展開來講。
首先,什麼是Docker?根據官網描述,我們可以得知,Docker是一個軟體/容器平台,使用了虛拟化技術(cgroups,namespaces)來實作作業系統的資源隔離和限制,對于開發人員來說,容器技術為應用的部署提供了沙盒環境,我們可以在獨立的容器運作和管理應用程式程序,Docker提供的抽象層使得開發人員之間可以保持開發環境相對的一緻,避免了沖突。
下面體驗下Docker的使用:
使用下面的shell指令安裝Docker
安裝成功後,使用下面的指令應該能顯示Docker的版本資訊,說明Docker已經被安裝了
接着我們使用Docker建立一個nginx的容器:
這條指令表示Docker基于nginx:alpine這個Docker鏡像,建立一個名稱為web的容器,并把容器内部的80端口與主控端上的80端口做映射,使得通過主控端80端口的流量轉發到容器内部的80端口上。
使用docker ps指令,可以列出正在運作的容器,可以看到,剛才基于nginx鏡像建立的容器已經處于運作狀态了:
現在通路主控端位址的80端口,看到nginx的歡迎頁面。

Docker容器本質上是一個運作的程序以及它需要的一些依賴,而Docker鏡像則是定義這個容器的一個"模版"。
使用docker images能看到目前的鏡像:
了解到這個事實之後,我們使用下面的指令進入剛才建立的容器内部
現在處于的是容器内部的根檔案系統(rootfs),它跟主控端以及其他容器的環境是隔離開的,看起來這個容器就是一個獨立的作業系統環境一樣。使用ps指令可以看到容器内正在運作的程序:
使用exit指令可以從容器中退出,回到主控端的環境:
使用docker inspect指令我們可以看到關于這個容器的更多詳細資訊:
結果是用json格式表示的容器相關資訊,拉到下面的Networks一列可以看到這個容器的網絡環境資訊:
内容顯示了這個容器使用了bridge橋接的方式通信,它是docker容器預設使用的網絡驅動(使用docker network ls可以看到所有的驅動),從上面可以看到這個容器的IP位址為172.17.0.2,網關位址為172.17.0.1。
現在回想剛才的例子,通路主控端的80端口,主控端是怎麼跟容器打交道,實作轉發通信的呢?
要解決這個問題,我們首先要知道,docker在啟動的時候會在主控端上建立一塊名為docker0的網卡,可以用ifconfig檢視:
這個網卡的ip位址為172.17.0.1,看到這裡你是否想起了剛才我們建立的容器使用的網關位址即為172.17.0.1?我們是否可以大膽地猜測,docker容器就是通過這張名為docker0的網卡進行通信呢?确實如此,以單機環境為例,Docker Daemon啟動時會建立一塊名為docker0的虛拟網卡,在Docker初始化時系統會配置設定一個IP位址綁定在這個網卡上,docker0的角色就是一個主控端與容器間的網橋,作為一個二層交換機,負責資料包的轉發。當使用docker建立一個容器時,如果使用了bridge模式,docker會建立一個vet對,一端綁定到docker0上,而另一端則作為容器的eth0虛拟網卡。
使用ifconfig也可以看到這個veth對的存在:
我找了一張圖,可以很好地表示veth對的存在方式:
而真正實作端口轉發的魔法的是nat規則。如果容器使用-p指定映射的端口時,docker會通過iptables建立一條nat規則,把主控端打到映射端口的資料包通過轉發到docker0的網關,docker0再通過廣播找到對應ip的目标容器,把資料包轉發到容器的端口上。反過來,如果docker要跟外部的網絡進行通信,也是通過docker0和iptables的nat進行轉發,再由主控端的實體網卡進行處理,使得外部可以不知道容器的存在。
使用iptables -t nat指令可以看到添加的nat規則:
從上面的最後一行可以觀察到流量轉發到了172.17.0.2的80端口上,這個位址就是剛才建立容器使用的IP位址。
現在知道在剛才的例子中主控端是怎麼跟容器通信了吧,那麼容器跟容器之間通信呢?類似地,也是通過這個docker0交換機進行廣播和轉發。
扯的有點多,開始進入正題,先寫一個Web應用壓壓驚。
一般情況下,如果你要編寫一個Web項目,你會做什麼呢?反正對于我來說,如果我要寫一個python web項目的話,我會先用virtualenv建立一個隔離環境,進入環境内,使用pip安裝Django,最後用django-admin startproject建立一個項目,搞定。
但是如果用容器化的方式思考,我們大可直接借助于容器的隔離性優勢,更好地控制環境和版本的隔離,通常情況下你都不需要再關心用pyenv,virtualenv這種方式來初始化python環境的了,一切交給docker來完成吧。
甚至把安裝django這個步驟也省了,直接通過一句指令來拉取一個安裝了django的Python環境的鏡像。
現在通過這個鏡像運作django容器,同時進入容器Shell環境:
在/usr/src這個目錄下建立一個app目錄,然後用django-admin指令建立一個django項目:
然後使用下面的指令,在容器8000端口上運作這個應用:
由于之前已經将容器的8000端口與主控端的8080端口做了映射,是以我們可以通過通路主控端的8080端口通路這個應用。
注意了,對這個容器的所有修改僅僅隻對這個容器有效,不會影響到鏡像和基于鏡像建立的其他容器,當這個容器被銷毀之後,所做的修改也就随之銷毀。
下面建立一個應用ping,作用是統計該應用的通路次數,每次通路頁面将次數累加1,傳回響應次數給前端頁面,并把通路次數存到資料庫中。
使用redis作為ping的資料庫,與之前類似,拉取redis的鏡像,運作容器。
由于django容器需要與redis容器通信的話首先要知道它的ip位址,但是像剛才那樣,每次都手工擷取容器的ip位址顯然是一件繁瑣的事情,于是我們需要修改容器的啟動方式,加入—link參數,建立django容器與redis容器之間的聯系。
删除掉之前的容器,現在重新修改django容器的啟動方式:
這次加入了兩個參數:
-v /code:/usr/src/app 表示把主控端上的/code目錄挂載到容器内的/usr/src/app目錄,可以通過直接管理主控端上的挂載目錄來管理容器内部的挂載目錄。
--link=redis:db 表示把redis容器以db别名與該容器建立關系,在該容器内以db作為主機名表示了redis容器的主機位址。
現在進入到django容器,通過ping指令确認django容器能通路到redis容器:
像之前一樣,建立一個項目,接着使用django-admin建立一個應用:
編寫ping的視圖,添加到項目的urls.py:
别忘了安裝redis的python驅動:
運作django應用,通路應用的根位址,如無意外便能看到随着頁面重新整理累加的數字。
你或許會想,每次建立一個容器都要手工做這麼多操作,好麻煩,有沒有更友善的方式地來建構容器,不需要做那麼多額外的環境和依賴安裝呢?
仔細一想,其實我們建立的容器都是建立在基礎鏡像上的,那麼有沒有辦法,把修改好的容器作為基礎鏡像,以後需要建立容器的時候都使用這個新的鏡像呢?當然可以,使用docker commit [CONTAINER]的方式可以将改動的容器導出為一個Docker鏡像。
當然,更靈活的方式是編寫一個Dockerfile來建構鏡像,正如Docker鏡像是定義Docker容器的模版,Dockerfile則是定義Docker鏡像的檔案。下面我們來編寫一個Dockerfile,以定義出剛才我們進行改動後的容器導出的鏡像。
下面加入supervisor和gunicorn以更好地監控和部署應用程序:
gunicorn的配置檔案:
supervisord的配置檔案:
以supervisord作為web應用容器的啟動程序,supervisord來管理gunicorn的程序。這裡說明一下的是,由于使用docker logs指令來列印容器的日志時預設是從啟動程序(supervisord)的stdout和stderr裡收集的,而gunicorn又作為supervisord的派生程序存在,是以要正确配置gunicorn和supervisord的日志選項,才能從docker logs中看到有用的資訊。
把上面所做的修改混雜在一起,終于得出了第一個Dockerfile:
上面的Dockerfile的說明如下:
FROM指令制定了該鏡像的基礎鏡像為django:latest。
三行COPY指令分别将主控端的代碼檔案和配置檔案複制到容器環境的對應位置。
接着兩行RUN指令,一條指令安裝supervisor,另一條指令安裝python的依賴以及初始化django應用。
最後運作supervisord,配置為剛才複制的supervisor的配置檔案。
上面每一條指令都會由docker容器執行然後送出為一個鏡像,疊在原來的鏡像層的上方,最後得到一個擁有許多鏡像層疊加的最終鏡像。
完成Dockerfile的編寫後,隻需要用docker build指令就能建構出一個新的鏡像:
接着就可以根據這個鏡像來建立和運作容器了:
目前為止,項目的應用結構圖如下:
現在,如果Redis這個節點出現故障的話會怎麼樣?
答案是,整個服務都會不可用了,更糟糕的是,資料備份和恢複同步成為了更棘手的問題。
很明顯,我們不能隻依賴一個節點,還要通過建立主從節點防止資料的丢失。再建立兩個redis容器,通過slaveof指令為Redis建立兩個副本。
現在寫入到Redis主節點的資料都會在從節點上備份一份資料。
現在看起來好多了,然而當Redis master挂掉之後,服務仍然會變的不可用,是以當master當機時還需要通過選舉的方式把新的master節點推上去(故障遷移),Redis Sentinel正是一個合适的方式,我們建立Sentinel叢集來監控Redis master節點,當master節點不可用了,再由Sentinel叢集根據投票選舉出slave節點作為新的master。
下面為Sentinel編寫Dockerfile,在redis鏡像的基礎上作改動:
Sentinel的配置檔案:
run-sentinel.sh:
建構出Sentinel的鏡像檔案,容器運作的方式類似于redis:
這下Sentinel的容器也搭建起來了,應用的結構圖如下:
簡單驗證一下當redis主節點挂掉後sentinel怎麼處理:
修改代碼用Sentinel擷取redis執行個體:
下面再來考慮這種情況:
假設我們對django_app容器進行伸縮,擴充出三個一模一樣的django應用容器,這時候怎麼辦,該通路哪個?顯然,這時候需要一個負載均衡的工具作為web應用的前端,做反向代理。
nginx是一個非常流行的web伺服器,用它完成這個當然沒問題,這裡不說了。
下面說一說個人嘗試過的兩種選擇:
LVS(Linux Virtual Server)作為最外層的服務,負責對系統到來的請求做負載均衡,轉發到後端的伺服器(Real Server)上,DR(Direct Route)算法是指對請求封包的資料鍊路層進行修改mac位址的方式,轉發到後端的一台伺服器上,後端的伺服器叢集隻需要配置和負載均衡伺服器一樣的虛拟IP(VIP),請求就會落到對應mac位址的伺服器上,跟NAT模式相比,DR模式不需要修改目的IP位址,是以在傳回響應時,伺服器可以直接将封包發送給用戶端,而無須轉發回負載均衡伺服器,是以這種模式也叫做三角傳輸模式。
Haproxy是一個基于TCP/HTTP的負載均衡工具,在負載均衡上有許多精細的控制。下面簡單地使用Haproxy來完成上面的負載均衡和轉發。
首先把haproxy的官方鏡像下載下傳下來:
這類的鏡像的Dockerfile都可以在Docker Hub上找到。
這次同樣選擇編寫Dockerfile的方式建構自定的haproxy鏡像:
暫時隻需要把配置檔案複制到配置目錄就可以了,因為通過看haproxy的Dockerfile可以看到最後有這麼一行,于是乎偷個懶~
haproxy的配置檔案如下:
這裡的app即web應用容器的主機名,運作haproxy容器時用link連接配接三個web應用容器,綁定到主控端的80端口。
這時候通路主控端的80端口後,haproxy就會接管請求,用roundrobin方式輪詢代理到後端的三個容器上,實作健康檢測和負載均衡。
現在又有一個問題了,每次我們想增加或者減少web應用的數量時,都要修改haproxy的配置并重新開機haproxy,十分的不友善。
理想的方式是haproxy能自動檢測到後端伺服器的運作狀況并相應調整配置,好在這種方式不難,我們可以使用etcd作為後端伺服器的服務發現工具,把買二手QQ地圖伺服器的資訊寫入到etcd的資料庫中,再由confd來間隔一段時間去通路etcd的api,将伺服器的資訊寫入到模版配置中,并更新haproxy的檔案以及重新開機haproxy程序。
按官方的說法,etcd是一個可靠的分布式的KV存儲系統,而confd則是使用模版和資料管理應用配置的一個工具,關于他倆我還沒太多了解,是以不多說,下面把他們內建到上面的應用中。
建立一個etcd的容器:
confd的處理比較簡單,把confd的二進制檔案和配置檔案內建到之前haproxy的Dockerfile中:
通過之前haproxy的配置檔案建立出新的模版檔案,修改backend的配置,加入模版指令,表示confd從etcd的字首為/app/servers的所有key中擷取鍵值對,作為server的key的value,逐條追加到配置檔案中去:
下面是confd的配置檔案:
confd會把資料填入上面的模版檔案,并把配置更新到haproxy配置的目标路徑,再使用reload_cmd指定的指令重新開機haproxy。
修改後的haproxy鏡像最後通過boot.sh啟動程序:
watcher.sh啟動了confd間隔一段時間去通路etcd的位址,檢查是否有更新:
啟動haproxy時建立與etcd容器間的連接配接:
下面通過調用etcd的api在/app/servers上建立一個伺服器節點:
觀察haproxy容器的日志,可以看到配置被更新了:
最終的應用結構圖如下:
運作在機器上的服務時刻有可能有意外發生,是以我們需要一個服務來監控機器的運作情況和容器的資源占用。netdata是伺服器的一個實時監測工具,利用它可以直覺簡潔地了解到伺服器的運作情況。
當docker鏡像和容器數量增多的情況下,手工去運作和定義docker容器以及其相關依賴無疑是非常繁瑣和易錯的工作。Docker Compose是由Docker官方提供的一個容器編排和部署工具,我們隻需要定義好docker容器的配置檔案,用compose的一條指令即可自動分析出容器的啟動順序和依賴,快速的部署和啟動容器。
下面編寫好compose的檔案: