从“能跑”到“长期稳定”:一次监控系统的工程演进记录


最近做了一件看似很小、但让我收获很大的事:
写一个程序,长期监控一个不定时放票的系统,一旦有票就提醒。

一开始我以为这是个“写点脚本就完事”的问题,结果它演变成了一次关于工程判断、稳定性以及抽象边界的完整实践


起点:先把事情跑起来

最初的目标很简单:

  • 定期检查某个系统是否出现“可预订”
  • 有变化就通知
  • 跑在一台 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

  1. 能直接用 API,就不要用浏览器自动化
  2. 稳定性问题,往往不是靠修补解决的,而是靠换架构
  3. “能跑”和“能长期跑”是两件完全不同的事
  4. 监控系统的目标不是“多快”,而是“可靠 + 不打扰”
  5. 当你开始关心“下次还能不能复用”,说明系统已经值得被抽象

好的工程实践,往往来自于对现实约束的尊重,而不是对工具能力的迷信。