Docker
初识 Docker
概述
系统平滑移植,容器虚拟化技术。
使得Docker得以打破过去「程序即应用」的观念。透过镜像将作业系统核心除外,运作应用所需要的系统环境,由下而上打包,达到应用跨平台间的无缝接轨运作。只需要一次配置好环境,换到别的机子上就可以一键部署好,大大简化了操作。是解决了运行环境和配置问题的软件容器。
Linux 容器不是模拟一个完整的操作系统而是对进程进行隔离。
有了容器,就可以将软件运行所需的所有资源打包到一个隔离的容器中。容器与虚拟机不同,不需要捆绑一整套操作系统,只需要软件工作所需的库资源和设置。系统因此而变得高效轻量并保证部署在任何环境中的软件都能始终如一地运行。
基本组成
镜像(image)
Docker 镜像(Image)就是一个
只读
的模板。镜像可以用来创建 Docker 容器,一个镜像可以创建很多容器
。容器(container)
Docker 利用容器(Container)独立运行的一个或一组应用,应用程序或服务运行在容器里面,容器就类似于一个虚拟化的运行环境,
容器是用镜像创建的运行实例
。仓库(repository)
仓库是
集中存放镜像文件的场所
。仓库分为公开仓库(Public)和私有仓库(Private)两种形式。
最大的公开仓库是 Docker Hub(https://hub.docker.com/),存放了数量庞大的镜像供用户下载。
国内的公开仓库包括阿里云 、网易云等
工作原理
Docker是一个Client-Server结构的系统,Docker守护进程运行在主机上, 然后通过Socket连接从客户端访问,守护进程从客户端接受命令并管理运行在主机上的容器。
基本流程
:
- 用户使用 Docker Client 与 Docker Daemon 建立通信,并发送请求给后者;
- Docker Daemon 作为 Docker 架构中的主体部分,首先提供 Docker Server 的功能使其可以接受 Docker Client 的请求;
- Docker Engine 执行 Docker 内部的一系列工作,每一项工作以一个 Job 的形式存在;
- Job 运行过程中,当需要容器镜像时,则从 Docker Registry 中下载镜像,并通过镜像管理驱动 Graph Driver 将下载的镜像以 Graph 的形式存储;
- 当需要为 Docker 创建网络环境时,通过网络管理驱动 Network Driver 创建并配置 Docker 容器网络环境;
- 当需要限制 Docker 容器运行资源或执行用户指令等操作时,则通过 Exec Driver 来完成;
- Libcontainer 是一项独立的容器管理包,Network Driver 以及 Exec Driver 都是通过 Libcontainer 来实现对容器的具体操作;
- 最后各个 Docker Container(容器实例)则运行在 rootfs(类似一个Linux系统)。
HelloWorld
安装步骤
前提
目前,CentOS 仅发行版本中的内核支持 Docker。Docker 运行在CentOS 7 (64-bit)上,要求系统为64位、Linux系统内核版本为 3.8以上。
gcc 相关
1
2yum -y install gcc
yum -y install gcc-c++安装需要软件包
1
yum install -y yum-utils
设置 stable 镜像仓库
1
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
更新 yum 软件包索引
1
yum makecache fast
安装 Docker CE
1
yum -y install docker-ce docker-ce-cli containerd.io
启动 Docker
1
systemctl start docker
测试
查看版本
1
docker version
hello-world
1
docker run hello-world
卸载
1
2
3
4systemctl stop docker
yum remove docker-ce docker-ce-cli containerd.io
rm -rf /var/lib/docker
rm -rf /var/lib/containerd
阿里云镜像加速
使用加速器可以提升获取Docker官方镜像的速度
常用命令
帮助启动类命令
- 启动docker: systemctl start docker
- 停止docker: systemctl stop docker
- 重启docker: systemctl restart docker
- 查看docker状态: systemctl status docker
- 开机启动: systemctl enable docker
- 查看docker概要信息: docker info
- 查看docker总体帮助文档: docker —help
镜像命令
docker images
列出本地主机上的镜像
各个选项说明:
REPOSITORY:表示镜像的仓库源
TAG:镜像的标签版本号
IMAGE ID:镜像ID
CREATED:镜像创建时间
SIZE:镜像大小
OPTIONS说明:
-a :列出本地所有的镜像(含历史映像层)
-q :只显示镜像ID
虚悬镜像
仓库名、标签都是
<none>
的镜像,俗称虚悬镜像 dangling imagedocker search [OPTIONS] 镜像名字
搜索镜像
OPTIONS说明:
—limit: 只列出N个镜像,默认25个
docker pull 镜像名字[:TAG]
下载镜像,没有指定TAG默认最新版。
docker system df 查看镜像/容器/数据卷所占的空间
docker rmi 镜像名字/ID
-f 强制删除
删除单个
docker rmi -f 镜像ID
删除多个
docker rmi -f 镜像名1:TAG 镜像名2:TAG
删除全部
docker rmi -f $(docker images -qa)
…
容器命令
有镜像才能创建容器,这是根本前提!!
1 docker pull ubuntu本次使用 ubuntu 演示
新建+启动容器
1
docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
OPTIONS说明:
—name=”容器名字”: 为容器指定一个名称
-d:后台运行容器并返回容器ID,也即启动守护式容器(后台运行)
-i:以交互模式运行容器,通常与 -t 同时使用
-t:为容器重新分配一个伪输入终端,通常与 -i 同时使用
-P:随机端口映射,大写P
-p:指定端口映射,小写p
hostPort 表示主机访问端口,containerPort 表示容器实例端口。
也就是用户访问本机对应端口,docker 再去访问对应实例端口。
| 参数 | 说明 |
| ——————————————- | ————————————————— |
| -p hostPort:containerPort | 端口映射 -p 8080:80 |
| -p ip:hostPort:containerPort | 配置监听地址 -p 10.0.0.100:8080:80 |
| -p ip::containerPort | 随机分配端口 -p 10.0.0.100::80 |
| -p hostPort:containerPort:udp | 指定协议 -p 8080:80:tcp |
| -p 81:80 -p 443:443 | 指定多个 |启动交互式容器(前台命令行)
docker run -it ubuntu /bin/bash
bin/bash:放在镜像名后的是命令,这里我们希望有个交互式 Shell,因此用的是 /bin/bash。
要退出终端,直接输入 exit。
在大部分场景下,我们希望 docker 的服务是在后台运行的,可以通过 -d 指定容器的后台运行模式。
很重要的一点
:容器运行的命令如果不是那些一直挂起的命令,是会自动停止容器的。例如docker run -d centos
启动运行后台 centos,然后docker ps
进行查看, 会发现容器已经停止。这个是docker的机制问题。后台运行后有的会自动停止容器,需要自己把握。列出当前所有正在运行的容器
1
docker ps [OPTIONS]
OPTIONS说明:
-a:列出当前所有正在运行的容器+历史上运行过的
-l:显示最近创建的容器
-n:显示最近n个创建的容器
-q:静默模式,只显示容器编号
退出容器
exit:run进去容器,exit退出,容器停止
ctrl+p+q:run进去容器,ctrl+p+q退出,容器不停止
启动已停止运行的容器
1
docker start 容器ID或者容器名
重启容器
1
docker restart 容器ID或者容器名
停止容器
1
docker stop 容器ID或者容器名
强制停止容器
1
docker kill 容器ID或容器名
删除容器
1
docker rm [-f] 容器ID/容器名
容器就算已经停止,没被删除的情况下是还存在的,也就是可以重启进行复原,删除之后容器则不能复原。只能再run一个。
-f 表示强制删除,如果容器还在运行着,不加 -f 的话docker不会让你删除。
一次性删除多个容器实例
1
2
3docker rm -f $(docker ps -a -q)
或
docker ps -a -q | xargs docker rm查看容器日志
1
docker logs 容器ID
查看容器内运行的进程
1
docker top 容器ID
查看容器内部细节
1
docker inspect 容器ID
进入正在运行的容器并以命令行交互
1
2
3docker exec -it 容器ID /bin/bash
或
docker attach 容器ID区别:
attach 进入容器启动命令的终端,不会启动新的进程。用 exit 退出,会导致容器的停止。
exec 是在容器中打开新的终端,并且可以启动新的进程。用 exit 退出,不会导致容器的停止。因为关掉的是自己打开的终端。
推荐使用 docker exec 命令,因为退出容器终端,不会导致容器的停止。
将容器内拷贝文件到主机上
1
docker cp 容器ID:容器内路径 目的主机路径
导入和导出容器
export 导出容器的内容留作为一个tar归档文件[对应import命令]
import 从tar包中的内容创建一个新的文件系统再导入为镜像[对应export]1
2docker export 容器ID > 文件名.tar
cat 文件名.tar | docker import - 镜像用户/镜像名:镜像版本号
其它命令
1 | attach # 当前 shell 下 attach 连接指定运行镜像 |
Docker 镜像
概述
是一种轻量级、可执行的独立软件包,它包含运行某个软件所需的所有内容,我们把应用程序和配置依赖打包好形成一个可交付的运行环境(包括代码、运行时需要的库、环境变量和配置文件等),这个打包好的运行环境就是image镜像文件。
UnionFS(联合文件系统):Union文件系统(UnionFS)是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem)。Union 文件系统是 Docker 镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。
特性:一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录
Docker的镜像实际上是由一层一层的 UnionFS 文件系统组成。当容器启动时,一个新的可写层被加载到镜像的顶部。这一层通常被称作“容器层“,“容器层”之下的都叫“镜像层”。所有对容器的改动 - 无论添加、删除、还是修改文件都只会发生在容器层中。只有容器层是可写的,容器层下面的所有镜像层都是只读的。这就表示可以在已有镜像的基础上去制作新的镜像。
构建镜像
演示ubuntu安装vim
从Hub上下载ubuntu镜像到本地并成功运行,原始的默认Ubuntu镜像是不带着vim命令的
run并安装vim
1
2apt-get update
apt-get -y install vim安装完成后,commit 成为新镜像
提交容器副本使之成为一个新的镜像
1
docker commit -m="提交的描述信息" -a="作者" 容器ID 要创建的目标镜像名:[标签名]
此时执行
docker images
会看到两个ubuntu 镜像,其中大的镜像就是刚刚提交上去的镜像总结
Docker中的镜像分层,支持通过扩展现有镜像,创建新的镜像。类似Java继承于一个Base基础类,自己再按需扩展。
新镜像是从 base 镜像一层一层叠加生成的。
这是根据已有镜像构建新镜像,还有一种通过 Dockerfile 构建镜像的方式,之后会讲。
阿里云镜像仓库
将本地镜像保存在云上
创建命名空间
创建镜像仓库
进入仓库可查看操作指南
推送镜像
拉取镜像并测试
私有镜像仓库
Dockerhub、阿里云这样的公共镜像仓库可能不太方便,涉及机密的公司不可能提供镜像给公网,所以需要创建一个本地私人仓库供给团队使用,基于公司内部项目构建镜像。Docker Registry
是官方提供的工具,可以用于构建私有镜像仓库。
下载镜像 Docker Registry
1
docker pull registry
运行私有库
相当于在本地有个私有仓库
1
docker run -d -p 5000:5000 -v /wjuse/myregistry:/var/lib/registry --privileged=true registry
默认情况,仓库被创建在容器的
/var/lib/registry
目录下,建议使用容器卷映射,方便于宿主机联调。:
左边表示主机路径,右边表示容器内路径。也就是将上传到registry的镜像备份一份到本地。--privileged=true
使用该参数,container内的root拥有真正的root权限,否则,container内的root只是外部的一个普通用户权限。查看库中镜像
1
curl -XGET http://localhost:5000/v2/_catalog
上图表示仓库为空。
将镜像修改成符合私服规范的Tag
1
2
3docker tag 镜像:Tag Host:Port/Repository:Tag
此处
docker tag hello-world:latest localhost:5000/hello-world:1.0.0修改配置文件使之支持http
1
vim /etc/docker/daemon.json
1
2
3
4{
"registry-mirrors": ["https://gprv3qkn.mirror.aliyuncs.com"],
+ "insecure-registries": ["localhost:5000"]
}注意逗号隔开,上面为之前配置的阿里云仓库。配置完最好重启一下docker。注意重启完记得run仓库。
push 到私有库并验证
1
2docker push localhost:5000/hello-world:1.0.0
curl -XGET http://localhost:5000/v2/_catalogpull 到本地
1
docker pull localhost:5000/hello-world:1.0.0
容器数据卷
概述
Docker容器产生的数据,如果不备份,那么当容器实例删除后,容器内的数据自然也就没有了。
卷就是目录或文件,由 docker 挂载到容器,但不属于联合文件系统,因此能够绕过 Union File System 提供一些用于持续存储或共享数据的特性。就是将 docker 容器内的数据保存进宿主机的磁盘中。卷的设计目的就是数据的持久化,完全独立于容器的生存周期,因此 Docker 不会在容器删除时删除其挂载的数据卷。
运行一个带有容器卷存储功能的容器实例,当然运行方式可以是前台或后台:
1 docker run -it --privileged=true -v /宿主机绝对路径目录:/容器内路径 镜像名
1 docker run -d -p 5000:5000 -v /wjuse/myregistry/:/tmp/registry --privileged=true registryDocker挂载主机目录访问如果出现 cannot open directory,因为是权限的问题。所以在挂载目录后多加一个 —privileged=true 参数即可。
特点
:
- 数据卷可在容器之间共享或重用数据
- 数据卷的更改可以直接实时生效到容器中,反之同样可以。就算数据卷是在容器停止时修改,容器启动后数据依然会同步。
- 数据卷中的更改不会包含在镜像的更新中
- 数据卷的生命周期一直持续到没有容器使用它为止
数据共享
运行带有容器卷的ubuntu
1
docker run -it --privileged=true -v /tmp/host_data:/tmp/docker_data ubuntu
查看ubuntu是否挂载数据卷
1
docker inspect 容器ID
数据共享
1)docker修改,主机同步更新
2)主机修改,docker同步更新
3)docker 容器 stop,主机修改,docker 容器重启数据同步更新
读写规则
读写(默认)
数据共享案例使用的就是默认规则。
1
docker run -it --privileged=true -v /宿主机目录:/容器内目录:rw 镜像名
只读
容器实例内部被限制,只能读取不能写。
1
docker run -it --privileged=true -v /宿主机目录:/容器内目录:ro 镜像名
此时如果宿主机写入内容,可以同步给容器内,容器可以读取到。
继承与共享
1 | docker run -it --privileged=true -v /mydocker/u:/tmp --name u1 ubuntu |
继承:--volumes-from
表示继承容器 u1 的规则。
共享:主机、容器u1、容器u2 之间数据共享。
常用安装
Tomcat
拉取tomcat
1
docker pull tomcat
运行
1
docker run -it -p 8080:8080 tomcat
访问猫首页
ip:8080
出现404,对应端口已经打开。这是新版 tomcat 的问题。
1
2
3
4
5进入tomcat实例
docker exec -it 容器ID /bin/bash
发现 webapps 文件夹内容为空
将 webapps.dist 覆盖成 webapps 再重新访问即可
mv webapps.dist webapps免修改版
安装tomcat8即可。
1
2docker pull billygoo/tomcat8-jdk8
docker run -d -p 8080:8080 --name mytomcat8 billygoo/tomcat8-jdk8
MySQL
拉取镜像
1
docker pull mysql:5.7
运行镜像
注意运行前查看本地3306端口是否已经被mysql占用。并且一定要使用数据卷进行备份。
1
docker run -d -p 3306:3306 --privileged=true -v /wjuse/mysql/log:/var/log/mysql -v /wjuse/mysql/data:/var/lib/mysql -v /wjuse/mysql/conf:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=root --name mysql mysql:5.7
设置字符集解决插入中文报错等问题
1
2cd /wjuse/mysql/conf/
vim my.cnf1
2
3
4
5[client]
default_character_set=utf8
[mysqld]
collation_server = utf8_general_ci
character_set_server = utf8重新启动mysql容器实例
1
2docker restart mysql
docker exec -it mysql /bin/bash进入mysql
1
mysql -uroot -p
查看字符集是否更改
1
SHOW VARIABLES LIKE 'character%';
之后即可进行正常数据库操作,就算容器被删除,再新建一个mysql容器实例并使用容器卷将主机路径指向之前的路径即可。
Redis
拉取redis
1
docker pull redis:6.0.8
宿主机新建目录
1
mkdir -p /app/redis
在上面目录下新建
redis.conf
去拷贝一份原始配置就行。
修改
redis.conf
1)允许redis外地连接
1
# bind 127.0.0.1
2)daemonize 设置为 no,因为该配置和docker run中 -d 参数冲突,会导致容器一直启动失败
1
daemonize no
运行redis
redis-server /etc/redis/redis.conf
指定配置文件为上面配置的conf。1
docker run -p 6379:6379 --name myr3 --privileged=true -v /app/redis/redis.conf:/etc/redis/redis.conf -v /app/redis/data:/data -d redis:6.0.8 redis-server /etc/redis/redis.conf
1
docker exec -it myr3 /bin/bash
进入redis
1
redis-cli
测试
复杂安装
MySQL主从复制
新建主服务器容器3307
1
2
3
4
5
6docker run -p 3307:3306 --name mysql-master \
-v /mydata/mysql-master/log:/var/log/mysql \
-v /mydata/mysql-master/data:/var/lib/mysql \
-v /mydata/mysql-master/conf:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=root \
-d mysql:5.7进入
/mydata/mysql-master/conf
目录新建my.conf
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[mysqld]
## 设置server_id,同一局域网中需要唯一
server_id=101
## 指定不需要同步的数据库名称
binlog-ignore-db=mysql
## 开启二进制日志功能
log-bin=mall-mysql-bin
## 设置二进制日志使用内存大小(事务)
binlog_cache_size=1M
## 设置使用的二进制日志格式(mixed,statement,row)
binlog_format=mixed
## 二进制日志过期清理时间。默认值为0,表示不自动清理。
expire_logs_days=7
## 跳过主从复制中遇到的所有错误或指定类型的错误,避免slave端复制中断。
## 如:1062错误是指一些主键重复,1032错误是因为主从数据库数据不一致
slave_skip_errors=1062修改完后重启master实例并进入
1
2
3docker restart mysql-master
docker exec -it mysql-master /bin/bash
mysql -uroot -prootMaster创建数据同步用户
1
2CREATE USER 'slave'@'%' IDENTIFIED BY '123456';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'slave'@'%';新建从服务器实例3308
1
2
3
4
5
6docker run -p 3308:3306 --name mysql-slave \
-v /mydata/mysql-slave/log:/var/log/mysql \
-v /mydata/mysql-slave/data:/var/lib/mysql \
-v /mydata/mysql-slave/conf:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=root \
-d mysql:5.7进入
/mydata/mysql-slave/conf
目录新建my.conf
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[mysqld]
## 设置server_id,同一局域网中需要唯一
server_id=102
## 指定不需要同步的数据库名称
binlog-ignore-db=mysql
## 开启二进制日志功能,以备Slave作为其它数据库实例的Master时使用
log-bin=mall-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=mall-mysql-relay-bin
## log_slave_updates表示slave将复制事件写进自己的二进制日志
log_slave_updates=1
## slave设置为只读(具有super权限的用户除外)
read_only=1重启slave实例并进入
1
2
3docker restart mysql-slave
docker exec -it mysql-slave /bin/bash
mysql -uroot -proot在从数据库中配置主从复制
1
change master to master_host='宿主机ip', master_user='slave', master_password='123456', master_port=3307, master_log_file='mall-mysql-bin.000001', master_log_pos=617, master_connect_retry=30;
1
2
3
4
5
6
7master_host:主数据库的IP地址;
master_port:主数据库的运行端口;
master_user:在主数据库创建的用于同步数据的用户账号;
master_password:在主数据库创建的用于同步数据的用户密码;
master_log_file:指定从数据库要复制数据的日志文件,通过查看主数据的状态,获取File参数;
master_log_pos:指定从数据库从哪个位置开始复制数据,通过查看主数据的状态,获取Position参数;
master_connect_retry:连接失败重试的时间间隔,单位为秒。开启主从同步
1
start slave;
Redis集群
前置问题
1~2亿条数据需要缓存,如何设计这个存储案例?单机100%无法完成,肯定使用分布式存储,但是应该如何落地?
哈希取余
2亿条记录就是2亿个k,v
,用户每次读写操作都是根据公式 hash(key) % N个机器台数
,计算出哈希值,用来决定数据映射到哪一个节点上。
优点
:
简单粗暴,直接有效,只需要预估好数据规划好节点,例如3台、8台、10台,就能保证一段时间的数据支撑。使用Hash算法让固定的一部分请求落到同一台服务器上,这样每台服务器固定处理一部分请求(并维护这些请求的信息),起到 负载均衡+分而治之 的作用。
缺点
:
原来规划好的节点,进行扩容或者缩容就比较麻烦了,不管扩缩,每次数据变动导致节点有变动,映射关系需要重新进行计算,在服务器个数固定不变时没有问题,如果需要弹性扩容或故障停机的情况下,原来的取模公式就会发生变化:Hash(key)/3会变成Hash(key) /?
。此时地址经过取余运算的结果将发生很大变化,根据公式获取的服务器也会变得不可控。
一致性哈希
算法背景
:
一致性哈希算法在1997年由麻省理工学院中提出的,设计目标是为了解决分布式服务器个数变动所带来的数据变动和映射问题。例如某个机器宕机了,分母数量改变了,自然取余数就不OK了。
三大步骤
:
构建一致性哈希环
一致性哈希算法必然有个hash函数并按照算法产生hash值,这个算法的所有可能哈希值会构成一个全量集,这个集合可以成为一个hash空间
[0,2^32-1]
,这个是一个线性空间
,但是在算法中,我们通过适当的逻辑控制将它首尾相连(0 = 2^32)
,这样让它逻辑上形成了一个环形空间
。它也是按照使用取模的方法,前面介绍的节点取模法是对节点(服务器)的数量进行取模。而一致性Hash算法是对
2^32
取模。一致性Hash算法将整个哈希值空间组织成一个虚拟的圆环
。整个空间按顺时针方向组织
,圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推直到2^32-1。我们把这个由2的32次方个点组成的圆环称为Hash环。节点映射
将集群中各个IP节点映射到环上的某一个位置
。将各个服务器进行哈希,具体可以选择服务器的IP或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置。假如4个节点NodeA、B、C、D,经过IP地址的哈希函数计算(hash(ip)),使用IP地址哈希后在环空间的位置如下:落键规则
当我们需要存储一个kv键值对时,首先计算key的hash值,hash(key),使用相同的函数Hash计算出哈希值并确定此数据在环上的位置,
从此位置沿环顺时针行走
,第一台遇到的服务器就是其应该定位到的服务器,并将该键值对存储在该节点上。如我们有Object A、Object B、Object C、Object D四个数据对象,经过哈希计算后,在环空间上的位置如下:
优点
:
容错性
假设 Node C 宕机,可以看到此时对象A、B、D不会受到影响,只有C对象被重定位到Node D。一般的,在一致性Hash算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响。简单说,C挂了,受到影响的只是B、C之间的数据,并且这些数据会转移到D进行存储。
扩展性
数据量增加了,需要增加一台节点 NodeX,X 的位置在A和B之间,那收到影响的也就是A到X之间的数据,重新把A到X的数据录入到X上即可,不会导致hash取余全部数据重新洗牌。
缺点
:
数据倾斜
一致性Hash算法在
服务节点太少时
,容易因为节点分布不均匀而造成数据倾斜
(被缓存的对象大部分集中缓存在某一台服务器上)问题。
哈希槽分区
是为了解决一致性哈希算法的
数据倾斜
问题而出现的。
哈希槽实质就是一个数组,数组[0,2^14 -1]形成hash slot空间。为解决均匀分配的问题,在数据和节点之间又加入了一层,把这层称为哈希槽(slot),用于管理数据和节点之间的关系,现在就相当于节点上放的是槽,槽里放的是数据。槽解决的是粒度问题,相当于把粒度变大了,这样便于数据移动。哈希解决的是映射问题,使用key的哈希值来计算所在的槽,便于数据分配。
一个集群只能有16384个槽,编号0-16383(0-2^14-1)。这些槽会分配给集群中的所有主节点,分配策略没有要求。可以指定哪些编号的槽分配给哪个主节点。集群会记录节点和槽的对应关系。解决了节点和槽的关系后,接下来就需要对key求哈希值,然后对16384取余,余数是几key就落入对应的槽里。slot = CRC16(key) % 16384。以槽为单位移动数据,因为槽的数目是固定的,处理起来比较容易,这样数据移动问题就解决了。
Redis 集群中内置了 16384 个哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。当需要在 Redis 集群中放置一个 key-value时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,也就是映射到某个节点上。
三主三从
新建6个redis实例
1
2
3
4
5
6
7
8
9
10
11docker run -d --name redis-node-1 --net host --privileged=true -v /data/redis/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 /data/redis/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 /data/redis/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 /data/redis/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 /data/redis/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 /data/redis/share/redis-node-6:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6386| 命令 | 说明 |
| ——————————- | —————————— |
| —net host | 使用宿主机的ip和端口 |
| —cluster-enabled yes | 开启redis集群 |
| —appendonly yes | 开启持久化 |
| —port 6386 | redis端口号 |进入redis-node-1
1
docker exec -it redis-node-1 /bin/bash
构建主从关系
记得打开服务器对应端口。(6381-6386)、(16381-16386),很重要。
1
redis-cli --cluster create 120.48.54.126:6381 120.48.54.126:6382 120.48.54.126:6383 120.48.54.126:6384 120.48.54.126:6385 120.48.54.126:6386 --cluster-replicas 1
--cluster-replicas 1
表示为每个master创建一个slave节点。一切OK的话,三主三从搞定。如下也可以看到redis自动进行了槽分区。以6381作为切入点,查看集群状态
1
2
3redis-cli -p 6381
cluster info
cluster nodes
数据读写存储
问题
:
进入6381测试
发现有的key能够存进去,有的不行。这是因为redis根据key计算hash值,并落入对应槽分区里头。图中k1、k4的hash值明显不在6381的槽分区内。
解决
:
表示以集群方式连接集群。
1 | redis-cli -p 6381 -c |
查看集群信息
1 | redis-cli --cluster check 120.48.54.126:6381 |
可看到各个节点上存储的key数量,以及对应的主从关系。
主从容错切换
连接6382查看主从情况
可以看到master节点1的slave节点为4
停掉6381主节点
重新查看节点情况,只剩下5个节点,而节点4成为新的主节点。
重启6381节点
查看节点情况,此时节点4依然是master节点,而节点1变为slave节点
主从扩容
新建6387、6388两个节点
1
2
3docker run -d --name redis-node-7 --net host --privileged=true -v /data/redis/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 /data/redis/share/redis-node-8:/data redis:6.0.8 --cluster-enabled yes --appendonly yes --port 6388进入6387容器实例内部
1
docker exec -it redis-node-7 /bin/bash
将6387节点作为master节点加入原集群
当然因为是集群,所以最右边端口随便一个集群机子端口即可。
1
redis-cli --cluster add-node 120.48.54.126:6387 120.48.54.126:6381
查看集群情况
1
redis-cli --cluster check 120.48.54.126:6381
此时新增的6387还没有被分配槽位。
重新分配槽位
1
redis-cli --cluster reshard 120.48.54.126:6381
查看集群情况
1
redis-cli --cluster check 120.48.54.126:6381
此时6387获得从其它三台主机分配过来的槽位。
为主节点6487分配从节点6388
1
redis-cli --cluster add-node 120.48.54.126:6388 120.48.54.126:6387 --cluster-slave --cluster-master-id 649808db07241bcf8e746d4fa5033c6e8c4568dc
查看集群情况
1
redis-cli --cluster check 120.48.54.126:6382
主从缩容
目的:下线6387和6388。
从集群中删除从节点6388
1
redis-cli --cluster del-node 120.48.54.126:6388 591b2ebd26202f7f993a472be99c3c9034d34ba2
重新分配槽位
将6387的4096个槽位清空,本次全都分配给6384
1
redis-cli --cluster reshard 120.48.54.126:6381
删除6387
1
redis-cli --cluster del-node 120.48.54.126:6387 649808db07241bcf8e746d4fa5033c6e8c4568dc
查看集群情况
1
redis-cli --cluster check 120.48.54.126:6381
DockerFile
概述
Dockerfile 是用来构建 Docker 镜像的文本文件,是由一条条构建镜像所需的指令和参数构成的脚本。
DockerFile基础知识
:
大体步骤
:
- docker从基础镜像运行一个容器
- 执行一条指令并对容器作出修改
- 执行类似docker commit的操作提交一个新的镜像层
- docker再基于刚提交的镜像运行一个新容器
- 执行dockerfile中的下一条指令直到所有指令都执行完成
常用保留字
保留字 | 说明 |
---|---|
FROM | 指定一个已经存在的镜像作为模板,第一条必须是FROM |
MAINTAINER | 镜像维护者的姓名和邮箱地址 |
RUN | 容器构建时需要运行的命令,在 docker build 时运行。两种格式: shell格式:RUN <命令行命令> exec格式:RUN [“可执行文件”,”参数1”,”参数2”] |
EXPOSE | 当前容器对外暴露出的端口 |
WORKDIR | 指定在创建容器后,终端默认登陆进来的工作目录,第一个落脚点 |
USER | 指定该镜像以什么样的用户去执行,默认是root |
ENV | 用来在构建镜像过程中设置环境变量,eg: ENV MY_PATH /usr/mytest WORKDIR $MY_PATH |
ADD | 将宿主机目录下的文件拷贝进镜像且会自动处理URL和解压tar压缩包 |
COPY | 类似ADD,拷贝文件和目录到镜像中。 将从构建上下文目录中的文件/目录复制到新的一层的镜像内 COPY src dest 或 COPY [“src”,”dest”] |
VOLUME | 容器数据卷,用于数据保存和持久化工作 |
CMD | 指定容器启动后的要干的事情,也就是在 docker run 时运行,可跟 shell 或 exec 格式。 注意 Dockerfile 中可以有多个 CMD 指令,但只有最后一个生效。CMD 会被 docker run 之后的参数替换。 |
ENTRYPOINT | 也是用来指定一个容器启动时要运行的命令,但是ENTRYPOINT不会被 docker run 后面的命令覆盖。 而且这些命令行参数会被当作参数送给 ENTRYPOINT 指令指定的程序。同样如果存在多个 ENTRYPOINT 指令,只有最后一个会生效。 |
CMD 与 ENTRYPOINT
:
ENTRYPOINT可以和CMD一起用,一般是变参才会使用 CMD ,这里的 CMD 等于是在给 ENTRYPOINT 传参。
按照dockerfile编写执行 | 传参运行 | |
---|---|---|
Docker命令 | docker run nginx:test | docker run nginx:test -c /etc/nginx/new.conf |
实际命令 | nginx -c /etc/nginx/nginx.conf | nginx -c /etc/nginx/new.conf |
案例
自定义镜像mycentosjava8,要求 Centos7 镜像具备vim+ifconfig+jdk8。
下载一个jdk压缩包
编写Dockerfile文件
1
vim Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24FROM centos
MAINTAINER wj<wj@123.com>
ENV MYPATH /usr/local
WORKDIR $MYPATH
#安装vim编辑器
RUN yum -y install vim
#安装ifconfig命令查看网络IP
RUN yum -y install net-tools
#安装java8及lib库
RUN yum -y install glibc.i686
RUN mkdir /usr/local/java
#ADD 是相对路径jar,把jdk-8u171-linux-x64.tar.gz添加到容器中,安装包必须要和Dockerfile文件在同一位置
ADD jdk-8u171-linux-x64.tar.gz /usr/local/java/
#配置java环境变量
ENV JAVA_HOME /usr/local/java/jdk1.8.0_171
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 /bin/bash构建
注意最后面有一个
.
,指定Dockerfile路径1
docker build -t centosjava8:1.5 .
运行并测试
1
docker run -it centosjava8:1.5
1
2
3java -version
ifconfig
vim a.txt
虚悬镜像
仓库名、标签都为
<none>
的镜像,俗称 dangling image。
构建时不写仓库名和标签就会生成虚悬镜像,或者构建时出错也会生成。
1 | docker build . |
查看所有虚悬镜像
1 | docker image ls -f dangling=true |
删除所有虚悬镜像
1 | docker image prune |
项目部署
本地运行并测试通过一个boot项目
打包项目
将打包后的jar包上传到服务器
在jar包目录下新建Dockerfile文件
1
vim Dockerfile
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# 基础镜像使用java
FROM java:8
# 作者
MAINTAINER swj
# VOLUME 指定临时文件目录为/tmp,在主机/var/lib/docker目录下创建了一个临时文件并链接到容器的/tmp
VOLUME /tmp
# 将jar包添加到容器中并更名为swj_docker.jar
ADD boot-docker-0.0.1-SNAPSHOT.jar swj_docker.jar
# touch 作用就是修改文件的访问时间和修改时间为当前时间
RUN bash -c 'touch /swj_docker.jar'
# 运行jar包
ENTRYPOINT ["java","-jar","/swj_docker.jar"]
#暴露6001端口作为微服务
EXPOSE 6001下载java:8
1
docker pull java:8
构建镜像
1
docker build -t swj_docker:1.0 .
运行容器
1
docker run -d -p 6001:6001 swj_docker:1.0
测试
Docker 网络
概述
Docker 启动后,会产生一个名为docker0的虚拟网桥。案例说明详解。
能干嘛?
- 容器间的互联通信以及端口映射
- 容器ip变动时可以通过服务名进行网络通信而不受到ip变动影响
容器有自身内部ip:
启动两个ubuntu容器实例
1
2docker run -it --name u1 ubuntu /bin/bash
docker run -it --name u2 ubuntu /bin/bashdocker inspect 容器ID or 容器名字
发现容器有自己的ip。
常用命令
ALL命令
1
docker network --help
查看网络
1
docker network ls
查看网络信息
1
docker network inspect 网络名字
创建网络
1
docker network create 网络名字
删除网络
1
docker network rm 网络名字
网络模式
网络模式 | 简介 |
---|---|
bridge | 为每个容器分配、设置ip等,并将容器连接到一个docker0虚拟网桥,默认该模式。 |
host | 容器将不会虚拟出自己的网卡,配置自己的ip等,而是使用宿主机的ip和端口。 |
none | 容器有独立的 Network namespace,但并没有对其进行任何网络设置,如分配 veth pair、网桥连接、ip等。 |
container | 新创建的容器不会创建自己的网卡和配置自己的ip,而是和一个指定的容器共享ip、端口范围等。 |
bridge
1
--network bridge
host
1
--network host
none
1
--network none
container
1
--network container
案例说明
bridge
Docker 服务默认会创建一个 docker0 网桥(其上有一个 docker0 内部接口),该桥接网络的名称为docker0,它在内核层连通了其他的物理或虚拟网卡,这就将所有容器和本地主机都放到同一个物理网络。Docker 默认指定了 docker0 接口 的 IP 地址和子网掩码,让主机和容器之间可以通过网桥相互通信
。
查看 bridge 网络的详细信息,并通过 grep 获取名称项
1 | docker network inspect bridge | grep name |
Docker使用Linux桥接,在宿主机虚拟一个Docker容器网桥(docker0),Docker启动一个容器时会根据Docker网桥的网段分配给容器一个IP地址,称为Container-IP,同时Docker网桥是每个容器的默认网关。因为在同一宿主机内的容器都接入同一个网桥,这样容器之间就能够通过容器的Container-IP直接通信。
docker run 的时候,网桥 docker0 创建一对对等虚拟设备接口一个叫veth,另一个叫eth0,成对匹配。
整个宿主机的网桥模式都是 docker0,类似一个交换机有一堆接口,每个接口叫 veth,在本地主机和容器内分别创建一个虚拟接口,并让他们彼此联通(这样一对接口叫veth pair);每个容器实例内部也有一块网卡,每个接口叫eth0。
通过上述,将宿主机上的所有容器都连接到这个内部网络上,各个容器在同一个网络下,会从这个网关下各自拿到分配的ip,此时容器间的网络是互通的。
启动两个tomcat实例进行测试:
1 | docker run -d -p 8081:8080 --name tomcat81 billygoo/tomcat8-jdk8 |
宿主机执行ip addr
发现新增了两个veth:
进入两个容器内部执行ip addr
查看发现各自都有eth并且与veth一一匹配:
host
直接使用宿主机的 IP 地址与外界进行通信,不再需要额外进行NAT 转换。容器将不会获得一个独立的Network Namespace, 而是和宿主机共用一个Network Namespace。容器将不会虚拟出自己的网卡而是使用宿主机的IP和端口
。
注意,以下方法启动会发出警告,因为直接使用宿主机的ip和端口就不存在端口映射这一说。
1 | docker run -d -p 8083:8080 --network host --name tomcat83 billygoo/tomcat8-jdk8 |
直接启动tomcat实例即可,不指定端口就默认就是8080。
1 | docker run -d --network host --name tomcat83 billygoo/tomcat8-jdk8 |
综上,使用host模式,容器实例内部网络与宿主机一致。
none
在none模式下,并不为Docker容器进行任何网络配置。 也就是说,这个Docker容器没有网卡、IP、路由等信息,只有一个lo。需要我们自己为Docker容器添加网卡、配置IP等。禁用网络功能,只有lo标识(就是127.0.0.1本地回环)
。
1 | docker run -d -p 8084:8080 --network none --name tomcat84 billygoo/tomcat8-jdk8 |
容器外部查看:
容器内部查看:
container
新建的容器和已经存在的一个容器共享一个网络ip配置而不是和宿主机共享。新创建的容器不会创建自己的网卡,配置自己的IP,而是和一个指定的容器共享IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的。
此处用 centos 演示,因为虽然共享网络,但是 tomcat 之间8080端口冲突,启动会报错。
1 | docker run -it --name centos1 centos /bin/bash |
注意此时如果停止centos1容器,centos2就会只剩下lo本地回环了。
自定义网络
before
:
1 | docker run -d -p 8081:8080 --name tomcat81 billygoo/tomcat8-jdk8 |
使用自定义网络之前,容器之间可以 ping ip,但不能通过容器名称去 ping。而容器ip是有可能发生改变,很麻烦的。
after
:
自定义桥接网络,自定义网络默认使用的是桥接网络bridge。
新建自定义网络
1 | docker network create swj_network |
新建容器并加入上一步新建的自定义网络
1 | docker run -d -p 8081:8080 --network swj_network --name tomcat81 billygoo/tomcat8-jdk8 |
Docker Compose
容器编排。
概述
Compose 是 Docker 公司推出的一个工具软件,可以管理多个 Docker 容器组成一个应用。需要定义一个 YAML 格式的配置文件docker-compose.yml
,写好容器之间的调用关系。之后只要一个命令,就能同时启动/关闭这些容器。
例如要实现一个Web微服务项目,除了Web服务容器本身,往往还需要再加上后端的数据库mysql服务容器,redis服务器,注册中心eureka,甚至还包括负载均衡容器等等 … …
Compose 允许用户通过一个单独的 docker-compose.yml 模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)
。可以很容易地用一个配置文件定义一个多容器的应用,然后使用一条指令安装这个应用的所有依赖,完成构建。Docker-Compose 解决了容器与容器之间如何管理编排的问题。
核心概念
一文件
docker-compose.yml
两要素
服务(service)
一个个应用容器实例,比如订单微服务、库存微服务、mysql容器、nginx容器或者redis容器
工程(project)
由一组关联的应用容器组成的一个完整业务单元,在 docker-compose.yml 文件中定义。
三个步骤
- 编写Dockerfile定义各个微服务应用并构建出对应的镜像文件
- 使用 docker-compose.yml 定义一个完整业务单元,安排好整体应用中的各个容器服务
- 最后,执行 docker-compose up 命令来启动并运行整个应用程序,完成一键部署上线
安装
1 | curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose |
卸载
:
1 | rm /usr/local/bin/docker-compose |
常用命令
1 | docker-compose -h # 查看帮助 |
编排案例
改造boot-docker使之连接数据库
可以连接本地进行测试,再将连接地址
ip:3306
改成mysql:3306
。因为后面在docker打算不通过ip访问,而是自定义网络从而使用容器名访问。mvn package 命令将项目打成jar包,并上传到服务器
编写Dockerfile
构建成镜像
编写docker-compose.yml文件
相当于编写项目和mysql两条run命令并定义执行顺序,创建自定义网络swj_net。
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
56
57
58
59
60
61
62
63
64
65
66version: "3"
services:
myService:
image: swj_docker:1.1
container_name: ms01
ports:
- "6001:6001"
volumes:
- /app/myService:/data
networks:
- swj_net
depends_on:
- mysql
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: 'root'
MYSQL_ALLOW_EMPTY_PASSWORD: 'no'
MYSQL_DATABASE: 'db2022'
MYSQL_USER: 'swj'
MYSQL_PASSWORD: 'swj123'
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:
- swj_net
command: --default-authentication-plugin=mysql_native_password # 解决外部无法访问
networks:
swj_net:执行以下命令,检查文件是否有语法错误,如果没有任何输出,表示语法正确
需在compose文件目录下执行
1
docker-compose config -q
执行docker-compose.yml文件
需在compose文件目录下执行
1
2
3docker-compose up
或
docker-compose up -d进入mysql容器实例并新建库表
测试
关停
1
docker-compose down
Portainer
Portainer 是一款轻量级的应用,它提供了图形化界面,用于方便地管理Docker环境,包括单机环境和集群环境。
docker 命令安装
--restart=always
表示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
访问
访问 ip:9000,第一次登录需要创建用户。
登录
选择local选项卡后查看本地docker详细信息
仪表盘其实也跟 docker system df 命令差不多
其它操作
等等等 … …,基本上可以舍弃ssh命令行工具了。
容器监控
通过 docker stats 原生命令可以很方便的看到当前宿主机上所有容器的CPU,内存以及网络流量等数据,一般小公司够用了。
但是,docker stats 统计结果只能是当前宿主机的全部容器,数据资料是实时的,没有地方存储、没有健康指标过线预警等功能。
三剑客
CAdvisor监控收集 + InfluxDB存储数据 + Granfana展示图表
CAdvisor
InfluxDB
Granfana
容器编排
新建目录
新建 docker-compose.yml
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99version: '3.1'
volumes:
grafana_data: {}
services:
influxdb:
image: tutum/influxdb:0.9
restart: always
environment:
- PRE_CREATE_DB=cadvisor
ports:
- "8083:8083"
- "8086:8086"
volumes:
- ./data/influxdb:/data
cadvisor:
image: google/cadvisor
links:
- influxdb:influxsrv
command: -storage_driver=influxdb -storage_driver_db=cadvisor -storage_driver_host=influxsrv:8086
restart: always
ports:
- "8080:8080"
volumes:
- /:/rootfs:ro
- /var/run:/var/run:rw
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
grafana:
user: "104"
image: grafana/grafana
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
- INFLUXDB_NAME=cadvisor
- INFLUXDB_USER=root
- INFLUXDB_PASS=root检查语法错误
1
docker-compose config -q
启动
1
docker-compose up -d
测试
注意除了以下端口还需打开8086端口,influxdb要用。
浏览cAdvisor收集服务,http://ip:8080/
浏览influxdb存储服务,http://ip:8083/
浏览grafana展现服务,http://ip:3000
默认帐户密码(admin/admin)
配置
配置数据源
配置面板