找出一次代码不执行因为内存超出杀死进程的问题
2025-08-22
问:
Aug 22 14:29:37 ali-national-pro-api kernel: oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=d1fc65f7640062b559d217d35e76b5ca8386c633322b0effca1a3f062fadecb7,mems_allowed=0,oom_memcg=/docker/d1fc65f7640062b559d217d35e76b5ca8386c633322b0effca1a3f062fadecb7,task_memcg=/docker/d1fc65f7640062b559d217d35e76b5ca8386c633322b0effca1a3f062fadecb7,task=php-fpm,pid=629188,uid=48
这是啥意思
答:
你贴的这一段日志其实是 Linux 内核的 OOM killer(Out Of Memory Killer) 在工作时的记录。意思是系统内存(或者 cgroup 限制的内存)不够了,内核强制杀掉了一些进程来释放内存。我们逐段解释一下:
日志逐句拆解
Aug 22 14:29:37 ali-national-pro-api kernel: oom-kill:
时间戳是 8 月 22 日 14:29:37,机器名是 ali-national-pro-api
,信息来自 kernel
。oom-kill
表示内存不足(Out Of Memory)时内核执行的“杀进程”操作。
constraint=CONSTRAINT_MEMCG
表示触发 OOM 的约束条件是 cgroup 内存限制。
也就是说,不是整个机器的内存耗尽,而是某个 容器(docker cgroup)分配的内存额度用完了。
nodemask=(null)
这里没有限制 NUMA 节点。
cpuset=d1fc65f7640062b559d217d35e76b5ca8386c633322b0effca1a3f062fadecb7
这是对应容器的 cpuset(CPU 和内存绑定的集合)。一串哈希,看起来就是 Docker 容器的 ID。
mems_allowed=0
允许使用的内存节点编号。单 NUMA 节点机器时一般就是 0。
oom_memcg=/docker/d1fc65f7640062b559d217d35e76b5ca8386c633322b0effca1a3f062fadecb7
说明是 Docker 容器里的 memory cgroup 内存超限。
task_memcg=/docker/d1fc65f7640062b559d217d35e76b5ca8386c633322b0effca1a3f062fadecb7
具体触发 OOM 的进程所在的 memory cgroup。
task=php-fpm,pid=629188,uid=48
最后是被杀掉的进程:php-fpm
,进程号 629188,用户 ID 48(在很多 Linux 里 UID=48 是 apache
或 php-fpm
的服务用户)。
总结
简而言之:
在 某个 Docker 容器 内,
php-fpm
进程占用内存过多,超出了该容器的内存限制;内核的 OOM Killer 被触发,挑选并杀掉了这个
php-fpm
进程以释放内存;并不是整台宿主机内存不足,而是容器级别的内存限制被打满。
建议的对策
检查容器内存限制
用docker inspect <container_id> | grep -i memory
看看容器设置的memory
限制是多少。查看 php-fpm 配置
每个子进程的内存消耗可能偏高。
检查
pm.max_children
等参数,避免开太多进程。监控内存使用
在容器里用top
、htop
或smem
,或者在宿主机上用docker stats
来观察内存曲线。调大内存或优化代码
若业务确实需要更多内存,调大 Docker 的
--memory
限制。如果是代码泄漏或缓存过度,要排查并优化 PHP 程序。
把以前针对收益法模型的失败重试机制启动起来,至少自己重试3次,以保证最大的成功率。
加入sheel脚本监控:
#!/usr/bin/env bash
set -euo pipefail
# =========================
# 配置(可在命令行参数覆盖)
# =========================
CONTAINERS=${CONTAINERS:-""} # 空则监控所有运行中的容器
THRESHOLD=${THRESHOLD:-85} # 预警阈值(百分比)
INTERVAL=${INTERVAL:-60} # 轮询间隔秒数(仅 --loop 时生效)
ONCE=${ONCE:-0} # 1=只检查一次后退出
HYSTERESIS=${HYSTERESIS:-5} # 抖动回滞:降至(阈值-该值)才视为恢复
STATE_DIR=${STATE_DIR:-/var/run/docker-mem-watch}
LOG_TAG="docker-mem-watch"
# 可选:设置 Slack/Webhook 通知(留空则不发)
WEBHOOK_URL="${WEBHOOK_URL:-}"
usage() {
cat <<EOF
用法: $0 [--containers "c1 c2"] [--threshold 85] [--interval 60] [--once] [--loop]
环境变量也可配置:CONTAINERS, THRESHOLD, INTERVAL, HYSTERESIS, WEBHOOK_URL
例子:
$0 --containers "web php" --threshold 90 --once
CONTAINERS="web php" THRESHOLD=90 WEBHOOK_URL="https://xxxx" $0 --loop
EOF
}
# 单位转换,例如 "1.23GiB" -> bytes
to_bytes() {
local v="$1"
# 形如 "123.45MiB" 或 "123B"
local num unit
num=$(echo "$v" | awk '{print $1}' | sed 's/,/./g')
unit=$(echo "$v" | awk '{print $2}')
case "$unit" in
B|Bytes|"") awk -v n="$num" 'BEGIN{printf "%.0f", n}' ;;
kB|KB) awk -v n="$num" 'BEGIN{printf "%.0f", n*1024}' ;;
MB|MiB) awk -v n="$num" 'BEGIN{printf "%.0f", n*1024*1024}' ;;
GB|GiB) awk -v n="$num" 'BEGIN{printf "%.0f", n*1024*1024*1024}' ;;
TB|TiB) awk -v n="$num" 'BEGIN{printf "%.0f", n*1024*1024*1024*1024}' ;;
*) # 某些 docker 版本 MemUsage like: "123MiB"
if [[ "$v" =~ ^([0-9\.\,]+)([KMGTP]i?B)$ ]]; then
num="${BASH_REMATCH[1]}"; unit="${BASH_REMATCH[2]}"
case "$unit" in
KiB|KB) awk -v n="$num" 'BEGIN{printf "%.0f", n*1024}' ;;
MiB|MB) awk -v n="$num" 'BEGIN{printf "%.0f", n*1024*1024}' ;;
GiB|GB) awk -v n="$num" 'BEGIN{printf "%.0f", n*1024*1024*1024}' ;;
TiB|TB) awk -v n="$num" 'BEGIN{printf "%.0f", n*1024*1024*1024*1024}' ;;
*) echo 0 ;;
esac
else
echo 0
fi
;;
esac
}
send_alert() {
local level="$1" # ALERT or RECOVER
local name="$2"
local cid="$3"
local pct="$4"
local usage_human="$5"
local limit_human="$6"
logger -t "$LOG_TAG" "$level: $name ($cid) memory ${pct}% [${usage_human}/${limit_human}]"
if [[ -n "$WEBHOOK_URL" ]]; then
# 简单 JSON;若是 Slack 兼容 webhook,直接显示文本
curl -m 5 -sS -X POST -H 'Content-Type: application/json' \
-d "{\"text\":\"[$level] ${name} (${cid}) 内存使用 ${pct}% (${usage_human}/${limit_human})\"}" \
"$WEBHOOK_URL" >/dev/null || true
fi
}
ensure_state() { mkdir -p "$STATE_DIR"; }
set_state() { echo "$2" > "${STATE_DIR}/$1.state"; }
get_state() { [[ -f "${STATE_DIR}/$1.state" ]] && cat "${STATE_DIR}/$1.state" || echo "OK"; }
check_one() {
local c="$1"
# docker stats 可能出现 locale 百分号,我们自行算百分比更稳妥
# MemUsage 形如 "123.45MiB / 2GiB"
local mem_usage_line
mem_usage_line=$(docker stats --no-stream --format "{{.MemUsage}}" "$c" 2>/dev/null || true)
if [[ -z "$mem_usage_line" ]]; then
logger -t "$LOG_TAG" "WARN: 无法获取 $c 的 stats"
return
fi
local used_human limit_human
used_human=$(echo "$mem_usage_line" | awk -F'/' '{gsub(/^[ \t]+|[ \t]+$/, "", $1); print $1}')
limit_human=$(echo "$mem_usage_line" | awk -F'/' '{gsub(/^[ \t]+|[ \t]+$/, "", $2); print $2}')
local used_bytes limit_bytes
used_bytes=$(to_bytes "$used_human")
limit_bytes=$(to_bytes "$limit_human")
# 若 inspect 有明确 Memory 限制,优先使用
local hard_limit
hard_limit=$(docker inspect -f '{{.HostConfig.Memory}}' "$c" 2>/dev/null || echo 0)
if [[ "$hard_limit" -gt 0 ]]; then
limit_bytes="$hard_limit"
# 友好显示
if [[ "$limit_bytes" -ge 1099511627776 ]]; then limit_human="$(awk -v b="$limit_bytes" 'BEGIN{printf "%.2fTiB", b/1024/1024/1024/1024}')"
elif [[ "$limit_bytes" -ge 1073741824 ]]; then limit_human="$(awk -v b="$limit_bytes" 'BEGIN{printf "%.2fGiB", b/1024/1024/1024}')"
else limit_human="$(awk -v b="$limit_bytes" 'BEGIN{printf "%.2fMiB", b/1024/1024}')"
fi
fi
if [[ "$limit_bytes" -le 0 ]]; then
logger -t "$LOG_TAG" "INFO: $c 未设置内存上限(可能使用宿主机上限),跳过阈值判断,仅记录。used=${used_human} limit=${limit_human}"
return
fi
local pct
pct=$(awk -v u="$used_bytes" -v l="$limit_bytes" 'BEGIN{printf "%.0f", (u/l)*100}')
local name cid
name=$(docker inspect -f '{{.Name}}' "$c" 2>/dev/null | sed 's#^/##' || echo "$c")
cid=$(docker inspect -f '{{.Id}}' "$c" 2>/dev/null | cut -c1-12 || echo "$c")
local state new_state
state=$(get_state "$cid")
new_state="$state"
if [[ "$pct" -ge "$THRESHOLD" && "$state" != "ALERT" ]]; then
new_state="ALERT"
send_alert "ALERT" "$name" "$cid" "$pct" "$used_human" "$limit_human"
elif [[ "$pct" -le $((THRESHOLD - HYSTERESIS)) && "$state" == "ALERT" ]]; then
new_state="OK"
send_alert "RECOVER" "$name" "$cid" "$pct" "$used_human" "$limit_human"
fi
set_state "$cid" "$new_state"
}
one_round() {
local list=()
if [[ -n "$CONTAINERS" ]]; then
# 用户指定
read -r -a list <<<"$CONTAINERS"
else
# 监控所有运行容器
mapfile -t list < <(docker ps --format '{{.Names}}')
fi
for c in "${list[@]}"; do
check_one "$c"
done
}
main() {
if [[ $# -gt 0 ]]; then
while [[ $# -gt 0 ]]; do
case "$1" in
--containers) shift; CONTAINERS="${1:-}";;
--threshold) shift; THRESHOLD="${1:-}";;
--interval) shift; INTERVAL="${1:-}";;
--once) ONCE=1;;
--loop) ONCE=0;;
-h|--help) usage; exit 0;;
*) echo "未知参数: $1"; usage; exit 1;;
esac
shift || true
done
fi
ensure_state
if [[ "$ONCE" -eq 1 ]]; then
one_round
else
while :; do
one_round
sleep "$INTERVAL"
done
fi
}
main "$@"
———
一、它在做什么(总体功能)
监控一个或一组 Docker 容器的内存占用。
计算“已用/上限”的百分比,达到阈值就发出“ALERT”告警;降回到阈值以下(含回滞 HYSTERESIS)时发“RECOVER”恢复。
告警默认写入系统日志(syslog/journal,tag 为 docker-mem-watch),可选通过 WEBHOOK_URL 以 HTTP POST 发送到外部(你不需要就留空)。
可单次检测(—once,适合 cron)或循环检测(—loop + INTERVAL,适合常驻)。
既支持你显式列容器名,也支持“自动监控当前所有运行中的容器”。
———
二、它如何做到(工作流程)
解析参数与环境变量
参数优先级高于环境变量。支持 —containers、—threshold、—interval、—once/—loop。
环境变量允许在 cron/systemd 里直接赋值(CONTAINERS/THRESHOLD/INTERVAL/HYSTERESIS/WEBHOOK_URL/STATE_DIR)。
确定监控对象
若设置 CONTAINERS,则按空格切分监控这些容器;
否则通过
docker ps --format '{{.Names}}'
获取所有运行中容器名进行监控。采样每个容器的内存
运行
docker stats --no-stream --format "{{.MemUsage}}" <container>
。该字段一般是“已用 / 上限”形如:
123.45MiB / 2GiB
。脚本从中切出“已用”和“上限”的人类可读数值,并通过
to_bytes
函数规范化为字节数,避免不同 Docker/locale 的单位差异。再用
docker inspect -f '{{.HostConfig.Memory}}'
获取容器硬上限(字节)。若有明确上限(>0),以此为准(更准确)。计算百分比并做阈值判定
百分比
pct = used_bytes / limit_bytes * 100
(四舍五入为整数)。若容器未设置上限(limit_bytes <= 0),脚本只记录信息并跳过阈值判断(避免误报)。
引入“回滞”HYSTERESIS:超过阈值触发一次 ALERT;只有降到
阈值 - 回滞
才触发一次 RECOVER,避免在边缘抖动。告警与状态记忆
通过
logger -t docker-mem-watch
把 ALERT/RECOVER 写到系统日志。若设置 WEBHOOK_URL,会再以简单 JSON POST 形式发送一条文本到你的 webhook。
每个容器的当前状态(OK/ALERT)保存在 STATE_DIR(默认
/var/run/docker-mem-watch
)下的CID.state
文件中,用来判断本轮是否需要再发告警或恢复通知。运行模式
单次模式:
--once
,跑一轮后退出(适合 cron)。循环模式:
--loop
,每次one_round
后sleep $INTERVAL
,持续运行(适合 systemd service 持久化)。
———
三、关键配置项(环境变量与参数)
CONTAINERS:空字符串表示监控所有运行容器;否则写
"php-fpm web"
这种空格分隔列表。THRESHOLD:阈值百分比(默认 85),到达或超过触发 ALERT。
HYSTERESIS:回滞(默认 5),在 ALERT 状态下需降到
THRESHOLD - HYSTERESIS
(例如 90-5=85%)才触发 RECOVER。INTERVAL:循环模式下的采样间隔秒(默认 60)。
WEBHOOK_URL:可选的外部通知地址(为空则不发送)。
STATE_DIR:状态文件目录(默认
/var/run/docker-mem-watch
)。命令行参数:
--containers
、--threshold
、--interval
、--once
、--loop
、-h/--help
。
———
四、它会输出什么(日志格式)
正常记录:
当超过阈值第一次发生:
ALERT: <name> (<cid12>) memory <pct>% [<used_human>/<limit_human>]
当从告警恢复:
RECOVER: <name> (<cid12>) memory <pct>% [<used_human>/<limit_human>]
采样异常(例如容器刚好退出):
WARN: 无法获取 <container> 的 stats
未设上限(只记流水、不做阈值判断):
INFO: <container> 未设置内存上限...
这些都通过 logger
进入 syslog/journal,你可用:journalctl -t docker-mem-watch -f
实时查看,或按你的 cron 重定向到 /var/log/docker-mem-watch.log
。
———
五、核心细节与实现要点
单位解析
to_bytes
兼容
B、KB/kB、MB/MiB、GB/GiB、TB/TiB
,并处理某些 Docker 输出中单位紧贴数字的情况(如123MiB
)。统一换算为字节,避免地区化小数点、单位差异带来的误差。
上限优先级
docker stats
的“上限”字段在 cgroup 未设置硬限制时可能是宿主机内存;若
docker inspect
读到.HostConfig.Memory
> 0,则以此为准(这是容器真正的硬上限),并把它格式化成人类可读显示。状态机与回滞
每容器维持一个状态文件(OK/ALERT),只在状态变化时告警/恢复;
回滞避免上下抖动导致的频繁通知。
运行健壮性
set -euo pipefail
:遇到未定义变量、管道错误或非零退出时直接失败,防止静默问题;2>/dev/null || true
:读取 docker 信息时容忍单点失败,不导致整轮退出;STATE_DIR
自动创建;未知参数时打印usage()
并退出。
———
六、怎么部署(你关心的“只写日志,不发 webhook”)
给执行权限(这是你先前 cron 报错的根本原因):
sudo chmod +x /usr/local/bin/docker-mem-watch.sh
用 cron 单次模式并把输出汇总到独立日志文件:
加入:
(不设置 WEBHOOK_URL,脚本就不会发外部请求;日志既有你重定向的 stdout/stderr,也有 syslog 里的
logger
记录。)* * * * * CONTAINERS="php-fpm" THRESHOLD=90 /usr/local/bin/docker-mem-watch.sh --once >> /var/log/docker-mem-watch.log 2>&1
sudo touch /var/log/docker-mem-watch.log
sudo chmod 644 /var/log/docker-mem-watch.log
sudo crontab -e
———
七、常见边界与排障
没有限制内存的容器:脚本只记录不告警。建议为关键容器设置
--memory
,防 OOM 拖垮宿主。权限问题:cron 默认用
/bin/sh
调用脚本,但脚本有 shebang#!/usr/bin/env bash
,前提是必须具备可执行权限;若非 root 执行,还需该用户在docker
组。容器瞬时抖动:可适当提高 THRESHOLD 或增大 HYSTERESIS,减少告警噪声。
只想监控特定容器:设置
CONTAINERS="php-fpm"
,避免监控全部。想看所有运行容器:留空
CONTAINERS
即可自动枚举。
———
八、你可以怎么改(按你的诉求更“只写日志”)
如果你希望完全不走 syslog、只往文件写,可以在脚本里把 send_alert()
的 logger -t ...
改成 echo
,再由 cron 重定向到文件即可。例如:
# 把这行logger -t "$LOG_TAG" "$level: $name ($cid) memory ${pct}% [${usage_human}/${limit_human}]"# 改成echo "$(date '+%F %T') $level: $name ($cid) memory ${pct}% [${usage_human}/${limit_human}]"
这样所有告警行都会走到你 cron 的 >> /var/log/docker-mem-watch.log
文件里,清清楚楚,一眼能看。
———
如夜话,至此。
发表评论: