1. 程式人生 > >Docker volume speed up npm install

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

來源參考

  • https://jenkins.io/zh/doc/book/pipeline/docker/#%E8%84%9A%E6%9C%AC%E5%8C%96%E6%B5%81%E6%B0%B4%E7%BA%BF%E7%9A%84%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95