返回博客

JVM 存活监控别盯 logback 滚动文件,要盯 stdout

给某后台 Java 服务写健康检查,规格是"日志文件超过 5 分钟没有新行就告警"。于是我自然地把告警目标指向 logback 输出的那个滚动文件——logs/app.log.2026-05-15-0.log,看起来最新、最顺手。结果业务低峰期持续告警,端口通、PID 在、服务健康,就是日志文件一动不动。

现象

凌晨流量低谷:

  • nc -z 127.0.0.1 8080 返回正常
  • ps aux | grep app.jar 进程存在
  • /proc/<PID>/net/tcp 连接活跃
  • stat logs/app.log.2026-05-15-0.log 显示 mtime 已经 8 小时前

监控脚本判定"超过阈值无写入",连续触发告警。

根因

logback 的滚动文件记录的是业务流量,不是进程活跃度。

几个具体机制:

  1. INFO 级别在零请求时完全沉默——没有请求就没有 access log,INFO 级业务日志同样安静
  2. ERROR 级别沉默才是好事——没有错误不写,mtime 不更新
  3. -0.log 可能是最旧的分片——logback 按大小滚动时,-0.log 是最早的一段,-N.log 才是当前写入点
  4. 跨日滚动——每天 0 点文件名换一个,旧文件名的 mtime 从此永远不变

所以,logback 文件的 mtime = 上一次业务事件的时间。业务空闲 8 小时 = mtime 静止 8 小时,但进程完全正常。

坑:logback 滚动文件 mtime 不等于进程存活信号

app.log.YYYY-MM-DD-0.log 的 mtime 当做 liveness 指标,会在低流量窗口(夜间、周末、冷备服务)持续误报。你盯的是"最后一次有用户访问的时间",不是"进程还在不在"。

正解

/proc/<PID>/fd/1 对应的 stdout 文件做活跃度探针。

nohup java -jar app.jar > app.log 2>&1 & 启动的 JVM,fd 1(stdout)和 fd 2(stderr)都重定向到同一个文件。即便零业务流量,这个文件也会被 JVM 本身持续写入:

  • Spring Boot 周期任务心跳
  • 连接池保活日志
  • Tomcat NIO connector 内部事件
  • JVM GC 输出(如果开了 verbose gc)
  • 框架的 @Scheduled 任务

实测 Spring Boot Web 服务在纯空载时,stdout 每 30 秒左右仍有写入。

先找准 stdout 对应的实际文件路径,不要凭文件名猜:

ls -la /proc/$(pgrep -f "app.jar")/fd/ | grep -E 'log|out|err'
# 输出示例:
# 1 -> /data/jar/svc/app.log     # stdout — 这才是 liveness 目标
# 2 -> /data/jar/svc/app.log     # stderr — 同一文件
# 8 -> /data/jar/svc/logs/app.log.2026-05-15-0.log  # logback — 业务日志

推荐的三层健康检查(按信号强度排序):

# 第一层:端口存活(最强信号,挂了必告警)
nc -z -w 2 127.0.0.1 8080 || alert "port down"
 
# 第二层:进程存在(兜底)
pgrep -f "app.jar" >/dev/null || alert "process dead"
 
# 第三层:stdout 活跃度(弱信号,阈值放宽)
LOG=/data/jar/svc/app.log
AGE=$(( $(date +%s) - $(stat -c %Y "$LOG") ))
[ $AGE -gt 900 ] && alert "stdout silent for ${AGE}s"

阈值不要拍脑袋定。先采样一周低谷时段的实际静默时长:

# 每 2 分钟记一次 stdout 的 mtime
*/2 * * * * stat -c '%Y' /data/jar/svc/app.log >> /tmp/mtime-samples.log

取 p99 再乘以 1.5 作为告警阈值。多数 Spring Boot Web 服务落在 10–15 分钟;纯批处理 worker 可能需要 30 分钟以上。

正解:盯 fd 1/2 的 stdout 文件,不是 logback 业务日志

通过 /proc/<PID>/fd/ 找到真正的 stdout 文件路径,以其 mtime 做弱存活信号。阈值采样定,不要默认 5 分钟。纯低频服务(如内部管理后台)直接跳过 stdout 检查,只做端口 nc。

几个特殊场景

纯低频内部后台(管理端、运营工具):确实可能 8 小时以上没有任何写入,stdout 检查没有意义。直接只做端口 nc,在监控配置里标注原因和采样依据。

有定时重启的服务(如 0 3 * * * restart.sh):凌晨 3 点 stdout 文件会被截断,mtime 重置,可能产生瞬间的"文件缩小"误判。检查脚本要忽略重启窗口(如 03:00–03:10)。

挂了 Arthas 的 JVM:不要在存活监控脚本里用 jstackjcmd 等 attach 类命令——attach 会短暂加锁 JVM,生产环境可能卡住请求。监控永远只做 ncpsstat 这类外部只读操作。

一句话外卖

logback 滚动文件的 mtime 是业务流量计,不是进程心跳;JVM liveness 探针应该指向 stdout(/proc/<PID>/fd/1),它在零业务时依然有框架级写入。