从996到007——OPC的无人值守运维体系

开篇:凌晨3:07,手机亮了

凌晨3:07,企业微信的告警声把你震醒。不是客户咨询,不是新订单——是内容工厂的流水线停了。数据库连不上,文章没发出去,A/B实验的收敛检查也没跑。你爬起来,开电脑,看日志,重启服务,确认恢复,躺回去。刚闭上眼,第二条告警又来了。

这是OPC最反讽的时刻:你把所有工作都自动化了,唯独「运维」这件事,自动化不了自己。 系统替你干了100件事,可系统自己需要人照顾——而那个人只能是你。

上一篇文章,我们搭好了3层自动化A/B测试系统([一个人也能做A/B测试——AI驱动的自动化优化系统](../articles/article-20.md)),系统能自己试错、自己收敛、自己切换。但这一切都建立在一个脆弱假设上:服务器必须活着。 保证服务器活着这件事,我们还在人肉值班——996都算轻的,那是007。

今天的解药是一套4层无人值守运维体系:健康检查、自动重启、异常告警、日志轮转。搭完之后,系统自己照顾自己:小病自己爬起来,大病才叫醒你,而且不会半夜把你吵醒,只为告诉你一句「我好了」。


一、先想明白:OPC要的运维,和运维工程师不一样

大公司运维的目标:99.99%可用性、多机房冗余、值班表、工单系统、变更审批。OPC没有这些资源,也不需要这些目标。OPC的现实是:一台服务器、一个部署目录、没有值班表、没有专职运维。

所以OPC的运维目标,改写为三句话:

  1. 挂了能自己爬起来(自愈)
  2. 爬不起来能叫醒我(告警)
  3. 别让小事变成大事(日志轮转和磁盘水位)

核心认知只有一句:无人值守不是「没人管」,是「机器先管,管不了才叫人」。 后面的所有设计,都围绕这句话展开。


二、整体架构:4层无人值守运维体系

![article21-ops-architecture: 4层无人值守运维体系架构总览图,从上到下依次为健康检查、自动重启、异常告警、日志轮转四个色块层,每层标注核心组件与参数,右侧箭头标注故障数据从检测到处理的流动方向](../diagrams/article21-ops-architecture.png)

架构图就是这篇文章的路线图:

┌──────────────────────────────────────────────┐
│ Layer 1: 健康检查                              │
│ → 服务暴露 /healthz,探针每30秒探一次           │
├──────────────────────────────────────────────┤
│ Layer 2: 自动重启                              │
│ → systemd Restart=on-failure,5秒后自动拉起     │
├──────────────────────────────────────────────┤
│ Layer 3: 异常告警                              │
│ → 连续3次失败才推企业微信,恢复后通知已自愈       │
├──────────────────────────────────────────────┤
│ Layer 4: 日志轮转                              │
│ → logrotate 按天+按大小轮转,压缩保留7份         │
└──────────────────────────────────────────────┘

每一层解决一类问题,下层是上层的兜底。下面逐层拆开。


三、Layer 1:健康检查——让系统自己报「我还活着」

先解决一个反直觉的坑:进程活着,不等于服务健康。

第一次写探活脚本时,只检查「进程在不在」:pgrep -f content_factory,进程在就认为没事。结果有一天进程在,服务却已经假死——数据库连接池占满,所有请求卡住超时。进程没死,业务全瘫。

所以健康检查要分三级:

  1. 进程级:进程在不在(最弱)
  2. 接口级:HTTP 接口通不通(中等)
  3. 业务级:业务状态正不正常(最强,也是OPC真正要的)

第一步,在服务里添加一个健康检查端点(FastAPI 示例):

# app/main.py - 健康检查端点
import fastapi
import psycopg2
import time

DB_DSN = "postgresql://user:***@127.0.0.1:5432/analytics"
app = fastapi.FastAPI()

@app.get("/healthz")
def healthz():
    status = "ok"
    try:
        conn = psycopg2.connect(DB_DSN, connect_timeout=3)
        conn.close()
    except Exception:
        status = "degraded"
    return {"status": status, "service": "content-factory", "ts": int(time.time())}

这个端点做了一件「进程活着」之外的事——真的去连一次数据库。接口通不代表依赖通,探活必须探到最脆弱的那个依赖。数据库是OPC系统里最容易挂的依赖,所以 /healthz 必须真连一次库,连接超时设3秒,探活脚本最多等5秒。

第二步,创建 healthcheck.sh 探活脚本(cron 每30秒跑一次):

#!/bin/bash
# healthcheck.sh - 探活脚本
HEALTH_URL="http://127.0.0.1:8000/healthz"

code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$HEALTH_URL")
if [ "$code" != "200" ]; then
  echo "health check failed: http=$code at $(date)" >> /var/log/healthcheck.log
  exit 1
fi
body=$(curl -s --max-time 5 "$HEALTH_URL")
if echo "$body" | grep -q '"status": "degraded"'; then
  echo "health check degraded: db unreachable at $(date)" >> /var/log/healthcheck.log
  exit 1
fi
exit 0

curl 的退出码和 HTTP 状态码都要检查,degraded 状态也要记日志。这一层不负责处理,只负责「发现」。发现之后,交给下一层。


四、Layer 2:自动重启——小病自己爬起来

健康检查发现问题后,谁来处理?如果是半夜,答案是:不要人处理,让 systemd 处理。

systemd 的 Restart 策略就是为这个设计的。第一步,在 /etc/systemd/system/ 下创建 content-factory.service 文件,写入以下内容:

# /etc/systemd/system/content-factory.service
[Unit]
Description=OPC Content Factory
After=network.target postgresql.service

[Service]
User=opc
WorkingDirectory=/home/opc/content-factory
ExecStart=/home/opc/.venv/bin/python3 -m uvicorn app.main:app --host 127.0.0.1 --port 8000
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=300
StartLimitBurst=5

[Install]
WantedBy=multi-user.target

两个参数最关键:

  • Restart=on-failure:进程异常退出就自动重启。不要用 Restart=always——如果进程因为配置错误反复崩溃,always 会无限重启刷爆日志;而 on-failure 配合 StartLimitBurst=5,5分钟内最多重启5次,超出就停止尝试并进入 failed 状态——这时才轮到告警出场。
  • RestartSec=5:重启前等5秒,给数据库一点恢复时间。

运行以下命令,启用服务并查看状态:

systemctl daemon-reload
systemctl enable content-factory
systemctl start content-factory
systemctl status content-factory --no-pager

自愈的完整决策流:

![article21-self-healing-flow: 自愈闭环决策流程图,探活失败后进入自动重启分支,重启后再探活,成功则记录已自愈,连续失败则累计次数并触发告警](../diagrams/article21-self-healing-flow.png)

探活失败 → systemd 自动重启 → 再探活 → 成功则记录「已自愈」;连续失败则计数,超过阈值进告警层。自动重启是「动作」,健康检查是「眼睛」,两者必须成对出现——缺了探活的重启,是盲人开车。


五、Layer 3:异常告警——爬不起来就叫人

systemd 自己重启几次还是起不来,说明不是偶发崩溃,是需要人介入的问题。这时候才轮到告警——而且告警本身要克制。

告警最怕误报。 一次误报的代价不是一条消息,是你从此不再信任告警,把通知静音,然后真正的大事被淹没。所以告警必须做两件事:连续确认 + 恢复通知。

创建 monitor.py 监控脚本,独立于 systemd 跑(cron 每分钟):

# monitor.py - 告警脚本,连续3次失败才推送
import urllib.request

HEALTH_URL = "http://127.0.0.1:8000/healthz"
WEBHOOK_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY"
STATE_FILE = "/var/run/opc-monitor.state"
MAX_FAILS = 3

def check():
    try:
        req = urllib.request.urlopen(HEALTH_URL, timeout=5)
        return req.status == 200 and b'"status": "ok"' in req.read()
    except Exception:
        return False

def send(text):
    payload = '{"msgtype": "text", "text": {"content": "%s"}}' % text
    data = payload.encode("utf-8")
    req = urllib.request.Request(WEBHOOK_URL, data=data, headers={"Content-Type": "application/json"})
    urllib.request.urlopen(req, timeout=10)

def main():
    try:
        with open(STATE_FILE) as f:
            fails = int(f.read().strip())
    except Exception:
        fails = 0

    if check():
        if fails >= MAX_FAILS:
            send("服务已恢复,自愈完成: content-factory")
        fails = 0
    else:
        fails += 1
        if fails == MAX_FAILS:
            send("告警: content-factory 连续3次探活失败,systemd重启无效,需要人工介入")

    with open(STATE_FILE, "w") as f:
        f.write(str(fails))

if __name__ == "__main__":
    main()

这里用标准库 urllib,不用装 requests。状态存在 /var/run 下的文件里,重启机器会清零,重新计数。行为拆解:

  • 失败1次、2次:只计数,不推送——抖动不打扰
  • 失败满3次:推送企业微信告警,且只推一次
  • 恢复:如果之前处于告警状态,推送「已自愈」,把计数清零

企业微信机器人的 webhook 就一行配置,手机装企业微信就能收到。cron 配置:

# crontab -e 添加以下行
* * * * * cd /home/opc && /usr/bin/python3 monitor.py >> /var/log/opc-monitor.log 2>&1

告警链路全貌:

![article21-alert-channel: 告警链路图,从服务异常出发经监控脚本连续确认,命中阈值后通过企业微信机器人Webhook推送到手机,同时日志落盘后由logrotate轮转归档](../diagrams/article21-alert-channel.png)

到这里,「系统自己照顾自己」已经成立:小病自愈,大病叫人,好了还告诉你一声。


六、Layer 4:日志轮转——别让日志撑爆磁盘

最后一层最不起眼,但它是OPC服务器上最常见的事故根因:日志把磁盘写满,服务全挂。

服务一跑起来,日志就以肉眼可见的速度增长。nginx 访问日志、应用日志、探活日志、监控日志,几个文件互相叠加。磁盘满了之后,数据库写不进去、服务起不来、连 SSH 都可能卡住。而且它往往发生在最忙的那天。

解法是 logrotate——Linux 自带的日志轮转工具:

# /etc/logrotate.d/opc
/var/log/content-factory/*.log {
    daily
    rotate 7
    compress
    delaycompress
    missingok
    notifempty
    size 50M
    copytruncate
}

配置含义:按天轮转,或单文件超过50M立即轮转;保留7份;旧日志压缩成 .gz;copytruncate 对正在写入的进程友好——先复制再清空,应用不用重启。

光轮转还不够,磁盘水位本身也要监控。再创建 disk_watch.sh 磁盘检查脚本:

# disk_watch.sh - 磁盘水位检查,cron 每小时跑一次
usage=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
if [ "$usage" -gt 85 ]; then
  curl -s -H "Content-Type: application/json" \
    -d '{"msgtype":"text","text":{"content":"告警: 磁盘使用率已超过85%,请清理日志或扩容"}}' \
    "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY"
fi

磁盘使用率超过85%就推一条告警。日志轮转负责「治」,磁盘水位负责「防」,两个合起来,才真正堵住这个最常见的坑。


七、Before vs After:你省下了什么

环节人肉运维无人值守体系
探活凭感觉「应该没问题」/healthz 每30秒真连一次数据库
重启半夜爬起来 systemctl restarton-failure 5秒自动拉起,5分钟最多5次
告警用户先发现,来质问你连续3次失败推企业微信,恢复推「已自愈」
日志忘了管,某天磁盘爆掉logrotate 按天+50M轮转,压缩保留7份
你的睡眠碎片化,被手机震醒一整夜,手机只在真出事时响

八、进阶思考:无人值守的核心不是自动化,是信任边界

很多人把无人值守理解成「全自动」,于是给每个环节都塞自动化,结果系统变得谁也看不懂,一挂全挂。方向反了。

无人值守的本质,是给「故障恢复」画一条信任边界:哪些事机器自己处理,哪些事必须叫人。 边界画得太宽,机器在错误的方向上反复重启,越搞越糟;边界画得太窄,机器啥都不敢动,你还是值班员。

这套体系里,边界画在三条线上:

  1. 可重试的动作交给机器:重启、重试、轮转,失败就再试,机器比人有耐心
  2. 需要判断的动作交给告警:连续失败、磁盘水位、数据库连不上,这些不是重试能解决的,必须叫人
  3. 恢复动作必须有回执:自愈了要通知,避免「半夜被叫醒,结果发现已经好了」——信任就是这么一点点攒起来的

再往深一层看,这套体系和前面几篇文章其实是同一个模式:检测 → 决策 → 动作 → 再检测。A/B测试是实验循环,无人值守是运维循环。Loop Engineering 的本质,就是把所有重复劳动都变成这种闭环。运维不再是负担,它是你产品的一部分——系统的可用性,本身就是用户对产品的体验。


九、总结:今天就能开始的两件事

回顾4层体系:

  1. 健康检查:/healthz 真连数据库,探针每30秒一次,curl 探活脚本
  2. 自动重启:systemd Restart=on-failure + RestartSec=5,5分钟最多5次
  3. 异常告警:monitor.py 连续3次失败推企业微信,恢复推「已自愈」
  4. 日志轮转:logrotate 按天+50M,压缩保留7份,磁盘85%水位告警

不要等下次事故。今天做两件事:给服务添加一个 /healthz 端点,再创建带 Restart=on-failure 的 systemd unit。做完这两件,你已经把「深夜重启」从人生里删掉了。

下一篇预告:把系统卖给更多人——从一人公司到可复制商业系统
你自己的OPC系统能写文章、能接待客户、能交付产品、能分析数据、能自己照顾自己了。但这一切都只服务你一个人。下一篇,我们聊聊如何把这套系统变成可复制的产品——让别人的生意也能跑在你的系统上,你从「一个人公司」升级成「系统公司」。

关于作者:魏无记,AI 和数智化实践者。专注 Agent 工程化与 Loop Engineering 研究以及数智化转型。公众号持续更新 Agent 工程化实战系列和数智化转型相关知识实践——每篇都是保姆级教程照做就行。
posted @ 2026-08-05 21:16  魏无记  阅读(1)  评论(0)    收藏  举报