【虎鲸进化论 09】活下去:自启、守护、探活
本文是「虎鲸进化论」系列第 10 篇(正文第 09 篇)。上一篇:起源;下一篇预告:自举 ------ 它第一次改自己的代码(含升级失败自动回滚)。
前言
上一篇的结尾说:第一课不是做事,是不死。
这一篇讲的就是"不死工程"。听起来很基建、很无聊 ------ 但如果你也在养一台无人看管的东西,你会发现这是优先级最高的一课,没有之一:
给一台无人看管的系统排优先级:活着 > 干活 > 干好。 顺序错一个,前面全废。
道理很硬:它自己就是生产系统。它挂了,没人第一时间知道;它挂着半死不活(进程在、服务死),比挂透更麻烦 ------ 所有的自动化都建立在一个前提上:这条命是它的,也是我们的。
一、从"能跑"到"打不死":四件事
"不死工程"拆开是四件事,难度递增:
- 开机自启 ------ 机器重启后,它自己回来;
- 守护 ------ 进程意外退出后,它自己回来;
- 探活 ------ "它还活着吗",由它自己(而不是等人发现)回答;
- 升级安全 ------ 换代的时候,不能把自己换死。
环境一句话交代:一台内网 Linux 服务器,没有专职运维 ------ 它对面的"运维",就是这个四人组自己。也就是说,它半夜死了,第一响应人是第二天早上揉着眼睛的我们。(这个设定听着残酷,但它恰好逼出了后来那套"不死工程":没有人类兜底链路的系统,只能自己把命看住。)
下面逐个讲。每一节里都藏着一个坑 ------ 那些坑都不是吓唬人的理论,是真实付过学费的。
二、开机自启:从裸奔到 systemd
最初它是怎么跑的?说出来难为情:裸奔。 nohup java -jar ... & 一把,人走进程在,全凭自觉。
而"重启机器后它没回来"这件事 ------ 是的,发生过,不止一次:机器维护重启后的某个上午,有人习惯性去点它,点不着,才想起"哦,它是要人手动拉起来的"。那次之后它多了第一条运维规则:"不许它比人娇贵。"
这个进化后来的路径是这样的:
阶段一:裸奔。 手动 nohup java -jar ... & 一把。跑得好好的 ------ 直到某次机器维护重启,没人记得它。
阶段二:写进启动脚本。 把它塞进某个开机脚本里。比裸奔强,但脚本自己挂不挂、顺序对不对、日志去哪了,全是玄学。
阶段三:systemd。 最终落在这里,理由就三点:它是机器的原生方式 (不引新组件)、语义直白 (进程挂了谁拉起、日志去哪、重启策略,都是配置项)、可靠到无聊(无聊是褒义词)。
一个示意性质的 unit 配置长这样(非真实配置,仅示结构):
ini
[Unit]
Description=<agent-service> # 化名
After=network-online.target
[Service]
ExecStart=/opt/<agent>/restart.sh start # 启动动作收敛到一个脚本
Restart=always # 挂了就拉起来
RestartSec=<NN> # 防抖:拉太快的恶性循环没意义
StandardOutput=journal
[Install]
WantedBy=multi-user.target
注意其中两个设计决定,后面还会反复遇到:
- 启动动作收敛成一个脚本 ------ systemd 只负责"什么时候拉",脚本负责"怎么拉"。控制权分层,谁都别越界;
- 重启有防抖窗口 ------ 一个疯狂崩溃的进程如果被立刻无限拉起,只会把机器拖垮。
RestartSec是那根安全带。
三、守护:谁来看护看护者
守护的问题问起来简单,想清楚不容易:进程挂了,谁来拉它?拉它的那个东西挂了,谁来拉?
无限套娃没有意义,所以工程上的答案从来不是"再写一个守护",而是尽量别自己造守护:
- systemd 的
Restart=always+RestartSec解决了 90% 的"意外退出"场景; - 真正需要自己写守护的场景(多实例、复杂健康判定),也比想象中少;
- 守护逻辑不自己造,还回避了一类最恶心的问题:守护进程和被守护进程互相"以为对方活着"。
直说吧:我们没栽在"守护把自己守护死"这种高级坑上 ------ 差一点栽在更土的地方:早期的开机脚本里,两行命令的先后顺序写反了,导致有一次它是"起了,但没完全起"。那次之后才彻底想通:排序这种细节不值得人肉伺候 ------ 交给机器的原生调度器,人只维护语义。
守护的最高境界,是让"守护"这个词消失 ------ 你在配置文件里读不到它,但每一秒它都在。
四、探活:最深的坑叫"自以为活着"
四件事里,探活的水最深。先记住一句话:
"进程活着"和"服务活着",是两件不同的事。
进程在,但数据库连接池耗尽 ------ 它对新请求全部超时;
进程在,但内部某个线程死了 ------ 它半个功能是哑的;
进程在,但磁盘满了 ------ 它每次写日志都失败,只是还没死。
如果探活只探"进程在不在",以上所有情况它都会上报:"我很好"。这就是"自以为活着"------ 比死了更危险,因为它会让所有信任它的人一起错。
所以探活要探三层,一层比一层深,每一层都问一个更狠的问题:
| 层级 | 探什么 | 回答什么问题 | 成本 |
|---|---|---|---|
| 一层 | 进程存在 | 它还在吗 | 极低 |
| 二层 | 端口/进程内自检 | 它还愿意接活吗 | 低 |
| 三层 | 业务级探针 | 它的关键能力还可用吗(比如:读一次、写一次、回滚一次) | 中 |
三层不是都要做满 ------ 而是由你对"它死了没人知道"的恐惧程度决定探到哪层。比如对这台 Agent:一层必须有(systemd 天然带)、二层必须有(HTTP 健康接口)、三层按能力点设(数据库、部署通道各自有独立探针)。
我们的实际配置:一层交给 systemd(进程在不在);二层每 NN 秒打一次健康接口(超时 NN 秒算失败,连续多次失败判死);三层最克制 ------ 读探针分钟级、写探针小时级,而且两条探针走独立的连接和开关,防止"探针自己把连接池占满"。频率这件事交过学费:最早二层是秒级频率,在机配一般的机器上,探针自己就吃掉一截 CPU ------ 探活把服务探"虚"了。改成 NN 秒之后才消停。
还有两个反直觉的坑,一并记下:
- 探太浅,是假绿;探太深也会坏事。 三层探针如果每秒都全量跑(读一次写一次),它自己就会把资源占住 ------ 探活把服务探病了。频率必须和业务量级匹配;
- 探活的结论要能被人消费。 探活的结果如果只印在日志里,等于没探。它必须变成一个"人能看到的状态"(哪怕就是一条每日巡检消息里的一个词)。
五、重启脚本的进化:探活 + 自动回滚
轮到四件事里最"高级"的一件:升级安全。
最初的升级动作很朴素:拉新版本、杀旧进程、拉新进程。这个流程有个致命缺陷 ------ 如果新版本起不来呢? 半夜升级失败 = 生产系统躺着等天亮。
(等天亮的那个"人",事后还要写故障报告。所以"自动回滚"保护的与其说是系统,不如说是打工人的睡眠 ------ 这是本项目里少数几件可以量化的人文关怀。)
于是重启脚本开始了它的进化,最终形态围绕一个简单原则:升级失败自动回滚。 示意流程:
#mermaid-svg-ulP94U6bC4oafGOQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ulP94U6bC4oafGOQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ulP94U6bC4oafGOQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ulP94U6bC4oafGOQ .error-icon{fill:#552222;}#mermaid-svg-ulP94U6bC4oafGOQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ulP94U6bC4oafGOQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ulP94U6bC4oafGOQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ulP94U6bC4oafGOQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ulP94U6bC4oafGOQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ulP94U6bC4oafGOQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ulP94U6bC4oafGOQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ulP94U6bC4oafGOQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ulP94U6bC4oafGOQ .marker.cross{stroke:#333333;}#mermaid-svg-ulP94U6bC4oafGOQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ulP94U6bC4oafGOQ p{margin:0;}#mermaid-svg-ulP94U6bC4oafGOQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ulP94U6bC4oafGOQ .cluster-label text{fill:#333;}#mermaid-svg-ulP94U6bC4oafGOQ .cluster-label span{color:#333;}#mermaid-svg-ulP94U6bC4oafGOQ .cluster-label span p{background-color:transparent;}#mermaid-svg-ulP94U6bC4oafGOQ .label text,#mermaid-svg-ulP94U6bC4oafGOQ span{fill:#333;color:#333;}#mermaid-svg-ulP94U6bC4oafGOQ .node rect,#mermaid-svg-ulP94U6bC4oafGOQ .node circle,#mermaid-svg-ulP94U6bC4oafGOQ .node ellipse,#mermaid-svg-ulP94U6bC4oafGOQ .node polygon,#mermaid-svg-ulP94U6bC4oafGOQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ulP94U6bC4oafGOQ .rough-node .label text,#mermaid-svg-ulP94U6bC4oafGOQ .node .label text,#mermaid-svg-ulP94U6bC4oafGOQ .image-shape .label,#mermaid-svg-ulP94U6bC4oafGOQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-ulP94U6bC4oafGOQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ulP94U6bC4oafGOQ .rough-node .label,#mermaid-svg-ulP94U6bC4oafGOQ .node .label,#mermaid-svg-ulP94U6bC4oafGOQ .image-shape .label,#mermaid-svg-ulP94U6bC4oafGOQ .icon-shape .label{text-align:center;}#mermaid-svg-ulP94U6bC4oafGOQ .node.clickable{cursor:pointer;}#mermaid-svg-ulP94U6bC4oafGOQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ulP94U6bC4oafGOQ .arrowheadPath{fill:#333333;}#mermaid-svg-ulP94U6bC4oafGOQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ulP94U6bC4oafGOQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ulP94U6bC4oafGOQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ulP94U6bC4oafGOQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ulP94U6bC4oafGOQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ulP94U6bC4oafGOQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ulP94U6bC4oafGOQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ulP94U6bC4oafGOQ .cluster text{fill:#333;}#mermaid-svg-ulP94U6bC4oafGOQ .cluster span{color:#333;}#mermaid-svg-ulP94U6bC4oafGOQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ulP94U6bC4oafGOQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ulP94U6bC4oafGOQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-ulP94U6bC4oafGOQ .icon-shape,#mermaid-svg-ulP94U6bC4oafGOQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ulP94U6bC4oafGOQ .icon-shape p,#mermaid-svg-ulP94U6bC4oafGOQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ulP94U6bC4oafGOQ .icon-shape .label rect,#mermaid-svg-ulP94U6bC4oafGOQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ulP94U6bC4oafGOQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ulP94U6bC4oafGOQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ulP94U6bC4oafGOQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
通过
未通过
新版本拉起(旧版本不杀)
探活通过?
切换流量/指认新版本
旧版本下线
判失败:回滚到上一版本
回滚后探活复验
记录:本次升级失败,已回滚
升级为人工介入事件
立即告警
这套设计里有三个关键决定,每一个都值得写进任何无人值守系统的教科书:
- 版本化。 每次发布都有一个明确的"版本落点",任何时刻都能回答"现在跑的是哪个版本、上一个版本在哪"。没有版本化,回滚就是一句空话;
- 原子切换。 升级不是"在原地改",而是"准备好新的,一次性指过去"。中途状态不存在,回滚才有意义;
- 回滚也要验证。 回滚不是"跑回去就完事"------ 回滚后的探活复验,才能证明"撤回来了"而不是"又换了一种死法"。
做完这三条,升级这件事的性质变了:从一次赌博,变成一次实验。 赌输了要人命;实验失败了,回滚,记录,明天再来。
(这条"把危险动作变成可退出的实验"的思路,后来在卷Ⅲ里被放大到了它的一切写操作上 ------ 第 18/19 篇(在线修复两篇)会讲它在生产上的应用,第 30 篇讲它的边界。)
六、这一课的普适性
"不死工程"这四件事讲完,回头看它们其实跟"AI"没关系 ------ 它们是一台无人看管的生产系统的通用第一课。
区别在于:普通的服务,挂了有人肉告警链路兜着;而一台要自进化的东西,它得先证明自己能独立活过一晚,我们才敢给它加第二只手。
后来它越长越大,能力越来越吓人,但我始终觉得这一天是它整个进化史里最重要的一天 ------ 因为它第一次证明了:"翻车不死"是刻在它底层里的,不是靠运气。
(你还记得第 29 篇的标题吗?《事故复盘:它把自己搞坏过》------ 到那一天,这套"不死"工程会接受一次真正的考试。)
结语(回味点)
给无人看管的系统排优先级:活着 > 干活 > 干好。 这不是保守,这是常识被工程化的样子。
能自启、能探活、能回滚 ------ 但它改的还只是脚本。
它想动自己的代码了。
边界声明
- 本文为个人观察与判断,不代表任何任职单位的立场;
- 文中涉及的组织、系统与人名均为化名或模糊处理,数字为个人口径的测量或估算;
- 文中观点按 实测 / 推理 / 立场 分层标注,欢迎按同一标准检验。
下一篇计划聊:自举 ------ 它第一次改自己的代码:信任边界第一次被画出来,以及"把刀递给它"之后发生了什么。

转载请注明出处: