使用docker快速部署微服务项目
前言 一个典型的微服务系统架构如图所示:
从图中可以看到,这里面有大量的第三方服务,如:nacos,MySQL,Minio,Redis、Elasticsearch等,要想快速完整的部署实施一套系统,以传统的裸机安装的方式,保守估计也得1-2天左右才能部署完成。但如果使用docker进行部署,可能10分钟都用不了,整个项目就部署完成了,可以节省很多不必要的时间。
Docker是什么 能快速节省部署时间的这个神器,就是docker。看官网的介绍:https://www.docker.com/resources/what-container
docker的logo非常形象的表达了它的定位。
可能看官网的介绍还是有点模糊,我试着用简单的语言来描述一下:使用docker,可以将程序与程序的运行时环境打包为一个服务进行部署。正如它的logo所表示的意思一样,上面的一个个小集装箱就是我们自己的服务,而docker就是承载这个众多服务的基石。
Docker基本概念
注:以下内容来自于 https://yeasy.gitbook.io/docker_practice/,强烈推荐阅读。
镜像 介绍 Docker 镜像 是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像 不包含 任何动态数据,其内容在构建之后也不会被改变。
分层存储 因为镜像包含操作系统完整的 root
文件系统,其体积往往是庞大的,因此在 Docker 设计时,就充分利用 Union FS 的技术,将其设计为分层存储的架构。所以严格来说,镜像并非是像一个 ISO
那样的打包文件,镜像只是一个虚拟的概念,其实际体现并非由一个文件组成,而是由一组文件系统组成,或者说,由多层文件系统联合组成。
镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。
分层存储的特征还使得镜像的复用、定制变的更为容易。甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。
容器 镜像(Image
)和容器(Container
)的关系,就像是面向对象程序设计中的 类
和 实例
一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的 命名空间 。因此容器可以拥有自己的 root
文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。也因为这种隔离的特性,很多人初学 Docker 时常常会混淆容器和虚拟机。
前面讲过镜像使用的是分层存储,容器也是如此。每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为 容器存储层 。
容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。
按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用 数据卷(Volume) 、或者 绑定宿主目录 ,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。
数据卷的生存周期独立于容器,容器消亡,数据卷不会消亡。因此,使用数据卷后,容器删除或者重新运行之后,数据却不会丢失。
仓库 介绍 镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry 就是这样的服务。
一个 Docker Registry 中可以包含多个 仓库 (Repository
);每个仓库可以包含多个 标签 (Tag
);每个标签对应一个镜像。
通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 <仓库名>:<标签>
的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest
作为默认标签。
以 Ubuntu 镜像 为例,ubuntu
是仓库的名字,其内包含有不同的版本标签,如,16.04
, 18.04
。我们可以通过 ubuntu:16.04
,或者 ubuntu:18.04
来具体指定所需哪个版本的镜像。如果忽略了标签,比如 ubuntu
,那将视为 ubuntu:latest
。
仓库名经常以 两段式路径 形式出现,比如 jwilder/nginx-proxy
,前者往往意味着 Docker Registry 多用户环境下的用户名,后者则往往是对应的软件名。但这并非绝对,取决于所使用的具体 Docker Registry 的软件或服务。
Docker Registry 公开服务 Docker Registry 公开服务是开放给用户使用、允许用户管理镜像的 Registry 服务。一般这类公开服务允许用户免费上传、下载公开的镜像,并可能提供收费服务供用户管理私有镜像。
最常使用的 Registry 公开服务是官方的 Docker Hub ,这也是默认的 Registry,并拥有大量的高质量的 官方镜像 。除此以外,还有 Red Hat 的 Quay.io ;Google 的 Google Container Registry ,Kubernetes 的镜像使用的就是这个服务;代码托管平台 GitHub 推出的 ghcr.io 。
由于某些原因,在国内访问这些服务可能会比较慢。国内的一些云服务商提供了针对 Docker Hub 的镜像服务(Registry Mirror
),这些镜像服务被称为 加速器 。常见的有 阿里云加速器 、DaoCloud 加速器 等。使用加速器会直接从国内的地址下载 Docker Hub 的镜像,比直接从 Docker Hub 下载速度会提高很多。
国内也有一些云服务商提供类似于 Docker Hub 的公开服务。比如 网易云镜像服务 、DaoCloud 镜像市场 、阿里云镜像库 等。
私有 Docker Registry 除了使用公开服务外,用户还可以在本地搭建私有 Docker Registry。Docker 官方提供了 Docker Registry 镜像,可以直接使用做为私有 Registry 服务。
开源的 Docker Registry 镜像只提供了 Docker Registry API 的服务端实现,足以支持 docker
命令,不影响使用。但不包含图形界面,以及镜像维护、用户管理、访问控制等高级功能。
除了官方的 Docker Registry 外,还有第三方软件实现了 Docker Registry API,甚至提供了用户界面以及一些高级功能。比如,Harbor 和 Sonatype Nexus 。
Dockerfile Dockerfile是一个用来自定义容器镜像的文本文件。其内包含了一条条的 **指令(Instruction)**,每一条指令构建一层镜像,因此每一条指令的内容,就是描述该层应当如何构建。当使用docker部署项目时,就需要编写Dockerfile来将项目构建成镜像。
项目实战 打包镜像 需要先将项目,打包成tgz
格式的部署包,再使用maven的docker插件,生成docker镜像。我这里使用maven的assembly
插件来进行打包。
assembly插件 maven中的配置如下:
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 <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-jar-plugin</artifactId > <configuration > <archive > <addMavenDescriptor > false</addMavenDescriptor > <manifest > <addClasspath > true</addClasspath > <classpathPrefix > lib/</classpathPrefix > <mainClass > com.alibaba.nacos.JeecgNacosApplication</mainClass > </manifest > <manifestEntries > <Class-Path > config/</Class-Path > </manifestEntries > </archive > <excludes > <exclude > dll/**</exclude > <exclude > config/**</exclude > <exclude > mybatis/**</exclude > <exclude > ftl/**</exclude > <exclude > static/**</exclude > <exclude > templates/**</exclude > <exclude > groovy/**</exclude > <exclude > *.txt</exclude > <exclude > *.xml</exclude > <exclude > *.properties</exclude > <exclude > *.yml</exclude > <exclude > *.json</exclude > <exclude > assembly*</exclude > <exclude > generate*</exclude > </excludes > </configuration > </plugin > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-assembly-plugin</artifactId > <configuration > <descriptors > <descriptor > src/main/resources/assembly.xml</descriptor > </descriptors > </configuration > <executions > <execution > <id > make-assembly</id > <phase > package</phase > <goals > <goal > single</goal > </goals > </execution > </executions > </plugin > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <configuration > <source > ${java.version}</source > <target > ${java.version}</target > <encoding > ${project.build.sourceEncoding}</encoding > </configuration > </plugin > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-resources-plugin</artifactId > <configuration > <encoding > ${project.build.sourceEncoding}</encoding > </configuration > </plugin >
在resources目录下,新建assembly.xml,
文件内容如下:
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 <assembly xmlns ="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd" > <id > bin</id > <formats > <format > tgz</format > </formats > <includeBaseDirectory > false</includeBaseDirectory > <dependencySets > <dependencySet > <useProjectArtifact > false</useProjectArtifact > <outputDirectory > lib</outputDirectory > <unpack > false</unpack > </dependencySet > </dependencySets > <fileSets > <fileSet > <directory > ${project.basedir}</directory > <outputDirectory > </outputDirectory > <includes > <include > README*</include > <include > LICENSE*</include > <include > NOTICE*</include > </includes > </fileSet > <fileSet > <directory > ${project.basedir}/src/main/resources</directory > <outputDirectory > config</outputDirectory > <includes > <include > dll/**</include > <include > config/**</include > <include > mybatis/**</include > <include > static/**</include > <include > templates/**</include > <include > groovy/**</include > <include > *.xml</include > <include > *.properties</include > <include > *.yml</include > <include > *.txt</include > <include > *.json</include > </includes > <excludes > <exclude > assembly*</exclude > <exclude > generate*</exclude > <exclude > application-dev.properties</exclude > </excludes > </fileSet > <fileSet > <directory > ${project.basedir}/src/main/bin</directory > <outputDirectory > </outputDirectory > </fileSet > <fileSet > <directory > ${project.build.directory}</directory > <outputDirectory > </outputDirectory > <includes > <include > *.jar</include > </includes > </fileSet > </fileSets > </assembly >
配置好后,使用maven开始打包
打包成功后,最终会在项目的target目录,生成最终的部署文件。
dockerfile插件 pom.xml中增加maven插件
1 2 3 4 5 6 7 8 9 10 11 <plugin > <groupId > com.spotify</groupId > <artifactId > dockerfile-maven-plugin</artifactId > <version > 1.4.13</version > <configuration > <dockerfile > Dockerfile</dockerfile > <repository > ${project.artifactId}</repository > <buildDirectory > ${project.build.directory}</buildDirectory > <tag > ${project.version}</tag > </configuration > </plugin >
增加此插件后,在idea的maven插件工具上,就可以看到docker相关的命令
在项目的根路径下,创建Dockerfile文件
编辑Dockerfile文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 FROM openjdk:8 u201-alpineRUN apk add fontconfig && apk add --update ttf-dejavu && fc-cache --force ARG HOME_DIR=/home/nacosARG TARGET_FILE=./target/jeecg-cloud-nacos-2.4 .5 -bin.tgzWORKDIR ${HOME_DIR} ADD ${TARGET_FILE} ${HOME_DIR} EXPOSE 8848 RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone ENTRYPOINT ["java" , "-jar" , "jeecg-cloud-nacos-2.4.5.jar" ]
上面我们已经将项目打包成tgz
文件了,现在开始打包生成镜像。
如图所示,双击dockerfile:build
命令,开始执行生成镜像命令。
打包成功后,打开本地的docker,可以看到刚刚打包成功的镜像文件
到此为止,我们就完成了打包单个项目的镜像了。如果你的项目只有一个springboot,那么现在就可以使用docker run 命令,执行镜像文件了。
容器编排 当我们的项目,子模块非常多时,如果用docker run命令挨个启动,那就太麻烦了,docker官方为我们提供了多个容器的统一管理工具docker compose
。
Compose
项目是 Docker 官方的开源项目,负责实现对 Docker 容器集群的快速编排。从功能上看,跟 OpenStack
中的 Heat
十分类似。
其代码目前在 https://github.com/docker/compose 上开源。
Compose
定位是 「定义和运行多个 Docker 容器的应用(Defining and running multi-container Docker applications)」,其前身是开源项目 Fig。
通过前面的介绍,我们知道使用一个 Dockerfile
模板文件,可以让用户很方便的定义一个单独的应用容器。然而,在日常工作中,经常会碰到需要多个容器相互配合来完成某项任务的情况。例如要实现一个 Web 项目,除了 Web 服务容器本身,往往还需要再加上后端的数据库服务容器,甚至还包括负载均衡容器等。
Compose
恰好满足了这样的需求。它允许用户通过一个单独的 docker-compose.yml
模板文件(YAML 格式)来定义一组相关联的应用容器为一个项目(project)。
Compose
中有两个重要的概念:
服务 (service
):一个应用的容器,实际上可以包括若干运行相同镜像的容器实例。
项目 (project
):由一组关联的应用容器组成的一个完整业务单元,在 docker-compose.yml
文件中定义。
Compose
的默认管理对象是项目,通过子命令对项目中的一组容器进行便捷地生命周期管理。
Compose
项目由 Python 编写,实现上调用了 Docker 服务提供的 API 来对容器进行管理。因此,只要所操作的平台支持 Docker API,就可以在其上利用 Compose
来进行编排管理。
docker-compose的用法,可以参考如下教程:https://yeasy.gitbook.io/docker_practice/compose/introduction
我们先将所有的项目,使用上面的dockerfile插件,统一生成docker镜像,然后来开始编写docker-compose.yml
文件来统一管理容器。
如图所示,我已将所有的项目全部生成镜像:
新建一个目录,目录名随便起,在目录下新建一个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 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 version: "2" services: mysql-jeecg: container_name: mysql-jeecg image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: 123456 MYSQL_ROOT_HOST: '%' TZ: Asia/Shanghai restart: always volumes: - ./mysql/data:/var/lib/mysql - ./init.d/mysql:/docker-entrypoint-initdb.d command: --max_connections=2000 --default-storage-engine=InnoDB --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci --explicit_defaults_for_timestamp=true --lower_case_table_names=1 --max_allowed_packet=128M --innodb_large_prefix=on --sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION --log-bin=mysql-bin --server-id=1 --binlog_format=ROW --expire_logs_days=7 ports: - "3307:3306" rabbitmq-jeecg: build: context: ./init.d/rabbitmq dockerfile: RabbitMQ.Dockerfile image: rabbitmq:rabbitmq-jeecg container_name: rabbitmq-jeecg ports: - "5672:5672" - "15672:15672" restart: always redis-jeecg: container_name: redis-jeecg image: redis:6.2.12 volumes: - ./redis:/data ports: - "6379:6379" command: --appendonly yes --requirepass "123456" restart: always minio-jeecg: container_name: minio-jeecg image: minio/minio:latest ports: - "9010:9000" - "9011:9001" environment: MINIO_ROOT_USER: admin MINIO_ROOT_PASSWORD: 12345678 command: server --console-address ":9001" /data/minio/ volumes: - ./minio:/data restart: always nginx-jeecg: container_name: nginx-jeecg image: nginx:latest ports: - "8080:80" volumes: - ./nginx/conf.d:/etc/nginx/conf.d - ./web:/usr/share/nginx/html restart: always jeecg-gateway: image: jeecg-cloud-gateway:2.4.5 container_name: jeecg-gateway volumes: - ./logs/gateway:/home/gateway/logs/gateway - ./config/gateway:/home/gateway/config ports: - "9999:9999" depends_on: - redis-jeecg restart: always jeecg-system: image: jeecg-cloud-system-start:2.4.5 container_name: jeecg-system volumes: - ./logs/system:/home/system/logs - ./config/system:/home/system/config ports: - "7001:7001" depends_on: - rabbitmq-jeecg - redis-jeecg - minio-jeecg - mysql-jeecg - jeecg-xxljob - jeecg-nacos restart: always jeecg-xxljob: image: jeecg-cloud-xxljob:2.4.5 container_name: jeecg-xxljob volumes: - ./logs/xxljob:/home/xxljob/logs/xxljob - ./config/xxljob:/home/xxljob/config ports: - "9081:9081" depends_on: - mysql-jeecg restart: always jeecg-nacos: image: jeecg-cloud-nacos:2.4.5 container_name: jeecg-nacos volumes: - ./logs/nacos:/home/nacos/logs/nacos - ./config/nacos:/home/nacos/config ports: - "8848:8848" depends_on: - mysql-jeecg restart: always jeecg-base: image: jeecg-cloud-module-base-start:2.4.5 container_name: jeecg-base volumes: - ./logs/base:/home/base/logs - ./config/base:/home/base/config depends_on: - jeecg-nacos ports: - "7005:7005" restart: always jeecg-contract: image: jeecg-cloud-module-contract-start:2.4.5 container_name: jeecg-contract volumes: - ./logs/contract:/home/contract/logs - ./config/contract:/home/contract/config depends_on: - jeecg-nacos ports: - "7004:7004" restart: always jeecg-pound: image: jeecg-cloud-module-pound-start:2.4.5 container_name: jeecg-pound volumes: - ./logs/pound:/home/pound/logs - ./config/pound:/home/pound/config depends_on: - jeecg-nacos ports: - "7010:7010" restart: always jeecg-reports: image: jeecg-cloud-module-reports-start:2.4.5 container_name: jeecg-reports volumes: - ./logs/reports:/home/reports/logs - ./config/reports:/home/reports/config depends_on: - jeecg-nacos ports: - "7007:7007" restart: always jeecg-vehicle: image: jeecg-cloud-module-vehicle-start:2.4.5 container_name: jeecg-vehicle volumes: - ./logs/vehicle:/home/vehicle/logs - ./config/vehicle:/home/vehicle/config depends_on: - jeecg-nacos ports: - "7002:7002" restart: always jeecg-wms: image: jeecg-cloud-module-wms-start:2.4.5 container_name: jeecg-wms volumes: - ./logs/wms:/home/wms/logs - ./config/wms:/home/wms/config depends_on: - jeecg-nacos ports: - "7003:7003" restart: always netty-server: image: netty-server:1.0 container_name: netty-server volumes: - ./logs/netty-server:/home/netty-server/logs - ./config/netty-server:/home/netty-server/config ports: - "11001:10001" - "11005:10005" - "11006:10006" restart: always
在此文件中,我将所有的配置及日志,都挂载到了宿主机上,这样当容器发生故障时,数据也不会丢失,再启动一个容器就行了。
最终的目录如下所示:
使用命令行,进入到此目录,并执行
这样所有的项目就都启动了。
特别注意:使用docker部署时,程序里连接数据库或者其他服务时,要使用域名的方式,否则无法连接。域名就是上面docker-compose中配置的容器的名称。
导出镜像 可以使用docker的导出命令,来将项目所有的镜像导出成一个tar包,这样当部署新机器时,直接使用导入命令,将镜像导入,然后使用docker-compose直接启动就行了,中间无需任何配置。
可以使用shell脚本,导出本地所有的镜像
1 2 3 4 5 6 7 #!/bin/bash sum =`docker images | wc -l`COUNT=`expr $sum - 1` echo 镜像数量:$COUNT TAG=`docker image list|grep -v REPOSITORY|awk '{print $1":" $2}' |awk 'ORS=NR%"' $COUNT '"?" ":"\n"{print}' ` echo TAG值:$TAG docker save $TAG -o power-material.tar
导入镜像 使用docker load 命令,导入镜像
1 2 #!/bin/bash docker load -i power-material.tar