尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

CloddsBot:多云状态监控与告警推送的轻量级实践

CloddsBot:多云状态监控与告警推送的轻量级实践 我大概一年多前在朋友圈发了条状态谁能推荐一个能把我所有云服务状态聚合到一起的工具最好还能自动出告警。底下一堆朋友给我发各种监控平台的链接都不是我想要的。后来我干脆自己动手写了一个也就是今天要聊的CloddsBot——一个把多云状态监控、告警推送、日常巡检打包成一套的小机器人项目。这东西解决的是个特别具体的痛点手上同时管着几套云环境有自建的、有厂商托管的账号门户一个又一个控制台切来切去。真要出故障我往往是最后一个知道的——不是云厂商不通知而是通知渠道太散邮件、短信、站内信铺得到处都是反而被淹没了。CloddsBot的核心思路就是把这些状态源统一收集起来再按严重级别推送到统一的消息出口。如果你是运维、SRE或者只是一个在自己服务器上折腾个人项目、但又不想天天盯着控制台看的开发者这篇文章可能对你有用。我会把整个项目从设计思路到踩坑过程都拆开讲清楚包括为什么我选了这个架构、某几个参数是怎么拍板的、哪些隐藏问题文档里根本不会写。项目本身开源代码在GitHub上但这篇文章不讲太多纯代码的细枝末节重点放在“为什么这么做”和“实操中哪些地方需要格外注意”。1. 项目初衷与整体设计思路1.1 从“我要一个监控机器人”到“我要一套调度逻辑”很多人第一反应是监控和告警的工具不是一大堆吗Prometheus、Grafana、Zabbix随便搭一个不就行了这话没毛病但这些工具解决的是“基础设施指标监控”——CPU、内存、QPS、错误率这些。CloddsBot解决的维度不一样它盯的是“云服务状态本身”。打个比方前者是关心一台服务器是不是快被流量打爆了后者是关心机房门口是不是贴了张“临时停水通知”。云厂商的Health Dashboard、状态页、公告栏都属于后者。这类状态没有标准协议没有统一接口每个厂商给的格式都不一样有的提供JSON API有的只给一个静态页还有的需要登录才能看。所以我早期面对的问题不是“怎么写一个监控”而是“怎么把这些乱七八糟的来源抽象成统一的数据结构再定义一套通用的处理流程”。在这个阶段我做的最重要的一件事就是“设计上的预先悲观”先想清楚哪些地方会出问题再决定架构怎么搭。来源不稳定、推送渠道可能失效、频控、断连这些问题我选择在第一天就处理而不是等上线之后再打补丁。1.2 工具选型为什么是Python 消息队列技术上我没有搞太复杂的东西整个CloddsBot基于以下三个核心组件Python 3.10抓取、解析、分发的主语言。Redis状态存储、临时缓存、某个轻量级消息队列。企业微信/钉钉/Telegram的自定义机器人统一告警出口。为什么还是Python市面上爬取和解析的任务用Python确实方便requests、BeautifulSoup、pydantic这几个库就能扛住绝大多数场景。而且这类任务本质上是IO密集型的——等网络响应的过程中CPU基本闲着Python的协程模型在这个场景下完全够用没有硬上Go或Rust的必要。Redis比较有意思。除了做缓存我把它当了一个轻量级的延迟队列用。每个抓取任务跑完之后把结果推到队列里由另一个worker统一处理“状态变化判断”和“消息生成”。这样做最大的好处是职责隔离抓取器只管抓分发器只管发中间通过Redis解耦。如果某个云厂商的API突然变慢抓取任务堆积分发器并不会受影响。当然如果你不想引Redis进来用Python的celery或arq也可以但这两种方案都要求额外跑一个worker部署上重一些。我倾向轻量所以走了“Redis做队列 asyncio定时任务”的路线。1.3 影响范围的边界到底监控什么、不监控什么这也是CloddsBot设计里一个容易被忽略但很关键的问题项目的边界在哪。我明确不做以下几件事不采集业务指标留给Prometheus这类专用工具。不做根因分析只负责告诉你“挂没挂、什么时候好的”不负责解释“为什么挂”。不做太多自定义告警规则引擎复杂规则交给下游我只提供基础的按严重级别分组。边界划清楚之后整个项目就简单了。CloddsBot只关心一件事某个受监控的云服务当前处于什么状态以及这个状态跟上一次比有没有变化。2. 核心细节拆解与实操要点2.1 状态模型的设计影响四级定义要清晰很多人在写这类项目时容易陷入的一个坑是状态分类做得太细结果自己都搞不清楚什么算“故障”什么算“维护”。CloddsBot的状态模型一共四级设计得很克制级别状态含义典型场景OK一切正常无公告、无事件、无异常MAINTENANCE计划内维护官方提前通知的升级、迁移DEGRADED性能降级响应变慢、部分功能不可用但服务未中断OUTAGE完全不可用服务中断、大面积报错这里第一版踩过坑。我最早分了六档还加了UNKNOWN和INCIDENT两个状态。UNKNOWN本来是给“获取失败”用的结果发现获取失败和正常状态之间无法逐一对应UNKNOWN状态的消息满天飞反而成了噪音。后来我把UNKNOWN从状态模型里删掉了单独做了一个“fetch_error”的标记挂在消息元数据里而不是参与状态流转。这个调整很关键。获取失败并不等于服务故障——上游接口超时可能只是我这边网络抖动。把“无法获取”和“服务异常”混在一起会造成误报时间久了人就会对告警麻木真正的故障来了反倒没人看。2.2 状态变化检测基于哈希的“聪明比较”有了状态模型接下来是状态变化检测。CloddsBot里每个被监控项的状态用一串结构化JSON表示{ service: ecs, region: cn-north-1, level: outage, title: ECS实例创建失败率上升, timestamp: 1708675200, detail_url: https://status.example.com/incidents/12345 }检测变化的逻辑很简单抓取新状态后计算新JSON与旧JSON的哈希值如果两者不同再进一步比较level和title是否有变化。级别变化或标题变化都算“状态变更”触发推送。这里最体现细节的地方是timestamp的处理。不同云厂商返回状态页时给的时间精确度不一样有的精确到秒有的只精确到小时。如果直接用原始时间参与比较会出现一种尴尬情况内容没变只是记录时间变了结果每次都触发“状态更新”告警。我的处理方式是做时间归一化——只看精确到分钟的时间分钟以内的差异视为无变化。这个逻辑看起来简单但真实场景里效果极好。上线后误报率从每天十几个锐减到偶尔一两个。2.3 消息推送的分级策略噪音与紧急的平衡推送这事最大的挑战不是“能不能发出去”而是“怎么让人愿意看”。最开始我做的是“有状态变化就推”结果正常维护也推、轻微故障也推、恢复了也推一个晚上下来群里几十条消息。第二天大家直接把群免打扰了。后来在分级上下了功夫OUTAGE级别立刻推送高频重试5分钟内重发三次电话或短信通道不在CloddsBot范围但如果消息出口支持所有人会顺手一下。DEGRADED级别立刻推送不重试。提醒关注但不反复骚扰。MAINTENANCE级别合并推送。同一个窗口期内所有维护公告攒成一条消息每天固定时间发一次汇总。OK级别恢复通知只在之前确实是故障状态的情况下推送。如果服务一直正常突然从正常变为正常比如上游页面架构变了导致解析方式变了则不推送。这套分级策略最重要的是做了“告警合并”。MAINTENANCE不逐条推、恢复消息只推一次背后传递的思想其实是少即是多让人尊重警报而不是习惯警报。3. 实操过程与核心环节实现3.1 完整目录结构与模块划分CloddsBot的目录结构我很早就固定下来了整体很直接cloddsbot/ ├── cloddsbot/ │ ├── __init__.py │ ├── __main__.py │ ├── config.py # 配置加载、环境变量处理 │ ├── collector.py # 抓取器基类和分发逻辑 │ ├── storage.py # Redis存取封装 │ ├── notifier.py # 消息推送到不同渠道 │ ├── parsers/ # 各云厂商的专属解析器 │ │ ├── base.py │ │ ├── qcloud.py │ │ ├── aliyun.py │ │ ├── aws.py │ │ └── generic.py # 通用解析器走状态页爬取 │ ├── scheduler.py # 定时调度与重试 │ └── models.py # 状态模型与pydantic数据类 ├── cloddsbot.yaml # 主配置文件 ├── requirements.txt └── run.py模块间依赖关系很明确run.py启动入口启动schedulerscheduler按每家的频率触发collectorcollector调用对应厂商的parser拿到结构化状态写入Redis再由notifier消费队列生成消息并推送。3.2 配置文件的“可解释性”设计配置文件我用的是YAML因为运维同学对这个格式基本上零门槛。下面是核心片段schedule: qcloud: 120 # 腾讯云每2分钟抓一次 aliyun: 180 # 阿里云每3分钟抓一次 aws: 300 # AWS每5分钟抓一次 region_hints: - cn-north-1 - ap-guangzhou - default notify: - channel: wecom webhook: ${WECOM_WEBHOOK} allow_levels: [degraded, outage, maintenance, ok] merge_maintenance: true merge_window: 3600 # 1小时内的维护公告合并为一条 storage: redis_url: redis://127.0.0.1:6379/0 state_prefix: cloddsbot:state: queue_key: cloddsbot:notify_queue几个值得解释的细节抓取频率不是越短越好。云厂商状态页更新的频率本身就有限制一般不会低于一分钟。腾讯云我设了120秒、AWS设了300秒是综合考虑了上游更新粒度和我们自己被打限流风险的结果。你把它调到10秒去抓并不会真的感知到“秒级故障”反而更容易被上游封IP。webhook放环境变量不在配置文件里明文写。这个不是防什么牛逼黑客而是防止哪天把YAML传到GitHub上泄露。这种低级的安全事故见过太多从项目第一天起就养成习惯。3.3 parser实现思路怎么适配“完全不一样”的厂商每个云厂商的状态页结构千差万别这也是整个项目工程量最大的地方。设计上我统一抽象了几个接口class BaseParser(ABC): abstractmethod def fetch_raw(self, region: str) - dict: 请求上游返回原始数据 abstractmethod def parse(self, raw: dict) - list[ServiceStatus]: 解析上游数据输出统一结构 abstractmethod def health_check(self) - bool: 判断上游返回是否有效防止把异常页面当成正常空状态这里最容易被忽视的是health_check。很多云厂商的状态页在“恢复正常”之后会把事件藏起来或者归档。如果解析逻辑只认“有事件就是故障”那么事件归档后系统就会认为故障直接消失了恢复节点会混乱。我在resolve逻辑里加了个判断如果上次状态是outage这次事件列表里找不到对应标题但不是“明确标记已恢复”那么保持outage并降级为“疑似恢复”等待下一次抓取确认。这种“两步确认恢复”的逻辑实测下来非常关键。有一次某家云厂商的页面出现回源异常事件列表整个空了如果我当场就推“服务已恢复”那用户花钱买了监控服务结果收到的是一条假消息信任感瞬间归零。3.4 通知的“幂等”设计如何避免重复轰炸通知模块是我重构最多的地方。坑在于“重试可能导致重复消息”。比如Redis队列消费时如果我拉取了消息但还没来得及推送到企业微信进程就被kill了重启后消息还在队列里会再推一次。对于OUTAGE级别的告警我本来就有意重试这个不算问题但OK级别恢复通知如果重复发用户就会很烦。解决方案是在Redis里给每条消息生成一个指纹service level title timestamp归一化后的值只在指纹不存在时才推送推送成功后写入一条带过期时间的记录。async def publish_if_unique(redis, fingerprint: str, message: dict, ttl3600): # set nx 就是“只有key不存在才能写入”配合过期时间实现去重 result await redis.set(fcloddsbot:dedup:{fingerprint}, 1, nxTrue, exttl) if result: await notifier.send(message)设计思路用一句话总结告警是允许重复的恢复通知不允许重复。3.5 部署与守护systemd 容器怎么选CloddsBot是定时任务型应用部署形式上有两种主流选择Systemd服务简单直接适合单机部署。Docker Compose适合和Redis一起编排迁移方便。我自己的线上实例是跑在Docker里的原因是Redis这个依赖也一起打包换机器时一条docker compose up就起来了。下面是compose片段version: 3.8 services: redis: image: redis:7-alpine restart: always volumes: - redis_data:/data cloddsbot: build: . restart: always environment: - WECOM_WEBHOOK${WECOM_WEBHOOK} - TELEGRAM_BOT_TOKEN${TELEGRAM_BOT_TOKEN} depends_on: - redis volumes: - ./cloddsbot.yaml:/app/cloddsbot.yaml:ro volumes: redis_data:还有一个很多人容易忽略的点时区。容器默认是UTC时区而云厂商状态页里的时间大多是本地时间。如果你不约定统一时区解析出来的“下午三点发生的故障”很可能被当作“晚上十一点发生”排序和合并都会出问题。我在Dockerfile里显式设置了ENV TZAsia/Shanghai确保所有时间处理在本地时区保持一致。4. 常见问题与排查技巧实录4.1 上游页面改版解析器直接抛出一堆0结果这是这个项目上线以来遇到最多的问题——云厂商状态页改版频率虽然不高但一旦改版HTML结构全变。因为我最初的通用解析器里用了一些CSS选择器改版后选择器匹配不到目标节点返回的是空数组。排查思路是这样的看到“所有服务状态为NULL”这种异常时不要急于改代码先把原始页面抓下来存档用肉眼看一下新版结构再判断是选择器变了、还是页面改成了SPA(前端渲染)导致requests拿到的HTML根本没有数据。如果是SPA页面requests这种纯HTTP抓包方式就拿不到内容了。解决办法有两个轻量方案是找有没有隐藏的JSON接口F12看Network面板很多SPA状态页都会调一个/api/status直接抓那个接口重方案是引入Playwright做无头浏览器渲染但我个人建议非必要不上浏览器自动化毕竟重量级依赖会拉高资源占用。4.2 消息推送通道被限流企业微信自定义机器人有一个限制每分钟最多20条消息。正常情况下CloddsBot的推送量远到不了这个上限但有一次多厂商同时故障加上重试机制直接触发了限流。实际表现是把消息丢进队列就返回成功了但企业微信那边根本不会发出。要排查这个问题建议大家先确认一下它是否属于“普通消息”而非“文件消息”然后看返回码里的errcode是否为45009频控限制。如果是退避策略就要加长不要用固定的5秒重试改成指数退避——第一次等30秒第二次等2分钟第三次等5分钟。4.3 Redis连接断开导致的状态丢失问题Redis连接断开导致的状态丢失问题也很典型。我第一版代码在每次抓取之前GET旧状态、抓取之后SET新状态一旦Redis在GET之后、SET之前重启新旧状态衔接就会出现空洞。更严重的是队列里的消息也一起丢了。改进方案是给Redis加一个持久化配置appendonly yes。虽然理论上会带来一点性能损失但对于这个项目来说完全可以在接受范围内——毕竟它的写入频率低到可以忽略。另外在代码层抓取器在处理之前会先把旧状态做一个内存副本如果下游存储一直不可用就自动进入“降级模式”只记录日志不改变任何状态标记。宁可暂时不通知也不要产生错误的恢复通知。4.4 常见问题速查表症状可能原因解决路径所有服务显示UNKNOWN状态页结构改版抓原始HTML比对CSS选择器/JSON接口告警消息重复推送队列消费后进程退出导致重复消费接入Redis去重nx ex总是收到“维护公告”但官网并没有parser把历史公告当作最新事件增加时间过滤只取当前时间窗口内的事件某渠道一直推不出去webhook失效检查渠道返回码单独调试curl测试webhook凌晨3点推送异常时区问题统一容器时区时间戳统一转int再比较消息推送延迟严重轮询间隔过长检查scheduler时间设置确认是否用了AsyncIO阻塞调用4.5 几个想分享的避坑心得最后聊几点真正属于“经验”层面的东西吧。第一个云厂商状态API的返回结构里“事件描述”字段经常用英文或者多语言版本不同地区看到的内容可能不一样。如果你要做“标题变化才推送”一定要对标题文本做归一化——比如去掉首尾空格、统一全角半角、忽略大小写。不然光是“ECS实例异常”和“ECS实例异常 ”这俩看似一模一样但哈希值不同的字符串能让你被投诉说“老发重复告警”。第二个不要把任何云厂商的状态API当作“永久稳定”的依赖。它们可能随时改版、随时下线、随时要求鉴权。所以parser层必须要做好异常兜底任何一个解析器抛出的异常都不能让整个主程序崩溃。Python里给每个parser的parse方法挂上装饰器异常时返回None并记录完整上下文是我认为最务实的一种防御。第三个如果想扩展新的云厂商不要急着去写代码。先去翻一遍这个厂商的历史状态页看它近几个月的故障事件是怎么展示的——有些页面会把历史事件隐藏有些维护公告只在特定页面展示。把展示逻辑摸清楚再动手能省掉后面非常多的调试时间。第四个CloddsBot后续要是想支持更多功能我建议可以往“告警聚合分析”方向发展。比如对历史状态数据做统计输出“本月各云厂商可用性排行”或者根据故障时长和故障等级计算一个服务健康评分。这个方向能显著提升项目的实用价值而且数据其实已经从第一天就在采集了积累下来就是资产。分享一个我实际操作中的体会做这种监控类项目最难的永远不是写代码的那一两个小时而是抵抗“把边界扩大”的诱惑。如果什么都想监控、什么告警都想接最后的结果往往是系统里充满了没人看的警告。把状态模型缩小到四级把推送策略简化到“紧急的立刻发、不紧急的合并发、恢复的只发一次”反而让CloddsBot成了我日常最依赖的工具之一。如果你刚好也有多云环境想管理可以试着先照这个思路搭一个最小版本出来只接一家云厂商只推到一个微信群跑上两天感受一下。我相信你也会体会到那种“故障了不用自己刷控制台机器人先喊我一声”的踏实感。
返回列表