如上图所示,过程分为三步
1、master 主库在事务提交时,会把数据变更记录在二进制日志文件 binlog 中;
2、从库读取主库的二进制日志文件 binlog,写入到从库的中继日志 relay log;
3、slave 从库执行中继日志中的事件。
docker run -p 3307:3306 \
--privileged=true \
-v /app/mysql-master/log:/var/log/mysql \
-v /app/mysql-master/data:/var/lib/mysql \
-v /app/mysql-master/conf:/etc/mysql/conf.d \
-e MYSQL_ROOT_PASSWORD=root \
--name mysql-master \
-d mysql:5.7
/app/mysql-master/conf
,新建my.cnf
配置文件my.cnf
配置文件内容[mysqld]
## 设置server_id, 同一个局域网中需要唯一
server_id=101
## 指定不需要同步的数据库名称
binlog-ignore-db=mysql
## 开启二进制日志功能
log-bin=mysql-bin
## 设置二进制日志使用内存大小(事务)
binlog_cache_size=1M
## 设置使用的二进制日志格式(mixed, statement, row)
binlog_format=mixed
## 二进制日志过期清理时间。默认值为0,表示不自动清理
expire_logs_days=7
## 跳过主从复制中遇到的所有错误或指定类型的错误,避免slave端复制中断
## 如:1062错误是指一些主键重复,1032错误是因为主从数据库数据不一致
slave_skip_errors=1062
mysql-master
容器实例docker restart mysql-master
mysql-master
容器实例docker exec -it mysql-master /bin/bash
# 登录mysql
mysql -uroot -p
# 创建数据同步用户
mysql> create user 'slave'@'%' identified by '123456';
# 授权
mysql> grant replication slave, replication client on *.* to 'slave'@'%';
mysql> flush privileges;
docker run -p 3308:3306 \
--privileged=true \
-v /app/mysql-slave/log:/var/log/mysql \
-v /app/mysql-slave/data:/var/lib/mysql \
-v /app/mysql-slave/conf:/etc/mysql/conf.d \
-e MYSQL_ROOT_PASSWORD=root \
--name mysql-slave \
-d mysql:5.7
/app/mysql-slave/conf
,新建my.cnf
配置文件my.cnf
配置文件内容[mysqld]
## 设置server_id, 同一个局域网内需要唯一
server_id=102
## 指定不需要同步的数据库名称
binlog-ignore-db=mysql
## 开启二进制日志功能,以备slave作为其它数据库实例的Master时使用
log-bin=mysql-slave1-bin
## 设置二进制日志使用内存大小(事务)
binlog_cache_size=1M
## 设置使用的二进制日志格式(mixed, statement, row)
binlog_format=mixed
## 二进制日志过期清理时间。默认值为0,表示不自动清理
expire_logs_days=7
## 跳过主从复制中遇到的所有错误或指定类型的错误,避免slave端复制中断
## 如:1062错误是指一些主键重复,1032是因为主从数据库数据不一致
slave_skip_errors=1062
## relay_log配置中继日志
relay_log=mysql-relay-bin
## log_slave_updates表示slave将复制事件写进自己的二进制日志
log_slave_updates=1
## slave设置只读(具有super权限的用户除外)
read_only=1
mysql-slave
容器实例docker restart mysql-slave
# 进入主数据库容器实例
docker exec -it mysql-master /bin/bash
# 登录mysql
mysql -uroot -p
# 查看主从同步状态
mysql> show master status;
# 进入从数据库容器实例
docker exec -it mysql-slave /bin/bash
# 登录mysql
mysql -uroot -p
# 在从数据库中配置主从复制
mysql> change master to master_host='192.168.198.131', master_user='slave', master_password='123456', master_port=3307, master_log_file='mysql-bin.000004', master_log_pos=154, master_connect_retry=30;
# 在从数据库中查看主从同步状态(注:加 \G 可以以键值对格式竖排展示)
mysql> show slave status \G;
主从复制命令参数 | 说明 |
---|---|
master_host | 主数据库的IP地址 |
master_user | 在主数据库创建的用于同步数据的用户账号 |
master_password | 在主数据库创建的用于同步数据的用户密码 |
master_port | 主数据库的运行端口 |
master_log_file | 指定从数据库要复制数据的日志文件,通过查看主数据库的状态,获取File参数 |
master_log_pos | 指定从数据库从哪个位置开始复制数据,通过查看主数据库的状态,获取Position参数 |
master_connect_retry | 连接失败重试的时间间隔,单位为秒 |
# 在从数据库中开启主从同步
mysql> start slave;
# 查看主从同步状态
mysql> show slave status \G;
# 主数据库建库建表加数据
create database testdb;
use testdb;
create table tb01 (id int,name varchar(20));
insert into tb01 (id,name) values (1,'jack');
select * from tb01;
# 从数据库查看记录
use testdb;
select * from tb01;
hash(key) % N
:key
是要存入redis的键名,N
是redis集群的机器台数。用户每次读写操作,都是根据redis的键名计算出哈希值,然后对机器台数取余来决定该键存储于哪台服务器上。
简单有效,只需要预估好数据规划好节点,就能保证一段时间的数据支撑。使用Hash算法让固定的一部分请求落到同一台服务器上,这样每台服务器固定处理一部分请求(并维护这些请求的信息),起到负载均衡+分而治之的作用。
原来规划好的节点,如果进行了扩容或者缩容,导致节点有变动,映射关系需要重新进行计算。
在服务器个数固定不变时没有问题,但如果在故障停机或者需要弹性扩容的情况下,原来取模公式中的
N
就会发生变化。由于机器台数数量变化,此时经过取模运算的结果就会发生很大变化,导致根据公式获取存储数据的服务器不可控。
一致性哈希算法
是为了解决哈希取余算法
中的分布式缓存数据变动和映射问题。目的是当服务器个数发生变化时,尽量减少影响到客户端与服务器的映射关系。
算法构建一致性哈希环
一致性哈希算法必然有个hash函数并按照算法产生hash值,这个算法的所有可能哈希值会构成一个全量集,这个集合可以成为一个hash区间
[0, 2^32 - 1]
,这是一个线性空间。但是在这个算法中,我们通过适当的逻辑控制将它首尾相连(0 = 2^32),这样让它逻辑上形成了一个环形空间(哈希环)。
一致性哈希算法也是按照取模的方式。前面的哈希取余算法是对节点个数进行取模,而一致性哈希算法是对
2^32
取模。
服务器IP节点映射
将集群中的各个IP节点映射到环上的某一个位置。
将各个服务器使用Hash算法得到一个哈希值,具体可以选择服务器的IP或主机名作为关键字进行哈希。这样每台机器就能确定其在哈希环上的位置。
假如4个节点 Node A、B、C、D,经过IP地址的哈希函数计算(
hash(ip)
),使用IP地址哈希值后在环空间的位置如下:
key落到服务器的落键规则
当我们需要存储一个键值对时,首先使用相同的hash函数计算
key
的hash
值(hash(key)
),确定此数据在哈希环上的位置,从此位置沿哈希环顺时针”行走“,第一台遇到的服务器就是其应该定位到的服务器,并将该键值对存储于该节点上。如下图所示,假如我们有Object A、B、C、D四个数据对象,经过哈希计算后,在环空间上的位置如下:根据一致性hash算法,数据 A 会被定位到 Node A 上,B 被定位到 Node B 上,C 被定位到 Node C 上,D 被定位到 Node D 上。
注:由于使用相同的hash函数,所以每个键值对一定也会落在环上。
容错性
如下图所示,假设 Node C 宕机,可以看到此时对象 A、B、D 不会受到影响,只有 C 对象被重新定位到Node D。
一般的,在一致性Hash算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间的数据,其他不会受到影响。
即下图所示:假设 Node C 宕机,只会影响到Hash定位到 Node B到Node C 之间的数据,并且这些数据会被转移到 Node D 进行存储。
扩展性
假如需要扩容,增加一台节点 Node X,Node X 的
hash(ip)
位于 Node B 和 Node C 之间,那受到影响的就是 Node B 到 Node X 之间的数据。重新将 Node B 到 Node X 的数据录入到 Node X 节点上即可,不会出现哈希取余算法导致全部数据重新洗牌的后果。
哈希环会存在数据倾斜问题:一致性Hash算法在服务节点太少时,容易因为节点分布不均匀而造成数据倾斜(被缓存的对象都集中在某一台或某几台服务器)
为了解决一致性哈希算法的数据倾斜问题。
哈希槽实质上就是一个数组,数组
[0, 2^14-1]
形成的 hash slot空间。
解决均匀分配的问题,在数据和节点之间又加了一层,把这层称之为哈希槽(slot),用于管理数据和节点之间的关系,就相当于节点上放的是槽,槽里放的是数据。
槽解决的是粒度问题,相当于把粒度变大了,这样便于数据移动。
哈希解决的是映射问题,使用
key
的哈希值来计算所在的槽,便于数据分配。
一个集群只能有 16384 个槽,编号 0-16383(
2^14-1
)。这些槽会分配给集群中所有的主节点,分配策略没有要求。可以指定哪些编号的槽分配给哪个主节点,集群会记录节点和槽的对应关系。解决了节点和槽的关系后,接下来就需要对
key
求哈希值,然后对16384取余,根据余数决定key
落到哪个槽里。
slot = CRC16(key) % 16384
以槽为单位移动数据,因为槽的数目是固定的,处理起来比较容易,这样数据移动问题就解决了。
为什么redis集群的最大槽数是16384个?
CRC16
算法产生的hash值有 16bit,该算法可以产生2^16
= 65536个值。但是为了心跳方便和数据传输最大化,槽的数量只能有2^14
个。
(1)如果槽位数量为65536,那么发送心跳信息的消息头将达到 8k,发送的心跳包过于庞大。
在消息头中最占空间的是
myslots[CLUSTER_SLOTS/8]
。当槽位为65536时,这块的大小是 :65536 ÷ 8 ÷ 1024 = 8kb
。每秒中 redis 节点需要发送一定数量的 ping 消息作为心跳,如果槽位为65536,那么这个 ping 消息头就会太大,浪费带宽。
(2)redis集群的主节点数量基本不可能超过1000个。
集群节点越多,心跳包的消息体内携带的数据越多。如果节点超过1000个,也会导致网络拥堵。因此 redis 作者不建议 redis cluster 节点超过1000个。对于节点数在1000以内的 redis cluster 集群,16384个槽位足够了,没有必要扩展到65536个。
(3)槽位越小,节点少的情况下压缩比越高,容易传输。
Redis 主节点的配置信息中它所负责的哈希槽是通过一张 bitmap 的形式来保存的,在传输过程中会对 bitmap 进行压缩,但是如果 bitmap 的填充率
slots / N
(N为节点数)很高的话,bitmap 的压缩率就很低。如果节点数很少,而哈希槽数很多的话,bitmap 的压缩率就很低。
哈希槽计算
Redis 集群中内置了 16384 个哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。当需要在 Redis 集群中放置一个
key-value
时,redis 先对key
使用CRC16
算法算出一个结果,然后把结果对 16384 取余,这样每个key
都会对应一个编号在0-16383之间的哈希槽,也就是映射到某个节点上。
@Test
public void test() {
// import io.lettuce.core.cluster.SlotHash;
System.out.println(SlotHash.getSlot('A')); // 6373
System.out.println(SlotHash.getSlot('B')); // 10374
System.out.println(SlotHash.getSlot('C')); // 14503
}
# 新建6个redis容器实例
docker run -d --name redis-node-1 --net host --privileged=true -v /app/redis-cluster/share/redis-node-1:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6381
docker run -d --name redis-node-2 --net host --privileged=true -v /app/redis-cluster/share/redis-node-2:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6382
docker run -d --name redis-node-3 --net host --privileged=true -v /app/redis-cluster/share/redis-node-3:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6383
docker run -d --name redis-node-4 --net host --privileged=true -v /app/redis-cluster/share/redis-node-4:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6384
docker run -d --name redis-node-5 --net host --privileged=true -v /app/redis-cluster/share/redis-node-5:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6385
docker run -d --name redis-node-6 --net host --privileged=true -v /app/redis-cluster/share/redis-node-6:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6386
命令参数 | 说明 |
---|---|
--name redis-node-1 | 容器名 |
--net host | 使用宿主机的IP和端口,默认 |
--privileged=true | 获取宿主机root用户权限 |
-v /app/redis-cluster/share/redis-node-1:/data | 容器卷,宿主机地址:docker内部地址 |
--cluster-enabled yes | 开启redis集群 |
--appendonly yes | 开启redis持久化 |
--port 6381 | 配置redis端口号 |
# 进入任意一个节点
docker exec -it redis-node-1 /bin/bash
# 构建主从关系
# --cluster-replicas 1 代表一个 master 后有几个 slave 节点
# 输入 yes 确认后,redis 会向其他节点发送信息加入集群,并分配哈希槽
redis-cli --cluster create 192.168.198.131:6381 192.168.198.131:6382 192.168.198.131:6383 192.168.198.131:6384 192.168.198.131:6385 192.168.198.131:6386 --cluster-replicas 1
# 连接进入redis
redis-cli -p 6381
# 查看集群状态
# 如下图所示,分配的哈希槽数量 cluster_slots_assigned 为 16384,集群节点数量 cluster_known_nodes 为 6
127.0.0.1:6381> cluster info
# 查看集群节点信息
127.0.0.1:6381> cluster nodes
# 连接进入redis
redis-cli -p 6381
# 新增 k1 v1, k2 v2,出现报错
127.0.0.1:6381> set k1 v1
(error) MOVED 12706 192.168.198.131:6383
127.0.0.1:6381> set k2 v2
OK
上述报错原因:k1 经过计算得到的哈希槽为 12706,但是当前连接的 redis-server 为 6381(即节点1),它的哈希槽为:[0,5460](在创建构建主从关系时redis有提示,也可以通过 cluster nodes查看),所以会因为存不进去而报错。
执行 set k2 v2 可以成功,因为 k2 计算出的哈希槽为 449,在[0-5460]区间中。
-c
,优化路由# 防止路由失效加上参数 -c 采用集群策略连接 redis
redis-cli -p 6381 -c
# 新增 k1 v1, k2 v2,会根据哈希槽重定向到对应节点
127.0.0.1:6381> set k1 v1
-> Redirected to slot [12706] located at 192.168.198.131:6383
OK
192.168.198.131:6383> set k2 v2
-> Redirected to slot [449] located at 192.168.198.131:6381
OK
# 输入任意一台节点地址都可以进行集群检查
redis-cli --cluster check 192.168.198.131:6381
# 先停止主机 6381,然后查看集群信息
docker stop redis-node-1
# 进入任意一个节点
docker exec -it redis-node-2 /bin/bash
# 用集群策略连接 redis
redis-cli -p 6382 -c
# 查看集群节点信息
127.0.0.1:6382> cluster nodes
从图中可以看出,6381 的 redis 宕机了,6384 的 redis 上位成为了新的 master
# 启动主机 6381,然后查看集群信息
docker start redis-node-1
# 进入任意一个节点
docker exec -it redis-node-2 /bin/bash
# 用集群策略连接 redis
redis-cli -p 6382 -c
# 查看集群节点信息
127.0.0.1:6382> cluster nodes
如图所示,恢复后的 redis-node-1 变成了 slave。
如果需要将 redis-node-1 恢复回 master,则将 redis-node-4 停止后再重启即可。
注:停止后不要立即重启,redis 之间发送心跳包需要一点时间
假如因为业务量激增,需要向当前3主3从的集群中再加入1主1从两个节点。
# 新建2个redis容器实例
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
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
# 进入 6387 容器实例
docker exec -it redis-node-7 /bin/bash
# 将新增的6387节点(空槽号)作为master节点加入原集群
# redis-cli --cluster add-node 本节点地址 要加入的集群中的其中一个节点地址
redis-cli --cluster add-node 192.168.198.131:6387 192.168.198.131:6381
# 检查集群情况
redis-cli --cluster check 192.168.198.131:6381
如图所示,6371节点已经作为 master 加入了集群,但是该节点没有被分配槽位
redis-cli --cluster reshard 192.168.198.131:6381
如图,redis经过槽位检查后,会提示需要分配的槽位数量。目前是4台master,
16384 / 4 = 4096
,所以给 node7 分配 4096 个槽位,这样每个节点都是4096个槽位。
输入4096之后,会提示让输入要接收这些哈希槽的节点ID,填入node7的节点ID即可(就是节点信息中很长的一串十六进制串);
然后会询问要从哪些节点中拨出一部分槽位凑足4096个槽位分给 node7。一般选择
all
,即把之前的所有主节点的槽位都匀一些给 node7,这样可以使得每个节点的槽位数均衡分配;输入
all
之后,redis 会列出一个计划,内容是自动从前面的 master 中拨出一部分槽位分给 node7 的槽位,输入yes
确认后,redis 便会自动重新洗牌,给 node7 分配槽位。
# 检查集群情况
redis-cli --cluster check 192.168.198.131:6381
可以发现重新洗牌后 node7 的槽位分配为:[0-1364],[5461-6826],[10923-12287];
因为可能有些槽位中已经存储了
key
,完全的重新洗牌重新分配的成本过高,所以 redis 选择从前3个节点中匀出来一部分槽位给 node7。
# redis-cli --cluster add-node 192.168.198.131:6388 192.168.198.131:6381 --cluster-slave --cluster-master-id node7节点的十六进制编号字符串
redis-cli --cluster add-node 192.168.198.131:6388 192.168.198.131:6381 --cluster-slave --cluster-master-id 3a14cf5c2e91e7269ab4eda4128e8d665354dd12
# 检查集群情况
redis-cli --cluster check 192.168.198.131:6381
假如业务高峰期过去,需要将4主4从重新缩容到3主3从。
# 进入6381节点
docker exec -it redis-node-1 /bin/bash
# 检查集群情况,获取6388节点的节点编号
redis-cli --cluster check 192.168.198.131:6381
# 从集群中移除6388节点
# redis-cli --cluster del-node 192.168.198.131:6388 6388节点编号
redis-cli --cluster del-node 192.168.198.131:6388 9024757fc7bc73c353f4787c82c571e9ed49aad0
# 对集群重新分配哈希槽
redis-cli --cluster reshard 192.168.198.131:6381
# 输入需要分配的槽位数量
How many slots do you want to move (from 1 to 16384)? 4096
# 输入接收槽位的节点编号,本例直接让6382节点接收全部空槽位
What is the receiving node ID? ea78a73218cf5da32fccc3a690a43703c4c1a14d
# 输入6387节点编号,告知从哪个节点分配槽位
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: 3a14cf5c2e91e7269ab4eda4128e8d665354dd12
Source node #2: done
# 检查集群情况
redis-cli --cluster check 192.168.198.131:6381
# redis-cli --cluster del-node 192.168.198.131:6387 6387节点编号
redis-cli --cluster del-node 192.168.198.131:6387 3a14cf5c2e91e7269ab4eda4128e8d665354dd12
# 检查集群情况
redis-cli --cluster check 192.168.198.131:6381
Dockerfile 是用来构建 Docker 镜像的文本文件,是由一条条构建镜像所需的指令和参数构成的脚本。
docker build
构建镜像docker run
容器实例每条指令保留字都必须为大写字母,且后面要跟随至少一个参数
指令按照从上到下顺序执行
#
表示注释
每条指令都会创建一个新的镜像层并对镜像进行提交
docker commit
的操作提交一个新的镜像层基础镜像,当前新镜像是基于哪个镜像,指定一个已经存在的镜像作为模板。Dockerfile第一条必须是
FROM
# FROM 镜像名
FROM tomcat:latest
镜像维护者的姓名和邮箱地址
# 非必须
MAINTAINER jack [email protected]
容器构建(docker build)时需要运行的命令。
有两种格式:shell 格式、exec 格式。
# shell 格式
# 等同于在终端操作的 shell 命令
# RUN <命令行命令>
RUN yum -y install vim
# exec 格式
# RUN ["可执行文件" , "参数1", "参数2"]
RUN ["./test.php", "dev", "offline"] # 等价于 RUN ./test.php dev offline
当前容器对外暴露出的端口。
# EXPOSE 要暴露的端口
EXPOSE 8080
指定在创建容器后, 终端默认登录进来的工作目录。
ENV CATALINA_HOME /usr/local/tomcat
WORKDIR $CATALINA_HOME
指定该镜像以什么样的用户去执行,如果不指定,默认是
root
。(一般不修改该配置)
USER zhoulx
在构建镜像过程中设置环境变量。这个环境变量可以在后续的任何
RUN
指令或其他指令中使用
# ENV 环境变量名 环境变量值
# 或者
# ENV 环境变量名=值
ENV CATALINA_HOME /usr/local/tomcat
# 使用环境变量
WORKDIR $CATALINA_HOME
创建一个匿名数据卷挂载点,用于数据保存和持久化工作。
当我们生成镜像的 Dockerfile 中以 Volume 声明了匿名卷,并且我们以这个镜像 run 了一个容器的时候,docker 会在安装目录下的指定目录下面生成一个目录来绑定容器的匿名卷。
# VOLUME 挂载点
# 挂载点可以是一个路径,也可以是数组(数组中的每一项必须用双引号)
VOLUME /var/lib/mysql
将宿主机目录下的文件拷贝进镜像,且会自动处理 URL 和解压 tar 压缩包。
该命令将复制指定的
下的内容到镜像中的
下。
# :可以是 Dockerfile 所在目录的一个相对路径(文件或目录);也可以是一个 URL;还可以是一个 tar 文件(自动解压为目录)
# :可以是镜像内绝对路径,或者相对于工作目录(WORKDIR)的相对路径
# 可以指定多个 资源,但如果它们是文件或目录,则它们的路径被解析为相对于构建上下文的源
# 路径支持正则表达式
ADD
ADD ["",... ""]
类似
ADD
,拷贝文件和目录到镜像中。将从构建上下文目录中
的文件目录复制到新的一层镜像内的
位置。
# :源文件或者源目录
# :容器内的指定路径,该路径不用事先建好,如果不存在会自动创建
COPY
COPY ["src",... "dest"]
ADD 支持添加远程 url 和自动提取压缩格式的文件,COPY 只允许从本机中复制文件
COPY 支持从其他构建阶段中复制源文件(–from)
根据官方 Dockerfile 最佳实践,除非真的需要从远程 url 添加文件或自动提取压缩文件才用 ADD,其他情况一律使用 COPY
指定容器启动后要干的事情。
有三种格式:shell 格式、exec 格式、参数列表格式(与 ENTRYPOINT 指令配合使用)。
# shell 格式
# CMD <命令>
CMD echo "hello world"
# exec 格式
# CMD ["可执行文件", "参数1", "参数2" ...]
CMD ["catalina.sh", "run"]
# 参数列表格式
# CMD ["参数1", "参数2" ....],与 ENTRYPOINT 指令配合使用
Dockerfile中如果出现多个
CMD
指令,只有最后一个生效。
CMD
会被docker run
之后的参数替换。
# 例如,对于 tomcat 镜像,执行以下命令会有不同的效果:
# 因为 tomcat 的 Dockerfile 中指定了 CMD ["catalina.sh", "run"]
# 所以直接 docker run 时,容器启动后会自动执行 catalina.sh run
docker run -it -p 8080:8080 tomcat
# 指定容器启动后执行 /bin/bash
# 此时指定的 /bin/bash 会覆盖掉 Dockerfile 中指定的 CMD ["catalina.sh", "run"]
docker run -it -p 8080:8080 tomcat /bin/bash
CMD
与RUN
命令的区别:
CMD
是在docker run
时运行,而RUN
是在docker build
时运行。
用来指定一个容器启动时要运行的命令。
类似于
CMD
命令,但是ENTRYPOINT
不会被docker run
后面的命令覆盖,这些命令参数会被当做参数送给ENTRYPOINT
指令指定的程序。
ENTRYPOINT
可以和CMD
一起用,一般是可变参数才会使用CMD
,这里的CMD
等于是在给ENTRYPOINT
传参。当指定了
ENTRYPOINT
后,CMD
的含义就发生了变化,不再是直接运行其命令,而是将CMD
的内容作为参数传递给ENTRYPOINT
指令,它们两个组合会变成。
" "
FROM nginx
ENTRYPOINT ["nginx", "-c"] # 定参
CMD ["/etc/nginx/nginx.conf"] # 变参
假设通过该 Dockerfile 构建了
nginx:test
镜像运行命令
docker run nginx:test
:容器启动后,会执行nginx -c /etc/nginx/nginx.conf
运行命令
docker run nginx:test /app/nginx/new.conf
,则容器启动后,会执行nginx -c /app/nginx/new.conf
FROM ubuntu
MAINTAINER zhoulx
ENV MYPATH /usr/local
WORKDIR $MYPATH
# 先更新 ubuntu 的包管理工具
RUN apt-get update
# 安装 vim
RUN apt-get -y install vim
# 安装 ifconfig 命令查看网络IP
RUN apt-get install net-tools
# ADD jdk8 的 jar 是相对路径,安装包需要与 Dockerfile 在同一目录下
ADD jdk-8u202-linux-x64.tar.gz /usr/local/java/
# 配置 java 环境变量
ENV JAVA_HOME /usr/local/java/jdk1.8.0_202
ENV JRE_HOME $JAVA_HOME/jre
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib:$CLASSPATH
ENV PATH $JAVA_HOME/bin:$PATH
EXPOSE 80
CMD echo $MYPATH
CMD echo "build success"
CMD /bin/bash
# 注意:定义的TAG后面有个空格,空格后面有个点
# docker build -t 新镜像名称:TAG .
docker build -t ubuntu:1.0.1 .
docker run -it ubuntu:1.0.1
# 验证 vim 是否安装
vim test.txt
# 验证ifconfig 是否安装
ifconfig
# 验证 java 是否安装
java -version
虚悬镜像:仓库名、标签名都是
的镜像,称为 dangling images(虚悬镜像)。
# 用 Dockerfile 造一个
from ubuntu
CMD echo 'build success'
# 构建时不指定镜像名称及标签
docker build .
# 列出docker中的虚悬镜像
docker image ls -f dangling=true
# 虚悬镜像没有存在价值,可以删除
# 删除所有的虚悬镜像
docker image prune
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.6.3version>
parent>
<groupId>org.examplegroupId>
<artifactId>docker-testartifactId>
<packaging>pompackaging>
<version>1.0-SNAPSHOTversion>
<modules>
<module>docker_bootmodule>
modules>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
properties>
project>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>docker-testartifactId>
<groupId>org.examplegroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>docker_bootartifactId>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
server:
port: 6001
package org.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DockerBootApplication {
public static void main(String[] args) {
SpringApplication.run(DockerBootApplication.class, args);
}
}
package org.example.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
@RestController
@RequestMapping("/docker_boot/test")
public class TestController {
@GetMapping("/sayHello")
public String sayHello() {
return "hello docker - " + UUID.randomUUID().toString();
}
}
# 基础镜像使用java8
FROM java:8
MAINTAINER zhoulx
# 在主机 /var/lib/docker 目录下创建一个临时文件,并链接到容器的 /tmp
VOLUME /tmp
# 将 jar 包添加到容器中,并命名为 docker_boot.jar
ADD docker_boot-1.0-SNAPSHOT.jar docker_boot.jar
# 运行 jar 包
ENTRYPOINT ["java", "-jar", "/docker_boot.jar"]
# 暴露 6001 端口
EXPOSE 6001
docker build -t docker_boot:1.0 .
docker run -d -p 6001:6001 --name docker_boot docker_boot:1.0
从其架构和运行流程来看,Docker 是一个C/S模式的架构,后端是一个松耦合架构,众多模块各司其职。
Docker整体架构图
1、用户是使用 Docker Client 与 Docker Daemon 建立通信,并发送请求给后者;
2、Docker Daemon 作为 Docker 架构中的主体部分,首先提供 Docker Server 的功能使其可以接受 Docker Client 的请求。
3、Docker Engine 执行 Docker 内部的一系列工作,每一项工作都是以一个 Job 的形式的存在;
4、Job 的运行过程中,当需要容器镜像时,则从 Docker Registry 中下载镜像,并通过镜像管理驱动 Graphdriver 将下载镜像以 Graph 的形式存储;
5、当需要为 Docker 创建网络环境时,通过网络管理驱动 Networkdriver 创建并配置 Docker 容器网络环境;
6、当需要限制 Docker 容器运行资源或执行用户指令等操作时,则通过 Execdriver 来完成;
7、Libcontainer 是一项独立的容器管理包,Networkdriver 以及 Execdriver 都是通过 Libcontainer 来实现具体对容器进行的操作。
在 docker 服务启动前,使用
ifconfig
或ip addr
查看网卡信息:
ens33
或eth0
:本机网卡
lo
:本机回环链路
可能有virbr0
(CentOS 安装时如果有选择libvirtd服务,就会多一个以网桥连接的私网地址的virbr0
网卡,作用是为连接其上的虚拟网卡提供 NAT 访问外网的功能。如果不需要该服务,可以使用yum remove libvirt-libs.x86_64
将其卸载)
启动 docker 服务后,会多出一个
docker0
网卡,其作用为:
容器间的互联和通信以及端口映射
容器IP变动时候可以通过服务名直接网络通信而不受到影响
docker network ls
docker network add xxx
docker network rm xxx
docker network inspect xxx
docker network prune
docker network connect 网络名称 容器ID
docker network disconnect 网络名称 容器ID
网络模式 | 简介 | 使用方式 |
---|---|---|
bridge | 默认值,在 Docker 网桥 docker0 上为容器创建新的网络栈 | --net bridge |
host | 容器将不会虚拟出自己的网卡、配置自己的IP等,而是共享宿主机的 Network namespace | --net host |
none | 容器有独立的 Network namespace,但并没有对其进行任何网络设置,如分配 veth pair 和 网桥连接、IP等,用户可进入容器自行配置 |
--net none |
container | 新创建的容器不会创建自己的网卡和配置自己的IP,而是与另一个指定的容器共享 Network namespace | --net container:NAME/容器ID |
自定义 | 用户自己使用 network 相关命令定义网络,创建容器的时候可以指定为自己定义的网络 | --net my_network |
# 通过 inspect 获取容器信息,最后20行即为容器的网络模式信息
docker inspect 容器ID | tail -n 20
# -d string:要管理网络的驱动程序(默认为 bridge)
# --subnet string:子网网段
# --gateway string:网关
docker network create -d bridge --subnet "172.33.0.0/16" --gateway "172.33.0.1" my_network
Docker 服务默认会创建一个 docker0 网桥(其上有一个 docker0 内部接口),该桥接网络的名称为 docker0,它在内核层连通了其他的物理或虚拟网卡,这就将所有容器和本地主机都放到同一个物理网络。Docker 默认指定了 docker0 接口 的 IP 地址和子网掩码,让主机和容器之间可以通过网桥相互通信。
# 查看 bridge 网络的详细信息,并通过 grep 获取名称项
docker network inspect bridge | grep name
Docker 启动一个容器时会根据 Docker 网桥的网段分配给容器一个IP地址,称为 Container-IP,同时 Docker 网桥是每个容器的默认网关。接入同一个网桥的容器之间就能够通过容器的 Container-IP 直接通信。
每个容器实例内部也有一块网卡,每个接口叫 eth0。docker0 上面的每个 veth 匹配某个容器实例内部的 eth0,两两配对(这样一对接口叫 veth pair)。
# 启动两个tomcat容器(billygoo/tomcat8-jdk8已经预装了ip指令)
docker run -d -p 8081:8080 --name tomcat81 billygoo/tomcat8-jdk8
docker run -d -p 8083:8080 --name tomcat83 billygoo/tomcat8-jdk8
# 如下图所示
# 每个veth都有个编号:vethXXXXXXX
# @if后面对应就是容器内的eth0网卡编号
# 容器内的网卡为 eth0
# @if后面就是docker0上对应的veth网卡的编号
直接使用宿主机的 IP 地址与外界进行通信,不再需要额外进行 NAT 转换。
容器将不会获得一个独立的 Network Namespace,而是和宿主机共享 Network Namespace。
容器将不会虚拟出自己的网卡,而是直接使用宿主机的 IP 和端口。
如果在
docker run
命令中同时使用了--network host
和-p
端口映射,例如:
docker run -d -p 8084:8080 --net host --name tomcat84 billygoo/tomcat8-jdk8
# 运行会出现警告
WARNING: Published ports are discarded when using host network mode
# 因为此时已经使用了host模式,本身就是直接使用的宿主机的IP和端口,此时的-p端口映射就没有了意义,也不会生效,端口号还是会以主机端口号为主。
# 正确做法是:不再进行-p端口映射,或者改用bridge模式
在
none
模式下,并不为 Docker 容器进行任何网络配置。也就是说,这个 Docker 容器没有网卡、IP、路由等信息,只有一个lo(本地回环),需要我们自己为 Docker 容器添加网卡、配置IP等。
docker run -d -p 8085:8080 --net none --name tomcat85 billygoo/tomcat8-jdk8
新建的容器和已经存在的一个容器共享网络IP配置,而不是和宿主机共享。
新创建的容器不会创建自己的网卡、IP,而是和一个指定的容器共享IP、端口范围。两个容器除了网络共享,其他的如文件系统、进程列表依然是隔离的。
# 用tomcat演示会由于使用同一IP同一端口,导致端口冲突
# 这里使用alpine做演示(Alpine操作系统是一个面向安全的轻型Linux发行版,大小不到6M,特别适合容器打包)
docker run -it --name alpine1 alpine /bin/sh
# 指定和 alpine1 容器共享网络
docker run -it --net container:alpine1 --name alpine2 alpine /bin/sh
此时使用
ip addr
查看两台容器的网络,会发现两台容器的eth0
网卡内的IP等信息完全相同。如果关掉了
alpine1
容器,因为alpine2
的网络使用的是alpine1
共享网络,所以关掉alpine1
后,alpine2
的eth0
网卡也随之消失了。
容器 IP 变动时候可以通过服务名直接网络通信而不受影响。(类似 Eureka,通过服务名直接互相通信,而不是写死IP地址)。
# 新建自定义网络
docker network create my_network
# 新建容器,加入上述新建的自定义网络
docker run -d -p 8086:8080 --network my_network --name tomcat86 billygoo/tomcat8-jdk8
docker run -d -p 8087:8080 --network my_network --name tomcat87 billygoo/tomcat8-jdk8
# 此时进入 tomcat86 中,使用 ping 命令测试连接 tomcat87 容器名,可以正常连通
结论:自定义网络本身就维护好了主机名与IP的对应关系(IP和域名都能 ping 通)
Docker-Compose
是 Docker 官方的开源项目,负责实现对 Docker 容器集群的快速编排。
Docker-Compose
可以管理多个 Docker 容器组成一个应用。需要定义一个 yaml 格式的配置文件docker-compose.yml
,配置好多个容器之间的调用关系,然后只需要一个命令就能同时启动/关闭这些容器。Docker 建议我们每个容器中只运行一个服务,因为 Docker 容器本身占用资源极少,所以最好是将每个服务单独的分割开来。但是如果我们需要同时部署多个服务,每个服务单独构建镜像构建容器就会比较麻烦。所以 Docker 官方推出了
docker-compose
多服务部署的工具。Compose 允许用户通过一个单独的
docker-compose.yml
模板文件来定义一组相关联的应用容器为一个项目(project
)。可以很容易的用一个配置文件定义一个多容器的应用,然后使用一条指令安装这个应用的所有依赖,完成构建。
# 例如从github下载 2.7.0 版本的docker-compose
# 下载下来的文件放到 /usr/local/bin 目录下,命名为 docker-compose
curl -SL https://github.com/docker/compose/releases/download/v2.7.0/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose
# 添加可执行权限
chmod +x /usr/local/bin/docker-compose
# 验证
docker-compose version
# 删除docker-compose文件即可
rm /usr/local/bin/docker-compose
注:
docker-compose.yml
中的 version 是 compose 文件格式的版本号,需要和 Docker Engine 对应配置文件的格式以及版本号对应关系参考官网:https://docs.docker.com/compose/compose-file/compose-file-v3
服务(service
):一个个应用容器实例
工程(project
):由一组关联的应用容器组成的一个完整业务单元,在docker-compose.yml
中定义
docker-compose.yml
定义一个完整业务单元,安排好整体应用中的各个容器服务docker-compose up
命令来启动并运行整个应用程序,完成一键部署上线# 查看帮助
docker-compose --help
# 启动所有docker-compose服务
docker-compose up
# 启动所有docker-compose服务并后台运行
docker-compose up -d
# 停止并删除容器、网络、卷、镜像
docker-compose down
# 进入容器实例内部docker-compose exec docker-compose.yml文件中写的服务id /bin/bash
docker-compose exec <yml里面的服务id> /bin/bash
# 展示当前docker-compose编排过的运行的所有容器
docker-compose ps
# 展示当前docker-compose编排过的容器进程
docker-compose top
# 查看容器输出日志
docker-compose logs <yml里面的服务id>
# 检查配置
docker-compose config
# 检查配置,有问题才有输出
docker-compose config -q
# 重启服务
docker-compose restart
# 启动服务
docker-compose start
# 停止服务
docker-compose stop
假设需要编排三个容器:微服务docker_boot、mysql、redis
# docker-compose文件版本号
version: "3"
# 配置各个容器服务
services:
microService:
image: docker_boot:1.0
container_name: ms01 # 容器名称,如果不指定,会生成一个服务名加上前缀的容器名
ports:
- "6001:6001"
volumes:
- /app/microService:/data
networks:
- ms_network
depends_on: # 配置该容器服务所依赖的容器服务
- redis
- mysql
redis:
image: redis:6.0.8
ports:
- "6379:6379"
volumes:
- /app/redis/redis.conf:/etc/redis/redis.conf
- /app/redis/data:data
networks:
- ms_network
command: redis-server /etc/redis/redis.conf
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: '123456'
MYSQL_ALLOW_EMPTY_PASSWORD: 'no'
MYSQL_DATABASE: 'db01'
MYSQL_USER: 'zhoulx'
MYSQL_PASSWORD: '123456'
ports:
- "3306:3306"
volumes:
- /app/mysql/db:/var/lib/mysql
- /app/mysql/conf/my.cnf:/etc/my.cnf
- /app/mysql/init:/docker-entrypoint-initdb.d
networks:
- ms_network
command: --default-authentication-plugin=mysql_native_password # 解决外部无法访问
networks:
# 创建 ms_network 网桥网络
ms_network:
# 将IP访问改为通过服务名访问
spring:
datasource:
# url: jdbc:mysql://192.168.198.131:3306/db01?useUnicode=true&Encoding=utf-8&useSSL=false
url: jdbc:mysql://mysql:3306/db01?useUnicode=true&Encoding=utf-8&useSSL=false
......
redis:
# host: 192.168.198.131
host: redis
port: 6379
......
docker build -t docker_boot:1.0 .
# 没有任何输出,说明配置正常
docker-compose config -q
docker-compose up -d
docker-compose stop
官网:https://www.portainer.io
官网安装文档:https://docs.portainer.io/start/install/server/docker/linux
# 旧版镜像地址为 portainer/portainer,从2022年1月标记为过期
# 新版镜像地址为 portainer/portainer-ce
# --restart=always 如果Docker引擎重启了,那么这个容器实例也会在Docker引擎重启后重启,类似开机自启
# 官网映射的端口9443是https,要使用http访问需要改为9000
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:latest
192.168.198.131:9000
首次进来时,需要创建 admin 的用户名(默认
admin
)、密码(必须满足校验规则,例如portainer@123
)。
选择
local
管理本地 docker,即可看到本地Docker的详细信息,包括
- 镜像(Images)
- 容器(Containers)
- 网络(Networks)
- 容器卷(Volumes)
- compose编排(Stacks)
通过
docker stats
命令可以很方便的查看当前宿主机上所有容器的CPU、内存、网络流量等数据,可以满足一些小型应用。但是
docker stats
统计结果只能是当前宿主机的全部容器,数据资料是实时的,没有地方存储、没有健康指标过线预警等功能。
CIG
。CAdvisor 是一个容器资源监控工具,包括容器的内存、CPU、网络IO、磁盘IO等监控,同时提供了一个Web页面用于查看容器的实时运行状态。
CAdvisor 默认存储2分钟的数据,而且只是针对单物理机。不过 CAdvisor 提供了很多数据集成接口,支持 InfluxDB、Redis、Kafka、Elasticsearch 等集成,可以加上对应配置将监控数据发往这些数据库存储起来。
CAdvisor 主要功能:
展示Host和容器两个层次的监控数据
展示历史变化数据
InfluxDB是用Go语言编写的一个开源分布式时序、事件和指标数据库,无需外部依赖。
CAdvisor默认只在本机保存2分钟的数据,为了持久化存储数据和统一收集展示监控数据,需要将数据存储到 InfluxDB 中。InfluxDB 是一个时序数据库,专门用于存储时序相关数据,很适合存储 CAdvisor 的数据。而且 CAdvisor 本身已经提供了 InfluxDB 的集成方法,在启动容器时指定配置即可。
InfluxDB 主要功能:
基于时间序列,支持与时间有关的相关函数(如最大、最小、求和等)
可度量性,可以实时对大量数据进行计算
基于事件,支持任意的事件数据
Grafana 是一个开源的数据监控分析可视化平台,支持多种数据源配置(支持的数据源包括 InfluxDB、MySQL、Elasticsearch、OpenTSDB、Graphite 等)和丰富的插件及模板功能,支持图表权限控制和报警。
Granfana 主要功能:
灵活丰富的图形化选项
可以混合多种风格
支持白天和夜间模式
多个数据源
docker-compose.yml
服务编排文件version: '3.8'
volumes:
grafana_data: {}
services:
influxdb:
# tutum/influxdb 相比influxdb多了web可视化视图。但是该镜像已被标记为已过时
image: tutum/influxdb:0.9
restart: always
environment:
- PRE_CREATE_DB=cadvisor
ports:
- "8083:8083" # 数据库web可视化页面端口
- "8086:8086" # 数据库端口
volumes:
- ./data/influxdb:/data
cadvisor:
image: google/cadvisor:v0.32.0
links:
- influxdb:influxsrv
command:
- -storage_driver=influxdb
- -storage_driver_db=cadvisor
- -storage_driver_host=influxsrv:8086
restart: always
ports:
- "8089:8080" # 笔者的8080端口被占用,暂用端口8089
volumes:
- /:/rootfs:ro
- /var/run:/var/run:rw
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
grafana:
image: grafana/grafana:8.5.2
user: '104'
restart: always
links:
- influxdb:influxsrv
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
environment:
- HTTP_USER=admin
- HTTP_PASS=admin
- INFLUXDB_HOST=influxsrv
- INFLUXDB_PORT=8086
docker-compose config -q
docker-compose up -d
http://192.168.198.131:8083
# 查看当前数据库中的数据库实例
# 可以看到自动创建了我们在配置文件中配置的 cadvisor 数据库实例
SHOW DATABASES
http://192.168.198.131:8089
http://192.168.198.131:3000
,默认用户名密码是:admin
/admin
。在
Configuration
选项卡中,选择Data Sources
,添加一个InfluxDB数据源:name:自定义一个数据源名称,例如
InfluxDB
Query Language:查询语言,默认
InfluxQL
即可URL:根据compose中的容器服务名连接,
http://influxdb:8086
database:我们在InfluxDB中创建的数据库实例,
cadvisor
User:InfluxDB的默认用户,
root
Password:默认是
root
保存并测试,可以连通即可
添加工作台
在Create(加号)选项卡中,选择创建 dashboard 工作台。在创建出来的工作台中,选择 Add panel 中的 Add a new panel 添加一个新的面板。
在右上角 Time series(时序图)位置可以切换展示的图表样式(柱状图、仪表盘、表格、饼图等等),右侧边栏为该图表配置相关信息:标题、描述。
图表下方可以配置该图表展示的数据的查询语句,例如:
FROM:cpu_usage_total(Grafana 会自动获取 InfluxDB 数据库中的元数据,可以直接选择对应表名)
WHERE:添加一个条件,例如:container_name=cig-cadvisor-1
ALIAS:配置一个别名,例如:CPU使用情况监控汇总