在互联网应用领域,服务的动态性需求十分常见,这就对服务的自动发现和可动态扩展提出了很高的要求。
微服务系统动辄上万个服务,而且还要动态伸缩。以人工写好的IP、Port 硬编码脚本的方式无法做到大规模自动化,稍微多点服务运维就傻了。微服务必然要做到ip和port自动分配,减少人工干预。我们需要让每个服务能动态的创建地址,同时调用方要能感知地址变化。
这就需要有一个服务注册与发现的机制,这篇文件就是讨论如何实现这个机制。
1. 服务注册发现的流程
我们做这个事情要达到的目的是:
注册发现模式 | 传统模式 |
服务启动后自动被发现 | 手动注册 |
动态变更负载均衡 | 人工写入静态配置 |
自动伸缩规模 | 运维较长时间的手动调整 |
1.1 服务 “自注册” 与 “第三方注册”。
按注册源分
1.自注册:服务内部启动客户端,连接注册中心,写入服务信息。
好处:
- 没有引入第三方,进程数量少,少依赖。
问题:
- 服务代码对注册中心进行了硬编码,若更换了注册中心,服务代码也必须跟着调整;
- 注册中心必须与每个服务都保持通信,来做心跳检测。如果服务很多时,对注册中心也是一种额外的开销;
2.第三方注册(本文采用方式):采用协同进程的方式,监听服务进程的变化,将服务信息写入注册中心。
- 好处:做到了服务与注册中心的解耦,对服务而言,完成了服务的自动化注册;
- 问题:协同进程本身也要考虑高可用,否则将成为单点故障的风险点;
1.2 自注册的实现
自注册不是我们本篇要讨论的,可以自己写代码实现,我们讨论第三方注册的实现。
1.3 第三方注册的实现
Docker 的出现,以及微服务架构的兴起,让众多开源项目开始关注在松耦合的架构前提下,如何基于 Docker 实现一套真正可动态扩展的服务架构。
这里我们使用 Registrator + Consul + Consul-template + Nginx 这几个开源组件来实现可动态扩展的服务注册与发现机制,当然,毫无疑问他们都跑在docker上。
首先看看流程:
服务注册中心:作为整个架构中的核心,要支持分布式、持久化存储,注册信息变动实时通知消费者。
服务提供者:服务以 docker 容器化方式部署(实现服务端口的动态生成),并以 docker-compose 的方式来管理,通过 Registrator 可以检测到docker进程信息以完成服务的自动注册。
服务消费者:要使用服务提供者提供的服务,和服务提供者往往是动态相互转位置的。
- 服务注册:服务提供者到注册中心注册;
- 服务订阅:服务消费者到注册中心订阅服务信息,对其进行监听;
- 缓存:本地缓存服务列表,减少与注册中心的网络通信;
- 服务调用:先查找本地缓存,找不到再去注册中心拉取服务地址,然后发送服务请求;
- 变更通知:服务节点变动时(新增、删除等),注册中心将通知监听节点,更新服务信息。
2. 工具介绍
2.1 Registrator
Registrator:一个由Go语言编写的,针对docker使用的,通过检查本机容器进程在线或者停止运行状态,去注册服务的工具。所以我们要做的实验,所有的工具都是在docker上运行的,就是因为registrator是通过检查docker容器的状态来判断服务状态的,这样就和我们的代码实现完全解耦了,对上层透明化,无感知。它有如下特点
- 通过docker socket直接监听容器event,根据容器启动/停止等event来注册/注销服务
- 每个容器的每个exposed端口对应不同的服务
- 支持可插拔的registry backend,默认支持Consul, etcd and SkyDNS
- 自身也是docker化的,可以容器方式启动
- 用户可自定义配置,如服务TTL(time-to-live)、服务名称、服务tag等
2.1 consul
我们上图所说的服务注册中心,就是这玩意。Consul 是一个分布式高可用的服务发现和配置共享的软件。由 HashiCorp 公司用 Go 语言开发。
Consul在这里用来做 docker 实例的注册与配置共享。
特点:
- 一致性协议采用 Raft 算法,比Paxos算法好用. 使用 GOSSIP 协议管理成员和广播消息, 并且支持 ACL 访问控制.
- 支持多数据中心以避免单点故障,内外网的服务采用不同的端口进行监听。而其部署则需要考虑网络延迟, 分片等情况等.zookeeper 和 etcd 均不提供多数据中心功能的支持.
- 健康检查. etcd 没有的.
- 支持 http 和 dns 协议接口. zookeeper 的集成较为复杂, etcd 只支持 http 协议.
- 还有一个web管理界面。
2.3 consul-template
一开始构建服务发现,大多采用的是zookeeper/etcd+confd。但是复杂难用。consul-template,大概取代了confd的位置,以后可以这样etcd+confd或者consul+consul-template。
consul template的使用场景:consul template可以查询consul中的服务目录、key、key-values等。这种强大的抽象功能和查询语言模板可以使consul template特别适合动态的创建配置文件。例如:创建apache/nginx proxy balancers、haproxy backends、varnish servers、application configurations。
consul-template提供了一个便捷的方式从consul中获取存储的值,consul-template守护进程会查询consul服务,来更新系统上指定的任何模板,当更新完成后,模板可以选择运行一些任意的命令,比如我们这里用它来更新nginx.conf这个配置文件,然后执行nginx -s reload命令,以更新路由,达到动态调节负载均衡的目的。
consul-template和nginx必须装到一台机器,因为consul-template需要动态修改nginx配置文件
2.4 nginx
这个耳熟能详的名字,不用过多介绍了,它在这里就是做负载均衡,转发请求用的。当然最擅长负载均衡的是直接用硬件,软件做性能比不上。但软件成本低、维护方便。
3. 单机实验
首先看一个简单的传统负载均衡web服务
这个很好理解吧,client访问nginx,然后被转发到后端某一个web server上,传统的负载均衡。如果后端有添加/删除web server,运维手动改下nginx.conf,然后重新载入配置,就可以调整负载均衡了。
再看看我们基于微服务自动注册和发现模式下的负载均衡:
负载均衡的方式没有变,只是多了一些外围的组件,当然这些组件对client是不可见的,client依然只能看到nginx入口,访问方式也没变化。
其中,我们用registrator来监控每个web server的状态。当有新的web server启动的时候,registrator会把它注册到consul这个注册中心上。由于consul_template已经订阅了该注册中心上的服务消息,此时consul注册中心会将新的web server信息推送给consul_template,consul_template则会去修改nginx.conf的配置文件,然后让nginx重新载入配置以达到自动修改负载均衡的目的。同样当一个web server挂了,registrator也能感知到,进而通知consul做出响应。
整个过程不需要运维人工的干预,自动完成。接下来我们找一台机器上实践下这个方案
3.1 环境
header | header |
操作系统 | ubuntu:16.04 x86_64,内核:4.8.0-58-generic |
主机ip | 10.111.152.136 |
docker | Docker version 1.12.6, build 78d1802 |
docker-compose | docker-compose version 1.8.0, build unknown |
首先安装 docker 和 docker-compose
$ apt-get install docker docker-compose -y
随便找个目录,创建模板文件 docker-compose.yml
#backend web application, scale this with docker-compose scale web=3
web:
image: liberalman/helloworld:latest
environment:
SERVICE_80_NAME: my-web-server
SERVICE_TAGS: backend-1
MY_HOST: host-1
ports:
- "80"
#load balancer will automatically update the config using consul-template
lb:
image: liberalman/nginx-consul-template:latest
hostname: lb
links:
- consulserver:consul
ports:
- "80:80"
consulserver:
image: progrium/consul:latest
environment:
SERVICE_TAGS: consul servers
hostname: consulserver
ports:
- "8300"
- "8400"
- "8500:8500"
- "53"
command: -server -ui-dir /ui -data-dir /tmp/consul -bootstrap-expect 1
# listen on local docker sock to register the container with public ports to the consul service
registrator:
image: gliderlabs/registrator:master
hostname: registrator
links:
- consulserver:consul
volumes:
- "/var/run/docker.sock:/tmp/docker.sock"
command: -internal consul://consul:8500
注意: liberalman/helloworld和liberalman/nginx-consul-template这两个镜像我已经实现了,可以pull下来,大家可以直接使用。想要看他们怎么写的,访问https://github.com/liberalman
3.2 启动
进入模板所在目录,执行
$ docker-compose up
没问题的话就启动成功了,其中的镜像自动被下。访问 http://localhost 可以看到一个 web 页面:
Hello World! I’m host-1 addr:172.17.0.2. I saw that you are 172.17.0.6:35612.
这个内容实际是后端web服务器helloworld所反馈的页面,它告诉我们它自己的地址是172.17.0.2(docker的内网地址),它所看到的前端访问过来的ip是172.17.0.6,实际上这个前端是我们的nginx的负载均衡的代理转发的,所以它看到的实际是nginx的地址。
这里的host-1是我自己设置的物理机的名称,注释不是操作系统那hostname,纯粹是为了在页面上好显示,以及后期多个物理机实验的时候好区分不同物理机器,所以自定义了一个临时名称。它对应docker-compose.yml中的MY_HOST环境变量,会通过docker容器传递到helloworld的运行环境中。
要停止服务Ctrl + C就行了,如果有些没有停止,则
$ docker-compose down
如果要在后台运行
$ docker-compose up -d
3.3 负载均衡
回到正题,在浏览器上多次刷新,可以看到后端地址没有变化,这是因为只有一个 web 后端服务器。
如果要测试一下nginx负载均衡的效果,则调整后端为 3 个服务器。先停掉服务,然后
$ docker-compose scale web=3
$ docker-compose up
再次访问 http://localhost ,多次刷新,可以看到页面的实际目标地址发生了变化,有3个ip轮换。新启动的 web 后端服务器被自动注册,并且 nginx 也把新的路由添加上了:
Hello World! I’m host-1 addr:172.17.0.2. I saw that you are 172.17.0.6:36710.
Hello World! I’m host-1 addr:172.17.0.3. I saw that you are 172.17.0.6:35210.
Hello World! I’m host-1 addr:172.17.0.4. I saw that you are 172.17.0.6:58678.
3.4 查看服务状态
要查看节点注册状况,到 http://localhost:8500 可以看到 consul web ui 的管理端
点击SERVICES这个按钮,列出所有被注册的服务。
- consul server,看到有多个是因为监听多个端口,还有udp端口的。
- my-web-server就是后端web服务,这个名称是要在docker-compose模板中设置SERVICE_80_NAME这个变量的,针对80端口,详情见registrator 用户指导手册
• https://gliderlabs.com/registrator/latest/user/services/。
- nginx-consul-template就是nginx和consul-template的合体服务。
点击my-web-server,可以看到它右侧的服务节点数,这里只有一个,有多个的话会依次列出
4. 两台物理机
以上都是在单台物理机上完成的,下面我们要测试下多台物理机情况下,真正分布式的效果。
host name | real ip | services |
host-1 | 10.111.152.136 | registrator、helloworld、consul-server、consul-template、nginx |
host-2 | 10.111.152.135 | registrator、helloworld |
第一台物理机host-1的docker-compse.yml
#backend web application, scale this with docker-compose scale web=3
web:
image: liberalman/helloworld:latest
environment:
SERVICE_80_NAME: my-web-server
SERVICE_TAGS: backend-1
MY_HOST: host-1
ports:
- "80"
#load balancer will automatically update the config using consul-template
lb:
image: liberalman/nginx-consul-template:latest
hostname: lb
links:
- consulserver:consul
ports:
- "80:80"
consulserver:
image: progrium/consul:latest
environment:
SERVICE_TAGS: consul servers
hostname: consulserver-node1
ports:
- "8300"
- "8400"
- "8500:8500"
- "53"
command: -server -ui-dir /ui -data-dir /tmp/consul -bootstrap-expect 1
# listen on local docker sock to register the container with public ports to the consul service
registrator:
image: gliderlabs/registrator:master
hostname: registrator-1
volumes:
- "/var/run/docker.sock:/tmp/docker.sock"
command: -ip=10.111.152.136 consul://10.111.152.136:8500
我们第二台机器host-2的yml文件:
#backend web application, scale this with docker-compose scale web=3
web:
image: liberalman/helloworld:latest
environment:
SERVICE_80_NAME: my-web-server
SERVICE_TAGS: backend-2
MY_HOST: host-2
ports:
- "80"
# listen on local docker sock to register the container with public ports to the consul service
registrator:
image: gliderlabs/registrator:master
hostname: registrator-2
volumes:
- "/var/run/docker.sock:/tmp/docker.sock"
command: -ip 10.111.152.135 consul://10.111.152.136:8500
这是我们将MY_HOST改为host-2了,以便在页面查看的时候可以直观看到。另外的重要改变就是registrator的启动参数,我们去掉了上报docker内部ip的-internal,转而使用了外部ip,将自己本机的ip 10.111.152.135上报了。同时要访问的consul服务器参数配置成host-1的ip地址 10.111.152.136。还有registrator的hostname要和第一台机器的区别开,我改成registrator-2了,这样在注册到consul中的时候,不会覆盖掉。hostname一样consul无法区分是哪个机器的,这样两个机器的registrator会相互覆盖。
host-1启动方式不变,我们现在到host-2上启动,看看效果,是否新节点被加上了。
Hello World! I’m host-1 addr:172.17.0.2. I saw that you are 172.17.0.5:41464.
Hello World! I’m host-2 addr:172.17.0.2. I saw that you are 10.111.152.136:41578.
刷新两次,发现一会儿是host-1,一会儿是host-2,说明我们host-2物理机上的服务被添加进来了,并且被nginx路由到了。
同时consul ui,看到新的节点果然被添加上了
不过发现个问题,如果在host-2上先将registrator关闭,再关闭host-2上的后端web,我们的consul服务器可以感知到,但是那个consul ui界面没更新,依然显示两个节点。
5. Consul Cluster
以上我们的实验其实是个单点的consul服务,点击consul ui页面的NODES按钮可以看到
只有一个consul server节点,也就是在我们host-1上跑的节点consulserver,另外一个物理机上没有运行consul节点。一旦它挂了整个服务注册功能就歇菜了。既然是分布式,一定要发挥集群的优势以解决单点问题。所以,我们要建立Consul Cluster。
在Consul方案中,每个提供服务的节点上都要部署和运行一个agent,所有运行Consul agent节点的集合构成Consul Cluster。
Consul agent有两种运行模式:Server和Client。这里的Server和Client只是Consul集群层面的区分,与搭建在Cluster之上的应用服务无关。
以Server模式运行的Consul agent节点用于维护Consul集群的状态,官方建议每个Consul Cluster至少有3个或以上的运行在Server mode的Agent,Client节点不限。
每个数据中心的Consul Cluster都会在运行于server模式下的agent节点中选出一个Leader节点,这个选举过程通过Consul实现的raft协议保证,多个 server节点上的Consul数据信息是强一致的。处于client mode的Consul agent节点比较简单,无状态,仅仅负责将请求转发给Server agent节点。
我们这次的架构有些调整,绘制一个服务器的逻辑上的部署图来说明下
这是一张逻辑上服务部署的图,我们找3台机器来实验。每台机器上部署几个web server,一个registrator和一个consul client,这是基本需求。另外再建立一个consul cluster集群,用来当我们的注册中心。当web server启动后,被registrator感知,进而将注册信息发送给consul client,consul client则访问注册中心的leader节点,上报新加入的服务信息。consul cluster会将新的服务信息推送给已经到它这里订阅了服务消息的consul-template,consul-template再去修改和自己同一台机器上的nginx,以达到动态调整负载均衡的目的。
注意:由于资源有限,我们没有单独使用机器去搭建consul集群,所以图中的consul client和consul server节点其实是同一个节点,因为server模式同时可以提供client的功能嘛。那个consul cluster集群其实是分布到3个host中建立起来的,我们就在3个host中分别启动一个consul进程,每个都同时担任server和client的功能。
5.1 配置
host name | real ip | services | note |
host-1 | 10.111.152.136 | registrator、helloworld*n、consul-server、consul-template、nginx | 放置consol web ui和nginx负载均衡 |
host-2 | 10.111.152.135 | registrator、helloworld*n、consul-server | |
host-3 | 10.111.152.168 | registrator、helloworld*n、consul-server |
host-1作为运行负载均衡的机器,部署consul-template和nginx。每个机器上都部署了consul-server节点,也就是我们有3个节点,接下来就研究这3个节点是如何选举leader的。
host-1的docker-compose.yml文件
#backend web application, scale this with docker-compose scale web=3
web:
image: liberalman/helloworld:latest
environment:
SERVICE_80_NAME: my-web-server
SERVICE_TAGS: backend-1
MY_HOST: host-1
ports:
- "80"
#load balancer will automatically update the config using consul-template
lb:
image: liberalman/nginx-consul-template:latest
hostname: lb
links:
- consulserver:consul
ports:
- "80:80"
consulserver:
image: progrium/consul:latest
environment:
SERVICE_TAGS: consul servers
hostname: consulserver-node1
ports:
- "8300:8300"
- "8301:8301"
- "8301:8301/udp"
- "8302:8302"
- "8302:8302/udp"
- "8400:8400"
- "8500:8500"
- "53:53/udp"
command: -server -ui-dir /ui -advertise 10.111.152.136 -bootstrap-expect 3
# listen on local docker sock to register the container with public ports to the consul service
registrator:
image: gliderlabs/registrator:master
hostname: registrator-1
volumes:
- "/var/run/docker.sock:/tmp/docker.sock"
command: -ip 10.111.152.136 consul://10.111.152.136:8500
参数解释下
- hostname,将来consul节点都靠这个来标识了,所以每个物理机上的节点名称都要区别开,以免冲突。
- -bootstrap-expect 3,这个参数的作用是,当consulserver-node1节点启动之后,等待另外两个节点的加入,3个节点聚齐后,之后才开始选举leader。
- -advertise 10.111.152.136,如果要让节点在WAN网络中被发现,就要配置这个参数,暴露出外网ip。如果只在LAN中被发现,就不用配置这个了,默认绑定内网ip。
- -ui-dir /ui,这个配置是指定当前节点支持consul ui的web页面。
host-2的docker-compose.yml文件
#backend web application, scale this with docker-compose scale web=3
web:
image: liberalman/helloworld:latest
environment:
SERVICE_80_NAME: my-web-server
SERVICE_TAGS: backend-2
MY_HOST: host-2
ports:
- "80"
consulserver:
image: progrium/consul:latest
environment:
SERVICE_TAGS: consul servers
hostname: consulserver-node2
ports:
- "8300:8300"
- "8301:8301"
- "8301:8301/udp"
- "8302:8302"
- "8302:8302/udp"
- "8400:8400"
- "8500:8500"
- "53:53/udp"
command: -server -advertise 10.111.152.135 -join 10.111.152.136
# listen on local docker sock to register the container with public ports to the consul service
registrator:
image: gliderlabs/registrator:master
hostname: registrator-2
volumes:
- "/var/run/docker.sock:/tmp/docker.sock"
command: -ip 10.111.152.135 consul://10.111.152.136:8500
与host-1不同的是,host-2使用了参数
-join 10.111.152.136 意思是把本节点加入到10.111.152.136这个ip的节点中,这是consulserver-node1的地址。我们上一个host的配置中表明,consulserver-node1这个节点启动后,会等待另外两个节点的加入,我们这里就是加入它。
host-3的docker-compose.yml文件
#backend web application, scale this with docker-compose scale web=3
web:
image: liberalman/helloworld:latest
environment:
SERVICE_80_NAME: my-web-server
SERVICE_TAGS: backend-3
MY_HOST: host-3
ports:
- "80"
consulserver:
image: progrium/consul:latest
environment:
SERVICE_TAGS: consul servers
hostname: consulserver-node3
ports:
- "8300:8300"
- "8301:8301"
- "8301:8301/udp"
- "8302:8302"
- "8302:8302/udp"
- "8400:8400"
- "8500:8500"
- "53:53/udp"
command: -server -advertise 10.111.152.168 -join 10.111.152.136
# listen on local docker sock to register the container with public ports to the consul service
registrator:
image: gliderlabs/registrator:master
hostname: registrator-3
volumes:
- "/var/run/docker.sock:/tmp/docker.sock"
command: -ip 10.111.152.168 consul://10.111.152.136:8500
注意:到这里你可能有疑问,上文的3个节点都是server节点,那client节点哪里去了,没有client节点怎么访问集群啊?我们和集群交互可是访问client,client再转发到server节点的。
我们前篇也提到过,其实每个server节点,本身就具有client的功能,只是多了一些把所有的信息持久化的本地以及选举leader的功能呢,这样遇到故障,信息是可以被保留的。
所以,这里我们每个主机上部署registrator的时候,配置的访问consul服务的地址也是就近访问本机上的consul节点,把它当成一个consul client访问就可以了。当然也可以单独部署一个client节点,只是我们至少要保证有3个server节点,才能完成leader选举,如果再多一台机器我会考虑专门加一个client节点。
5.2 启动
依次在host-1、host-2和host-3上启动3个节点。注意执行docker-compose up之后,不要关闭终端,让它一直打印,后续我们还要在这里看日志,别的操作都转到新开终端上执行。访问 http://10.111.152.136:8500/ui/#/dc1/nodes 看到节点都被添加上了
除了查看ui界面外,也可以使用命令行看看有哪些服务注册了,在新终端下执行
~# curl 10.111.152.136:8500/v1/catalog/services|jq .
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 308 100 308 0 0 54892 0 --:--:-- --:--:-- --:--:-- 61600
{
"consul": [],
"consul-53": [
"consul servers",
"udp"
],
"consul-8300": [
"consul servers"
],
"consul-8301": [
"consul servers",
"udp"
],
"consul-8302": [
"consul servers",
"udp"
],
"consul-8400": [
"consul servers"
],
"consul-8500": [
"consul servers"
],
"my-web-server": [
"backend-1",
"backend-2",
"backend-3"
],
"nginx-consul-template": []
}
访问 http://10.111.152.136 查看nginx负载均衡的效果,依次刷新,得到
Hello World! I’m host-1 addr:172.17.0.2. I saw that you are 172.17.0.1:49728.
Hello World! I’m host-2 addr:172.17.0.3. I saw that you are 10.111.152.136:54640.
Hello World! I’m host-3 addr:172.17.0.3. I saw that you are 10.111.152.136:58660.
OK,看起来一切正常。那我们现在分析下到底哪个节点是leader,有节点退出会怎样?
现在新开一个终端,在host-1上,执行
docker ps -f name=consul
查到consul节点的容器id是4364cd41f2ba。登录这个容器
docker exec -it 4364cd41f2ba /bin/sh
然后就进入容器的操作系统环境了,在该环境下执行
/ # consul members
Node Address Status Type Build Protocol DC
consulserver-node3 10.111.152.168:8301 alive server 0.5.2 2 dc1
consulserver-node1 10.111.152.136:8301 alive server 0.5.2 2 dc1
consulserver-node2 10.111.152.135:8301 alive server 0.5.2 2 dc1
一目了然的看到了我们的3个consul节点。查看当前节点信息
/ # consul info
......
consul:
bootstrap = false
known_datacenters = 1
leader = false
server = true
raft:
applied_index = 192
commit_index = 192
fsm_pending = 0
last_contact = 15.960533ms
last_log_index = 192
last_log_term = 2
last_snapshot_index = 0
last_snapshot_term = 0
num_peers = 2
state = Follower
term = 2
......
输出信息很多,省略掉,只给出重要的。server = true确实是server节点。看到leader=false,说明这个节点不是leader。state = Follower,看来确实是个Follower节点哦。last_contact = 15.960533ms心跳剩余时间,term = 2说是第二个term,已经选过2回了。
执行上述命令的同时,由于之前在host-1上执行docker-compose up命令的时候,日志是直接输出到屏幕上的,我们此时可以节点1输出的日志
consulserver_1 | 2017/07/26 05:08:20 [INFO] agent.rpc: Accepted client: 127.0.0.1:47084
consulserver_1 | 2017/07/26 05:08:24 [INFO] agent.rpc: Accepted client: 127.0.0.1:47086
......
我们刚才执行的命令都是客户端发到当前consul server上的。
同样的方式,在节点在conserver-node3上
consul:
bootstrap = false
known_datacenters = 1
leader = true
server = true
原来leader是节点3.
5.3 去掉节点
让一个节点挂掉,看看会发生什么。
5.3.1 关闭一个节点
在host-1上新开终端执行
docker stop 4364cd41f2ba
看到host-1的日志滚动
gocode_consulserver_1 exited with code 1
lb_1 | 2017/07/26 06:02:51.211894 [WARN] (view) health.service(my-web-server|passing): Get http://consul:8500/v1/health/service/my-web-server?index=40&passing=1&stale=&wait=60000ms: dial tcp 172.17.0.4:8500: i/o timeout (retry attempt 1 after "250ms")
ex=40&passing=1&stale=&wait=60000ms: dial tcp 172.17.0.4:8500: i/o timeout (retry attempt 1 after "250ms")
lb_1 | 2017/07/26 06:03:10.099572 [WARN] (view) health.service(my-web-server|passing): Get http://consul:8500/v1/health/service/my-web-server?index=40&passing=1&stale=&wait=60000ms: dial tcp 172.17.0.4:8500: getsockopt: no route to host (retry attempt 2 after "500ms")
......
lb_1会不断的打印重试到http://consul:8500的健康检查。
不过此时访问 http://10.111.152.136 发现nginx并没有被破坏,还是可以正常路由到后端三个节点的,后端的web server也正常可用。没有受到一个consul server节点挂掉的影响。
只是consul web ui无法访问了,http://10.111.152.136:8500/ui/#/dc1/services 因为刚好把这个节点停掉了。
另外两个节点的日志情况
host-2机器上,consulserver-node2节点,也是一个Follower状态的节点上
consulserver_1 | 2017/07/26 06:02:24 [INFO] memberlist: Suspect consulserver-node1 has failed, no acks received
consulserver_1 | 2017/07/26 06:02:27 [INFO] memberlist: Suspect consulserver-node1 has failed, no acks received
consulserver_1 | 2017/07/26 06:02:27 [INFO] memberlist: Marking consulserver-node1 as failed, suspect timeout reached
consulserver_1 | 2017/07/26 06:02:27 [INFO] serf: EventMemberFailed: consulserver-node1 10.111.152.136
consulserver_1 | 2017/07/26 06:02:27 [INFO] consul: removing server consulserver-node1 (Addr: 10.111.152.136:8300) (DC: dc1)
consulserver_1 | 2017/07/26 06:03:19 [INFO] serf: attempting reconnect to consulserver-node1 10.111.152.136:8301
consulserver_1 | 2017/07/26 06:03:49 [INFO] serf: attempting reconnect to consulserver-node1 10.111.152.136:8301
consulserver_1 | 2017/07/26 06:05:19 [INFO] serf: attempting reconnect to consulserver-node1 10.111.152.136:8301
......
每隔30s尝试重连node1.
host-3机器上,consulserver-node3节点,我们的leader
consulserver_1 | 2017/07/26 06:02:21 [INFO] raft: aborting pipeline replication to peer 10.111.152.136:8300
consulserver_1 | 2017/07/26 06:02:21 [ERR] raft: Failed to AppendEntries to 10.111.152.136:8300: EOF
consulserver_1 | 2017/07/26 06:02:21 [ERR] raft: Failed to heartbeat to 10.111.152.136:8300: dial tcp 10.111.152.136:8300: connection refused
consulserver_1 | 2017/07/26 06:02:21 [ERR] raft: Failed to AppendEntries to 10.111.152.136:8300: dial tcp 10.111.152.136:8300: connection refused
consulserver_1 | 2017/07/26 06:02:21 [ERR] raft: Failed to heartbeat to 10.111.152.136:8300: dial tcp 10.111.152.136:8300: connection refused
consulserver_1 | 2017/07/26 06:02:21 [ERR] raft: Failed to AppendEntries to 10.111.152.136:8300: dial
......
也在尝试重连,而且它间隔2s就尝试一次,频率上更快。由于一直连不上,后来干脆去掉node1节点了。
consulserver_1 | 2017/07/26 06:02:27 [INFO] memberlist: Suspect consulserver-node1 has failed, no acks received
consulserver_1 | 2017/07/26 06:02:27 [INFO] memberlist: Marking consulserver-node1 as failed, suspect timeout reached
consulserver_1 | 2017/07/26 06:02:27 [INFO] serf: EventMemberFailed: consulserver-node1 10.111.152.136
consulserver_1 | 2017/07/26 06:02:27 [INFO] consul: removing server consulserver-node1 (Addr: 10.111.152.136:8300) (DC: dc1)
不过虽然去掉了node1,但是其他节点依然没有放弃尝试重连node1。重连的操作一直都在继续中。
5.3.2 恢复节点
把刚才在host-1上关闭的容器重新启动
docker start 4364cd41f2ba
看看3个机器都会输出什么。
host-1上
consulserver_1 | ==> Starting raft data migration...
consulserver_1 | ==> Starting Consul agent...
consulserver_1 | ==> Starting Consul agent RPC...
consulserver_1 | ==> Consul agent running!
consulserver_1 | Node name: 'consulserver-node1'
consulserver_1 | Datacenter: 'dc1'
consulserver_1 | Server: true (bootstrap: false)
consulserver_1 | Client Addr: 0.0.0.0 (HTTP: 8500, HTTPS: -1, DNS: 53, RPC: 8400)
consulserver_1 | Cluster Addr: 10.111.152.136 (LAN: 8301, WAN: 8302)
consulserver_1 | Gossip encrypt: false, RPC-TLS: false, TLS-Incoming: false
consulserver_1 | Atlas: <disabled>
......
consulserver_1 | 2017/07/26 06:23:11 [INFO] consul: New leader elected: consulserver-node2
......
consulserver-node1节点又重新启动了,并且整个集群选举了新的leader上来:consulserver-node2
host-2上
consulserver_1 | 2017/07/26 06:23:05 [INFO] consul: adding server consulserver-node1 (Addr: 10.111.152.136:8300) (DC: dc1)
......
consulserver_1 | 2017/07/26 06:23:11 [INFO] consul: New leader elected: consulserver-node2
......
感知到了consulserver-node1的复活,并且也参与了选举,选出新leader,是自己,哈哈。
host-3上
consulserver_1 | 2017/07/26 06:23:05 [INFO] consul: adding server consulserver-node1 (Addr: 10.111.152.136:8300) (DC: dc1)
......
consulserver_1 | 2017/07/26 06:23:11 [INFO] consul: New leader elected: consulserver-node2
......
同样感知到了consulserver-node1的复活,并且也参与了选举,选出新leader。
此时ngxin依然没受影响,web服务正常。而且consul web ui也可以正常访问了。一切都恢复如初。具体这3个节点是如何选举leader和处理节点的退出和重入的,参考我另外一篇文章 consul实现的raft算法选举过程解析。
创建于 2017-07-23 北京,更新于 2017-07-27 北京
该文章在以下平台同步
- LIBERALMAN:
- CSDN:
- 简书:
- [1] 引用 http://tonybai.com/2015/07/06/implement-distributed-services-registery-and-discovery-by-consul/
- [2] 引用 http://alice.blog.51cto.com/707092/1896078
附录:
文中架构图都是用graphviz绘制的,附上图源码
digraph G {
size="6,6";
label="services register"
node [colorscheme=paired12, color=1, style=filled];
register_center [label="注册中心", color=5, shape="record"]
consumer [label="服务消费者", color=4, shape="record"]
service [label="服务提供者", color=2, shape="record"]
consumer -> register_center [label="2.订阅"]
register_center -> consumer [label="5.通知" style=dashed]
consumer -> service [label="4.调用"]
consumer -> consumer [label="3.缓存" style=dashed]
service -> register_center [label="1.注册"]
}
digraph G {
size="6,6";
label="load balance web servers"
node [colorscheme=paired12, color=1, style=filled];
nginx [label="nginx", color=3, shape="record"]
my_web_server_1 [label="my_web_server_1", color=4, shape="record"]
my_web_server_2 [label="my_web_server_2", color=4, shape="record"]
my_web_server_3 [label="my_web_server_3", color=4, shape="record"]
{Client1 Client2 Client3} -> nginx [label="访问"]
nginx -> {my_web_server_1 my_web_server_2 my_web_server_3} [label="转发"]
}
digraph G {
size="6,6";
label="Services register and find"
node [colorscheme=paired12, color=1, style=filled];
consul [label="consul", color=1]
consul_template [label="consul_template", color=2]
nginx [label="nginx", color=3, shape="record"]
registrator [label="registrator", color=5]
my_web_server_1 [label="my_web_server_1", color=4, shape="record"]
my_web_server_2 [label="my_web_server_2", color=4, shape="record"]
my_web_server_3 [label="my_web_server_3", color=4, shape="record"]
{Client1 Client2 Client3} -> nginx [label="访问"]
nginx -> {my_web_server_1 my_web_server_2 my_web_server_3} [label="转发"]
{my_web_server_1 my_web_server_2 my_web_server_3} -> registrator [color="red",style="dashed",label="监控"]
registrator -> consul [color="red",style="dashed",label="注册"]
consul -> consul_template [dir=both color=red style="dashed" label="订阅服务"]
consul_template -> nginx [color=red,style="dashed",label="配置更新"]
}
digraph G {
size="6,6";
label="Services register and find, consul cluster"
node [colorscheme=paired12, color=1, style=filled];
consul_node1 [label="consul_node1(leader)", color=7]
consul_node2 [label="consul_node2", color=7]
consul_node3 [label="consul_ndoe3", color=7]
consul_client1 [label="consul_client1", color=7]
consul_client2 [label="consul_client2", color=7]
consul_client3 [label="consul_client3", color=7]
consul_template [label="consul_template", color=2]
nginx [label="nginx", color=3, shape="record"]
registrator_1 [label="registrator_1", color=5]
registrator_2 [label="registrator_2", color=5]
registrator_3 [label="registrator_3", color=5]
my_web_server_1 [label="my_web_server_1", color=4, shape="record"]
my_web_server_2 [label="my_web_server_2", color=4, shape="record"]
my_web_server_3 [label="my_web_server_3", color=4, shape="record"]
my_web_server_4 [label="my_web_server_4", color=4, shape="record"]
my_web_server_5 [label="my_web_server_5", color=4, shape="record"]
my_web_server_6 [label="my_web_server_6", color=4, shape="record"]
{Client1 Client2 Client3} -> nginx [label="访问"]
nginx -> {my_web_server_1 my_web_server_2 my_web_server_3 my_web_server_4 my_web_server_5 my_web_server_6} [label="转发"]
{my_web_server_1 my_web_server_2} -> registrator_1 [color="red",style="dashed",label="监控"]
{my_web_server_3 my_web_server_4} -> registrator_2 [color="red",style="dashed",label="监控"]
{my_web_server_5 my_web_server_6} -> registrator_3 [color="red",style="dashed",label="监控"]
registrator_1 -> consul_client1 [color="red",style="dashed",label="注册"]
registrator_2 -> consul_client2 [color="red",style="dashed",label="注册"]
registrator_3 -> consul_client3 [color="red",style="dashed",label="注册"]
{consul_client1 consul_client2 consul_client3} -> consul_node1 [color="red",style="dashed",label="注册"]
consul_node1 -> consul_node2 -> consul_node3 [dir=both style=dashed color=blue]
consul_node1 -> consul_template [dir=both color=red style="dashed" label="订阅服务"]
consul_template -> nginx [color=red,style="dashed",label="配置更新"]
subgraph cluster_host_1 {
label="host_1"
my_web_server_1
my_web_server_2
registrator_1
consul_client1
}
subgraph cluster_host_2 {
label="host_2"
my_web_server_3
my_web_server_4
registrator_2
consul_client2
}
subgraph cluster_host_3 {
label="host_3"
my_web_server_5
my_web_server_6
registrator_3
consul_client3
}
subgraph cluster_clu {
label="consul cluster"
consul_node1
consul_node2
consul_node3
}
}