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