lac_agent自愈链路上篇——crontab守护的那些坑与健康检查实战



兼容 是对前人努力的尊重 是确保业务平稳过渡的基石 然而 这仅仅是故事的起点


凌晨三点半,手机震了一下,是Zabbix告警。

我迷迷糊糊摸过手机看了一眼,差点没从床上弹起来------三台机器同时报license异常。不是什么网络抖动,是lac_agent进程直接没了,授权文件过了校验窗口期之后自动失效,数据库进入了只读模式。

关键是,这三台机器的crontab都是配好的,按道理说每5分钟就会尝试拉起一次lac_agent。结果呢?一个都没拉起来。

后来排查了一宿才搞清楚原因,今天就把这些坑记录下来,也给后面用LAC做集中授权管理的朋友提个醒。

crontab守护到底干了啥

先说清楚crontab守护的工作机制,不然后面有些坑你理解不了。

执行 lac_agent start 的时候,这个命令做了两件事:第一,把lac_agent作为后台常驻进程启动起来;第二,在当前用户的crontab里面写入一条定时任务,每5分钟尝试启动一次lac_agent。

对,你没看错,是写crontab,不是systemd,不是supervisor,就是最朴素的crontab。

你可以看看自己的crontab:

bash 复制代码
$ crontab -l
# 下面这行就是lac_agent start自动写入的
*/5 * * * * /home/kingbase/Server/bin/lac_agent start >> /home/kingbase/lac_cron.log 2>&1

这个设计的初衷是好的------进程挂了不要紧,5分钟后crontab会重新拉起。听起来挺美,但实际运行中你会发现,这个"自愈"远没有想象中那么可靠。

第一个坑:进程僵死,crontab以为"还活着"

这是最坑的一种情况。

lac_agent进程没有完全退出,但已经进入了僵死状态(zombie)或者不可中断睡眠状态(D state)。这时候你去ps看,进程是存在的:

bash 复制代码
$ ps aux | grep lac_agent
kingbase  12345  0.0  0.1  45678  2345 ?   Ss   Jun20   0:03 lac_agent

看着挺正常的对吧?PID还在,状态是Ss。但你执行 lac_agent status 看看:

bash 复制代码
$ lac_agent status
lac_agent is not responding

进程在,但是不干活了。crontab每5分钟跑一次 lac_agent start,这个start命令内部会检查进程是否存在------发现PID在,就认为"哦,lac_agent还活着呢",然后退出,不会重新拉起。

这就是典型的"植物人"问题。人躺在那儿,心跳还有,但已经不省人事了。crontab只检查心跳不检查意识。

我遇到那次就是这种情况。某次网络抖动导致lac_agent在连接LAC服务端的时候卡住了,底层socket调用进入了D状态(uninterruptible sleep),这种状态连kill -9都杀不掉,只能等I/O完成或者重启机器。

怎么检测僵死?光看PID是不够的,得结合多种方式:

bash 复制代码
#!/bin/bash
# lac_agent健康检查脚本 v1
# 用法: bash check_lac_agent.sh

KB_HOME="/home/kingbase/Server"
LAC_LOG="/home/kingbase/lac/lac_agent.log"

# 第一层:检查进程是否存在
PID=$(pgrep -f "${KB_HOME}/bin/lac_agent")
if [ -z "$PID" ]; then
    echo "[CRIT] lac_agent进程不存在!"
    # 直接拉起
    ${KB_HOME}/bin/lac_agent start
    exit 1
fi

# 第二层:检查进程状态是否为Z(僵尸)或D(不可中断睡眠)
STATE=$(ps -o state= -p $PID 2>/dev/null)
if [[ "$STATE" == "Z" ]] || [[ "$STATE" == "D" ]]; then
    echo "[CRIT] lac_agent进程僵死,状态: $STATE"
    # 尝试kill
    kill -9 $PID 2>/dev/null
    sleep 2
    # 检查是否真的杀掉了
    if ps -p $PID > /dev/null 2>&1; then
        echo "[ALERT] kill -9无法杀死进程,状态为D,可能需要重启机器"
        exit 2
    fi
    # 重新拉起
    ${KB_HOME}/bin/lac_agent start
    exit 1
fi

# 第三层:调用lac_agent status检查实际响应
STATUS=$(${KB_HOME}/bin/lac_agent status 2>&1)
if echo "$STATUS" | grep -qi "not responding\|not running\|error"; then
    echo "[WARN] lac_agent进程在但不响应,status输出: $STATUS"
    kill -9 $PID 2>/dev/null
    sleep 2
    ${KB_HOME}/bin/lac_agent start
    exit 1
fi

# 第四层:检查日志是否太久没更新
if [ -f "$LAC_LOG" ]; then
    LAST_MODIFY=$(stat -c %Y "$LAC_LOG")
    NOW=$(date +%s)
    DIFF=$(( (NOW - LAST_MODIFY) / 60 ))
    if [ $DIFF -gt 30 ]; then
        echo "[WARN] lac_agent日志超过${DIFF}分钟未更新,可能已假死"
        kill $PID 2>/dev/null
        sleep 2
        ${KB_HOME}/bin/lac_agent start
        exit 1
    fi
fi

echo "[OK] lac_agent运行正常,PID: $PID"
exit 0

这个脚本做了四层检查:进程在不在、进程状态对不对、lac_agent自身状态响应、日志最后更新时间。基本上能覆盖大部分"假活"场景。

第二个坑:crontab拉起逻辑的陷阱

你以为crontab就是简单的"5分钟拉一次"?实际上 lac_agent start 被重复调用的时候,内部有一些判断逻辑。

当crontab调用 lac_agent start 的时候,它会检查是不是已经有一个lac_agent在运行。如果检测到了已有的进程,它就不会再启动新的。这个检测机制是基于PID文件还是基于进程名匹配,官方文档没细说,但根据我的实际经验,它应该是通过进程名+端口来判断的。

问题出在哪呢?假设你的lac_agent因为某种原因(比如OOM被系统kill了),但是它的PID文件没来得及清理。下一次crontab触发的时候,lac_agent start检测到PID文件存在,可能就会认为"还在运行",然后什么都不做就退出了。

这种情况你需要手动清理PID文件:

bash 复制代码
# 查找PID文件位置
find /home/kingbase/Server -name "*.pid" | grep lac

# 确认进程确实不存在后再删
ps aux | grep lac_agent | grep -v grep
# 如果确认没有进程在跑,但PID文件还在
rm -f /home/kingbase/Server/var/run/lac_agent.pid

# 然后重新拉起
/home/kingbase/Server/bin/lac_agent start

还有一种更隐蔽的情况------crontab拉起成功了,但是新的lac_agent启动之后发现端口被占用了(旧进程虽然崩了但端口还没释放),新进程启动失败就退出了。然后下一个5分钟,又拉起,还是端口占用......一直循环。

所以我在健康检查脚本里加了一步,拉起之前先确保端口是干净的:

bash 复制代码
# 检查11234端口是否被占用(LAC默认端口)
PORT_OCCUPIED=$(ss -tlnp | grep :11234 | grep -v lac_agent)
if [ -n "$PORT_OCCUPIED" ]; then
    echo "[WARN] 端口11234被非lac_agent进程占用"
    echo "$PORT_OCCUPIED"
    # 根据实际情况决定是否kill
fi

第三个坑:多实例环境下crontab打架

这个坑是我在一个双机热备环境里踩到的。

那套环境一台物理机上跑了两套KES实例,分别属于不同的kingbase用户(kingbase1和kingbase2),每个实例都有自己的lac_agent。

问题来了:两个用户各自执行了 lac_agent start,各自在自己的crontab里写了定时任务。本来相安无事,但有一次做系统维护的时候,运维同事用root身份修改了其中一台的lac_agent.conf配置,导致配置文件的属主变成了root。

然后kingbase1的lac_agent重启时就读不到配置文件了(权限不对),启动失败。但kingbase2的lac_agent正常运行。crontab每5分钟尝试拉起kingbase1的lac_agent,每次都因为权限问题失败。

关键是,这个问题不会在lac_agent.log里面留下很明显的错误提示------因为lac_agent进程根本没启动起来,日志都没开始写。你得去看crontab的执行日志:

bash 复制代码
# 查看crontab执行记录
grep "lac_agent" /var/log/cron | tail -50

# 或者直接看crontab的输出日志
cat /home/kingbase1/lac_cron.log | tail -100

所以我后来把健康检查脚本改了一下,在拉起之前先检查配置文件权限:

bash 复制代码
LAC_CONF="/home/kingbase/Server/share/lac_agent.conf"
if [ -f "$LAC_CONF" ]; then
    CONF_OWNER=$(stat -c %U "$LAC_CONF")
    CURRENT_USER=$(whoami)
    if [ "$CONF_OWNER" != "$CURRENT_USER" ]; then
        echo "[WARN] 配置文件属主不匹配: $CONF_OWNER vs $CURRENT_USER"
        # 这里可以选择修复权限或者直接报警
        # chown $CURRENT_USER:$CURRENT_USER "$LAC_CONF"
    fi
fi

第四个坑:crond自己挂了

这个概率很低,但确实遇到过。

crond是整个crontab调度的基础,如果crond进程本身出了问题(僵死、退出),那所有依赖crontab的守护机制都会失效。

我之前遇到过一次,那台机器负载特别高,crond进入了D状态,所有定时任务都不执行了。lac_agent的crontab守护自然也就废了。

检测方法很简单:

bash 复制代码
# 检查crond进程状态
systemctl status crond

# 如果crond僵死了
ps aux | grep crond
# 看状态是不是D或Z

# 尝试重启crond
systemctl restart crond

但说实话,crond挂了这个事情已经超出了lac_agent自愈的能力范围了。你能做的就是把它纳入到整体的健康检查体系里面去。

实战:用lac_cron.sh做更精细的控制

安装完LAC客户端之后,在bin目录下会有一个 lac_cron.sh 脚本。这个脚本其实就是crontab每次调用的那个入口。你可以看看它的内容:

bash 复制代码
cat /home/kingbase/Server/bin/lac_cron.sh

这个脚本里面做了些基本的检查,但说实话不够用。我的做法是写一个包装脚本,把健康检查的逻辑嵌进去,然后让crontab调用我的包装脚本而不是直接调用lac_agent start:

bash 复制代码
#!/bin/bash
# /home/kingbase/lac/lac_watchdog.sh
# 增强版lac_agent守护脚本,替代crontab直接调用lac_agent start

KB_HOME="/home/kingbase/Server"
LOG_FILE="/home/kingbase/lac/watchdog.log"
ALERT_FILE="/home/kingbase/lac/alert.flag"

log_msg() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOG_FILE"
}

# 检查lac_agent进程
check_and_recover() {
    local PID=$(pgrep -u $(whoami) -f "${KB_HOME}/bin/lac_agent")
    
    # 场景1:进程完全不存在
    if [ -z "$PID" ]; then
        log_msg "[RECOVER] lac_agent进程不存在,尝试拉起"
        ${KB_HOME}/bin/lac_agent start >> "$LOG_FILE" 2>&1
        sleep 3
        # 验证是否成功
        if pgrep -u $(whoami) -f "${KB_HOME}/bin/lac_agent" > /dev/null; then
            log_msg "[OK] lac_agent已成功拉起"
            rm -f "$ALERT_FILE"
        else
            log_msg "[FAIL] lac_agent拉起失败"
            # 写入告警标记
            echo "$(date)" > "$ALERT_FILE"
        fi
        return
    fi
    
    # 场景2:进程存在但僵死
    local STATE=$(ps -o state= -p $PID 2>/dev/null)
    if [[ "$STATE" == "Z" ]]; then
        log_msg "[RECOVER] lac_agent僵死(Z状态),清理并重启"
        # 僵尸进程需要先杀父进程
        local PPID=$(ps -o ppid= -p $PID 2>/dev/null | tr -d ' ')
        if [ -n "$PPID" ] && [ "$PPID" != "1" ]; then
            kill -9 $PPID 2>/dev/null
        fi
        kill -9 $PID 2>/dev/null
        sleep 2
        ${KB_HOME}/bin/lac_agent start >> "$LOG_FILE" 2>&1
        return
    fi
    
    # 场景3:进程存在但无响应(通过status命令检测)
    local STATUS=$(${KB_HOME}/bin/lac_agent status 2>&1)
    if echo "$STATUS" | grep -qi "not responding\|not running\|error\|failed"; then
        log_msg "[RECOVER] lac_agent无响应,status: $STATUS"
        kill -9 $PID 2>/dev/null
        sleep 2
        # 清理可能残留的PID文件
        find ${KB_HOME} -name "*.pid" -path "*lac*" -exec rm -f {} \;
        ${KB_HOME}/bin/lac_agent start >> "$LOG_FILE" 2>&1
        return
    fi
    
    # 场景4:检查日志新鲜度
    local LAC_LOG=$(grep "^log_file" ${KB_HOME}/Server/share/lac_agent.conf 2>/dev/null | awk -F'= ' '{print $2}')
    if [ -n "$LAC_LOG" ] && [ -f "$LAC_LOG" ]; then
        local LAST_MOD=$(stat -c %Y "$LAC_LOG" 2>/dev/null)
        local NOW=$(date +%s)
        local DIFF=$(( (NOW - LAST_MOD) / 60 ))
        if [ $DIFF -gt 20 ]; then
            log_msg "[WARN] lac_agent日志${DIFF}分钟未更新,疑似假死"
            kill $PID 2>/dev/null
            sleep 3
            # 如果kill不掉,尝试kill -9
            if ps -p $PID > /dev/null 2>&1; then
                kill -9 $PID 2>/dev/null
                sleep 2
            fi
            ${KB_HOME}/bin/lac_agent start >> "$LOG_FILE" 2>&1
        fi
    fi
}

# 执行检查
check_and_recover

# 如果存在告警标记,通知运维
if [ -f "$ALERT_FILE" ]; then
    # 这里可以对接你的告警通道(钉钉、企业微信、邮件等)
    # curl -X POST "https://oapi.dingtalk.com/robot/send?access_token=xxx" \
    #   -H 'Content-Type: application/json' \
    #   -d '{"msgtype":"text","text":{"content":"lac_agent自愈失败,需人工介入"}}'
    log_msg "[ALERT] 告警标记存在,已触发通知"
fi

然后把crontab改成调用这个脚本:

bash 复制代码
# 删掉原来的crontab条目
lac_agent stop

# 手动编辑crontab
crontab -e
# 写入:
*/5 * * * * /home/kingbase/lac/lac_watchdog.sh

注意,我用了 lac_agent stop 先停掉原来的守护,这样它会自动清除crontab里面的旧条目。然后手动写入新的crontab,指向我的watchdog脚本。

关于lac_agent stop的小细节

顺便说一下,lac_agent stoplac_agent stop -n 的区别。

  • lac_agent stop:停止lac_agent进程 并且 删除crontab中的定时任务
  • lac_agent stop -n:只停止进程,保留 crontab定时任务

这个 -n 参数很多人不知道,其实挺有用的。比如你想临时停掉lac_agent做个维护,但又不想影响crontab守护,等维护完了lac_agent会被crontab自动拉起来。

另外还有一个参数 lac_agent start -n:启动进程但 不写 crontab。这个参数在你打算用systemd或者其他方式守护的时候很有用,避免crontab和systemd两套守护同时存在打架。

一个完整的时间线复盘

把我那次凌晨故障的排查过程捋一遍,可能更有代入感:

03:17 --- Zabbix报警,3台机器license异常

03:19 --- SSH登录第一台机器,执行 ps aux | grep lac_agent,发现进程确实不在了。奇怪,crontab为什么没拉起来?

03:22 --- 查看crontab:crontab -l,定时任务还在。查看cron日志:grep lac_agent /var/log/cron | tail -20,发现最近一次执行是02:45,之后就没有了。

03:25 --- 检查crond服务:systemctl status crond,发现crond的状态是"active (running)"但最后活动时间停在了02:48。进一步查 dmesg | tail -50,发现02:48左右有一次内存告警,OOM killer杀掉了几个进程。

03:30 --- 原来OOM killer不仅杀了lac_agent,还顺带把crond给搞崩了(进入了某种异常状态)。crond虽然显示running,但实际上已经不调度任务了。

03:32 --- systemctl restart crond 重启crond,然后手动拉起lac_agent。

03:35 --- 在其他两台机器上重复同样操作。

03:50 --- 全部恢复,数据库从只读模式回到正常读写。

事后总结,核心问题就一个:crontab守护不是真正的自愈。它只解决了"进程被杀"这种简单场景,对于僵死、crond异常、PID残留等问题完全无能为力。

那怎么办?

短期方案就是我上面写的那个watchdog脚本,用crontab调用它替代直接调用 lac_agent start,至少能覆盖大部分常见的假死场景。

长期方案?那肯定是上systemd。官方其实已经提供了 lac_agentd.sh 脚本来注册systemd服务,但这里面也有不少坑要填。systemd方式需要root权限,重启策略要自己配,而且跟crontab方式在管理逻辑上有很多不一样的地方。

这部分内容比较多,放到下篇来详细说。

说到这里,前阵子我在金仓社区看到有个"同行者计划"的活动,就是鼓励大家把这些实际项目里踩的坑、沉淀的经验分享出来( bbs.kingbase.com.cn/forumDetail... )。我这种半夜三更排查授权故障的经历,感觉还挺适合拿去交流的------毕竟一个人踩过的坑,能帮一群人少走弯路。

回过头来想想,crontab守护这套机制就像是你请了个保安,跟他说"每5分钟巡逻一次看看大门有没有人"。但保安可能自己打瞌睡了(crond异常),可能看到有人躺在地上以为是在睡觉(进程僵死),也可能巡逻记录本被人撕了几页(日志轮转导致误判)。真正的自愈不是"定时检查",而是"实时感知+自动恢复+异常升级"。

下篇我们聊聊怎么用systemd把这套东西做得更靠谱。

相关推荐
笨鸟飞不快1 小时前
从 MVC 到 DDD:一次真实的渐进式迁移实录
后端·架构
程序员威哥1 小时前
C#也能玩转YOLO:工业视觉原生推理方案,零Python依赖
后端
kfaino1 小时前
你好,我叫 Prompt——其实,你一直在给 AI 写程序
后端·openai·ai编程
caibixyy2 小时前
springboot+langchain4j实战Day 16 — 混合检索 + Reranker 重排序
后端
Ai拆代码的曹操2 小时前
揭秘"幽灵 CPU":top 抓不到的短命进程,才是真正的 CPU 杀手
后端
IT_陈寒2 小时前
Python里这个赋值坑,连老司机都能翻车
前端·人工智能·后端
唐青枫2 小时前
推荐一个 Zig Web 工程骨架:wing-app
后端
葫芦和十三12 小时前
图解 MongoDB 13|WiredTiger 存储引擎:B-tree、页和 checkpoint 三件套
后端·mongodb·agent
葫芦和十三12 小时前
图解 MongoDB 14|Cache 与淘汰:WiredTiger 的内存治理
后端·mongodb·面试