【编者的话】这篇博客是Riot的Docker实践系列博客的第三篇,主要讨论了Docker中的数据持久化,并详细介绍了如何使用数据卷容器来持久化Jenkins的日志文件。
在 上一篇博客 中,我们讨论了如何基于Cloudbees镜像来编写自己的Dockerfile,从而更好地控制Jenkins Docker镜像。通过Dockerfile,我们可以设置一些基本的默认值,不需要每次将它们作为 docker run
的参数了。我们也可以定义Jenkins的日志目录,并用 docker exec
来查看运行中的容器的日志。
我也提到了,我们还需要数据持久化技术来使它更有用。容器和其中的数据是转瞬即逝的(ephemeral),只要重启容器,我们就会丢失Jenkins中所有的插件和任务数据。Cloudbees文档提到,需要使用卷(volumes)来保存数据。他们建议挂载宿主机上的目录到容器中,这是一种传统的保存数据的方式。
还有另一种方式,那就是Docker数据卷容器。你可以参考 Docker官方文档 来了解数据卷容器。
本文将涉及:
所谓“主机挂载卷”,指的是Docker宿主机在自己的文件系统中存储数据。此时,当你使用 docker run
运行容器,Docker会将物理存储挂载到你的容器中。
这种方法有很多优点,其中最明显的就是易用性。在更复杂的环境中,存储可能是NAS(Network Attached Storage)或者SATA(Serial Advanced Technology Attachment),有着不错的空间和性能。
缺点就是需要预先设置Docker宿主机上的挂载点,这消除了Docker的两大优点:容器的可移植性和应用的“run anywhere”。如果你需要一个可以运行在任意主机上的真正可移植的容器,那就不能指望宿主机是预先配置好的。
数据卷容器就可以解决这个问题。所谓数据卷容器,其实是一个定义了存储空间的Docker镜像。容器本身只是定义了Docker虚拟文件系统中数据的存储位置。容器中并不运行任何进程,事实上它会在调用 docker run
之后立即“停止”。因此,容器以停止状态退出,其中的数据也是。
优点是Docker容器可以共享数据,而不需要宿主机配置一个正确的挂载点。用户可以通过Docker命令来相互交互,不需要主机参与。
缺点是性能略差,原因是数据是存储在Docker的虚拟文件系统中。因此,如果应用需要非常好的IO性能的话,那么数据卷容器可能不是最理想的选择。然而,对于大多数应用来说,这种性能差异并不显著。另一方面,数据卷容器也造成了额外的复杂性,原因是你的应用至少需要两个镜像(即两个Dockerfile),一个是应用本身,另一个是存储。
需要说明的是,两种方法都是100%有效的,但是取决于你希望它如何工作。我自己的选择是应用应当是尽可能地保持独立性,因此,本文将展示如何使用数据卷容器。
我们从 前一篇博客 中的Dockerfile开始。
FROM jenkins:1.609.1
MAINTAINER Maxfield Stewart
USER root
RUN mkdir /var/log/jenkins
RUN chown -R jenkins:jenkins /var/log/jenkins
USER jenkins
ENV JAVA_OPTS="-Xmx8192m"
ENV JENKINS_OPTS="--handlerCountStartup=100 --handlerCountMax=300 --logfile=/var/log/jenkins/jenkins.log"
对于初学者来说,我们需要创建一个镜像来保存日志文件。我们可以在一个根目录中创建两个Dockerfile,但是,我更喜欢在它们自己的目录中创建不同的Dockerfile。
mkdir jenkins-master
mkdir jenkins-data
将原来的Jenkins Dockerfile放在jenkins-master目录中:
mv Dockerfile jenkins-master
为了确定它仍能工作,我们先构建jenkins-master Dockerfile。
docker build -t myjenkins jenkins-master/.
你可以使用相对路径来指定Dockerfile的位置。如果在单一的根目录中管理多个Dockerfile,使用相对路径非常有用。现在,我们需要为jenkins-data创建一个新的Dockerfile。
jenkins-data
目录中创建 Dockerfile
Jenkins
: 以下是完整的Dockerfile:
FROM debian:jessie
MAINTAINER yourname
RUN useradd -d "/var/jenkins_home" -u 1000 -m -s /bin/bash jenkins
RUN mkdir -p /var/log/jenkins
RUN chown -R jenkins:jenkins /var/log/jenkins
VOLUME ["/var/log/jenkins"]
USER jenkins
CMD ["echo", "Data container for Jenkins"]
保存文件,并构建之:
docker build -t myjenkinsdata jenkins-data/.
这样一来,基础镜像已经包含了Jenkins数据卷。然而,我们需要调整现有的镜像来使用它。
首先,我们启动新的数据卷容器。
docker run --name=jenkins-data myjenkinsdata
你将会看到我们加到CMD中的输出消息。如果你运行:
docker ps
你将会看到没有任何运行中的容器。如果你运行:
docker ps -a
你将会看到新的数据卷容器已经停止了。这是正常的,这就是数据卷容器的运行方式。只要这个容器还在那,那么 /var/log/jenkins
中的数据就会是持久化的,原因是我们将这个目录定义成了数据卷。我们可以将Jenkins主容器使用该数据卷容器,即使删除了主容器,我们的日志仍然会被保存下来。
这个部分是比较容易的。所有困难的工作已经在设置数据卷时做掉了。为了使用它,我们只需要在调用 docker run
时添加 volumes-from
参数即可:
docker run -p 8080:8080 -p 50000:50000 --name=jenkins-master --volumes-from=jenkins-data -d myjenkins
从上面的命令可以看到,我加了一个新的端口映射 50000:50000
,从而能够处理来自基于JNLP的从节点的连接。在未来的博客中,我将会详细说明这一点。
注意我们使用了 jenkins-data
这个容器名。Docker很聪明,可以引用这些名字。你可以通过日志文件内容验证一切正常:
docker exec jenkins-master tail -f /var/log/jenkins/jenkins.log
但是我们怎么知道数据卷挂载是否成功呢?很简单,因为默认情况下Jenkins会以追加模式(append)写入日志文件——一个简单的 start/clean/restart
可以验证挂载是否成功:
docker stop jenkins-master
docker rm jenkins-master
docker run -p 8080:8080 -p 50000:50000 --name=jenkins-master --volumes-from=jenkins-data -d myjenkins
docker exec jenkins-master cat /var/log/jenkins/jenkins.log
在日志文件中,你可以看到第一条和第二条Jenkins启动消息。Jenkins可以崩溃,或者升级,我们总可以保存原来的日志。当然,这也意味着你不得不清理日志和日志目录,就像通用的Jenkins主机一样。
不要忘记 docker cp
。你可以将日志文件从数据卷容器中拷贝出来,即使你丢失了主容器:
docker cp jenkins-data:/var/log/jenkins/jenkins.log jenkins.log
保存日志文件只是一个次要的优势——我们可以在容器重启时,使用它来保存Jenkins数据,例如插件和任务。保存日志文件是展示数据卷容器如何工作的很好的一个例子。
首先,我们在数据卷中添加Jenkins家目录。编辑 jenkins-data/Dockerfile
,更新 VOLUME
命令:
VOLUME ["/var/log/jenkins", "/var/jenkins_home"]
因为已经创建了属于Jenkins用户的目录,我们不需要做任何事,除了添加它为容器挂载点。不要忘记重建新的数据镜像,并在重启前清理原来的容器。
docker rm jenkins-data
docker build -t myjenkinsdata jenkins-data/.
docker run --name=jenkins-data myjenkinsdata
在使用之前,还有一件事情需要处理。CloudBees默认的Docker镜像中,在 jenkins_home
中存储了未压缩的Jenkins war文件,这意味着我们需要在Jenkins运行时保存数据。这不是理想的,因为我们不需要保存这些数据,而且当Jenkins版本变化时这也会引起混淆。因此,我们使用另一个Jenkins启动选项,来把war包移到 /var/cache/jenkins
。
编辑Jenkins-Master Dockerfile,更新 JENKINS_OPTS
:
ENV JENKINS_OPTS="--handlerCountStartup=100 --handlerCountMax=300 --logfile=/var/log/jenkins/jenkins.log --webroot=/var/cache/jenkins/war"
这个选项将设置Jenkins的webroot目录。然而,我们需要保证这个目录存在,并给予Jenkins用户合适的权限。
USER root
RUN mkdir /var/log/jenkins
RUN mkdir /var/cache/jenkins
RUN chown -R jenkins:jenkins /var/log/jenkins
RUN chown -R jenkins:jenkins /var/cache/jenkins
USER jenkins
保存Dockerfile,重建jenkins-master镜像,重启它。使用完之后,我们需要删除数据卷,请注意需要使用 rm -v
。Docker默认情况下不会删除它们,因为你可能希望保留它们,以备不时之需。
docker stop jenkins-master
docker rm -v jenkins-master
docker build -t myjenkins jenkins-master/.
docker run -p 8080:8080 -p 50000:50000 --name=jenkins-master --volumes-from=jenkins-data -d myjenkins
你的容器将会启动。通过运行以下指令,你可以验证WAR文件已经移动了:
docker exec jenkins-master ls /var/cache/jenkins/war
你可以看到未压缩的内容。但是我们怎么知道这种新布局真的保存了Jenkins数据呢?
我们可以容易地测试这一点。当jenkins-master运行时,我们创建一个Jenkins构建任务。
“无用的、仅供测试的(useless for anything but testing)”新任务应该出现在主任务列表中。现在,停止并删除Jenkins容器。
docker stop jenkins-master
docker rm jenkins-master
注意我们并未使用“-v”,只有在希望完全删除数据卷的时候才应该使用“-v”。记住,数据卷就像指针一样。Docker所做的只是在磁盘上创建一个虚拟文件系统——只有一个容器指向它,它就会存在。当jenkins-master容器被删除了,jenkins-data数据卷仍然指向了虚拟文件系统。如果我们使用了“-v”,那么Docker将删除该虚拟文件系统。这会删除jenkins-data对它的引用。
在原来的镜像里,上述操作会删除我们的任务。然而,对于新的镜像,当我们重新创建容器时:
docker run -p 8080:8080 -p 50000:50000 --name=jenkins-master --volumes-from=jenkins-data -d myjenkins
刷新浏览器地址 http://yourdockermachineip:8080
,等待Jenkins启动。我们发现测试任务仍然存在。
如同之前的博客一样,你可以在我的 Github仓库 中找到更新和示例文件。你会注意到makefile又更新了,包含了一个“clean-data”的新命令,可以彻底清理你的数据容器。
至此,我们有了一个具备全功能的Jenkins镜像。我们可以保存日志、任务和插件因为我们把jenkins_home放置在数据卷容器中。还有一个有益的副作用,即便Docker守护进程崩溃,或者宿主机重启,数据都会被持久化。原因是容器会保留停止的容器。
尽管我们可以以此起步,但是在实践中,仍然有很多地方可以优化。例如:
每个优化点都可以写成一篇独立的博客。之后,我们将要设置一个web代理,并讨论如何处理三个容器,这意味着我们将引入Docker Compose。其他的优化点,将在未来的博客中逐一介绍。敬请期待!
原文链接: DOCKER & JENKINS: DATA THAT PERSISTS (翻译:夏彬 校对:)