作者 qin lang
部分资料借鉴 cloudman
容器是一种轻量级、可移植、自包含的软件打包技术,使应用程序可以在几乎任何地方以相同的方式运行。开发人员在自己笔记本上创建并测试好的容器,无需任何修改就能够在生产系统的虚拟机、物理服务器或公有云主机上运行。
容器与虚拟机
谈到容器,就不得不将它与虚拟机进行对比,因为两者都是为应用提供封装和隔离。
容器由两部分组成:
1 应用程序本身
2 依赖:比如应用程序需要的库或其他软件
容器在 Host 操作系统的用户空间中运行,与操作系统的其他进程隔离。这一点显著区别于的虚拟机。
传统的虚拟化技术,比如 VMWare, KVM, Xen,目标是创建完整的虚拟机。为了运行应用,除了部署应用本身及其依赖(通常几十 MB),还得安装整个操作系统(几十 GB)。
为什么需要容器?容器到底解决的是什么问题?
简要的答案是:容器使软件具备了超强的可移植能力。
我们来看看今天的软件开发面临着怎样的挑战?
如今的系统在架构上较十年前已经变得非常复杂了。以前几乎所有的应用都采用三层架构(Presentation/Application/Data),系统部署到有限的几台物理服务器上(Web Server/Application Server/Database Server)。
而今天,开发人员通常使用多种服务(比如 MQ,Cache,DB)构建和组装应用,而且应用很可能会部署到不同的环境,比如虚拟服务器,私有云和公有云。
一方面应用包含多种服务,这些服务有自己所依赖的库和软件包;另一方面存在多种部署环境,服务在运行时可能需要动态迁移到不同的环境中。这就产生了一个问题:
如何让每种服务能够在所有的部署环境中顺利运行?
于是我们得到了下面这个矩阵:
各种服务和环境通过排列组合产生了一个大矩阵。开发人员在编写代码时需要考虑不同的运行环境,运维人员则需要为不同的服务和平台配置环境。对他们双方来说,这都是一项困难而艰巨的任务。
如何解决这个问题呢?
聪明的技术人员从传统的运输行业找到了答案。
几十年前,运输业面临着类似的问题。
每一次运输,货主与承运方都会担心因货物类型的不同而导致损失,比如几个铁桶错误地压在了一堆香蕉上。另一方面,运输过程中需要使用不同的交通工具也让整个过程痛苦不堪:货物先装上车运到码头,卸货,然后装上船,到岸后又卸下船,再装上火车,到达目的地,最后卸货。一半以上的时间花费在装、卸货上,而且搬上搬下还容易损坏货物。
这同样也是一个 NxM 的矩阵。
幸运的是,集装箱的发明解决这个难题。
任何货物,无论钢琴还是保时捷,都被放到各自的集装箱中。集装箱在整个运输过程中都是密封的,只有到达最终目的地才被打开。标准集装箱可以被高效地装卸、重叠和长途运输。现代化的起重机可以自动在卡车、轮船和火车之间移动集装箱。集装箱被誉为运输业与世界贸易最重要的发明。
Docker 将集装箱思想运用到软件打包上,为代码提供了一个基于容器的标准化运输系统。Docker 可以将任何应用及其依赖打包成一个轻量级、可移植、自包含的容器。容器可以运行在几乎所有的操作系统上。
其实,“集装箱” 和 “容器” 对应的英文单词都是 “Container”。
“容器” 是国内约定俗成的叫法,可能是因为容器比集装箱更抽象,更适合软件领域的原故吧。
我个人认为:在老外的思维中,“Container” 只用到了集装箱这一个意思,Docker 的 Logo 不就是一堆集装箱吗?
Docker 的特性
我们可以看看集装箱思想是如何与 Docker 各种特性相对应的。
Docker 客户端 - Client
Docker 服务器 - Docker daemon
Docker 镜像 - Image
Registry
Docker 容器 - Container
Docker daemon 是服务器组件,以 Linux 后台服务的方式运行。
Docker daemon 运行在 Docker host 上,负责创建、运行、监控容器,构建、存储镜像。
默认配置下,Docker daemon 只能响应来自本地 Host 的客户端请求。如果要允许远程客户端请求,需要在配置文件中打开 TCP 监听,步骤如下:
编辑配置文件 /etc/systemd/system/multi-user.target.wants/docker.service,在环境变量 ExecStart 后面添加 -H tcp://0.0.0.0,允许来自任意 IP 的客户端连接。
如果使用的是其他操作系统,配置文件的位置可能会不一样。
重启 Docker daemon。
服务器 IP 为 192.168.56.102,客户端在命令行里加上 -H 参数,即可与远程服务器通信。
info 子命令用于查看 Docker 服务器的信息。
可将 Docker 镜像看着只读模板,通过它可以创建 Docker 容器。
例如某个镜像可能包含一个 Ubuntu 操作系统、一个 Apache HTTP Server 以及用户开发的 Web 应用。
镜像有多种生成方法:
可以从无到有开始创建镜像
也可以下载并使用别人创建好的现成的镜像
还可以在现有镜像上创建新的镜像
Docker 容器就是 Docker 镜像的运行实例。
用户可以通过 CLI(docker)或是 API 启动、停止、移动或删除容器。可以这么认为,对于应用软件,镜像是软件生命周期的构建和打包阶段,而容器则是启动和运行阶段。
Registry 是存放 Docker 镜像的仓库,Registry 分私有和公有两种。
Docker Hub(https://hub.docker.com/) 是默认的 Registry,由 Docker 公司维护,上面有数以万计的镜像,用户可以自由下载和使用。
ECR AWS 公司容器存放位置
docker pull 命令可以从 Registry 下载镜像。
docker run 命令则是先下载镜像(如果本地没有),然后再启动容器。
yum -y install docker
环境就绪,马上运行第一个容器,执行命令:
docker run -d -p 80:80 httpd
其过程可以简单的描述为:
Docker 客户端执行 docker run 命令。
Docker daemon 发现本地没有 httpd 镜像。
daemon 从 Docker Hub 下载镜像。
下载完成,镜像 httpd 被保存到本地。
Docker daemon 启动容器。
hello-world 是 Docker 官方提供的一个镜像,通常用来验证 Docker 是否安装成功。
docker pull hello-world
[root@ip-172-29-140-52 ~]# docker pull hello-world Using default tag: latest latest: Pulling from library/hello-world 0e03bdcc26d7: Pull complete Digest: sha256:49a1c8800c94df04e9658809b006fd8a686cab8028d33cfba2cc049724254202 Status: Downloaded newer image for hello-world:latest docker.io/library/hello-world:latest
# 容器信息获取命令 docker ps #列出当前运行中的容器 docker ps -a #列出该机器上所有的容器,包括启动和未启动的 docker images #列出该机器上所有的镜像 docker top 容器id #获取容器的top信息 docker stats 容器id #获取容器容器信息 docker logs 容器id #获取容器的输出日志 docker logs -f 容器id #实时获取容器的日志 docker inspect 容器ID #查看容器信息 docker history 镜像ID #查看容器的历史构建记录 # 容器操作命令 docker tag 镜像 新的名字 例如 docker tag nginx:latest wuerp-nginx:latest #新打出来的包会单独的存在 docker run #启动容器 -i 以交互模式运行容器,通常与 -t 同时使用; -t 为容器重新分配一个伪输入终端,通常与 -i 同时使用; -d 后台运行容器,并返回容器ID; docker cp 文件 容器id:/root #把文件cp到容器的root目录下 docker cp 容器id:/root/wuerp.conf . #把容器/root/wuerp.conf的文件拷贝到本地 docker start 容器id #启动容器,在容器停止的时候使用 docker stop 容器id #停止容器,在容器启动的时候使用 docker restart 容器id #重启容器 docker pause 容器id #暂停容器 docker unpause 容器id #取暂停容器 docker rm 容器id #删除容器 docker rmi 镜像id #删除镜像ID 小技巧 删除所有容器 docker ps $(docker ps -a -q) 删除容器镜像 docker rmi $(docker images) docker rename 容器名字 新的容器名字 #给容器--name 属性的值改名 docker exec -it 容器id /bin/bash #进入容器使用/bin/bash解释器 docker container update --restart=always 容器ID #更新容器的一些属性 例如这更新容器的启动属性 always docker启动的时候会自动启动容器 #容器获取上传命令 docker login #登录镜像仓库 默认情况是登录dockerhub的,可以自己去注册一个 #登录公司的ECR仓库方法 aws ecr get-login-password --region cn-northwest-1 | docker login --username AWS --password-stdin 097364988641.dkr.ecr.cn-northwest-1.amazonaws.com.cn docker logout #退出镜像仓库 docker search 镜像 # 在dockerhub搜索容器镜像 docker pull #镜像仓库/镜像:标签 #拉取镜像到本地 docker push #推送镜像到仓库 docker push 097364988641.dkr.ecr.cn-northwest-1.amazonaws.com.cn/local_dotnet_core:latest #注意,镜像是什么名字,推送到的镜像仓库就是什么名字
docker run -it -p 8080:80 -v /root/2020-07-29:/root/log --name nginx --restart=always nginx
-p 8080:80 将本机的8080端口映射到容器的80上
-v /root/2020-07-29:/root/log 把本机的/root/2020-07-29文件夹挂载到容器的/root/log下
–name nginx 给容器启一个名字
–restart=always 容器启动策略,会自动重启容器
最后的nginx是Images的名称,如果没有就从dockerhub上默认拉取
sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": ["https://352w7nbu.mirror.aliyuncs.com"] } EOF sudo systemctl daemon-reload sudo systemctl restart docker
Linux 操作系统由内核空间和用户空间组成。如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TnPAL5M2-1623205435987)(1E33BB6B44954BA992EE1172FF4F9F0C)]
rootfs
内核空间是 kernel,Linux 刚启动时会加载 bootfs 文件系统,之后 bootfs 会被卸载掉。
用户空间的文件系统是 rootfs,包含我们熟悉的 /dev, /proc, /bin 等目录。
对于 base 镜像来说,底层直接用 Host 的 kernel,自己只需要提供 rootfs 就行了。
而对于一个精简的 OS,rootfs 可以很小,只需要包括最基本的命令、工具和程序库就可以了。相比其他 Linux 发行版,CentOS 的 rootfs 已经算臃肿的了,alpine 还不到 10MB。
我们平时安装的 CentOS 除了 rootfs 还会选装很多软件、服务、图形桌面等,需要好几个 GB 就不足为奇了。
base 镜像提供的是最小安装的 Linux 发行版。
下面是 CentOS 镜像的 Dockerfile 的内容:
FROM scratch ADD centos-7-docker.tar.xz CMD ["/bin/bash"]
第二行 ADD 指令添加到镜像的 tar 包就是 CentOS 7 的 rootfs。在制作镜像时,这个 tar 包会自动解压到 / 目录下,生成 /dev, /porc, /bin 等目录。
注:可在 Docker Hub 的镜像描述页面中查看 Dockerfile 。
https://hub.docker.com/
不同 Linux 发行版的区别主要就是 rootfs。
比如 Ubuntu 14.04 使用 upstart 管理服务,apt 管理软件包;而 CentOS 7 使用 systemd 和 yum。这些都是用户空间上的区别,Linux kernel 差别不大。
所以 Docker 可以同时支持多种 Linux 镜像,模拟出多种操作系统环境。
Docker 支持通过扩展现有镜像,创建新的镜像。
实际上,Docker Hub 中 99% 的镜像都是通过在 base 镜像中安装和配置需要的软件构建出来的。比如我们现在构建一个新的镜像,Dockerfile 如下:
FROM centos RUN yum -y install wget RUN yum -y install zip CMD ["/bin/bash"]
① 新镜像不再是从 scratch 开始,而是直接在 centos 镜像上构建。
② 安装 wget。
③ 安装 zip。
④ 容器启动时运行 bash。
[root@ip-172-29-140-52 ~]# docker build -t wuerp-test . Sending build context to Docker daemon 465.7MB Step 1/4 : FROM centos latest: Pulling from library/centos 6910e5a164f7: Pull complete Digest: sha256:4062bbdd1bb0801b0aa38e0f83dece70fb7a5e9bce223423a68de2d8b784b43b Status: Downloaded newer image for centos:latest ---> 831691599b88 Step 2/4 : RUN yum -y install wget ---> Running in b6e2f927de4d CentOS-8 - AppStream 1.7 MB/s | 5.8 MB 00:03 CentOS-8 - Base 736 kB/s | 2.2 MB 00:03 CentOS-8 - Extras 7.4 kB/s | 7.0 kB 00:00 Last metadata expiration check: 0:00:01 ago on Fri Jul 31 14:00:18 2020. Dependencies resolved. ================================================================================ Package Arch Version Repository Size ================================================================================ Installing: wget x86_64 1.19.5-8.el8_1.1 AppStream 735 k Installing dependencies: libpsl x86_64 0.20.2-5.el8 BaseOS 61 k publicsuffix-list-dafsa noarch 20180723-1.el8 BaseOS 56 k Transaction Summary ================================================================================ Install 3 Packages Total download size: 852 k Installed size: 3.0 M Downloading Packages: (1/3): publicsuffix-list-dafsa-20180723-1.el8.n 200 kB/s | 56 kB 00:00 (2/3): libpsl-0.20.2-5.el8.x86_64.rpm 96 kB/s | 61 kB 00:00 (3/3): wget-1.19.5-8.el8_1.1.x86_64.rpm 666 kB/s | 735 kB 00:01 -------------------------------------------------------------------------------- Total 343 kB/s | 852 kB 00:02 warning: /var/cache/dnf/AppStream-02e86d1c976ab532/packages/wget-1.19.5-8.el8_1.1.x86_64.rpm: Header V3 RSA/SHA256 Signature, key ID 8483c65d: NOKEY CentOS-8 - AppStream 1.6 MB/s | 1.6 kB 00:00 Importing GPG key 0x8483C65D: Userid : "CentOS (CentOS Official Signing Key) <security@centos.org>" Fingerprint: 99DB 70FA E1D7 CE22 7FB6 4882 05B5 55B3 8483 C65D From : /etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial Key imported successfully Running transaction check Transaction check succeeded. Running transaction test Transaction test succeeded. Running transaction Preparing : 1/1 Installing : publicsuffix-list-dafsa-20180723-1.el8.noarch 1/3 Installing : libpsl-0.20.2-5.el8.x86_64 2/3 Installing : wget-1.19.5-8.el8_1.1.x86_64 3/3 Running scriptlet: wget-1.19.5-8.el8_1.1.x86_64 3/3 Verifying : wget-1.19.5-8.el8_1.1.x86_64 1/3 Verifying : libpsl-0.20.2-5.el8.x86_64 2/3 Verifying : publicsuffix-list-dafsa-20180723-1.el8.noarch 3/3 Installed: libpsl-0.20.2-5.el8.x86_64 publicsuffix-list-dafsa-20180723-1.el8.noarch wget-1.19.5-8.el8_1.1.x86_64 Complete! Removing intermediate container b6e2f927de4d ---> 15030461b7ce Step 3/4 : RUN yum -y install zip ---> Running in 2739aa93af31 Last metadata expiration check: 0:00:06 ago on Fri Jul 31 14:00:18 2020. Dependencies resolved. ================================================================================ Package Architecture Version Repository Size ================================================================================ Installing: zip x86_64 3.0-23.el8 BaseOS 270 k Installing dependencies: unzip x86_64 6.0-43.el8 BaseOS 195 k Transaction Summary ================================================================================ Install 2 Packages Total download size: 465 k Installed size: 1.2 M Downloading Packages: (1/2): zip-3.0-23.el8.x86_64.rpm 289 kB/s | 270 kB 00:00 (2/2): unzip-6.0-43.el8.x86_64.rpm 170 kB/s | 195 kB 00:01 -------------------------------------------------------------------------------- Total 260 kB/s | 465 kB 00:01 Running transaction check Transaction check succeeded. Running transaction test Transaction test succeeded. Running transaction Preparing : 1/1 Installing : unzip-6.0-43.el8.x86_64 1/2 Installing : zip-3.0-23.el8.x86_64 2/2 Running scriptlet: zip-3.0-23.el8.x86_64 2/2 Verifying : unzip-6.0-43.el8.x86_64 1/2 Verifying : zip-3.0-23.el8.x86_64 2/2 Installed: unzip-6.0-43.el8.x86_64 zip-3.0-23.el8.x86_64 Complete! Removing intermediate container 2739aa93af31 ---> e6882e168358 Step 4/4 : CMD ["/bin/bash"] ---> Running in f62d45699df5 Removing intermediate container f62d45699df5 ---> bb1e77a27181 Successfully built bb1e77a27181 Successfully tagged wuerp-test:latest
可以看到,新镜像是从 base 镜像一层一层叠加生成的。每安装一个软件,就在现有镜像的基础上增加一层。
问什么 Docker 镜像要采用这种分层结构呢?
最大的一个好处就是 - 共享资源。
比如:有多个镜像都从相同的 base 镜像构建而来,那么 Docker Host 只需在磁盘上保存一份 base 镜像;同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了。而且镜像的每一层都可以被共享,我们将在后面更深入地讨论这个特性。
这时可能就有人会问了:如果多个容器共享一份基础镜像,当某个容器修改了基础镜像的内容,比如 /etc 下的文件,这时其他容器的 /etc 是否也会被修改?
答案是不会!
修改会被限制在单个容器内。
这就是我们接下来要学习的容器 Copy-on-Write 特性。
可写的容器层
当容器启动时,一个新的可写层被加载到镜像的顶部。
这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”。
所有对容器的改动 - 无论添加、删除、还是修改文件都只会发生在容器层中。
只有容器层是可写的,容器层下面的所有镜像层都是只读的。
下面我们深入讨论容器层的细节。
镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。如果不同层中有一个相同路径的文件,比如 /a,上层的 /a 会覆盖下层的 /a,也就是说用户只能访问到上层中的文件 /a。在容器层中,用户看到的是一个叠加之后的文件系统。
只有当需要修改时才复制一份数据,这种特性被称作 Copy-on-Write。可见,容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。
这样就解释了我们前面提出的问题:容器层记录对镜像的修改,所有镜像层都是只读的,不会被容器修改,所以镜像可以被多个容器共享。
*小技巧 在上面的镜像层构建较大不常常改动的内容,例如上面安装许多软件,在下面放置配置文件,但是配置文件可能会经常改动,如果配置文件在安装包上面的话,每次构建的时候检测到每一层的md5值变化,就会从产生变化的镜像层开始重新构建
以目前准备使用的keycloak和Mysql举例
mkdir -p {conf,data,logs,backup}
vim $PWD/conf/my.cnf
[client] default-character-set = utf8 [mysql] default-character-set = utf8 [mysqld] character_set_server = utf8 collation-server = utf8_bin transaction_isolation = READ-COMMITTED
docker run --name mysql --restart always -p 3306:3306 -e MYSQL_ROOT_PASSWORD=wuerp -v $PWD/data:/var/lib/mysql -v $PWD/logs:/logs -v $PWD/conf:/etc/mysql/conf.d -v $PWD/backup:/backup -itd mysql:5.7
- 启动一个名字叫做mysql 启动策略总是启动的容器
- -p 映射的端口把本地3306 映射到容器的3306
- -e 传递环境变量到容器内 这里传入一个password
- -v 把$pwd/data 映射到/var/lib/mysql这个位置 把当前目录下的pwd
- -v 把$pwd/logs映射到/logs
- -v 把$pwd/backup 映射到/backup
- -itd 把容器放在后台运行返回容器id
- mysql:5.7 使用的镜像版本
[root@ip-172-29-140-52 test]# docker run --name mysql --restart always -p 3306:3306 -e MYSQL_ROOT_PASSWORD=wuerp -v $PWD/data:/var/lib/mysql -v $PWD/logs:/logs -v $PWD/conf:/etc/mysql/conf.d -v $PWD/backup:/backup -itd mysql:5.7 Unable to find image 'mysql:5.7' locally 5.7: Pulling from library/mysql 6ec8c9369e08: Already exists 177e5de89054: Pull complete ab6ccb86eb40: Pull complete e1ee78841235: Pull complete 09cd86ccee56: Pull complete 78bea0594a44: Pull complete caf5f529ae89: Pull complete 4e54a8bcf566: Pull complete 50c21ba6527b: Pull complete 68e74bb27b39: Pull complete 5f13eadfe747: Pull complete Digest: sha256:97869b42772dac5b767f4e4692434fbd5e6b86bcb8695d4feafb52b59fe9ae24 Status: Downloaded newer image for mysql:5.7 ceb20e33681d239e0e9cde25d9a4eeb713354895f70c8447d2b423ced86b6203 [root@ip-172-29-140-52 test]# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ceb20e33681d mysql:5.7 "docker-entrypoint.s…" 3 seconds ago Up 2 seconds 0.0.0.0:3306->3306/tcp, 33060/tcp mysql a4ce76b48985 nginx "/docker-entrypoint.…" 6 minutes ago Up 6 minutes 80/tcp funny_lederberg [root@ip-172-29-140-52 test]# docker exec -it mysql /bin/bash root@ceb20e33681d:/# ls backup bin boot dev docker-entrypoint-initdb.d entrypoint.sh etc home lib lib64 logs media mnt opt proc root run sbin srv sys tmp usr var root@ceb20e33681d:/# mysql -uroot -pwuerp mysql: [Warning] Using a password on the command line interface can be insecure. Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 2 Server version: 5.7.31 MySQL Community Server (GPL) Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | | sys | +--------------------+ 4 rows in set (0.00 sec) mysql>
成功访问,mysql安装成功
# KEYCLOAK_USER 用户名 # KEYCLOAK_PASSWORD 密码 # DB_ADDR 数据库地址 # DB_PORT数据库端口 # DB_DATABASE 数据库名称 # DB_USER 数据库用户 # DB_PASSWORD 数据库密码 docker run \ -d \ --name keycloak \ -p 8080:8080 \ -p 8443:8443 \ -e KEYCLOAK_USER=admin \ -e KEYCLOAK_PASSWORD=123456 \ -e DB_ADDR=127.0.0.1 \ -e DB_PORT=3306 \ -e DB_DATABASE=keycloak \ -e DB_USER=root \ -e DB_PASSWORD=wuerp \ jboss/keycloak:latest
访问该机器 https://localhost:8443