Docker结合Consul实现的服务发现

如何使用Consul相关的系列工具,以使得服务发现其他的周边功能更容易实现。

 

架构回顾以及下一步要做的

在这篇文章里,我们将继续扩展之前上一篇里所创建的架构设置。在上篇里,我们借助Docker和Consul创建了如下的微服务架构:

 

 

因此,如果你还没有这样做的话,赶紧先走通上篇文章里的步骤,然后确保你已经获得了一个可以使用Docker Swarm,Docker Compose以及正确配置了Docker网络的环境吧。为了检查是否真的万事俱备,你可以通过以下命令的输出内容来做进一步的确认:

 

# 你可以先看看上篇文章的内容,了解这些命令的具体意义
#
# 首先,检查Consul的状态。如果consul还没起来的话,swarm将不会起来或者正常工作。

$ . dm-env nb-consul
$ docker ps --format '{{ .ID }}\t{{ .Image }}\t{{ .Command }}\t{{ .Names}}'

b5d55e6df248       progrium/consul "/bin/start -server -"  clever_panini

# 如果consul没有起来的话,先确保在尝试任何swarm命令前将它启动起来

$ . dm-env nb1 --swarm
$ docker ps -a --format '{{ .ID }}\t{{ .Image }}\t{{ .Command }}\t{{ .Names}}'

bf2000882dcc    progrium/consul "/bin/start -ui-dir /"  nb1/consul_agent_1  
a1bc26eef516    progrium/consul "/bin/start -ui-dir /"  nb2/consul_agent_2  
eb0d1c0cc075    progrium/consul "/bin/start -ui-dir /"  nb3/consul_agent_3  
d27050901dc1    swarm:latest    "/swarm join --advert"  nb3/swarm-agent  
f66738e086b8    swarm:latest    "/swarm join --advert"  nb2/swarm-agent  
0ac59ef54207    swarm:latest    "/swarm join --advert"  nb1/swarm-agent  
17fc5563d018    swarm:latest    "/swarm manage --tlsv"  nb1/swarm-agent-master

# 这至少会列出swarm 管理端,swarm 客户端,以及consul 客户端。
# 如果没有的话,先回过头来看下之前的文章里是怎样配置环境的。

# 最后一个我们要检查的便是网络。在swarm管理端被选举出来后,执行如下命令:

$ docker network ls | grep -i my-net
8ecec72e7b68        my-net                overlay

# 如果它显示出一个名字是my-net的覆盖网络,那我们就算是大功告成了。

 

你也许能看到一些前端和后端服务跑着,最好是把它们停掉或者删掉。这样一来你就可以照着本文接下来内容里的命令来操作了。

那么,在这篇文章里我们将会做些什么?好吧,我们将会向你展示怎样使用来自Consul世界的下列两款工具:

  • Consultemplate: 借助Consultemplate,你可以监听Consul的事件(例如一个服务被添加进来),然后基于这些更新,重写并重新加载某个配置文件。我们可以借此自动地更新一个基于HAProxy实现的反向代理的配置,刷新成最新的那批健康服务。
  • EnvConsul: 借助Envconsul,你可以轻松地直接从Consul里读取环境变量,而不必再在启动docker容器时手工将它们传进去。下面,我们将为你展示该如何把这两个工具用到我们文章里所使用的服务。

Consultemplate, docker 以及 HAProxy

在之前的配置里,我们创建了一个上图所示的架构。虽然借助Consul自带的DNS功能实现的服务发现已然运转地很好并且还附带了一些基本的故障转移支持,但是我们仍然不得不依赖于DNS以及套接字的超时,从而决定该服务是否变得不可用。虽说这个方案是可以工作的,但是它并不是非常的可靠,而且只提供了一些非常简单的故障转移和负载均衡功能。我们接下来在这块要做的便是创建如下架构:

 

这样一来我们应用的具体场景将会变成如下模式:

  1. 用户将会通过HAProxy访问到我们的前端服务;
  2. HAProxy将会把请求转发给其中一个健康的服务实体;
  3. 前端服务,将会去访问某个后端服务。这一点也是通过HAProxy的组件实现。

Consul将会确保每当一个服务向Consul注册它本身时,它将会对应更新HAProxy的配置。

它是如何工作的

如果你之前拉取过这些文章里的仓库(https://github.com/josdirksen/next-build-consul) ,你应该可以找到里面有个叫extra/consul-template的目录。该目录下即是我们这个例子里将用到的HAProxy镜像。我也已经事先将它提交到了dockerhub (https://hub.docker.com/r/josdirksen/demo-haproxy/) 里,这使得用起它来更加方便些。在我们关注需要在模板里定义些什么内容之前,先让我们一起来看看这个docker镜像实际做了什么事情吧。最简单的办法莫过于看下它的启动脚本:

#!/bin/bash

HAPROXY="/etc/haproxy"  
PIDFILE="/var/run/haproxy.pid"  
CONFIG_FILE=${HAPROXY}/haproxy.cfg

cd "$HAPROXY"

haproxy -f "$CONFIG_FILE" -p "$PIDFILE" -D -st $(cat $PIDFILE)

/usr/local/bin/consul-template -consul=${CONSUL_ADDRESS} -config=/consul.hcl

 

这里没什么特别之处,它做的事情便是当我们启动这个容器时,consul-template将会以一个特定的配置文件跑起来。值得一提的是,我们需要提供CONSUL_ADDRESS环境变量,将consul-template指向其中一个consul客户端或者服务端节点。有意思的地方便是consul.hcl文件里的内容:

max_stale = "10m"  
retry     = "10s"  
wait      = "5s:20s"

template {  
  source = "/etc/haproxy/haproxy.template"
  destination = "/etc/haproxy/haproxy.cfg"
  command = "/hap.sh"
  perms = 0600
}

 

这个文件看上去再明白不过了。基本上来说便是每当Consul监听到某处改动,haproxy.template模板将会随之重新从Consul取一次信息,然后渲染结果便会替换掉之前的haproxy.cfg文件。在那之后,将会运行一个hap.sh的命令以使得服务重新加载新的配置。hap.sh文件的完整内容看上去是这个样子:

#!/bin/bash

haproxy -f /etc/haproxy/haproxy.cfg -p /var/run/haproxy.pid -D -st $(cat /var/run/haproxy.pid)

 

这里做的事情便是干净利索地重新加载更新后的配置文件。那么,template里面的内容是什么?让我们一起来看看haproxy.template:

global  
  log 127.0.0.1 local0
  log 127.0.0.1 local1 notice
  chroot /var/lib/haproxy
  user haproxy
  group haproxy

defaults  
  log global
  mode http
  option httplog
  option dontlognull
  balance roundrobin
  timeout connect 5000
  timeout client 50000
  timeout server 50000
  errorfile 400 /etc/haproxy/errors/400.http
  errorfile 403 /etc/haproxy/errors/403.http
  errorfile 408 /etc/haproxy/errors/408.http
  errorfile 500 /etc/haproxy/errors/500.http
  errorfile 502 /etc/haproxy/errors/502.http
  errorfile 503 /etc/haproxy/errors/503.http
  errorfile 504 /etc/haproxy/errors/504.http

listen stats  
  bind *:8001
  stats enable
  stats uri /
  stats auth admin:123123q
  stats realm HAProxy\ Statistics

frontend nb-front  
  bind *:1080
  mode http
  default_backend nb-frontend

frontend nb-back  
  bind *:1081
  mode http
  default_backend nb-backend

backend nb-frontend  
    balance roundrobin{{range service "frontend-service"}}
    server {{.Node}} {{.Address}}:{{.Port}} check{{end}}

backend nb-backend  
   balance roundrobin{{range service "backend-service"}}
   server {{.Node}} {{.Address}}:{{.Port}} check{{end}}

 

上述模板里,第一段的内容看上去不是那么吸引人。而在我们定义前端和后端元素的地方它便派上用场了。这里要说明的是,我们设定了HAproxy将会在1080端口上监听来自前端服务的请求,然后将这些请求转发给定义在backend nb-frontend里的服务。在这个元素里我们配置的即是一个模板。而在这个例子里,我们从Consul取出所有名字是frontend-service的服务,然后针对每个服务写下对应的一条记录。因此当我们调用监听在1080端口的HAproxy时,它将会把请求转发到任意一个在Consul里名字注册为frontend-service的服务。针对backend-service,我们自然也是同样的做法。

让它跑起来吧!

那么,如今我们已经知道了它的工作原理,是时候实际运行这个HAproxy了。为此,我们已经事先定义好了一个docker-compose文件,它将在nb1节点上运行HAProxy服务:

version: '2'

services:  
  nb-proxy:
    image: josdirksen/demo-haproxy
    container_name: nb-haproxy
    ports:
      - 1080:1080
      - 1081:1081
    environment:
      - CONSUL_ADDRESS=192.168.99.106:8500
      - "constraint:node==nb1"

networks:  
  default:
    external:
      name: my-net

 

执行如下命令以达成这一目的:

# 确认我们是在swarm master上
$ . dm-env nb1 --swarm

# 切换到nextbuild-consul项目的根路径下
$ docker-compose -f ./docker-compose-haproxy.yml up -d
Creating nb-haproxy

$ docker ps -a --format '{{ .ID }}\t{{ .Image }}\t{{ .Command }}\t{{ .Names}}'

dc28caa4c420    josdirksen/demo-haproxy "/startup.sh"   nb1/nb-haproxy  
bf2000882dcc    progrium/consul "/bin/start -ui-dir /"  nb1/consul_agent_1  
a1bc26eef516    progrium/consul "/bin/start -ui-dir /"  nb2/consul_agent_2  
eb0d1c0cc075    progrium/consul "/bin/start -ui-dir /"  nb3/consul_agent_3  
d27050901dc1    swarm:latest    "/swarm join --advert"  nb3/swarm-agent  
f66738e086b8    swarm:latest    "/swarm join --advert"  nb2/swarm-agent  
0ac59ef54207    swarm:latest    "/swarm join --advert"  nb1/swarm-agent  
17fc5563d018    swarm:latest    "/swarm manage --tlsv"  nb1/swarm-agent-master

 

在我的环境里,我可以看到HAProxy服务已经启动了起来。但是,由于我们还没有实际运行任何后端或者前端服务,HAproxy的配置也许应该会是这个样子:

$ docker exec -ti nb1/nb-haproxy cat /etc/haproxy/haproxy.cfg | tail -n 15
frontend nb-front  
  bind *:1080
  mode http
  default_backend nb-frontend

frontend nb-back  
  bind *:1081
  mode http
  default_backend nb-backend

backend nb-frontend  
    balance roundrobin

backend nb-backend  
   balance roundrobin

 

然后,如果我们打开1080(针对前端服务)或者1081端口(针对后端API),我们将可以看到一个HAProxy抛出的报错。

这是预料之中的事情,毕竟我们还没有跑任何的前端或者后端服务。那么让我们给HAProxy提供几个一起工作的后端服务吧:

$ docker-compose -f ./docker-compose-backend.yml up -d
Creating Backend2  
Creating Backend3  
Creating Backend1

 

如今我们应该在HAProxy后面跑起来了几个后端服务。首先,让我们检查下Consul是否会真的帮我们更新HAProxy实例的配置:

$ docker exec -ti nb1/nb-haproxy cat /etc/haproxy/haproxy.cfg | tail -n 8
backend nb-frontend  
    balance roundrobin

backend nb-backend  
   balance roundrobin
   server a1bc26eef516 10.0.9.7:8081 check
   server bf2000882dcc 10.0.9.9:8081 check
   server eb0d1c0cc075 10.0.9.8:8081 check

太棒了,不是吗!HAProxy上如今已经定义了三个服务。这样一来我们应该就可以打开 http://nb1.local:1081 了,而它应该会返回某个后端服务的API:

如果你多刷新几次的话,应该可以看到它会在几个服务之间来回更替。

然后,如果我们停掉一个服务的话,我们应该可以看到它将会自动地跳过被干掉的那个:

$ docker stop nb1/Backend1
nb1/Backend1

$ curl nb1.local:1081

        {"result" : {
          "servername" : "Server2",
          "querycount" : 80
          }
        }
$ curl nb1.local:1081

        {"result" : {
          "servername" : "Server3",
          "querycount" : 86
          }
        }

 

现在,让我们一起来看看我们的前端服务是否也可以这样做。为此,我们将以如下方式启动前端组件:

$ docker-compose up -f ./docker-compose-frontend-proxy

# 别忘了我们已经干掉了一个后端服务,因此就只能看到两个后端节点了
$ docker exec -ti nb1/nb-haproxy cat /etc/haproxy/haproxy.cfg | tail -n 10
backend nb-frontend  
    balance roundrobin
    server a1bc26eef516 10.0.9.11:8090 check
    server bf2000882dcc 10.0.9.9:8090 check
    server eb0d1c0cc075 10.0.9.10:8090 check

backend nb-backend  
   balance roundrobin
   server a1bc26eef516 10.0.9.7:8081 check
   server eb0d1c0cc075 10.0.9.8:8081 check

 

并且看上去HAProxy已经正确更新了新的服务。如今我们应该可以通过1080端口上的HAproxy服务调用到某个前端服务,并且可以使用前端上UI提供的按钮调用一个可用的后端服务(同样是通过HAProxy)。

一切正如预期那样!如果你刷新这个页面的话,你将能看到它会在所有前端服务间循环往复地切换,然后当你多次点击按钮时,它便只会调用那些可用的服务。并且,正如你所想的那样,一旦我们再次启动之前干掉的那个后端服务,它将会在下一次点击按钮时重新显示在列表里:

$ docker start nb1/Backend1
nb1/Backend1

 

结果便是:

对 HAProxy 和 consul template 的总结

以上对于Docker和Consultemplate以及HAProxy如何结合使用做了一个非常快速的介绍。正如你所看到的那样,让这一切跑起来再简单不过了。一旦你配好了docker跟consul,其余的组件自然也就变得更加容易了。在实际的场景里我们也会通过Consul让HAProxy自己注册自己,这样一来其他的服务便可以轻松地发现到HAProxy实例。

在本文的最后部分里,我们不妨快速浏览一下EnvConsul提供的一些特性。

EnvConsul, 在启动一个应用时轻松注入环境参数

将配置数据传入到应用的一般做法(特别是在一个docker/微服务的架构里)通常会是使用环境变量。这是一个简单的,非侵入性,具备语言无关性的配置服务的方式。它甚至是12元素应用(12 factor app)的其中一个主题。

“十二元素应用将配置信息保存在环境变量里(通常简称为env vars或者env)。环境变量在发布过程中比较容易修改而无需变动任何代码;这不像配置文件,它们存在一定的小概率会被意外带入到代码仓库里;也不像一些自定义的配置文件,或者其他配置机制,比如Java的系统参数,它们则是一个对语言和操作系统均敏感的标准。”

然而,当你处理的是一些大型应用或者配置数据很大时,特别是在你不得不处理引号/换行符的转义的情况下,这种做法又会显得非常笨拙。

幸运的是,我们可以通过Consul来解决这个问题。如今你可能也发现了,Consul也是一个分布式的键值对存储:

 

 

借助EnvConsul,我们可以在启动应用之前从Consul的KV存储里取出信息作为环境参数。那么这是如何工作的呢?由于Envconsul只是一个我们能够运行的golang可执行程序,我们可以非常简单的验证这一点。我已经把mac版的代码放到了仓库里(在extras目录下),但是你也可以从这里下载你本地操作系统所需要的构建版本: https://releases.hashicorp.com/envconsul/0.6.1/

那么我们就运行一下它,然后看看会发生什么:

./envconsul -consul=nb-consul.local:8500 -prefix docker -once env
...
network/v1.0/endpoint/815dd44b77f391bd9a63f4e107aa1a7d3f371c91427abcf4be34493aa7ec25cd/=  
nodes/192.168.99.112:2376=192.168.99.112:2376  
swarm/nodes/192.168.99.110:2376=192.168.99.110:2376  
...

 

我已经把输出结果的大部分给省略掉了,不过基本上我们这里所做的,便是使用env-consul去获取所有它存储到docker树里的键,然后把它们以环境变量的形式添加进去。在这些都设置完成后我们执行env命令的话,它的输出即是我们所有的环境变量。如果你自己去执行这条命令的话,你将可以看到一整套的大量与docker-network和docker-swarm相关的信息集合组成的环境变量。

我们当然也可以设置几个我们自己的KV对:

 

然后当我们检索这些信息时,我们可以看到对应的值会直接传入到我们的命令里作为注入的环境变量:

$ ./envconsul -consul=nb-consul.local:8500 -prefix smartjava -once env | tail -n 2
key2=The value of key 2  
key1=The Value of Key 1

 

然而,envconsul可以做到的还远不止这些。它还提供了一个简单的针对键值对变化的响应方式。设想一下你有一个服务正在运行,它之前已经通过一个env参数在启动时配置好了。然后一旦该env参数发生了变化,你其实应该重启该服务以使得配置重新生效。这也正是consul所支持的功能:

$ cat env.sh
#!/bin/bash
env  
tail -f /dev/null

$ ./envconsul -consul=nb-consul.local:8500 -pristine -prefix=smartjava ./env.sh
PWD=/Users/jos/dev/git/nextbuild-consul/extra/envconsul  
SHLVL=1  
key2=The value of key 2  
key1=The Value of Key 1  
_=/usr/bin/env

在这个时候,进程将会一直跑着,然后envconsul将会监听等候着Consul键值存储的更新。如此一来,当我们在这里更新了一些东西时,结果便会是:

 

正如上图所能看到的,每当我们改变KV存储里的一个值,我们的脚本便会将新的配置以环境变量的形式传入,然后重启该服务。

结论

在这篇文章里,我们所展示的使用HAproxy的案例实际是非常容易办到的。你可以利用我们之前文章里看到的相同的概念和想法将此融入到你自己的架构里。把它和与ConsulTemplate结合在一起,那么一个高度可配置的,软件负载均衡的环境自然唾手可得。

另一个很酷的技术便是EnvConsul,它可以很好地跟Consul提供的分布式KV存储集成在一起。甚至于它还提供了在配置变更时重启应用的功能,这一点的确非常强大。

而这些即是本系列第二篇文章的全部内容。在这一系列的下一部分里,我们将能看到你可以通过Consul怎样简化容器的注册工作。我们在前面文章里所展示的自定义脚本也会因此被替换成更高级的方案。

发表评论