Docker Swarm - 服务发现和负载均衡原理

本文将介绍基于 DNS 的负载均衡、基于 VIP 的负载均衡和路由网格(Routing Mesh)。

1. 使用的技术

Docker 使用了 Linux 内核 iptables 和 IPVS 的功能来实现服务发现和负载均衡。

  • iptables 是 Linux 内核中可用的包过滤技术,它可用于根据数据包的内容进行分类、修改和转发决策。
  • IPVS 是 Linux 内核中可用的传输级负载均衡器。

2. 准备工作

  • swarm 集群:【Manager】node1、【Worker】node2
  • 客户端镜像: registry.cn-hangzhou.aliyuncs.com/anoy/ubuntu
  • 服务端镜像: registry.cn-hangzhou.aliyuncs.com/anoy/vote

img

如图所示,我们将在 swarm 集群中部署 “client” 服务 和 “vote” 服务,其中 “vote” 服务部署多个副本。客户端请求 “vote” 服务时,输出结果中包含服务端的容器 ID,这样就更方便演示网络请求。

集群状态

1
2
3
4
[root@node1 ~]# docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
rnr2i1y2of3n5vy2vzh2vkzq0 * node1 Ready Active Leader 18.03.1-ce
qvik057dvphx5s06evmswahaf node2 Ready Active 18.03.1-ce

使用如下命令,创建 overlay 网络:

1
docker network create --driver overlay overlay1

3. 基于 DNS 的负载均衡

下图描述了基于 DNS 的负载均衡是如何工作的:

img

DNS server 内嵌于 Docker 引擎。Docker DNS 解析服务名 “vote” 并返回容器 ID 地址列表(随机排序)。客户端通常会挑第一个 IP 访问,因此负载均衡可能发生在服务器的不同实例之间。

使用如下命令创建 2 个基于 DNS 负载均衡的服务 “client” 、 “vote”:

1
2
3
docker service create --endpoint-mode dnsrr --replicas 1 --name client --network overlay1 registry.cn-hangzhou.aliyuncs.com/anoy/ubuntu ping anoyi.com

docker service create --endpoint-mode dnsrr --name vote --network overlay1 --replicas 2 registry.cn-hangzhou.aliyuncs.com/anoy/vote

查看服务信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@node1 ~]# docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
2mrj3pqyioc3 client replicated 1/1 registry.cn-hangzhou.aliyuncs.com/anoy/ubuntu:latest
826s79tsixuh vote replicated 2/2 registry.cn-hangzhou.aliyuncs.com/anoy/vote:latest

[root@node1 ~]# docker service ps client
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
f74i688vbh12 client.1 registry.cn-hangzhou.aliyuncs.com/anoy/ubuntu:latest node2 Running Running 2 minutes ago

[root@node1 ~]# docker service ps vote
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
7iiuzl2a63hy vote.1 registry.cn-hangzhou.aliyuncs.com/anoy/vote:latest node1 Running Running 47 seconds ago
uyhxxqfdima7 vote.2 registry.cn-hangzhou.aliyuncs.com/anoy/vote:latest node2 Running Running about a minute ago

可以看出 “client” 运行于 node2,在 node2 上进入 client 容器,使用 dig 来解析服务名 “vote”,如下所示,”vote” 解析到 10.0.0.6 和 10.0.0.5

1
2
3
4
5
6
7
8
9
10
11
12
[root@node2 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1eed67d37cbb registry.cn-hangzhou.aliyuncs.com/anoy/vote:latest "gunicorn app:app -b…" About a minute ago Up About a minute 80/tcp vote.2.uyhxxqfdima7smos5pki84wul
436702b21a1c registry.cn-hangzhou.aliyuncs.com/anoy/ubuntu:latest "ping anoyi.com" 3 minutes ago Up 3 minutes client.1.f74i688vbh12on8oniufht633

[root@node2 ~]# docker exec -it 436702b21a1c /bin/bash

root@436702b21a1c:/# dig vote

;; ANSWER SECTION:
vote. 600 IN A 10.0.0.5
vote. 600 IN A 10.0.0.6

使用 ping 解析 “vote” 服务,如下所示,交替解析到 10.0.0.6 和 10.0.0.5

1
2
3
4
5
6
7
root@436702b21a1c:/# ping -c1 vote
PING vote (10.0.0.6) 56(84) bytes of data.
64 bytes from vote.2.uyhxxqfdima7smos5pki84wul.overlay1 (10.0.0.6): icmp_seq=1 ttl=64 time=0.087 ms

root@436702b21a1c:/# ping -c1 vote
PING vote (10.0.0.5) 56(84) bytes of data.
64 bytes from vote.1.7iiuzl2a63hyj084qgufc175v.overlay1 (10.0.0.5): icmp_seq=1 ttl=64 time=0.767 ms

如果使用 curl,如下所示,请求也能解析到不同的容器

1
2
3
4
5
6
7
8
9
10
11
root@436702b21a1c:/# curl vote  | grep -i "container id"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 3162 100 3162 0 0 7542 0 --:--:-- --:--:-- --:--:-- 7546
Processed by container ID 9b42319d4f13

root@436702b21a1c:/# curl vote | grep -i "container id"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 3162 100 3162 0 0 452k 0 --:--:-- --:--:-- --:--:-- 514k
Processed by container ID 1eed67d37cbb

基于 DNS 负载均衡存在如下问题:

  • 某些应用程序将 DNS 主机名缓存到 IP 地址映射,这会导致应用程序在映射更改时超时
  • 具有非零 DNS ttl 值会导致 DNS 条目反映最新的详细信息时发生延迟

4. 基于 VIP 的负载均衡

基于 VIP 的负载均衡克服了基于 DNS 负载均衡的一些问题。在这种方法中,每个服务都有一个 IP 地址,并且该 IP 地址映射到与该服务关联的多个容器的 IP 地址。在这种情况下,与服务关联的服务 IP 不会改变,即使与该服务关联的容器死亡并重新启动。

下图描述了基于 VIP 的负载均衡是如何工作的:

img

DNS server 会将服务名 “vote” 解析到 VIP,使用 iptables 和 ipvs,VIP 实现 2 个服务端 “vote” 容器的负载均衡。

使用如下命令创建 2 个 VIP 模式的服务 “client” 、 “vote”:

1
2
3
docker service create --replicas 1 --name client --network overlay1 registry.cn-hangzhou.aliyuncs.com/anoy/ubuntu ping anoyi.com

docker service create --name vote --network overlay1 --replicas 2 registry.cn-hangzhou.aliyuncs.com/anoy/vote

查看这 2 个服务和它们的服务 IP:

1
2
3
4
5
[root@node1 ~]# docker service inspect --format {{.Endpoint.VirtualIPs}}  vote
[{tetug0isdx1gri62g7cfm889i 10.0.0.9/24}]

[root@node1 ~]# docker service inspect --format {{.Endpoint.VirtualIPs}} client
[{tetug0isdx1gri62g7cfm889i 10.0.0.7/24}]

在 “client” 的容器中使用如下命令,可以看到服务名 “vote” 映射到 VIP “10.0.0.9”

1
2
3
4
5
6
[root@node2 ~]# docker exec -it f3d1c4ef53f8 /bin/bash

root@f3d1c4ef53f8:/# dig vote

;; ANSWER SECTION:
vote. 600 IN A 10.0.0.9

Service IP “10.0.0.9” 使用 Linux 内核的 iptables 和 IPVS 负载均衡到 2 个容器。iptables 实现防火墙规则,IPVS 实现负载均衡。为了证明这一点,我们需要使用 nsenter 进入容器的网络空间 (namespace)。为此,我们需要找到网络的命名空间。

如下是 node2 上的网络命名空间:

1
2
3
4
[root@node2 ~]# cd /run/docker/netns/

[root@node2 netns]# ls
1-tetug0isdx 1-vyy22w04t6 be7330b99a27 d67fa9efb59e ingress_sbox

前 2 个命名空间是用于 overlay 网络,后面的用于容器。下面的命令用于找到 “client” 容器的网络命名空间:

1
2
3
4
5
6
7
8
[root@node2 netns]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
43a789312e70 registry.cn-hangzhou.aliyuncs.com/anoy/vote:latest "gunicorn app:app -b…" 3 minutes ago Up 3 minutes 80/tcp vote.1.u46ms31e8zjdxtwrxvaec8zub
f3d1c4ef53f8 registry.cn-hangzhou.aliyuncs.com/anoy/ubuntu:latest "ping anoyi.com" 4 minutes ago Up 4 minutes client.1.ycox088aek5ajejezubwsjqf2

[root@node2 netns]# docker inspect f3d1c4ef53f8 | grep -i sandbox
"SandboxID": "be7330b99a274a03a7f58e9e991346dc6f048836a1682c7244a6068acbfb664c",
"SandboxKey": "/var/run/docker/netns/be7330b99a27",

SandboxID 即为 “client” 容器的网络命名空间。

使用如下命令,我们就能够进入到 “client” 容器的网络命令空间:

1
nsenter --net=f3d1c4ef53f8 sh

下面,我们可以看到 iptables 的转发规则和 IPVS 输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sh-4.2# iptables -nvL -t mangle

Chain OUTPUT (policy ACCEPT 606 packets, 50867 bytes)
pkts bytes target prot opt in out source destination
0 0 MARK all -- * * 0.0.0.0/0 10.0.0.7 MARK set 0x102
0 0 MARK all -- * * 0.0.0.0/0 10.0.0.9 MARK set 0x103

sh-4.2# ipvsadm
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
FWM 258 rr
-> node2:0 Masq 1 0 0
FWM 259 rr
-> 10.0.0.10:0 Masq 1 0 0
-> 10.0.0.11:0 Masq 1 0 0

Service IP “10.0.0.9” 使用 iptables OUTPUT 链获得标记 0x103 (十六进制 -> 十进制:259),然后 IPVS 使用此标记并将它负载均衡到 “10.0.0.10” 和 “10.0.0.11” 。

查看 vote 服务的 2 个容器的 IP 如下所示,即 VIP “10.0.0.9” 负载均衡到不同的容器实例:

1
2
3
4
5
[root@node2 netns]# docker inspect vote.1.u46ms31e8zjdxtwrxvaec8zub | grep IPv4
"IPv4Address": "10.0.0.10"

[root@node1 ~]# docker inspect vote.2.tutj19i4iwu1xn7arsaq815cu | grep IPv4
"IPv4Address": "10.0.0.11"

进入 client 服务的容器,使用 curl 请求 vote 服务,输出结果如下,即请求分发到不同的容器:

1
2
3
4
5
6
7
8
9
10
11
root@f3d1c4ef53f8:/# curl vote | grep -i "container id"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 3162 100 3162 0 0 14409 0 --:--:-- --:--:-- --:--:-- 14438
Processed by container ID c2af209c4e90

root@f3d1c4ef53f8:/# curl vote | grep -i "container id"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 3162 100 3162 0 0 165k 0 --:--:-- --:--:-- --:--:-- 171k
Processed by container ID 43a789312e70

5. 路由网格 (Routing mesh)

使用路由网格,服务暴露的端口会暴露在 Swarm 集群中的所有工作节点。Docker 是通过创建 “ingress” overlay 网络来实现这一点的,所有节点默认使用内在的 sandbox 网络命名空间成为 “ingress” overlay 网络的一部分。

下图描述了 Routing mesh 如何实现负载均衡的:

img

首先,会将 Hostname 或 IP 映射到 Sandbox IP,Sandbox 中的 iptables 和 IPVS 负责将请求负载均衡到 2 个 vote 容器。Ingress sandbox 网络命名空间驻留在 swarm 集群中的所有工作节点,它通过将主机映射的端口负载均衡到后端容器来协助路由网格功能。

使用如下命令创建 vote 服务,使用路由网格暴露端口到所有节点:

1
docker service create --name vote --network overlay1 --replicas 2 -p 8080:80 registry.cn-hangzhou.aliyuncs.com/anoy/vote

下图显示了 Sandbox、容器和每个节点的网络之间的映射关系:

img

如图所示,Sandbox 和 vote 容器是 “ingress” 网络的一部分,它有助于路由网格。client 容器和 vote 容器是 “overlay1” 网络的一部分,它有助于内部负载均衡。所有容器都是默认 “docker_gwbridge” 网络的一部分。

遵循 iptables 中的 NAT 规则显示,端口 8080 上的主机流量发送到 node1 里的 Sandbox:

1
2
3
4
5
6
[root@node1 ~]# iptables -nvL -t nat

Chain DOCKER-INGRESS (2 references)
pkts bytes target prot opt in out source destination
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.18.0.2:8080
315 18876 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0

进入 node1 上的 Sandbox 网络命名空间 (ingress_sbox),查看 iptables 的转发规则和 IPVS 输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@node1 netns]# nsenter --net=ingress_sbox sh

sh-4.2# iptables -nvL -t mangle
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 MARK tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 MARK set 0x105

sh-4.2# ipvsadm
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
FWM 261 rr
-> 10.255.0.5:0 Masq 1 0 0
-> 10.255.0.6:0 Masq 1 0 0

端口 8080 标记为 0x105 (十六进制 -> 十进制:261),IPVS 使用此标记将它负载均衡到 “10.255.0.5” 和 “10.255.0.6” 。

查看 vote 服务的 2 个容器的 IP 如下所示,即主机端口 8080 的流量会负载均衡到不同的容器实例:

1
2
3
4
5
6
7
[root@node1 netns]# docker inspect 6173afd5fab8 | grep IPv4
"IPv4Address": "10.255.0.6"
"IPv4Address": "10.0.0.14"

[root@node2 ~]# docker inspect b07e95c5c681 | grep IPv4
"IPv4Address": "10.255.0.5"
"IPv4Address": "10.0.0.13"

验证负载均衡,在 node1 上通过 node2 的 IP 和 8080 端口请求 vote 服务:

1
2
3
4
5
6
7
8
9
10
11
[root@node1 netns]# curl node2:8080 | grep -i "container id"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 3162 100 3162 0 0 199k 0 --:--:-- --:--:-- --:--:-- 192k
Processed by container ID 6173afd5fab8

[root@node1 netns]# curl node2:8080 | grep -i "container id"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 3162 100 3162 0 0 7551 0 --:--:-- --:--:-- --:--:-- 7546
Processed by container ID b07e95c5c681

在 node2 上通过 node1 的 IP 和 8080 端口请求 vote 服务:

1
2
3
4
5
6
7
8
9
10
11
[root@node2 ~]# curl node1:8080 | grep -i "container id"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 3162 100 3162 0 0 7531 0 --:--:-- --:--:-- --:--:-- 7546
Processed by container ID 6173afd5fab8

[root@node2 ~]# curl node1:8080 | grep -i "container id"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 3162 100 3162 0 0 169k 0 --:--:-- --:--:-- --:--:-- 171k
Processed by container ID b07e95c5c681

Docker swarm 的负载均衡

Docker Swarm 的负载均衡分为两种:

Swarm集群内的service之间的相互访问需要做负载均衡,称为内部负载均衡(Internal LB)

从Swarm集群外部访问服务的公开端口,也需要做负载均衡,称外部部负载均衡(Exteral LB or Ingress LB)

1. Internal LB

内部负载均衡就是服务发现,集群内部通过DNS访问service时,Swarm默认通过VIP(virtual IP)、iptables、IPVS转发到某个容器。

img

当在docker swarm集群模式下创建一个服务时,会自动在服务所属的网络上给服务额外的分配一个虚拟IP,当解析服务名字时就会返回这个虚拟IP,对虚拟IP的请求会通过overlay网络自动的负载到这个服务所有的健康任务上。

这个方式也避免了客户端的负载均衡,因为只有单独的一个IP会返回到客户端,docker会处理虚拟IP到具体任务的路由,并把请求平均的分配给所有的健康任务。

img

swarm中服务还有另外一种负载均衡技术可选DNS round robin (DNS RR) (在创建服务时通过–endpoint-mode配置项指定),在DNSRR模式下,docker不再为服务创建VIP,docker DNS服务直接利用轮询的策略把服务名称直接解析成一个容器的IP地址。

2. Exteral LB

Exteral LB 又可以称为Ingress LB 或者 Swarm Mode Routing Mesh。

看名字就知道,这个负载均衡方式和提到的Ingress网络有关。

Swarm网络要提供对外访问的服务就需要打开公开端口,并映射到宿主机。

Ingress LB就是外部通过公开端口访问集群时做的负载均衡。

当创建或更新一个服务时,你可以利用–publish选项把一个服务暴露到外部,在docker swarm模式下发布一个端口意味着在集群中的所有节点都会监听这个端口,这时当访问一个监听了端口但是并没有对应服务运行在其上的节点会发生什么呢?

接下来就该我们的路由网格(routing mesh)出场了,路由网格是docker1.12引入的一个新特性,它结合了IPVS和iptables创建了一个强大的集群范围的L4层负载均衡,它使所有节点接收服务暴露端口的请求成为可能。

当任意节点接收到针对某个服务暴露的TCP/UDP端口的请求时,这个节点会利用预先定义过的Ingress overlay网络,把请求转发给服务对应的虚拟IP。

ingress网络和其他的overlay网络一样,只是它的目的是为了转换来自客户端到集群的请求,它也是利用我们前一小节介绍过的基于VIP的负载均衡技术。

启动服务后,你可以为应用程序创建外部DNS记录,并将其映射到任何或所有Docker swarm节点。你无需担心你的容器具体运行在那个节点上,因为有了路由网这个特性后,你的集群看起来就像是单独的一个节点一样。

img

img

上面这个图表明了路由网是怎么工作的:

\1. 服务(app)拥有两份复制,并把端口映射到外部端口的8000

\2. 路由网在集群中的所有节点上都暴露出8000

\3. 外部对服务app的请求可以是任意节点,在本例子中外部的负载均衡器将请求转发到了没有app服务的主机上

\4. docker swarm的IPVS利用ingress overlay网路将请求重新转发到运行着app服务的节点的容器中

Docker Swarm 负载均衡详解 or 模式选择

Docker Swarm 负载均衡详解

  • Swarm模式内置DNS组件,可以自动为集群中的每个服务分配DNS记录。
  • Swarm manager使用内部负载均衡,根据服务的DNS名称在集群内的服务之间分发请求。
  • Swarm manager使用 ingress load blancing暴露你想从外部访问集群提供的服务。
  • Swarm manager自动为服务分配一个范围30000-32767端口的Published Port,也可以为该服务指定一个Published Port。

ingress network是一个特殊的overlay网络,便于服务的节点直接负载均衡。当任何swarm节点在已发布的端口上接收到请求时,它将该请求转发给调用的IPVS模块,IPVS跟踪参与该服务的所有容器IP地址,选择其中一个,并通过ingress network将请求路由给它。

img


Docker Swarm 负载均衡试验测试

1、管理节点:创建一个测试容器my_web

1
docker service create --replicas 3 --network my-network --name my_web nginx

2、管理节点:添加暴露端口

1
docker service update --publish-add 8080:80 my_web

3、管理节点:获取虚拟IP

1
2
3
docker service inspect -f '{{json .Endpoint.VirtualIPs}}' my_web

[{"NetworkID":"3d1ut7rm89tvqvhh98wl3bxtx","Addr":"10.0.0.2/24"}]

注:可用虚拟VIP或者服务名称去访问。

测试负载均衡:192.168.1.79:8080

img

注:在每个不同副本中访问。


Docker Swarm 负载均衡模式选择

负载均衡有两种模式:VIP、DNSRR

  • VIP:分配独立的虚拟IP,DNS记录解析到服务名中作为代理IP。
  • dnsrr:DNS记录不解析VIP,而去解析每个容器内的IP。dnsrr模式不支持端口对外暴露。

1、管理节点:通过查看服务详细信息筛选当前模式负载均衡模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
docker service inspect my_web
[
"EndpointSpec": {
"Mode": "vip"
}
},
"Endpoint": {
"Spec": {
"Mode": "vip",
"Ports": [
{
"Protocol": "tcp",
"TargetPort": 80,
"PublishedPort": 8080,
"PublishMode": "ingress"
}
]
},
]

注:当前模式为VIP模式。

2、管理节点:设置DNS轮询模式

1
2
3
4
5
6
docker service create \
--replicas 3 \
--name my-web \
--network my-network \
--endpoint-mode dnsrr \
nginx

3、管理节点:创建一个测试容器my_web2

1
docker service create --replicas 3 --network my-network --name my_web2 nginx 

4、管理节点:添加dnsrr模式

1
docker service update --endpoint-mode dnsrr my_web2 

5、工作节点:进入容器测试

1
docker exec -it 06a5a7ae6e7e sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 测试进入容器查看解析记录
/ # nslookup my_web2
Server: 127.0.0.11
Address: 127.0.0.11:53
Non-authoritative answer:
Name: my_web2
Address: 10.0.0.13
Name: my_web2
Address: 10.0.0.4
Name: my_web2
Address: 10.0.0.12



# 工作节点:测试进入容器多次ping服务名
/ # ping my_web2
PING my_web2 (10.0.0.4): 56 data bytes
64 bytes from 10.0.0.4: seq=0 ttl=64 time=0.116 ms
/ # ping my_web2
PING my_web2 (10.0.0.12): 56 data bytes
64 bytes from 10.0.0.12: seq=0 ttl=64 time=0.745 ms
/ # ping my_web2
PING my_web2 (10.0.0.13): 56 data bytes
64 bytes from 10.0.0.13: seq=0 ttl=64 time=0.546 ms

Docker Swarm 负载均衡模扩展知识

可在容器Swarm负载均衡之上在建立一层负载均衡。HAProxy可代理工作节点端暴露的端口进行再次代理,做到双层负载均衡的作用。保证高可用与大规模的应用。

img