从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的运维目标,改写为三句话:
- 挂了能自己爬起来(自愈)
- 爬不起来能叫醒我(告警)
- 别让小事变成大事(日志轮转和磁盘水位)
核心认知只有一句:无人值守不是「没人管」,是「机器先管,管不了才叫人」。 后面的所有设计,都围绕这句话展开。
二、整体架构:4层无人值守运维体系

架构图就是这篇文章的路线图:
┌──────────────────────────────────────────────┐
│ Layer 1: 健康检查 │
│ → 服务暴露 /healthz,探针每30秒探一次 │
├──────────────────────────────────────────────┤
│ Layer 2: 自动重启 │
│ → systemd Restart=on-failure,5秒后自动拉起 │
├──────────────────────────────────────────────┤
│ Layer 3: 异常告警 │
│ → 连续3次失败才推企业微信,恢复后通知已自愈 │
├──────────────────────────────────────────────┤
│ Layer 4: 日志轮转 │
│ → logrotate 按天+按大小轮转,压缩保留7份 │
└──────────────────────────────────────────────┘
每一层解决一类问题,下层是上层的兜底。下面逐层拆开。
三、Layer 1:健康检查——让系统自己报「我还活着」
先解决一个反直觉的坑:进程活着,不等于服务健康。
第一次写探活脚本时,只检查「进程在不在」:pgrep -f content_factory,进程在就认为没事。结果有一天进程在,服务却已经假死——数据库连接池占满,所有请求卡住超时。进程没死,业务全瘫。
所以健康检查要分三级:
- 进程级:进程在不在(最弱)
- 接口级:HTTP 接口通不通(中等)
- 业务级:业务状态正不正常(最强,也是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
自愈的完整决策流:

探活失败 → 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
告警链路全貌:

到这里,「系统自己照顾自己」已经成立:小病自愈,大病叫人,好了还告诉你一声。
六、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 restart | on-failure 5秒自动拉起,5分钟最多5次 |
| 告警 | 用户先发现,来质问你 | 连续3次失败推企业微信,恢复推「已自愈」 |
| 日志 | 忘了管,某天磁盘爆掉 | logrotate 按天+50M轮转,压缩保留7份 |
| 你的睡眠 | 碎片化,被手机震醒 | 一整夜,手机只在真出事时响 |
八、进阶思考:无人值守的核心不是自动化,是信任边界
很多人把无人值守理解成「全自动」,于是给每个环节都塞自动化,结果系统变得谁也看不懂,一挂全挂。方向反了。
无人值守的本质,是给「故障恢复」画一条信任边界:哪些事机器自己处理,哪些事必须叫人。 边界画得太宽,机器在错误的方向上反复重启,越搞越糟;边界画得太窄,机器啥都不敢动,你还是值班员。
这套体系里,边界画在三条线上:
- 可重试的动作交给机器:重启、重试、轮转,失败就再试,机器比人有耐心
- 需要判断的动作交给告警:连续失败、磁盘水位、数据库连不上,这些不是重试能解决的,必须叫人
- 恢复动作必须有回执:自愈了要通知,避免「半夜被叫醒,结果发现已经好了」——信任就是这么一点点攒起来的
再往深一层看,这套体系和前面几篇文章其实是同一个模式:检测 → 决策 → 动作 → 再检测。A/B测试是实验循环,无人值守是运维循环。Loop Engineering 的本质,就是把所有重复劳动都变成这种闭环。运维不再是负担,它是你产品的一部分——系统的可用性,本身就是用户对产品的体验。
九、总结:今天就能开始的两件事
回顾4层体系:
- 健康检查:/healthz 真连数据库,探针每30秒一次,curl 探活脚本
- 自动重启:systemd Restart=on-failure + RestartSec=5,5分钟最多5次
- 异常告警:monitor.py 连续3次失败推企业微信,恢复推「已自愈」
- 日志轮转:logrotate 按天+50M,压缩保留7份,磁盘85%水位告警
不要等下次事故。今天做两件事:给服务添加一个 /healthz 端点,再创建带 Restart=on-failure 的 systemd unit。做完这两件,你已经把「深夜重启」从人生里删掉了。
下一篇预告:把系统卖给更多人——从一人公司到可复制商业系统
你自己的OPC系统能写文章、能接待客户、能交付产品、能分析数据、能自己照顾自己了。但这一切都只服务你一个人。下一篇,我们聊聊如何把这套系统变成可复制的产品——让别人的生意也能跑在你的系统上,你从「一个人公司」升级成「系统公司」。
关于作者:魏无记,AI 和数智化实践者。专注 Agent 工程化与 Loop Engineering 研究以及数智化转型。公众号持续更新 Agent 工程化实战系列和数智化转型相关知识实践——每篇都是保姆级教程照做就行。

浙公网安备 33010602011771号