从“能跑”到“长期稳定”:一次监控系统的工程演进记录
最近做了一件看似很小、但让我收获很大的事:
写一个程序,长期监控一个不定时放票的系统,一旦有票就提醒。
一开始我以为这是个“写点脚本就完事”的问题,结果它演变成了一次关于工程判断、稳定性以及抽象边界的完整实践。
起点:先把事情跑起来
最初的目标很简单:
- 定期检查某个系统是否出现“可预订”
- 有变化就通知
- 跑在一台 1GB 内存的小云服务器上
- 可以无人值守长期运行
第一反应也很自然:
用浏览器自动化。
浏览器能看到什么,我就能抓到什么:
- 打开页面
- 翻月份
- 看日历上是 available 还是 unavailable
这套方案很快跑通了,而且逻辑直观、调试方便。
问题出现:它“能跑”,但不“能长期跑”
真正的问题不是当天能不能用,而是几天之后:
- 浏览器进程偶尔崩
- 页面对象进入半失效状态
- 进程还在,但功能已经不正常
- 偶尔漏掉通知,或者漏掉每日状态
在 1GB 内存 的机器上,这种问题尤其明显。
这时我意识到一件事:
这不是“再加几个 try/except”能解决的问题。
它暴露的是一个更底层的矛盾:
- 浏览器自动化很强
- 但对资源、稳定性的要求也很高
- 对“长期无人值守”的任务并不友好
关键转折:停止修补,重新观察问题
真正的转折点不是改代码,而是换了观察角度。
我开始问自己一个问题:
页面展示的数据,是不是一定要靠浏览器才能拿到?
于是去看了 Network 请求。
结果发现:
- 页面日历数据来自一个后端接口
- 返回的是结构化数据
- 浏览器只是“展示层”
这意味着一件事:
我其实并不需要浏览器。
重构:从浏览器到纯 API
方案随之发生变化:
- 不再启动浏览器
- 直接请求后端接口
- 解析返回的数据
- 判断状态变化
架构从:浏览器 → DOM → 状态 变成了:HTTP API → 数据 → 状态
这个改变非常“安静”,但影响巨大:
- 内存占用大幅下降
- 稳定性明显提升
- 不再出现“页面还在但已经坏了”的灰色状态
进一步思考:监控 ≠ 刷屏
系统稳定之后,我开始关注另一个问题:
通知体验本身是不是合理?
最终形成了几个原则:
- 只在有意义的变化时通知
(例如:不可预订 → 可预订) - 用每日摘要确认系统状态,而不是频繁打扰
- 加一个心跳消息,解决“它是不是还在跑”的不确定感
这样,系统才能真正做到:
不用盯、不用管,但该来的时候一定会提醒。
为什么选择 Telegram 作为通知通道
在监控系统中,“怎么通知”和“监控本身一样重要”。
在 Email / Slack / Webhook 等方案中,最终选择了 Telegram Bot,原因很现实:
即时性与到达率
- 手机、桌面同时推送
- 延迟通常在秒级
- 非常适合“短窗口”的放票场景
接入成本极低
- 不需要额外服务
- 不需要复杂鉴权
- 只需要一个 bot token 和 chat_id
- 一个 HTTP POST 即可完成通知
适合“人直接阅读”的信息结构
- 支持多行文本、emoji
- 可以在一条消息中分组展示重点日期
- 非常适合直接作为“行动提醒”
Telegram 通知策略设计(避免刷屏)
选择 Telegram 之后,通知策略比技术更重要。
只在有行动价值时通知
系统遵循一个非常明确的规则:
只在
不可预订 / 未知 → 可预订时发送通知
避免同一天反复提醒,确保每一条消息都值得点开。
用“优先级分组”而不是频控
没有引入复杂的 rate limit,而是通过信息结构解决注意力问题:
- P0:节假日、必须抢的日期
- P1:周末
- P2:工作日(低优先级)
一眼就能判断是否需要立刻行动。
摘要 + 心跳,降低不确定感
除了事件通知,还设计了两类“非打扰型消息”:
- 每日摘要(08:00)
即使当天没有放票,也能确认系统状态 - 每日心跳(12:00)
告诉你脚本还在跑、最近一次成功扫描时间
解决了一个现实问题:
“没收到消息,是没放票,还是系统挂了?”
最终状态
这套系统现在的状态是:
- 跑在 1GB 小机器上
- 不依赖浏览器
- 长期稳定
- 有变化会提醒
- 没变化也能确认系统还活着
对我来说,真正的目标已经达成了:
我可以完全不再关心它。
延伸思考:为什么这不只是“一次性脚本”
系统稳定运行后,我开始回看整个过程,问了自己一个问题:
如果下次我要刷的不是停车位,而是营地、门票、名额,这套东西还能不能用?
答案逐渐清晰:
核心逻辑是高度可复用的,变化的只是“数据来源”。
抽象的关键:什么该变,什么不该变
不该反复修改的部分
- 轮询调度(频率、抖动)
- 异常处理与退避
- 状态对比与去重
- 通知策略(避免刷屏)
- 每日摘要与心跳
- 长期无人值守的运行保障
这些内容与刷什么目标无关。
一定会变化的部分
真正因目标不同而变化的,其实只有一件事:
如何从外部系统拿到“可用 / 不可用”的状态
也就是:
- 调哪个接口
- 带哪些参数
- 怎么解析返回结果
这部分逻辑天然应该被隔离。
自然的演进方向:Core + Adapter
因此,一个非常自然的抽象方式浮现出来:
- Core(核心引擎)
- 负责轮询、退避、状态管理、通知
- Adapter(适配器)
- 负责把不同系统的数据,翻译成统一的状态
这种拆分让系统可以:
- 核心稳定不动
- 新目标只新增 adapter
- 避免牵一发动全身
为什么没有一开始就做成“通用服务”
一个刻意的选择是:
没有在一开始就设计成通用框架或服务。
原因很简单:
- 在真实使用前,很难判断哪些抽象是正确的
- 过早抽象,往往会抽象错
- 只有跑过、踩过坑之后,抽象才有意义
抽象不是目标,而是自然结果。
Key Learnings
- 能直接用 API,就不要用浏览器自动化
- 稳定性问题,往往不是靠修补解决的,而是靠换架构
- “能跑”和“能长期跑”是两件完全不同的事
- 监控系统的目标不是“多快”,而是“可靠 + 不打扰”
- 当你开始关心“下次还能不能复用”,说明系统已经值得被抽象
好的工程实践,往往来自于对现实约束的尊重,而不是对工具能力的迷信。