Docker简介
本书到这里即将完成本次微服务开发之旅。在前面的章节中讲述了从单体架构应用到微服务架构应用转变时需要的开发技术及Spring Cloud所提供的解决方案。但是还有一点,在单体架构应用开发时系统上线通常是将构建的应用打包成WAR文件然后部署到Web容器中,当转移到微服务架构下时涉及非常多的微服务实例,总不能像前面章节那样使用Java命令来启动应用吧。因此,在本章中我们将快速了解一些微服务运行所需要的容器技术,以及打造在实际生产环境中如何自动构建发布管道。
Docker的使用
Docker诞生于2013年初,自发布以来一直备受关注,被认为是由可能改变软件行业系统交付流程现状的最佳工具。短短几年的发展,Docker已经发展成为世界领先的软件容器虚拟化平台。全世界各大厂商甚至一些小公司都已逐渐利用它来提升软件开发效率。那么Docker火爆的背后是什么原因呢?
众所周知,软件行业最繁琐的事情之一就是”环境配置“。软件从开发到发布,都需要对操作系统进行一系列的配置,比如安装软件所需要依赖的库、组件和配置数据等。仅仅一个Java应用就需要安装JDK库及配置环境变量。而且不用的操作系统平台所需要做的配置也不尽相同。一旦将应用部署到另外一台不同环境的计算机上,那么所有的配置都要重来一次,极其浪费人力、时间和资源。那么有没有办法可以完成一次环境配置后将其完整地迁移到其他计算机上使用呢?答案是肯定的。
虚拟机的出现在一定程度上解决了环境配置所带来的困扰。虚拟机简单说就是指在一个操作系统里运行另外一个操作系统,如在Windows系统上运行Linux系统。虚拟机对于宿主系统来说就是一个普通的文件,完全可以做到迁移到另外一个新的虚拟机上,这就达到了完成一次环境配置后复制到其他计算机上使用的目的。但虚拟机也有其缺点,若读者有使用过VMware、VirtualBox等虚拟机工具的经验则体会更深。对于虚拟机,具有以下缺陷:
- 创建步骤复杂。每次黄建新的虚拟机,都需要下载对应操作系统的镜像文件,然后再做一系列如内存、磁盘大小等配置,创建过程几乎与安装一个操作系统的步骤是一样的。
- 资源占用多。每个虚拟机都会独占一部分内存和磁盘空间,在它们运行的时候,其他应用程序不能使用所占用的这部分资源。而且几遍虚拟机内部没有任何应用程序在运行,它依然会占用资源,因为它相当于在运行一个操作系统。
- 启动慢。虚拟机本身其实是在运行一个操作系统,所以其启动过程完成就是在启动一个操作系统。启动操作系统需要花多长时间,虚拟机启动完毕就需要花多长时间,等待它启动完毕去运行内部应用程序可能是几分钟之后的事情了。
此外,虚拟机也不能完全解决”环境配置“的问题,于是开发者又开发出了另外一种新的虚拟机技术:Linux容器。
Linux容器(Linux Containers, LXC),它不像虚拟机那样模拟运行一个完成的操作系统,而是对进程进行隔离。或者说,Linux容器就像是一个箱子,里面放了一系列的进程,从而将这些进程与操作系统进行隔离。正因为容器是进程级别的,所以相对虚拟机很很多的优势,像前面提到的虚拟机的缺陷在容器上几乎是不存在的。容器启动相当于启动本机上的一个进程,而不是启动一个操作系统,启动速度非常快;容器启动才会占用所需要的资源,不会占用额外的资源,而且多个容器间的资源时可以共享的,虚拟机却只能独享;由于容器仅是进程的集合,而且仅包含所需的组件即可,因此体积占用非常小,这方面虚拟机也是无法比拟的。除了启动快、占用资源少、体积小外,容器还有很多的优点,这里就不一一细说了,当读者使用了Docker后会有所感受。
Linux容器是属于系统底层的一种技术,而且使用起来也不方便,所以Docker就基于Linux容器进行了封装,并提供了简单易用的容器接口。可以说,Docker是目前最流行的Linux容器解决方案。Docker可以很方便地将软件与所需要依赖的库或组件打包整合到一个镜像文件里,然后运行这个镜像,就生成了一个虚拟容器。同时,配置好的镜像文件可以很容易地通过Docker的接口进行备份导出,上传到仓库,从而达到复用的目的,而不需要每台计算机都重新配置一遍。
Docker的出现很好地解决了前面所提到的问题,这也是其快速发展的重要原因之一,因为它解决了软件行业中的几大”痛点“。作为一种新兴的虚拟化技术,Docker相比传统虚拟技术具有以下优势:
- 占用资源极少;
- 更快的启动速度;
- 更轻量的体积;
- 更好的复用性。
到此,相信大家对Docker应该有所了解了,接下来就讲讲它到底能为我们解决什么问题呢?
Docker的使用
在传统开发过程中,通常是开发人员将已开发好的软件打包好,然后交付给运维人员部署到服务器上。若是新服务器,还需要进行一系列配置,部署好后由测试人员进行测试。测试过程可能会产生一些bug,开发人员修复bug后重新打包、交付,其过程十分繁琐。
在软件真正发布到生产环境之前,可能不知道要重复经历多少遍的交付、部署、测试这样的流程。
Docker的出现可以说将DevOps(开发运维一体化)过程变得更加自动化、便捷,加速了软件和服务的交付。通过自动化持续构建工具,开发人员提交代码后,就会检测代码的变动,然后自动将新的代码构建成Docker镜像并进行部署,部署成功后直接通知测试人员进行测试,这中间省去了运维人员的工作,自动化构建工具代替了运维人员原来所做的工作,减轻了运维人员不少的工作压力。并且由于Docker镜像提供了一致的运行环境,使得DevOps过程变得更加可靠、快速,不容易产生因为环境问题而出现的一些部署问题。
Docker适用于以下场景:
- 持续构建与持续交付
- 更加自动、轻量、快捷的DevOps流程
- 弹性云服务的动态扩容与缩容
- 搭建微服务架构
使用Docker可以轻松做到以前软件行业做不到或很难做的一些事情。比如,发布Java Web应用,以前需要在服务器安装Tomcat和JDK,然后上传打包的WAR文件,最后才能够启动应用。当使用了Docker后,可以基于Tomcat镜像将WAR包复制到自定义构建的镜像中,然后直接在服务器上运行镜像即可。这中间节省的可不是一个或两个步骤这么简单,所以说这就是技术发展带来的便利。
1、安装
随着Docker不断流行与发展,规模也日益壮大,Docker公司也开启了商业化之路。Docker从17.03版本后分为社区版(CE)和企业版(EE),社区版免费,企业版收费。两者的区别在于企业版除了能够得到Docker公司的服务支持外,还提供了一些社区版没有的高级功能,如LDAP/AD用户集成、基于角色的访问控制和安全扫描等。
除非需要Docker的某些高级功能,否则对于一般用户来说,Docker CE版本的功能基本可以满足日常需要,所以接下来将采用Docker CE版本来完成本书中的示例。
Docker是跨平台的,在主流的操作系统上基本都可以安装,但也有一些前提条件,主要体现在体系架构和内核的支持上,因此官网针对每个支持的操作系统都有详细的安装说明。随着Docker的不断升级,未来的版本安装步骤可能会有所不同,因此这里将不对安装部分做详细讲解,读者可以到Docker官网(https://www.docker.com)查阅具体的安装说明。
在满足了前提条件下,安装Docker就非常简单了,通常只需要一条命令即可完成安装。比如在Ubuntu下(后面演示例子也都将在Ubuntu上完成),可以使用如下命令安装:
$ sudo apt-get install docker-ce
安装结束后,如果需要验证Docker CE是否已正确安装,可使用如下命令运行一个hello world镜像:
$ sudo docker run hello-world
该命令将下载一个测试镜像(hello world)并在容器中运行。如果容器运行成功,将在控制台输出一条参考消息并退出。
在Linux操作系统上,由于Docker守护进程始终以root用户身份运行,普通用户若需要使用Docker命令时则需要先添加sudo,若不想每次都使用sudo,则可以将用户添加到Docker分组中,Docker分组将授予等同于root用户的特权。
// (1) 添加Docker分组
$ sudo groupadd docker
// (2) 添加指定用户到分组
$ sudo usermod -aG docker $USER
添加到分组后需要注销并重新登录,以便用户权限起效,如果是SSH远程用户,直接退出然后再重新登录即可。运行一下命令看是否还需要sudo:
$ docker run hello-world
对于Docker的使用,涉及命令、细节和技巧等方方面面的知识,这里我们不全部介绍了,仅讲一下最基本的入门知识。而对于入门,官方网站文档提供了最好的入门途径(https://docs.docker.com/),建议刚接触Docker的读者都去试读并研究一下。Docker提供了一套完整的命令体系,因此可以通过命令直接查看和了解如何使用。要想用好Docker,必须熟练掌握Docker命令。
1. 查看Docker所支持的所有命令
通过在控制台直接运行docker命令,可以列出Docker所支持的所有命令和通用的参数信息。
2. 查看Docker命令帮助
在控制台执行docker COMMAND —help格式指令,可以列出对应命令的相关帮助信息,该指令在不熟悉Docker命令时十分有用。例如,执行docker image —help命令。
2、镜像
在Docker中有3个重要的概念:仓库、镜像和容器。
Docker将应用程序及其依赖的库等打包到同一个文件里从而形成镜像,镜像可以包含完成的操作系统,也可以仅包含Tomcat或JDK运行环境。镜像被创建后,可以保存在本地,也可以上传到仓库,所以Docker仓库就是用于存放镜像的地方。Docker官方仓库里包含非常多的镜像。我们日常应用基本都是基于仓库里某个镜像来创建适合自己使用的镜像,如基于Ubuntu系统的镜像。
容器是基于镜像运行的虚拟实例,类似前面所提到的虚拟机,但又与虚拟机有所区别。因为它仅是一系列进程的集合,所以有着虚拟机不可比拟的速度,资源占用小且体积小,而且每个容器之间是相互隔离,互不影响的。当然每个容器之间也可以进行连接从而达到资源共享。基于一个镜像,在服务器允许的情况下,可以无限创建容器。
如上文所说,镜像可以包含单个应用甚至整个操作系统,若是以普通的方式来存储,其体积必然是庞大的,因此Docker在设计时利用Union File System(联合文件系统)技术来存储镜像。Union File System是一种轻量级高性能分层文件系统,它支持对文件系统的修改作为一次提交来一层层地叠加,同时可以将不同目录挂在到同一个虚拟文件系统下。
Union File System是镜像的基础,镜像可以通过分层来实现继承。得益于这种技术,使镜像的复用、定制变得更容易,基于基础镜像可以制作各种各样的镜像。由于镜像是通过分层存储的,在构建镜像时会一层层叠加,后一层会叠加在前一层上,因此通过Dockerfile来构建镜像时,尽量较少构建层数,每一层仅包含该层需要的东西即可。
Docker内嵌了一系列命令来制作、管理、上传和下载镜像。镜像的管理操作相对比较单间,本节将重点介绍的是使用Dockerfile来构建镜像。
1. 列出本机镜像
通过docker images命令,可以列出本机已经存在的镜像信息。
得到镜像信息后,可以通过下面的命令来查看指定镜像的详细信息:
$ docker image inspect [IMAGE ID]
2. 从仓库拉取镜像
在Docker官方仓库中存在各种各样的镜像,在构建镜像的时候,基本都是从仓库中选取某个镜像作为基础镜像,然后通过继承这个基础镜像来定义出适合自己使用的镜像。拉取镜像的命令格式如下:
$ docker pull NAME[:TAG]
其中,TAG为镜像的标签,也可以理解为版本,一个镜像可以存在多个标签,每次构建镜像可能都会重新定义新标签。若不指定TAG,则TAG默认为latest。
对于Java应用来说最需要依赖的库就是JDK,如果在本地计算机上安装JDK,可能需要进行一些额外的配置,如环境变量,但使用Docker镜像则不需要,因为镜像已经帮我们做好了这些事。此外,在Docker中最常使用的JDK镜像是openjdk镜像,该镜像在官方仓库拥有众多的标签,我们可以从中选择一个适合的拉取下来,比如openjdk:8u131-jre。执行拉取命令后,控制台输出如下。
拉取成功后,可以通过docker images命令查看本地镜像列表,确认镜像是否拉取成功。
3. Dockerfile文件
有时候从仓库中所拉取的镜像可能并不适合我们使用,如前面所拉取的openjdk镜像,在实际应用中可能需要配置镜像内部时区、UTF-8编码及运行内存等。这时通常使用Dockerfile文件对镜像进行自定义,利用Dockerfile文件定义好配置信息之后,就可以通过命令来构建属于自己的镜像。
Dockerfile是一个文本文件,其中包含了指令(Instruction)信息,每一条指令构建一层。因此每一条指令的内容就是描述该层应当如何构建。下面基于前面拉取的镜像openjdk:8u131-jre增加一些配置来构建我们所需要的JDK镜像。在指定目录内创建名为Dockerfile的文件,并增加如下内容:
# 基于openjdk的8u131-jre版本构建
FROM openjdk:8u131-jre
# 维护者
MAINTAINER microserv-usr
# Java环境变量
ENV JAVA_OPTXS="-server -Xmx1G -Xms1G -Xmn200M -Duser.timezone=GMT+08"
# 容器运行执行命令
# 默认运行 /jar/app.jar
ENTRYPOINT [ "sh”, "-c", "java $JAVA_OPTS -jar /jar/app.jar" ]
保存后,在Dockerfile文件所在的目录下,通过下面的命令来构建:
$ docker pull microserv/openjdk:1.0.0
总段结果输出如图11-7所示。从构建过程日志输出来看,定制镜像就是定制每一层所需要的配置、文件,而每一条指令则会将构建增加一层,而层数的多少回直接影响镜像的体积、构建速度,所以应尽量减少指令条数。
除了以上Dockerfile文件中的FROM、ENTRYPOINT和ENV这几个指令外,还有一些指令,下面逐一介绍。
- FROM:指定基于那个基础镜像构建,这个指令是必须有的。
- COPY:将构建目录上下文中的文件或目录复制到镜像内。
- ADD:和COPY性质基本一致,但比COPY更高级,比如可以指定通过网络URL进行下载。若从本地复制文件到镜像中,则推荐使用COPY。
- ENV:设置环境变量。
- ARG:效果和使用ENV命令的效果是一样的,区别是,ARG命令设置的环境在未来运行的容器中是不存在的。
- EXPOSE:暴露镜像所占用的端口,未来容器运行时所提供的服务端口。若容器中所运行的应用,如Tomcat有特定端口,建议都设置次参数,这样可以明确告诉使用者服务端口是哪个。
- RUN:执行命令行。此指令特别有用,尤其是镜像需要安装依赖、创建用户等操作,通过RUN可以很简单的向镜像内添加这些东西。但有一点需要注意,如果执行多个命令时,则尽量拼接在一层中去处理,如apt-get update & apt-get install nano。
- WORKDIR:指定工作目录。
- USER:指定容器运行所使用的用户。例如,运行Tomcat容器,可以将Tomcat指定为特定用户才允许运行。
- CMD:容器启动命令。若容器运行时需要启动某个脚本或应用,可以通过该参数进行设置,如打印字符串CMD [ “sh”, “-c”, “echo hello cms”]。
- ENTRYPOINT:和CMS目的一样,都是在指定容器中启动程序及参数。CMD的参数设置更像是指定了默认值,容器运行时可以覆盖参数值,而ENTRYPOINT则表示容器运行时所需要的初始参数,这些参数不可被重写覆盖。
以上是在使用Dockerfile文件时一些常用的指令介绍。若想更深入了解,可参考官方文档:https://docs.docker.com/engine/reference/builder/。
4. 上传镜像
在本地构建完基础镜像后,若要使用的话,还需要将镜像上传到仓库中,这样才能在其他环境下拉取到镜像。上传镜像到仓库前,需要先到镜像仓库中申请账号密码,账号申请、镜像上传和下载都是免费的。
账号申请好以后还需要创建一个仓库组织,如图11-8所示,示例中我们设置为microserv。如果没有申请组织,则无法正常上传镜像文件。
创建好仓库组织后就可以在本地使用命令来上传镜像了,如图11-9所示。
因为笔者之前上传过镜像,所以Docker检测镜像已经存在不用再重复上传。上传完毕后可以到Docker仓库页面确认镜像是否上传成功了,如果在如图11-10所示的界面中出现了我们所定制的镜像openjdk,说明镜像已经上传成功。
Docker镜像的其他常用操作还有两个。
- 删除指定镜像:docker rm [REPOSITORY | IMAGE ID]
- 批量删除未被使用的镜像:docker rmidocker images -aq
技巧:有时下载镜像可能会遇到下载不完整而出现None这样的一些镜像,可以通过命令docker images —no-trunc | grep nono | awk ‘{print $3}’ | xargs -r docker rmi进行批量清理。
3、容器
容器是Docker的核心内容,相对于传统的虚拟化,其在性能上为Docker带来了极大的优势。Docker容器都是基于镜像来创建运行的。基于某个镜像,可以创建多个容器,而且容器之间是相互隔离互不影响的,它们各自拥有唯一的ID和名字,这样能够更有效地保护各个容器能够正常运行而不受其他容器的影响。
我们可以将应用程序复制到容器内运行,也可以通过挂在宿主机上的应用程序文件来运行。通过挂在宿主机的文件到容器内部有很多好处,其中之一就是停止删除容器后所挂载的文件不会丢失,它们依然会被保留在宿主机内部,这对于运行MySQL等数据库需要保留数据文件的容器来说特别有用。这种挂载的行为被称为数据卷,运行容器时通过-v参数将宿主机上的某个文件或目录挂载到容器内部指定的文件或目录下,这样在容器内部就可以对挂载的文件或目录进行操作。若使用Docker部署应用到生产环境中,都推荐将相关文件目录挂载到宿主机上,如日志文件和数据文件等,避免删除容器后造成文件丢失。下面将介绍Docker针对容器的一些常用操作。
1. 列出容器
通过docker ps命令可以列出本机正在运行的所有容器,如图11-11所示。
docker ps命令是列出正在运行的容器,若查看所有(包括已停止的容器),可在后面加上参数-a,状态(STATUS)列中带有Exited关键字的表示该容器已退出。
2. 启动和重启容器
启动容器命令:docker start [image…]
重启容器命令:docker restart [image…]
其中,image…表示容器名或者容器ID,可以一次性启动多个容器。重启命令可作用于正在运行或已停止的容器,而启动命令则仅可启动已停止的容器。
3. 停止和删除容器
若需要删除某个容器,则需要先停止该容器。停止容器和删除容器的命令如下:
- 停止容器:docker stop [images …]
- 删除容器:docker rm [images…]
通常情况下,容器被删除后,除非内部文件被挂载到宿主机上,否则在容器内部修改或添加的东西都将会丢失。所以若使用容器运行比较重要的应用,如数据库,建议将相关目录挂载存储到宿主机的目录上。
4. 新建并运行容器
新建并运行容器的命令主要是docker run [options],其中,options是启动容器所需要的一系列参数,具体可以通过—help命令查看。在前面示例中可以通过下面的命令启动hello-world容器:
$ docker run hello-world
这是基于hello-world镜像运行的一个简单例子,没有任何参数设置,也不能在后台以守护方式运行,运行完后即停止退出了,在实际应用中肯定不会如此简单。若要真正部署运行我们所构建的微服务应用,还需要做一些参数配置,如制定后台运行模式、容器名称、数据卷挂载等。
4、容器实战:MySQL
接下来我们将一起搭建一个可用于生产环境的MySQL容器。在Docker仓库中提供一系列不同版本的MySQL镜像,但要想在国内使用,则需要对原始镜像做一系列的配置更改,如设置UTF-8编码,支持emoji表情符号,支持东八区时区等,如果不做这些配置,则会出现中文乱码和时间不正确等问题。
首先从仓库中选取适合的MySQL版本镜像,比如下面选择mysql:5.7,并使用以下命令拉取该镜像:
$ docker pull mysql:5.7
然后创建MySQL数据文件存储目录,增加相应的自定义配置文件,最终的目录文件结构如图11-12所示。
其中,MySQL需要的配置文件my.cnf的内容如下:
# 设置UTF-8,避免查询乱码
[client]
default-character-set=utf8mb4
[mysql]
default-character-set=utf8mb4
[mysql]
# 增加对emoji表情存储的支持
collation-server=utf8mb4_unicode_ci
character-set-server=utf8mb4
default_password_lefttime=0
# 将时区设置为东八区
default-time-zone='+8:00'
镜像、存储目录和配置准备妥当后,就可以新建并运行MySQL容器。所要执行的命令如下:
$ docker run --name mysqldb -d \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=rootPwd2175 \
-e MYSQL_USER=mysqluser \
-e MYSQL_PASSWORD=mysqlUsr6335 \
-e LANG=C.UTF-8 \
-v /etc/localtime:/etc/localtime \
-v /usr/local/springcloud/docker/mysql/conf.d:/etc/mysql/conf.d \
-v /usr/local/springcloud/docker/mysql/data:/var/lib/mysql \
-v /usr/local/springcloud/docker/mysql/dumps:/var/lib/dumps \
mysql:5.7
这里将所要运行容器的名称设置为mysqldb,通过-p参数暴露占用了宿主机的端口3306,然后设置了用户密码、UTF-8编码、本地时区及挂载宿主机的目录到容器内部以便MySQL读取配置文件,并将数据文件、备份文件保存到宿主机本地目录上。-e参数是MySQL镜像本身提供的设置参数。类似于MySQL,基本上官方(具体应用的厂商)所提供的镜像都会有各种相应参数设置,如Nginx、Gitlab、Redis等。参数-d则表示指定该容器在后台运行。
最后把这段命令复制到命令终端运行,如无意外,MySQL容器将会在后台运行起来,成功后将在终端打印出容器的唯一ID。而MySQL所生成的相关数据文件、日志等,则存储在所指定的目录/usr/local/springcloud/docker/mysql/data下。
真正的、可运行于生产环境的容器通过上面的简单几步就构建完成了。另外,我们可以结合前面的知识使用Dockerfile将上面MySQL相关的配置构建成一个适合自己的基础镜像,这样以后在使用时可以免去这些配置,当然,如数据挂载目录这些配置还是不可以避免的。
运行容器命令中常用选项的说明如下。
- —name:指定容器的名称。
- -d:在后台运行容器并打印容器ID,若不设置则默认在前台运行,终端退出后,容器也会跟着退出。
- -p:将镜像中暴露的端口号映射到宿主机上,这个配置很重要,若镜像有特定端口而容器不映射出来,那么外网将访问不到该容器。
- -e:设置环境变量。
- -v:挂载宿主机文件目录到容器内部。
- —link:连接某个容器,如—link mysql:mysql会在当前容器的host文件内加入名为mysql的主机,这样容器可以无须知道MySQL容器的IP就能通过别名MySQL进行连接。但有个缺点:一旦MySQL重建或重启,则容器也必须重新启动才可连接到MySQL上,所以当需要连接容器时一般推荐通过IP+端口的方式进行访问。
Docker与Spring Cloud微服务
了解了Docker基础知识之后,我们就可以将前面章节中锁构建的微服务应用通过Docker容器进行发布和运行。在这里提示一下,在生产环境中使用Docker时,强烈建议使用Linux操作系统,除了足够安全之外,Docker在Linux操作系统上也是最成熟、稳定的,基本不会出现什么问题。因此,接下来的例子都是基于Ubuntu 16.04系统来演示的.
针对通过Maven管理依赖的Java项目,Maven本身提供集成到Docker插件,利用该插件可以构建出Java项目的镜像,但是通过这种方式构建出来的镜像文件一般都比较大。如果在本地运行则是一个不错的选择,但如果应用在云端,那么在上传宽带有限的情况下,要将所构建的镜像推送到仓库中是非常耗时的。所以这里将介绍另外一种方式,即Linux Shell脚本方式。使用Linux Shell脚本的发布流程如下:
- 预先构建好基础镜像,并推送到仓库中,发布的时候可直接复用,不用再次构建。
- 本地对所要发布的微服务应用进行编译打包,得到JAR包或WAR包。
- 使用Linux工具scp,将JAR包或WAR包上传到Docker宿主服务器上。
- 备份正在运行的项目,包括JAR包或WAR包、日志文件等(这一步可省略)。
- 使用基础镜像通过挂载数据卷方式,启动并运行微服务应用。
通过Linux Shell脚本方式来构建发布应用有以下优势:
- 无须修改源码或配置
- 可直接复用构建好的基础镜像来运行应用。
- 应用文件如JAR包或WAR包独立,不用打包到镜像中,也不用推送到仓库中。
- 通过集成Jenkins工具可以直接复用shell脚本来实现自动化部署。
通过Linux Shell脚本可以将应用的备份、发布动作在同一个任务内完成。
1、部署Eureka服务
接下来以服务治理服务器的发布为例,详细讲解如何将应用发布到Docker中运行。首先,在项目目录内创建shells目录,并在该目录下创建下面两个文件。
release-docker.sh:发布脚本,运行Docker应用,需要上传到服务器。
- build.sh:用于应用的编译打包,上传到服务器,最后执行发布脚本。
release-docker.sh脚本的具体内容如下:
#! /bin/bash
# Docker容器基本信息
dockerName="servdiscovery"
dockerPort="8260"
# 挂载路径建议为绝对路径
dockerJarVolumn="/home/hucw/springcloud/${dockerName}"
# 停止删除现有容器
echo "停止删除容器:${dockerName}"
docker stop ${dockerName} && docker rm ${dockerName}
# Docker容器运行命令
echo ""
docker run --name=${dockerName} -d \
-p ${dockerPort}:${dockerPort} \
-v {$dockerJarVolumn}/jar:/jar \
microserv/openjdk:1.0.0
echo "${dockerName}容器已启动,使用命令查看启动日志:docker logs --tail=500 ${dockerName}"
release-docker.sh发布脚本是要上传到服务器上执行的,主要用来运行Docker容器。如果需要备份,则在该脚本中直接编写相关命令即可。运行的容器是基于前面所构建的基础镜像microserv/openjdk:1.0.0,在这个镜像的Dockerfile文件中定义了默认运行文件/jar/app.jar。因此我们需要将服务治理应用打包后的文件名称更爱为app.jar,并通过-v参数将该文件挂载到容器中,这样容器在启动时就可以直接运行服务治理应用。另外,在该脚本中将容器所暴露的端口设置为8260,也就是服务治理应用的端口。
build.sh脚本的具体内容如下:
#! /bin/bash
# 进入脚本所在目录,然后返回项目跟目录
scriptPath="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)"
cd $scriptPath
cd ..
# 服务器SSH配置
sshIP=yourserverIP
sshUsr=yourUser
sshPwd=yourpass
sshPort=22
# Docker容器基本信息
dockerName="servdiscovery"
# 编译打包
mvn clean package
# 复制JAR包到目录下
outputPath="${scriptPath}/${dockerName}";
if [ -d ${outputPath} ]; then
rm -rf ${outputPath}
fi
mkdir ${outputPath} && mkdir ${outputPath}/jar
cp ./target/*.jar ${outputPath}/jar/app.jar
cp ${scriptPath}/release-docker.sh ${outputPath}/release-docker.sh
# 删除服务器中已存在的目录
cd ${scriptPath}
appPath='~/springcloud'
ssh -p ${sshPort} ${sshUser}@${sshIP} "[-d ${appPath}]||mdkir -p ${appPath}"
ssh -p ${sshPort} ${sshUser}@${sshIP} "[-d ${appPath}/${dockerName}] && rm -rf ${appPath}/${dockerName}"
ssh -p ${sshPort} ${sshUser}@${sshIP} "[-d ${appPath}/${dockerName}] || mdkir -p ${appPath}/${dockerName}"
# 将整个目录上传到服务器上
scp -r -P ${sshPort} ./${dockerName} ${sshUser}@${sshIP}:${appPath}
# 远程执行发布命令
ssh -p ${sshPort} ${sshUser}@${sshIP} "sh ${appPath}/${dockerName}/release-docker.sh"
在build.sh脚本中先定义了编译发布所需要的变量值,如SSH配置,然后通过Maven命令对微服务应用进行编译打包。打包成功之后,将所生成的微服务应用JAR文件复制到指定目录下,并上传到服务器上,最后再远程调用执行发布脚本release-docker.sh发布应用。在该脚本中涉及较多的SSH命令,SSH是一种加密的网络传输协议,基本上每个Linux系统中都会有这些命令,稍加学习就可以掌握。
脚本编写好之后,就可以通过下面的命令来部署服务治理服务器了。
$ sh ./shells/build.sh
终端输出结构如图11-13所示。
当出现容器已启动输出之后,就可以登录到宿主服务器中通过命令docker logs —tail=500 servdiscovery来查看服务日志信息,在输出的日志中可以看到类似下面的输出:
说明服务治理服务器已经成功部署并启动成功了,接下来我们完成其他微服务的部署吧。
2、部署应用微服务
下面我们把用户微服务和商品微服务也部署上去。首先把service-discovery项目中的shells目录分别复制到user-service和product-service目录下,然后分别将build.sh和release-docker.sh脚本中的容器名称和端口号修改如下。
- user-service:将dockerName修改为userservice,将dockerPort修改为2100。
- product-service:将dockerName修改为productservice,将dockerPort修改为2200。
修改完毕后,分别执行微服务的build.sh脚本,如无意外,用户微服务和商品微服务都可以部署成功,启动后则会注册到Eureka服务器中。此时可以到服务器中查看service-discovery服务的Docker日志进行确认:
$ docker logs --tail=500 servdiscovery
日志结果如图11-14所示。
从日志输出中可以看到,用户微服务和商品微服务都已成功注册,那么可以尝试使用Postman访问这两个微服务,商品微服务访问结果图11-15所示,说明微服务功能运行正常。
到这里为止,我们已经成功地将微服务通过Docker容器来运行了。但是通过手工执行发布对微服务架构应用来说不是很好的选择,下面来看看如何借助工具实现自动进行构建和部署。
微服务与Jenkins
微服务的优势相信大家都已经知道了,但它也有缺点。如果微服务的数量一旦多起来,那么不论部署到开发环境、测试环境或生产环境都是麻烦的事情。大家可以会说用Shell脚本一键部署,虽然Shell脚本很方便,但是对于微服务来说,有时可能一天需要部署多次,而重复执行一项任务最好的方式就是交给计算机来处理。那么,有没有一种解决方案可以检测微服务源码的变化,然后自动构建部署,从而以达到持续集成发布的目的呢?答案是肯定的。业界中自动构建部署工具非常多,这里介绍一款轻量级开源的工具——Jenkins。
Jenkins是一个用Java编写的开源的持续集成工具,它提供了软件开发的持续集成服务,可用于自动执行、构建、测试、交付或部署相关任务。Jenkins可以执行基于Apache Ant和Apache Maven所构建的项目,以及任意的Shell脚本和Windows批处理命令。同时,Jenkins也是一个高度可扩展的产品,提供了强大的插件生态环境,通过安装插件几乎能够满足任何你想要的构建任务。
1、安装Jenkins
Jenkins可以直接通过Docker安装、运行,也可以通过任何安装了Java运行时环境(JRE)的计算机独立运行。这里推荐在已安装了JDK环境的计算机上安装使用,如果通过Docker来运行Jenkins的话,在使用过程中会产生一些问题,一方面是由于Jenkins本身的配置问题,另外一方面是在构建任务时需要依赖。比如,当所构建的前端项目依赖Node.js时,就需要在容器内部安装Node.js,一旦容器出现问题需要移除,那么这些在容器内部安装的依赖就会丢失,需要再次进行安装。因此,这里建议大家直接运行。
Jenkins的安装非常简单,下载官方提供的WAR包并将其存放在对应的目录下然后运行即可。在Linux下,我们可以通过下面的命令安装、运行Jenkins:
# 下载WAR包
$ wget http://mirrors.jenkins.io/war-stable/latest/jenkins.war
# 运行Jenkins
$ java -jar jenkins.war --httpPort=8080
运行后访问地址http://servip:8080。首次运行时会在控制台中产生一个管理员密码,在第一次登录访问时需要使用该密码,输入密码后按照提示继续把Jenkins安装配置完成即可。图11-16就是配置完成后的Jenkins管理界面。
2、Jenkins配置
前面已经将Jenkins安装并正常运行起来了。但是想真正使用它,还需要进行一些配置。因为Jenkins自带中文语言,而且针对每一项配置都有帮助说明,所以进行配置时也很简单,这也是Jenkins易于使用的一大有点。
前面所构建的微服务项目是基于Maven管理的,因此首先需要在Jenkins中配置JDK和Maven这两个工具。通过“系统管理”|“全局工具配置”命令进入配置界面,如图11-17和图11-18所示。
Jenkins中的全局工具提供了两种安装方式:一种是自动安装,通过指定名称和版本,Jenkins会自动下载并安装,但安装JDK需要提供Oracle官网的账号及密码;另一种自定义安装(在安装界面中取消勾选自动安装选项),但需要开发者先自行下载对应的工具,然后选择工具所在目录即可安装。为了方便,这里选择自动安装,单击“保存”按钮后,Jenkins就开始自动安装了,此时只需静待Jenkins安装完毕即可。如果项目中还需要Git、Gradle、Docker等配置,可以自行完成。
此外,Jenkins还提供了本身的自定义配置,如工作目录、邮件通知配置等,都可以在系统管理下看到相关配置。若需要进行权限控制,比如某个用户只能看到某些任务,Jenkins也提供了完整的权限控制配置,在系统管理下的“管理用户”中就可以完成配置,配置步骤首先新建一个用户,然后转到“全局安全配置”页面进行权限分配。如图11-19所示为新建用户界面,图11-20是权限分配界面。
Jenkins权限配置主要是授权策略,可以根据不用的授权策略控制赋予用户权限,但这种配置方式略显复杂,可以使用Role Strategy Plugin插件简化配置。具体如何进行权限配置,这里就不深入讲解了,读者可以查询官网上的相关文档。
3、构建任务
在新增构建任务前,需要先将所构建的微服务项目源码上传到Git服务器中,如GitHub。然后也可以使用其他版本的管理服务器,如SVN。然后还需要增加一个Maven Integration插件,用于构建Maven风格的项目。安装方式非常简单,在插件管理页面中,选择Maven Integration选项,如图11-21所示,直接安装即可,然后在弹出的安装界面中选择“安装完成后重启Jenkins”选项,等待Jenkins安装完成后重启即可。
安装完重启之后就可以正式创建任务了。接下来我们以构建商品微服务为例,在首页单击“新建任务”按钮,填写任务名称build-productservice,并选择“构建一个maven项目”选项,如图11-22所示。
单击“确定”按钮后进入项目配置界面,如图11-23所示。在该界面中填写微服务源码的GitHub地址,如果使用的是SVN版本管理,可以在“源码管理”项中进行配置,如图11-23所示。
然后对构建触发器进行配置,所谓构建触发器是指由Jenkins主动去检测微服务源码是否有变化,一旦检测到有变化,就可以执行渣滓洞构建并部署。在该配置中有一个Poll SCM选项,意思是通过定时检查源码是否有变化,检测的标准则是SCM软件中的相应版本号,若有更新则拉取最新代码然后执行构建任务,单击该选项右侧的问号图标,会有详细说明,读者可以详细了解一下。例如,在如图11-24所示界面中配置每过5分钟就检查一次源码是否有所变更。
在进入正式构建前,还可以增加Maven命令来检测项目源码是否可以正常编译成功,只有编译成功后才可以继续往下执行具体的构建。配置如图11-25所示。
我们前面已经使用Shell编写了发布脚本,因此这里可以直接进行复用。不过需要进行一些改动,因为在本示例中构建的任务是直接发布微服务到Jenkins所在的服务器上,所以前面脚本中的SSH远程操作部分可以删除。为了能够兼容手工发布的方式(因为我们不知道将来什么时候还需要手工执行),将新建一个文件夹build-jenkins用于存放Jenkins部署需要的脚本,而将原来的部署脚本移入到build目录下,最终商品微服务目录结构图如图11-26所示。
修改后的build.sh脚本文件内容如下:
#! /bin/bash
# 进入脚本所在目录,然后返回项目跟目录
scriptPath="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)"
cd $scriptPath
cd ../../
# Docker容器基本信息
appHome = "/home/hucw/springcloud"
dockerName="productservice"
dockerPort="2200"
dockerJarVolumn="${appHome}/${dockerName}"
# 编译打包
mvn clean package
# 将Jar包复制到目录下
outputPath="${scriptPath}/${dockerName}";
if [ -d ${outputPath} ]; then
rm -rf ${outputPath}
fi
mkdir ${outputPath} && mkdir ${outputPath}/jar
cp ./target/*.jar ${outputPath}/jar/app.jar
# 将整个目录复制到应用目录中
cd ${scriptPath}
cp -rf ${output} ${appHome}/
# 停止删除现有容器
echo "停止删除容器:${dockerName}"
docker stop ${dockerName} && docker rm ${dockerName}
# Docker容器运行命令
echo "运行容器"
docker run --name=${dockerName} -d \
-p ${dockerPort}:${dockerPort} \
-v ${dockerJarVolumn}/jar:/jar \
microserv/openjdk:1.0.0
然后再Jenkins的Post Steps配置中使用Execute shell方式执行构建任务,其配置如图11-27所示。
到这里,商品微服务的Jenkins构建任务已经全部配置完成。单击“保存”按钮创建任务,回到任务页面后单击左侧的“立即构建”链接,将会在构建历史列表中出现构建任务进度,如图11-28所示。
通过单击构建的进度条,进入控制台输出界面,在该界面中可以查看构建日志输出,如图11-29所示。
如果构建过程中没有出现错误,等待构建完毕后,会自动部署运行商品微服务的Docker容器。之后Jenkins会每隔5分钟检测一次源码是否有变化。如果有变化则自动执行该任务。
Jenkins除了用来实现持续构建外,还有其他很多的应用场景,如IOS/android打包平台、自动化测试持续集成等。对于我们的微服务架构应用,通过Jenkins自动构建可以节省大量的时间,当新的功能开发完毕并提交到源码管理器之后,Jenkins就会执行更新部署,从而测试人员可以基于最新的代码进行测试。
微服务编排
在本章的最后,再介绍一下微服务编排的相关知识。有分布式开发经验的读者可能已经发现,前面章节中的示例都是针对一个微服务进行构建发布的,但在实际的生产环境中,所需要部署的微服务不仅仅是一个,而是多个,而这必然会暴露出服务器硬件设施、服务之间的联调、保证服务访问健壮性等一系列的问题。硬件设施问题一般较容易解决,但如何才能够保证微服务架构的健壮性呢?因为微服务的集群部署始终会成为其中最先考虑的方案之一。
集群部署方案其实就是将同一个微服务部署到不同的机器上,通过负载均衡方式来调度,不同用户请求可能会分发到不同的目标服务中,如果某个服务宕机,那就略过此服务而转发请求到其他正常的服务中,这就是传统集群部署,若在Docker微服务架构上使用集群部署,那么要考虑的不但是负载均衡,还需要包含以下几个问题:
- 容器编排;
- 服务调度;
- 容器集群管理;
- 容器健康检查。
前面讲过可以通过Dockerfile创建一个属于自己的镜像,然后使用该镜像来运行一个单独的应用容器。但在实际生产中,通常一个应用需要多个微服务相互配合才能提供完整的功能。如果不进行荣期间的编排,通过docker run命令方式直接启动多个容器是比较烦琐的,如果有几十个、上百个微服务节点需要启动的话,相信再熟练的运维人员也有出错的时候。
单独使用Docker进行微服务的集群部署是无法做到的,必须与其他工具一起配合才能够打造出高可用的集群服务。下面让我们简单了解一下这些工具。
1. Docker Compose工具
Docker Compose是Docker官方的开源项目,负责实现对Docker容器集群的快速编排,允许用户通过一个单独的docker-compose.yml文件将一组相关联的应用容器定义为一个项目。实例如下:
version: "2"
services:
mysqldb:
image: mysql:5.7
ports:
- "3306:3306"
web:
image: tomcat:8.5-jre8
volumes:
- ./webapps:/usr/local/tomcat/webapps
links:
- mysqldb
然后就可以通过docker-compose up命令来整合、启动相关的容器。
2. Docker Swarm工具
Docker Swarm和Docker Compose同样都是Docker官方的开源项目,是一套较为简单的工具。Docker Swarm负责提供Docker容器集群服务,是官方提供给云生态支持的核心方案。通过Docker Swarm项目可以将一群Docker宿主机变成一个单一的虚拟主机,从而让使用者感觉是一台容器。
Docker Swarm可以通过标准的Docker镜像来安装,而无须外部其他依赖。安装完后可以通过命令创建集群并启动Swarm,然后再将其他正在运行的Docker主机逐步加入到集群中。
3. Kubernets(K8s)工具
最后介绍的一个工具就是大名鼎鼎的Kubernetes,简称K8s。Kubernetes是Google十多年大规模容器管理技术Borg的开源版本,用于容器集群管理,可以实现容器集群的自动化部署、扩容、缩容、维护等处理。其所提供的功能基本涵盖了Docker Compose和Docker Swarm的大部分功能。相比这两个工具,无论从技术功能的成熟度,还是社区活跃度看,Kubernetes在容器集群管理方面非常优秀。使用Kubernetes可以轻松做到:
- 自动化容器的部署和复制;
- 随时扩展或收缩容器规模;
- 将容器组织成组,并且提供容器间的负载均衡;
- 很容易地升级应用程序容器的新版本;
- 提供容器弹性,如果容器失效就替换它。
但是Kubernetes入门门槛稍高,首先需要理解它的一些概念,如Pod、Label、Service和Node等,然后需要学习其所提供的一套指令和配置文件,虽然有难度,但对于微服务架构和运维来说还是非常值得的,因此这里推荐读者使用Kubernetes对容器进行集群管理。