使用Docker建立一個動態負載平衡的分布式Web系統
這是一個使用Docker將Node.JS或Java等Web應用實現 分布式 部署的文章,關鍵是解決應用服務的自動發現。
分布式系統的核心是負載平衡和服務發現,邏輯圖如下:

每個應用都運行在Docker容器中。
我們希望能夠動態配置管理,,當我們啟動/停止一個新的容器,新的后端服務器能給自動注冊到負載均衡器中。這就是服務發現;負載均衡器應該能自動發現能夠提供服務的容器。
服務發現的目的是減少或消除組件之間“手動”連接。當你在生產中配置您的應用程序時,如數據庫服務器主機和端口,REST服務URL等,在一個高度可擴展體系架構中,這些類似電線的跳轉配置都應該可以動態更改。比如如果添加一個新的后端服務器。如果數據庫節點崩潰停止了。您的應用程序需要適應這種動態環境。
Aache ZooKeeper之類的工具可用于管理這個需求。這些工具的原理是很常見的:當一個服務器開始啟動后,必須注冊一個服務實例到專門的配置服務器。當它停止(正常關閉或崩潰后),必須能從配置服務器中刪除這個節點。注冊后,其他服務也能在配置服務器的列表中搜索提供特定的服務的服務實例(主機和端口)。
工具庫包介紹:
1.Docker. 運行應用的開源容器平臺
2.HAproxy. 在一系列后端服務器列表中平衡TCP流量的代理服務器。它會開啟一個端口,將這個端口的進入流量轉發到后端列表中的任何一個服務器。
3.Synapse. 簡化服務發現,有兩部分組成:
(1)Watchers: 他們持續檢查配置服務器的服務列表,可以通過連接Zookeeper,或etcd, 或進入Docker容器實現。這里采取最后一種實現方式。
(2)操作HAproxy: Synapse能按照watcher結果自動改變 HAproxy,這意味著當一個新的服務實例被watcher發現時,這個后端服務器的主機IP和端口參數將加入 HAproxy ,以便將進來的請求轉發到這個后端服務器上,同樣,當一個實例停止后,Synapse會刪除后端配置。
案例實例的架構原理圖如下:

Synapse 管理和Docker運行在同樣機器上但是端口是8000的HAproxy實例
Synapse通過Docker發現某個運行的暴露端口的實例,然后加入到HAproxy 配置。
安裝Docker
$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 36A1D7869245C8950F966E92D8576A8BA88D21E9
$ sudo sh -c "echo deb https://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list"
$ sudo apt-get update
$ sudo apt-get install lxc-docker
將下面一行加入到/etc/default/docker以激活Docker在TCP的API:
DOCKER_OPTS="-H 127.0.0.1:4243"
然后重啟Docker:
$ sudo service docker restart
最后,為Docker客戶端定義使用TCP API的環境:
$ export DOCKER_HOST=tcp://127.0.0.1:4243
創建一個Web應用的發布Image
這里以Node.js為案例,適合其他web.這里以Ubuntu 14.04 amd64.為基礎,獲得ubuntu的最新images:
$ sudo -E docker pull ubuntu:latest
啟動一個新容器:
$ sudo -E docker run -ti ubuntu bash
在這個新容器中,安裝node.js:
$ apt-get update && apt-get install -y nodejs
創建應用內容在/server.js文件中:
var http = require('http');var os = require('os'); var server = http.createServer(function (request, response) { response.writeHead(200, {"Content-Type": "text/plain"}); response.end("Hello from " + os.hostname() + "\n"); }); server.listen(8000); console.log("Server running at http://127.0.0.1:8000/");
在容器中啟動腳本/run.sh:
#! /bin/sh
/usr/bin/nodejs /server.js
$ chmod a+x /run.sh
停止這個容器,為其創建一個新的部署包image:
$ exit
# Get the ID of the container
$ sudo -E docker ps -a
# Change 3796ab3f5b76 in the following command with the ID listed above
$ sudo -E docker commit 3796ab3f5b76 local/nodeapp
# Remove the old container
$ sudo -E docker rm 3796ab3f5b76
安裝synapse
$ sudo apt-get install build-essential ruby ruby-dev haproxy
$ sudo gem install synapse
編輯/etc/default/haproxy 設置 ENABLED 為 1
啟動后端實例
啟動一個web應用容器實例:
$ sudo -E docker run -d -p 8000 local/nodeapp /run.sh
測試是否啟動成功,首先我們要獲得Docker對外暴露的端口,一般是8000,這里是49153:
$ sudo docker ps
$ curl http://127.0.0.1:49153
應該得到響應: Responds with "Hello from {container_id}"
使用Synapse自動配置HAproxy
下面到了最關鍵的服務自動發現環節。使用下面內容創建一個文件/etc/synapse.json.conf :
{ "services": { "nodesrv": { "discovery": { "method": "docker", "servers": [ { "name": "localhost", "host": "localhost" } ], "container_port": 8000, "image_name": "local/nodeapp" }, "haproxy": { "port": 8080, "listen": [ "mode http", "option httpchk /", "http-check expect string Hello" ] } } }, "haproxy": { "reload_command": "service haproxy reload", "config_file_path": "/etc/haproxy/haproxy.cfg", "do_writes": true, "do_reloads": true, "global": [ "chroot /var/lib/haproxy", "user haproxy", "group haproxy", "daemon" ], "defaults": [ "contimeout 5000", "clitimeout 50000", "srvtimeout 50000" ] } }
解釋配置如下:
1. services.nodesrv.discovery: 這是watcher的配置.這里我們使用 Docker API來發現一個名為local/nodeapp的應用容器,端口在8000.
2.services.nodesrv.haproxy: 與 HAproxy相關配置,配置其和nodesrv服務相關的端口.
3.haproxy: 由Synapse管理的全局 HAproxy 實例配置。
啟動HAproxy 和 Synapse
$ sudo service haproxy start
$ sudo synapse -c /etc/synapse.json.conf
通過HAProxy訪問測試,這時它的端口是8080
$ curl http://localhost:8080
得到的響應是: Responds Hello from {container_id}
下面再次啟動nodeapp的第二個Docker:
$ sudo -E docker run -d -p 8000 local/nodeapp /run.sh
然后在測試HAproxy,過了幾秒會發現兩個節點服務器都有了響應。
下面我們測試如果一個節點關閉,是否會影響正常訪問,這實際就是失敗容錯failover。
下面命令是每過2秒循環調用一個HAproxy:
while :do curl http://localhost:8080 sleep 2 done
然后停止其中一個容器:
$ sudo -E docker stop {container_id}
過了幾秒以后,只有一個服務器在響應。
相關參考:
(Docker類似亞馬遜中實例instance容器)。