Hướng dẫn Setup Jenkins cho hệ thống Monolith + Bài học 38 + Xử lý sự cố phổ biến: Lỗi tràn bộ nhớ (Out of Memory), treo Pipeline và xung đột Plugin
Ở Bài 37, chúng ta đã dựng xong trạm radar giám sát Prometheus & Grafana để theo dõi từng nhịp đập của hệ thống Jenkins. Nhờ có biểu đồ giám sát, bạn sẽ sớm phát hiện ra những khoảnh khắc "sinh tử" trong vận hành thực tế của một hệ thống Monolith:
-
Đang giữa giờ cao điểm release, Jenkins Master bỗng dưng sập nguồn với dòng chữ kinh hoàng:
java.lang.OutOfMemoryError: Java heap spacehoặc bị hệ điều hành Linux lạnh lùng giết chết (OOM Killer - Exit code 137). -
Một bản build Monolith bị treo cứng ở trạng thái đang chạy (Spinning wheel) suốt 12 tiếng đồng hồ, bấm nút dấu
[X]đỏ trên giao diện chục lần vẫn không tắt được (Zombie Pipeline), chiếm chặt khe thực thi (Executor) của cả công ty. -
Sau khi nâng cấp một vài Plugin vào cuối tuần, sáng thứ Hai mở lên toàn bộ các Pipeline đều báo lỗi
NoSuchMethodErrorhoặc Jenkins rơi vào màn hình chết chócError 503 Service Unavailable.
Bài học hôm nay là cuốn cẩm nang Cấp cứu & Xử lý sự cố thực chiến (Troubleshooting Playbook) giúp bạn chẩn đoán đúng nguyên nhân gốc rễ (Root Cause Analysis) và giải cứu hệ thống Jenkins chỉ trong vài phút!
1. Phân biệt 2 loại lỗi tràn bộ nhớ (Out of Memory - OOM) kinh điển trong dự án Monolith
Khi một bản build của dự án Monolith chết vì thiếu RAM, 90% kỹ sư thường nhầm lẫn và đi tăng RAM bừa bãi. Thực tế, có 2 kiểu tràn bộ nhớ hoàn toàn khác nhau xảy ra ở 2 nơi khác nhau:
| Loại lỗi OOM | Dấu hiệu trong Console Log | Thủ phạm thực sự là ai? | Cách xử lý triệt để |
|---|---|---|---|
Loại 1: java.lang.OutOfMemoryError: Java heap space |
Xuất hiện stack trace của Java ngay trên màn hình Jenkins UI hoặc trong /var/log/jenkins/jenkins.log. |
Jenkins Master (JVM) bị tràn bộ nhớ khi cố gắng đọc một file báo cáo JUnit/Coverage XML nặng 500MB hoặc parse Console Log dài hàng triệu dòng. | Tinh chỉnh tham số JAVA_OPTS (-Xms, -Xmx, G1GC) của Jenkins Master và tuyệt đối không xử lý JSON/XML khổng lồ bằng hàm Groovy trực tiếp trên Master. |
Loại 2: script returned exit code 137 (Killed) |
Đang chạy lệnh composer install, npm run build hoặc phpunit thì dừng đột ngột kèm dòng: Killed / script returned exit code 137. |
Linux Kernel (OOM Killer) trên máy chủ Worker Agent đã bắn hạ tiến trình build của Monolith vì ngốn hết sạch RAM vật lý của máy Agent! (137 = 128 + 9 SIGKILL). |
Tạo Swap File dự phòng trên Agent, giới hạn bộ nhớ của Node.js (--max-old-space-size), PHP (memory_limit) và giảm số lượng luồng parallel. |
2. Khắc phục triệt để lỗi OOM trên cả Jenkins Master và Worker Agent
Giải pháp A: Cấu hình chuẩn bộ nhớ JVM (JAVA_OPTS) cho Jenkins Master
Mặc định nếu không khai báo, Java chỉ cấp cho Jenkins một lượng RAM rất nhỏ (hoặc chiếm quá tay nếu chạy trong Docker không giới hạn). Hãy cập nhật tệp docker-compose.yml của Jenkins Master (từ Bài 34) với bộ tham số Garbage Collection G1GC tối ưu nhất cho hệ thống CI/CD tải nặng:
YAML
services:
jenkins-master:
image: jenkins/jenkins:lts-jdk17
container_name: jenkins-master
# Giới hạn cứng trên Docker: Cấp tối đa 6GB RAM cho container, trong đó JVM Heap dùng 4GB (Chừa 2GB cho OS & Native Memory)
deploy:
resources:
limits:
memory: 6G
environment:
- >-
JAVA_OPTS=
-Xms2048m
-Xmx4096m
-XX:+UseG1GC
-XX:+ExplicitGCInvokesConcurrent
-XX:+ParallelRefProcEnabled
-XX:+UseStringDeduplication
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/jenkins_home/logs/heapdump.hprof
-Djenkins.install.runSetupWizard=false
-Dorg.jenkinsci.plugins.workflow.steps.durable_task.DurableTaskStep.USE_WATCHING=true
-
-Xms2048m -Xmx4096m: Khởi tạo ngay 2GB RAM và cho phép tăng tối đa lên 4GB RAM cho vùng nhớ Heap. -
-XX:+UseStringDeduplication: Cực kỳ hiệu quả với Jenkins! Tự động gộp các chuỗi ký tự trùng lặp trong Console Log và biến môi trường giúp giảm tới 20% lượng RAM tiêu thụ. -
-D...DurableTaskStep.USE_WATCHING=true: Chuyển cơ chế đọc log từ kiểu hỏi thăm liên tục (Polling) sang kiểu sự kiện (Event-driven), chống đơ Master khi có nhiều nhánh song song in log cùng lúc.
Giải pháp B: Trị dứt điểm lỗi Exit Code 137 khi build Monolith trên Worker Agent
Khi trình biên dịch Vite/Webpack của Frontend (npm run build) hoặc bộ kiểm thử PHPUnit/Golang chạy song song trên máy chủ Agent (Bài 36), đỉnh điểm tiêu thụ RAM (Memory Spike) trong vài giây có thể vượt quá dung lượng RAM vật lý. Hãy áp dụng 2 chốt chặn sau:
-
Khống chế trần bộ nhớ của trình biên dịch ngay trong
Jenkinsfile:Groovy
environment { // Giới hạn Node.js (Vite/Webpack) chỉ được phép dùng tối đa 2GB RAM khi build Frontend NODE_OPTIONS = "--max-old-space-size=2048" // Cho phép Composer sử dụng bộ nhớ linh hoạt nhưng có kiểm soát COMPOSER_MEMORY_LIMIT = "2G" } -
Thiết lập 4GB Swap File làm "phao cứu sinh" trên máy chủ Linux Agent: Nếu máy chủ Agent có 8GB RAM vật lý, hãy tạo thêm 4GB Swap trên ổ cứng SSD để khi có đỉnh tải đột biến trong 5 giây, Linux sẽ đẩy bớt trang nhớ sang Swap thay vì giết chết bản build bằng mã
137:Bash
# Thực hiện 1 lần trên máy chủ Worker Agent (Bài 31) sudo fallocate -l 4G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
3. Kỹ thuật "Tiêu diệt Zombie Pipeline" (Bản build bị treo không thể tắt bằng nút [X])
Trong quá trình vận hành, đôi khi một kết nối SSH sang máy chủ Staging bị treo mạng, hoặc một tiến trình Docker bị kẹt I/O khiến bản build đứng hình. Khi bạn bấm vào dấu [X] (Abort) trên giao diện web mà bản build vẫn cứ quay vòng tròn không chịu dừng, Jenkins thực chất đang cung cấp 3 cấp độ cưỡng chế dừng từ nhẹ đến nặng, và nếu cả 3 đều bó tay thì chúng ta sẽ dùng Groovy Script Console:
Cấp độ 1: Sử dụng đường dẫn ẩn /stop, /term và /kill ngay trên thanh địa chỉ trình duyệt
Giả sử bản build số #45 của Job monolith-prod-pipeline đang bị treo cứng. Thay vì bấm nút [X] trên giao diện, bạn hãy gửi một request POST (hoặc bấm trực tiếp vào link gợi ý hiện ra trong Console Log của Jenkins):
-
Dừng lịch sự (
/stop): Tương đương với bấm nút[X]trên UI. -
Cưỡng chế kết thúc tiến trình (
/term- Terminate): Ép dừng bước đang kẹt. -
Bắn hạ tức thì (
/kill- Hard Kill): Cắt đứt hoàn toàn luồng Pipeline ngay lập tức mà không chờ chạy khốipost { always }.
Cấp độ 2 (Vũ khí tối thượng): Xóa sổ Zombie Build bằng Groovy Script Console
Nếu ngay cả lệnh /kill cũng không ăn thua do luồng Java bị khóa chết (Deadlock), bạn hãy vào Manage Jenkins -> Script Console và dán đoạn mã "Phẫu thuật cắt bỏ Zombie" dưới đây vào:
Groovy
// Chạy trong Manage Jenkins -> Script Console để cưỡng chế kết thúc bản Build bị treo cứng (Zombie Build)
import jenkins.model.Jenkins
import org.jenkinsci.plugins.workflow.job.WorkflowJob
import org.jenkinsci.plugins.workflow.job.WorkflowRun
import hudson.model.Result
// 1. Điền chính xác tên Job (Nếu nằm trong Folder thì ghi 'Folder-Name/Job-Name') và Số thứ tự bản Build bị treo
String targetJobName = "Monolith-System/monolith-prod-pipeline"
int stuckBuildNumber = 45
WorkflowJob job = Jenkins.instance.getItemByFullName(targetJobName, WorkflowJob.class)
if (job == null) {
println "❌ Không tìm thấy Job: ${targetJobName}"
return
}
WorkflowRun build = job.getBuildByNumber(stuckBuildNumber)
if (build == null) {
println "❌ Không tìm thấy bản Build số #${stuckBuildNumber}"
return
}
println "🔨 Đang tiến hành cưỡng chế tiêu diệt Zombie Build: ${targetJobName} #${stuckBuildNumber}..."
// Bước 1: Thử gửi tín hiệu ngắt chuẩn (Interrupt)
build.doStop()
// Bước 2: Cưỡng chế kết thúc trực tiếp ở tầng Execution Engine
if (build.getExecution() != null) {
build.getExecution().interrupt(Result.ABORTED)
}
// Bước 3: Nếu vẫn còn giữ chặt Executor Slot (Hàng đợi), ép giải phóng ngay lập tức!
def executor = build.getExecutor()
if (executor != null) {
println "⚡ Phát hiện Executor đang bị khóa cứng! Đang gửi lệnh interrupt..."
executor.interrupt(Result.ABORTED)
}
// Bước 4 (Chỉ dùng khi 3 bước trên vẫn không nhúc nhích): Ghi đè trạng thái kết thúc xuống đĩa
build.finish(Result.ABORTED, new java.io.IOException("Cưỡng chế hủy bỏ Zombie Build bởi Quản trị viên qua Script Console"))
println "✅ Đã giải phóng hoàn toàn bản Build #${stuckBuildNumber}!"
4. Phòng chống treo Pipeline từ trong trứng nước: Đặt timeout cho từng lệnh nguy hiểm
Tại sao lại để đến mức phải vào Script Console giết Zombie Build? Nguyên nhân phổ biến nhất khiến một Pipeline Monolith bị treo là:
-
Lệnh
composer installhoặcnpm ciđứng chờ người dùng nhập[yes/no]từ bàn phím do thiếu cờ--no-interaction. -
Lệnh
sshhoặccurlkết nối tới một máy chủ bị rớt mạng nhưng không có thời gian chờ tối đa (connect-timeout), dẫn đến việc nó ngồi đợi phản hồi suốt 60 phút!
Quy tắc phòng vệ chủ động: Ngoài timeout(time: 45, unit: 'MINUTES') cho toàn bộ Pipeline ở đầu bài, bạn luôn phải bọc timeout ngắn (3 – 5 phút) quanh các bước có giao tiếp qua mạng (như waitForQualityGate, sshPublisher, docker push):
Groovy
// Bọc timeout riêng cho bước đẩy Docker Image hoặc chạy lệnh từ xa để không bao giờ bị treo vô hạn
timeout(time: 5, unit: 'MINUTES') {
sh '''
# Luôn thêm cờ --connect-timeout và --max-time khi gọi curl trong Pipeline
curl --connect-timeout 5 --max-time 15 -f http://127.0.0.1:8001/api/health
'''
}
5. Xử lý sự cố "Xung đột Plugin" (Plugin Dependency Hell) sau khi nâng cấp
Jenkins có hơn 1.800 Plugins do cộng đồng phát triển. Đôi khi bạn bấm Update All Plugins, phiên bản mới của plugin A lại không tương thích với plugin B (hoặc yêu cầu phiên bản Jenkins Core mới hơn), dẫn đến 2 kịch bản thảm họa:
Kịch bản 1: Jenkins vẫn vào được Web UI, nhưng chạy Pipeline bị báo lỗi lạ
-
Truy cập ngay vào Manage Jenkins -> Plugins -> Tab Installed plugins.
-
Tìm đến tên Plugin vừa nâng cấp bị lỗi -> Nếu Jenkins giữ lại bản cũ trong ổ cứng, bạn sẽ thấy ngay nút
Downgrade to X.Y.Z(Hạ cấp về phiên bản trước đó). -
Bấm Downgrade và khởi động lại Jenkins là xong!
Kịch bản 2 (Nghiêm trọng): Xung đột Plugin khiến Jenkins Master sập hoàn toàn (Không mở được giao diện Web!)
Khi một Plugin bị lỗi nặng ngay lúc khởi động (Boot Failure), giao diện Web của Jenkins sẽ chết cứng. Đừng hoảng loạn! Như đã học ở Bài 33, toàn bộ các Plugin của Jenkins đều nằm gọn trong thư mục /var/jenkins_home/plugins/ dưới dạng các tệp tin .jpi (hoặc .hpi).
Bạn chỉ cần SSH vào máy chủ Host và thực hiện 3 bước giải cứu từ dòng lệnh Linux:
Bash
# Bước 1: Kiểm tra log khởi động của container Jenkins để biết chính xác tên Plugin nào đang gây sập
docker logs --tail 100 jenkins-master | grep -iE "exception|failed|plugin"
# Bước 2: Vào thư mục chứa Plugins trên ổ cứng
cd /var/jenkins_home/plugins/
# KỸ THUẬT A: Nếu bạn muốn HẠ CẤP (Rollback) một plugin (ví dụ: git.jpi) về bản trước khi bấm Update:
# Mỗi khi nâng cấp một plugin, Jenkins luôn tự động lưu bản cũ vào tệp có đuôi ".bak"!
ls -la git.*
# Bạn sẽ thấy: git.jpi (bản mới bị lỗi) và git.bak (bản cũ đang chạy tốt)
sudo mv git.jpi git.jpi.broken
sudo cp git.bak git.jpi
sudo chown 1000:1000 git.jpi
# KỸ THUẬT B: Nếu bạn muốn VÔ HIỆU HÓA (Disable) tạm thời một plugin gây lỗi để Jenkins khởi động lên được:
# Chỉ cần tạo một tệp rỗng có thêm đuôi ".disabled" trùng tên với plugin đó!
sudo touch offending-plugin-name.jpi.disabled
# Bước 3: Khởi động lại container Jenkins Master
docker restart jenkins-master
Chiến lược quản lý Plugin "Bất biến" với
plugins.txt(Chuẩn Production): Để không bao giờ rơi vào cảnh "chiều Chủ nhật bấm Update Plugin rồi sáng thứ Hai sập hệ thống", các đội ngũ DevOps chuyên nghiệp khóa cứng danh sách phiên bản Plugin trong một tệpplugins.txt(đi kèm với tệpjenkins.yamlcủa JCasC ở Bài 34) và luôn thử nghiệm nâng cấp trên một container Jenkins локаl trước khi áp dụng lên máy chủ chính!
All rights reserved