Docker volume speed up npm install

上一节决定在Jenkins中采用Docker作为构建环境,于是就可以为所欲为的使用各种node版本编译我们的项目。解决了版本切换问题。然而,Docker设计的目的就是纯净的执行环境,因此每次运行docker容器都相当于一个新的系统,所以就不会有缓存。而npm install需要下载大量的依赖,我们总不能每次都去下载吧。而且,node-sass的下载速度总是让人以为卡死了。作为CI,每天即便达不到成千上万次构建也算很频繁了。

经调研google, 复制node_modules可以快速加载依赖,但可操作性太差,需要定制脚本。复用npm cache基本可以解决离线缓存,减少联网下载的次数。

创建volume

通过如下方式可以在docker磁盘上创建一个磁盘卷npm_cache

sudo docker volume create npm_cache
> sudo docker volume ls
DRIVER VOLUME NAME
local 0cf39840bd652ef744137b177537357b1ce18a1b55521e381524501996db2ea2
local npm_cache

初始化是空的,位置在

> sudo docker volume inspect npm_cache
[
{
"CreatedAt": "2019-07-26T14:17:29+08:00",
"Driver": "local",
"Labels": {},
"Mountpoint": "/data/docker/volumes/npm_cache/_data",
"Name": "npm_cache",
"Options": {},
"Scope": "local"
}
]

使用volume, 这里通过-v指令在运行容器时挂载:

sudo docker run -d -v npm_cache:/root/.npm  -v `pwd`:/tmp  node

上述命令的含义是:

运行node容器,挂载磁盘npm_cache到/root/.npm, 挂载当前项目路径到/tmp. 这样就可以在/tmp目录下构建本项目。

测试构建时间

比如如下依赖,分别采用cache和不采用cache的构建时间比较

"dependencies": {
"axios": "^0.19.0",
"element-ui": "^2.11.0",
"vue": "^2.6.10",
"vue-router": "^3.0.7",
"vuex": "^3.1.1"
},
"devDependencies": {
"babel-core": "^6.26.3",
"node-sass": "^4.12.0"
}

不挂载.npm

added 220 packages from 163 contributors and audited 756 packages in 268.127s

found 0 vulnerabilities

挂载.npm并已有cache

added 220 packages from 163 contributors and audited 756 packages in 9.38s

found 0 vulnerabilities

同时,可以在本地磁盘看到缓存的依赖

root@ryan-computer:/data/docker/volumes/npm_cache/_data# tree -L 2
.
├── anonymous-cli-metrics.json
├── _cacache
│   ├── content-v2
│   ├── index-v5
│   └── tmp
├── _locks
└── node-sass
└── 4.12.0

Jenkins中使用

首先,安装Docker Pipeline Plugin.

使用Jenkinsfile构建流水线。

在Jenkinsfile中添加stage

stage('Build') {
echo "2. Build" try {
docker.image('node:12.6.0-buster').inside(" -v npm_cache:/home/node/.npm") {
sh 'npm install --registry=https://registry.npm.taobao.org;'
sh 'npm run test:ci --registry=https://registry.npm.taobao.org'
}
} catch (Exception ex) {
updateGitlabCommitStatus name: 'build', state: 'failed'
throw ex;
} updateGitlabCommitStatus name: 'build', state: 'success'
}

上述脚本将会在node中构建我们的项目并执行test. 本质上,上述命令会转换为

docker run -t -d -u 1000:1000 -v npm_cache:/home/node/.npm -w /data/opt/jenkins/workspace/dbrest-web_master -v /data/opt/jenkins/workspace/dbrest-web_master:/data/opt/jenkins/workspace/dbrest-web_master:rw,z -v /data/opt/jenkins/workspace/dbrest-web_master@tmp:/data/opt/jenkins/workspace/dbrest-web_master@tmp:rw,z -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** node:12.6.0-buster cat

需要注意的地方

我们为什么使用docker run -u 1000:1000?

因为执行命令的用户是Jenkins,Jenkins的id是1000,为了防止容器里构建的dist等asset文件权限变成root,需要使用当前dir拥有者的权限。说的有点绕,换句话说,docker将当前文件作为工作目录,构建会产生dist文件,这个dist文件的权限取决于runner。

cache为什么挂载到/home/node/.npm ?

这里就是docker初学者容易疏漏的地方,当docker -u uid:gid来运行容器的时候, 容器里的执行用户是这个id。有意思的是,node官方docker镜像的Dockerfile也专门创建了一个用户node, 其id也是1000. 所以,我们的容器在宿主机器以1000的Jenkins用户执行,容器内部以1000的node执行。

因此,workspace下node项目就会被编译。如果不喜欢使用Jenkins docker插件,也可以直接使用docker命令。

复用cache前后的对比

使用cache后时间reduce是分钟级别的。


以下来自官方文档:

设计流水线的目的是更方便地使用 Docker镜像作为单个 Stage或整个流水线的执行环境。 这意味着用户可以定义流水线需要的工具,而无需手动配置代理。 实际上,只需对 Jenkinsfile进行少量编辑,任何 packaged in a Docker container的工具, 都可轻松使用。

Jenkinsfile (Declarative Pipeline)
pipeline {
agent {
docker { image 'node:7-alpine' }
}
stages {
stage('Test') {
steps {
sh 'node --version'
}
}
}
}

当流水线执行时, Jenkins 将会自动地启动指定的容器并在其中执行指定的步骤:

[Pipeline] stage
[Pipeline] { (Test)
[Pipeline] sh
[guided-tour] Running shell script
+ node --version
v7.4.0
[Pipeline] }
[Pipeline] // stage
[Pipeline] }

容器的缓存数据

许多构建工具都会下载外部依赖并将它们缓存到本地以便于将来的使用。 由于容器最初是由 "干净的" 文件系统构建的, 这导致流水线速度变慢, 因为它们不会利用后续流水线运行的磁盘缓存。 on-disk caches between subsequent Pipeline runs.

流水线支持 向Docker中添加自定义的参数, 允许用户指定自定义的 Docker Volumes 装在, 这可以用于在流水线运行之间的 agent上缓存数据。下面的示例将会在 流水线运行期间使用 maven container缓存 ~/.m2, 从而避免了在流水线的后续运行中重新下载依赖的需求。

Jenkinsfile (Declarative Pipeline)
pipeline {
agent {
docker {
image 'maven:3-alpine'
args '-v $HOME/.m2:/root/.m2'
}
}
stages {
stage('Build') {
steps {
sh 'mvn -B'
}
}
}
}

使用多个容器

代码库依赖于多种不同的技术变得越来越容易。比如, 一个仓库既有基于Java的后端API 实现 and 有基于JavaScript的前端实现。 Docker和流水线的结合允许 Jenkinsfile 通过将 agent {} 指令和不同的阶段结合使用 multiple 技术类型。

Jenkinsfile (Declarative Pipeline)
pipeline {
agent none
stages {
stage('Back-end') {
agent {
docker { image 'maven:3-alpine' }
}
steps {
sh 'mvn --version'
}
}
stage('Front-end') {
agent {
docker { image 'node:7-alpine' }
}
steps {
sh 'node --version'
}
}
}
}

脚本化流水线的高级用法

运行 "sidecar" 容器

在流水线中使用Docker可能是运行构建或一组测试的所依赖的服务的有效方法。类似于 sidecar 模式, Docker 流水线可以"在后台"运行一个容器 , 而在另外一个容器中工作。 利用这种sidecar 方式, 流水线可以为每个流水线运行 提供一个"干净的" 容器。

考虑一个假设的集成测试套件,它依赖于本地 MySQL 数据库来运行。使用 withRun 方法, 在 Docker Pipeline 插件中实现对脚本化流水线的支持, Jenkinsfile 文件可以运行 MySQL作为sidecar :

node {
checkout scm
/*
* In order to communicate with the MySQL server, this Pipeline explicitly
* maps the port (`3306`) to a known port on the host machine.
*/
docker.image('mysql:5').withRun('-e "MYSQL_ROOT_PASSWORD=my-secret-pw" -p 3306:3306') { c ->
/* Wait until mysql service is up */
sh 'while ! mysqladmin ping -h0.0.0.0 --silent; do sleep 1; done'
/* Run some tests which require MySQL */
sh 'make check'
}
}

该示例可以更进一步, 同时使用两个容器。 一个 "sidecar" 运行 MySQL, 另一个提供执行环境, 通过使用Docker 容器链接。

node {
checkout scm
docker.image('mysql:5').withRun('-e "MYSQL_ROOT_PASSWORD=my-secret-pw"') { c ->
docker.image('mysql:5').inside("--link ${c.id}:db") {
/* Wait until mysql service is up */
sh 'while ! mysqladmin ping -hdb --silent; do sleep 1; done'
}
docker.image('centos:7').inside("--link ${c.id}:db") {
/*
* Run some tests which require MySQL, and assume that it is
* available on the host name `db`
*/
sh 'make check'
}
}
}

上面的示例使用 withRun公开的项目, 它通过 id 属性具有可用的运行容器的ID。使用该容器的 ID, 流水线通过自定义 Docker 参数生成一个到inside() 方法的链。

The id property can also be useful for inspecting logs from a running Docker container before the Pipeline exits:

sh "docker logs ${c.id}"

来源参考

Docker volume speed up npm install的更多相关文章

  1. docker volume创建、备份、nfs存储

    docker存储volume #环境 centos7.4 , Docker version 17.12.0-ce docker volume创建.备份.nfs存储 #docker volume 数据存 ...

  2. Docker学习笔记之docker volume 容器卷的那些事(一)

    预览目录 volume 方式 相关用例 使用方式 使用 volume driver bind mount 方式 相关用例 使用方式 配置selinux标签 配置macOS的安装一致性 tmpfs 方式 ...

  3. (转)Docker volume plugin - enabled create local volume on docker host

    原文地址:https://hub.docker.com/r/cwspear/docker-local-persist-volume-plugin/ Short Description Create n ...

  4. 为什么npm install 经常失败

    Hello 您好,我是大粽子.深耕线上商城的攻城狮(程序员)一枚. 前言 这段时间真的是忙,最近能抽时间搞搞大家在自己环境中遇到的各种问题了,我呢就是见不得我的代码在你的电脑运行不起来的.就像姜子牙睡 ...

  5. 转:解决npm install慢的问题

    使用NPM(Node.js包管理工具)安装依赖时速度特别慢,为了安装Express,执行命令后两个多小时都没安装成功,最后只能取消安装,笔者20M带宽,应该不是我网络的原因,后来在网上找了好久才找到一 ...

  6. Npm install failed with “cannot run in wd”

    Linux环境下,root账户,安装某些npm包的时候报下面的错误,例如安装grunt-contrib-imagemin时: Error: EACCES, mkdir '/usr/local/lib/ ...

  7. npm全局安装和本地安装和本地开发安装(npm install --g/--save/--save-dev)

    详细说明参考:http://www.cnblogs.com/PeunZhang/p/5629329.html 我个人理解: 1.全局安装(npm install -g)是为了用命令行,比如在windo ...

  8. npm install报错Error: ENOENT

    E:\projects\ueditor\ueditor1_4_3_3-src>npm installError: ENOENT, stat 'C:\Users\Lucas\AppData\Roa ...

  9. Windows环境下npm install常见错误

    Windows环境下npm install安装包依赖时,常出现一些错误,下面为个人解决办法: 错误一 缺少python环境: G:\nodejs\moviesite\node_modules\bcry ...

随机推荐

  1. Google+团队如何测试移动应用 - from Google Testing Blog

    How the Google+ Team Tests Mobile Apps by Eduardo Bravo Ortiz “移动第一”在当下已成为很多公司的口头禅.但是能够用一种合理的方法来测试移动 ...

  2. Scala 学习之路(六)—— 常用集合类型之 List & Set

    一.List字面量 List是Scala中非常重要的一个数据结构,其与Array(数组)非常类似,但是List是不可变的,和Java中的List一样,其底层实现是链表. scala> val l ...

  3. Windows使用Python虚拟环境

    Windows使用virtualenv和virtualenvwrapper-win 在Windows上使用virtualenv进行多版本Python隔离. 安装Python 在Python官网下载Py ...

  4. [MFC.Windows程序设计(第2版) 第一章

    1,windows编程模型如下图: 2, windows的消息有成百上千种,以下列举10个: 3,消息处理函数的四个参数:窗口句柄(表示消息属于哪个窗口,32值.该窗口句柄引用一个数据结构,数据结构存 ...

  5. C# Redis分布式锁(基于ServiceStack.Redis)

    相关的文章其实不少,我也从中受益不少,但是还是想自己梳理一下,毕竟自己写的更走心! 首先给出一个拓展类,通过拓展方法实现加锁和解锁. 注:之所以增加拓展方法,是因为合理使用拓展类(方法),可以让程序更 ...

  6. Go语言学习——channel的死锁其实没那么复杂

    1 为什么会有信道 协程(goroutine)算是Go的一大新特性,也正是这个大杀器让Go为很多路人驻足欣赏,让信徒们为之欢呼津津乐道. 协程的使用也很简单,在Go中使用关键字“go“后面跟上要执行的 ...

  7. spring源码深度解析— IOC 之 bean 创建

    在 Spring 中存在着不同的 scope,默认是 singleton ,还有 prototype.request 等等其他的 scope,他们的初始化步骤是怎样的呢?这个答案在这篇博客中给出. s ...

  8. json数据转为对象,一般在前台把数据传回后端中使用 转https://www.cnblogs.com/zxtceq/p/6610214.html

    public static JArray GetData2JArray(string url, string key) { string jsonData = HttpHelper.HttpGet(u ...

  9. iOS自动化探索(十)代码覆盖率统计

    iOS APP代码覆盖率统计 今年Q3季度领导给加了个任务要做前后端代码覆盖率统计, 鉴于对iOS代码代码比较熟就选择先从iOS端入手,折腾一整天后终于初步把流程跑通了记录如下 覆盖率监测的原理 Xc ...

  10. 安卓学习资料推荐《深入理解Android:卷2》下载

    下载地址:百度云下载地址 编辑推荐 <深入理解Android:卷2>编辑推荐:经典畅销书<深入理解Android:卷I>姊妹篇,51CTO移动开发频道和开源中国社区一致鼎力推荐 ...