Hướng dẫn Setup Jenkins cho hệ thống Monolith - Bài 18: Tối ưu hóa thời gian build cho mã nguồn Monolith cồng kềnh với tính năng caching
Sau khi đi qua 17 bài học, Pipeline của chúng ta đã trở nên rất toàn diện: từ kéo code, cài thư viện, chạy Unit Test, đo Coverage, quét bảo mật Trivy, phân tích tĩnh SonarQube cho đến đóng gói Artifact. Nhưng lúc này, một vấn đề thực tế bắt đầu xuất hiện: Thời gian chạy một bản build cho khối mã nguồn Monolith bị đội lên tới 15 – 25 phút!
Khi một lập trình viên chỉ sửa đúng 1 dòng chữ trên giao diện hoặc 1 câu điều kiện if nhỏ, họ vẫn phải ngồi chờ Jenkins tải lại từ đầu 500 gói Composer/NPM, tải hàng trăm MB thư viện Maven/Go modules và biên dịch lại toàn bộ dự án. Bài học hôm nay sẽ hướng dẫn bạn kỹ thuật Caching đa tầng trên Jenkins để cắt giảm thời gian build của hệ thống Monolith từ 20 phút xuống chỉ còn 2 – 3 phút.
1. Giải phẫu các "điểm nghẽn" thời gian (Bottlenecks) khi build Monolith
Trong một Pipeline tiêu chuẩn của dự án Monolith, 80% thời gian chờ đợi không nằm ở việc biên dịch code mới, mà bị tiêu tốn vào 4 tác vụ lặp đi lặp lại sau:
Điểm nghẽn (Bottleneck) |
Nguyên nhân gây chậm trong hệ thống Monolith | Thời gian tiêu tốn trung bình |
|---|---|---|
| 1. Git Clone lịch sử đồ sộ | Dự án Monolith chạy 3–5 năm thường tích lũy hàng chục nghìn commits và dung lượng lịch sử tệp nặng hàng trăm MB. | 1 – 3 phút |
| 2. Tải lại Dependencies từ Internet | Các lệnh composer install, npm ci, mvn package hay go mod download luôn tải lại các gói từ kho lưu trữ quốc tế nếu cache bị xóa trắng. |
3 – 8 phút |
3. Biên dịch lại từ đầu (Cold Compile) |
Golang hoặc Java phải biên dịch lại hàng nghìn class/package dù không thay đổi; Frontend (Webpack / Vite) phải bundle lại toàn bộ assets. |
3 – 6 phút |
| 4. Cài đặt lại Dev-Dependencies ở bước cuối | Ở bước Test cài full thư viện, đến bước Package lại xóa đi chạy lại composer install --no-dev làm tốn gấp đôi thời gian I/O ổ cứng. |
1 – 2 phút |
2. Tầng Cache số 1: Tối ưu hóa Git Checkout với Shallow Clone
Mặc định, lệnh checkout scm trong Jenkins sẽ kéo toàn bộ lịch sử commit từ ngày đầu tiên dự án Monolith được thành lập. Để phục vụ việc biên dịch và lấy tag version (như Bài 17), chúng ta chỉ cần lịch sử của khoảng 50 commits gần nhất.
Thay vì dùng checkout scm mặc định, bạn hãy cấu hình GitSCM với tính năng Shallow Clone (depth: 50):
Groovy
// Thay thế lệnh checkout scm mặc định bằng cấu hình tối ưu cho Monolith lớn
checkout([
$class: 'GitSCM',
branches: scm.branches,
extensions: [
// Chỉ lấy lịch sử 50 commits gần nhất, giữ nguyên việc kéo Tags để phục vụ Versioning (Bài 17)
[$class: 'CloneOption', depth: 50, noTags: false, reference: '', shallow: true, timeout: 10]
],
userRemoteConfigs: scm.userRemoteConfigs
])
3. Tầng Cache số 2: Lưu trữ bộ nhớ đệm cho các trình quản lý thư viện (Dependency Caching)
Bí quyết cốt lõi khi caching thư viện cho hệ thống Monolith là: Không cache trực tiếp thư mục vendor/ hay node_modules/ nằm trong Workspace (vì dễ gây lỗi lệch trạng thái khi chuyển nhánh Git), mà hãy cache thư mục Global Cache của trình quản lý gói vào một phân vùng cố định trên máy chủ Jenkins (/var/jenkins_home/.cache).
Khi chạy lệnh cài đặt, nếu tệp composer.lock, package-lock.json, go.sum hoặc pom.xml không đổi, trình quản lý gói sẽ copy thẳng từ ổ cứng nội bộ (Local Disk Cache) vào Workspace với tốc độ tính bằng giây mà không cần gọi ra Internet!
Cấu hình biến môi trường Cache chuẩn cho từng nền tảng Monolith:
A. Đối với PHP / Laravel Monolith (Composer Cache)
Chỉ định biến môi trường COMPOSER_CACHE_DIR về thư mục cố định. Khi chạy composer install, Composer sẽ đọc các file .zip đã tải sẵn từ các bản build trước:
Bash
export COMPOSER_CACHE_DIR="/var/jenkins_home/.cache/composer"
composer install --no-interaction --prefer-dist
B. Đối với Golang Monolith (Go Module Cache & Go Build Cache)
Golang có cơ chế caching cực kỳ thông minh gồm 2 phần: GOMODCACHE (lưu các thư viện tải về từ go.mod) và GOCACHE (lưu kết quả biên dịch .a của từng package để lần sau chỉ biên dịch lại đúng những file .go vừa bị sửa):
Bash
export GOMODCACHE="/var/jenkins_home/.cache/go/pkg/mod"
export GOCACHE="/var/jenkins_home/.cache/go/build-cache"
# Lần chạy thứ 2 trở đi, cả go test và go build sẽ nhanh gấp 5-10 lần!
go test -v -race ./...
go build -o bin/monolith-app ./cmd/main.go
C. Đối với Node.js / Frontend trong Monolith (NPM Cache)
Sử dụng cờ --prefer-offline kết hợp với thư mục cache cố định để npm ci ưu tiên lấy gói từ ổ cứng trước khi hỏi Registry:
Bash
export NPM_CONFIG_CACHE="/var/jenkins_home/.cache/npm"
npm ci --prefer-offline --no-audit
D. Đối với Java Spring Boot Monolith (Maven Local Repository)
Mặc định Maven lưu thư viện tại ~/.m2/repository. Để an toàn và dùng chung được cho cả Docker Agent, hãy trỏ cờ -Dmaven.repo.local về thư mục cố định:
Bash
mvn clean package -DskipTests -Dmaven.repo.local=/var/jenkins_home/.cache/m2/repository
4. Tầng Cache số 3: Tối ưu hóa khi chạy bằng Docker Agent (Volume Mounting)
Nếu bạn cấu hình Jenkins chạy các bước build bên trong các Docker Container cô lập (Dynamic Docker Agent), mỗi khi container tắt đi thì toàn bộ cache bên trong nó sẽ biến mất.
Để giữ lại toàn bộ cache giữa các lần chạy container, bạn chỉ cần truyền tham số -v (Volume Mount) vào phần args của khối agent docker trong Jenkinsfile:
Groovy
agent {
docker {
image 'php:8.2-cli'
// Ánh xạ thư mục cache từ máy chủ Host vào bên trong Container đang build
args '-v /var/jenkins_home/.cache:/var/jenkins_home/.cache'
}
}
5. Tầng Cache số 4: Sử dụng Job Cacher Plugin (Dựa trên Hash của Lockfile)
Nếu bạn muốn đạt tới đỉnh cao tối ưu như trên GitHub Actions hay GitLab CI — tức là lưu nén nguyên cả thư mục vendor/ và node_modules/, và chỉ khi nào nội dung file composer.lock hoặc package-lock.json thay đổi thì mới giải nén cài lại:
-
Vào Manage Jenkins -> Plugins -> Cài đặt plugin Job Cacher.
-
Trong
Jenkinsfile, bọc bước cài đặt thư viện vào trong khốicache(...)như sau:
Groovy
cache(maxCacheSize: 500, caches: [
arbitraryFileCache(
path: 'vendor',
includes: '**/*',
cacheValidityDecidingFile: 'composer.lock', // Chỉ xóa cache khi file composer.lock có thay đổi!
compressionMethod: 'TARGZ'
),
arbitraryFileCache(
path: 'node_modules',
includes: '**/*',
cacheValidityDecidingFile: 'package-lock.json',
compressionMethod: 'TARGZ'
)
]) {
sh 'composer install --no-interaction --prefer-dist'
sh 'npm ci --prefer-offline'
}
6. Hoàn thiện Jenkinsfile siêu tốc độ với kiến trúc Caching toàn diện
Dưới đây là tệp Jenkinsfile đã được nâng cấp toàn diện cho hệ thống Monolith: Tích hợp Shallow Clone, Global Dependency & Build Cache, và kỹ thuật Copy-on-Package (tránh phải chạy lại composer install nhiều lần trên cùng một Workspace):
Groovy
pipeline {
agent any
options {
buildDiscarder(logRotator(numToKeepStr: '15', artifactNumToKeepStr: '5'))
timeout(time: 30, unit: 'MINUTES')
timestamps()
}
environment {
APP_NAME = "monolith-core-system"
// KHAI BÁO CÁC THƯ MỤC CACHE CỐ ĐỊNH TRÊN JENKINS SERVER
CACHE_ROOT = "/var/jenkins_home/.cache"
COMPOSER_CACHE_DIR = "${CACHE_ROOT}/composer"
NPM_CONFIG_CACHE = "${CACHE_ROOT}/npm"
GOMODCACHE = "${CACHE_ROOT}/go/pkg/mod"
GOCACHE = "${CACHE_ROOT}/go/build-cache"
SONAR_USER_HOME = "${CACHE_ROOT}/sonar" // Cache luôn cả các plugin phân tích của SonarScanner!
}
stages {
stage('1. Fast Checkout & Versioning') {
steps {
echo "=== Thực hiện Shallow Clone (chỉ lấy 50 commits gần nhất) ==="
checkout([
$class: 'GitSCM',
branches: scm.branches,
extensions: [
[$class: 'CloneOption', depth: 50, noTags: false, shallow: true, timeout: 10]
],
userRemoteConfigs: scm.userRemoteConfigs
])
script {
// Khởi tạo sẵn các thư mục cache nếu lần đầu chạy chưa có
sh "mkdir -p ${COMPOSER_CACHE_DIR} ${NPM_CONFIG_CACHE} ${GOMODCACHE} ${GOCACHE} ${SONAR_USER_HOME}"
env.GIT_SHORT_HASH = sh(script: 'git rev-parse --short=7 HEAD', returnStdout: true).trim()
env.LATEST_SEMVER_TAG = sh(script: 'git describe --tags --abbrev=0 2>/dev/null || echo "v1.0.0"', returnStdout: true).trim()
env.CLEAN_BRANCH = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim().replaceAll('[^a-zA-Z0-9.-]', '-')
if (env.CLEAN_BRANCH == 'main' || env.CLEAN_BRANCH == 'master') {
env.BUILD_VERSION = "${env.LATEST_SEMVER_TAG}-b${env.BUILD_NUMBER}-${env.GIT_SHORT_HASH}"
} else {
env.BUILD_VERSION = "${env.LATEST_SEMVER_TAG}-${env.CLEAN_BRANCH}.${env.BUILD_NUMBER}-${env.GIT_SHORT_HASH}"
}
currentBuild.displayName = "${env.BUILD_VERSION}"
}
}
}
stage('2. Cached Install Dependencies') {
steps {
echo "=== Cài đặt thư viện tốc độ cao từ Local Cache ==="
sh '''
# Composer và NPM sẽ đọc trực tiếp từ COMPOSER_CACHE_DIR và NPM_CONFIG_CACHE
composer install --no-interaction --prefer-dist
if [ -f package-lock.json ]; then
npm ci --prefer-offline --no-audit
npm run build
fi
'''
}
}
stage('3. Run Unit Tests & Coverage') {
environment {
APP_ENV = "testing"
DB_CONNECTION = "sqlite"
DB_DATABASE = ":memory:"
XDEBUG_MODE = "coverage"
}
steps {
sh '''
mkdir -p reports
./vendor/bin/phpunit \
--log-junit reports/junit.xml \
--coverage-clover reports/coverage-clover.xml
'''
}
post {
always {
junit testResults: 'reports/junit.xml', allowEmptyResults: false
}
}
}
stage('4. Security & SonarQube Gate (Cached)') {
steps {
script {
// Trivy cũng sử dụng cache database lỗ hổng tại CACHE_ROOT/trivy để không phải tải lại DB mỗi lần quét
sh "trivy fs --cache-dir ${CACHE_ROOT}/trivy --scanners vuln,secret --severity CRITICAL --exit-code 1 ."
def scannerHome = tool 'SonarScanner-CLI'
withSonarQubeEnv('SonarQube-Main-Server') {
sh "${scannerHome}/bin/sonar-scanner -Dsonar.projectVersion=${env.BUILD_VERSION}"
}
}
timeout(time: 5, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
stage('5. Package Production Artifact') {
steps {
echo "=== Gỡ bỏ gói Dev-Dependencies từ Cache và đóng gói Artifact ==="
sh '''
# Vì đã có cache, lệnh chuyển từ dev sang --no-dev chỉ mất khoảng 1-2 giây để gỡ symlink!
composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
cat <<EOF > version.json
{
"service": "${APP_NAME}",
"version": "${BUILD_VERSION}",
"commit": "${GIT_SHORT_HASH}"
}
EOF
tar --exclude='.git' --exclude='node_modules' --exclude='tests' --exclude='reports' \
-czf ${APP_NAME}-${BUILD_VERSION}.tar.gz .
'''
}
}
}
post {
success {
archiveArtifacts artifacts: "${APP_NAME}-${env.BUILD_VERSION}.tar.gz, version.json", fingerprint: true
}
always {
sh 'rm -rf reports/ version.json .env'
}
}
}
7. Lưu ý thực chiến: Chiến lược chống "Tràn ổ cứng" do Cache (Cache Eviction)
Khi bật tính năng Caching cho hệ thống Monolith, qua vài tháng vận hành với hàng chục phiên bản thư viện mới được nâng cấp, thư mục /var/jenkins_home/.cache có thể phình to lên hàng chục GB. Để hệ thống vận hành bền bỉ mà không làm đầy ổ đĩa máy chủ (sẽ đi sâu hơn ở Bài 33), bạn nên áp dụng quy tắc bảo trì định kỳ:
-
Tự động dọn dẹp Cache quá hạn: Thiết lập một Cronjob nhỏ trên máy chủ Linux (hoặc một Jenkins Job chạy vào 2h sáng Chủ Nhật hàng tuần) để xóa các file cache không được truy cập trong vòng 30 ngày qua:
Bash
# Xóa các file trong thư mục cache của Jenkins nếu quá 30 ngày không được đọc tới (atime +30) find /var/jenkins_home/.cache -type f -atime +30 -delete -
Cơ chế "Clear Cache" khẩn cấp: Đôi khi bộ nhớ đệm bị hỏng (Corrupted Cache) khiến bản build báo lỗi lạ. Bạn có thể thêm một tham số boolean
CLEAR_CACHEvào Pipeline để khi cần, DevOps chỉ việc tích chọn ô này lúc bấm Build with Parameters là Jenkins sẽ xóa sạch cache cũ và làm mới lại từ đầu.
All rights reserved