Docker笔记
Docker简介:
- Docker为什么出现:
一款产品开发和上线不能保证环境完全一致。给项目的完整上线会带来巨大麻烦。而且更换环境配置十分麻烦,例如每个机器都要部署集群Redis,Hadoop,mysql,jdk等环境配置十分麻烦。于是出现了Docker;在发布一个项目时,带上运行环境一起打包!开发部署上线一套完成。 - Docker的思想:
Docker的核心思想就是隔离机制,将项目以及环境打包装箱,每个箱子都相互隔离;Docker通过隔离机制将服务器利用到极致。 - Docker的相关信息:
Docker是基于Go语言开发的开源项目。
官网:https://www.docker.com/
docker官方文档:https://docs.docker.com/
docker仓库:https://hub.docker.com/
菜鸟教程地址:https://www.runoob.com/docker/docker-tutorial.html - Docker的优点:
(1)以前使用的虚拟机技术资源占用十分多,操作步骤冗余,启动慢。
(2)传统的虚拟机,虚拟出一条硬件,运行一个完整的操作系统,然后在这个系统上安装和运行软件。
(3)Docker容器内的应用直接运行在宿组机的内容,容器没有自己的内核,也没有虚拟我们的硬件,所以就轻便了。
(4)每个容器间是互相隔离,每个容器内都有一个自己的文件系统,互不影响。
(5)传统部署是一堆帮助文档,安装程序,Docker是直接打包镜像发布测试,一键运行。
(6)在使用Docker后,开发,测试环境都是高度一致的。
(7)Docker可以在一个物理机上运行多个容器实例,将服务器的性能压榨到极致。
5.Docker的底层架构:
镜像(image):docker镜像就好比一个模板,可以通过这个模板来创建容器服务;通过这个镜像可以创建多个容器(最终服务运行或者项目运行在容器中)
容器(containers):Docker利用容器技术,独立运行一个或者一组应用,是通过镜像来创建的;可以将容器理解为一个简单的linux系统。
仓库(repository):仓库就是存放镜像的地方,Docker Hub是国外仓库,可以使用阿里云的。
安装Docker
参考官方文档:https://docs.docker.com/engine/install/centos/
docker只支持centos7及以上版本;
查看自己linux版本:
cat /etc/os-release
第一步:卸载旧版本的docker
yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
第二步:安装yum-utils包(提供yum-config-manager 实用程序)并设置稳定存储库。
yum install -y yum-utils
使用阿里云的仓库地址
yum-config-manager \
--add-repo \
http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
第三步:更新自己的yum软件包索引
yum makecache fast
第四步:安装 Docker 引擎
yum install docker-ce docker-ce-cli containerd.io
第五步:检查docker是否安装成功
docker version
安装成功!
第六步启动docker:
systemctl start docker
第七步:经典helloword,通过运行hello-world 映像验证 Docker Engine 是否已正确安装。
docker run hello-world
查看docker下载的hello-word镜像:
docker images
卸载docker的命令:
需要卸载 Docker Engine、CLI 和 Containerd 包:
yum remove docker-ce docker-ce-cli containerd.io
主机上的映像、容器、卷或自定义配置文件不会自动删除。删除所有镜像、容器和卷:
rm -rf /var/lib/docker
rm -rf /var/lib/containerd
配置阿里云镜像加速:
在主页搜索就能找到
然后执行命令:
docker run 的流程和原理
docker常用命令
官网命令帮助文档:https://docs.docker.com/reference/
菜鸟教程帮助文档:https://www.runoob.com/docker/docker-command-manual.html
命令结构图:
1.帮助命令:
docker version # 显示docker的版本信息
docker info # 显示docker的系统信息,包括镜像和容器的数量
docker 命令 --help # 帮助命令
2.镜像命令:
docker images 查看本地镜像
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-world latest d1165f221234 5 months ago 13.3kB
#解释
REPOSITORY :镜像的仓库源
TAG :镜像的标签
IMAGE ID:镜像的ID
CREATED :镜像的创建时间
SIZE: 镜像的大小
#可选项
docker images --help
Usage: docker images [OPTIONS] [REPOSITORY[:TAG]]
List images
Options:
-a, --all Show all images (default hides intermediate images) 显示所有镜像
--digests Show digests
-f, --filter filter Filter output based on conditions provided
--format string Pretty-print images using a Go template
--no-trunc Don't truncate output
-q, --quiet Only show image IDs 根据镜像ID,指定显示镜像
docker search 搜索镜像
docker search mysql #查询mysql的镜像
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
mysql MySQL is a widely used, open-source relation… 11213
#可选项
docker search --help
Usage: docker search [OPTIONS] TERM
Search the Docker Hub for images
Options:
-f, --filter filter Filter output based on conditions provided #可以通过条件过滤镜像例如STARS
--format string Pretty-print search using a Go template
--limit int Max number of search results (default 25)
--no-trunc Don't truncate output
#过滤镜像,查询STARS大于五千的mysql
docker search mysql --filter=STARS=5000
docker pull 下载镜像
下载的仓库地址:https://hub.docker.com/
手动搜索:
#下载镜像 docker pull 镜像名[:tag] 不写tag默认版本为latest
docker pull mysql
docker pull mysql:8.0
查看镜像
docker images
docker rmi 移除镜像
docker rmi [OPTIONS] IMAGE [IMAGE...]
#[OPTIONS]可选参数
docker rmi -f IMAGE [IMAGE...]暴力删除指定镜像
#[IMAGE...]一个或者多个镜像ID(通过 docker images查看镜像ID)
docker rmi -f 镜像ID/镜像名称:TAG名称 #指定删除镜像
docker rmi -f 镜像ID 镜像ID 镜像ID ....#删除多个镜像
docker rmi -f $(docker images -aq) #删除全部镜像
参数:
3.容器命令
有了镜像才可以创建容器,linux下下载一个centos镜像来测试
docker pull centos
创建容器并启动
docker run --help #查看帮助以及相关参数
官方文档:https://docs.docker.com/engine/reference/commandline/run/
#启动centos 并进入到容器中
docker run -it centos /bin/bash
#退出
exit
docker ps:列出所有正在运行的容器
#查看所有正在运行的容器
docker ps #正在运行的容器
docker ps -a#正在运行的容器以及历史运行的容器
退出容器
exit #退出并且停止容器
Ctrl+P+Q #退出容器但不停止
删除容器:
docker rm 容器名字
docker rm -f (docker ps -aq) #删除所有的容器
docker ps --filter status=exited -q | xargs docker rm #删除所有已经停止的容器
启动和停止容器操作:
# 使用docker ps -a查看容器id
docker start 容器id #启动容器
docker restart 容器id #重启容器
docker stop 容器id #停止容器
docker kill 容器id# 强行停止容器
其他常用命令:
docker logs : 获取容器的日志
语法
docker logs [OPTIONS] CONTAINER
OPTIONS说明:
-f : 跟踪日志输出
--since :显示某个开始时间的所有日志
-t : 显示时间戳
--tail :仅列出最新N条容器日志
docker top :查看容器中运行的进程信息,支持 ps 命令参数。
语法
docker top [OPTIONS] CONTAINER [ps OPTIONS]
容器运行时不一定有/bin/bash终端来交互执行top命令,而且容器还不一定有top命令,可以使用docker top来实现查看container中正在运行的进程。
docker top mymysql
docker inspect : 获取容器/镜像的元数据。
语法
docker inspect [OPTIONS] NAME|ID [NAME|ID...]
OPTIONS说明:
-f :指定返回值的模板文件。
-s :显示总的文件大小。
--type :为指定类型返回JSON。
docker pause :暂停容器中所有的进程。
docker unpause :恢复容器中所有的进程。
语法
docker pause CONTAINER [CONTAINER...]
docker unpause CONTAINER [CONTAINER...]
docker create :创建一个新的容器但不启动它
用法同 docker run
语法
docker create [OPTIONS] IMAGE [COMMAND] [ARG...]
语法同 docker run
实例
使用docker镜像nginx:latest创建一个容器,并将容器命名为myrunoob
docker create --name myrunoob nginx:latest
docker exec :在运行的容器中执行命令
语法
docker exec [OPTIONS] CONTAINER COMMAND [ARG...]
OPTIONS说明:
-d :分离模式: 在后台运行
-i :即使没有附加也保持STDIN 打开
-t :分配一个伪终端
实例
在容器 mynginx 中以交互模式执行容器内 /root/runoob.sh 脚本:
runoob@runoob:~$ docker exec -it mynginx /bin/sh /root/runoob.sh
http://www.runoob.com/
在容器 mynginx 中开启一个交互模式的终端:
runoob@runoob:~$ docker exec -i -t mynginx /bin/bash
root@b1a0703e41e7:/#
也可以通过 docker ps -a 命令查看已经在运行的容器,然后使用容器 ID 进入容器。
查看已经在运行的容器 ID:
# docker ps -a
...
9df70f9a0714 openjdk "/usercode/script.sh…"
...
第一列的 9df70f9a0714 就是容器 ID。
通过 exec 命令对指定的容器执行 bash:
# docker exec -it 9df70f9a0714 /bin/bash
docker attach :连接到正在运行中的容器。
语法
docker attach [OPTIONS] CONTAINER
要attach上去的容器必须正在运行,可以同时连接上同一个container来共享屏幕(与screen命令的attach类似)。
docker events : 从服务器获取实时事件
语法
docker events [OPTIONS]
OPTIONS说明:
-f :根据条件过滤事件;
--since :从指定的时间戳后显示所有事件;
--until :流水时间显示到指定的时间为止;
实例
显示docker 2016年7月1日后的所有事件。
runoob@runoob:~/mysql$ docker events --since="1467302400"
显示docker 镜像为mysql:5.6 2016年7月1日后的相关事件。
runoob@runoob:~/mysql$ docker events -f "image"="mysql:5.6" --since="1467302400"
docker export :将文件系统作为一个tar归档文件导出到STDOUT。
语法
docker export [OPTIONS] CONTAINER
OPTIONS说明:
-o :将输入内容写到文件。
实例
将id为a404c6c174a2的容器按日期保存为tar文件。
runoob@runoob:~$ docker export -o mysql-`date +%Y%m%d`.tar a404c6c174a2
docker port :列出指定的容器的端口映射,或者查找将PRIVATE_PORT NAT到面向公众的端口。
语法
docker port [OPTIONS] CONTAINER [PRIVATE_PORT[/PROTO]]
实例
查看容器mynginx的端口映射情况。
runoob@runoob:~$ docker port mymysql
docker commit :从容器创建一个新的镜像。
语法
docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]
OPTIONS说明:
-a :提交的镜像作者;
-c :使用Dockerfile指令来创建镜像;
-m :提交时的说明文字;
-p :在commit时,将容器暂停。
实例
将容器a404c6c174a2 保存为新的镜像,并添加提交人信息和说明信息。
runoob@runoob:~$ docker commit -a "runoob.com" -m "my apache" a404c6c174a2 mymysql:v1
docker cp :用于容器与主机之间的数据拷贝。
语法
docker cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|-
docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH
OPTIONS说明:
-L :保持源目标中的链接
实例
将主机/www/runoob目录拷贝到容器96f7f14e99ab的/www目录下。
docker cp /www/runoob 96f7f14e99ab:/www/
将主机/www/runoob目录拷贝到容器96f7f14e99ab中,目录重命名为www。
docker cp /www/runoob 96f7f14e99ab:/www
将容器96f7f14e99ab的/www目录拷贝到主机的/tmp目录中。
docker cp 96f7f14e99ab:/www /tmp/
docker安装nginx
第一步:先搜索nginx
docker search nginx
第二步:下载nignx
docker pull nginx
第三步:查看nginx镜像
docker images
第四步:后台启动nginx
-d #后台运行
--name #给容器命名
-p #宿主主机端口:容器nginx内部端口(外网通过访问3344端口,来访问nginx)
docker run -d --name nginx01 -p 3344:80 nginx
云服务器需要设置安全组
本地测试:
curl localhost:3344
开启防火墙3344端口:
//查看防火墙状态
firewall-cmd --state
//如果防火墙关闭,开启防火墙
systemctl start firewalld
//开放8080端口
firewall-cmd --permanent --add-port=3344/tcp
//查看端口,执行
firewall-cmd --permanent --query-port=3344/tcp
firewall-cmd --permanent --list-ports
//重启防火墙
firewall-cmd --reload
外网访问:http://ip地址:3344/
启动并进入nginx:
docker exec -it nginx01 /bin/bash
退出并关闭:
exit
查看容器信息:
docker ps
nginx容器映射图:
docker 安装tomcat
第一步:先搜索tomcat
docker search tomcat
第二步:下载tomcat
docker pull tomcat
第三步:查看tomcat镜像
docker images
第四步:后台启动tomcat
-d #后台运行
--name #给容器命名
-p #宿主主机端口:容器nginx内部端口(外网通过访问3355端口,来访问tomcat)
docker run -d --name tomcat01 -p 3355:8080 tomcat
#启动tomcat并进入tomcat目录
docker exec -it tomcat01 /bin/bash
#将tomcat下的webapps.dist下的文件拷贝到webapp中
cp -r webapps.dist/* webapps
云服务器需要设置安全组
//查看防火墙状态
firewall-cmd --state
//如果防火墙关闭,开启防火墙
systemctl start firewalld
//开放8080端口
firewall-cmd --permanent --add-port=3355/tcp
//查看端口,执行
firewall-cmd --permanent --query-port=3355/tcp
firewall-cmd --permanent --list-ports
//重启防火墙
firewall-cmd --reload
外网访问:http://ip地址:3355/
安装docker的可视化工具
docker图形页面管理工具基本上是3个工具,docker UI,shipyard,portainer。对比后,shipyard最强大,其次是portainer,最后是doucker UI
我们先使用portainer;
什么是portainer:Docker的图形化界面管理工具,提供一个后台面板给我们使用;
第一步:安装portainer
docker run -d -p 8088:9000 \
--restart=always -v /var/run/docker.sock --privileged=true portainer/portainer
云服务器需要设置安全组
//查看防火墙状态
firewall-cmd --state
//如果防火墙关闭,开启防火墙
systemctl start firewalld
//开放8080端口
firewall-cmd --permanent --add-port=8088/tcp
//查看端口,执行
firewall-cmd --permanent --query-port=8088/tcp
firewall-cmd --permanent --list-ports
//重启防火墙
firewall-cmd --reload
外网访问:http://ip地址:8088/
docker镜像加载原理
1.UnionFS(联合文件系统)
UnionFS(联合文件系统):Union文件系统(UnionFS)是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem)。Union 文件系统是 Docker 镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。
特性:一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录
2.docker镜像加载原理
docker的镜像实际上由一层一层的文件系统组成,这种层级的文件系统UnionFS。
bootfs(boot file system)主要包含bootloader和kernel, bootloader主要是引导加载kernel, Linux刚启动时会加载bootfs文件系统,在Docker镜像的最底层是bootfs。这一层与我们典型的Linux/Unix系统是一样的,包含boot加载器和内核。当boot加载完成之后整个内核就都在内存中了,此时内存的使用权已由bootfs转交给内核,此时系统也会卸载bootfs。
rootfs (root file system) ,在bootfs之上。包含的就是典型 Linux 系统中的 /dev, /proc, /bin, /etc 等标准目录和文件。rootfs就是各种不同的操作系统发行版,比如Ubuntu,Centos等等。
平时我们安装进虚拟机的CentOS都是好几个G,为什么docker这里才200M??
对于一个精简的OS,rootfs可以很小,只需要包括最基本的命令、工具和程序库就可以了,因为底层直接用Host的kernel,自己只需要提供 rootfs 就行了。由此可见对于不同的linux发行版, bootfs基本是一致的, rootfs会有差别, 因此不同的发行版可以公用bootfs。
3.分层的镜像
以我们的pull为例,在下载的过程中我们可以看到docker的镜像好像是在一层一层的在下载
4.为什么 Docker 镜像要采用这种分层结构呢
最大的一个好处就是 - 共享资源
比如:有多个镜像都从相同的 base 镜像构建而来,那么宿主机只需在磁盘上保存一份base镜像,
同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了。而且镜像的每一层都可以被共享。
二、特点
Docker镜像都是只读的
当容器启动时,一个新的可写层被加载到镜像的顶部。
这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”
commit镜像:
docker commit :从容器创建一个新的镜像。
语法
docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]
OPTIONS说明:
-a :提交的镜像作者;
-c :使用Dockerfile指令来创建镜像;
-m :提交时的说明文字;
-p :在commit时,将容器暂停。
#查看容器id
docker ps
# docker commit -m="提交的信息" -a="作者" 容器id 目标镜像名称:[TAG]
测试:
#启动一个默认的tomcat容器
docker run -d -p 3355:8080 --name tomcat01 tomcat
#tomcat由于官方镜像原因,webapp为空,我们将基本文件拷贝进去
docker exec -it tomcat01 /bin/bash
cp -r webapps.dist/* webapps
#查看容器id
docker ps
#将拷贝后的tomcat,commit为新的镜像
docker commit -a="sun" -m="add webapps app" 36b7e2294097 tomcat01:1.0
查看镜像:
docker images
commit容器的目的:当官方镜像的配置不符合我们需要,我们可以通过自定义后commit为新的镜像,方便我们以后的使用。
容器数据卷
什么是容器数据卷:
docker是将应用和环境打包成一个镜像。然后我们通过镜像创建容器,但是为了方便容器数据的管理,就出现容器数据卷。将容器内的文件映射到linux文件系统中,方便数据的管理,不用启动容器后再进入容器修改文件,直接在本地文件系统修改相关文件,容器数据卷会同步数据到容器内,也可以理解为数据的双向绑定。
测试:
docker run :创建一个新的容器并运行一个命令
语法
docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
OPTIONS说明:
-a stdin: 指定标准输入输出内容类型,可选 STDIN/STDOUT/STDERR 三项;
-d: 后台运行容器,并返回容器ID;
-i: 以交互模式运行容器,通常与 -t 同时使用;
-P: 随机端口映射,容器内部端口随机映射到主机的端口
-p: 指定端口映射,格式为:主机(宿主)端口:容器端口
-t: 为容器重新分配一个伪输入终端,通常与 -i 同时使用;
--name="nginx-lb": 为容器指定一个名称;
--dns 8.8.8.8: 指定容器使用的DNS服务器,默认和宿主一致;
--dns-search example.com: 指定容器DNS搜索域名,默认和宿主一致;
-h "mars": 指定容器的hostname;
-e username="ritchie": 设置环境变量;
--env-file=[]: 从指定文件读入环境变量;
--cpuset="0-2" or --cpuset="0,1,2": 绑定容器到指定CPU运行;
-m :设置容器使用内存最大值;
--net="bridge": 指定容器的网络连接类型,支持 bridge/host/none/container: 四种类型;
--link=[]: 添加链接到另一个容器;
--expose=[]: 开放一个端口或一组端口;
--volume , -v: 绑定一个卷
#创建并启动进入一个centos容器,并且指定容器数据卷
docker run -it -v /home/centostest:/home centos /bin/bash
查看文件挂载信息:
docker inspect 容器id
创建test.java查看本地映射文件夹和docker容器文件是否一致:
在本地修改文件:vim test.java
测试文件同步,数据双向绑定。
安装mysql并使用容器数据卷
第一步:先搜索mysql镜像
docker search mysql
第二步:下载mysql
docker pull mysql:5.7
第三步:查看mysql镜像
docker images
第四步:后台启动mysql
参数解释
-d: 后台运行容器,并返回容器ID;
-i: 以交互模式运行容器,通常与 -t 同时使用;
-P: 随机端口映射,容器内部端口随机映射到主机的端口
-p: 指定端口映射,格式为:主机(宿主)端口:容器端口
-t: 为容器重新分配一个伪输入终端,通常与 -i 同时使用;
--name="nginx-lb": 为容器指定一个名称;
-e username="ritchie": 设置环境变量;
--volume , -v: 绑定一个卷
官网提供的安装mysql容器命令:
docker run -itd --name mysql-test -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql
应用数据卷的安装mysql容器命令
docker run -d -e MYSQL_ROOT_PASSWORD=123456 --name mysql01 -v /home/mysql/conf:/etc/mysql/conf.d -v /data/mysql/data:/var/lib/mysql -p 3310:3306 mysql:5.7
云服务器需要设置安全组
//查看防火墙状态
firewall-cmd --state
//如果防火墙关闭,开启防火墙
systemctl start firewalld
//开放8080端口
firewall-cmd --permanent --add-port=3310/tcp
//查看端口,执行
firewall-cmd --permanent --query-port=3310/tcp
firewall-cmd --permanent --list-ports
//重启防火墙
firewall-cmd --reload
测试连接:
新建一个数据库test,在cd /data/mysql/data查看
删除容器
docker rm -f 容器id
删除容器后,本地文件依然在,实现数据持久化!!
删除容器 , 并删除容器挂载的数据卷:
docker rm -v 容器id
具名挂载(主要使用,不指定绝对路径,通过卷名查找本地的存储地址)
不指定主机地址
主机名不使用 “ / ” ,否则就会变成绝对地址
docker run -d -p 3310:3306 -v myclearnginx:/etc/nginx nginx
查看卷名
docker volume ls
通过卷名查看指定卷所在路径
docker volume inspect 卷名
拓展:指定具名卷的权限
ro
docker run -d -P --name nginx01 -v mynginx:/etc/nginx:ro nginx
ro:read-only--说明路径只能通过主机来操作,容器内部无法改变
rw
docker run -d -P --name nginx01 -v mynginx:/etc/nginx:rw nginx
一旦设定了设置了容器权限,挂载出来的内容就有了限定
初识Dockerfile
Dockerfile就是用来构建docker镜像的构建文件!命令脚本。
体验使用Dockerfile构建一个centos镜像:
第一步:创建一个测试文件夹
mkdir docker-test-volume
第二步进入文件夹内创建脚本
vim dockerfile01
FROM centos
VOLUME ["volume01","volume02"]
CMD echo "--------end------"
CMD /bin/bash
第三步:创建镜像
docker build 命令用于使用 Dockerfile 创建镜像。
语法
docker build [OPTIONS] PATH | URL | -
OPTIONS说明:
--build-arg=[] :设置镜像创建时的变量;
--cpu-shares :设置 cpu 使用权重;
--cpu-period :限制 CPU CFS周期;
--cpu-quota :限制 CPU CFS配额;
--cpuset-cpus :指定使用的CPU id;
--cpuset-mems :指定使用的内存 id;
--disable-content-trust :忽略校验,默认开启;
-f :指定要使用的Dockerfile路径;
--force-rm :设置镜像过程中删除中间容器;
--isolation :使用容器隔离技术;
--label=[] :设置镜像使用的元数据;
-m :设置内存最大值;
--memory-swap :设置Swap的最大值为内存+swap,"-1"表示不限swap;
--no-cache :创建镜像的过程不使用缓存;
--pull :尝试去更新镜像的新版本;
--quiet, -q :安静模式,成功后只输出镜像 ID;
--rm :设置镜像成功后删除中间容器;
--shm-size :设置/dev/shm的大小,默认值是64M;
--ulimit :Ulimit配置。
--squash :将 Dockerfile 中所有的操作压缩为一层。
--tag, -t: 镜像的名字及标签,通常 name:tag 或者 name 格式;可以在一次构建中为一个镜像设置多个标签。
--network: 默认 default。在构建期间设置RUN指令的网络模式
docker build -f /home/docker-test-volume/dockerfile1 -t sun/centos:1.0 .
查看镜像:
docker images
第五步:创建容器并进入容器:
docker run -it 18e0018b2e8d /bin/bash
其中:
使用docher inspect 容器id查看挂载的数据卷目录在哪里:
自动挂载数据卷是因为我们的脚本中含有volume字段,这种方式构建的容器会自动挂载数据卷,而不用我们在run命令中手动添加-v挂载:
容器数据卷
多个centos同步数据,共享同一份数据
第一步:通过centos:1.0(自己创建的镜像)创建并启动docker01容器
docker run -it --name docker01 sun/centos:1.0
在挂载目录下创建文件
创建第二个容器,并使用–volumes-from 实现数据同步:
docker run -it --name docker02 --volumes-from docker01 sun/centos:1.0
通过–volumes-from 容器名 镜像名 实现创建的容器数据共享
多个mysql实现数据共享:
注意开放的外部端口必须不同,不然会出现端口冲突
docker run -d -p 3306:3306 -v /home/mysql/conf:/etc/mysql/conf.d -v /home/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 --name mysql01 mysql:5.7
docker run -d -p 3307:3306 -e MYSQL_ROOT_PASSWORD=123456 --name mysql02 --volumes-from mysql01 mysql:5.7
第一步开启云服务器安全组
第二步开启防火墙端口
//查看防火墙状态
firewall-cmd --state
//如果防火墙关闭,开启防火墙
systemctl start firewalld
//开放3307 3307端口
firewall-cmd --permanent --add-port=3306/tcp
firewall-cmd --permanent --add-port=3307/tcp
//查看端口,执行
firewall-cmd --permanent --query-port=3306/tcp
firewall-cmd --permanent --query-port=3307/tcp
firewall-cmd --permanent --list-ports
//重启防火墙
firewall-cmd --reload
使用连接工具远程连接数据库。
总结:
数据卷技术就是将我们的容器内的目录,挂载到Linux上面。以及实现容器间的数据共享。
解决的问题有两个:
第一个:可以让我们不用再进入容器内修改文件信息,直接通过挂载目录在linux上直接修改。
第二个:实现容器间的数据共享,因为docker的核心就是隔离机制,让容器间相互隔离,数据卷技术很好的解决了容器间数据共享问题。
dockerfile
dockerfile 是用来构建docker 镜像的文件!命令参数脚本
构建的步骤:
1.编写一个dockerfile文件
(1)创建dockerfile文件
(2)执行从上到下顺序执行,符合分成下载
(3)#表示注释
(4)每一个指令都会创建提交一个新的镜像层,并提交
2.docker build构建成为一个镜像
3.docker run 运行镜像
4.docker push 发布镜像(DockerHub 或者阿里云镜像仓库)
官网案例:
dockerfile的相关命令:
FROM #基础镜像,一切从这里开始构建
MAINTAINER #镜像是谁写的,姓名+邮箱
RUN #镜像构建的时候需要运行的命令
ADD #步骤,tomcat镜像,这个tomcat压缩包!添加内容
WORKDIR #镜像的工作目录
VOLUME#挂载的目录
EXPOSE#保留端口配置
CMD#指定这个容器启动的时候要运行的命令,只有最后一个会生效,可被替代(在创建容器时,不能再追加参数,追加命令会覆盖文件中的命令)
ENTRYPOINT#指定这个容器启动的时候要运行的命令,可以追加命令
ONBUILO#当构建一个被继承DockerFile这个时候就会运行ONBUILO的指令,触发指令
COPY#类似ADD,将我们文件拷贝到镜像中
ENV#构建的时候设置环境变量
实战:创建一个自己的centos镜像(更完整版)
第一步:创建文件夹,编写dockerfile文件
FROM centos
MAINTAINER sun<1178834550@qq.com>
ENV MYPATH /usr/local
WORKDIR $MYPATH
RUN yum -y install vim
RUN yum -y install net-tools
EXPOSE 80
CMD echo $MYPATH
CMD echo "---------end----"
CMD /bin/bash
第二步构建镜像:(在dockerfile本目录下可以使用相对路径,在其他目录下,需要使用绝对路径)
docker build -f mydockerfile-centos -t mycentos:1.0 .
构建成功。
查看镜像:
docker images
创建centos容器:
docker run -it mycentos:1.0
查看镜像历史信息:
docker history 镜像id
与dockerfile编写的命令一致。分层执行
CMD和ENTRYPOINT的区别
CMD#指定这个容器启动的时候要运行的命令,只有最后一个会生效,可被替代(在创建容器时,不能再追加参数,追加命令会覆盖文件中的命令)
ENTRYPOINT#指定这个容器启动的时候要运行的命令,可以追加命令
测试:
编写cmd的dockerfile测试文件
[root@ dockerfile]# vim dockerfile-cmd-test
[root@ dockerfile]# cat dockerfile-cmd-test
FROM centos
CMD ["ls","-a"]
创建镜像:
docker build -f dockerfile-cmd-test -t cmdtest .
创建容器并进入:
docker run 镜像id
启动后,会自动执行ls -a的命令
但是在后面追加参数会报错:
#追加一个-l ls -al
docker run eb022b02b7cd -l
docker: Error response from daemon: OCI runtime create failed: container_linux.go:380: starting container process caused: exec: "-l": executable file not found in $PATH: unknown.
#可以改为追加ls -al 覆盖命令
docker run eb022b02b7cd ls -al
编写entrypoint的dockerfile测试文件
[root@ dockerfile]# vim dockerfile-entrypoint-test
[root@ dockerfile]# cat dockerfile-entrypoint-test
FROM centos
ENTRYPOINT ["ls","-a"]
创建镜像:
docker build -f dockerfile-entrypoint-test -t entrypointtest .
创建容器并进入:
docker run 镜像id
启动后,会自动执行ls -a的命令
在后面追加-l,执行ls -al,追加参数在命令后面
docker run 镜像id -l
实战构建自己的tomcat镜像
创建文件夹,编写dockerfile文件:
[root@ home]# mkdir docker-tomcat
[root@ home]# cd docker-tomcat/
[root@ docker-tomcat]# touch readme.txt
[root@ docker-tomcat]# vim Dockerfile
[root@ docker-tomcat]# cat Dockerfile
FROM centos
MAINTAINER sun<1178834550@qq.com>
COPY readme.txt /usr/local/readme.txt
ADD jdk-8u301-linux-x64.tar.gz /usr/local/
ADD apache-tomcat-9.0.50.tar.gz /usr/local/
RUN yum -y install vim
ENV MYPATH /usr/local
WORKDIR $MYPATH
ENV JAVA_HOME /usr/local/jdk1.8.0_301
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
ENV CATALINA_HOME /usr/local/apache-tomcat-9.0.50
ENV CATALINA_BASH /usr/local/apache-tomcat-9.0.50
ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/lib:$CATALINA_HOME/bin
EXPOSE 8080
CMD /usr/local/apache-tomcat-9.0.50/bin/startup.sh && tail -F /usr/local/apache-tomcat-9.0.50/bin/logs/catalina.out
开启云服务器安全组
开启防火墙端口
//查看防火墙状态
firewall-cmd --state
//如果防火墙关闭,开启防火墙
systemctl start firewalld
//开放3355端口
firewall-cmd --permanent --add-port=3355/tcp
//查看端口,执行
firewall-cmd --permanent --query-port=3355/tcp
firewall-cmd --permanent --list-ports
//重启防火墙
firewall-cmd --reload
构建镜像
docker build -f Dockerfile -t diycentos:1.0 .
创建并启动容器,挂载数据卷:
docker run -d -p 3355:8080 --name sun -v /home/docker-tomcat/test:/usr/local/apache-tomcat-9.0.50/webapps/test -v /home/docker-tomcat/tomcatlogs/:/usr/local/apache-tomcat-9.0.50/logs diycentos:1.0
进去test创建WEB-INF文件:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<display-name>db</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
</web-app>
创建index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<body>
<h2>Hello World!</h2>
</body>
</html>
项目部署成功。
发布镜像到dockerHub
官网地址:https://hub.docker.com/
注册账号:
使用docker login -u用户名 -p密码
进行远程登录
修改镜像tag:
docker tag 镜像id dockerhub的用户名/镜像名:[tag]
使用docker push
提交镜像:
docker push dockerhub的用户名/镜像名:[tag]
使用阿里云镜像仓库:
创建仓库后,根据提示进行操作!!
到这里docker基本算是入门:命令图总结
Docker的网络原理:
查看网络基本配置:
ip addr
lo:为本机回环地址
etho:阿里云内网地址
docker0:为docker0地址
docker是如何处理容器网络访问:
创建一个tomcat容器,查看容器内部网络地址:
docker run -d -P --name tomcat01 tomcat
docker exec -it tomcat01 ip addr
docker会为容器分配一个eth0@网络地址
linux本机是可以ping通所有的docker容器内部的。
ip addr
我们每次启动一个docker容器,docker 就会给容器分配一个ip,而且会增加一个vethc,这里使用了evth-pair技术。
容器间能否直接ping通:
再次创建一个容器:
docker run -d -P --name tomcat02 tomcat
docker exec -it tomcat02 ip addr
容器之间使用ip地址能ping通:
docker exec -it tomcat02 ping tomcat01ip地址
ping通的逻辑图:
使用容器名ping通:–link
docker run -d -P --name tomcat03 --link tomcat02 tomcat
docker exec -it tomcat03 ping tomcat02
缺点:反向不能ping通。
自定义网络:
ip地址:
计算广播地址
广播地址:网络地址的主机位全部变成1 ,10011111 即159 即:202.112.14.159
主机数
主机号有5位,那么这个地址中,就只能有25−2=3025−2=30个主机
因为其中全0作为网络地址,全1作为广播地址
根据每个网络的主机数量进行子网地址的规划和计算子网掩码
这也可按上述原则进行计算。
比如一个子网有10台主机,那么对于这个子网需要的IP地址是
10+1+1+1=13
注意:加的第一个1是指这个网络连接时所需的网关地址,接着的两个1分别是指网络地址和广播地址。
因为13小于16(16等于2的4次方),所以主机位为4位。
而 256-16=240 所以该子网掩码为255.255.255.240。
如果一个子网有14台主机,不少人常犯的错误是:依然分配具有16个地址空间的子网,而忘记了给网关分配地址。这样就错误了,因为:
14+1+1+1=17
17.大于16,所以我们只能分配具有32个地址(32等于2的5次方)空间的子网。这时子网掩码为:255.255.255.224
5) 主机的数量
206 110 4 0/18被划分成16个子网,每个子网掩码?
(划分成16个子网,根据子网掩码/18就表示有18个1,就要从的IP地址的主机位借4位来用作网络位!)
子网掩码是255.255.252.0
每个子网可以容纳的主机数是1024台。
下面我来给你详细解答:
206.110.1.0 /18 由最后的那个/18,我们可以知道这个IP已经规定了它的网络位是18位,它默认的子网掩码就是11111111.11111111.11 | 000000.00000000(其中1代表网络位,0代表主机位)
可以看出我们可以操作的位数就是后面的14个0,也就是说我们可以在地面划分出几位作为子网的网络位,进而来划分子网。要求是切分成16个子网,我们知道2的4次方刚好等于16,这就说明子网网络位的位数是4位,那14-4=10就是子网的主机位。所以上面我写的那串二进制就可以变成:11111111.11111111.111111 | 00.00000000(其中1代表网络位,0代表主机位)
ip段/数字-如192.168.0.1/24是什么意思?
后面这个数字标示了我们的网络号的位数,也就是子网掩码中前多少号为1
129.168.1.1 /24 这个24就是告诉我们网络号是24位
也就相当于告诉我们了
子网掩码是:11111111 11111111 11111111 00000000
即:255.255.255.0
172.16.10.33/27 中的/27
也就是说子网掩码是255.255.255.224 即27个全1
11111111 11111111 11111111 11100000
自定义网络:
docker network create --driver bridge --subnet 192.168.0.0/16 --gateway 192.168.0.1 mynet
查看设置:
docker network inspect mynet
查看所有的docker网络
docker network ls
网络模式
bridge:桥接 docker(默认,自己创建的也是使用bridge模式)
none:不配置网络
host:和宿主机共享网络
container:容器内网络连通!(用的少!局限很大)
使用自己的网络创建容器:
docker run -d -P --name tomcat-net-01 --net mynet tomcat
docker run -d -P --name tomcat-net-02 --net mynet tomcat
查看网络设置:
docker network inspect mynet
使用了containers容器内网络连通
现在ping通,既可以通过名字也可以通过ip地址,不用再设置–link:
docker exec -it tomcat-net-01 ping 192.168.0.3
docker exec -it tomcat-net-01 ping tomcat-net-02
部署Redis集群
# 创建网卡
docker network create redis --subnet 172.38.0.0/16
# 通过脚本创建六个redis配置
for port in $(seq 1 6); \
do \
mkdir -p /mydata/redis/node-${port}/conf
touch /mydata/redis/node-${port}/conf/redis.conf
cat << EOF >/mydata/redis/node-${port}/conf/redis.conf
port 6379
bind 0.0.0.0
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.38.0.1${port}
cluster-announce-port 6379
cluster-announce-bus-port 16379
appendonly yes
EOF
done
# 运行6个节点
docker run -p 6371:6379 -p 16371:16379 --name redis-1 \
-v /mydata/redis/node-1/data:/data \
-v /mydata/redis/node-1/conf/redis.conf:/etc/redis/redis.conf \
-d --net redis --ip 172.38.0.11 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf
docker run -p 6372:6379 -p 16372:16379 --name redis-2 \
-v /mydata/redis/node-2/data:/data \
-v /mydata/redis/node-2/conf/redis.conf:/etc/redis/redis.conf \
-d --net redis --ip 172.38.0.12 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf
docker run -p 6373:6379 -p 16373:16379 --name redis-3 \
-v /mydata/redis/node-3/data:/data \
-v /mydata/redis/node-3/conf/redis.conf:/etc/redis/redis.conf \
-d --net redis --ip 172.38.0.13 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf
docker run -p 6374:6379 -p 16374:16379 --name redis-4 \
-v /mydata/redis/node-4/data:/data \
-v /mydata/redis/node-4/conf/redis.conf:/etc/redis/redis.conf \
-d --net redis --ip 172.38.0.14 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf
docker run -p 6375:6379 -p 16375:16379 --name redis-5 \
-v /mydata/redis/node-5/data:/data \
-v /mydata/redis/node-5/conf/redis.conf:/etc/redis/redis.conf \
-d --net redis --ip 172.38.0.15 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf
docker run -p 6376:6379 -p 16376:16379 --name redis-6 \
-v /mydata/redis/node-6/data:/data \
-v /mydata/redis/node-6/conf/redis.conf:/etc/redis/redis.conf \
-d --net redis --ip 172.38.0.16 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf
#查看redis容器启动情况:
docker ps
#进入redis容器内
docker exec -it redis-1 /bin/sh
# 创建集群
/data # redis-cli --cluster create 172.38.0.11:6379 172.38.0.12:6379 172.38.0.13:6379 172.38.0.14:6379 172.38.0.15:6379 172.38.0.16:6379 --cluster-replicas 1
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 172.38.0.15:6379 to 172.38.0.11:6379
Adding replica 172.38.0.16:6379 to 172.38.0.12:6379
Adding replica 172.38.0.14:6379 to 172.38.0.13:6379
M: ebfbbc88a83fa84c3d73fe048fc37ab82841fa0e 172.38.0.11:6379
slots:[0-5460] (5461 slots) master
M: ed7bac9d22ce436c650f9a51c3ae7a3aa84a7537 172.38.0.12:6379
slots:[5461-10922] (5462 slots) master
M: 2f1fdb553165c31c599f5cdbac5a42fa15901602 172.38.0.13:6379
slots:[10923-16383] (5461 slots) master
S: d2edadd37c2b243ffa3a13ac7bb352062160f4b8 172.38.0.14:6379
replicates 2f1fdb553165c31c599f5cdbac5a42fa15901602
S: 98bec4772bd3cdc7335111f75bc3f6b6ad2fae26 172.38.0.15:6379
replicates ebfbbc88a83fa84c3d73fe048fc37ab82841fa0e
S: 6e8dda893836d8182af1a7cbe0ff71b32d0743ef 172.38.0.16:6379
replicates ed7bac9d22ce436c650f9a51c3ae7a3aa84a7537
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
....
>>> Performing Cluster Check (using node 172.38.0.11:6379)
M: ebfbbc88a83fa84c3d73fe048fc37ab82841fa0e 172.38.0.11:6379
slots:[0-5460] (5461 slots) master
1 additional replica(s)
S: 98bec4772bd3cdc7335111f75bc3f6b6ad2fae26 172.38.0.15:6379
slots: (0 slots) slave
replicates ebfbbc88a83fa84c3d73fe048fc37ab82841fa0e
S: d2edadd37c2b243ffa3a13ac7bb352062160f4b8 172.38.0.14:6379
slots: (0 slots) slave
replicates 2f1fdb553165c31c599f5cdbac5a42fa15901602
S: 6e8dda893836d8182af1a7cbe0ff71b32d0743ef 172.38.0.16:6379
slots: (0 slots) slave
replicates ed7bac9d22ce436c650f9a51c3ae7a3aa84a7537
M: 2f1fdb553165c31c599f5cdbac5a42fa15901602 172.38.0.13:6379
slots:[10923-16383] (5461 slots) master
1 additional replica(s)
M: ed7bac9d22ce436c650f9a51c3ae7a3aa84a7537 172.38.0.12:6379
slots:[5461-10922] (5462 slots) master
1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
redis-cli介绍一些重要参数以及使用场景。
1、-r 代表将命令重复执行多次
$redis-cli -r 3 ping
PONG
PONG
PONG
ping命令可用于检测redis实例是否存活,如果存活则显示PONG。
2、-i
每隔几秒(如果想用ms,如10ms则写0.01)执行一次命令,必须与-r一起使用。
$redis-cli -r 3 -i 1 ping
PONG
PONG
PONG
$redis-cli -r 10 -i 1 info|grep used_memory_human
used_memory_human:2.95G
.....................................
used_memory_human:2.95G
每隔1秒输出内存的使用量,一共输出10次。
$redis-cli -h ip -p port info server|grep process_id
process_id:999
获取redis的进程号999
3、-x
代表从标准输入读取数据作为该命令的最后一个参数。
$echo "world" |redis-cli -x set hello
Ok
4、-c
连接集群结点时使用,此选项可防止moved和ask异常。
5、-a
如配置了密码,可用a选项。
6、--scan和--pattern
用于扫描指定模式的键,相当于scan命令。
7、--slave
当当前客户端模拟成当前redis节点的从节点,可用来获取当前redis节点的更新操作。合理利用可用于记录当前连接redis节点的一些更新操作,这些更新可能是实开发业务时需要的数据。
8、--rdb
会请求redis实例生成并发送RDB持久化文件,保存在本地。可做定期备份。
9、--pipe
将命令封装成redis通信协议定义的数据格式,批量发送给redis执行。
10、--bigkeys
统计bigkey的分布,使用scan命令对redis的键进行采样,从中找到内存占用比较大的键,这些键可能是系统的瓶颈。
11、--eval
用于执行lua脚本
12、--latency
有三个选项,--latency、--latency-history、--latency-dist。它们可检测网络延迟,展现的形式不同。
13、--stat
可实时获取redis的重要统计信息。info命令虽然比较全,但这里可看到一些增加的数据,如requests(每秒请求数)
14、--raw 和 --no-raw
--no-raw 要求返回原始格式。--raw 显示格式化的效果。
cluster集群基本命令:
cluster info :打印集群的信息
cluster nodes :列出集群当前已知的所有节点( node),以及这些节点的相关信息。
节点
cluster meet <ip> <port> :将 ip 和 port 所指定的节点添加到集群当中,让它成为集群的一份子。
cluster forget <node_id> :从集群中移除 node_id 指定的节点。
cluster replicate <master_node_id> :将当前从节点设置为 node_id 指定的master节点的slave节点。只能针对slave节点操作。
cluster saveconfig :将节点的配置文件保存到硬盘里面。
槽(slot)
cluster addslots <slot> [slot ...] :将一个或多个槽( slot)指派( assign)给当前节点。
cluster delslots <slot> [slot ...] :移除一个或多个槽对当前节点的指派。
cluster flushslots :移除指派给当前节点的所有槽,让当前节点变成一个没有指派任何槽的节点。
cluster setslot <slot> node <node_id> :将槽 slot 指派给 node_id 指定的节点,如果槽已经指派给
另一个节点,那么先让另一个节点删除该槽>,然后再进行指派。
cluster setslot <slot> migrating <node_id> :将本节点的槽 slot 迁移到 node_id 指定的节点中。
cluster setslot <slot> importing <node_id> :从 node_id 指定的节点中导入槽 slot 到本节点。
cluster setslot <slot> stable :取消对槽 slot 的导入( import)或者迁移( migrate)。
键
cluster keyslot <key> :计算键 key 应该被放置在哪个槽上。
cluster countkeysinslot <slot> :返回槽 slot 目前包含的键值对数量。
cluster getkeysinslot <slot> <count> :返回 count 个 slot 槽中的键 。
#连接集群结点
redis-cli -c
#查看节点信息
cluster info
cluster nodes
#在改节点,设置值
set a b
重新启动一个窗口,停止设置值的redis节点
docker stop redis-3
更根据主从复制,主节点挂了,从节点变为主节点,体现集群高可用,说明集群搭建成功。
重新获取值
/data # redis-cli -c
127.0.0.1:6379> get a
"b"
再次查看节点信息:
列出集群当前已知的所有节点( node),以及这些节点的相关信息。
cluster nodes
说明集群搭建成功。
springBoot项目打包成Docker镜像
1、创建springboot项目
2、打包应用
3、编写dockerfile(idea中安装docker插件比较方便,会有高亮提示)
spingBoot内置了tomcat
FROM java:8
COPY *.jar /app.jar
CMD ["--server.port=8080"]
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app.jar"]
4、将jar包和Dockerfile文件传输到Linux服务器上
通过Dockerfile构建镜像:
docker build -f Dockerfile -t spingboottest:1.0 .
开启云服务器安全组
开启防火墙端口
//查看防火墙状态
firewall-cmd --state
//如果防火墙关闭,开启防火墙
systemctl start firewalld
//开放8080端口
firewall-cmd --permanent --add-port=8080/tcp
//查看端口,执行
firewall-cmd --permanent --query-port=8080/tcp
firewall-cmd --permanent --list-ports
//重启防火墙
firewall-cmd --reload
6、发布运行
docker run -d -p 8080:8080 --name springboot-test spingboottest:1.0
http://ip地址:8080/hello
springBoot项目打包成Docker镜像运行成功
最后感谢狂神