见meeting之类的词。前段时间Node.js正式发布了Node8.9.3LTS版本,官网首页提示新版本有重要安全更新,“重要安全发布,请立即更新!”,然后我立马入手升级公司产品各模块的Node版本。发布基础镜像我们所有的项目都是使用Node.js实现的,使用Docker容器交付部署。因此,升级所有产品的在线Node.js版本,只需要在打包Docker镜像时更新配置文件Dockerfile即可。下面是一个典型项目的Dockerfile配置文件。FROMmaichong/node:8.9.1MAINTAINER麦虫云RUNapt-getupdate\&&apt-getinstall-y--no-install-recommendsopenssh-client\&&apt-getclean\&&rm-rf/var/lib/apt/lists/*COPYpackage.json/app/WORKDIR/appRUNnpminstall--production\&&rm-R~/.npm*COPY./appCMDnodeindex.js脚本含义:基于麦冲/node:8.9.1基础镜像构建记录作者信息运行apt,为镜像安装openssh-client软件将代码package.js复制到目标镜像中设置镜像容器的工作目录为/app在镜像中运行npm即可installdependencies将代码复制到目标镜像中在/app目录下设置镜像的启动命令技巧:这里先复制package.js,然后安装npm依赖,最后复制代码。这样做的好处是项目代码经常变动,但是项目的npm依赖一般不会变动。这种顺序安排可以有效利用dockerbuildcache,跳过npm依赖安装的漫长时间,无需更改package.js,大大提高了打包速度。从上面的Dockerfile配置文件可以看出,我们的项目都依赖于基础镜像maichong/node,这是一个我们自己发布的Node.js镜像。与官方Node.js镜像的主要区别在于,我们的镜像使用了阿里云的debianapt源镜像,使用registry.npm.taobao.org作为npm加速镜像。所以,首先要做的是发布一个新版本的Node.js镜像:maichong/node:8.9.3。发布过程非常简单。编写基础镜像的Dockerfile,在本地构建镜像,最后将镜像推送到Docker官方仓库。完毕!基础镜像的Dockerfile可以在这里找到https://github.com/liangxingc…。之所以在本地构建,而不是使用Docker仓库的自动化构建,是因为我们的镜像使用了国内阿里云的Debianapt源,再加上一些奇葩的网络因素,在DockerHub中自动构建的时候,apt升级总是失败。升级项目maichong/node:8.9.3基础镜像准备好了,接下来开始升级公司的各个项目。我们所有的项目都基于麦虫网进行管理,包括代码仓库的持续集成流程、自动化构建和自动化部署。修改项目的Dockerfile,将基础镜像修改为maichong/node:8.9.3,然后将代码提交到代码仓库,然后起来泡杯咖啡,悠闲的等待ImpulseCloud自动编译打包Docker镜像上线,并自动上传最新的镜像部署到服务器集群,完成升级。事故!当我端着现煮的咖啡回到自己的工位时,却意外地发现ImpulseCloud在线编译构建失败了!心凉了,我*,哪里有BUG惹事了?!赶紧上网查看构建日志,导致失败的日志关键部分如下:2017-12-1210:04:05Step5/12:RUNapt-getupdate&&apt-getinstall-y--no-install-recommendsopenssh-client&&apt-getclean&&rm-rf/var/lib/apt/lists/*2017-12-1210:04:05--->在c3fb701ef9252017-12-1210中运行:04:06Ignhttp://mirrors.aliyun.comjessieInRelease...2017-12-1210:04:071s获取11.1MB(6028kB/s)2017-12-1210:04:09阅读packagelists...2017-12-1210:04:10构建依赖树...2017-12-1210:04:10读取状态信息...2017-12-1210:04:10以下extra将安装的包:2017-12-1210:04:10adduserdebconfdebianutilsdpkg...(共40个包)2017-12-1210:04:10建议包:...2017-12-1210:04:10推荐包:...2017-12-1210:04:120个升级,40个新安装,0个删除,0个未升级。2017-12-1210:04:12需要得到16.7MBofarchives.2017-12-1210:04:12之后是操作,将使用44.7MB的额外磁盘空间。2017-12-1210:04:12获取:1http://mirrors.aliyun.com/debian/jessie/mainsensible-utilsall0.0.9[11.3kB]...2017-12-1210:04:22dpkg:errorprocessingarchive/var/cache/apt/archives/libgcc1_1%3a4.9.2-10_amd64.deb(--unpack):2017-12-1210:04:22预依赖问题-未安装libgcc1:amd64为了限制篇幅,对上面贴出的构建日志进行了简化,大量apt网络请求和解压包日志替换为...,apt显示大量必要或推荐的安装包也结束与...省略。日志报错是在dockerbuild打包过程中,运行apt安装openssh-client失败。最直接的错误是openssh-client依赖的libgcc1包安装失败。第一反应是,难道又是apt软件仓库依赖的问题?!哎,我怎么又说了?apt是Debian和Ubuntu系统使用的包管理器。它类似于Node.js世界中的npm。用于管理和安装Linux系统中的各种软件包。各种软件包有不同的版本并且相互依赖。apt安装软件时,会从网络服务器获取所需的软件包和相关的依赖包。这里的“网络服务器”称为“源”,所以上面说的麦冲/节点镜像使用国内阿里云的“源”是为了加快apt安装软件时的网速。阿里云的“源”服务器是Debian官方源的加速镜像。由于apt管理的软件包数量多,版本多,相互依赖,对指定版本相互依赖,很容易造成依赖问题。比如A依赖B的1.0版本,C依赖B的2.0版本,如果安装了A,再安装C会报错,因为B的1.0和2.0版本不能在系统中共存。顺便说一句,npm使用多层node_modules目录嵌套来解决一个包的不同版本共存的问题。仔细观察安装openssh-client时安装的40个依赖包,发现居然有dpkg这么一个很基础的包,怎么可能?!dpkg是Debian系统最基本的包管理器。apt依赖于dpkg。所有能运行的Debian系统都必须有dpkg包。难道说……基础形象“差”了?构建Docker镜像后,它必须能够正确执行。我从未见过某个图像本身是“坏的”。也许这一次我需要睁开眼睛了!之所以在这里说“坏”,是因为镜像中整个apt管理依赖乱七八糟,也可能是某些原因造成了镜像的“损坏”!在本地执行构建命令:dockerbuild-ttest./然后构建成功!说明基本图像没问题,难道又是环境差异的问题?!本地构建日志如下:Step5/12:RUNapt-getupdate&&apt-getinstall-y--no-install-recommendsopenssh-client&&apt-getclean&&rm-rf/var/lib/apt/lists/*--->Runningin237bec98f4e6Ignhttp://mirrors.aliyun.comjessieInRelease...Fetched11.1MBin11s(972kB/s)读取包列表...构建依赖树...读取状态信息..将安装以下额外包:libbsd0libedit2建议包:ssh-askpasslibpam-sshkeychainmonkeysphere推荐包:xauth将安装以下新包:libbsd0libedit2openssh-client0newlyupgraded,toinsm0retallyupgraded,38未升级。从日志中发现,我在本地构建时,apt安装openssh-client只需要安装3个包。这怎么可能?!同样的基础镜像也使用了阿里云的源。难道是……阿里云源镜像数据有问题?阿里云的源图服务是建立在CDN之上的。CDN的目的是让用户就近访问不同地理位置的服务器,让不同地区的用户可以快速访问。因此,即使使用相同的源镜像地址,但在不同位置更新apt时,请求的阿里云服务器是不同的。那么有可能是阿里CDN数据不同或者国外Debian官方服务器数据同步不全导致的问题。登录到远程PulseCloudBuilderRunner,这是一个专门用于执行用户自动化集成的服务器,我们的在线构建首先发生在该服务器上。在Runner上pingmirrors.aliyun.com得到的源镜像IP地址确实和我本地不一样。然后我在Runner上获取到的IP地址加入到我本地的DNS解析中,这样我在本地运行apt的时候,我访问的服务器和Runner上是一样的。但是,在本地再次构建成功。这说明不是阿里云源镜像数据的问题。同一个镜像,同一个源服务器,apt运行结果却不一样,难道是……远程主机上的镜像损坏了?会不会是我本地拉取的镜像是好的,而远程Runner拉取的镜像是坏的?这是不可能的,因为Docker拉取镜像后会校验镜像,所以多人拉取同一个镜像版本后,可以保证大家拉取的是一模一样的。不可能有人拉出损坏的图像。似乎已经排除了所有可能,问题依旧没有解决。真是莫名其妙。手中的线索全部被斩断,案件的侦查进入了僵局。我把遇到的情况跟团队说明了,隔壁桌的李老师断言:“肯定是环境不同导致的bug!”是的,肯定是环境差异的问题,但是已经排除了所有环境差异,同样的网络环境,同样的构建配置,同样的镜像……我们知道Docker的优势关键在于将软件和运行环境打包成一个镜像,一个镜像在不同的外部环境中执行,可以保证镜像中程序所在的镜像内部环境。完全一样,因为Docker运行容器的时候,环境是完全隔离的。不,没有“完全”的隔离,难道是……宿主内核的区别?分别在本地和Ranner上执行uname-a,得到的内核版本不同。本地:Linuxlocal4.4.0-103-generic#126-UbuntuSMPMonDec416:23:28UTC2017x86_64x86_64x86_64GNU/LinuxRunner:Linuxrunner024.4.0-98-generic#121-UbuntuSMPTueOct1014:24:03UTC2017x86_64x86_64x86_64GNU/Linux本地Linux内核版本为4.4.0-103-generic,远程Runner主机内核版本为4.4.0-98-generic。尝试升级远程Runner主机内核:apt-getupdateapt-getdist-upgrade将主机内核升级到最新版本4.4.0-103-generic。重启Runner后,在线Docker打包成功!小结事实上,Docker在运行一个容器的时候,并没有完全隔离所有的环境。例如,主机内核不是孤立的。Docker不像VMware,在启动虚拟机的时候就完全虚拟出一个硬件环境,然后从头加载虚拟机操作系统的内核。Docker容器在运行时,仍然依赖于宿主机内存中正在运行的内核。虽然不同的容器使用不同的镜像,但是镜像的本质是一个压缩的文件系统包,它可以让你的容器在运行时拥有自定义的文件系统和文件系统。软件组,在执行容器程序的时候,不会给你从头启动一个系统内核。所以我们称Docker为一种轻量级的虚拟化技术。本文遇到问题的原因应该是在构建maichong/node:8.9.3镜像时,在4.4.0-103-generic版本的内核环境下执行,apt安装的一系列软件适用于4.4。0-103-generic版本,当在Runner上执行构建时,内核再次变为4.4.0-98-generic。可能是apt识别出之前基础镜像中的某些软件无效,需要重新安装。以前从来没有见过这种情况,所以写了一篇文章记录一下。可以看出,编译DockerHub的大部分镜像时的内核环境和我们本地的内核是不一样的。为什么apt就在这个时候出错?apt管理软件时,系统内核对apt有什么具体影响?让我们看看未来什么时候合适。