Docker volume speed up npm install

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}"

来源参考

posted @ 2019-07-26 16:38  Ryan.Miao  阅读(2370)  评论(0编辑  收藏  举报