最近在帮团队排查一个诡异的 Jenkins 构建问题:前端项目打包时日志显示 SUCCESS,但实际产物却缺失关键文件。这种情况特别容易导致线上事故,因为构建系统告诉你成功了,但实际上传的是残缺的代码包。
这个问题有几个非常典型的特征:
Killed 字样这种"假成功"比直接构建失败更危险,原因有三:
提示:我曾遇到过团队因此问题导致线上事故,花了 3 小时才定位到是构建环节的问题,教训深刻。
当系统内存不足时,Linux 内核的 OOM Killer(Out-Of-Memory Killer)会选择性终止某些进程来保护系统。它基于一套评分机制:
在前端构建场景中,Node.js 进程(特别是 Webpack/Vite)通常会成为受害者,因为:
这里存在两个关键误解:
典型的问题脚本模式:
bash复制npm run build # 被 OOM Killer 终止,但退出码可能仍为 0
zip -r dist.zip dist # 继续执行
scp dist.zip user@server:/path # 继续执行
# 最后一步成功,整体脚本返回 0,Jenkins 显示 SUCCESS
很多工程师看到服务器总内存充足就认为不会出问题,实际上有几个认知盲区:
即使机器有 16GB 内存:
--max_old_space_size 参数显式调整当 Jenkins 运行在 Docker 中时:
bash复制docker inspect jenkins | grep -i memory
可能会发现容器内存限制远小于宿主机(如 2GB vs 16GB)
监控系统显示的平均内存使用率可能具有欺骗性:
在 Jenkins 服务器上执行:
bash复制sudo dmesg -T | grep -i -E "out of memory|killed process|oom"
典型输出示例:
code复制[Fri Jul 14 10:23:45 2023] Out of memory: Kill process 12345 (node) score 998 or sacrifice child
[Fri Jul 14 10:23:45 2023] Killed process 12345 (node) total-vm:2465432kB, anon-rss:1896544kB, file-rss:0kB
在构建脚本中加入校验步骤:
bash复制# 检查关键文件是否存在
test -f dist/index.html || exit 1
test -f dist/assets/main.*.js || exit 1
test -f dist/assets/main.*.css || exit 1
# 或者计算文件数量(根据项目调整阈值)
file_count=$(find dist -type f | wc -l)
if [ "$file_count" -lt 20 ]; then
echo "Error: dist file count too low ($file_count)"
exit 1
fi
在构建脚本最前面添加:
bash复制export NODE_OPTIONS="--max_old_space_size=4096" # 4GB
内存大小建议:
在脚本开头添加:
bash复制#!/usr/bin/env bash
set -euo pipefail
set -e:任何命令失败立即退出set -u:使用未定义变量时报错set -o pipefail:管道中任意命令失败则整个管道失败如果使用 Docker,确保容器有足够内存:
bash复制docker run -d \
--name jenkins \
--memory 8g \ # 限制 8GB
--memory-swap 10g \ # 交换分区 2GB
-p 8080:8080 \
jenkins/jenkins:lts
即使物理内存充足,Swap 也能提供缓冲:
bash复制# 创建 4GB swap 文件
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
验证:
bash复制free -h
total used free shared buff/cache available
Mem: 16G 5.2G 8.1G 456M 2.7G 10G
Swap: 4.0G 512M 3.5G
修改前端构建配置:
javascript复制// vite.config.js
export default defineConfig({
build: {
sourcemap: false, // 关闭 sourcemap 可节省内存
cssCodeSplit: true,
chunkSizeWarningLimit: 1500, // 调整 chunk 大小警告阈值
}
})
在 Jenkins 系统配置中:
lock 插件防止并行:groovy复制lock(resource: 'node_build_lock') {
sh 'npm run build'
}
groovy复制pipeline {
agent any
environment {
NODE_OPTIONS = "--max_old_space_size=4096"
}
stages {
stage('Setup') {
steps {
sh '''
node -v
npm -v
npm ci
'''
}
}
stage('Build') {
steps {
sh '''
set -euo pipefail
npm run build
# 产物校验
test -d dist || (echo "dist directory missing!" && exit 1)
test -f dist/index.html || (echo "index.html missing!" && exit 1)
js_files=$(find dist/assets -name "*.js" | wc -l)
[ "$js_files" -gt 0 ] || (echo "No JS files found!" && exit 1)
'''
}
}
stage('Deploy') {
steps {
sh '''
zip -r dist.zip dist
scp -o StrictHostKeyChecking=no dist.zip deploy@server:/path/
ssh deploy@server "unzip -o /path/dist.zip -d /app"
'''
}
}
}
post {
failure {
slackSend channel: '#alerts', message: "Build failed: ${currentBuild.fullDisplayName}"
}
}
}
安装 Prometheus 插件,监控构建过程中的内存使用:
groovy复制stage('Build') {
steps {
wrap([$class: 'PrometheusBuildWrapper']) {
sh 'npm run build'
}
}
}
在 Jenkins 全局配置中添加日志扫描规则:
\bKilled\b在 Jenkinsfile 中添加资源报告:
groovy复制post {
always {
script {
def duration = currentBuild.durationString
def maxMemory = sh(script: "grep VmPeak /proc/self/status | awk '{print \$2}'", returnStdout: true).trim()
echo "Build duration: ${duration}, Peak memory: ${maxMemory} KB"
}
}
}
每月执行一次全量构建审计:
Killed 记录将以下内容加入 DevOps 入职培训:
dmesg 日志查看方法对于特别大型的前端项目:
这个问题教会我们几个重要的 DevOps 原则:
在实际操作中,我发现最容易忽略的是瞬时内存峰值问题。有一次我们团队在升级 Webpack 版本后突然出现这个问题,就是因为新版本在某些 edge case 下会产生短暂但极高的内存使用。通过增加 Swap 空间和调整 Node 内存参数,最终稳定了构建过程。