0%

基本命令

1
2
3
docker volume ls                // 列出所有volume
docker volume rm <VOLUME...> // 删除一个或多个volume
docker volume rm $(docker volume ls -qf dangling=true) //删除失效的volume:

Data Volume

volume是docker数据持久化的一种方式,那么怎样使用volume呢?

Dockerfile使用

可以通过mysql官方的Dockerfile看到volume的使用方式

命令模式使用

启动一台mysql;

1
docker run -d --name mysql1 -e MYSQL_ALLOW_EMPTY_PASSWORD=true mysql

查看volume:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pengshiliang@pengshiliang-virtual-machine:~$ docker volume ls
DRIVER VOLUME NAME
local 195d16514c70f7990f190b1557fb2131a2b8942c48ef50025b7f08fc7b082dcd
pengshiliang@pengshiliang-virtual-machine:~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
20752e526d9e mysql "docker-entrypoint.s…" 14 seconds ago Up 13 seconds 3306/tcp, 33060/tcp mysql1
pengshiliang@pengshiliang-virtual-machine:~$ docker volume inspect 195d16514c70f7990f190b1557fb2131a2b8942c48ef50025b7f08fc7b082dcd
[
{
"CreatedAt": "2019-03-04T21:41:29+08:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/195d16514c70f7990f190b1557fb2131a2b8942c48ef50025b7f08fc7b082dcd/_data",
"Name": "195d16514c70f7990f190b1557fb2131a2b8942c48ef50025b7f08fc7b082dcd",
"Options": {},
"Scope": "local"
}
]
pengshiliang@pengshiliang-virtual-machine:~$

可以通过inspect命令查看volume的存储路径

删掉mysql container

发现volume仍然存在,也确认了docker可以通过volume持久化存储数据

还可以通过下面的实例来证实

为了避免环境影响,删掉刚才产生的volume,重新启动一台mysql

valume的名称太长了,加入一个-v参数,来给volume起个别名,然后启动mysql,并指定volume存放位置

1
docker run -d -v mysql:/var/lib/mysql --name mysql1 -e MYSQL_ALLOW_EMPTY_PASSWORD=true mysql

进入当前container,并创建一个数据库

然后退出,把当前的mysql1容器删掉

检查volume

再去启动一台mysql2

1
docker run -d -v mysql:/var/lib/mysql --name mysql2 -e MYSQL_ALLOW_EMPTY_PASSWORD=true mysql

进入到当前容器的mysql数据库查看详情

发现刚才创建的数据库还在,也证明了volume是持久化存储的方式

bind mouting

命令:

1
docker -run -v /home/aaa:/root/aaa

通过docker bind mouting将本地和服务器(容器)上的资源绑定,改变一方都对数据同步,从而达到直接修改本地资源,服务器上的资源自动更新

准备一个目录,创建index.html文件

可以发现,当前目录下的文件是和container内部的/usr/share/nginx/html文件是同步的

安装

1
sudo curl -L "https://github.com/docker/compose/releases/download/1.23.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

基本命令

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
docker-compose up -d nginx                     构建建启动nignx容器

docker-compose exec nginx bash 登录到nginx容器中

docker-compose down 删除所有nginx容器,镜像

docker-compose ps 显示所有容器

docker-compose restart nginx 重新启动nginx容器

docker-compose run --no-deps --rm php-fpm php -v 在php-fpm中不启动关联容器,并容器执行php -v 执行完成后删除容器

docker-compose build nginx 构建镜像 。

docker-compose build --no-cache nginx 不带缓存的构建。

docker-compose logs nginx 查看nginx的日志

docker-compose logs -f nginx 查看nginx的实时日志



docker-compose config -q 验证(docker-compose.yml)文件配置,当配置正确时,不输出任何内容,当文件配置错误,输出错误信息。

docker-compose events --json nginx 以json的形式输出nginx的docker日志

docker-compose pause nginx 暂停nignx容器

docker-compose unpause nginx 恢复ningx容器

docker-compose rm nginx 删除容器(删除前必须关闭容器)

docker-compose stop nginx 停止nignx容器

docker-compose start nginx 启动nignx容器

环境说明

两台vagrant 创建的 docker 虚拟机,虚拟机配置信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
boxes = [
{
:name => "docker-node1",
:eth1 => "192.168.205.10",
:mem => "1024",
:cpu => "1"
},
{
:name => "docker-node2",
:eth1 => "192.168.205.11",
:mem => "1024",
:cpu => "1"
}
]

启动两个 docker 容器

1
2
sudo docker run -d --name test busybox /bin/sh -c "while true; do sleep 3600; done"
sudo docker run -d --name test1 busybox /bin/sh -c "while true; do sleep 3600; done"

avatar

此时发现da678d729936的ip地址为172.17.0.3,cf393747710e的ip地址为172.17.0.2

执行docker exec da678d729936 ping 172.17.0.2试试可不可以ping通cf393747710e的ip

avatar

发现可以ping通,证明docker两个container之间的ip namespace是隔离开的,但是两个ip之间是可以ping通的,那么具体原理是什么呢?
我们可以通过下面的实验模拟来理解一下

network namespace

创建net namespace

1
2
3
sudo ip netns add <name>        //  添加一个namespace
sudo ip netns list
sudo ip netns exec test ip a // 查看test ip info

创建两个namespace

1
2
sudo ip netns add test
sudo ip netns add test1

执行sudo ip netns exec test ip link 查看ip信息
avatar

接口启动命令

1
sudo ip netns exec test ip link set dev lo up

此时lo网卡是down状态

执行sudo ip netns exec test ip link set dev lo up 将lo 开启
avatar

出现unknown原因是因为lo是单个接口,只有做link后的成对接口可以up

添加一对veth接口,执行sudo ip link add veth-test type veth peer name veth-test1

avatar

添加 veth-test 到 test namespace 中

1
sudo ip link set veth-test netns test

分别查看本地和test ip link 信息

avatar

发现本地的veth-test接口被添加到了test中,同理,添加test1

1
sudo ip link set veth-test1 netns test1

执行

1
2
sudo ip netns exec test ip addr add 192.168.1.1/24 dev veth-test
sudo ip netns exec test1 ip addr add 192.168.1.2/24 dev veth-test1

然后启动接口,参照上面启动lo接口的命令

1
2
sudo ip netns exec test ip link set dev veth-test up
sudo ip netns exec test1 ip link set dev veth-test1 up

查看test和test1 ip 状态
avatar

发现两个接口状态都是state up,并且test和test1均已分配192.168.1.1和192.168.1.2表示接口已经启动成功,测试相互ping一下

avatar

docker network

bridge

把之前docker container 中的两个容器stop和rm掉,重新启动一个test容器

1
sudo docker run -d --name test busybox /bin/sh -c "while true; do sleep 3600; done"
1
2
3
4
// 列举出docker的网络
sudo docker network ls
// 查看某个docker network id 的具体信息
sudo docker network inspect <networkid>

eg: 查看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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
[vagrant@docker-node1 labs]$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8378c2ff8198 busybox "/bin/sh -c 'while t…" 3 seconds ago Up 3 seconds test
[vagrant@docker-node1 labs]$ docker network ls
NETWORK ID NAME DRIVER SCOPE
1fb7b167deb2 bridge bridge local
2d13173ae2bf host host local
6d2c375cd6cd none null local
[vagrant@docker-node1 labs]$ docker network inspect 1fb7b167deb2
[
{
"Name": "bridge",
"Id": "1fb7b167deb233148d61b85c03bb68d9b8e3cbb124b60deb7be8acf88abdac21",
"Created": "2019-03-03T02:04:36.334606846Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.17.0.0/16",
"Gateway": "172.17.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"8378c2ff8198bbac6647463286408de11a7814c7882e99232404ecab0e1517ad": {
"Name": "test",
"EndpointID": "f976820b8133d11cd37602ece5f762106aba2f3735420272ad0ed3bd8b1dc58a",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
}
},
"Options": {
"com.docker.network.bridge.default_bridge": "true",
"com.docker.network.bridge.enable_icc": "true",
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "docker0",
"com.docker.network.driver.mtu": "1500"
},
"Labels": {}
}
]
[vagrant@docker-node1 labs]$

avatar

通过上图可以看出本地的 vethd347bdc@if11 和 容器内的 eth0@if12 是一对接口
这一对veth peer 连接到了docker0的网络上面,可以通过下面的演示验证

1
sudo yum install bridge-utils

brctl命令说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[vagrant@docker-node1 labs]$ brctl
Usage: brctl [commands]
commands:
addbr <bridge> add bridge
delbr <bridge> delete bridge
addif <bridge> <device> add interface to bridge
delif <bridge> <device> delete interface from bridge
hairpin <bridge> <port> {on|off} turn hairpin on/off
setageing <bridge> <time> set ageing time
setbridgeprio <bridge> <prio> set bridge priority
setfd <bridge> <time> set bridge forward delay
sethello <bridge> <time> set hello time
setmaxage <bridge> <time> set max message age
setpathcost <bridge> <port> <cost> set path cost
setportprio <bridge> <port> <prio> set port priority
show [ <bridge> ] show a list of bridges
showmacs <bridge> show a list of mac addrs
showstp <bridge> show bridge stp info
stp <bridge> {on|off} turn stp on/off
[vagrant@docker-node1 labs]$

启动test2容器

1
sudo docker run -d --name test1 busybox /bin/sh -c "while true; do sleep 3600; done"

通过brctl show命令可以看到如下结果:

1
2
3
4
[vagrant@docker-node1 labs]$ brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.02425db75bb1 no veth4de57f6
vethd347bdc

可以看到docker0和两个veth peer接口的关系

host 和 none

none

1
sudo docker run -d --name test4 --network none busybox /bin/sh -c "while true; do sleep 3600; done"

avatar

这种方式创建的容器没有ip地址,只能通过exec的方式进入

avatar

host

1
sudo docker run -d --name test5 --network host busybox /bin/sh -c "while true; do sleep 3600; done"

avatar

进入test5容器查看ip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[vagrant@docker-node1 ~]$ docker exec test5 ip a
...
37: veth9cb9ecd@if36: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue master docker0
link/ether ce:61:3e:72:57:f9 brd ff:ff:ff:ff:ff:ff
inet6 fe80::cc61:3eff:fe72:57f9/64 scope link
valid_lft forever preferred_lft forever
...
[vagrant@docker-node1 ~]$ ip a
...
37: veth9cb9ecd@if36: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP
link/ether ce:61:3e:72:57:f9 brd ff:ff:ff:ff:ff:ff link-netnsid 4
inet6 fe80::cc61:3eff:fe72:57f9/64 scope link
valid_lft forever preferred_lft forever
...

我们发现这种方式的docker container 共享了主机里面的ip namespace

多机器通信

avatar

Mutil-host networking with etcd

setup etcd cluster

在docker-node1上

1
2
3
4
5
6
7
8
9
10
vagrant@docker-node1:~$ wget https://github.com/coreos/etcd/releases/download/v3.0.12/etcd-v3.0.12-linux-amd64.tar.gz
vagrant@docker-node1:~$ tar zxvf etcd-v3.0.12-linux-amd64.tar.gz
vagrant@docker-node1:~$ cd etcd-v3.0.12-linux-amd64
vagrant@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上

1
2
3
4
5
6
7
8
9
10
vagrant@docker-node2:~$ wget https://github.com/coreos/etcd/releases/download/v3.0.12/etcd-v3.0.12-linux-amd64.tar.gz
vagrant@docker-node2:~$ tar zxvf etcd-v3.0.12-linux-amd64.tar.gz
vagrant@docker-node2:~$ cd etcd-v3.0.12-linux-amd64/
vagrant@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状态

1
2
3
4
vagrant@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
38
39
40
41
42
43
44
45
46
[vagrant@docker-node1 etcd-v3.0.12-linux-amd64]$ sudo docker network ls
NETWORK ID NAME DRIVER SCOPE
22c070951371 bridge bridge local
2d13173ae2bf host host local
c144a2167891 my-bridge bridge local
6d2c375cd6cd none null local
[vagrant@docker-node1 etcd-v3.0.12-linux-amd64]$ sudo docker network create -d overlay demo
6f58ba8913c3e0df5ac9086f79e87cc62e57ac723d26dcb05edb7635f19103c8
[vagrant@docker-node1 etcd-v3.0.12-linux-amd64]$ sudo docker network ls
NETWORK ID NAME DRIVER SCOPE
22c070951371 bridge bridge local
6f58ba8913c3 demo overlay global
2d13173ae2bf host host local
c144a2167891 my-bridge bridge local
6d2c375cd6cd none null local
[vagrant@docker-node1 etcd-v3.0.12-linux-amd64]$ sudo docker network inspect demo
[
{
"Name": "demo",
"Id": "6f58ba8913c3e0df5ac9086f79e87cc62e57ac723d26dcb05edb7635f19103c8",
"Created": "2019-03-03T13:03:20.535696601Z",
"Scope": "global",
"Driver": "overlay",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "10.0.0.0/24",
"Gateway": "10.0.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]

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

1
2
3
4
5
6
vagrant@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

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

1
2
3
4
5
6
7
8
9
10
11
[vagrant@docker-node2 etcd-v3.0.12-linux-amd64]$ ./etcdctl ls /docker
/docker/network
/docker/nodes
[vagrant@docker-node2 etcd-v3.0.12-linux-amd64]$ ./etcdctl ls /docker/nodes
/docker/nodes/192.168.205.10:2375
/docker/nodes/192.168.205.11:2375
[vagrant@docker-node2 etcd-v3.0.12-linux-amd64]$ ./etcdctl ls /docker/network/v1.0/network
/docker/network/v1.0/network/6f58ba8913c3e0df5ac9086f79e87cc62e57ac723d26dcb05edb7635f19103c8
[vagrant@docker-node2 etcd-v3.0.12-linux-amd64]$ ./etcdctl get /docker/network/v1.0/network/6f58ba8913c3e0df5ac9086f79e87cc62e57ac723d26dcb05edb7635f19103c8
{"addrSpace":"GlobalDefault","attachable":false,"configFrom":"","configOnly":false,"created":"2019-03-03T13:03:20.535696601Z","enableIPv6":false,"generic":{"com.docker.network.enable_ipv6":false,"com.docker.network.generic":{}},"id":"6f58ba8913c3e0df5ac9086f79e87cc62e57ac723d26dcb05edb7635f19103c8","inDelete":false,"ingress":false,"internal":false,"ipamOptions":{},"ipamType":"default","ipamV4Config":"[{\"PreferredPool\":\"\",\"SubPool\":\"\",\"Gateway\":\"\",\"AuxAddresses\":null}]","ipamV4Info":"[{\"IPAMData\":\"{\\\"AddressSpace\\\":\\\"GlobalDefault\\\",\\\"Gateway\\\":\\\"10.0.0.1/24\\\",\\\"Pool\\\":\\\"10.0.0.0/24\\\"}\",\"PoolID\":\"GlobalDefault/10.0.0.0/24\"}]","labels":{},"loadBalancerIP":"","loadBalancerMode":"NAT","name":"demo","networkType":"overlay","persist":true,"postIPv6":false,"scope":"global"}
[vagrant@docker-node2 etcd-v3.0.12-linux-amd64]$

在node1中执行

1
sudo docker run -d --name test1 --net demo busybox /bin/sh -c "while true; do sleep 3600; done"

在node2中执行

1
sudo docker run -d --name test2 --net demo busybox /bin/sh -c "while true; do sleep 3600; done"

分别查看test1,test2 ip

1
2
3
4
5
6
7
8
9
10
11
12
13
[vagrant@docker-node1 etcd-v3.0.12-linux-amd64]$ docker exec test1 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1
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
19: eth0@if20: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue
link/ether 02:42:0a:00:00:02 brd ff:ff:ff:ff:ff:ff
inet 10.0.0.2/24 brd 10.0.0.255 scope global eth0
valid_lft forever preferred_lft forever
21: eth1@if22: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:13:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.19.0.2/16 brd 172.19.255.255 scope global eth1
valid_lft forever preferred_lft forever
1
2
3
4
5
6
7
8
9
10
11
12
13
docker exec test2 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1
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 1450 qdisc noqueue
link/ether 02:42:0a:00:00:03 brd ff:ff:ff:ff:ff:ff
inet 10.0.0.3/24 brd 10.0.0.255 scope global eth0
valid_lft forever preferred_lft forever
10: eth1@if11: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.2/16 brd 172.18.255.255 scope global eth1
valid_lft forever preferred_lft forever

查看是否能ping通

1
2
3
4
5
6
7
8
9
10
11
12
[vagrant@docker-node2 etcd-v3.0.12-linux-amd64]$ docker exec test2 ping 10.0.0.2
PING 10.0.0.2 (10.0.0.2): 56 data bytes
64 bytes from 10.0.0.2: seq=0 ttl=64 time=9.354 ms
64 bytes from 10.0.0.2: seq=1 ttl=64 time=0.789 ms
64 bytes from 10.0.0.2: seq=2 ttl=64 time=1.062 ms
^C
[vagrant@docker-node2 etcd-v3.0.12-linux-amd64]$ docker exec test2 ping test1
PING test1 (10.0.0.2): 56 data bytes
64 bytes from 10.0.0.2: seq=0 ttl=64 time=0.724 ms
64 bytes from 10.0.0.2: seq=1 ttl=64 time=0.590 ms
ç64 bytes from 10.0.0.2: seq=2 ttl=64 time=0.789 ms
64 bytes from 10.0.0.2: seq=3 ttl=64 time=0.651 ms

端口映射

1
docker run --name web -d nginx

avatar

由上图可以看到我们在虚拟机内部访问container的ip的方式可以访问到nginx欢迎页,但是访问本地地址映射不到,
可以通过端口映射来解决这个问题

删除刚才的nginx container,重新启动

1
2
3
4
docker stop web
docker rm web
docker run --name web -d -p 80:80 nginx
docker ps

avatar

映射成功

我的vagrant ip 映射配置
avatar
avatar

映射流程

avatar

图中的192.168.205.10:80为我本机的ip私有地址,外网不能访问,如果我们是在一个云主机上创建的web服务,云主机就可以分配一个public的ip就可以作为外网的出口ip来提供服务

docker link

由于docker container 之中的 ip在为创建之前是未知的,不利于服务与服务之间的配置连接,所以docker 提供了一种办法来解决这个问题,
可以通过 docker name 之间的link来解决

avatar

创建test2并link到test1

1
docker run -d --name test2 --link test1 busybox /bin/sh -c "while true; do sleep 3600; done"

进入到test2容器

1
2
docker exec -it test2 /bin/sh
ping test1

avatar

这种方式的优点是:
假如test1有一个数据库,我们可以在test2容器中通过mysql -u <name> -P <port> -h test1来访问了

由于是test2 去link test1 所以,在test1容器中,ping test2是不可用的

network 创建

删除掉test2容器并重新创建test2

1
docker run -d --name test2 busybox /bin/sh -c "while true; do sleep 3600; done"

创建bridge

1
2
docker network create -d bridge my-bridge
docker network ls

avatar

创建test3并指定network到my-bridge

如果不指定network默认连接是docker0

1
docker run -d --name test3 --network my-bridge busybox /bin/sh -c "while true; do sleep 3600; done"

avatar

查看test3 container network信息

1
docker network inspect <my-bridge id>

avatar

bridge连接

1
docker network connect my-bridge test2
1
2
docker network inspect bridge
docker network inspect my-bridge

我们可以看到bridge和my-bridge的container中都包含了test2

进入到test2容器中

1
docker exec -it test2 /bin/sh

avatar

可以发现在test2容器中可以ping通test3但是不能ping test1, 实际上docker在用户自己创建的bridge中做了一层link,所以test2和test3容器可以相互ping 通对方

把test1也加入到my-bridge中

1
docker network connect my-bridge test1

avatar

此时test1也可以ping通了

vagrant 使用

init centos

centos7 box 下载地址centos7

添加vagrant box到box list

1
vagrant box add centos7 Vagrant-CentOS-7.box

初始化一个虚拟机使用刚才添加的vagrant box

1
2
3
mkdir centos
cd centos
vim Vagrantfile

添加下面内容到Vagrantfile中

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
40
41
42
43
44
45
# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.require_version ">= 1.6.0"

boxes = [
{
:name => "docker-node1",
:eth1 => "192.168.205.10",
:mem => "1024",
:cpu => "1"
},
{
:name => "docker-node2",
:eth1 => "192.168.205.11",
:mem => "1024",
:cpu => "1"
}
]

Vagrant.configure(2) do |config|

config.vm.box = "centos7"

boxes.each do |opts|
config.vm.define opts[:name] do |config|
config.vm.hostname = opts[:name]
config.vm.provider "vmware_fusion" do |v|
v.vmx["memsize"] = opts[:mem]
v.vmx["numvcpus"] = opts[:cpu]
end

config.vm.provider "virtualbox" do |v|
v.customize ["modifyvm", :id, "--memory", opts[:mem]]
v.customize ["modifyvm", :id, "--cpus", opts[:cpu]]
end

config.vm.network :private_network, ip: opts[:eth1]
end
end

config.vm.provision "shell", privileged: true, path: "./setup.sh"

end

install docker的setup.sh文件

在当前目录创建setup.sh文件并添加如下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#/bin/sh

# install some tools
sudo yum install -y git vim gcc glibc-static telnet bridge-utils

# install docker
curl -fsSL get.docker.com -o get-docker.sh
sh get-docker.sh

# start docker service
sudo groupadd docker
sudo usermod -aG docker vagrant
sudo systemctl start docker

rm -rf get-docker.sh

启动安装

1
vagrant up

vagrant 报unknown filesystem type ‘vboxsf’ 解决方案

1
2
vagrant plugin install vagrant-vbguest
vagrant destroy && vagrant up

init ubuntu

使用清华源

ubuntu18的box,终端运行如下命令

1
2
3
vagrant box add \
https://mirrors.tuna.tsinghua.edu.cn/ubuntu-cloud-images/bionic/current/bionic-server-cloudimg-amd64-vagrant.box \
--name ubuntu/bionic

Vagrantfile这样写:

1
2
3
...
config.vm.box = "ubuntu/bionic"
...

接着就是vagrant up && vagrant ssh

基本命令

列出所有Box

1
vagrant box list

添加一个Box

1
vagrant box add [options] <name, url, or path

可以从https://app.vagrantup.com/boxes/search下载各种Vagrant映像文件

1
vagrant box add ubuntu/trusty64

通过指定的URL添加远程box

1
vagrant box add https://atlas.hashicorp.com/ubuntu/boxes/trusty64

添加一个本地box

1
vagrant box add {box_name} {file_path}

初始化一个新VM

1
vagrant init ubuntu/trustry64

此命令会在当前目录创建一个名为Vagrantfile的配置文件,内容大致如下:

1
2
3
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/trusty64"
end

初始化一个新VM

1
vagrant up

启用SSH登陆VM

1
vagrant ssh <node_name>

如果需要从虚拟机中退出,直接在虚拟机中的命令行输入exit命令即可

查看VM当前的状态

进入Vagrantfile配置文件所在的目录,执行以下命令:

1
vagrant status

关闭VM

1
vagrant halt

销毁VM

1
vagrant destory [name|id]

Dockerfile语法梳理

FROM

from 后面接base image

eg:

1
2
3
FROM scratch
FROM centos
FROM ubuntu

尽量使用官方的image 作为base image

LABEL

1
2
3
LABEL maintainer="username@gmail.com" 
LABEL version="1.0"
LABEL description="this is description"

MetaData 不可少

RUN

执行命令并创建新的IMAGE LAYER

为了美观,复杂的RUN用反斜杠换行,避免无用分层,合并多条命令成一行。

最佳实践

1
2
RUN yum update && yum install -y vim \
python-dev
1
2
3
RUN apt-get update && apt-get install -y perl \
pwgen --no-install-recommends && rm -rf \
/var/lib/apt/lists/* #清理cache
1
RUN /bin/bash -c 'source $HOME/.bashrc;echo #HOME'

WORKDIR

1
2
3
WORKDIR test       # 如果没有会自动创建test文件夹
WORKDIR demo
RUN pwd # 打印/test/demo

尽量使用WORKDIR,不要使用RUN cd,尽量使用绝对路径

ADD and COPY

1
2
3
4
5
6
ADD hello /
ADD test.tar.gz / # 添加到根目录并解压缩
WORKDIR /root
ADD hello test / # /root/test/hello
WORKDIR /root
COPY hello test /

大部分情况COPY优于,ADD有额外的解压功能,添加远程文件或目录用curl或wget

ENV

1
2
3
ENV MYSQL_VERSION 5.6 #常量
RUN apt-get install -y mysql-server= "${MYSQL_VERSION}" \
&& rm -rf /var/lib/apt/lists/*

CMD & ENTRYPOINT

CMD:设置容器启动后默认执行的命令和参数
如果docker run指定了其它的命令,则忽略CMD命令
定义多个CMD,只有最后一个会执行

1
2
docker run <image>
docker run -it <image> /bin/bash //此命令会忽略CMD中的命令

ENTRYPOINT:设置容器启动时运行的命令
让容器已应用程序或者服务的方式执行
不会被忽略,一定会执行

SHELL & EXEC

SHELL:

1
2
3
RUN apt-get install -y vim
CMD echo "hello docker"
ENTRYPOINT echo "hello docker"

EXEC:

1
2
3
RUN ["apt-get", "install", "y", "vim"]
CMD ["/bin/echo", "hello docker"]
ENTRYPOINT ["/bin/echo", "hello docker"]

EXEC方式需要指明运行环境,eg:

1
2
3
FROM centos
ENV name word
ENTRYPOINT ["/bin/bash", "c", "echo hello $name"]

更多详见扩展阅读

Dockerfile实战

1
2
3
mkdir flask-hello-word
cd flask-hello-word
vim app.py

app.py内容

1
2
3
4
5
6
7
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return "hello docker"
if __name__ == '__main__':
app.run(host="0.0.0.0", port=5000)

编写Dockerfile

1
2
3
4
5
6
7
FROM python:2.7
LABEL maintainer="peng.shiliang<1390509500@qq.com>"
RUN pip install flask
COPY app.py /app/ # app后面必须接/,否则会当作文件
WORKDIR /app
EXPOSE 5000 # 端口映射,保证远程能够访问
CMD ["python", "app.py"]

执行

1
2
docker build -t pengshiliang/flask-hello-word .
docker push pengshiliang/flask-hello-word:latest

运行flask-hello-word

1
2
3
docker run -d --name=demo pengshiliang/flask-hello-word     //--name 便于docker container 操作
docker exec -it demo ip a //查看docker容器ip
curl <demo ip> //输出hello docker

赋予docker权限

1
2
3
4
5
vagrant@ubuntu-bionic:~$ sudo groupadd docker
groupadd: group 'docker' already exists
vagrant@ubuntu-bionic:~$ sudo gpasswd -a vagrant docker
Adding user vagrant to group docker
vagrant@ubuntu-bionic:~$ sudo service docker restart

退出vagrant在重新进入

docker基本命令

1
2
3
4
5
6
7
8
9
10
11
12
// 列举所有镜像
docker image ls
// 查看image build 历史
docker history <image id>
// 运行一个image
docker run <image id>
// 列举所有正在运行的容器
docker container ls
// 列举所有的容器
docker container ls -a
// 交互式运行运行(常驻运行)
docker run -it <image>

docker image 命令

1
2
docker images   (docker image ls缩写)
docker rmi <image id> (docker image rm <image id>缩写)//移除一个镜像

docker container命令

1
2
3
4
5
6
docker ps -a    (docker container ls -a缩写)
docker rm <image id> (docker container rm <image id>缩写) //删除一个容器
docker ps -aq //列举所有容器id
docker ps -f "status=exited" -q //列举所有已退出的容器
docker rm $(docker ps -aq)
docker rm $(docker ps -f "status=exited" -q)

avatar
avatar

build一个hello word image

生成hello-word程序

1
2
3
mkdir hello-word
cd hello-word
vim hello.c

hello.c内容

1
2
3
4
5
#include<stdio.h>
int main()
{
printf("hello word\n");
}
1
2
3
sudo apt-get install gcc
sudo apt-get install build-essential
gcc -static hello.c -o hello

avatar

编写Dockerfile

执行vim Dockerfile

1
2
3
FROM scratch
ADD hello /
CMD ["/hello"]

build命令

1
docker build -t <tag> <dir>

eg:

1
docker build -t pengshiliang/hello-word .

avatar

1
docker run pengshiliang/hello-word

出现hello word 即为正常build

发布

1
docker login
1
docker push pengshiliang/hello-word:latest

Container

  1. 通过image创建
  2. 在Image layer之上建立一个Cotainer layer
  3. 类面向对象:类和实例
  4. image复制存储和分发,container负责运行app

经历过技术面试的小伙伴想必对 CAP & BASE 这个两个理论已经再熟悉不过了!

我当年参加面试的时候,不夸张地说,只要问到分布式相关的内容,面试官几乎是必定会问这两个分布式相关的理论。一是因为这两个分布式基础理论是学习分布式知识的必备前置基础,二是因为很多面试官自己比较熟悉这两个理论(方便提问)。

我们非常有必要将这两个理论搞懂,并且能够用自己的理解给别人讲出来。

# CAP 理论

CAP 理论/定理open in new window起源于 2000 年,由加州大学伯克利分校的 Eric Brewer 教授在分布式计算原理研讨会(PODC)上提出,因此 CAP 定理又被称作 布鲁尔定理(Brewer’s theorem)

2 年后,麻省理工学院的 Seth Gilbert 和 Nancy Lynch 发表了布鲁尔猜想的证明,CAP 理论正式成为分布式领域的定理。

# 简介

CAP 也就是 Consistency(一致性)Availability(可用性)Partition Tolerance(分区容错性) 这三个单词首字母组合。

img

CAP 理论的提出者布鲁尔在提出 CAP 猜想的时候,并没有详细定义 ConsistencyAvailabilityPartition Tolerance 三个单词的明确定义。

因此,对于 CAP 的民间解读有很多,一般比较被大家推荐的是下面 👇 这种版本的解读。

在理论计算机科学中,CAP 定理(CAP theorem)指出对于一个分布式系统来说,当设计读写操作时,只能同时满足以下三点中的两个:

  • 一致性(Consistency) : 所有节点访问同一份最新的数据副本
  • 可用性(Availability): 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。
  • 分区容错性(Partition Tolerance) : 分布式系统出现网络分区的时候,仍然能够对外提供服务。

什么是网络分区?

分布式系统中,多个节点之前的网络本来是连通的,但是因为某些故障(比如部分节点网络出了问题)某些节点之间不连通了,整个网络就分成了几块区域,这就叫 网络分区

partition-tolerance

# 不是所谓的“3 选 2”

大部分人解释这一定律时,常常简单的表述为:“一致性、可用性、分区容忍性三者你只能同时达到其中两个,不可能同时达到”。实际上这是一个非常具有误导性质的说法,而且在 CAP 理论诞生 12 年之后,CAP 之父也在 2012 年重写了之前的论文。

当发生网络分区的时候,如果我们要继续服务,那么强一致性和可用性只能 2 选 1。也就是说当网络分区之后 P 是前提,决定了 P 之后才有 C 和 A 的选择。也就是说分区容错性(Partition tolerance)我们是必须要实现的。

简而言之就是:CAP 理论中分区容错性 P 是一定要满足的,在此基础上,只能满足可用性 A 或者一致性 C。

因此,分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。 比如 ZooKeeper、HBase 就是 CP 架构,Cassandra、Eureka 就是 AP 架构,Nacos 不仅支持 CP 架构也支持 AP 架构。

为啥不可能选择 CA 架构呢? 举个例子:若系统出现“分区”,系统中的某个节点在进行写操作。为了保证 C, 必须要禁止其他节点的读写操作,这就和 A 发生冲突了。如果为了保证 A,其他节点的读写操作正常的话,那就和 C 发生冲突了。

选择 CP 还是 AP 的关键在于当前的业务场景,没有定论,比如对于需要确保强一致性的场景如银行一般会选择保证 CP 。

另外,需要补充说明的一点是: 如果网络分区正常的话(系统在绝大部分时候所处的状态),也就说不需要保证 P 的时候,C 和 A 能够同时保证。

# CAP 实际应用案例

我这里以注册中心来探讨一下 CAP 的实际应用。考虑到很多小伙伴不知道注册中心是干嘛的,这里简单以 Dubbo 为例说一说。

下图是 Dubbo 的架构图。注册中心 Registry 在其中扮演了什么角色呢?提供了什么服务呢?

注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小。

img

常见的可以作为注册中心的组件有:ZooKeeper、Eureka、Nacos…。

  1. ZooKeeper 保证的是 CP。 任何时刻对 ZooKeeper 的读请求都能得到一致性的结果,但是, ZooKeeper 不保证每次请求的可用性比如在 Leader 选举过程中或者半数以上的机器不可用的时候服务就是不可用的。
  2. Eureka 保证的则是 AP。 Eureka 在设计的时候就是优先保证 A (可用性)。在 Eureka 中不存在什么 Leader 节点,每个节点都是一样的、平等的。因此 Eureka 不会像 ZooKeeper 那样出现选举过程中或者半数以上的机器不可用的时候服务就是不可用的情况。 Eureka 保证即使大部分节点挂掉也不会影响正常提供服务,只要有一个节点是可用的就行了。只不过这个节点上的数据可能并不是最新的。
  3. Nacos 不仅支持 CP 也支持 AP。

# 总结

在进行分布式系统设计和开发时,我们不应该仅仅局限在 CAP 问题上,还要关注系统的扩展性、可用性等等

在系统发生“分区”的情况下,CAP 理论只能满足 CP 或者 AP。要注意的是,这里的前提是系统发生了“分区”

如果系统没有发生“分区”的话,节点间的网络连接通信正常的话,也就不存在 P 了。这个时候,我们就可以同时保证 C 和 A 了。

总结:如果系统发生“分区”,我们要考虑选择 CP 还是 AP。如果系统没有发生“分区”的话,我们要思考如何保证 CA 。

# 推荐阅读

  1. CAP 定理简化open in new window (英文,有趣的案例)
  2. 神一样的 CAP 理论被应用在何方open in new window (中文,列举了很多实际的例子)
  3. 请停止呼叫数据库 CP 或 AP open in new window (英文,带给你不一样的思考)

# BASE 理论

BASE 理论open in new window起源于 2008 年, 由 eBay 的架构师 Dan Pritchett 在 ACM 上发表。

# 简介

BASEBasically Available(基本可用)Soft-state(软状态)Eventually Consistent(最终一致性) 三个短语的缩写。BASE 理论是对 CAP 中一致性 C 和可用性 A 权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的,它大大降低了我们对系统的要求。

# BASE 理论的核心思想

即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。

也就是牺牲数据的一致性来满足系统的高可用性,系统中一部分数据不可用或者不一致时,仍需要保持系统整体“主要可用”。

BASE 理论本质上是对 CAP 的延伸和补充,更具体地说,是对 CAP 中 AP 方案的一个补充。

为什么这样说呢?

CAP 理论这节我们也说过了:

如果系统没有发生“分区”的话,节点间的网络连接通信正常的话,也就不存在 P 了。这个时候,我们就可以同时保证 C 和 A 了。因此,如果系统发生“分区”,我们要考虑选择 CP 还是 AP。如果系统没有发生“分区”的话,我们要思考如何保证 CA 。

因此,AP 方案只是在系统发生分区的时候放弃一致性,而不是永远放弃一致性。在分区故障恢复后,系统应该达到最终一致性。这一点其实就是 BASE 理论延伸的地方。

# BASE 理论三要素

BASE理论三要素

# 基本可用

基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。但是,这绝不等价于系统不可用。

什么叫允许损失部分可用性呢?

  • 响应时间上的损失: 正常情况下,处理用户请求需要 0.5s 返回结果,但是由于系统出现故障,处理用户请求的时间变为 3 s。
  • 系统功能上的损失:正常情况下,用户可以使用系统的全部功能,但是由于系统访问量突然剧增,系统的部分非核心功能无法使用。

# 软状态

软状态指允许系统中的数据存在中间状态(CAP 理论中的数据不一致),并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。

# 最终一致性

最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

分布式一致性的 3 种级别:

  1. 强一致性 :系统写入了什么,读出来的就是什么。
  2. 弱一致性 :不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只是会尽量保证某个时刻达到数据一致的状态。
  3. 最终一致性 :弱一致性的升级版,系统会保证在一定时间内达到数据一致的状态。

业界比较推崇是最终一致性级别,但是某些对数据一致要求十分严格的场景比如银行转账还是要保证强一致性。

那实现最终一致性的具体方式是什么呢? 《分布式协议与算法实战》open in new window 中是这样介绍:

  • 读时修复 : 在读取数据时,检测数据的不一致,进行修复。比如 Cassandra 的 Read Repair 实现,具体来说,在向 Cassandra 系统查询数据的时候,如果检测到不同节点 的副本数据不一致,系统就自动修复数据。
  • 写时修复 : 在写入数据,检测数据的不一致时,进行修复。比如 Cassandra 的 Hinted Handoff 实现。具体来说,Cassandra 集群的节点之间远程写数据的时候,如果写失败 就将数据缓存下来,然后定时重传,修复数据的不一致性。
  • 异步修复 : 这个是最常用的方式,通过定时对账检测副本数据的一致性,并修复。

比较推荐 写时修复,这种方式对性能消耗比较低。

# 总结

ACID 是数据库事务完整性的理论,CAP 是分布式系统设计理论,BASE 是 CAP 理论中 AP 方案的延伸。

1. 2PC(两阶段提交)

两阶段提交是一种保证分布式系统数据一致性的协议,现在很多数据库都是采用的两阶段提交协议来完成 分布式事务 的处理。

在介绍2PC之前,我们先来想想分布式事务到底有什么问题呢?

还拿秒杀系统的下订单和加积分两个系统来举例吧(我想你们可能都吐了🤮🤮🤮),我们此时下完订单会发个消息给积分系统告诉它下面该增加积分了。如果我们仅仅是发送一个消息也不收回复,那么我们的订单系统怎么能知道积分系统的收到消息的情况呢?如果我们增加一个收回复的过程,那么当积分系统收到消息后返回给订单系统一个 Response ,但在中间出现了网络波动,那个回复消息没有发送成功,订单系统是不是以为积分系统消息接收失败了?它是不是会回滚事务?但此时积分系统是成功收到消息的,它就会去处理消息然后给用户增加积分,这个时候就会出现积分加了但是订单没下成功。

所以我们所需要解决的是在分布式系统中,整个调用链中,我们所有服务的数据处理要么都成功要么都失败,即所有服务的 原子性问题

在两阶段提交中,主要涉及到两个角色,分别是协调者和参与者。

第一阶段:当要执行一个分布式事务的时候,事务发起者首先向协调者发起事务请求,然后协调者会给所有参与者发送 prepare 请求(其中包括事务内容)告诉参与者你们需要执行事务了,如果能执行我发的事务内容那么就先执行但不提交,执行后请给我回复。然后参与者收到 prepare 消息后,他们会开始执行事务(但不提交),并将 UndoRedo 信息记入事务日志中,之后参与者就向协调者反馈是否准备好了。

第二阶段:第二阶段主要是协调者根据参与者反馈的情况来决定接下来是否可以进行事务的提交操作,即提交事务或者回滚事务。

比如这个时候 所有的参与者 都返回了准备好了的消息,这个时候就进行事务的提交,协调者此时会给所有的参与者发送 Commit 请求 ,当参与者收到 Commit 请求的时候会执行前面执行的事务的 提交操作 ,提交完毕之后将给协调者发送提交成功的响应。

而如果在第一阶段并不是所有参与者都返回了准备好了的消息,那么此时协调者将会给所有参与者发送 回滚事务的 rollback 请求,参与者收到之后将会 回滚它在第一阶段所做的事务处理 ,然后再将处理情况返回给协调者,最终协调者收到响应后便给事务发起者返回处理失败的结果。

2PC流程

个人觉得 2PC 实现得还是比较鸡肋的,因为事实上它只解决了各个事务的原子性问题,随之也带来了很多的问题。

  • 单点故障问题,如果协调者挂了那么整个系统都处于不可用的状态了。
  • 阻塞问题,即当协调者发送 prepare 请求,参与者收到之后如果能处理那么它将会进行事务的处理但并不提交,这个时候会一直占用着资源不释放,如果此时协调者挂了,那么这些资源都不会再释放了,这会极大影响性能。
  • 数据不一致问题,比如当第二阶段,协调者只发送了一部分的 commit 请求就挂了,那么也就意味着,收到消息的参与者会进行事务的提交,而后面没收到的则不会进行事务提交,那么这时候就会产生数据不一致性问题。

2. 3PC(三阶段提交)

因为2PC存在的一系列问题,比如单点,容错机制缺陷等等,从而产生了 3PC(三阶段提交) 。那么这三阶段又分别是什么呢?

千万不要吧PC理解成个人电脑了,其实他们是 phase-commit 的缩写,即阶段提交。

  1. CanCommit阶段:协调者向所有参与者发送 CanCommit 请求,参与者收到请求后会根据自身情况查看是否能执行事务,如果可以则返回 YES 响应并进入预备状态,否则返回 NO 。
  2. PreCommit阶段:协调者根据参与者返回的响应来决定是否可以进行下面的 PreCommit 操作。如果上面参与者返回的都是 YES,那么协调者将向所有参与者发送 PreCommit 预提交请求,参与者收到预提交请求后,会进行事务的执行操作,并将 UndoRedo 信息写入事务日志中 ,最后如果参与者顺利执行了事务则给协调者返回成功的响应。如果在第一阶段协调者收到了 任何一个 NO 的信息,或者 在一定时间内 并没有收到全部的参与者的响应,那么就会中断事务,它会向所有参与者发送中断请求(abort),参与者收到中断请求之后会立即中断事务,或者在一定时间内没有收到协调者的请求,它也会中断事务。
  3. DoCommit阶段:这个阶段其实和 2PC 的第二阶段差不多,如果协调者收到了所有参与者在 PreCommit 阶段的 YES 响应,那么协调者将会给所有参与者发送 DoCommit 请求,参与者收到 DoCommit 请求后则会进行事务的提交工作,完成后则会给协调者返回响应,协调者收到所有参与者返回的事务提交成功的响应之后则完成事务。若协调者在 PreCommit 阶段 收到了任何一个 NO 或者在一定时间内没有收到所有参与者的响应 ,那么就会进行中断请求的发送,参与者收到中断请求后则会 通过上面记录的回滚日志 来进行事务的回滚操作,并向协调者反馈回滚状况,协调者收到参与者返回的消息后,中断事务。

3PC流程

这里是 3PC 在成功的环境下的流程图,你可以看到 3PC 在很多地方进行了超时中断的处理,比如协调者在指定时间内为收到全部的确认消息则进行事务中断的处理,这样能 减少同步阻塞的时间 。还有需要注意的是,**3PCDoCommit 阶段参与者如未收到协调者发送的提交事务的请求,它会在一定时间内进行事务的提交。为什么这么做呢?是因为这个时候我们肯定保证了在第一阶段所有的协调者全部返回了可以执行事务的响应,这个时候我们有理由相信其他系统都能进行事务的执行和提交,所以不管**协调者有没有发消息给参与者,进入第三阶段参与者都会进行事务的提交操作。

总之,3PC 通过一系列的超时机制很好的缓解了阻塞问题,但是最重要的一致性并没有得到根本的解决,比如在 PreCommit 阶段,当一个参与者收到了请求之后其他参与者和协调者挂了或者出现了网络分区,这个时候收到消息的参与者都会进行事务提交,这就会出现数据不一致性问题。

所以,要解决一致性问题还需要靠 Paxos 算法⭐️ ⭐️ ⭐️ 。

3.Paxos 算法

Paxos 算法是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一,其解决的问题就是在分布式系统中如何就某个值(决议)达成一致

Paxos 中主要有三个角色,分别为 Proposer提案者Acceptor表决者Learner学习者Paxos 算法和 2PC 一样,也有两个阶段,分别为 Prepareaccept 阶段。

3.1. prepare 阶段

  • Proposer提案者:负责提出 proposal,每个提案者在提出提案时都会首先获取到一个 具有全局唯一性的、递增的提案编号N,即在整个集群中是唯一的编号 N,然后将该编号赋予其要提出的提案,在第一阶段是只将提案编号发送给所有的表决者
  • Acceptor表决者:每个表决者在 accept 某提案后,会将该提案编号N记录在本地,这样每个表决者中保存的已经被 accept 的提案中会存在一个编号最大的提案,其编号假设为 maxN。每个表决者仅会 accept 编号大于自己本地 maxN 的提案,在批准提案时表决者会将以前接受过的最大编号的提案作为响应反馈给 Proposer

下面是 prepare 阶段的流程图,你可以对照着参考一下。

paxos第一阶段

3.2. accept 阶段

当一个提案被 Proposer 提出后,如果 Proposer 收到了超过半数的 Acceptor 的批准(Proposer 本身同意),那么此时 Proposer 会给所有的 Acceptor 发送真正的提案(你可以理解为第一阶段为试探),这个时候 Proposer 就会发送提案的内容和提案编号。

表决者收到提案请求后会再次比较本身已经批准过的最大提案编号和该提案编号,如果该提案编号 大于等于 已经批准过的最大提案编号,那么就 accept 该提案(此时执行提案内容但不提交),随后将情况返回给 Proposer 。如果不满足则不回应或者返回 NO 。

paxos第二阶段1

Proposer 收到超过半数的 accept ,那么它这个时候会向所有的 acceptor 发送提案的提交请求。需要注意的是,因为上述仅仅是超过半数的 acceptor 批准执行了该提案内容,其他没有批准的并没有执行该提案内容,所以这个时候需要向未批准的 acceptor 发送提案内容和提案编号并让它无条件执行和提交,而对于前面已经批准过该提案的 acceptor 来说 仅仅需要发送该提案的编号 ,让 acceptor 执行提交就行了。

paxos第二阶段2

而如果 Proposer 如果没有收到超过半数的 accept 那么它将会将 递增Proposal 的编号,然后 重新进入 Prepare 阶段

对于 Learner 来说如何去学习 Acceptor 批准的提案内容,这有很多方式,读者可以自己去了解一下,这里不做过多解释。

3.3. paxos 算法的死循环问题

其实就有点类似于两个人吵架,小明说我是对的,小红说我才是对的,两个人据理力争的谁也不让谁🤬🤬。

比如说,此时提案者 P1 提出一个方案 M1,完成了 Prepare 阶段的工作,这个时候 acceptor 则批准了 M1,但是此时提案者 P2 同时也提出了一个方案 M2,它也完成了 Prepare 阶段的工作。然后 P1 的方案已经不能在第二阶段被批准了(因为 acceptor 已经批准了比 M1 更大的 M2),所以 P1 自增方案变为 M3 重新进入 Prepare 阶段,然后 acceptor ,又批准了新的 M3 方案,它又不能批准 M2 了,这个时候 M2 又自增进入 Prepare 阶段。。。

就这样无休无止的永远提案下去,这就是 paxos 算法的死循环问题。

那么如何解决呢?很简单,人多了容易吵架,我现在 就允许一个能提案 就行了。