这里直接进入正题。关于“Docker部署Hadoop”系列,前几篇文章已经把镜像拉取、容器创建、伪分布式环境调通这些步骤都走完了,但这套环境基本还停留在自己机器上。不少人在这一步卡住:测试环境里一切正常,换一台电脑,或者想把环境提交给同事做二次开发,就只能从头再装一遍JDK、再配一遍SSH、再格式化一次NameNode,浪费时间不说,过程中还容易因为版本差异搞出各种莫名其妙的问题。这篇第四篇讲的就是怎么把已经调好的Hadoop容器打包带走——容器导出导入,以及导入之后怎么快速恢复使用。对整个“Docker部署Hadoop”系列来说,这一步补齐了环境迁移的最后一环,让你不再被绑死在一台机器上。
1. 为什么要把Hadoop容器导出导入:迁移与复用场景
1.1 什么时候用得着“导出导入”这套操作
很多人觉得容器这东西,直接用Dockerfile重新build一个不就完了,为什么还要折腾导出导入?我最早也有这个疑惑,直到自己做了一次环境交接才明白。实际工作中遇到最多的几种场景是下面这些。
第一类是环境迁移。你在自己的笔记本上把Hadoop伪分布式环境调好了,但公司给你换了台新电脑,或者你想把活儿挪到一台性能更好的服务器上继续跑。重新搭一遍意味着要把CentOS/Ubuntu基础镜像、JDK版本、Hadoop发行包、一堆配置文件全部重来,快则半小时,慢则半天,而Docker导出导入只需要一条命令加一个tar包。
第二类是团队协作和教学。我见过不少学校实验课、培训机构,老师在一台机器上配好Hadoop环境,然后要把这套环境分发给几十个学生。如果让每个学生都从零开始配,光帮他们排查环境变量就得累死。把配置好的容器导出成镜像文件,学生拿过去直接导入就能用,效率完全不是一个量级。
第三类是备份和时间回溯。容器用久了,配置文件可能会被改乱,HDFS里可能存了一些实验数据。隔一段时间把容器导出一次,相当于给整个环境做了一个快照。后面万一改坏了,直接导入之前的备份文件,几分钟就能回到正常状态,比在容器里一点一点排查省心太多。
第四类是离线环境部署。有些机房内网环境没法直接访问Docker Hub,这时候在能联网的机器上导出镜像或容器文件,拷贝到内网机器上导入,是绕过网络限制最实用的办法。
1.2 导出导入 vs 重新搭建:算笔时间账
我拿自己常用的Hadoop伪分布式环境做过一次对比。从拉取基础镜像开始,装JDK、传Hadoop安装包、改配置文件、配置SSH免密、启动NameNode和DataNode、跑通WordCount示例,一套流程熟练工做下来也得20到30分钟。中间但凡遇到网络波动下载慢一点,或者某个配置文件写错一个路径,时间直接奔着一个小时去。
而用Docker导出导入的方式,导出过程基本就是几秒钟到几十秒的事,主要看容器里数据量的大小。导入再加启动容器,两分钟以内搞定。也就是说,只要你的环境已经配置好一次,后续复制这套环境的边际成本几乎为零。尤其当你需要同时起三台机器做集群实验的时候,手动搭三遍环境简直是灾难,导出镜像再批量导入才是正路。
1.3 先搞清楚:导出的是“容器”还是“镜像”
动手之前,有一个基本概念必须理清,不然很容易在后面的操作中产生误解。Docker里有两种导出方式,一种是docker export,另一种是docker save,它们的操作对象、产物格式、适用场景都不一样。
docker export针对的是容器,它把容器当前的文件系统整体打包成一个tar包,但不会保留镜像的分层结构、历史命令和元数据。导入的时候需要用docker import,导入结果是一个新的镜像,原有容器的端口映射、容器名、网络配置统统不会保留。
docker save针对的是镜像,它把镜像的所有分层、标签、元数据一起打包保存,导入的时候用docker load,导入结果仍然是镜像,可以最大程度还原镜像的原始信息。
用一句话总结:如果你要的是“整个容器的运行状态快照”,用export;如果你要的是“一个完整可追溯、带历史信息的镜像”,用save。在Hadoop场景下,两种方式都能用,但实际踩坑点差别很大,下面会细说。
2. 导出前的准备:先检查这些细节,别等导出完才后悔
2.1 确认容器当前状态
这一步看着简单,但很多人忽略了一个关键前提:容器必须处于运行中或者至少是已创建状态,导出的文件系统才完整。如果你已经docker rm了容器,那就没法导出容器了,这种情况只能去导出对应的镜像。
实际操作时,我习惯先执行一条命令看一眼当前容器列表:
docker ps -a输出里能看到所有容器的ID、名称、状态、端口映射信息。对于Hadoop容器,我重点关注两件事:容器名是什么、端口映射是否正常。比如一个典型的Hadoop伪分布式容器,可能会把NameNode的9870端口(Hadoop 3.x)或50070端口(Hadoop 2.x)映射到宿主机上,这些信息在导出后需要记下来,因为后面导入新容器时要手动重新指定。
另一个容易忽略的点是,导出前最好确认一下容器里的服务进程状态。比如HDFS的NameNode、DataNode是否正常运行,YARN的ResourceManager是否活着。你可以进容器看一眼:
docker exec -it hadoop-test bash jps确保关键进程都在,再执行导出。虽然导出过程中服务进程有没有在跑,理论上不影响文件系统的完整性,但拿到新机器上恢复之后,如果发现HDFS元数据有问题,排查起来会比较痛苦。导出前先确认一次状态,相当于提前排掉一个隐患。
2.2 检查数据放在哪里:容器层还是Volume
这是我在前面几篇文章里反复强调的点,也是导出导入操作里最容易踩的一个大坑。Hadoop容器在运行过程中会产生大量数据,包括NameNode的元数据(在dfs/name目录下)、DataNode的数据块(在dfs/data目录下)、日志文件等。
这些数据如果直接写在容器的可写层里,那么docker export导出的是包含这些数据的完整文件系统,导出包会比较大,但恢复之后数据直接可用。如果这些数据是挂载在Volume或者宿主机目录上的,比如启动容器时指定了-v /host/data:/opt/hadoop/data,那docker export导出的是不包含挂载卷里的内容的。
这个坑我踩过一次。有一次帮同事导出一套已经跑了一段时间的Hadoop环境,导出完之后在另一台机器上导入、启动容器,一看NameNode起不来,报错说元数据目录是空的。排查了半天才发现,他当时把HDFS的数据目录挂载到了宿主机路径上,导出tar包的时候这些数据根本不在里面。
所以导出前,先确认一下你的容器启动命令里有没有挂载Volume:
docker inspect hadoop-test看Mounts部分。如果Source和Destination里挂了数据目录,你要么把宿主机上对应的目录一起拷贝走,要么在导出前把HDFS的数据重新落到容器内部目录里(比如把core-site.xml里的数据目录改回容器内路径),再执行导出。
2.3 清理临时文件和日志,缩小导出体积
容器用久了,里面会积累不少没用的东西,包括系统包管理器缓存、Hadoop运行日志、临时文件等。这些文件对恢复没有帮助,但会白白增加导出包的体积。尤其当你的tar包要通过U盘或者网盘传输时,动辄几个GB的文件传起来非常痛苦。
我习惯在导出前做几个简单的清理动作。先清掉包管理器的缓存:
docker exec -it hadoop-test bash yum clean all # CentOS系 # 或者 apt-get clean # Ubuntu系然后是Hadoop的日志目录。Hadoop运行一段时间后,$HADOOP_HOME/logs下面会有大量日志文件,这些日志对迁移没有意义,可以放心清理:
docker exec -it hadoop-test bash rm -rf $HADOOP_HOME/logs/*如果你在容器里装过一些临时下载的安装包,比如从宿主机拷贝进去的Hadoop tar包,解压完之后也可以删掉,省出几百MB甚至上GB的空间。
另外提醒一下,清理操作和导出操作之间最好隔一小会儿,让文件系统的状态稳定下来,再执行导出。不要清理完立刻导出,也不要在容器内有大量数据写入的时候导出,否则导出的文件系统可能处于一种“写了一半”的状态。
3. 两种导出方式深度对比:docker export 与 docker save 怎么选
3.1 原理层面:一个打包文件系统,一个打包镜像分层
docker export的原理比较直接:它把容器整个文件系统看作一棵目录树,然后把这棵目录树的所有文件和目录打包成一个tar归档文件。它不关心这个容器是怎么构建出来的,不保留镜像的历史分层,也不保留容器启动时的配置信息。一句话,它打的是“当前这一刻的实物快照”。
docker save的原理则完全不一样。它把镜像的所有分层一层一层地打包,每一层对应的内容都保留下来,同时还包括镜像的元数据、标签、环境变量、默认CMD等信息。导入的时候,Docker能够把这些层重新组装成一个完整的镜像,docker history能看到这个镜像是怎么一步步构建出来的。
实际使用中,这个区别带来一个直接后果:docker save导出的包往往比docker export更大,因为它包含了多个分层的内容,而且同一份文件在不同层里可能重复出现。但它的优势也很明显,镜像的历史信息完整,后面基于这个镜像再去二次构建Dockerfile,基础层是可以被Docker缓存复用的。
3.2 使用场景:Hadoop容器迁移更适合哪种
如果是针对Hadoop这种需要保留运行数据的场景,我的建议是:
- 如果你希望迁移后HDFS数据、NameNode元数据、配置修改全部保留,并且不介意丢失镜像构建历史,那么用
docker export,导入后得到的镜像能最快恢复到可用状态。 - 如果你更在乎的是镜像的可复现性,希望迁移后的环境能基于这个镜像继续打标签、继续改Dockerfile迭代,那么用
docker save更合适。
不过在实际操作中,我还有一种组合用法,这里分享给你:先用docker export导出一份“带数据版本”的容器快照,用于日常迁移和备份;再用docker commit把当前容器转换成镜像,然后用docker save导出一份“纯环境版本”的镜像文件,用于后续二次开发和镜像分发。两个包各司其职,应对不同需求。
3.3 再补一个易混淆点:docker commit 和 docker export 的关系
docker commit也经常和这两个命令放在一起比较。它同样是把容器转换成镜像,但转换过程是在Docker内部完成的,产物直接变成一个新的镜像,而不是一个tar文件。如果要用docker commit迁移环境,通常还要配合docker save把生成的镜像再导出成文件。
三者的关系可以这样理解:docker export不生成镜像,只生成文件系统快照;docker commit生成镜像但不生成文件;docker save把镜像打包成文件。迁移到另一台机器时,最终面向的都是tar文件,所以export配上import是一条路径,commit配上save配上load是另一条路径。两条路径都能走通,但效果和侧重点区别很大。
4. 实操:把Hadoop容器导出成可传输的tar包
4.1 用 docker export 导出容器快照
先演示最简单、最常用的一种方式。假设当前环境里有一个正在运行的Hadoop容器,名字叫hadoop-test,执行下面的命令把它导出:
docker export hadoop-test -o hadoop-container.tar或者用另一种写法:
docker export hadoop-test > hadoop-container.tar两种写法等价。-o参数指定输出文件的路径和名字,不写的话默认输出到标准输出,终端里会直接涌出一堆二进制内容,所以生产环境里-o是更推荐的写法。
导出完成后,在宿主机上确认一下文件信息:
ls -lh hadoop-container.tar正常情况下你会看到几GB甚至几十GB的文件,具体取决于容器里Java、Hadoop安装目录的大小,以及HDFS数据积累了多少。如果文件大小只有几百MB,就要回头检查一下是不是数据目录挂在Volume里没打进去了。
4.2 用 gzip 压缩导出包,传输更省力
tar包不压缩的话,传输起来会比较慢,尤其是要拷到远程服务器时。我一般导出后顺手压缩一下,一条命令搞定:
docker export hadoop-test | gzip > hadoop-container.tar.gzLinux下传输压缩包比传输原始tar包快不少,特别是在网络环境一般的情况下体感很明显。唯一的代价是导入的时候需要先解压,不过现在机器性能都不差,多花十几秒换传输时间的大幅缩短,这笔账很划算。
如果你的机器上装了pigz,还可以用多线程压缩进一步提速。不过这个属于锦上添花,单机环境下gzip已经完全够用了。
4.3 用 docker save 导出镜像(备用方案)
如果你打算走commit + save这条路径,操作也不复杂。先在原有容器基础上生成一台新镜像:
docker commit hadoop-test hadoop-env:snapshot然后把这个新镜像保存成tar包:
docker save hadoop-env:snapshot -o hadoop-image.tar和docker export不同,docker save可以同时导出多个镜像,并且会完整保留镜像的分层信息。注意看你的文件大小,如果包含相同的父镜像层,docker save的体积通常比docker export大,这属于正常现象。
4.4 导出后的文件完整性校验
文件传到目标机器之前,建议先做一次完整性校验。最们的做法是给tar包算一个SHA256值,记录在案:
sha256sum hadoop-container.tar.gz把输出的哈希值同文件一起拷贝走。在目标机器上导入之前,先重新计算一遍哈希,比对一致再执行导入,能避免文件在传输过程中损坏导致导入失败。尤其是通过U盘拷贝、网盘下载这种非可靠链路,这个步骤很值得做。
5. 实操:把容器导入到新机器并恢复Hadoop服务
5.1 docker import 导入容器快照
把压缩包拷贝到目标机器后,先解压再导入。假设文件是gzip压缩过的,两种方式都行。第一种是先解压再导入:
gzip -d hadoop-container.tar.gz docker import hadoop-container.tar hadoop-env:imported第二种是直接用管道把解压和导入合并成一条命令:
cat hadoop-container.tar.gz | docker import - hadoop-env:imported这里hadoop-env:imported是导入后新镜像的仓库名和标签,可以自己定义。这个步骤执行完之后,用docker images确认一下新镜像是否已经在列表里了。
5.2 启动导入后的Hadoop容器
docker import只是把文件系统转成了镜像,这时候还没有容器。需要手动创建并启动容器。以Hadoop伪分布式环境为例,启动命令要基于你之前记录的端口映射信息,把关键端口重新映射到宿主机上:
docker run -d --name hadoop-restored \ -p 9870:9870 \ -p 8088:8088 \ -p 9000:9000 \ hadoop-env:imported \ /usr/sbin/sshd -D这段命令的含义是:以后台模式运行一个名为hadoop-restored的容器,把容器的9870、8088、9000端口分别映射到宿主机对应端口,然后启动SSH服务作为前台进程,保证容器不会启动后立刻退出。由于docker import不保留CMD指令,这里手动指定启动命令是很有必要的。
如果你之前用的是docker save + docker load方式导入了原始镜像,那么镜像里自带的CMD和ENTRYPOINT会保留下来,启动命令可以更简单一些。但即便如此,Hadoop容器在启动后服务进程一般也不会自动拉起,需要手动进容器里做一次启停操作。
5.3 进入容器并验证Hadoop服务是否正常
容器启动后,先进去看一眼进程状态:
docker exec -it hadoop-restored bash jps如果之前导出时数据保留得比较完整,你大概率能看到NameNode、DataNode进程正在运行。如果进程没起来,不要慌,按顺序手动启动。先启动HDFS:
$HADOOP_HOME/sbin/start-dfs.sh再启动YARN:
$HADOOP_HOME/sbin/start-yarn.sh之后用jps重新确认进程列表,再打开浏览器访问宿主机IP的9870端口(NameNode Web UI)和8088端口(YARN ResourceManager UI),能正常打开页面就说明环境基本恢复了。
如果NameNode启动时报错,常见原因是元数据目录不完整,或者由于IP变化导致HDFS想要重新格式化。这个问题在实操中很常见,我已经把排查思路整理在下一节。
6. 常见问题与排查技巧实录
6.1 导入后NameNode启动失败,一直报格式相关错误
遇到最多的一个问题就是:导入镜像、启动容器后,执行start-dfs.sh提示NameNode启动失败,去看日志会发现类似NameNode is not formatted或者There appears to be a gap in the transaction ID之类的信息。这类问题的根源大多是HDFS的元数据目录不完整,或者目录数据与集群ID不匹配。
排查思路分两步。先看dfs.namenode.name.dir指向的目录是否为空:
ls -la $HADOOP_HOME/data/name/current/如果目录为空,说明导出时元数据确实没有被打包进去。这时候可以手动重新格式化NameNode:
hdfs namenode -format但格式化之后,之前存在HDFS里的数据会全部丢失。这是最坏情况,一般只适合新环境或者用来恢复“环境本身”而不是“环境里积累的数据”。
如果目录里有文件但依然出错,可以考虑是集群ID不一致。HDFS的VERSION文件里保存着clusterID,NameNode和DataNode的clusterID必须一致。不一致的话,可以手动把DataNode的clusterID改成和NameNode一致,或者清理DataNode数据目录让它重新注册。
6.2 容器能起来但Hadoop Web界面打不开
容器正常启动了,jps也能看到进程,但浏览器访问9870端口就是打不开。这种情况八成是端口映射没配对。回顾一下你的docker run命令,确认一下-p 9870:9870是不是真的写了。如果用docker ps看到端口映射为0.0.0.0:9870->9870/tcp,基本就是映射成功了。
还有一种情况是容器内的Hadoop服务绑定了固定IP。如果你在core-site.xml里把fs.defaultFS配成了某个具体的IP地址,比如hdfs://192.168.1.100:9000,而导入后的容器IP已经变了,服务之间就会互相找不到。这时候需要修改core-site.xml和hdfs-site.xml里的IP为0.0.0.0或新环境的实际IP,然后重启Hadoop集群。
6.3 导入后容器启动立即退出
用docker run启动导入的容器,几秒钟后容器就退出了,通过docker logs hadoop-restored查看日志,发现没有输出任何内容。这个问题通常是因为启动命令写错了。docker import不会保留镜像的CMD和ENTRYPOINT,你必须手动指定一个长期运行的进程。推荐用/usr/sbin/sshd -D作为前台进程,容器就能持续运行。如果你之前用的是/bin/bash,那容器启动后会立刻退出,因为它没有任何阻塞任务在跑。
6.4 导出的tar包太大,传输和导入都慢
tar包体积动辄几个GB,U盘拷贝都嫌慢,怎么办?我常用的降体积方案有三个。第一,导出前清理容器里的日志、缓存、临时文件,这个前面说过了。第二,用gzip压缩,Hadoop安装目录里的文本配置文件、日志文件压缩率通常很高,实测能减少40%到60%的体积。第三,考虑用docker export而非docker save,前者的包一般更小,而且对于Hadoop这种“只要数据保留就行”的场景完全够用。
6.5 快速自查清单
我把上面这些经验总结成了一份速查表格,方便你实际操作时挨个对照。
| 问题现象 | 常见原因 | 排查与解决思路 |
|---|---|---|
| 容器启动即退出 | 启动命令没有阻塞进程 | 手动指定/usr/sbin/sshd -D作为前台进程 |
| NameNode启动失败 | 元数据目录不存在或clusterID不一致 | 检查dfs.namenode.name.dir目录内容,必要时重新格式化或同步clusterID |
| Web界面打不开 | 端口映射丢失或服务绑定旧IP | 确认docker run的-p参数,修改配置文件中的IP为0.0.0.0或新IP |
| HDFS里数据不见了 | 数据目录在Volume里,导出时没包含 | 导出前把数据目录改到容器内路径,或把宿主机挂载目录一起拷贝走 |
| tar包体积过大 | 日志、缓存、重复分层占用空间 | 清理日志和包管理器缓存,用gzip压缩,优先用docker export |
最后分享一个我个人的小习惯。每次导出一套Hadoop环境后,我都会随手在tar包旁边放一个纯文本文件,记录下容器的启动命令、端口映射、关键配置文件的修改点和数据目录位置。这个文件我管它叫“环境说明书”。表面上看起来是多了一点点工作量,但等你三个月后需要从备份里恢复一套环境,或者同事拿着你的tar包在另一台机器上怎么也起不来的时候,这份说明能帮你省下大量沟通和排查成本。迁移结束之后,这套环境就真正成为了你手里可以随时复用的资产,而不只是某台机器上的一个运行实例。