1. 程式人生 > 其它 >用Docker部署一個Web應用

用Docker部署一個Web應用

本文將以個人(開發)的角度,講述如何使用Docker技術在線上單機模式下部署一個Web應用,如有錯誤歡迎指出。

上次在這篇文章提到了Docker,這次打算把這個坑展開來講。

首先,什麼是Docker?根據官網描述,我們可以得知,Docker是一個軟體/容器平臺,使用了虛擬化技術(cgroups,namespaces)來實現作業系統的資源隔離和限制,對於開發人員來說,容器技術為應用的部署提供了沙盒環境,我們可以在獨立的容器執行和管理應用程式程序,Docker提供的抽象層使得開發人員之間可以保持開發環境相對的一致,避免了衝突。

下面體驗下Docker的使用:

使用下面的shell命令安裝Docker

$ curl -sSL https://get.docker.com/ | sh

安裝成功後,使用下面的命令應該能顯示Docker的版本資訊,說明Docker已經被安裝了

$ docker -v
Docker version 17.04.0-ce, build 4845c56

接著我們使用Docker建立一個nginx的容器:

$ docker run -d --name=web -p 80:80 nginx:latest

這條命令表示Docker基於nginx:alpine這個Docker映象,建立一個名稱為web的容器,並把容器內部的80埠與宿主機上的80埠做對映,使得通過宿主機80埠的流量轉發到容器內部的80埠上。

使用docker ps命令,可以列出正在執行的容器,可以看到,剛才基於nginx映象建立的容器已經處於執行狀態了:

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                         NAMES
a89d281829f9        nginx:latest        "nginx -g 'daemon ..."   8 minutes ago       Up 8 minutes        .0.0.0:80->80/tcp, 443/tcp   web

現在訪問宿主機地址的80埠,看到nginx的歡迎頁面。

Docker容器本質上是一個執行的程序以及它需要的一些依賴,而Docker映象則是定義這個容器的一個"模版"。

使用docker images能看到目前的映象:

$ docker images
REPOSITORY
nginx                 latest              bedece1f06cc        10 minutes ago         54.3MB

瞭解到這個事實之後,我們使用下面的命令進入剛才建立的容器內部

$ docker exec -i -t web bash

現在處於的是容器內部的根檔案系統(rootfs),它跟宿主機以及其他容器的環境是隔離開的,看起來這個容器就是一個獨立的作業系統環境一樣。使用ps命令可以看到容器內正在執行的程序:

$ ps -l
PID   USER     TIME   COMMAND
    1 root       :00 nginx: master process nginx -g daemon off;
    5 nginx      :00 nginx: worker process
   23 root       :00 ps -l

使用exit命令可以從容器中退出,回到宿主機的環境:

$ exit

使用docker inspect命令我們可以看到關於這個容器的更多詳細資訊:

$ docker inspect web

結果是用json格式表示的容器相關資訊,拉到下面的Networks一列可以看到這個容器的網路環境資訊:

"Networks": {
  "bridge": {
    "IPAMConfig": null,
    "Links": null,
    "Aliases": null,
    "NetworkID": "716496983db3eef8257dae57f4e0084c8242d8f5277da8a35b5ce265ccb4b3e5",
    "EndpointID": "e3ab409f152e87594fe2f07e32cea2577983b352f3bba8cc99de6092682d6774",
    "Gateway": "172.17.0.1",
    "IPAddress": "172.17.0.2",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": ,
    "MacAddress": "02:42:ac:11:00:02"
  }
}

內容顯示了這個容器使用了bridge橋接的方式通訊,它是docker容器預設使用的網路驅動(使用docker network ls可以看到所有的驅動),從上面可以看到這個容器的IP地址為172.17.0.2,閘道器地址為172.17.0.1。

現在回想剛才的例子,訪問宿主機的80埠,宿主機是怎麼跟容器打交道,實現轉發通訊的呢?

要解決這個問題,我們首先要知道,docker在啟動的時候會在宿主機上建立一塊名為docker0的網絡卡,可以用ifconfig檢視:

$ ifconfig
docker0   Link encap:Ethernet  HWaddr 02:42:a4:e4:10:80
          inet addr:172.17.0.1  Bcast:0.0.0.0  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:1414 errors:0 dropped:0 overruns:0 frame:0
          TX packets:1778 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:181802 (181.8 KB)  TX bytes:142440 (142.4 KB)

這個網絡卡的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對的存在:

veth8231e5b Link encap:Ethernet  HWaddr 16:e8:f2:1d:e1:4d
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:29 errors:0 dropped:0 overruns:0 frame:0
          TX packets:29 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:4033 (4.0 KB)  TX bytes:3741 (3.7 KB)

我找了一張圖,可以很好地表示veth對的存在方式:

而真正實現埠轉發的魔法的是nat規則。如果容器使用-p指定對映的埠時,docker會通過iptables建立一條nat規則,把宿主機打到對映埠的資料包通過轉發到docker0的閘道器,docker0再通過廣播找到對應ip的目標容器,把資料包轉發到容器的埠上。反過來,如果docker要跟外部的網路進行通訊,也是通過docker0和iptables的nat進行轉發,再由宿主機的物理網絡卡進行處理,使得外部可以不知道容器的存在。

使用iptables -t nat命令可以看到新增的nat規則:

$ iptables -t nat -xvL
Chain PREROUTING (policy ACCEPT 376 packets, 21292 bytes)
    pkts      bytes target     prot opt in     out     source               destination
   78606  4609864 DOCKER     all  --  any    any     anywhere             anywhere             ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT 376 packets, 21292 bytes)
    pkts      bytes target     prot opt in     out     source               destination

Chain OUTPUT (policy ACCEPT 286 packets, 21190 bytes)
    pkts      bytes target     prot opt in     out     source               destination
                DOCKER     all  --  any    any     anywhere            !127.0.0.0/8          ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT 290 packets, 21430 bytes)
    pkts      bytes target     prot opt in     out     source               destination
                MASQUERADE  all  --  any    !docker0  172.17.0.0/16        anywhere
                MASQUERADE  tcp  --  any    any     172.17.0.2           172.17.0.2           tcp dpt:http

Chain DOCKER (2 references)
    pkts      bytes target     prot opt in     out     source               destination
                RETURN     all  --  docker0 any     anywhere             anywhere
       4      240 DNAT       tcp  --  !docker0 any     anywhere             anywhere             tcp dpt:http to:172.17.0.2:80

從上面的最後一行可以觀察到流量轉發到了172.17.0.2的80埠上,這個地址就是剛才建立容器使用的IP地址。

現在知道在剛才的例子中宿主機是怎麼跟容器通訊了吧,那麼容器跟容器之間通訊呢?類似地,也是通過這個docker0交換機進行廣播和轉發。

扯的有點多,開始進入正題,先寫一個Web應用壓壓驚。

一般情況下,如果你要編寫一個Web專案,你會做什麼呢?反正對於我來說,如果我要寫一個python web專案的話,我會先用virtualenv建立一個隔離環境,進入環境內,使用pip安裝Django,最後用django-admin startproject建立一個專案,搞定。

但是如果用容器化的方式思考,我們大可直接藉助於容器的隔離性優勢,更好地控制環境和版本的隔離,通常情況下你都不需要再關心用pyenv,virtualenv這種方式來初始化python環境的了,一切交給docker來完成吧。

甚至把安裝django這個步驟也省了,直接通過一句命令來拉取一個安裝了django的Python環境的映象。

$ docker pull django

現在通過這個映象執行django容器,同時進入容器Shell環境:

$ docker run -it --name=app -p 8080:8000 django bash

在/usr/src這個目錄下新建一個app目錄,然後用django-admin命令新建一個django專案:

$ cd /usr/src
$ mkdir app
$ cd app
$ django-admin startproject django_app

然後使用下面的命令,在容器8000埠上執行這個應用:

$ python manage.py makemigartions
$ python manage.py migrate
$ python manage.py runserver .0.0.0:8000 &

由於之前已經將容器的8000埠與宿主機的8080埠做了對映,因此我們可以通過訪問宿主機的8080埠訪問這個應用。

$ exit #退出容器
$ curl -L http://127.0.0.1:8080/

注意了,對這個容器的所有修改僅僅只對這個容器有效,不會影響到映象和基於映象建立的其他容器,當這個容器被銷燬之後,所做的修改也就隨之銷燬。

下面新建一個應用ping,作用是統計該應用的訪問次數,每次訪問頁面將次數累加1,返回響應次數給前端頁面,並把訪問次數存到資料庫中。

使用redis作為ping的資料庫,與之前類似,拉取redis的映象,執行容器。

$ docker pull redis
$ docker run -d --name=redis -p 6379 redis

由於django容器需要與redis容器通訊的話首先要知道它的ip地址,但是像剛才那樣,每次都手工獲取容器的ip地址顯然是一件繁瑣的事情,於是我們需要修改容器的啟動方式,加入—link引數,建立django容器與redis容器之間的聯絡。

刪除掉之前的容器,現在重新修改django容器的啟動方式:

$ docker run -it --name=app -p 8080:8000 -v /code:/usr/src/app --link=redis:db django bash

這次加入了兩個引數:

  • -v /code:/usr/src/app表示把宿主機上的/code目錄掛載到容器內的/usr/src/app目錄,可以通過直接管理宿主機上的掛載目錄來管理容器內部的掛載目錄。

  • --link=redis:db表示把redis容器以db別名與該容器建立關係,在該容器內以db作為主機名錶示了redis容器的主機地址。

現在進入到django容器,通過ping命令確認django容器能訪問到redis容器:

$ ping db
PING db (192.168.32.12): 56 data bytes
64 bytes from 192.168.32.12: icmp_seq= ttl=64 time=.463 ms
64 bytes from 192.168.32.12: icmp_seq=1 ttl=64 time=.086 ms

像之前一樣,建立一個專案,接著使用django-admin新建一個應用:

$ django-admin startapp ping

編寫ping的檢視,新增到專案的urls.py:

from django.shortcuts import render
from django.http import HttpResponse
import redis

rds = redis.StrictRedis('db', 6379)

def ping(request):
    rds.incr('count', 1)
    cnt = rds.get('count')
    cnt = b'0' if cnt is None else cnt
    return HttpResponse(cnt.decode())

''' urls.py
from pingtest.views import ping

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^$', ping)
]
'''

別忘了安裝redis的python驅動:

$ pip install redis

執行django應用,訪問應用的根地址,如無意外便能看到隨著頁面重新整理累加的數字。

$ curl http://127.0.0.1/
3243
$ curl http://127.0.0.1/
3244

你或許會想,每次建立一個容器都要手工做這麼多操作,好麻煩,有沒有更方便的方式地來構建容器,不需要做那麼多額外的環境和依賴安裝呢?

仔細一想,其實我們建立的容器都是建立在基礎映象上的,那麼有沒有辦法,把修改好的容器作為基礎映象,以後需要建立容器的時候都使用這個新的映象呢?當然可以,使用docker commit [CONTAINER]的方式可以將改動的容器匯出為一個Docker映象。

當然,更靈活的方式是編寫一個Dockerfile來構建映象,正如Docker映象是定義Docker容器的模版,Dockerfile則是定義Docker映象的檔案。下面我們來編寫一個Dockerfile,以定義出剛才我們進行改動後的容器匯出的映象。

下面加入supervisor和gunicorn以更好地監控和部署應用程序:

gunicorn的配置檔案:

import multiprocessing

bind = "0.0.0.0:8000"
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = 'meinheld.gmeinheld.MeinheldWorker'
user = 'root'
loglevel = 'warning'
reload = True
accesslog = '-' #to supervisord's stdout
#errorlog

supervisord的配置檔案:

[supervisord]
nodaemon=true
logfile_maxbytes=10MB
loglevel=debug

[program:ping]
command=gunicorn -c /etc/gunicorn_conf.py django_app.wsgi:application
directory=/usr/src/app
user=root
process_name=root
numprocs=1
autostart=true
autorestart=true
redirect_stderr=True

以supervisord作為web應用容器的啟動程序,supervisord來管理gunicorn的程序。這裡說明一下的是,由於使用docker logs命令來列印容器的日誌時預設是從啟動程序(supervisord)的stdout和stderr裡收集的,而gunicorn又作為supervisord的派生程序存在,因此要正確配置gunicorn和supervisord的日誌選項,才能從docker logs中看到有用的資訊。

把上面所做的修改混雜在一起,終於得出了第一個Dockerfile:

FROM django:latest

COPY ./app /usr/src/app

COPY supervisord.conf /etc/supervisord.conf

COPY gunicorn_conf.py /etc/gunicorn_conf.py

RUN apt-get update && \
    apt-get install -y supervisor && \
    rm -rf /var/lib/apt/lists/*

RUN pip install meinheld && \
    pip install gunicorn && \
    cd /usr/src/app && \
    pip install -r requirement.txt && \
    python manage.py makemigrations && \
    python manage.py migrate

WORKDIR /usr/src/app

CMD supervisord -c /etc/supervisord.conf

上面的Dockerfile的說明如下:

  • FROM指令制定了該映象的基礎映象為django:latest。

  • 三行COPY指令分別將宿主機的程式碼檔案和配置檔案複製到容器環境的對應位置。

  • 接著兩行RUN指令,一條指令安裝supervisor,另一條指令安裝python的依賴以及初始化django應用。

  • 最後執行supervisord,配置為剛才複製的supervisor的配置檔案。

上面每一條指令都會由docker容器執行然後提交為一個映象,疊在原來的映象層的上方,最後得到一個擁有許多映象層疊加的最終映象。

完成Dockerfile的編寫後,只需要用docker build命令就能構建出一個新的映象:

docker build -t test/app .

接著就可以根據這個映象來建立和執行容器了:

$ docker run -d --name=app -p 8080:8000 -v /code:/usr/src/app --link=redis:db test/app

目前為止,專案的應用結構圖如下:

現在,如果Redis這個節點出現故障的話會怎麼樣?

答案是,整個服務都會不可用了,更糟糕的是,資料備份和恢復同步成為了更棘手的問題。

很明顯,我們不能只依賴一個節點,還要通過建立主從節點防止資料的丟失。再建立兩個redis容器,通過slaveof指令為Redis建立兩個副本。

$ docker run -d --name=redis_slave_1 -p 6380:6379 --link=redis:master redis redis-server --slaveof master 6379
$ docker run -d --name=redis_slave_2 -p 6381:6379 --link=redis:master redis redis-server --slaveof master 6379

現在寫入到Redis主節點的資料都會在從節點上備份一份資料。

現在看起來好多了,然而當Redis master掛掉之後,服務仍然會變的不可用,所以當master宕機時還需要通過選舉的方式把新的master節點推上去(故障遷移),Redis Sentinel正是一個合適的方式,我們建立Sentinel叢集來監控Redis master節點,當master節點不可用了,再由Sentinel叢集根據投票選舉出slave節點作為新的master。

下面為Sentinel編寫Dockerfile,在redis映象的基礎上作改動:

FROM redis:latest

COPY run-sentinel.sh /run-sentinel.sh

COPY sentinel.conf /etc/sentinel.conf

RUN chmod +x /run-sentinel.sh

ENTRYPOINT ["/run-sentinel.sh"]

Sentinel的配置檔案:

port 26379

dir /tmp

sentinel monitor master redis-master 6379 2

sentinel down-after-milliseconds master 30000

sentinel parallel-syncs master 1

sentinel failover-timeout master 180000

run-sentinel.sh:

#!/bin/bash

exec redis-server /etc/sentinel.conf --sentinel

構建出Sentinel的映象檔案,容器執行的方式類似於redis:

$ docker run -d --name=sentinel_1 --link=redis:redis-master [build_sentinel_image]
$ docker run -d --name=sentinel_2 --link=redis:redis-master [build_sentinel_image]
$ docker run -d --name=sentinel_3 --link=redis:redis-master [build_sentinel_image]

這下Sentinel的容器也搭建起來了,應用的結構圖如下:

簡單驗證一下當redis主節點掛掉後sentinel怎麼處理:

$ docker pause redis-master
$ docker logs -f --tail=100 sentinel_1
1:X 17 Apr 14:32:51.633 # +sdown master master 192.168.32.12 6379
1:X 17 Apr 14:32:52.006 # +new-epoch 1
1:X 17 Apr 14:32:52.007 # +vote-for-leader 35ff9e1686f3425f4cbe5680a741e366a1863cae 1
1:X 17 Apr 14:32:52.711 # +odown master master 192.168.32.12 6379 #quorum 3/2
1:X 17 Apr 14:32:52.711 # Next failover delay: I will not start a failover before Mon Apr 17 14:33:02 2017
1:X 17 Apr 14:32:53.221 # +config-update-from sentinel 35ff9e1686f3425f4cbe5680a741e366a1863cae 192.168.32.7 26379 @ master 192.168.32.12 6379
1:X 17 Apr 14:32:53.221 # +switch-master master 192.168.32.12 6379 192.168.32.5 6379
1:X 17 Apr 14:32:53.221 * +slave slave 192.168.32.6:6379 192.168.32.6 6379 @ master 192.168.32.5 6379
1:X 17 Apr 14:32:53.221 * +slave slave 192.168.32.12:6379 192.168.32.12 6379 @ master 192.168.32.5 6379
$ docker exec -it sentinel_1 redis-cli -p 26379 info Sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=master,status=ok,address=192.168.32.5:6379,slaves=2,sentinels=3

修改程式碼用Sentinel獲取redis例項:

sentinel = Sentinel([('sentinel', 26379)])
rds = sentinel.master_for('master')

下面再來考慮這種情況:

假設我們對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的官方映象下載下來:

$ docker pull haproxy

這類的映象的Dockerfile都可以在Docker Hub上找到。

這次同樣選擇編寫Dockerfile的方式構建自定的haproxy映象:

FROM haproxy:latest

COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg

暫時只需要把配置檔案複製到配置目錄就可以了,因為通過看haproxy的Dockerfile可以看到最後有這麼一行,於是乎偷個懶~

CMD ["haproxy", "-f", "/usr/local/etc/haproxy/haproxy.cfg"]

haproxy的配置檔案如下:

global
    log 127.0.0.1 local0
    maxconn 4096
    daemon
    nbproc 4

defaults
    log 127.0.0.1 local3
    mode http
    option dontlognull
    option redispatch
    retries 2
    maxconn 2000
    balance roundrobin
    timeout connect 5000ms
    timeout client 5000ms
    timeout server 5000ms

frontend main
    bind *:6301
    default_backend webserver

backend webserver
    server app1 app1:8000 check inter 2000 rise 2 fall 5
    server app2 app2:8000 check inter 2000 rise 2 fall 5
    server app3 app3:8000 check inter 2000 rise 2 fall 5

這裡的app即web應用容器的主機名,執行haproxy容器時用link連線三個web應用容器,繫結到宿主機的80埠。

$ docker run -d --name=lb -p 80:6301 --link app1:app1 --link app2:app2 --link app3:app3  [build_haproxy_image]

這時候訪問宿主機的80埠後,haproxy就會接管請求,用roundrobin方式輪詢代理到後端的三個容器上,實現健康檢測和負載均衡。

現在又有一個問題了,每次我們想增加或者減少web應用的數量時,都要修改haproxy的配置並重啟haproxy,十分的不方便。

理想的方式是haproxy能自動檢測到後端伺服器的執行狀況並相應調整配置,好在這種方式不難,我們可以使用etcd作為後端伺服器的服務發現工具,把買二手QQ地圖伺服器的資訊寫入到etcd的資料庫中,再由confd來間隔一段時間去訪問etcd的api,將伺服器的資訊寫入到模版配置中,並更新haproxy的檔案以及重啟haproxy程序。

按官方的說法,etcd是一個可靠的分散式的KV儲存系統,而confd則是使用模版和資料管理應用配置的一個工具,關於他倆我還沒太多瞭解,所以不多說,下面把他們整合到上面的應用中。

建立一個etcd的容器:

docker run -d \
-e CLIENT_URLS=http://0.0.0.0:2379 \
-e PEER_URLS=http://0.0.0.0:2380 \
-p 2379:2379 \
-p 2380:2380 \
-p 4001:4001 \
-p 7001:7001 \
-v /etc/ssl/certs/:/etc/ssl/certs/ \
elcolio/etcd \
-name etcd \
-initial-cluster-token=etcd-cluster-1 \
-initial-cluster="etcd=http://etcd:2380"\
-initial-cluster-state=new \
-advertise-client-urls=http://etcd:2379 \
-initial-advertise-peer-urls http://etcd:2380

confd的處理比較簡單,把confd的二進位制檔案和配置檔案整合到之前haproxy的Dockerfile中:

FROM haproxy:latest

COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg

COPY confd .

RUN chmod +x confd

COPY haproxy.toml /etc/confd/conf.d/

COPY haproxy.tmpl /etc/confd/templates/

COPY boot.sh .

COPY watcher.sh .

CMD ["./boot.sh"]

通過之前haproxy的配置檔案創建出新的模版檔案,修改backend的配置,加入模版指令,表示confd從etcd的字首為/app/servers的所有key中獲取鍵值對,作為server的key的value,逐條追加到配置檔案中去:

backend webserver
    {{range gets "/app/servers/*"}}
    server {{base .Key}} {{.Value}} check inter 2000 rise 2 fall 5
    {{end}}

下面是confd的配置檔案:

[template]
src = "haproxy.tmpl"
dest = "/usr/local/etc/haproxy/haproxy.cfg"
keys = [
    "/app/servers"
]
owner = "root"
mode = "0644"
reload_cmd = "kill -HUP 1"

confd會把資料填入上面的模版檔案,並把配置更新到haproxy配置的目標路徑,再使用reload_cmd指定的命令重啟haproxy。

修改後的haproxy映象最後通過boot.sh啟動程序:

#!/bin/bash

./watcher.sh &

exec /docker-entrypoint.sh haproxy -f /usr/local/etc/haproxy/haproxy.cfg

watcher.sh啟動了confd間隔一段時間去訪問etcd的地址,檢查是否有更新:

./confd -interval 10 -node http://etcd:2379 -config-file /etc/confd/conf.d/haproxy.toml

啟動haproxy時建立與etcd容器間的連線:

$ docker run -d --name=lb -p 80:6301 --link app1:app1 --link app2:app2 --link app3:app3  --link=etcd:etcd [build_haproxy_image]

下面通過呼叫etcd的api在/app/servers上新建一個伺服器節點:

$ docker exec -it etcd etcdctl set /app/servers/app1 172.17.0.5:8000

觀察haproxy容器的日誌,可以看到配置被更新了:

$ docker logs -f lb
2017-04-16T16:24:00Z 78745b65a3d4 ./confd[7]: INFO Backend set to etcd
<7>haproxy-systemd-wrapper: executing /usr/local/sbin/haproxy -p /run/haproxy.pid -f /usr/local/etc/haproxy/haproxy.cfg -Ds
2017-04-16T16:24:00Z 78745b65a3d4 ./confd[7]: INFO Starting confd
2017-04-16T16:24:00Z 78745b65a3d4 ./confd[7]: INFO Backend nodes set to http://etcd:2379
2017-04-16T16:24:00Z 78745b65a3d4 ./confd[7]: INFO /usr/local/etc/haproxy/haproxy.cfg has md5sum 8e6fc297a13fbb556426f55e130cecf4 should be f5e8a4b8fbea0b20da3796334bac1ddb
2017-04-16T16:24:00Z 78745b65a3d4 ./confd[7]: INFO Target config /usr/local/etc/haproxy/haproxy.cfg out of sync
2017-04-16T16:24:00Z 78745b65a3d4 ./confd[7]: INFO Target config /usr/local/etc/haproxy/haproxy.cfg has been updated
<5>haproxy-systemd-wrapper: re-executing on SIGHUP.
<7>haproxy-systemd-wrapper: executing /usr/local/sbin/haproxy -p /run/haproxy.pid -f /usr/local/etc/haproxy/haproxy.cfg -Ds -sf 15 16 17 18

最終的應用結構圖如下:

執行在機器上的服務時刻有可能有意外發生,因此我們需要一個服務來監控機器的執行情況和容器的資源佔用。netdata是伺服器的一個實時監測工具,利用它可以直觀簡潔地瞭解到伺服器的執行情況。

當docker映象和容器數量增多的情況下,手工去執行和定義docker容器以及其相關依賴無疑是非常繁瑣和易錯的工作。Docker Compose是由Docker官方提供的一個容器編排和部署工具,我們只需要定義好docker容器的配置檔案,用compose的一條命令即可自動分析出容器的啟動順序和依賴,快速的部署和啟動容器。

下面編寫好compose的檔案:

version: '2.1'
services:
  haproxy:
    container_name: lb
    build: ./builds/haproxy
    ports:
     - 80:6301
    restart: always
    links:
    - ping-app
    - etcd:etcd
    - netdata:netdata

  ping-app:
    build: .
    restart: always
    volumes:
    - ./app:/usr/src/app
    links:
    - sentinel
    - redis-master:db

  redis-master:
    image: redis:latest
    ports:
     - 6379:6379
    restart: always

  redis-slave:
    image: redis:latest
    command: redis-server --slaveof master 6379
    restart: always
    links:
    - redis-master:master
 
  sentinel:
    build: ./builds/sentinel
    restart: always
    links:
    - redis-master:redis-master
    - redis-slave

  etcd:
    image: elcolio/etcd
    command: -name etcd -initial-cluster-token=etcd-cluster-1 -initial-cluster="etcd=http://etcd:2380" -initial-cluster-state=new -advertise-client-urls=http://etcd:2379 -initial-advertise-peer-urls http://etcd:2380
    environment:
     - CLIENT_URLS=http://0.0.0.0:2379
     - PEER_URLS=http://0.0.0.0:2380
    ports:
     - 2379:2379
     - 2380:2380
     - 4001:4001
     - 7001:7001
    volumes:
     - /etc/ssl/certs/:/etc/ssl/certs/

  netdata:
    image: titpetric/netdata
    restart: always
    cap_add:
     - SYS_PTRACE
    volumes:
     - /proc:/host/proc:ro
     - /sys:/host/sys:ro

只需幾條命令,就能啟動和伸縮容器:

docker-compose -f docker-compose.yml up -d
docker-compose -f docker-compose.yml scale redis-slave=2
docker-compose -f docker-compose.yml scale sentinel=3
docker-compose -f docker-compose.yml scale ping-app=3

再通過一個指令碼把web應用註冊到etcd中去:

APP_SERVERS=$(docker-compose -f docker-compose.yml ps ping-app | awk '{print $1}' |sed '1,2d'|xargs docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' | xargs)
APP_SERVER_PORT=8000
INDEX=1
for ETCD_NODE in ${APP_SERVERS//\s/};
do
    docker-compose -f docker-compose.yml exec etcd etcdctl set /app/servers/app$INDEX $ETCD_NODE:$APP_SERVER_PORT
    INDEX=$(expr $INDEX + 1)
done

就這樣,一個基於Docker構建並具有良好可用性的web應用就完成了。