Hướng dẫn Setup Jenkins cho hệ thống Monolith + Bài học 32 + Tự động hóa dọn dẹp không gian đĩa cứng và quản lý Log Rotation nâng cao trong Jenkins
Ở Bài 31, chúng ta đã tách biệt thành công kiến trúc Jenkins Master - Agent để giải phóng CPU và RAM cho máy chủ điều phối chính. Tuy nhiên, có một nguy cơ ngầm luôn rình rập mọi hệ thống CI/CD của dự án Monolith sau khoảng 1 – 2 tháng vận hành: Sự cố đầy ổ cứng 100% (java.io.IOException: No space left on device) hoặc Cạn kiệt Inodes (chỉ mục tệp tin).
Khi dung lượng đĩa khả dụng trên Jenkins Master hoặc Agent tụt xuống dưới ngưỡng an toàn (mặc định là 1GB), Jenkins sẽ lập tức kích hoạt cơ chế tự vệ: Đánh dấu Offline toàn bộ các Node Agent và từ chối chạy mọi bản build mới. Bài học hôm nay sẽ đi sâu vào kiến trúc quản trị lưu trữ cấp độ hệ điều hành Linux (Storage & Inode Management) và thiết lập cơ chế Log Rotation nâng cao trong Jenkins để hệ thống Monolith vận hành tự động 24/7 mà không bao giờ bị nghẽn đĩa.
1. Giải phẫu 2 nguyên nhân gây tràn đĩa kinh điển của dự án Monolith: Dung lượng (Disk Blocks) và Chỉ mục (Inodes)
Khác với Microservices có kích thước nhỏ gọn, một khối mã nguồn Monolith (đặc biệt là các dự án PHP Laravel kết hợp Node.js/Frontend hoặc Java/Go) khi chạy qua Pipeline mỗi ngày 20 – 30 lần sẽ tấn công ổ cứng máy chủ theo 2 chiều hướng khác nhau:
| Loại tài nguyên đĩa | Lệnh kiểm tra trên Linux | Thủ phạm chính trong dự án Monolith | Triệu chứng khi chạm ngưỡng 100% |
|---|---|---|---|
| Disk Space (Dung lượng GB) | df -h |
File nén Artifact (.tar.gz, .jar nặng 200MB/bản), Docker BuildKit Cache (/var/lib/docker/buildkit), Console Log dài hàng trăm nghìn dòng và báo cáo Coverage XML. |
Lệnh docker build hoặc tar báo lỗi No space left on device dù mới chạy được một nửa. |
| Disk Inodes (Số lượng tệp tối đa) | df -i |
Thư mục vendor/ (~30.000 file nhỏ) và node_modules/ (~80.000 file nhỏ) nhân bản ra ở hàng chục nhánh feature/* trong thư mục workspace/. |
Cực kỳ nguy hiểm: Lệnh df -h báo ổ cứng vẫn còn trống tới 40GB, nhưng Jenkins vẫn báo lỗi No space left on device vì hệ điều hành đã hết sạch Inode để cấp phát cho file mới! |
2. Kỹ thuật cấu hình Log Rotation phân tầng theo Nhánh (Branch-Aware Retention Policy)
Ở các bài trước, chúng ta đã khai báo buildDiscarder(logRotator(...)) với một con số cố định. Tuy nhiên, trong thực tế vận hành Monolith đa môi trường, việc giữ lại 30 bản build cho một nhánh feature/fix-typo là sự lãng phí tài nguyên vô ích, trong khi nhánh main (Production) lại cần lưu lịch sử lâu hơn để phục vụ kiểm toán (Audit).
Bạn có thể sử dụng biểu thức điều kiện (Ternary Operator) ngay bên trong khối options của Declarative Pipeline để tự động áp dụng chính sách giữ log khác nhau tùy theo nhánh Git đang chạy:
Groovy
options {
// PHÂN TẦNG LOG ROTATION THEO NHÁNH GIT:
// - Nhánh Production (main/master): Giữ 30 lịch sử build (60 ngày), giữ 5 gói Artifact (14 ngày).
// - Các nhánh còn lại (develop/staging/feature): Chỉ giữ 10 lịch sử build (7 ngày) và đúng 1 gói Artifact mới nhất!
buildDiscarder(logRotator(
numToKeepStr: (env.BRANCH_NAME in ['main', 'master']) ? '30' : '10',
daysToKeepStr: (env.BRANCH_NAME in ['main', 'master']) ? '60' : '7',
artifactNumToKeepStr: (env.BRANCH_NAME in ['main', 'master']) ? '5' : '1',
artifactDaysToKeepStr: (env.BRANCH_NAME in ['main', 'master']) ? '14' : '3'
))
}
Tinh chỉnh chu kỳ quét ngầm BackgroundBuildDiscarder của Jenkins Master
Mặc định, ngay cả khi bạn cấu hình logRotator, Jenkins chỉ chạy tiến trình quét xóa các bản build hết hạn (daysToKeepStr) mỗi 24 giờ một lần. Nếu hệ thống Monolith của bạn có tần suất build cực cao (hàng trăm lượt/ngày), hãy ép Jenkins Master kiểm tra và cắt tỉa các bản build cũ mỗi giờ một lần bằng cách chạy đoạn script nhỏ sau trong Manage Jenkins -> Script Console:
Groovy
// Kích hoạt tiến trình quét dọn Log Rotation chạy ngay lập tức trên toàn bộ các Job
import jenkins.model.Jenkins
import hudson.model.Job
import jenkins.model.BackgroundBuildDiscarder
Jenkins.instance.getAllItems(Job.class).each { job ->
if (job.getBuildDiscarder() != null) {
println "Đang thực thi chính sách LogRotator cho Job: ${job.fullName}"
job.logRotate()
}
}
3. Khống chế kích thước Console Log và Tệp tạm @tmp trong lúc chạy Pipeline
Một nguyên nhân khiến thư mục /var/jenkins_home/jobs/<job>/builds/<number>/log phình to lên hàng trăm MB chỉ sau 1 lần chạy là do các công cụ trong hệ thống Monolith (như composer install -vvv, npm build, phpunit --debug, hoặc mvn test) xả ra hàng triệu dòng thông báo tiến độ (progress bars).
Để triệt tiêu hiện tượng "lũ lụt log" (Log Flooding), hãy áp dụng 3 quy tắc cấu hình sau cho hệ thống Monolith:
-
Tắt thanh tiến trình động (Progress Bars) trên các câu lệnh CLI:
-
Với Composer: Luôn dùng cờ
--no-progress --no-interaction. -
Với NPM: Dùng cờ
--no-progress --loglevel=error. -
Với Maven: Dùng cờ
-B(Batch mode) hoặc-ntp(--no-transfer-progresstừ Maven 3.6.1+). -
Với
curl/wget: Dùngcurl -sS(Ẩn thanh % tải file nhưng vẫn hiển thị lỗi nếu đứt mạng).
-
-
Dọn dẹp thư mục
@tmpvàcachesbằngcleanWs()kết hợp bộ lọc: Mỗi khi Jenkins chạy khốish,withCredentialshoặcdir, nó sẽ tự động sinh ra một thư mục song song có tên<workspace>@tmpđể chứa các tệp script tạm. Hãy đảm bảo cấu hìnhcleanWs()ở cuối Pipeline luôn quét sạch các thành phần tiêu tốn nhiều Inode nhất:
Groovy
post {
always {
// Giải phóng ngay các thư mục tiêu tốn hàng chục nghìn Inodes sau khi đã đóng gói xong Docker Image
cleanWs(
cleanWhenNotBuilt: false,
deleteDirs: true,
disableDeferredWipeout: true, // Bắt buộc xóa đồng bộ ngay trên đĩa, không đưa vào hàng đợi xóa ngầm
notFailBuild: true,
patterns: [
[pattern: 'vendor/**', type: 'INCLUDE'], // Giải phóng ~30.000 inodes (Vì đã lưu cache zip ở COMPOSER_CACHE_DIR)
[pattern: 'node_modules/**', type: 'INCLUDE'], // Giải phóng ~80.000 inodes (Vì đã lưu cache ở NPM_CONFIG_CACHE)
[pattern: 'reports/**', type: 'INCLUDE'],
[pattern: '*.tar.gz*', type: 'INCLUDE'],
[pattern: '.env*', type: 'INCLUDE']
]
)
}
}
4. Cấu hình Linux logrotate cho Docker Daemon và Jenkins System Logs
Ngoài các bản log nằm trong từng Job của Jenkins, bản thân dịch vụ Jenkins Daemon (/var/log/jenkins/jenkins.log) và các Docker Containers của hệ thống Monolith (/var/lib/docker/containers/*/*-json.log) cũng ghi log không giới hạn ra ổ cứng máy chủ Linux.
Hãy thực hiện 2 cấu hình cấp hệ điều hành sau trên cả máy chủ Jenkins Agent và Production Server:
A. Giới hạn dung lượng JSON Log của Docker Engine (/etc/docker/daemon.json)
Mặc định Docker không bao giờ tự xóa file *-json.log của các container. Nếu container Monolith của bạn in stdout/stderr liên tục, chỉ sau 2 tháng file log này có thể nặng tới 50GB!
Bash
# Cấu hình Docker tự động cắt (rotate) log khi đạt 50MB, chỉ giữ tối đa 3 file quay vòng cho mỗi container
sudo tee /etc/docker/daemon.json << 'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
}
}
EOF
# Khởi động lại dịch vụ cấu hình của Docker
sudo systemctl reload docker || sudo systemctl restart docker
B. Cấu hình Linux logrotate cho tệp nhật ký hệ thống của Jenkins Master
Tạo tệp /etc/logrotate.d/jenkins-custom trên máy chủ Jenkins Master:
Bash
sudo tee /etc/logrotate.d/jenkins-custom << 'EOF'
/var/log/jenkins/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
copytruncate
}
EOF
(Tham số copytruncate cực kỳ quan trọng đối với ứng dụng Java như Jenkins: Nó sao chép nội dung log cũ sang file nén rồi cắt rỗng file hiện tại về 0 byte ngay tại chỗ mà không cần khởi động lại tiến trình Jenkins!).
5. Hoàn thiện Pipeline "Tự động bảo trì toàn diện" (Kiểm tra cả Disk Space & Inodes + Cảnh báo Telegram)
Bây giờ, chúng ta hãy nâng cấp hoàn chỉnh tệp Jenkinsfile.maintenance (chạy định kỳ lúc 02:00 sáng mỗi ngày trên các cụm Agent): Không chỉ dọn dẹp Docker BuildKit, Cache quá hạn và Workspace mồ côi, mà còn tự động đo lường cả % Disk Space lẫn % Inodes và bắn cảnh báo khẩn cấp về Telegram nếu phát hiện bất kỳ chỉ số nào vượt ngưỡng 80%!
Groovy
pipeline {
agent {
node {
label 'monolith-worker'
}
}
// Tự động chạy vào khoảng 02:00 sáng mỗi ngày
triggers {
cron('H 2 * * *')
}
options {
buildDiscarder(logRotator(numToKeepStr: '14'))
timeout(time: 30, unit: 'MINUTES')
timestamps()
}
environment {
AGENT_ROOT = "/var/jenkins"
CACHE_ROOT = "/var/jenkins/cache"
ALERT_THRESHOLD = "80" // Ngưỡng cảnh báo 80% cho cả Disk Space và Inodes
}
stages {
stage('1. Audit Storage & Inodes (Before)') {
steps {
echo "=== Kiểm tra trạng thái Dung lượng đĩa (Blocks) và Chỉ mục tệp (Inodes) trước khi dọn ==="
sh '''
echo "--- 1. Dung lượng ổ cứng (Disk Space) ---"
df -h /
echo "--- 2. Chỉ mục tệp tin (Inode Usage) ---"
df -i /
echo "--- 3. Dung lượng hệ thống Docker ---"
docker system df || true
'''
}
}
stage('2. Deep Clean Docker & BuildKit Cache') {
steps {
echo "=== Cắt tỉa Docker Containers, Images và giới hạn BuildKit Cache ==="
sh '''
# Xóa container đã dừng quá 12 giờ
docker container prune -f --filter "until=12h"
# Xóa các image không sử dụng quá 48 giờ
docker image prune -a -f --filter "until=48h"
# Giới hạn dung lượng tối đa của Docker BuildKit Cache ở mức 8GB
docker builder prune -f --keep-storage 8GB || docker builder prune -f --filter "until=48h"
'''
}
}
stage('3. Purge Stale Workspaces & Dependency Caches') {
steps {
echo "=== Dọn dẹp các Workspace nhánh cũ và Cache thư viện không truy cập quá 21 ngày ==="
sh '''
# 1. Xóa các file cache Composer/NPM/Go/Trivy không được đọc tới trong 21 ngày qua
if [ -d "${CACHE_ROOT}" ]; then
find "${CACHE_ROOT}" -type f -atime +21 -delete
find "${CACHE_ROOT}" -type d -empty -delete
fi
# 2. Xóa các thư mục tạm (@tmp, @script, _ws-cleanup) bị kẹt quá 24 giờ
if [ -d "${AGENT_ROOT}/workspace" ]; then
find "${AGENT_ROOT}/workspace" -maxdepth 1 \\( -name "*@tmp*" -o -name "*_ws-cleanup*" \\) -mtime +1 -exec rm -rf {} +
# Xóa các thư mục workspace của các nhánh feature cũ không chạy quá 7 ngày (Giải phóng hàng triệu Inodes)
find "${AGENT_ROOT}/workspace" -mindepth 1 -maxdepth 1 -type d -mtime +7 -exec rm -rf {} +
fi
# 3. Làm sạch các tệp tạm trong /tmp do tiến trình test tạo ra
find /tmp -maxdepth 1 -user jenkins -mtime +1 -exec rm -rf {} + 2>/dev/null || true
'''
}
}
stage('4. Verify Health & Send Telegram Report') {
steps {
script {
// Đo lại tỷ lệ % Disk Space và % Inodes sau khi dọn dẹp
def diskUsed = sh(script: "df / | tail -1 | awk '{print \$5}' | tr -d '%'", returnStdout: true).trim().toInteger()
def inodeUsed = sh(script: "df -i / | tail -1 | awk '{print \$5}' | tr -d '%'", returnStdout: true).trim().toInteger()
def freeSpaceGB = sh(script: "df -h / | tail -1 | awk '{print \$4}'", returnStdout: true).trim()
currentBuild.displayName = "#${env.BUILD_NUMBER} | Disk: ${diskUsed}% | Inode: ${inodeUsed}%"
def threshold = env.ALERT_THRESHOLD.toInteger()
if (diskUsed >= threshold || inodeUsed >= threshold) {
unstable "⚠️ CẢNH BÁO ĐỎ: Tài nguyên đĩa vượt ngưỡng an toàn (Disk: ${diskUsed}%, Inode: ${inodeUsed}%)!"
sendStorageAlert("CRITICAL", diskUsed, inodeUsed, freeSpaceGB)
} else {
echo "✅ Hệ thống lưu trữ hoàn toàn khỏe mạnh: Disk ${diskUsed}% (Trống ${freeSpaceGB}), Inode ${inodeUsed}%."
}
}
}
}
}
}
def sendStorageAlert(String level, int diskPercent, int inodePercent, String freeGB) {
withCredentials([
string(credentialsId: 'telegram-bot-token', variable: 'TG_TOKEN'),
string(credentialsId: 'telegram-chat-id', variable: 'TG_CHAT_ID')
]) {
sh """
curl -s -X POST "https://api.telegram.org/bot\${TG_TOKEN}/sendMessage" \\
-d chat_id="\${TG_CHAT_ID}" \\
-d parse_mode="HTML" \\
--data-urlencode text="🚨 <b>[CẢNH BÁO Ổ CỨNG JENKINS AGENT - ${level}]</b>
━━━━━━━━━━━━━━━
🖥 <b>Node:</b> <code>${env.NODE_NAME}</code>
💾 <b>Disk Usage:</b> <code>${diskPercent}%</code> (Còn trống: <b>${freeGB}</b>)
🗂 <b>Inode Usage:</b> <code>${inodePercent}%</code>
⚠️ <b>Ngưỡng cảnh báo:</b> <code>${env.ALERT_THRESHOLD}%</code>
━━━━━━━━━━━━━━━
<i>Hệ thống đã chạy kịch bản tự động dọn dẹp lúc 02:00 sáng nhưng dung lượng vẫn cao. Vui lòng kiểm tra mở rộng EBS/Disk Volume!</i>" > /dev/null || true
"""
}
}
All Rights Reserved