Docker学习笔记第三篇-网络下篇

在网络的上篇中我们了解了命名空间是什么,已经如果让两个命名空间能够通信。

我们通过手动创建两个命名空间,然后创建一对VETH将两个命名空间连接起来,然后再分别赋予IP这样完成了命名空间之间的通信。

那直接创建的两个容器之间是为何能够直接通信的呢,为什么创建的容器内部可以连通外网呢?

让我们带着两个问题继续学习吧!!!!

Docker bridge详解
1
2
3
4
5
6
7
8
9
10
11
[vagrant@docker-node1 ~]$ docker exec -it 2ac19f6c6c53 /bin/sh
/ # ping www.baidu.com
PING www.baidu.com (115.239.211.112): 56 data bytes
64 bytes from 115.239.211.112: seq=0 ttl=61 time=6.018 ms
64 bytes from 115.239.211.112: seq=1 ttl=61 time=6.283 ms
64 bytes from 115.239.211.112: seq=2 ttl=61 time=5.720 ms
^C
--- www.baidu.com ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 5.720/6.007/6.283 ms
/ #

先介绍一个命令docker network ls

1
2
3
4
5
[vagrant@docker-node1 ~]$ docker network ls
NETWORK ID NAME DRIVER SCOPE
f3e37efd5e71 bridge bridge local
b1947e6b2d78 host host local
14d86ca05d8f none null local

这个命令会将主机支持的网络类型列举出来。

我现在开着一个上节建的容器:

1
2
3
[vagrant@docker-node1 ~]$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2ac19f6c6c53 busybox "/bin/sh -c 'while t…" 3 hours ago Up 10 minutes test1

这个容器的网络是连到bridge上面的,通过docker network inspect NETWORKID查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[vagrant@docker-node1 ~]$ docker network inspect f3e37efd5e71
[
{
"Containers": {

"2ac19f6c6c53646df7f444e8ead3ef7a7da673de40f9ea04907a0c62edb37cc6": {
"Name": "test1",
"EndpointID": "392e6349a6c2aac0ece27bb870287f9031a1b6b87c08308ded7c9180137c4437",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
}
},

}
]

我们看到在Containers中包含我们创建的容器test1

说明我们的容器链接到了bridge网络上,这个bridge到底是什么呢?

我们看下本地网络:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[vagrant@docker-node1 ~]$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 52:54:00:c0:42:d5 brd ff:ff:ff:ff:ff:ff
inet 10.0.2.15/24 brd 10.0.2.255 scope global noprefixroute dynamic eth0
valid_lft 63201sec preferred_lft 63201sec
inet6 fe80::5054:ff:fec0:42d5/64 scope link
valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 08:00:27:fa:f3:9c brd ff:ff:ff:ff:ff:ff
inet 192.168.205.10/24 brd 192.168.205.255 scope global noprefixroute eth1
valid_lft forever preferred_lft forever
inet6 fe80::a00:27ff:fefa:f39c/64 scope link
valid_lft forever preferred_lft forever
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:b2:b1:26:8a brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:b2ff:feb1:268a/64 scope link
valid_lft forever preferred_lft forever
12: vethfff2d55@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
link/ether 5a:ae:7f:6a:6a:83 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 fe80::58ae:7fff:fe6a:6a83/64 scope link
valid_lft forever preferred_lft forever

我们看下最后两个接口,其中的docker0就是本机的网络命名空间。vethfff2d55@if11就是一个VETH的一个端口。

那我们新建的容器是如何链接到docker0上的呢?

image-20181025180037895

其中的vethfff2d55@if11是链接到docker0eth0@if12是链接到Docker的命名空间。

我们再新建一个容器test2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[vagrant@docker-node1 ~]$ docker network inspect f3e37efd5e71
"Containers": {
"2ac19f6c6c53646df7f444e8ead3ef7a7da673de40f9ea04907a0c62edb37cc6": {
"Name": "test1",
"EndpointID": "392e6349a6c2aac0ece27bb870287f9031a1b6b87c08308ded7c9180137c4437",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
},
"820fc3fb33ddb9e45c6beafdf2728ba93efd835229be4c90e13e9f1d1e4fb251": {
"Name": "test2",
"EndpointID": "c7a5df3931b1757aba9e96c52bd1fa951d741641de9f9d8519bbc37617b427d6",
"MacAddress": "02:42:ac:11:00:03",
"IPv4Address": "172.17.0.3/16",
"IPv6Address": ""
}
},

新建容器之后我们看到容器test2也加入到了bridge中。

image-20181025181225988

我们看下总得拓扑图:image-20181025181452488

我们先看第一个问题,不同容器如何通信的:

从图中我们看到不同的容器都会和本地的命名空间docker0关联起来,docker0起到了一个中间作用。

第二个问题,容器内部是如何与外网通信的:

我们通过一个网络地址转换NAT(iptables实现)将容器发送到docker0的数据通过地址转换发送到eth0进而发送到外网。

我们之前创建两个容器,在容器内部可以相互ping通,前提是使用IP地址。我们可以使用link机制在创建第二个容器的时候link到第一个容器上,这样我们就可以在第二个容器中直接使用容器的名字而不是IP进行通信了。

image-20181026141049149

相比之前创建容器的时候指定要链接的容器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[vagrant@docker-node1 ~]$ docker exec -it abdcec200555 /bin/sh
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
7: eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever

# 在容器内部可以使用ip通信
/ # ping 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.106 ms
64 bytes from 172.17.0.2: seq=1 ttl=64 time=0.079 ms
64 bytes from 172.17.0.2: seq=2 ttl=64 time=0.079 ms
^C
--- 172.17.0.2 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.079/0.088/0.106 ms

# 在容器内部可以使用容器名字通信
/ # ping test1
PING test1 (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.079 ms
64 bytes from 172.17.0.2: seq=1 ttl=64 time=0.119 ms
64 bytes from 172.17.0.2: seq=2 ttl=64 time=0.090 ms
^C
--- test1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.079/0.096/0.119 ms

从上面看出 我们在容器test2中可以直接使用容器test1的容器名进行通信,(相当于增加了一个DNS解析)那在容器test1中可以吗?

image-20181026141933107

事实证明是不行,为什么会这样呢?

这是因为link是单向的,我们创建容器的test2的时候link到了test1,因此我们只能在test2里使用test1容器名通信。

link实际工作中不多

我们在创建容器的时候是可以指定网络类型的,如果没有指定则是默认的bridge

1
2
3
4
5
[vagrant@docker-node1 ~]$ docker  network  ls
NETWORK ID NAME DRIVER SCOPE
81dc995d2c59 bridge bridge local
b1947e6b2d78 host host local
14d86ca05d8f none null local

我们可以自己建个网络,然后创建容器的时候将网络指定为我们自己创建的。

image-20181026143741619

现在新增一个容器,指定网络为我们自己创建的。

image-20181026144109660

新的网络已经有了接口。

我们不仅可以在创建容器的时候指定网络,还可以再运行的时候更改网络。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 将容器 test2 的网络改为 my-bridge 
[vagrant@docker-node1 ~]$ docker network connect my-bridge test2

[vagrant@docker-node1 ~]$ docker network inspect my-bridge
[
{
"Containers": {
"92465e9468ab7afb02f2d4c0e2fb72d7bee2fc77504d6cb3ea0feeb63b2adc45": {
"Name": "test3",
"EndpointID": "81cec594daec5a586d0013d27884b704a6afbb4275042b6ec10add8f69aa349d",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
},
"abdcec2005554f8ff4b951e28a4793b92556e68d24ceb850e86e2d3bc8853de4": {
"Name": "test2",
"EndpointID": "76b9f0cd75411bf5f5dd9cb9a4722536a034fcf2ccb0e716a19e8140d5e5d402",
"MacAddress": "02:42:ac:12:00:03",
"IPv4Address": "172.18.0.3/16",
"IPv6Address": ""
}
},
}
]

我们登上test3进行以下操作:

image-20181026144633855

我们发现可以在test3中直接使用test2的容器名进行通信。这是因为当容器加入到我们自己创建的网络而不是默认的网络的时候,会自动建立好双向的link

1
2
3
4
5
6
7
8
[vagrant@docker-node1 ~]$ docker exec -it test2 ping test3
PING test3 (172.18.0.2): 56 data bytes
64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.057 ms
64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.091 ms
^C
--- test3 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.057/0.074/0.091 ms
容器的端口映射

之所以需要端口映射是因为在容器中启动的某些服务如果不做端口映射我们在外网是无法访问使用的。

image-20181026160552787

我们将docker的端口80映射到docker主机的80端口。在docker主机上进行访问。

因为我们是使用vagrant创建的虚拟机,在Vagrantfile中我们指定了当前容器的IP192.168.205.10

直接在本机mac上浏览器访问IP地址即可:

image-20181026161122994

如果是云主机,直接访问云主机对应的公网IP即可。

容器网络的 host 和 none

这个小节我们看下hostnone这两个网络。

关于host

1
2
[vagrant@docker-node1 ~]$ docker run -d --name test1 --network none  busybox /bin/sh -c "while true; do sleep 3600; done"
d52d4e5f1b6749457622c42da616f432d8221da8c50e53d2430940444e06573f

image-20181026162348913

我们看到😀MacAddress等都是为空。

1
2
3
4
5
6
7
[vagrant@docker-node1 ~]$ docker  exec -it test1 /bin/sh
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
/ #

容器内部的网络接口也只有本地回环。没有网络接口证明这容器test1是一个孤立的容器,只能通过exec进入。

关于host

image-20181026162939145

我们看到MacAddress等地址也是为空。

image-20181026163035215

但是进入到容器内部之后看到的网络接口是外层docker主机的。

通过这种方式创建的容器有一个问题是会和docker主机竞争端口,因为离外层用的是同一套命名空间。

一个多容器的应用部署示栗

首先看下我们要部署的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from flask import Flask
from redis import Redis
import os
import socket

app = Flask(__name__)
redis = Redis(host=os.environ.get('REDIS_HOST', '127.0.0.1'), port=6379)


@app.route('/')
def hello():
redis.incr('hits')
return 'Hello Container World! I have been seen %s times and my hostname is %s.\n' % (redis.get('hits'),socket.gethostname())


if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)

这个程序需要使用redis服务,我们将redis使用一个容器提供服务,flask应用使用一个容器进行部署。

首先我们创建一个redis容器

image-20181026173337506

根据Dockerfile创建一个应用程序容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[vagrant@docker-node1 labs]$ ls
app.py Dockerfile
[vagrant@docker-node1 labs]$ cat Dockerfile
FROM python:3.7
LABEL maintaner="hongshaorou"
COPY . /app
WORKDIR /app
RUN pip install flask redis
EXPOSE 5000
CMD [ "python", "app.py" ]
[vagrant@docker-node1 labs]$ docker build -t hongshaorou/flask-redis .

[vagrant@docker-node1 labs]$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hongshaorou/flask-redis latest 466f8679b8a5 52 seconds ago 934MB
python 3.7 2cc378c061f7 35 hours ago 923MB
busybox latest 59788edf1f3e 3 weeks ago 1.15MB

image-20181026174359651

上面使用-e参数在运行容器的时候设定环境变量。

1
2
3
4
5
# curl 127.0.0.1:5000
Hello Container World! I have been seen b'1' times and my hostname is 0290aa8efccf.
# curl 127.0.0.1:5000
Hello Container World! I have been seen b'2' times and my hostname is 0290aa8efccf.
#

在容器内访问我们的程序已经生效了。

image-20181026174818476

我们将5000端口映射到docker主机,这样可以在docker主机进行访问部署在容器中的程序。

docker的多机器通信

我们这个小节学习下不同机器的容器是如何通信的

image-20181026175315150

我们的实现方式是将容器发送的数据当成一个数据包放到外层的通信层中,因为1011之间是可以相互通信的。

image-20181026182811465

具体实现细节原理为VXLANhttps://cizixs.com/2017/09/25/vxlan-protocol-introduction/

原理大致了解下就好,重在知道怎么用!

docker overlay网络 和 etcd 实现多机容器通信

这个小节我们学习以overlay的模式实现多机通信。通过overlay的网络我们需要第三方的通信(分布式存储)。

image-20181027104946463

为什么需要一个分布式的存储呢?

看上面的图,我们在多机容器通信中需要分配不同容器不同的IP,那么标识IP是否已经使用就是将IP存放到分布式存储中,每次分配IP查询一下是否占用。

分布式存储我们将使用etcdhttps://coreos.com/etcd/

首先我们需要搭建一个etcdcluster

首先注备好两台主机
1
2
3
4
5
➜  chapter4 vagrant status
Current machine states:

docker-node1 running (virtualbox)
docker-node2 running (virtualbox)
下载安装集群工具包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
## setup etcd cluster

在docker-node1上


ubuntu@docker-node1:~$ wget https://github.com/coreos/etcd/releases/download/v3.0.12/etcd-v3.0.12-linux-amd64.tar.gz
ubuntu@docker-node1:~$ tar zxvf etcd-v3.0.12-linux-amd64.tar.gz
ubuntu@docker-node1:~$ cd etcd-v3.0.12-linux-amd64
ubuntu@docker-node1:~$ nohup ./etcd --name docker-node1 --initial-advertise-peer-urls http://192.168.205.10:2380 \
--listen-peer-urls http://192.168.205.10:2380 \
--listen-client-urls http://192.168.205.10:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://192.168.205.10:2379 \
--initial-cluster-token etcd-cluster \
--initial-cluster docker-node1=http://192.168.205.10:2380,docker-node2=http://192.168.205.11:2380 \
--initial-cluster-state new&



在docker-node2上


ubuntu@docker-node2:~$ wget https://github.com/coreos/etcd/releases/download/v3.0.12/etcd-v3.0.12-linux-amd64.tar.gz
ubuntu@docker-node2:~$ tar zxvf etcd-v3.0.12-linux-amd64.tar.gz
ubuntu@docker-node2:~$ cd etcd-v3.0.12-linux-amd64/
ubuntu@docker-node2:~$ nohup ./etcd --name docker-node2 --initial-advertise-peer-urls http://192.168.205.11:2380 \
--listen-peer-urls http://192.168.205.11:2380 \
--listen-client-urls http://192.168.205.11:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://192.168.205.11:2379 \
--initial-cluster-token etcd-cluster \
--initial-cluster docker-node1=http://192.168.205.10:2380,docker-node2=http://192.168.205.11:2380 \
--initial-cluster-state new&


检查cluster状态

ubuntu@docker-node2:~/etcd-v3.0.12-linux-amd64$ ./etcdctl cluster-health
member 21eca106efe4caee is healthy: got healthy result from http://192.168.205.10:2379
member 8614974c83d1cc6d is healthy: got healthy result from http://192.168.205.11:2379
cluster is healthy
重启docker服务

在docker-node1上

1
2
$ sudo service docker stop
$ sudo /usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --cluster-store=etcd://192.168.205.10:2379 --cluster-advertise=192.168.205.10:2375&

在docker-node2上

1
2
$ sudo service docker stop
$ sudo /usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --cluster-store=etcd://192.168.205.11:2379 --cluster-advertise=192.168.205.11:2375&
创建overlay network

在docker-node1上创建一个demo的overlay network

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
ubuntu@docker-node1:~$ sudo docker network ls
NETWORK ID NAME DRIVER SCOPE
0e7bef3f143a bridge bridge local
a5c7daf62325 host host local
3198cae88ab4 none null local
ubuntu@docker-node1:~$ sudo docker network create -d overlay demo
3d430f3338a2c3496e9edeccc880f0a7affa06522b4249497ef6c4cd6571eaa9
ubuntu@docker-node1:~$ sudo docker network ls
NETWORK ID NAME DRIVER SCOPE
0e7bef3f143a bridge bridge local
3d430f3338a2 demo overlay global
a5c7daf62325 host host local
3198cae88ab4 none null local
ubuntu@docker-node1:~$ sudo docker network inspect demo
[
{
"Name": "demo",
"Id": "3d430f3338a2c3496e9edeccc880f0a7affa06522b4249497ef6c4cd6571eaa9",
"Scope": "global",
"Driver": "overlay",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "10.0.0.0/24",
"Gateway": "10.0.0.1/24"
}
]
},
"Internal": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]

我们会看到在node2上,这个demo的overlay network会被同步创建

1
2
3
4
5
6
ubuntu@docker-node2:~$ sudo docker network ls
NETWORK ID NAME DRIVER SCOPE
c9947d4c3669 bridge bridge local
3d430f3338a2 demo overlay global
fa5168034de1 host host local
c2ca34abec2a none null local

通过查看etcdkey-value, 我们获取到,这个demo的network是通过etcd从node1同步到node2的

etcd是一个key-valuedarabases

我们看下etcd的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
[vagrant@docker-node2 etcd-v3.0.12-linux-amd64]$ ./etcdctl ls /docker
/docker/nodes
/docker/network

# 存储两个 node 的信息
[vagrant@docker-node2 etcd-v3.0.12-linux-amd64]$ ./etcdctl ls /docker/nodes
/docker/nodes/192.168.205.11:2375
/docker/nodes/192.168.205.10:2375

# 看下网络 网络ID 和在 node1创建的一致
ubuntu@docker-node2:~/etcd-v3.0.12-linux-amd64$ ./etcdctl ls /docker/network/v1.0/network
/docker/network/v1.0/network/3d430f3338a2c3496e9edeccc880f0a7affa06522b4249497ef6c4cd6571eaa9

这样我们分别在两台主机上创建两个容器,这两个容器之间是可以直接通过容器内的IP相互ping通。

因为我的环境崩了,暂时不演示了! 上面只要再创建容器就好了!!

我们可以将上面部署的 flask-redis通过 overlay网络进行部署。

我们再看一张图

image-20181027151612139

这张图片里我们在本机又建了一个自己的网络docker_gwbridge然后将链接到overlay网络的容器也加到这个网络上。这样我们通过overlay网络传输数据,然后通过underlay网络层将数据传输到外面。

详情可以查看官方文档:https://github.com/docker/labs/blob/master/networking/concepts/06-overlay-networks.md

知识就是财富
如果您觉得文章对您有帮助, 欢迎请我喝杯水!