本文介绍如何使用docker应用封装一个java应用(名字叫cspj)。这个java应用涉及数据持久化以及RMI调用。
docker是一种容器技术,对操作系统、文件系统、网络等进行了封装,使其中的进程可以完整运行。
docker和虚拟机是不同的技术。虚拟机虚拟了一套硬件环境,需要在这个硬件环境之上安装完整的操作系统、jdk等相关软件,才能运行一个java应用,虚拟机和宿主机在操作系统层面就是相互隔离的。以Linux下的docker为例,它使用的依旧是宿主机中的Linux内核,只不过在宿主机的用户层上虚拟了一个容器,这个容器中的Linux只是操作系统中的一小部分,使用的依旧是宿主机的Linux内核。比如宿主机是Ubuntu,docker虚拟的操作系统可以是alpine,这只是虚拟了alpine和Ubuntu不同的部分。
一个docker容器中一般只运行一个应用,这和虚拟机也是不同的。比如我们的一个应用有java应用,有数据库mysql,那么java应用运行在一个容器里,mySql运行在另一个容器里。他们之间可以通过docker虚拟的网络进行交互。
docker中的容器就是运行中的进程。它是通过镜像进行启动的。docker中的镜像就相当于一个模板,启动一个容器就相当于通过模板创建一个可执行的应用。因此,只要镜像不变,所有通过这个镜像创建的容器都是一摸一样的。又因为docker进行了操作系统、文件系统、网络等方面的封装,所以这个镜像就可以在各种不同的环境上运行,从而保证一致的执行效果。
容器运行之后,在其中会有一个可读写层,这是用来临时保存容器中应用在运行中产生的数据的。当这个容器被销毁之后,所保存的数据也就消失了。使用原有的镜像重新运行一个新的容器,就又是一个全新的应用了。
所以,如果我们需要对容器中的数据进行持久化,就需要用到volume或者bind mounts技术。比如我们的java应用中有一个内置文件数据库Derby,如果需要保留对这个文件数据库的修改,同时又不想改变镜像文件,就可以把这个文件数据库使用volume或bind mounts技术保存到宿主机的文件系统中。这样,即使容器被销毁,容器中所修改的文件数据库也会被保留下来。
还有一种方法保存容器中的临时数据,就是使用commit命令把容器可读写层中的临时数据也一起生成一个新的镜像。以后通过这个新镜像运行的容器,就都保留了这部分数据,这部分数据也就成了新镜像的一层,而且无法被修改。通过这个新镜像运行的容器,会生成一个新的可读写层,用来临时保存此次运行中生成的数据。如果一直使用commit保存数据,新的镜像就会越来越大。docker官方不推荐使用这种方法保存数据。
在详细说一下docker的镜像。docker的镜像是使用Dockerfile制作的。Dockerfile是一个脚本,docker build命令会读取这个脚本,按照其指令构造镜像。docker的镜像是一层一层的。每一个Dockerfile指令,都会生成镜像中的一层。
我们自己制作的docker镜像通常不会从最底层开始构建。比如我们要制作一个java应用的镜像,我们就要依赖于openjdk:8-alpine的官方镜像。在这个基础之上,再制作我们的java应用镜像层。而官方的openjdk:8-alpine则是基于alpine操作系统制作的镜像,在这个操作系统之上,它为我们设置好了各种环境变量,我们在这个镜像之上就可以直接制作我们自己的java应用镜像,而不必关心jdk的设置了。
aphine 是一个特别简洁的官方的Linux操作系统系统容器镜像,只有5M大小。从中也可以看出docker和虚拟机的区别,虚拟机中运行的操作系统一定是完整的操作系统,通常都会有几个G的大小。
Intel-Core-i7 CPU, 安装Windows10操作系统,使用VirtualBox安装了CentOS-7虚拟机。我们将在CentOS-7虚拟机上安装Docker。关于如何在安装设置虚拟机,请参看 这里 。
如果要执行8.2节中的实例,必须使用VMWare虚拟机安装CentOS-7系统,因为VMWare支持nested vm。还需要设置vmware虚拟机的处理器中,选择“虚拟化Intel VT-x/EPT或AMD-V/RVI(V)。
如果使用AMD处理器,则可以使用VirtualBox安装CentOS-7,因为最新的VirtualBox-6支持在AMD系统上打开netstad vm。
VirtualBox中安装的CentOS-7系统的IP地址是192.168.56.104.
Docker分为社区版和企业版,我们使用社区版即可。
$ sudo yum remove docker / docker-client / docker-client-latest / docker-common / docker-latest / docker-latest-logrotate / docker-logrotate / docker-engine
# 安装依赖 $ sudo yum install -y yum-utils / device-mapper-persistent-data / lvm2 # 设置国内镜像源 $ sudo yum-config-manager / --add-repo / https://mirrors.ustc.edu.cn/docker-ce/linux/centos/docker-ce.repo # 安装最新版本 $ sudo yum install docker-ce docker-ce-cli containerd.io # 启动 $ sudo systemctl start docker # 验证,或从docker官方下载hello-world镜像并根据镜像运行容器。这个镜像只有不到2K $ sudo docker run hello-world # 把用户添加到docker组中,这样执行docker命令时就不必使用sudo了 $ sudo usermod -aG docker your-user
设置镜像加速器可以加速从Docker Hub获取镜像的速度。在 /etc/docker/daemon.json
文件中(如不存在请新建)添加如下内容:
{ "registry-mirrors": [ "https://dockerhub.azk8s.cn", "https://reg-mirror.qiniu.com" ] }
之后启动服务:
$ sudo systemctl daemon-reload $ sudo systemctl restart docker
更详细的安装方法请参看 Get Docker Engine - Community for CentOS 和 安装 Docker
docker其实是C/S模式的,我们在Linux终端输入的docker命令其实是客户端,后台还有一个服务端在运行。客户端和服务端可以不运行在同一个机器上。
新建一个空目录,把java应用程序放入到这个目录中,并新建Dockerfile文件。这里我们先不考虑临数据库持久化的问题,直接把所有应用程序进行打包:
[eric@centos7min2 cspj-server]$ ll total 4 drwxrwxr-x. 2 eric eric 206 Sep 28 21:53 bin drwxrwxr-x. 2 eric eric 207 Sep 27 16:23 conf drwxrwxr-x. 4 eric eric 92 Sep 29 14:03 database -rw-rw-r--. 1 eric eric 93 Sep 29 14:19 Dockerfile drwxr-xr-x. 3 eric eric 278 Sep 27 16:12 lib
这个java应用程序的启动脚本是bin/startServer.sh,这个脚本中启动命令最后有 &
符号,需要去掉。因为容器中运行的程序都是在前台运行的,如果加上&符号,这个在前台运行的startServer.sh脚本就执行完毕,这个容器也就立即停止了。
bin/setEnv.sh中设定了一些RMI参数,为了可以进行RMI连接,设置其内容如下:
#!/bin/sh export IP=`awk 'END{print $1}' /etc/hosts` echo "$IP cspj-host" >> /etc/hosts cat /etc/hosts export JAVA_EXECUTE=java export CSPJ_LIBPATH=../lib/*.jar export CSPJ_LIBPATH_OPT=../lib/opt/*.jar export CSPJ_CLASSPATH=../conf/ export JVM_OPTARGS="-Xmx1024m -Xms1024m" export CSPJ_OPTARGS="-Dcspj.home=$PWD/../ -Dfile.encoding=UTF-8" export CSPJ_JMXARGS="-Djava.rmi.server.hostname=cspj-host -Dcom.sun.management.jmxremote.port=9998 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false" echo $CSPJ_JMXARGS
其中IP是容器运行时动态获取容器的IP地址,把这个地址写入到/etc/hosts中时为了后续进行RMI连接,设置-Djava.rmi.server.hostname=cspj-host也是为了后续的RMI连接
Dockerfile中内容为:
FROM openjdk:8-alpine COPY . /cspj-server/ WORKDIR /cspj-server/bin CMD ["./startServer.sh"]
在构建镜像时,docker中每条指令都会构建一层,所以如果有RUN命令时,一般把多个操作都写在一行里。
在刚才的目录中,执行 docker build -t ws3495/cspj-server:v1.0.0 .
命令,构建镜像。 :v1.0.0
可以省略,此时默认是 :latest
。注意不要丢掉最后的“.”,它以宿主机的一个文件夹作为"context",Dockerfile中的指令就是基于这个“context”进行构建的。比如这个docker build命令指定了当前路径(/home/eric/dockertest/forbuildimage/cspj-server)为“context”,那么在Dockerfile中, COPY . /cspj-server/ 指令中的“.”指的就是宿主机的/home/eric/dockertest/forbuildimage/cspj-server目录。关于docker build指令可以参考 docker build ,关于上下文可以参考 这里 。
通过执行刚才的命令,其执行过程为:
[eric@centos7min2 cspj-server]$ docker build -t ws3495/cspj-server:v1.0.0 . Sending build context to Docker daemon 32.8MB Step 1/4 : FROM openjdk:8-alpine ---> a3562aa0b991 Step 2/4 : COPY . /cspj-server/ ---> 27361ab40a65 Step 3/4 : WORKDIR /cspj-server/bin ---> Running in aef7152e561a Removing intermediate container aef7152e561a ---> b0fcdabdde69 Step 4/4 : CMD ["./startServer.sh"] ---> Running in 7a11c32dccae Removing intermediate container 7a11c32dccae ---> 4e56f3b72f1d Successfully built 4e56f3b72f1d Successfully tagged ws3495/cspj-server:v1.0.0
通过这个执行过程中每个step,可以看出docker构建镜像时的操作:
通过执行 docker image ls
命令,可以看到刚才构建的镜像:
[eric@centos7min2 cspj-server]$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE ws3495/cspj-server v1.0.0 4e56f3b72f1d 12 minutes ago 137MB ws3495/cspj-server v1.0.1 e5868fe7c123 4 hours ago 132MB openjdk 8-alpine a3562aa0b991 4 months ago 105MB registry latest f32a97de94e1 6 months ago 25.8MB hello-world latest fce289e99eb9 9 months ago 1.84kB prakhar1989/static-site latest f01030e1dcf3 3 years ago 134MB
这个命令输出一个类似表格的结构,第一行是表头。第二行就是我们刚构建的ws3495/cspj-server:v1.0.0,第3行是我们从docker hub上拉取的openjdk:8-alpine镜像。
执行命令 docker run -d -p 27449:27449 -p 27450:27450 ws3495/cspj-server:v1.0.0
,依据刚才制作的镜像,启动一个容器:
[eric@centos7min2 cspj-server]$ docker run -d / > -p 27449:27449 -p 27450:27450 / > ws3495/cspj-server:v1.0.0 47ed8277b0e0bfbb90a798a8b5499a0ee693499fd2342615388248ad72e932ab
命令执行结束后,返给我们一个字符串,这个串就是这个刚刚启动的容器的ID。由于我们使用了 -d
参数,所以这个容器在后台运行。
命令中的 -p <host port>:<container port>
参数把容器中的端口和宿主机中的端口进行了映射。外部访问host port的连接就会被转发到这个容器的container port上。
使用 docker logs id
指令可以查看容器的日志,id仅需前几位即可:
[eric@centos7min2 cspj-server]$ docker logs 47ed827 127.0.0.1 localhost ::1 localhost ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters 172.17.0.2 47ed8277b0e0 172.17.0.2 cspj-host -Djava.rmi.server.hostname=cspj-host -Dcom.sun.management.jmxremote.port=9998 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false Starting CSPJ Server... 2019-09-29_10:35:33.263[0000]CSPJ Server Process ID:10 2019-09-29_10:35:33.275[0000]CSPJ Server Version:CSPJ_V1.3.7.1 Build:2019-08-03 10:54:51 2019-09-29_10:35:33.276[0000]系统日志初始化成功[/cspj-server/log/syslog.trace] 2019-09-29_10:35:33.277[0000]平台主目录:/cspj-server 2019-09-29_10:35:33.277[0000]平台配置信息主目录:/cspj-server/conf ... 2019-09-29_10:35:37.587[0000]终端[RMI Registry]监听端口[27449]数据端口[27450] 2019-09-29_10:35:37.587[0000]终端守护进程启动成功 2019-09-29_10:35:37.587[0000]初始化交易主控入口…… 2019-09-29_10:35:37.589[0000]交易主控入口初始化成功[DefaultTransactionInvoker] 2019-09-29_10:35:37.589[0000]启动通讯口岸…… 2019-09-29_10:35:37.599[0000]启动通讯口岸:SocketPortal[NIO] 2019-09-29_10:35:37.599[0000]通讯口岸启动成功 2019-09-29_10:35:37.649[0000]CSPJ Server 启动成功
使用命令 docker ps
可以查看正在运行的容器:
[eric@centos7min2 cspj-server]$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 47ed8277b0e0 ws3495/cspj-server:v1.0.0 "./startServer.sh" 53 minutes ago Up 53 minutes 0.0.0.0:27449-27450->27449-27450/tcp jolly_shockley
对于这个运行中的容器,可以使用命令 docker exec -it id sh
,进入这个容器进行查看和修改:
[eric@centos7min2 cspj-server]$ docker exec -it 47ed827 sh /cspj-server/bin # ls SecInputKey.sh derby.log jmxremote.password serverStatus.sh startServer.sh transform.sh codeinfo.sh ij.sh pwdgen.sh setEnv.sh stopServer.sh /cspj-server/bin # cd ../log /cspj-server/log # ls error.trace root.trace syslog.trace trace /cspj-server/bin #
此时就进入到了容器里,在里面对容器中的内容进行修改,就会写入到容器的可读写层(应该是最上层)。之后再执行 docker restart <id>
时,这个容器的修改不会消失。只有在使用 docker rm <id>
命令删除这个容器时,所有临时存储的文件就会消失。或者再使用 docker run ... ws3495/cspj-server:v1.0.0
命令运行一个新容器时,这个容器中所作的修改也不会被新容器知道。
在我们的windows 10系统上,修改 C:/Windows/System32/drivers/etc/hosts
文件,添加一行 192.168.56.104 cspj-host
,其中192.168.56.104是CentOS-7虚拟机的IP。就可以使用IDE(RMI连接的客户端工具)连接了:
其原理是:
现在这个集群共有3个IP地址:
由于docker启动时设置了 -p 27449:27449 -p 27450:27450
参数,所有发送到IP2:27449和IP2:27450的信息都会被转发到IP3:27449和IP3:27450。这两个端口是我们设置的RMI提供服务的端口。
在docker容器中启动的java应用(RMI服务端,IP3)在启动时设置了-Djava.rmi.server.hostname=cspj-host选项,当客户端使用rmi方式连接到docker容器中的进程时,容器中的进程会向客户端返回一个本机的 cspj-host
参数标定服务端所在的地址(已在docker中的 /etc/hosts
中设置了 IP3 cspj-host
(参看5.1节中的docker logs指令的输出结果))。客户端会从本机的 hosts
文件中查找 cspj-host
所在的地址。
客户端需要以RMI方式连接到IP3上时,需要通过IP2进行中转,所以在RMI客户端所在的机器上,需要在 hosts
文件中设置 IP2 cspj-host
,使客户端去IP2:27449获取RMI服务。又由于IP2会把所有27449端口的数据包都转发到IP3:27449,所以就会最终找到真正的RMI服务。
当有更多的IP对数据包进行转发时,也是一样的,客户端需要设置hosts中cspj-host为IP2所在的地址。
注意: 连接过程中可能会碰到NoSuchObject的异常,需要多试几次。或者在IP2上启动一个cspj,IDE连接上之后,再关闭IP2上的cspj,然后再试
通过5.2节图中的界面,我们可以修改Derby数据库文件。主要修改内容是在容器中开放18000端口,所有向这个端口发送的数据,都会收到一个返回信息,信息中标明这个容器的IP地址。修改之后,使用 docker commit <id> ws3495/cspj-server:tmp
命令,把这个容器存为一个新镜像ws3495/cspj-server:tmp
[eric@centos7min2 cspj-server]$ docker commit 47ed8277b0e0 ws3495/cspj-server:tmp sha256:ae4f6b8ecf435b714c331372d93c96bbb56460469bb9dd06e4e0f93faa8659a8 [eric@centos7min2 cspj-server]$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE ws3495/cspj-server tmp ae4f6b8ecf43 9 seconds ago 141MB ws3495/cspj-server v1.0.0 4e56f3b72f1d About an hour ago 137MB ws3495/cspj-server v1.0.1 e5868fe7c123 5 hours ago 132MB openjdk 8-alpine a3562aa0b991 4 months ago 105MB registry latest f32a97de94e1 6 months ago 25.8MB hello-world latest fce289e99eb9 9 months ago 1.84kB prakhar1989/static-site latest f01030e1dcf3 3 years ago 134MB
可以看出,commit生成的镜像tmp比原始镜像v1.0.0镜像大了不少。
使用命令 docker stop
命令停掉现在这个容器,再使用 docker ps -a
命令,可以看到其状态为Exited(stop的容器可以使用 docker start
命令再启动,其修改不会丢失):
[eric@centos7min2 cspj-server]$ docker stop 47ed8277b0e0 47ed8277b0e0 [eric@centos7min2 cspj-server]$ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 47ed8277b0e0 ws3495/cspj-server:v1.0.0 "./startServer.sh" 58 minutes ago Exited (137) 31 seconds ago jolly_shockley
我们根据刚才commit的镜像,启动一个新容器,这次把18000端口也映射到宿主机:
[eric@centos7min2 cspj-server]$ docker run -d / > -p 27449:27449 -p 27450:27450 -p 18000:18000 / > ws3495/cspj-server:tmp f211f0844f78dc38460260c34b7e9cb7f9ae8245c7d246e35e7be7cf4d3ba23c
新运行的容器和刚才那个容器的ID是不一样的。使用IDE连接到这个新容器上,可以看到刚才在那个容器中所作的修改,这里都存在。
在windows 10上,使用telnet连接到CentOS-7的18000端口,发送一段数据,可以看到返回信息:
其中的ip addr is 172.17.0.2即容器中java应用返回信息。
v1.0.0镜像启动了一个容器47ed8277b0e0,可不可以在这个运行的容器上再用类似 -p
的参数映射出一个端口呢?根据 How do I assign a port mapping to an existing Docker container? 这个答案,需要修改docker守护进程的配置文件,不是一个很好的解决方案。
commit会使镜像不断增大。可以使用bind mounts技术,在 docker run
命令中,使用 --mount type=bind,source=<host dir>,target=/cspj-server/database
参数,把宿主机上的一个文件夹挂载到容器中。我们的java应用所有数据库修改都是修改/cspj-server/database目录,这样对数据库的修改就可以保存下来,即使容器被删除了,对数据库的修改也不会消失。不过要注意,<host dir>中因该包含我们java应用中database目录所需的一些基础文件:
[eric@centos7min2 cspj-server]$ ll database/ total 20 drwxrwxr-x. 2 eric eric 97 Sep 29 14:03 log -rw-rw-r--. 1 eric eric 608 Sep 29 14:03 README_DO_NOT_TOUCH_FILES.txt drwxrwxr-x. 2 eric eric 8192 Sep 29 14:03 seg0 -rw-rw-r--. 1 eric eric 1003 Sep 29 14:03 service.properties
或者使用外置的数据库,不要和java应用集成在一起。例如连接到外部的oracle数据库;或者启动一个mySQL的docker container,使用 docker-compose
工具把他们关联到一起。详情请参看“容器集群”一节
如果无法连接Docker Hub,我们可以搭建私有仓库。使用官方镜像registry即可搭建:
# 搭建本地的registry,默认在/etc/lib/registry中 [eric@centos7min2 cspj-server]$ docker run -d -p 5000:5000 --restart=always --name registry registry 821497a1688646027389b8c3547ab3e321e8df1c1fa442987506b3b5784de52e # 查看本地registry上存在的镜像 [eric@centos7min2 cspj-server]$ curl 127.0.0.1:5000/v2/_catalog {"repositories":[]} # 使用docker tag标记一个到127.0.0.1:5000的镜像 [eric@centos7min2 cspj-server]$ docker tag ws3495/cspj-server:tmp 127.0.0.1:5000/ws3495/cspj-server:tmp [eric@centos7min2 cspj-server]$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE 127.0.0.1:5000/ws3495/cspj-server tmp ae4f6b8ecf43 About an hour ago 141MB ws3495/cspj-server tmp ae4f6b8ecf43 About an hour ago 141MB ws3495/cspj-server v1.0.0 4e56f3b72f1d 3 hours ago 137MB ws3495/cspj-server v1.0.1 e5868fe7c123 6 hours ago 132MB openjdk 8-alpine a3562aa0b991 4 months ago 105MB registry latest f32a97de94e1 6 months ago 25.8MB hello-world latest fce289e99eb9 9 months ago 1.84kB prakhar1989/static-site latest f01030e1dcf3 3 years ago 134MB # 推送这个镜像 [eric@centos7min2 cspj-server]$ docker push 127.0.0.1:5000/ws3495/cspj-server:tmp The push refers to repository [127.0.0.1:5000/ws3495/cspj-server] e907982060bf: Pushed 4c70c37a74c9: Pushed ceaf9e1ebef5: Pushed 9b9b7f3d56a0: Pushed f1b5933fe4b5: Pushed tmp: digest: sha256:588c372c92e3909fb280311d273be512dfc5641eb66e0514b9c90c9db314dcb4 size: 1369 # 查看结果 [eric@centos7min2 cspj-server]$ curl 127.0.0.1:5000/v2/_catalog {"repositories":["ws3495/cspj-server"]} # 重新pull [eric@centos7min2 cspj-server]$ docker image rm 127.0.0.1:5000/ws3495/cspj-server:tmp Untagged: 127.0.0.1:5000/ws3495/cspj-server:tmp Untagged: 127.0.0.1:5000/ws3495/cspj-server@sha256:588c372c92e3909fb280311d273be512dfc5641eb66e0514b9c90c9db314dcb4 [eric@centos7min2 cspj-server]$ docker pull 127.0.0.1:5000/ws3495/cspj-server:tmp tmp: Pulling from ws3495/cspj-server Digest: sha256:588c372c92e3909fb280311d273be512dfc5641eb66e0514b9c90c9db314dcb4 Status: Downloaded newer image for 127.0.0.1:5000/ws3495/cspj-server:tmp 127.0.0.1:5000/ws3495/cspj-server:tmp [eric@centos7min2 cspj-server]$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE ws3495/cspj-server tmp ae4f6b8ecf43 About an hour ago 141MB 127.0.0.1:5000/ws3495/cspj-server tmp ae4f6b8ecf43 About an hour ago 141MB ws3495/cspj-server v1.0.0 4e56f3b72f1d 3 hours ago 137MB ws3495/cspj-server v1.0.1 e5868fe7c123 6 hours ago 132MB openjdk 8-alpine a3562aa0b991 4 months ago 105MB registry latest f32a97de94e1 6 months ago 25.8MB hello-world latest fce289e99eb9 9 months ago 1.84kB prakhar1989/static-site latest f01030e1dcf3 3 years ago 134MB
更多搭建私有仓库的方法请参看 私有仓库
一个container(容器)只做一件事情,一个container中只运行一个程序。如果我们的应用由好几个部分组成,应该如何把它们组织到一起呢?比如一个应用可以分为server(逻辑处理)、db(数据库操作)、monitor(监控)等几部分,一般会把这几部分分别制作成镜像,启动到不同的container中。怎么把它们组成一个完整的可以对外提供服务的应用呢?如果需要多个应用,如何进行负载均衡呢?
这里就要用到集群管理。Docker自带一个集群管理工具Swarm(蜂群),使用它可以解决我们刚才提出的问题。
使用Swarm,先要明确与之相关的一些概念:
docker命令中的 node
、 service
、 stack
指令,都必须在swarm集群环境下使用。
我们前面创建了ws3495/cspj-server:tmp镜像。它对外提供一个服务,对任意发送到18000端口的请求,返回一个容器的IP地址。 IDE工具可以用RMI方式连接到这个容器,对容器的数据进行操作。
现在需要实现对这个服务的负载均衡。我们需要根据这个镜像启动多个容器,让它们共同对外提供更加稳定可靠的服务。
在任意位置创建一个 compose文件 docker-compose.yml
:
version: "3" services: server: image: ws3495/cspj-server:tmp deploy: replicas: 2 resources: limits: cpus: "0.5" memory: 1024M restart_policy: condition: on-failure ports: # <host port> : <container port> - "27449:27449" - "27450:27450" - "18000:18000" networks: - cspjnet networks: cspjnet:
swarm中的节点(node)分为manager和worker。我们需要初始化一个manager,然后把其它节点作为worker加入进来。这里我们只有一个机器(CentOS-7),所以我们只创建一个manager节点。
执行命令 docker swarm init --advertise-addr 192.168.56.104
创建manager节点:
[eric@centos7min2 swarm]$ docker swarm init --advertise-addr 192.168.56.104 Swarm initialized: current node (eby778btfq9hvpzn62gnnv5ux) is now a manager. To add a worker to this swarm, run the following command: docker swarm join --token SWMTKN-1-4famuxyeqheaiiip2rp2vyk7fuq5v4csvn4432w2czbm7ctov2-6yjw4idxlrdy4vjbknd4d1gdg 192.168.56.104:2377 To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
这个命令会创建几个network,用于集群管理。ingress和none就是swarm在这个节点上创建的,docker_gwbridge可能也是:
[eric@centos7min2 swarm]$ docker network ls NETWORK ID NAME DRIVER SCOPE 015532cb540e bridge bridge local a4025c27d82d docker_gwbridge bridge local 20ce1819213b host host local 5fnz5sfhkka7 ingress overlay swarm ec1e8f535e07 none null local
可以执行一些检查,看一看这个节点现在的状态:
# swarm中只有一个节点,并且是manager [eric@centos7min2 swarm]$ docker node ls ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION eby778btfq9hvpzn62gnnv5ux * centos7min2 Ready Active Leader 19.03.2 # 还没有stack [eric@centos7min2 swarm]$ docker stack ls NAME SERVICES ORCHESTRATOR # 也没有service [eric@centos7min2 swarm]$ docker service ls ID NAME MODE REPLICAS IMAGE PORTS
执行命令 docker stack deploy -c docker-compose.yml cspj
,swarm就可以根据docker-compose.yml文件中的配置,自动部署并启动应用。我们给这个stack命名为cspj。
# 部署应用。会根据yml文件中的设置,创建虚拟网络cspj_cspjnet,使用ws3495/cspj-server:tmp镜像启动一个服务cspj_server [eric@centos7min2 swarm]$ docker stack deploy -c docker-compose.yml cspj Creating network cspj_cspjnet Creating service cspj_server # swarm又新建了一个cspj_cspjnet的虚拟网络 [eric@centos7min2 swarm]$ docker network ls NETWORK ID NAME DRIVER SCOPE 015532cb540e bridge bridge local d5a7fdih7h92 cspj_cspjnet overlay swarm a4025c27d82d docker_gwbridge bridge local 20ce1819213b host host local 5fnz5sfhkka7 ingress overlay swarm ec1e8f535e07 none null local # 查看所有stack。现在只有1个,名字叫cspj,里面有1个service [eric@centos7min2 swarm]$ docker stack ls NAME SERVICES ORCHESTRATOR cspj 1 Swarm # 查看所有service。输出信息表明cspj_server这个service中有两个task(REPLICAS),并且都已经启动了 [eric@centos7min2 swarm]$ docker service ls ID NAME MODE REPLICAS IMAGE PORTS uq82928el14d cspj_server replicated 2/2 ws3495/cspj-server:tmp *:18000->18000/tcp, *:27449-27450->27449-27450/tcp # 查看cspj_server这个service中的task。swarm自动为每个task进行了编号 [eric@centos7min2 swarm]$ docker service ps cspj_server ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS j7kjtj81re1o cspj_server.1 ws3495/cspj-server:tmp centos7min2 Running Running 5 minutes ago 1elk64sfezfo cspj_server.2 ws3495/cspj-server:tmp centos7min2 Running Running 5 minutes ago # 按照普通方式查看container,发现ID和service ps中显示的ID不一样。为什么? [eric@centos7min2 swarm]$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8d9f67c15cb6 ws3495/cspj-server:tmp "./startServer.sh" 21 minutes ago Up 21 minutes 27449-27450/tcp cspj_server.2.1elk64sfezfoql0ex1njwjlnp dee88b746baf ws3495/cspj-server:tmp "./startServer.sh" 21 minutes ago Up 21 minutes 27449-27450/tcp cspj_server.1.j7kjtj81re1o1ge5anwnlmzka 821497a16886 registry "/entrypoint.sh /etc…" 24 hours ago Up 24 hours 0.0.0.0:5000->5000/tcp registry
在Windows 10上,向CentOS-7的18000端口发送数据,可以看到负载均衡的效果:
IDE也可以正常连接。
使用 docker stack rm cspj
关闭并移除cspj这个task
# 关闭stack [eric@centos7min2 swarm]$ docker stack rm cspj Removing service cspj_server Removing network cspj_cspjnet # stack已被移除 [eric@centos7min2 swarm]$ docker stack ls NAME SERVICES ORCHESTRATOR # service已被移除 [eric@centos7min2 swarm]$ docker service ls ID NAME MODE REPLICAS IMAGE PORTS # cspj_cspjnet已被移除 [eric@centos7min2 swarm]$ docker network ls NETWORK ID NAME DRIVER SCOPE 015532cb540e bridge bridge local a4025c27d82d docker_gwbridge bridge local 20ce1819213b host host local 5fnz5sfhkka7 ingress overlay swarm ec1e8f535e07 none null local # task已被关闭并移除 [eric@centos7min2 swarm]$ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 821497a16886 registry "/entrypoint.sh /etc…" 24 hours ago Up 24 hours 0.0.0.0:5000->5000/tcp registry
使用命令 docker swarm leave --force
移除swarm集群(即把最后一个node从swarm集群中移除掉)。之后, docker node
, docker stack
, docker service
这些命令就不可用了:
# 关闭swarm [eric@centos7min2 compose]$ docker swarm leave --force Node left the swarm.
本节使用docker-machine工具创建多个虚拟docker node,并把它们都用swarm组织成一个集群。同时,我们再在docker-compose.yml文件中添加一个service,使多个service共同工作。操作平台是CentOS-7。docker-machine可以完全独立使用,不必同时安装docker,docker-machine创建的虚拟机中就有docker服务。
由于docker-machine创建虚拟机需要先安装virtualbox,而目前版本的virtualbox(6.0)仅能在AMD的CPU上支持嵌套的虚拟机,所以在本节中我们使用vmware workstation pro 15(有30天免费试用期,或者使用vmware workstation palyer),在这个虚拟机上安装CentOS-7,然后再在CentOS-7上安装docker-machine,docker-machine再创建基于virtualbox的虚拟机。
新搭建的CentOS-7系统的IP地址是192.168.154.100.
docker-machine 可以快速部署带有docker服务的虚拟机。
在CentOS-7上使用docker-machine需要先安装virtual-box。新建 /etc/yum.repos.d/virtualbox.repo
文件,内容如下:
[virtualbox] name=Oracle Linux / RHEL / CentOS-$releasever / $basearch - VirtualBox baseurl=http://download.virtualbox.org/virtualbox/rpm/el/$releasever/$basearch enabled=1 gpgcheck=1 repo_gpgcheck=1 gpgkey=https://www.virtualbox.org/download/oracle_vbox.asc
然后执行如下命令安装virtualbox。安装成功之后,执行 sudo systemctl status vboxdrv
,可以查看virtualbox的状态:
sudo yum update
,会更新所有软件,可以不执行。如果执行,需在执行后 重启系统 。 yum install -y kernel-devel kernel-headers gcc make perl
,之后完后可能需要 重启系统 sudo yum install VirtualBox-6.0
[eric@vmwmin1 ~]$ sudo systemctl status vboxdrv ● vboxdrv.service - VirtualBox Linux kernel module Loaded: loaded (/usr/lib/virtualbox/vboxdrv.sh; enabled; vendor preset: disabled) Active: active (exited) since Wed 2019-10-02 00:23:22 CST; 23min ago Process: 822 ExecStart=/usr/lib/virtualbox/vboxdrv.sh start (code=exited, status=0/SUCCESS) Oct 02 00:19:51 vmwmin1 systemd[1]: Starting VirtualBox Linux kernel module... Oct 02 00:19:54 vmwmin1 vboxdrv.sh[822]: vboxdrv.sh: Starting VirtualBox services. Oct 02 00:19:54 vmwmin1 vboxdrv.sh[855]: Starting VirtualBox services. Oct 02 00:19:54 vmwmin1 vboxdrv.sh[822]: vboxdrv.sh: Building VirtualBox kernel modules. Oct 02 00:19:54 vmwmin1 vboxdrv.sh[860]: Building VirtualBox kernel modules. Oct 02 00:23:22 vmwmin1 systemd[1]: Started VirtualBox Linux kernel module.
安装docker-machine:
$ base=https://github.com/docker/machine/releases/download/v0.16.0 && curl -L $base/docker-machine-$(uname -s)-$(uname -m) >/tmp/docker-machine && sudo mv /tmp/docker-machine /usr/local/bin/docker-machine && chmod +x /usr/local/bin/docker-machine
安装成功后,执行 docker-machine ls
,可以看到还不存在由docker-machine创建的虚拟机。
使用 docker-machine create --driver virtualbox <vm-name>
可以直接创建带有docker服务的虚拟机,不必事先安装docker。这里我们创建两个虚拟机,分别为 myvm1
和 myvm2
:
# 创建myvm1,由于是第一次执行,会从github上下载一些文件 [eric@vmwmin1 ~]$ docker-machine create --driver virtualbox myvm1 Running pre-create checks... (myvm1) Image cache directory does not exist, creating it at /home/eric/.docker/machine/cache... (myvm1) No default Boot2Docker ISO found locally, downloading the latest release... (myvm1) Latest release for github.com/boot2docker/boot2docker is v18.09.9 (myvm1) Downloading /home/eric/.docker/machine/cache/boot2docker.iso from https://github.com/boot2docker/boot2docker/releases/download/v18.09.9/boot2docker.iso... (myvm1) 0%....10%....20%....30%....40%....50%....60%....70%....80%....90%....100% Creating machine... (myvm1) Copying /home/eric/.docker/machine/cache/boot2docker.iso to /home/eric/.docker/machine/machines/myvm1/boot2docker.iso... (myvm1) Creating VirtualBox VM... (myvm1) Creating SSH key... (myvm1) Starting the VM... (myvm1) Check network to re-create if needed... (myvm1) Found a new host-only adapter: "vboxnet0" (myvm1) Waiting for an IP... Waiting for machine to be running, this may take a few minutes... Detecting operating system of created instance... Waiting for SSH to be available... Detecting the provisioner... Provisioning with boot2docker... Copying certs to the local machine directory... Copying certs to the remote machine... Setting Docker configuration on the remote daemon... Checking connection to Docker... Docker is up and running! To see how to connect your Docker Client to the Docker Engine running on this virtual machine, run: docker-machine env myvm1 # 创建myvm2,省略了一些输出内容 [eric@vmwmin1 ~]$ docker-machine create --driver virtualbox myvm2 ... Docker is up and running! To see how to connect your Docker Client to the Docker Engine running on this virtual machine, run: docker-machine env myvm2 # 查看这两个虚拟机 [eric@vmwmin1 ~]$ docker-machine ls NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS myvm1 - virtualbox Running tcp://192.168.99.100:2376 v18.09.9 myvm2 - virtualbox Running tcp://192.168.99.101:2376 v18.09.9
本文开始时我们使用的CentOS-7系统的地址是192.168.56.104,我们在这个CentOS-7系统上使用docker搭建个了一个私有仓库。现在我们又使用vmware新搭建了一个CentOS-7系统,其地址是192.168.154.100,在这个系统上使用docker-machine创建了两个虚拟机myvm1和myvm2。我们需要使myvm1和myvm2可以访问这个私有仓库,所以需要对myvm1和myvm2进行一些配置,使其可以以不安全的方式访问私有仓库。
通过执行命令 docker-machine scp <filename> <your-machine-name>:<path>
会把文件拷贝到对应的虚拟机中。
通过执行命令 docker-machine ssh <your-machine-name> "<your-docker-command>"
可以直接在虚拟机中执行命令。如果省略 ""
中的内容,就可以以ssh方式连接到虚拟机中。
在docker-machine所在的CentOS-7系统上,在任意位置新建一个文件 daemon.json
,内容为:
{ "insecure-registries": [ "192.168.56.104:5000" ] }
192.168.56.104是私有仓库所在的地址。
把这个文件拷贝到myvm1和myvm2的/etc/docker/目录下:
# 拷贝文件 $ docker-machine scp daemon.json myvm1:~ $ docker-machine scp daemon.json myvm2:~ $ docker-machine ssh myvm1 "sudo mv daemon.json /etc/docker/" $ docker-machine ssh myvm2 "sudo mv daemon.json /etc/docker/" # 重启 [eric@vmwmin1 ~]$ docker-machine restart myvm1 myvm2 Restarting "myvm2"... Restarting "myvm1"... (myvm2) Check network to re-create if needed... (myvm2) Waiting for an IP... Waiting for SSH to be available... (myvm1) Check network to re-create if needed... (myvm1) Waiting for an IP... Waiting for SSH to be available... Detecting the provisioner... Detecting the provisioner... Restarted machines may have new IP addresses. You may need to re-run the `docker-machine env` command. # 查看重启后状态 [eric@vmwmin1 ~]$ docker-machine ls NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS myvm1 - virtualbox Running tcp://192.168.99.102:2376 v18.09.9 myvm2 - virtualbox Running tcp://192.168.99.103:2376 v18.09.9
配置好daemon.json之后,myvm1和myvm2就可以以不安全的方式(HTTP)访问私有仓库了。
使用swarm命令,把这两个节点加入到swarm集群中:
# 初始化myvm1,会自动设置myvm1为manager [eric@vmwmin1 ~]$ docker-machine ssh myvm1 "docker swarm init --advertise-addr 192.168.99.102" Swarm initialized: current node (tjpk0hxlhij9v77yh30ehnzkg) is now a manager. To add a worker to this swarm, run the following command: docker swarm join --token SWMTKN-1-11sad8xx5hp9tyt9oed3gdgzx9ma7lfkk2chm0l8hi3mc0we2s-0bjuixv1bpsvryjsizppfr7bz 192.168.99.102:2377 To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions. # 根据上一个输出的提示,把myvm2加入到swarm中 [eric@vmwmin1 ~]$ docker-machine ssh myvm2 "docker swarm join --token SWMTKN-1-11sad8xx5hp9tyt9oed3gdgzx9ma7lfkk2chm0l8hi3mc0we2s-0bjuixv1bpsvryjsizppfr7bz 192.168.99.102:2377" This node joined a swarm as a worker. # 查看swarm中的节点,*标记的myvm1是manager [eric@vmwmin1 ~]$ docker-machine ssh myvm1 "docker node ls" ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION tjpk0hxlhij9v77yh30ehnzkg * myvm1 Ready Active Leader 18.09.9 w46api0hvap58ghhe3spdd9i7 myvm2 Ready Active 18.09.9
新建一个 docker-compose2.yml
,其内容为:
version: "3" services: server: image: 192.168.56.104:5000/ws3495/cspj-server:tmp deploy: replicas: 2 resources: limits: cpus: "0.5" memory: 1024M restart_policy: condition: on-failure ports: # <host port> : <container port> - "27449:27449" - "27450:27450" - "18000:18000" networks: - cspjnet visualizer: image: 192.168.56.104:5000/dockersamples/visualizer:stable ports: - "8080:8080" volumes: - "/var/run/docker.sock:/var/run/docker.sock" deploy: placement: constraints: [node.role == manager] networks: - cspjnet networks: cspjnet:
这个compose文件中有两个service,一个是我们的java应用server;另一个是visualizer,这是一个可以通过浏览器观察swarm节点状态的镜像。我们已经提前把它们push到私有仓库了(visualizer镜像也可以直接从Docker Hub中获取)。
把这个compose文件拷贝到myvm1上(必须是manager节点,不能是worker节点),就可以部署了:
# 把docker-compose2.yml拷贝到myvm1的~目录下 [eric@vmwmin1 ~]$ docker-machine scp docker-compose2.yml myvm1:~ docker-compose2.yml 100% 693 405.5KB/s 00:00 # 向myvm1虚拟机发送指令,进行service部署 [eric@vmwmin1 ~]$ docker-machine ssh myvm1 "docker stack deploy -c docker-compose2.yml cspj" Creating network cspj_cspjnet Creating service cspj_server Creating service cspj_visualizer # 查看新部署的stack [eric@vmwmin1 ~]$ docker-machine ssh myvm1 "docker stack ls" NAME SERVICES ORCHESTRATOR cspj 2 Swarm # 查看新部署的service,可以看到现在是部署并启动了2个service,其中cspj_server启动了2份,cspj_visualizer启动了1份 [eric@vmwmin1 ~]$ docker-machine ssh myvm1 "docker service ls" ID NAME MODE REPLICAS IMAGE PORTS lo86uqnj2unu cspj_server replicated 2/2 192.168.56.104:5000/ws3495/cspj-server:tmp *:18000->18000/tcp, *:27449-27450->27449-27450/tcp 89d5hype8ert cspj_visualizer replicated 1/1 192.168.56.104:5000/dockersamples/visualizer:stable *:8080->8080/tcp # CentOS-7系统上(192.168.154.100),所有发送到8080端口的数据包都会被转发到myvm1(192.168.99.102)的8080端口 [eric@vmwmin1 ~]$ sudo firewall-cmd --list-forward-ports port=8080:proto=tcp:toport=:toaddr=192.168.99.102 port=18000:proto=tcp:toport=:toaddr=192.168.99.102 port=27449:proto=tcp:toport=:toaddr=192.168.99.102 port=27450:proto=tcp:toport=:toaddr=192.168.99.102
使用浏览器连接 http://192.168.154.100:8080/ ,可以看到swarm中节点和service的状态:
通过使用命令 docker-machine ssh myvm1 "docker stack rm cspj"
关闭并删除cspj这个stack,会同时停止并删除server和visualizer这两个service,会同时停止并删除cspj_server.1, cspj_server.2, cspj_visualizer这3个docker容器。
通过使用命令 docker-machine stop myvm1 myvm2
和 docker-machine rm myvm1 myvm2
停止并删除这两个虚拟机。
执行 docker-machine env myvm1
,按照其输出结果的提示,执行 eval $(docker-machine env myvm1)
,可以把myvm1设置为active。此时可以在CentOS-7系统上直接执行docker命令即会向myvm1发送执行,而不必通过 docker-machine ssh myvm1 "<command>"
向myvm1发送指令了。有兴趣可以自行尝试。
docker为应用部署提供了极大方便,镜像设置好之后,可以在任何地方快速部署,保证一样的执行效果。docker的镜像尽量把每个功能拆分出来,使多个镜像组成stack共同对外提供服务。如果需要数据持久化,可以使用volume功能把数据存储在宿主机上。volume功能也可以在多个service之间共享存储数据。
docker-machine提供了便捷搭建虚拟机,便捷管理虚拟机的能力,使得集群的管理更加方便。
docker中还有很多地方值得探索,比如如何使用config设置配置文件,如何搭建HTTPS方式的私有仓库,docker的底层工作机制是如何实现的。后续会进一步对这些内容进行分析。