Docker 高级篇
封面来源:本文封面来源于网络,如有侵权,请联系删除。
参考链接:尚硅谷Docker实战教程
1. 安装 MySQL 主从复制
新建主服务器容器实例 3307
1 | docker run -p 3307:3306 --name mysql-master \ |
进入
/mydata/mysql-master/conf
目录下新建my.cnf
文件
1 | cd /mydata/mysql-master/conf |
my.cnf
文件内容如下:
1 | [mysqld] |
修改完配置后重启 master 实例
1 | docker restart mysql-master |
进入 mysql-master 容器
1 | docker exec -it mysql-master /bin/bash |
1 | mysql -uroot -proot |
master 容器实例内创建数据同步用户
1 | -- 创建 slave 用户 |
新建从服务器容器实例 3308
1 | docker run -p 3308:3306 --name mysql-slave \ |
执行 docker ps
命令检查主机、从机所在的容器是否都正常运行。
进入
/mydata/mysql-slave/conf
目录下新建my.cnf
文件
my.cnf
文件,内容如下:
1 | [mysqld] |
修改完配置后重启 slave 实例
1 | docker restart mysql-slave |
在 主数据库 中查看主从同步状态
1 | show master status; |
进入 mysql-slave 容器
1 | docker exec -it mysql-slave /bin/bash |
在 从数据库 中配置主从复制
1 | change master to master_host='宿主机IP', master_user='slave', master_password='123456', |
主从复制命令参数说明:
-
master_host
主数据库的 IP 地址; -
master_port
主数据库的运行端口; -
master_user
在主数据库创建的用于同步数据的用户账号; -
master_password
在主数据库创建的用于同步数据的用户密码; -
master_log_file
指定从数据库要复制数据的日志文件,通过查看主数据的状态,获取 File 参数; -
master_log_pos
指定从数据库从哪个位置开始复制数据,通过查看主数据的状态,获取 Position 参数; -
master_connect_retry
连接失败重试的时间间隔,单位为秒。
如果使用的是云服务器,master_host
填写对应内网 IP。 注意,是内网 IP,不是公网 IP! 阿里云下指「主私网 IP」。
master_log_file
和 master_log_pos
填写的信息需要在主数据中执行 show master status;
命令获得。
在 从数据库 中查看主从同步状态
1 | show slave status \G; |
在 从数据库 中开启主从同步
1 | start slave; |
查看 从数据库 状态发现已经同步
1 | show slave status \G; |
此时的 Slave_IO_Running
和 Slave_SQL_Running
信息均为 Yes
,证明主从复制配置完成。
在某些情况下,Slave_IO_Running
的值可能会是 Connecting
。这可能是因为使用了较高的 MySQL 版本,比如 MySQL 8.0,此时需要在 主数据库 中执行以下两条命令:
1 | ALTER USER 'slave'@'%' IDENTIFIED WITH mysql_native_password BY '123456'; |
1 | flush privileges; |
执行完成后,重启从数据库即可。
主从复制测试
现在主数据库里创建一个数据库:
1 | CREATE DATABASE db01; |
使用这个数据库:
1 | USE db01; |
新建一张表:
1 | CREATE TABLE t1 (id INT, name VARCHAR(20)); |
向表里插入一条数据:
1 | INSERT INTO t1 VALUES(1, 'mofan'); |
查询这张表里的所有数据:
1 | SELECT * FROM t1; |
mysql> SELECT * FROM t1; +------+-------+ | id | name | +------+-------+ | 1 | mofan | +------+-------+ 1 row in set (0.00 sec)
接下来切换到从数据库,尝试使用同名的数据库:
1 | USE db01; |
命令执行后没有报错,再尝试查询表里所有数据:
1 | SELECT * FROM t1; |
最终能够查询到在主数据库中添加的数据,证明主从复制功能无误。
2. 分布式数据分区算法
如果有亿级数据需要缓存,应该如何设计这个存储案例?
此时使用单机 Redis 肯定是不行的,需要使用到分布式存储,那应该怎么落地?
目前业界关于分布式数据分区有三种解决方案:
- 哈希取余算法
- 一致性哈希算法
- 哈希槽算法
2.1 哈希取余算法
以 2 亿条记录为例,在 Redis 里就是 2 亿个 KV 键值对,必须要使用分布式多机。
假设 N 台机器构成一个集群,用户每次读写操作都要根据以下公式计算出哈希值,用来决定数据映射到哪一个 Redis 节点上:
优点
简单粗暴,直接有效,只需要预估好数据就能规划好节点,能保证一段时间的数据支撑。使用 Hash 算法让固定的一部分请求落到同一台服务器上,每台服务器固定处理一部分请求(并维护这些请求的信息),起到负载均衡与分而治之的作用。
缺点
如果集群进行了扩容或缩容导致节点数量发生变动,映射关系就需要重新计算。在服务器数量固定不变时没有问题,而在需要弹性扩容或故障停机的情况下,原来取模公式中的 就会发生变化,此时运算结果会发生很大的变化,导致根据公式获取的服务器信息变得不可控。
2.2 一致性哈希算法
一致性哈希算法在 1997 年由麻省理工学院提出,设计目标是为了解决分布式缓存数据变动和映射问题。
使用一致性哈希算法,当服务器个数发生变动时, 尽量减少客户端与服务器之间的映射关系的影响。
一致性哈希算法分为 3 步:
- 构建哈希环
- 节点映射
- 落键规则
构建哈希环
首先需要一个 hash
函数用于计算哈希值,其所有可能的哈希值会构成一个全量集,该集合构成一个范围为 的线性哈希区间。在一致性哈希算法中,需要通过适当的逻辑将线性哈希区间首尾相连,此时 与 在同一个位置,让它在逻辑上形成一个环形空间。
一致性哈希算法也将哈希取余算法类似的取模方法,只不过不再是对节点(服务器)的数量进行取模,而是对 进行取模。
简单来说,一致性哈希算法将整个哈希值空间组织成一个虚拟的圆环。假设某哈希函数 H
的值空间为 (即哈希值是一个 32 位无符号整型),整个哈希环如下图:
整个空间按顺时针方向组织,圆环正上方的点代表 , 点右侧的第一个点代表 ,并以此类推,直到 ,也就是说 点左侧的第一个点是 , 和 在零点钟方向重合,把这个由 个点组成的圆环称为哈希环。
节点映射
得到哈希环之后,需要将集群中各个节点映射到环上的某个位置。
使用服务器的 IP 或主机名作为关键词,在利用先前的 Hash
进行计算得到各个节点的哈希值,确定集群中每台机器在哈希环上的位置。
假如有 4 个节点 A、B、C、D,经过计算后,它们在哈希环上的空间位置如下:
落键规则
在 Redis 集群上使用一致性哈希算法,当需要存储一个 KV 键值对时,首先计算 Key 的哈希值确定其在哈希环上的位置,之后从此位置沿环顺时针移动,遇到的第一台 Redis 就是目标存机器,之后将键值对保存在该节点上。
假如有 、、 和 四个数据对象,经过计算后,它们在环空间上的位置如下:
根据一致性哈希算法, 会被定位到 Node A 上, 被定位到 Node B 上, 被定位到 Node C 上, 被定为到 Node D 上。
一致性哈希算法的优点
一致性哈希算法具有一定的容错性和扩展性。
容错性:
假设 Node C 宕机,此时 、 和 不会受到影响,只有 会被重定位到 Node D。
在一致性哈希算法中,如果一台服务器不可用,受影响的数据仅仅是此服务器到其环空间中的前一台服务器(沿逆时针方向移动遇到的第一台服务器)之间数据,其它数据不会受到影响。
简单说来说,当 Node C 宕机了,只有 Node B 和 Node C 之间的数据会受到影响,这些数据会转移到 Node D 进行存储。
扩展性:
如果需要增加一个节点 Node X,其位于 Node B 和 Node C 之间,此时受到影响的只有 Node B 到 Node X 之间的数据,这使得 Node B 到 Node X 之间的数据会被写入 Node X 中,而不会导致所有数据重新洗牌。
一致性哈希算法的缺点
当节点数量太少时,会出现因为节点分布不均而导致数据倾斜,大量数据会集中打向同一个节点。
为了解决这个问题,一致性哈希算法引入了虚拟节点机制。对每一个节点计算多个哈希值,每个计算结果位置都放置一个相同的服务节点(称为虚拟节点)。
具体做法可以先确定每个节点关联的虚拟节点数量,然后原本节点名称后面加上编号。
例如对 Node A 节点虚拟出 NodeA#1、NodeA#2 和 NodeA#3 共 3 个节点,对 Node B 虚拟出 NodeB#1、NodeB#2 和 NodeB#3 共 3 个节点,总共形成 6 个虚拟节点。
如果再加入或删除节点,只会影响哈希环中顺时针方向相邻节点,而对其他节点无影响。
此时数据的分布和节点的位置有关,因为这些虚拟节点不是均匀分布在哈希环上的,所以无法均匀地存储数据。
2.3 哈希槽算法
哈希槽的本质是一个数组,数组 形成哈希槽(hash slot)空间。
使用哈希槽能够解决数据不均匀分配的问题。在数据和节点之间添加一层哈希槽(slot),用于管理数据和节点之间的关系,此时节点上放的是槽,槽里放的才是数据。
槽解决的是粒度问题,相当于把粒度变大了,更便于数据移动。哈希解决的是映射问题,使用 Key 的哈希值来计算所在的槽,更便于数据分配。
一个 Redis 集群只能有 个槽,编号范围 ,即 至 。Redis 根据节点数量将这些槽大致均等地分配给集群中的所有主节点,对分配策略没有要求,可以指定哪些编号的槽分配给哪个主节点,集群会记录节点和槽的对应关系。
解决了节点和槽的关系后,接下来就需要对 Key 求哈希值,然后对 取余,根据结果确定 Key 归属的槽:
之后会以槽为单位移动数据,因为槽的数目是固定的,处理起来比较容易,这样数据移动问题就解决了。
Redis 集群为什么只能有 16384 个槽
原 GitHub issue:why redis-cluster use 16384 slots? · Issue #2576 · redis/redis
可以参考的中文回答:issues#2576#issuecomment
CRC16 算法产生的 hash 值有 16bit,该算法可以产生 个值,但为了心跳方便和数据传输最大化,槽的数量只能有 个(即 16384)。
如果槽位数量为 个,那么发送心跳信息的消息头将达到 8k,发送的心跳包过于庞大。在消息头中最占空间的是 myslots[CLUSTER_SLOTS/8]
。当槽位为 时,这块的大小是 :
Redis 节点每秒需要发送一定数量的 ping 消息作为心跳,如果槽位为 ,那么这个 ping 消息头就会太大而浪费带宽。
Redis 集群的主节点数量基本不可能超过 1000 个。集群节点越多,心跳包的消息体内携带的数据越多(心跳包带有节点的完整配置,可以用幂等的方式替换旧节点的配置,以更新旧节点)。如果节点超过 1000 个,也会导致网络拥堵。因此 Redis 作者不建议 Redis 集群节点超过 1000 个。对于节点数在 1000 以内的 Redis 集群,16384 个槽位足够了,没有必要扩展到 65536 个。
槽位越小,节点少的情况下压缩比越高,更容易传输。Redis 主节点的配置信息中,它所负责的哈希槽是通过一个 Bitmap 来保存的,在传输过程中会对 Bitmap 进行压缩,但如果 Bitmap 的填充率 ( 为节点数)很高,Bitmap 的压缩率会很低。如果节点数很少,但哈希槽很多,Bitmap 的压缩率就会很低。
3. 安装 Redis 集群
3.1 三主三从 Redis 集群配置
新建 6 个 Docker 容器实例
1 | 启动第 1 台节点 |
命令解析:
--net host
使用宿主机的 IP 和端口,默认--cluster-enabled yes
开启 Redis 集群--appendonly yes
开启 Redis 持久化--port 6381
配置 Redis 端口号
启动 6 个容器实例后,执行 docker ps
命令检查容器状态:
进入容器
redis-node-1
并为 6 台机器构建集群关系
进入容器 redis-node-1
:
1 | docker exec -it redis-node-1 /bin/bash |
在容器内构建 Redis 主从关系:
1 | redis-cli --cluster create 宿主机IP:6381 宿主机IP:6382 宿主机IP:6383 宿主机IP:6384 宿主机IP:6385 宿主机IP:6386 --cluster-replicas 1 |
--cluster-replicas 1
表示为每个 master 构建一个 slave 节点。
如果使用的是云服务器,此处的宿主机 IP 使用私网 IP,阿里云下就是「主私网IP」,并且记得在安全组里开放指定的 6 个端口。
执行命令后能够看到哈希槽的分配情况和 Redis 主从关系:
>>> Performing hash slots allocation on 6 nodes... Master[0] -> Slots 0 - 5460 Master[1] -> Slots 5461 - 10922 Master[2] -> Slots 10923 - 16383 Adding replica 宿主机IP:6385 to 宿主机IP:6381 Adding replica 宿主机IP:6386 to 宿主机IP:6382 Adding replica 宿主机IP:6384 to 宿主机IP:6383
同时出现以下询问:
Can I set the above configuration? (type 'yes' to accept):
键入 yes
即可。之后如果得到如下信息,证明主从关系构建成功:
[OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered.
在容器
redis-node-1
登入 Redis,并将其作为切入点,查看集群状态
登入 Redis:
1 | redis-cli -p 6381 |
查看集群信息:
1 | cluster info |
root@mofan:/data# redis-cli -p 6381 127.0.0.1:6381> cluster info cluster_state:ok cluster_slots_assigned:16384 cluster_slots_ok:16384 cluster_slots_pfail:0 cluster_slots_fail:0 cluster_known_nodes:6
可以看到:
- 集群状态
cluster_state
是ok
- 被分配的哈希槽数量
cluster_slots_assigned
有16384
个 - 集群中已知的节点
cluster_known_nodes
有6
个
执行以下命令查看集群节点情况:
1 | cluster nodes |
3.2 主从容错切换迁移
数据读写存储
先进入 redis-node-1
容器:
1 | docker exec -it redis-node-1 /bin/bash |
进入 Redis:
1 | redis-cli -p 6381 |
查看所有键信息:
127.0.0.1:6381> keys * (empty array)
添加一个键值对:
127.0.0.1:6381> set k1 v1 (error) MOVED 12706 宿主机IP:6383
居然出现了 error?😲
换一个试试:
127.0.0.1:6381> set k2 v2 OK
可以存储 k2-v2
的键值对。这又是为什么呢?
添加 k1
时,计算得到的哈希槽为 ,但是当前连接的 redis-server 为 6381
,它的哈希槽编号范围是 ,所以会存不进去导致报错。 添加 k2
能够成功,自然是因为这时计算出的哈希槽编号在 区间之间。
为解决这个问题,在连接 Redis 时需要添加 -c
选项防止路由失效:
1 | redis-cli -p 6381 -c |
再添加 k1-v1
键值对:
127.0.0.1:6381> set k1 v1 -> Redirected to slot [12706] located at 宿主机IP:6383 OK 宿主机IP:6383>
键值对添加成功,并显示重定向到端口 6383
的 Redis。
集群信息检查
进入容器后,输入任意一个 master 节点地址进行集群检查。以 6381
为例:
1 | redis-cli --cluster check 宿主机IP:6381 |
如果使用的是云服务器,此处的宿主机 IP 使用私网 IP,阿里云下就是「主私网IP」。
执行上述命令后会返回以下信息:
- 当前集群各节点存储的 key 的数量
- 主从机信息
3.3 主从容错切换迁移
已知当前 Redis 集群信息如下:
主机名称:端口 | 对应从机名称:端口 |
---|---|
redis-node-1:6381 |
redis-node-4:6384 |
redis-node-2:6382 |
redis-node-5:6385 |
redis-node-3:6383 |
redis-node-6:6386 |
模拟 6381
端口的 redis-node-1
容器宕机。
查找 6381
端口的 PID:
1 | lsof -i:6381 |
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME redis-ser 25946 polkitd 6u IPv6 83743781 0t0 TCP *:6381 (LISTEN)
kill
掉 PID 为 25946
的进程:
1 | kill -9 25946 |
再使用 docker ps -a
命令查看所有容器状态,会发现 redis-node-1
容器已经停止运行。
进入 redis-node-2
容器:
1 | docker exec -it redis-node-2 /bin/bash |
登入 6382
端口的 Redis:
1 | redis-cli -p 6382 -c |
查看当前集群信息:
1 | cluster nodes |
此时 6381
端口的 Redis 已经失去连接,显示 disconnected
,同时其原本对应的从机 —— 6384
端口的 Redis 已经称为主机。
在上一节中向 Redis 内添加了 k1-v1
和 k2-v2
键值对,此时尝试通过 Key 获取对应的 Value 也是完全可行的:
127.0.0.1:6382> get k1 -> Redirected to slot [12706] located at 宿主机IP:6383 "v1" 宿主机IP:6383> get k2 -> Redirected to slot [449] located at 宿主机IP:6384 "v2"
那如果这时宕机的 6381
又活了过来呢?使用 docker start 容器ID
模拟 6381
复活。
再次查看当前集群信息:
1 | cluster nodes |
尽管此时 6381
端口的 Redis 已经恢复连接,显示 connected
,但它变成了从机,而 6384
端口的 Redis 依旧是主机。
如果又断开 6384
的连接呢?
1 | 简单处理,用 docker stop 命令断开连接 |
这时 6381
又会恢复成主机,如果 6384
再复活,它也还是只能是从机。
3.4 主从扩容
现在需要往 3 主 3 从的集群里再添加 1 主 1 从两个节点。
首先启动 2 个新的容器,并指定端口信息:
1 | docker run -d --name redis-node-7 --net host --privileged=true -v /app/redis-cluster/share/redis-node-7:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6387 |
1 | docker run -d --name redis-node-8 --net host --privileged=true -v /app/redis-cluster/share/redis-node-8:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6388 |
使用 docker ps
查看当前正在运行的容器,可以看到新增两个名为 redis-node-7
和 redis-node-8
的容器。
进入 redis-node-7
容器内部:
1 | docker exec -it redis-node-7 /bin/bash |
将该容器内的 Redis 作为 master 节点加入原有集群:
1 | redis-cli --cluster add-node 宿主机IP:6387 宿主机IP:6381 |
其中的 宿主机IP:6387
表示加入集群的节点地址,宿主机IP:6381
可以是目标集群中任意一个节点的地址,甚至用 宿主机IP:6386
也行。
添加成功后查看集群状态:
1 | redis-cli --cluster check 宿主机IP:6381 |
M: 2c02d6705ec757cee03219f51091b2afc3a6d6c5 宿主机IP:6387 slots: (0 slots) master
可以看到 6387
已经作为 master 节点加入了集群,但是该节点还没有分配槽位,即 0 slots
。
执行以下命令重新分配槽位:
1 | redis-cli --cluster reshard 宿主机IP:6381 |
最后的 宿主机IP:6381
可以是集群中任意节点的地址,甚至可以是刚刚加入集群的 宿主机IP:6387
。
执行命令后会提示需要移动多少个槽位:
How many slots do you want to move (from 1 to 16384)?
由于总共 个槽位,现在有 台主机,根据 ,因此需要移动的槽位数是 。
输入 4096
后回车,又会提示需要输入接受节点 ID:
What is the receiving node ID?
输入先前的 6387
节点的 ID,表示将 4096
个槽位分配给 6387
节点。
节点 ID 是查看节点信息时前面很长的那串十六进制字符串:
M: 2c02d6705ec757cee03219f51091b2afc3a6d6c5 宿主机IP:6387 slots: (0 slots) master
比如这里 6387
节点的 ID 就是 2c02d6705ec757cee03219f51091b2afc3a6d6c5
。
输入 6387
节点 ID 后再次回车,接下来又会提示:
Please enter all the source node IDs. Type 'all' to use all the nodes as source nodes for the hash slots. Type 'done' once you entered all the source nodes IDs. Source node #1:
要求输入需要以哪些节点为源节点来分配槽到 6387
节点,这里需要将所有节点作为源节点,表示从每个节点里都「匀」点槽给 6387
节点,因此输入 all
。
接下来遇到最后一个提示信息:
Do you want to proceed with the proposed reshard plan (yes/no)?
询问是否需要继续执行哈希槽分配计划,输入 yes
即可。
耐心等待 个槽的重新分配,之后再次查看集群信息:
1 | redis-cli --cluster check 宿主机IP:6381 |
root@mofan:/data# redis-cli --cluster check 宿主机IP:6381 宿主机IP:6381 (282567ff...) -> 0 keys | 4096 slots | 1 slaves. 宿主机IP:6383 (910bf425...) -> 1 keys | 4096 slots | 1 slaves. 宿主机IP:6382 (daae6662...) -> 0 keys | 4096 slots | 1 slaves. 宿主机IP:6387 (2c02d670...) -> 1 keys | 4096 slots | 0 slaves. [OK] 2 keys in 4 masters. 0.00 keys per slot on average. >>> Performing Cluster Check (using node 宿主机IP:6381) M: 282567ffb3946e7017e5f92b6560c5a56e505bca 宿主机IP:6381 slots:[1365-5460] (4096 slots) master 1 additional replica(s) S: 5f56f6e19ae385a327bc224fd7818f4e296e6cbc 宿主机IP:6386 slots: (0 slots) slave replicates 910bf425f16e7481486658734565f10b8ededcbd M: 910bf425f16e7481486658734565f10b8ededcbd 宿主机IP:6383 slots:[12288-16383] (4096 slots) master 1 additional replica(s) M: daae6662affc36ac6fa68af021fc049cfbdad391 宿主机IP:6382 slots:[6827-10922] (4096 slots) master 1 additional replica(s) M: 2c02d6705ec757cee03219f51091b2afc3a6d6c5 宿主机IP:6387 slots:[0-1364],[5461-6826],[10923-12287] (4096 slots) master S: 1facb270069567af4f26a2ddbc66d7b76341c33e 宿主机IP:6385 slots: (0 slots) slave replicates daae6662affc36ac6fa68af021fc049cfbdad391 S: 8759bf90bbf13e795bc76a677b34db1a433626c4 宿主机IP:6384 slots: (0 slots) slave replicates 282567ffb3946e7017e5f92b6560c5a56e505bca [OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered.
可以看到:
- 现在集群里有 7 台 Redis,其中 4 主 3 从;
- 分配槽位时,原本每台主机都给
6387
「匀」了一点,6387
占有的槽位编号是[0-1364],[5461-6826],[10923-12287]
,共4096
个。
接下来需要为 6387
分配 6388
从节点:
1 | redis-cli --cluster add-node 宿主机IP:6388 宿主机IP:6381 --cluster-slave --cluster-master-id 目标主节点的节点ID |
其中的 宿主机IP:6381
可以是节点里的任意节点,最后的「目标主节点的节点 ID」就是 6387
节点的 ID。
最后查看集群状态:
1 | redis-cli --cluster check 宿主机IP:6381 |
此时集群中共有 4 台 Redis,新增 6387
6388
两台一主一从,6388
作为从节点挂在 6387
下。
3.5 主从缩容
现在需要贯彻落实开源节流,4 主 4 从的 Redis 集群太奢侈了,3 主 3 从就够用。那么要怎么缩容呢?
假设需要将 6387
和 6388
「干掉」,先进入其他任意不包含这两个 Redis 的容器中:
1 | docker exec -it redis-node-1 /bin/bash |
查看集群状态:
1 | redis-cli --cluster check 宿主机IP:6381 |
在缩容时,需要先移除从节点。
将 6388
节点从集群中移除:
1 | redis-cli --cluster del-node 宿主机IP:6388 6388节点ID |
root@mofan:/data# redis-cli --cluster del-node 宿主机IP:6388 7b06b67eb4cc8a6f47307a8f065053638c5093c4 >>> Removing node 7b06b67eb4cc8a6f47307a8f065053638c5093c4 from cluster 宿主机IP:6388 >>> Sending CLUSTER FORGET messages to the cluster... >>> Sending CLUSTER RESET SOFT to the deleted node.
再次查看集群状态:
1 | redis-cli --cluster check 宿主机IP:6381 |
此时容器中的配置是 4 主 3 从,6388
已经被移除。
接下来需要移除 6387
主机,但是该主机上还有被分配的槽位,需要先将其槽位分配给其他主节点。
对集群重新分配哈希槽可以像前文扩容那样使用如下命令:
1 | redis-cli --cluster reshard 宿主机IP:6381 |
询问需要移动多少个槽位:
How many slots do you want to move (from 1 to 16384)?
6387
上有 4096
个槽,因此输入 4096
。
接着询问接受这些槽的节点 ID:
What is the receiving node ID?
先把 6387
的槽移给 6381
,这里输入 6381
的节点 ID。
然后询问需要使用哪些节点作为源节点:
Please enter all the source node IDs. Type 'all' to use all the nodes as source nodes for the hash slots. Type 'done' once you entered all the source nodes IDs. Source node #1:
要移除 6387
节点,自然是把 6387
的槽分配出来,因此输入 6387
的节点 ID。输入后回车,然后再输入 done
:
Source node #1: 6387节点ID Source node #2: done
最后询问是否继续:
Do you want to proceed with the proposed reshard plan (yes/no)?
输入 yes
即可。
等待分配完成后查看集群状态:
1 | redis-cli --cluster check 宿主机IP:6381 |
此时依旧是 4 主 3 从的配置,但是 6387
节点上已经没有槽,是 0 slots
。
尝试移除 6387
节点:
1 | redis-cli --cluster del-node 宿主机IP:6387 6387节点ID |
再次查看集群状态:
1 | redis-cli --cluster check 宿主机IP:6381 |
此时集群的配置变成 3 主 3 从。
由于 6381
从 6387
中获得了 个槽位,因此它现在有 个槽位。
可以使用以下命令将当前集群中的槽位再次分配均匀:
1 | redis-cli --cluster rebalance --cluster-use-empty-masters 宿主机IP:6381 |
其中:
rebalance
表示重新分配集群中的哈希槽,使数据分配更均匀--cluster-use-empty-masters
表示将空节点(未分配任何槽位的节点)也纳入平衡逻辑,就是分配时还会把槽位分配给空节点。当前的场景下这个参数没啥用,不需要填充空节点。
最后查看集群状态:
1 | redis-cli --cluster check 宿主机IP:6381 |
集群中 3 个主机拥有的槽位数量大致均匀。
4. Dockerfile
4.1 概述
官方网站:Dockerfile reference | Docker Docs
Dockerfile 是用来构建 Docker 镜像的文本文件,是由一条条构建镜像所需的指令和参数构成的脚本。
构建三步骤:
- 编写 Dockerfile 文件
- 使用
docker build
命令构建镜像 - 使用
docker run
命令运行容器实例
4.2 构建过程
Dockerfile 内容基础知识
- 每条保留字指令都 必须为大写字母 且后面要跟随至少一个参数
- 指令按照从上到下,顺序执行
#
表示注释- 每条指令都会创建一个新的镜像层并对镜像进行提交
Docker 执行 Dockerfile 的大致流程
- Docker 从基础镜像运行一个容器
- 执行一条指令并对容器作出修改
- 执行类似
docker commit
的操作提交一个新的镜像层 - Docker 再基于刚提交的镜像运行一个新容器
- 执行 Dockerfile 中的下一条指令直到所有指令都执行完成
小总结
从应用软件的角度来看,Dockerfile、Docker 镜像与 Docker 容器分别代表软件的三个不同阶段:
- Dockerfile 是软件的原材料
- Docker 镜像是软件的交付品
- Docker 容器则是软件镜像的运行态,是依照镜像运行的容器实例
Dockerfile 面向开发,Docker 镜像成为交付标准,Docker 容器涉及部署与运维,三者缺一不可,合力充当 Docker 体系的基石。
- Dockerfile 定义了进程需要的一切东西。Dockerfile 涉及的内容包括执行代码、文件、环境变量、依赖包、运行时环境、动态链接库、操作系统的发行版、服务进程和内核进程(当应用进程需要和系统服务和内核进程打交道时,需要考虑如何设计 namespace 的权限控制)等等;
- Docker 镜像,在用 Dockerfile 定义一个文件之后,执行
docker build
命令时会产生一个 Docker 镜像,运行 Docker 镜像时会真正开始提供服务; - Docker 容器是直接提供服务的。
4.3 Dockerfile 保留字
参考 Tomcat 的 Dockerfile:docker-library/tomcat
FROM
基础镜像,当前新镜像是基于哪个镜像的,指定一个已经存在的镜像作为模板,第一条必须是 From
。
MAINTAINER
镜像维护者的姓名和邮箱地址。
RUN
容器构建时需要运行的命令。
两种格式:
1 | # shell 格式 |
1 | # exec 格式 |
RUN
是在 docker build
时运行。
EXPOSE
当前容器对外暴露出的端口。
WORKDIR
一个落脚点。指定在创建容器后,终端默认登陆的进来工作目录。
USER
指定该镜像以什么样的用户去执行,如果都不指定,默认 root
。
ENV
在构建镜像过程中设置环境变量。
比如:
1 | ENV MY_PATH /usr/mytest |
指定的 MY_PATH
环境变量可以在后续的任何 RUN
指令中使用,也可以在其他指令中直接使用这些环境变量,比如:
1 | WORKDIR $MY_PATH |
ADD
将宿主机目录下的文件拷贝进镜像,并自动处理 URL 和解压 tar 压缩包。
COPY
类似 ADD
,用于拷贝文件和目录到镜像中。
将构建上下文目录中 <源路径> 的文件/目录
复制到新的一层的镜像内的 <目标路径>
位置。
与 RUN
类似,也有两种书写方式:
1 | # 1. shell 格式 |
1 | # 2. exec 格式 |
src
表示源文件或源目录dest
表示容器内的指定路径,如果路径不存在,会自动创建
VOLUME
容器数据卷,用于数据保存和持久化工作。
作用与 docker run
命令中的 -v
参数类型。
CMD
指定容器启动后要做的事。
与 RUN
类似,也有两种书写方式:
1 | # 1. shell 格式 |
1 | # 2. exec 格式 |
Dockerfile 中可以有多个 CMD
指令,但是 只有最后一个生效, 并且最后一个 CMD
还会被 docker run
之后的参数替换。
以 Tomcat 的 Dockerfile 最后一行为例:
1 | CMD ["catalina.sh", "run"] |
这表示在容器启动后运行 catalina.sh
文件。
在使用 tomcat 镜像创建容器实例时会使用以下命令:
1 | docker run -d -p 8080:8080 tomcat |
命令执行后,访问 8080
端口能够看到「那只猫」:
还记得使用 ubuntu 镜像创建容器实例的命令吗?
1 | docker run -it ubuntu /bin/bash |
该命令最后在 ubuntu
后追加了 /bin/bash
,如果使用 tomcat 镜像创建容器实例时也追加 /bin/bash
呢?
1 | docker run -d -p 8080:8080 tomcat /bin/bash |
容器实例能够正常创建,但容器状态是 Exited
,容器已经停止运行,访问 8080
端口自然也看不到「那只猫」。
这是因为 Dockerfile 最后的 CMD ["catalina.sh", "run"]
指令被 /bin/bash
替换,导致容器没有正常启动。
CMD
与 RUN
的区别:
CMD
在docker run
时运行RUN
在docker build
时运行
ENTRYPOINT
ENTRYPOINT
也是用来指定容器启动时需要运行的命令,类似于 CMD
指令。
不同的是,ENTRYPOINT
不会被 docker run
命令后面的参数覆盖,并且这些命令行参数会被当作参数传递给 ENTRYPOINT
指令指定运行的程序。
指令格式:
1 | ENTRYPOINT ["<executeable>", "<param1>", "<param2>", ...] |
ENTRYPOINT
可以和 CMD
一起使用。
当两者一起使用时,CMD
的含义就发生了变化,此时的 CMD
相当于在给 ENTRYPOINT
传参,而不是直接运行其命令。
假设已经通过 Dockerfile 构建了 nginx:test
镜像:
1 | FROM nginx |
当执行 docker run nginx:test
后,在容器启动时,相当于又执行了:
1 | nginx -c /etc/nginx/nginx.conf |
而如果执行的是 docker run nginx:test /etc/nginx/new.conf
,在容器启动时,相当于执行了:
1 | nginx -c /etc/nginx/new.conf |
这是因为 Dockerfile 中的 CMD
指令被 docker run
命令后的 /etc/nginx/new.conf
参数替换了。
综上,在执行 docker run
时可以指定 ENTRYPOINT
运行所需的参数。
但请注意,与 CMD
一样,如果 Dockerfile 中存在多个 ENTRYPOINT
指令,也是只有最后一个生效。
小总结
4.4 案例演示
自定义镜像
mycentosjava8
拉取 centos7 镜像:
1 | docker pull centos:7 |
使用该镜像创建容器实例:
1 | docker run -it 镜像ID /bin/bash |
尝试在容器中使用各种命令:
[root@c0652f0b7533 /]# vim a.txt bash: vim: command not found [root@c0652f0b7533 /]# ifconfig bash: ifconfig: command not found [root@c0652f0b7533 /]# java -version bash: java: command not found
根据运行结果可知,vim
、ifconfig
和 java
在当前容器中都不存在。
现在需要在容器里安装这些组件,需要怎么做?
挨个执行 yum
命令进行安装,然后再用 docker commit
命令构建新镜像?
错误的!现在应该使用更「高级」的 Dockerfile 进行构建。
退出当前容器,在根目录下创建 /myfile
目录:
1 | mkdir /myfile |
在该目录下下载 JDK8,记得下载的是后缀名为 .tar.gz
压缩包:
1 | wget 下载地址 |
可以参考的下载地址:
之后执行 vim Dockerfile
命令,开始编写 Dockerfile:
1 | FROM centos:7 |
变成完成后,使用 Dockerfile 构建 Docker 镜像:
1 | docker build -t 新镜像名字:TAG . |
注意: 定义的 TAG
后面有个空格,空格后面还有个 .
。
等待镜像构建成功后,执行以下命令创建容器实例:
1 | docker run -it mycentosjava8:1.0 /bin/bash |
进入容器后,重新验证 yum
、ifconfig
和 java
命令,如果无误,证明镜像构建成功。
4.5 虚悬镜像
虚悬镜像,即 dangling image,仓库名、标签都是 <none>
的镜像。
可以使用以下 Dockerfile 构建一个虚悬镜像:
1 | from ubuntu |
基于 Dockerfile 构建镜像:
1 | docker build . |
之后查看所有镜像:
1 | docker images |
REPOSITORY TAG IMAGE ID CREATED SIZE mycentosjava8 1.0 af6045743961 23 hours ago 1.41GB <none> <none> 05e9a6757e02 3 years ago 72.8MB
可以看到存在一个 REPOSITORY
和 TAG
都是 <none>
的镜像。
可以单独使用以下命令查看虚悬镜像:
1 | docker image ls -f dangling=true |
针对虚悬镜像,应当尽快删除:
1 | docker image prune |
[root@mofan test]# docker image prune WARNING! This will remove all dangling images. Are you sure you want to continue? [y/N]
键入 y
删除所有虚悬镜像。
5. 微服务实战
5.1 搭建项目
注意: 本次搭建的项目将采用当前最新的 SpringBoot 3.4.3 与 JDK21。
本节的主要目的是获取项目打包后对应的 JAR 包,如果不了解 Java、SpringBoot、Maven 的基本操作,可以点击 链接 下载所需 JAR 包。
使用 IDEA 创建 SpringBoot 项目,pom.xml
内容如下:
1 |
|
修改配置文件 application.properties
,指定服务端口为 9999
:
1 | server.port=9999 |
SpringBoot 主启动类如下:
1 |
|
一个简单的 Controller 层:
1 |
|
使用 Maven 打包项目,在项目的 target
目录下得到名为 docker-boot-0.0.1-SNAPSHOT.jar
的 JAR 包。
5.2 发布到容器
先尝试拉取 openjdk:21
镜像:
1 | docker pull openjdk:21 |
配置的 Docker 镜像加速器可能对该镜像的支持较弱,可以在 /etc/docker/daemon.json
文件中追加以下镜像加速器地址:
1 | { |
上述镜像加速器来源网址:轩辕镜像
执行以下命令,在根目录下创建 mydocker
目录:
1 | mkdir /mydocker |
进入 /mydocker
目录:
1 | cd /mydocker |
把前一节得到的 docker-boot-0.0.1-SNAPSHOT.jar
文件上传至该目录。
使用 vim Dockerfile
命令创建 Dockerfile 文件,并添加如下内容:
1 | # 基础镜像使用 openjdk21 |
构建 mofan-docker:1.0
镜像:
1 | docker build -t mofan-docker:1.0 . |
使用新构建的 mofan-docker:1.0
镜像创建容器实例:
1 | docker run -d -p 9999:9999 mofan-docker:1.0 |
最后尝试能否成功访问对应接口:
1 | curl 127.0.0.1:9999/order/docker |
[root@mofan mydocker]# curl 127.0.0.1:9999/order/docker hello world: 9999, UUID: a6e8b395-20ae-424e-bb1a-54456128055d
1 | curl 127.0.0.1:9999/order/index |
[root@mofan mydocker]# curl 127.0.0.1:9999/order/index 服务端口号: 9999, UUID: f7682e19-c565-4d27-b983-b143e564f617
6. Docker 网络
6.1 启动前后的网络
在 首次 启动 Docker 服务前,使用 ifconfig
或 ip addr
查看网卡信息,可能出现:
-
ens33
或eth0
:本机网卡 -
lo
:本机回环网络网卡 -
如果使用的是虚拟机,还可能出现
virbr0
。在 CentOS7 的安装过程中如果选择了相关虚拟化的服务(libvirt
服务),启动网卡时会发现有一个以网桥连接的私网地址的virbr0
网卡,它有一个固定的 IP 地址192.168.122.1
,作为虚拟机网桥使用,为连接其上的虚机网卡提供 NAT 访问外网的功能。如果不需要可以直接将libvirt
服务卸载,使用yum remove libvirt-libs.x86_64
命令。
使用 systemctl start docker
命令 首次 启动 Docker 服务后,就会多出 docker0
网卡:
-
容器间的互联、通信以及端口映射
-
容器 IP 变动时候可以通过服务名进行网络通信
Docker 容器的网络隔离是通过 Linux 内核特性 namespace
和 cgroup
实现的。
6.2 网络命令
查看 Docker 网络模式
1 | docker network ls |
默认有以下 3 个网络模式:
[root@mofan ~]# docker network ls NETWORK ID NAME DRIVER SCOPE 2bb8b4f40c6a bridge bridge local 56db994e8554 host host local 5ccc6a0ec3d0 none null local
当然,不同设备上的 NETWORK ID
会不一样。
可以看到其中有名为 bridge
的网络模式,即网桥模式。
桥接网络使用软件桥接,它允许连接到同一桥接网络的容器进行通信,同时提供与未连接到该桥接网络的容器的隔离。Docker 网桥驱动程序会自动在主机上安装规则,这样不同网桥网络上的容器就不能直接相互通信。
网络命令帮助
1 | docker network --help |
[root@mofan ~]# docker network --help
Usage: docker network COMMAND
Manage networks
Commands: connect Connect a container to a network create Create a network disconnect Disconnect a container from a network inspect Display detailed information on one or more networks ls List networks prune Remove all unused networks rm Remove one or more networks
Run 'docker network COMMAND --help' for more information on a command.
更多命令
添加网络:
1 | docker network add 网络名称 |
删除网络:
1 | docker network rm 网络名称 |
查看指定网络信息:
1 | docker network inspect 网络名称 |
删除所有无效的网络:
1 | docker network prune |
6.3 网络模式
网络模式 | 简介 | 使用 |
---|---|---|
bridge | 为每一个容器分配、设置 IP 等,并将容器连接到一个 docker0 虚拟网桥,默认为该模式 |
--network bridge ,默认使用 docker0 |
host | 容器将不会虚拟出自己的网卡、配置自己的 IP 等,而是使用宿主机的 IP 和端口 | --network host |
none | 容器有独立的 Network namespace,但并没有对齐进行任何网络设置,如分配 veth pari 和 网桥连接、IP 等 |
--network none |
container | 新创建的容器不会创建自己的网卡和配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等 | --network container:NAME或者容器ID |
容器实例内默认网络 IP 生成规则
创建两个 ubuntu 容器实例:
1 | docker run -it --name u1 ubuntu bash |
查看容器运行情况:
1 | docker ps |
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 681e5033f136 ubuntu "bash" 16 seconds ago Up 15 seconds u2 a66712ac8b88 ubuntu "bash" 31 seconds ago Up 30 seconds u1
查看 u1
容器的网络信息:
1 | docker inspect u1 | tail -n 21 |
"Networks": { "bridge": { "IPAMConfig": null, "Links": null, "Aliases": null, "MacAddress": "02:42:ac:11:00:02", "NetworkID": "xxx", "EndpointID": "xxx", "Gateway": "172.17.0.1", "IPAddress": "172.17.0.2", "IPPrefixLen": 16, "IPv6Gateway": "", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "DriverOpts": null, "DNSNames": null } } } } ]
查看 u1
容器的网络信息:
1 | docker inspect u2 | tail -n 21 |
"Networks": { "bridge": { "IPAMConfig": null, "Links": null, "Aliases": null, "MacAddress": "02:42:ac:11:00:03", "NetworkID": "xxx", "EndpointID": "xxx", "Gateway": "172.17.0.1", "IPAddress": "172.17.0.3", "IPPrefixLen": 16, "IPv6Gateway": "", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "DriverOpts": null, "DNSNames": null } } } } ]
u1
和 u2
的网络模式都是 bridge。
u1
容器的 IPAddress
是 172.17.0.2
,u2
容器的 IPAddress
是 172.17.0.3
。
此时关闭 u2
容器实例,新建 u3
容器实例,再查看 u3
的网络信息:
1 | docker stop u2 |
u3
的 IPAddress
与先前关闭的 u2
一样,都是 172.17.0.3
。
这说明,Docker 容器内部的 IP 是有可能会发生改变的。
6.4 docker0
Docker 服务默认会创建一个 docker0
网桥(其上有一个 docker0
内部接口),该桥接网络的名称为 docker0
,它在 内核层 连通了其他的物理或虚拟网卡,将所有容器和本地主机放到 同一个物理网络。
Docker 默认指定了 docker0
接口的 IP 地址和子网掩码,让主机和容器之间可以通过网桥互相通信。
查看 bridge
网络的详细信息,并通过 grep
获取名称:
1 | docker network inspect bridge | grep name |
可以看到其名称为 docker0
。
6.5 bridge 模式
Docker 使用 Linux 桥接,在宿主机虚拟一个 Docker 容器网桥(即 docker0
),Docker 启动一个容器时会根据 Docker
网桥的网段分配给容器一个 IP 地址,称为 Container-IP
,同时 Docker 网桥是每个容器的默认网关。在同一个宿主机内的容器接入同一个网桥后,容器之间能够通过容器的 Container-IP
直接通信。
执行 docker run
且没有指定 --network
时,默认使用的网桥模式就是 bridge,使用的是 docker0
。在宿主机中使用 ifconfig
命令就可以看到 docker0
和自己创建的网络。
网桥 docker0
创建一对对等虚拟设备接口,一个叫 veth
,另一个叫 eth0
,它们成对匹配。
整个宿主机的网桥模式都是 docker0
,类似一个交换机有一堆接口,每个接口叫 veth
,在本地主机和容器内分别创建一个虚拟接口,并让他们彼此联通(这样一对接口叫做 veth pair
)。
每个容器实例内部也有一块网卡,容器内的网卡接口叫做 eth0
。
docker0
上面的每个 veth
都会匹配某个容器实例内部的eth0
。
案例说明
尝试创建两个 tomcat 容器实例:
1 | docker run -d -p 8081:8080 --name tomcat1 tomcat |
先查看宿主机的 IP 信息:
1 | ip addr |
存在以下两个网络信息:
121: veth642ed91@if120: 123: veth3f66e1c@if122:
flowchart LR
subgraph docker0
121:veth
123:veth
end
121:veth --> 120
123:veth --> 122
进入 tomcat1
容器实例内部:
1 | docker exec -it tomcat1 bash |
然后使用 ip addr
查看 IP 信息。如果提示 bash: ip: command not found
,就先安装 iproute2
包:
1 | apt-get update |
再次使用 ip addr
能够看到以下 IP 信息:
120: eth0@if121:
flowchart LR
subgraph Container:tomcat1
120:eth0
end
120:eth0 --> 121
再进入 tomcat2
容器实例内部:
1 | docker exec -it tomcat2 bash |
再使用 ip addr
查看 tomcat2
容器的 IP 信息:
122: eth0@if123:
flowchart LR
subgraph Container:tomcat2
122:eth0
end
122:eth0 --> 123
可以看到,宿主机的网络信息和容器的网络信息两两匹配。
6.6 host 模式
host 模式将直接使用宿主机的 IP 地址与外界进行通信,不再额外进行 NAT 转换。
容器将不会获得一个独立的 Network namespace,而是和宿主机共用一个 Network space。
容器将不会虚拟出自己的网卡,而是直接使用宿主机的 IP 和端口。
案例说明
如果执行 docker run
命令时同时使用了 --network host
以采用 host 网络模式和 -p
指定端口映射,如:
1 | docker run -d -p 8083:8080 --network host --name tomcat3 tomcat |
此时容器实例依旧能够启动成功,但是会出现以下警告:
WARNING: Published ports are discarded when using host network mode
这是因为使用 host 网络模式时,将直接使用的宿主机的 IP 和端口,-p
选项指定的端口映射没有任何作用,依旧以主机端口号为主,重复时递增。
解决方法是不使用 -p
选项,或者使用 Docker 的其他网络模式,例如 --network bridge
,当然也可以不指定,默认就会使用 bridge 模式。
使用以下命令正确创建 tomcat3
容器实例:
1 | docker run -d --network host --name tomcat3 tomcat |
查看 tomcat3
容器的网络信息:
1 | docker inspect tomcat3 | tail -n 21 |
"Networks": { "host": { "IPAMConfig": null, "Links": null, "Aliases": null, "MacAddress": "", "NetworkID": "xxx", "EndpointID": "xxx", "Gateway": "", "IPAddress": "", "IPPrefixLen": 0, "IPv6Gateway": "", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "DriverOpts": null, "DNSNames": null } } } } ]
可以看到,tomcat3
的网络模式是 host,并且 Gateway
和 IPAddress
信息都是空。
查看宿主机 IP 信息:
1 | ip addr |
然后进入 tomcat3
容器内部:
1 | docker exec -it tomcat3 bash |
使用 ip addr
查看容器的 IP 信息,可以发现,容器的 IP 信息与宿主机的 IP 信息一致。
使用 host 模式时,不再使用 -p
设置端口映射,此时容器的 IP 将借用宿主机的,使得外部主机可以直接与容器通信。如果要访问 tomcat3
容器内部的 Tomcat,需要使用 宿主机IP:8080
的形式进行访问。
6.7 none 模式
none 模式将禁用网络功能。
在 none 模式下,不为 Docker 容器进行任何网络配置,Docker 容器内没有网卡、IP、路由等信息,需要自行为 Docker 容器添加网卡、配置 IP 等。
进入容器内,使用 ip addr
查看 IP 信息,只能看到 lo
(本地回环网络 127.0.0.1
)。
使用以下命令创建使用 none 模式的容器实例:
1 | docker run -d -p 8084:8080 --network none --name tomcat4 tomcat |
6.8 container 模式
新建的容器和已经存在的一个容器共享网络 IP 配置,而不是和宿主机共享。
新创建的容器不会创建自己的网卡、配置自己的 IP,而是和一个指定的容器共享IP、端口范围等。两个容器除了网络共享,其他的如文件系统、进程列表依然是隔离的。
案例说明
先创建 tomcat5
容器实例:
1 | docker run -d -p 8085:8080 --name tomcat5 tomcat |
再创建 tomcat6
容器实例,并指定网络为 container 模式,沿用 tomcat5
的网络配置:
1 | docker run -d -p 8086:8080 --network container:tomcat5 --name tomcat6 tomcat |
此时不出意外的话会出现意外,提示以下错误:
docker: Error response from daemon: conflicting options: port publishing and the container type network mode.
这相当于 tomcat5
和 tomcat6
共用了同一个 IP 和同一个端口,导致端口冲突,因此这里不适合使用 Tomcat 进行演示。
正确的案例说明
这里将使用 alpine 镜像进行演示。
Alpine Linux 是一款独立的、非商业的通用 Linux 发行版,专为追求安全性、简单性和资源效率的用户而设计。 可能很多人没听说过这个 Linux 发行版,但经常用 Docker 的朋友可能都用过,因为它以小、简单、安全而著称,所以作为基础镜像是非常好的一个选择,可谓是麻雀虽小但五脏俱全。其镜像十分小巧,不到 6M 的大小,特别适合容器打包。
运行以下命令,进入 alpine1
容器实例终端:
1 | docker run -it --name alpine1 alpine /bin/sh |
再使用以下命令创建 alpine2
容器实例,使其沿用 alpine1
的网络配置:
1 | docker run -it --network container:alpine1 --name alpine2 alpine /bin/sh |
分别使用 ip addr
查看两个容器的 IP 信息,可以发现,它们俩的 IP 信息都一样。
如果此时停止 alpine1
容器的运行,再次查看 alpine2
容器的 IP 信息,会发现其 eth0
网卡不见了。这是因为 alpine2
使用了 alpine1
的网络共享,当后者「下线」时,alpine2
的 eth0
网卡自然也就不见了。
6.9 自定义网络模式
Docker 容器内部的 IP 是有可能会发生变化的。
当容器的 IP 发生变化时,希望能够通过容器名进行通信,而不受到影响。
不使用自定义网络
使用以下命令创建 tomcat1
和 tomcat2
容器实例:
1 | docker run -d -p 8081:8080 --name tomcat1 tomcat |
进入 tomcat1
容器内部:
1 | docker exec -it tomcat1 bash |
使用 ip addr
查看其 IP 信息,其 eth0
下对应的 IP 是 172.17.0.2
。
不退出 tomcat1
容器,再进入 tomcat1
容器内部:
1 | docker exec -it tomcat2 bash |
查看 tomcat2
的 IP 信息,其 eth0
下对应的 IP 是 172.17.0.3
。
回到 tomcat1
容器中,ping tomcat2
容器的 IP:
1 | ping 172.17.0.3 |
能够成功 ping 通。
如果提示 ping: command not found
,使用以下命令安装对应工具包:
1 | apt-get update |
然后又去 tomcat2
中 ping tocmat1
:
1 | ping 172.17.0.2 |
也能够 ping 通。
如果换成服务名呢?比如:
1 | ping tomcat1 |
都将提示 Name or service not known
。
这显然是不行的,Docker 容器的 IP 信息可能会变,只使用对应 IP 通信并不是一个好方案,能够使用服务名进行通信才是最好的。
自定义网络模式
默认 bridge 模式不支持直接使用容器名进行通信,可以新建一个自定义网络,它本身就维护好了主机名和 IP 的对应关系,使用自定义网络可以通过容器名进行通信。
新建的自定义网络默认使用的也是桥接网络。
首先删除原先的 tomcat 容器:
1 | docker rm -f $(docker ps -q) |
新建自定义网络:
1 | docker network create mofan_network |
查看网络列表:
1 | docker network ls |
[root@mofan ~]# docker network ls NETWORK ID NAME DRIVER SCOPE 2bb8b4f40c6a bridge bridge local 56db994e8554 host host local 8fd083e40a4a mofan_network bridge local 5ccc6a0ec3d0 none null local
创建容器实例时,使用自定义网络:
1 | docker run -d -p 8081:8080 --network mofan_network --name tomcat1 tomcat |
之后再尝试利用容器名互相 ping:
1 | ping tomcat1 |
现在都能 ping 通了。
7. Docker Compose
7.1 概述
由于 Docker 本身占用资源极少,Docker 官方建议每个容器中只运行一个服务,将每个服务单独的分割开来。如果需要同时部署多个服务,单独构建镜像、容器就比较麻烦,因此 Docker 官方推出了 Docker Compose 来部署多服务。
比如要实现一个 Web 微服务项目,除了 Web 服务容器本身,还需要加上 MySQL、Redis 等一大批服务,此时单独部署就显得十分费劲。
Docker Compose 是 Docker 官方的开源项目,负责实现对 Docker 容器集群的快速编排,管理多个 Docker 容器组成一个应用。
定义一个 compose.yaml
(新版写法,docker-compose.yml
是过去写法)配置文件,内部包含多个容器之间的调用关系,然后只需要一个命令就能同时启动或关闭这些容器。
7.2 安装
Docker Compose 安装文档:Install using the repository
查看 Docker 版本:
1 | docker version |
Version: 26.1.4
在当前版本中,安装 Docker Engine 时已经一并安装了 Docker Compose。
执行以下命令查看 Docker Compose 版本:
1 | docker compose version |
[root@mofan ~]# docker compose version Docker Compose version v2.27.1
7.3 核心概念
一文件,即 compose.yaml
。
两要素:
- 服务(
service
):一个个应用容器实例 - 工程(
project
):由一组关联的应用容器组成的一个 完整业务单元,在compose.yaml
中定义
7.4 使用步骤
- 编写 Dockerfile 定义各个应用容器,并构建出对应的镜像文件;
- 编写
compose.yaml
,定义一个完整的业务单元,安排好应用中的各个容器服务; - 执行
docker compose up
命令,创建并运行整个应用程序,完成一键部署上线。
7.5 常用命令
执行命令时,需要在 compose.yaml
(docker-compose.yaml
或 docker-compose.yml
)文件所在目录下执行。
注意: 在最新的 Docker 中,Docker Compose 的命令不再是 docker-compose
,不需要其中的连字号(Hyphen),而是使用 docker compose
,甚至使用旧命令还会提示 docker-compose: command not found
。
查看帮助
1 | docker compose -h |
启动所有 Docker Compose 服务(类比
docker run
)
1 | docker compose up |
停止并删除容器、网络、卷、镜像(类比
docker stop
+docker rm
)
1 | docker compose down |
进入容器实例内部
1 | docker compose exec <yml里面的服务id> /bin/bash |
展示当前 Docker Compose 编排过的运行的所有容器
1 | docker compose ps |
展示当前 Docker Compose 编排过的容器进程
1 | docker compose top |
查看容器输出日志
1 | docker compose log <yml里面的服务id> |
检查配置
1 | docker compose config |
重启服务
1 | docker compose restart |
启动服务
1 | docker compose start |
停止服务
1 | docker compose stop |
7.6 编排微服务
Compose file 官网:Compose file reference
不必聚焦微服务项目代码的编写,只看 Docker Compose 的使用。
compose.yaml
文件内容:
1 | # 文件版本号(已过时) |
建议编写完 compose.yaml
文件后进行语法检查:
1 | docker compose config -q |
如果没有输出错误,进行构建、启动:
1 | docker compose up -d |
注意: 在使用 Docker Compose 后,容器间的通信不再使用容器名,而是使用 compose.yaml
文件里指定的服务名。
本节的主要目标是熟悉 compose.yaml
文件的简单书写方式并打开思维,实际生产中多半会更加复杂,一切以官方文档为准。
8. Portainer
官网:Kubernetes and Docker Container Management Software
Portainer 是一款轻量级的应用,提供了图形化界面用于管理 Docker 环境,包括单机环境和集群环境。
Portainer 分为开源社区版(CE版)和商用版(BE版/EE版)。
下载
Portainer 也被制作成了一个 Docker 镜像,可以直接使用 Docker 运行:
1 | docker run -d -p 8000:8000 -p 9000:9000 --name portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:alpine |
此处使用的镜像是 portainer/portainer-ce:alpine
,相比于 portainer/portainer-ce
镜像体积更小,而 portainer/portainer
作为旧镜像名称,已被标记为过期。
命令中的 --restart=always
参数表示如果 Docker 引擎重启了,该容器实例也会在 Docker 引擎重启后重启,类似开机自启。
访问页面
安装成功后访问 宿主机IP:9000
进入 Portainer 首页,创建 admin
。
如果使用的是云服务器,其中的宿主机 IP 就是云服务器的公网 IP,记得把 9000
端口添加到安全组,方便外网访问。
Username
不动,设置 Password
,值至少是 8 位字符,设置完毕后点击 Create user 登入。
首次登录后点击 Local
,用于管理本地 Docker 环境:
选择 local
选项卡后展示本地 Docker 详细信息(相当于于 docker system df
命令):
其中的 Stacks
表示当前环境下有几个使用 Docker Compose 编排的容器应用。
使用案例 —— 安装 Nginx
创建 Nginx 容器实例:
填写容器实例信息并部署:
等待部署完成后,按照与访问 Portainer 类似的方式,访问 宿主机IP:80
访问 Nginx 首页:
9. CIG 容器监控
9.1 背景
可以使用以下命令查看 Docker 容器的运行状态:
1 | docker stats |
注意命令是 stats
而不是 status
。
该命令能够很方便地查看当前宿主机下上所有 CPU 的占用情况、内存使用以及网络流量等数据,但并没有地方存储、健康指标过线预警等功能。
此时可以使用 CIG 容器监控,即 CAdvisor(监控收集) + InfluxDB(存储数据) + Granfana(展示图表)。
9.2 组成部分
CAdvisor
CAdvisor 是一个容器资源监控工具,能够对容器的内存、CPU、网络 IO、磁盘 IO 等信息进行监控,同时提供了一个 Web 页面用于查看容器的实时运行状态。
CAdvisor 默认存储 2 分钟的数据,而且只是针对单物理机。不过 CAdvisor 提供了很多数据集成接口,支持 InfluxDB、Redis、Kafka、Elasticsearch 等集成,可以添加对应配置将监控数据发往到这些数据库存储起来。
CAdvisor 主要功能有两点:
- 展示 Host 和容器两个层次的监控数据
- 展示历史变化数据
InfluxDB
InfluxDB 是用 Go 语言(已用 Rust 语言改写)编写的一个开源分布式时序、事件和指标数据库,无需外部依赖。
由于 CAdvisor 默认只在本机保存 2 分钟的数据,为了持久化存储数据和统一收集展示监控数据,就可以将数据存储到 InfluxDB 中。InfluxDB 是一个时序数据库,专门用于存储时序相关数据,很适合存储 CAdvisor 的数据。
CAdvisor 本身已经提供了集成 InfluxDB 的方法,在启动容器时指定配置即可。
InfluxDB 主要功能:
-
基于时间序列,支持与时间有关的相关函数(如最大、最小、求和等)
-
可度量性,可以实时对大量数据进行计算
-
基于事件,支持任意的事件数据
Granfana
Grafana 是一个开源的数据监控分析可视化平台,支持多种数据源配置(包括 InfluxDB、MySQL、Elasticsearch、OpenTSDB、Graphite 等)和丰富的插件及模板功能,支持图表权限控制和报警。
Granfana 主要功能:
-
灵活丰富的图形化选项
-
可以混合多种风格
-
支持白天和夜间模式
-
多数据源
9.3 安装部署
新建 /mydocker/cig
目录:
1 | mkdir /mydocker/cig |
进入 cig
目录:
1 | cd /mydocker/cig |
在该目录下编写 compose.yaml
,使用 Docker Compose 一套带走。
截止本文首次发布时,CAdvisor 并不支持最新的 InfluxDB V2,见 Issue #2843 · google/cadvisor,因此务必保证使用的镜像版本与下文一致,以免出现各种问题。
1 | services: |
检查 compose.yaml
文件的正确性:
1 | docker compose config -q |
创建并运行容器:
1 | docker compose up -d |
查看三个容器是否启动成功:
1 | docker ps |
9.4 配置 Grafana
在浏览器中以 宿主机IP:3000
的方式访问 Grafana,默认用户名和密码都是 admin
。
如果使用的是云服务器,其中的宿主机 IP 就是云服务器的公网 IP,记得把 3000
端口添加到安全组,方便外网访问。
首次进入登录页时可能比较慢,请耐心等待。
登录成功后,进行数据源配置:
搜索 InfluxDB
,选择 InfluxDB 作为数据源:
点击 Select 后配置 InfluxDB 数据源:
如果出现上图中的提示,证明 InfluxDB 数据源配置成功。
接下来创建 Dashboard: