Hướng dẫn Setup Jenkins cho hệ thống Monolith + Bài học 36 + Tối ưu hóa tốc độ Pipeline cho Monolith bằng kỹ thuật thực thi song song (Parallel Stages)
Ở Bài 18, chúng ta đã áp dụng kỹ thuật Caching đa tầng để giảm thời gian tải thư viện từ 15 phút xuống còn vài phút. Ở Bài 35, chúng ta đã đóng gói toàn bộ quy trình vào Shared Library cực kỳ gọn gàng. Tuy nhiên, nếu quan sát kỹ sơ đồ chạy của Pipeline hiện tại, bạn sẽ nhận ra một hạn chế lớn về mặt kiến trúc: Toàn bộ các bước đang chạy tuần tự nối đuôi nhau (Sequential Execution)!
Trong một hệ thống Monolith lớn, sau khi kéo code và cài đặt thư viện xong, tại sao bước Biên dịch Frontend (Vite/Webpack) phải ngồi chờ bước Chạy Unit Test Backend (PHPUnit/Go test) kết thúc rồi mới được chạy? Tại sao bước Quét bảo mật Trivy lại phải xếp hàng chờ bước Kiểm tra Coding Style (Linter/PHPStan)? Chúng hoàn toàn độc lập với nhau!
Bài học hôm nay sẽ hướng dẫn bạn sử dụng vũ khí tăng tốc mạnh mẽ nhất của Jenkins Declarative Pipeline: Thực thi song song (parallel) kết hợp kỹ thuật Chia để trị bộ Unit Test (Test Sharding), giúp cắt giảm thêm 50% – 70% tổng thời gian chạy CI/CD cho khối mã nguồn Monolith.
1. So sánh Thực thi Tuần tự (Sequential) và Thực thi Song song (Parallel)
Hãy nhìn vào bảng phân tích thời gian thực tế của một dự án Monolith (ví dụ: Laravel/Go Backend kết hợp Vue/React Frontend nằm chung một Repository) khi chạy qua các bước kiểm tra chất lượng:
Plaintext
[TUẦN TỰ - Mất 14 phút]:
Checkout (1p) ──► Unit Test (5p) ──► Frontend Build (3p) ──► Static Analysis (3p) ──► Trivy Scan (2p)
[SONG SONG - Chỉ mất 6 phút!]:
├──► Nhánh 1: Backend Unit Test (5p) ──┐
Checkout (1p) ────┼──► Nhánh 2: Frontend Assets (3p) ──┼──► (Hội tụ) ──► Build Docker & Deploy
├──► Nhánh 3: PHPStan / Linter (3p) ──┤
└──► Nhánh 4: Trivy Security Scan (2p)──┘
| Tiêu chí | Chạy tuần tự (Sequential Stages) | Chạy song song (Parallel Stages) |
|---|---|---|
| Công thức tính tổng thời gian | (Cộng dồn tất cả các bước). | (Chỉ bằng thời gian của nhánh lâu nhất!). |
| Mức độ tận dụng phần cứng Agent | Chỉ dùng 1–2 nhân CPU tại một thời điểm, các nhân CPU còn lại của máy chủ Agent (Bài 31) ngồi chơi. | Vắt kiệt sức mạnh đa nhân (Multi-core CPU) hoặc phân bổ ra nhiều máy chủ Agent cùng chạy một lúc. |
| Tốc độ phản hồi khi có lỗi | Nếu lỗi nằm ở bước Trivy (cuối cùng), lập trình viên phải đợi hết 12 phút trước đó mới biết bản build bị đỏ. | Cả 4 nhánh cùng xuất phát ở giây thứ 0; nhánh nào lỗi sẽ báo đỏ ngay lập tức nhờ cờ failFast true. |
2. Hai quy tắc "Sống còn" khi chạy Parallel cho hệ thống Monolith
Khác với việc chạy song song các Microservices tách biệt, khi bạn chạy nhiều luồng song song trên cùng một khối mã nguồn Monolith, nếu không nắm vững 2 quy tắc dưới đây, các luồng sẽ dẫm đạp lên nhau gây lỗi hỏng file (Race Condition):
Quy tắc 1: Chia sẻ tệp tin an toàn giữa các nhánh bằng stash và unstash
-
Nếu các nhánh song song cùng chạy chung trên 1 thư mục Workspace duy nhất: Nhánh 1 đang chạy
phpunitđọc thư mụcvendor/, trong khi Nhánh 2 lại chạy lệnh tối ưu xóa mất một số file tạm trongvendor/-> Bản build sẽ sập ngẫu nhiên! -
Nếu các nhánh song song được phân bổ chạy trên nhiều máy chủ Agent khác nhau (
monolith-worker-01vàmonolith-worker-02): Nhánh chạy trên Agent 2 sẽ không hề có thư mục code hayvendor/mà Bước 1 vừa cài ở Agent 1! -
Giải pháp chuẩn của Jenkins: Sử dụng cặp lệnh
stash(Đóng gói nhanh các tệp đã chuẩn bị ở bước 1 lưu tạm lên bộ nhớ đệm của Jenkins Master) vàunstash(Giải nén gói đó xuống từng nhánh song song).
Quy tắc 2: Bật cờ failFast true để tiết kiệm tài nguyên
Giả sử bạn chạy 4 nhánh song song. Ngay ở giây thứ 10, nhánh Security Scan phát hiện lỗ hổng nghiêm trọng và báo FAILED. Nếu không bật failFast true, 3 nhánh còn lại vẫn cứ tiếp tục chạy ngốn CPU suốt 5 phút nữa rồi cuối cùng cả Pipeline vẫn bị đánh rớt. Khi khai báo failFast true, chỉ cần 1 nhánh gãy, Jenkins sẽ lập tức hủy (abort) các nhánh đang chạy dở còn lại!
3. Kỹ thuật nâng cao: "Chia để trị" hàng nghìn Unit Test của Monolith (Test Sharding)
Trong một hệ thống Monolith lâu năm, riêng bước chạy Unit Test & Integration Test có thể chứa tới 2.000 test cases và mất 10 phút nếu chạy trên 1 luồng. Bạn có thể giảm thời gian này xuống chỉ còn 2.5 phút bằng hai cấp độ song song hóa:
Cấp độ 1: Chạy đa tiến trình ngay bên trong 1 Agent (Multi-process Test Runner)
-
Với PHP / Laravel Monolith (Sử dụng
ParaTesthoặcPest --parallel): Thay vì gọi./vendor/bin/phpunitchỉ chạy 1 nhân CPU, hãy dùng ParaTest để tự động chia các file test trong thư mụctests/ra cho 4 hoặc 8 tiến trình PHP cùng xử lý đồng thời (Mỗi tiến trình sẽ tự động được cấp một biến môi trườngTEST_TOKEN=1, 2, 3, 4riêng để kết nối vào các database test độc lậptest_db_1,test_db_2tránh ghi đè dữ liệu của nhau!):Bash
# Chạy Unit Test trên 4 luồng song song và gộp kết quả vào 1 file junit.xml duy nhất ./vendor/bin/paratest --processes=4 --runner=WrapperRunner --log-junit=reports/junit.xml -
Với Golang Monolith: Sử dụng cờ
-parallelđể chỉ định số lượng goroutines chạy test đồng thời trong mỗi package:Bash
go test -v -race -parallel 4 -coverprofile=reports/coverage.out ./...
Cấp độ 2: Chia nhỏ bộ Test theo Module chạy trên nhiều nhánh Jenkins parallel (Test Suite Sharding)
Nếu dự án Monolith của bạn được tổ chức theo từng phân hệ lớn (ví dụ: tests/Unit/Order, tests/Unit/Payment, tests/Unit/Inventory), bạn có thể tách chúng thành các Stage chạy song song trên nhiều Worker Agent khác nhau!
4. Hoàn thiện Jenkinsfile với kiến trúc Parallel Stages & Stash/Unstash
Dưới đây là cách nâng cấp Stage kiểm thử và biên dịch của hệ thống Monolith lên kiến trúc Thực thi song song 4 luồng đồng thời:
-
Luồng 1: Kiểm tra cú pháp tĩnh & Bảo mật thư viện (
PHPStan / Linter & Trivy FS). -
Luồng 2: Thực thi Unit Test nhóm Core & Order (
Unit Tests - Shard A). -
Luồng 3: Thực thi Unit Test nhóm Payment & User (
Unit Tests - Shard B). -
Luồng 4: Biên dịch tài nguyên Frontend (
Node.js / Vite Build Assets).
Sau khi cả 4 luồng cùng cán đích thành công, kết quả JUnit XML và thư mục public/build của Frontend sẽ được gom lại (unstash) để đóng gói vào Docker Image!
Groovy
pipeline {
agent none
options {
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '3'))
timeout(time: 45, unit: 'MINUTES')
timestamps()
disableConcurrentBuilds()
}
environment {
APP_NAME = "monolith-core-system"
DOCKER_REGISTRY = "registry.yourcompany.com"
DOCKER_REPO = "${DOCKER_REGISTRY}/monolith/${APP_NAME}"
REGISTRY_CRED = "docker-registry-credentials"
CACHE_ROOT = "/var/jenkins/cache"
}
stages {
// =====================================================================
// STAGE 1: KÉO MÃ NGUỒN, CÀI THƯ VIỆN BACKEND & LƯU TẠM (STASH)
// =====================================================================
stage('1. Prepare & Stash Dependencies') {
agent { label 'monolith-worker' }
steps {
checkout([
$class: 'GitSCM',
branches: scm.branches,
extensions: [[$class: 'CloneOption', depth: 50, noTags: false, shallow: true]],
userRemoteConfigs: scm.userRemoteConfigs
])
script {
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.-]', '-')
env.TARGET_ENV = (env.CLEAN_BRANCH in ['main', 'master']) ? 'production' : 'staging'
env.BUILD_VERSION = "${env.LATEST_SEMVER_TAG}-${env.TARGET_ENV}.b${env.BUILD_NUMBER}-${env.GIT_SHORT_HASH}"
env.IMAGE_FULL_URL = "${env.DOCKER_REPO}:${env.BUILD_VERSION}"
currentBuild.displayName = "⚡ [${env.TARGET_ENV.toUpperCase()}] ${env.BUILD_VERSION}"
}
echo "=== Cài đặt thư viện Composer từ Local Cache và đóng gói Stash ==="
sh '''
export COMPOSER_CACHE_DIR="${CACHE_ROOT}/composer"
composer install --no-interaction --prefer-dist --no-progress
'''
// Đóng gói mã nguồn và thư mục vendor/ lưu vào bộ nhớ đệm của Jenkins để chia cho các nhánh song song
stash name: 'monolith-backend-source', includes: '**, .env.example', excludes: '.git/**, node_modules/**, reports/**'
}
}
// =====================================================================
// STAGE 2 (BÀI 36): CHẠY SONG SONG 4 LUỒNG KIỂM THỬ & BIÊN DỊCH ASSETS
// =====================================================================
stage('2. Parallel Quality, Security & Frontend Build') {
// Bật failFast: Nếu 1 trong 4 nhánh dưới đây bị lỗi, lập tức dừng ngay 3 nhánh còn lại!
failFast true
parallel {
// -------------------------------------------------------------
// NHÁNH SONG SONG 1: Quét bảo mật Trivy & Kiểm tra Static Linter
// -------------------------------------------------------------
stage('2A. Security & Linter Scan') {
agent { label 'monolith-worker' }
steps {
unstash 'monolith-backend-source'
echo "🛡️ [Luồng 1] Đang chạy phân tích tĩnh (Linter) và quét CVE/Secrets với Trivy..."
sh '''
# 1. Kiểm tra cú pháp PHP cực nhanh trên đa luồng
find app routes config -name "*.php" -print0 | xargs -0 -n1 -P4 php -l >/dev/null
# 2. Quét lỗ hổng thư viện và lộ khóa bí mật bằng Trivy
trivy fs --cache-dir ${CACHE_ROOT}/trivy --scanners vuln,secret --severity CRITICAL --exit-code 1 .
'''
}
}
// -------------------------------------------------------------
// NHÁNH SONG SONG 2: Chạy Unit Test Phân hệ A (Core, Auth, User)
// -------------------------------------------------------------
stage('2B. Unit Tests (Shard 1: Core & Auth)') {
agent { label 'monolith-worker' }
environment {
APP_ENV = "testing"
DB_CONNECTION = "sqlite"
DB_DATABASE = ":memory:"
XDEBUG_MODE = "coverage"
}
steps {
unstash 'monolith-backend-source'
echo "🧪 [Luồng 2] Đang thực thi Unit Test cho nhóm Core & Auth..."
sh '''
mkdir -p reports
./vendor/bin/phpunit --testsuite=Core,Auth \
--log-junit reports/junit-shard1.xml \
--coverage-clover reports/coverage-shard1.xml
'''
// Lưu lại kết quả báo cáo của Nhánh 2
stash name: 'test-reports-shard1', includes: 'reports/*.xml'
}
}
// -------------------------------------------------------------
// NHÁNH SONG SONG 3: Chạy Unit Test Phân hệ B (Order, Payment, Inventory)
// -------------------------------------------------------------
stage('2C. Unit Tests (Shard 2: Commerce)') {
agent { label 'monolith-worker' }
environment {
APP_ENV = "testing"
DB_CONNECTION = "sqlite"
DB_DATABASE = ":memory:"
XDEBUG_MODE = "coverage"
}
steps {
unstash 'monolith-backend-source'
echo "🧪 [Luồng 3] Đang thực thi Unit Test cho nhóm Order, Payment & Inventory..."
sh '''
mkdir -p reports
./vendor/bin/phpunit --testsuite=Order,Payment,Inventory \
--log-junit reports/junit-shard2.xml \
--coverage-clover reports/coverage-shard2.xml
'''
// Lưu lại kết quả báo cáo của Nhánh 3
stash name: 'test-reports-shard2', includes: 'reports/*.xml'
}
}
// -------------------------------------------------------------
// NHÁNH SONG SONG 4: Biên dịch Frontend Assets (Webpack / Vite)
// -------------------------------------------------------------
stage('2D. Compile Frontend Assets') {
agent { label 'monolith-worker' }
steps {
unstash 'monolith-backend-source'
echo "🎨 [Luồng 4] Đang cài đặt NPM từ Cache và biên dịch giao diện Frontend..."
sh '''
export NPM_CONFIG_CACHE="${CACHE_ROOT}/npm"
if [ -f package-lock.json ]; then
npm ci --prefer-offline --no-audit --no-progress
npm run build
else
mkdir -p public/build
fi
'''
// Lưu lại thư mục thành phẩm public/build để bước sau đóng vào Docker Image!
stash name: 'compiled-frontend-assets', includes: 'public/build/**'
}
}
}
}
// =====================================================================
// STAGE 3: HỘI TỤ KẾT QUẢ (FAN-IN), QUÉT SONARQUBE & BUILD DOCKER IMAGE
// =====================================================================
stage('3. Aggregate Reports, SonarQube & Build Image') {
agent { label 'monolith-worker' }
steps {
echo "=== Gom toàn bộ kết quả từ 4 nhánh song song về một mối (Fan-In) ==="
unstash 'monolith-backend-source'
unstash 'test-reports-shard1'
unstash 'test-reports-shard2'
unstash 'compiled-frontend-assets'
script {
def scannerHome = tool 'SonarScanner-CLI'
withSonarQubeEnv('SonarQube-Main-Server') {
// SonarQube hỗ trợ nhận danh sách nhiều file báo cáo phân cách bởi dấu phẩy!
sh """
${scannerHome}/bin/sonar-scanner \\
-Dsonar.projectVersion=${env.BUILD_VERSION} \\
-Dsonar.junit.reportPaths=reports/junit-shard1.xml,reports/junit-shard2.xml \\
-Dsonar.php.coverage.reportPaths=reports/coverage-shard1.xml,reports/coverage-shard2.xml
"""
}
}
timeout(time: 5, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
echo "=== Đóng gói Docker Image (Đã có sẵn public/build từ Nhánh 2D) ==="
sh """
DOCKER_BUILDKIT=1 docker build \\
--pull \\
--build-arg BUILD_VERSION=${env.BUILD_VERSION} \\
--build-arg GIT_COMMIT=${env.GIT_SHORT_HASH} \\
-t ${env.IMAGE_FULL_URL} \\
-f Dockerfile .
"""
}
post {
always {
// JUnit Plugin tự động gộp tất cả các file reports/junit-*.xml thành 1 bảng tổng hợp duy nhất!
junit testResults: 'reports/junit-*.xml', allowEmptyResults: false
cleanWs(deleteDirs: true, disableDeferredWipeout: true, notFailBuild: true)
}
}
}
}
}
5. Mẹo xử lý khi nhiều nhánh chạy chung trên 1 Máy chủ Agent (ws() Isolation)
Một câu hỏi thực tế rất hay gặp: "Nếu tôi chỉ có 1 máy chủ Worker Agent (monolith-worker-01) nhưng cấu hình Number of executors = 4, khi 4 nhánh song song cùng chạy trên máy chủ đó, Jenkins xử lý thư mục Workspace như thế nào?"
-
Cơ chế tự động của Jenkins: Khi có 4 luồng thực thi cùng lúc cho một Job trên cùng một máy chủ Agent, Jenkins sẽ tự động tạo ra các thư mục Workspace tách biệt bằng cách gắn thêm hậu tố
@2,@3,@4:-
Luồng 1 chạy tại:
/var/jenkins/workspace/monolith-pipeline -
Luồng 2 chạy tại:
/var/jenkins/workspace/monolith-pipeline@2 -
Luồng 3 chạy tại:
/var/jenkins/workspace/monolith-pipeline@3 -
Luồng 4 chạy tại:
/var/jenkins/workspace/monolith-pipeline@4
-
-
Chính vì mỗi luồng nằm ở một thư mục khác nhau (
@2,@3,@4), việc sử dụngstashở Stage 1 vàunstashở đầu mỗi nhánh song song (như trongJenkinsfilemẫu ở mục 4) là bắt buộc để đảm bảo các thư mục@2,@3,@4đều nhận được đầy đủ mã nguồn và thư viện đã cài đặt!
6. Trải nghiệm giao diện trực quan với Blue Ocean hoặc Pipeline Stage View
Khi bạn áp dụng cấu trúc parallel ở trên, giao diện bảng truyền thống của Jenkins có thể hiển thị hơi hẹp. Để quan sát trực quan sơ đồ 4 nhánh tách ra từ Stage 1, chạy song song rồi chụm lại (Fan-in) ở Stage 3:
-
Hãy cài đặt plugin Pipeline: Stage View hoặc Pipeline Graph View Plugin (thế hệ mới nhẹ và hiện đại hơn Blue Ocean).
-
Khi mở chi tiết bản Build, bạn sẽ thấy sơ đồ mạng lưới các nút (Nodes Graph) hiển thị thời gian chạy của từng nhánh song song theo thời gian thực, giúp phát hiện ngay nhánh nào đang là "điểm nghẽn" chậm nhất để tiếp tục chia nhỏ!
All rights reserved