使用Docker建立一个动态负载平衡的分布式Web系统

banq 14-08-02
                   



这是一个使用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微容器+微服务将颠覆传统的软件架构

在亚马逊AWS上扩展微服务架构(英文PPT)(Docker类似亚马逊云计算中实例instance容器)。

                   

7