ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

轻量级自托管监控engarde816:从健康检查到告警通知的完整实践

轻量级自托管监控engarde816:从健康检查到告警通知的完整实践 简介Engarde816是一款面向击剑赛事组织者与裁判的专业编排工具专注解决选手分组、赛制设定、对阵表生成以及成绩统计等赛事后勤难题。软件内置单淘汰、双败淘汰、循环赛等多种赛制支持种子选手分配、随机抽签并且能够在比赛过程中实时跟踪进度、自动更新积分榜还可对接电子计分板硬件覆盖从小型友谊赛到国际大赛的各类办赛需求。资源包共38个文件大小仅566KB其中exe为主程序fta文件对应不同赛制定义ptt与css用于赛事网页模板和样式txt与res承载配置说明和界面文字整体结构紧凑而功能完整。目前已有2433人学习/下载。借助这一完整包件读者既能直接安装部署以快速搭建赛事运行环境也能通过文件构成理解击剑编排系统从规则配置到结果展示的实现思路从而提升实际办赛的效率与公平性。 engarde816这个代号一开始只是我随手敲在笔记里的一行字。它是击剑术语En Garde加一串数字的组合——击剑裁判下令后运动员要摆出准备姿势随时应对对手的进攻而我当时正好在折腾一台天天抽风的旧服务器8月16号那天差点被一个半夜挂掉的服务坑到失眠于是想给这套监控方案起个有点仪式感的名字。这个项目最终长成了一个极简的自托管服务状态监控工具盯HTTP接口、TCP端口、定时任务在服务不可用的时候第一时间通知我。它很小但意外地能打今天就把整个思路和实现过程摊开聊聊。1. 项目命名与定位先聊聊engarde816到底想解决什么问题1.1 En Garde背后的哲学把准备做成一种习惯击剑里的En Garde不是进攻指令而是准备口令。裁判喊完之后你要站稳、举剑、盯住对手任何方向的进攻都能第一时间做出反应。我给自托管监控工具起这个名字就是想强调一件事系统运维里最值钱的能力不是事后救火而是事前准备。过去的自己就是反面教材。一台跑着几个小服务的VPS假认真部署完就不管了。结果某个周六凌晨数据库连接池被打满服务直接不可用等早上看到手机里的一堆报错日志才后知后觉。从那天起我决定做一个自己的监控工具不要高大上不要一堆图表只要能在服务挂掉时第一时间喊我在恢复时告诉我警报解除就够了。1.2 为什么不用现成的监控系统做之前我认真对比过几套成熟方案Uptime Kuma、Grafana Prometheus、Zabbix甚至商业的监控SaaS。选型表拉出来看方案优点缺点Uptime Kuma自带UI、告警丰富、部署快状态页和告警绑定得比较死二次开发不顺手Prometheus Grafana指标能力极强、生态完善对个人项目来说太重采集、存储、展示三层都要维护商业SaaS开箱即用、免运维数据出网而且免费额度各种限制自研engarde816逻辑透明、完全可控、可定制要自己写代码维护最后选了自研这条路。核心原因是我需要的功能其实非常收敛周期性探测目标、判断活着还是死了、失败到一定次数就告警、恢复后通知这套逻辑用FastAPI加SQLite就能撑起来完全不需要引入一套分布式监控体系。另外自研还能顺手把TLS证书过期检查、接口响应时间波动这类细节自己做进去这是通用工具不容易覆盖的。技术上我选了Python FastAPI做后端异步客户端跑HTTP探测前端用最简单的Vue3单页加原生JavaScript轮询数据库用SQLite配SQLModel存历史检查记录。这个组合对单机自托管场景非常合适——零额外依赖、一条Docker命令就能拉起来、数据文件就是一个SQLite文件备份直接复制走人。2. 整体设计思路把监控拆成可落地的三个层次2.1 核心需求拆解监控什么、怎么判断、怎么通知在设计engarde816时我给自己定了三条需求基线后面所有功能都没越过这条线。第一监控目标要有三种类型。HTTP接口是最常见的返回非200状态码、响应时间超过阈值、域名证书剩不到7天都算异常TCP端口用于内网机器和数据库实例比如MySQL的3306、Redis的6379定时任务监控则用心跳思路实现——任务每跑完一次就往指定接口发一个心跳超过设定时间没收到心跳就告警。第二告警触发不能太敏感。网络抖动是常态一次探测失败就发告警半夜手机能响成筛子。我设计了连续3次失败才触发告警的规则配合每次探测间隔60秒相当于服务连续不可用3分钟才告警这个灵敏度对绝大多数场景来说很合适。第三通知渠道要聚合。我封装了一个NotificationSender模块统一对接Telegram Bot、钉钉群机器人和邮件三个渠道。告警信息包含服务名、失败原因、连续失败次数、当前响应码恢复通知则包含本次故障总时长。2.2 数据模型与调度机制用最小成本存下有用信息数据模型是项目的骨架我设计了四张表Targets存监控目标配置Checks存每次探测的原始记录Alerts存告警事件Heartbeats存定时任务心跳。核心字段如下class Target(SQLModel, tableTrue): id: int Field(primary_keyTrue) name: str # 服务名如博客主站 kind: str # http / tcp / heartbeat url: str # 探测地址 expect_code: int 200 # 预期HTTP状态码 timeout: float 5.0 # 超时时间单位秒 interval: int 60 # 探测间隔单位秒 fail_threshold: int 3 # 连续失败多少次触发告警 enabled: bool True created_at: datetime Field(default_factorydatetime.utcnow)调度机制上我没有引入Celery这类重量级任务队列而是用FastAPI的BackgroundTasks配合一个简单的轮询循环。进程启动后拉起一个asyncio任务每隔5秒扫描一次所有Target把该检查但还没检查的目标丢进探测函数。这种设计对几十个监控目标来说完全够用而且代码路径短出了bug好排查。调度伪代码大致是这样的逻辑管理员通过API增删Target后调度器按interval字段算出每个Target的下一次执行时间存入内存字典后台循环每5秒检查一次到点就执行探测。这样不用数据库锁也不会有重复触发是单实例场景下最稳妥的做法。3. 实操过程从零搭一个engarde816实例3.1 健康检查模块:把活着变成可量化的指标健康检查是整个系统的发动机也是最需要抠细节的部分。HTTP检查我封装了一个独立模块用httpx.AsyncClient发起请求记录状态码、响应时间、错误信息同时把SSL证书剩余天数一并查出来。# checkers.py import httpx import ssl import socket from datetime import datetime, timezone async def check_http(url: str, expect_code: int 200, timeout: float 5.0): started datetime.now(timezone.utc) try: async with httpx.AsyncClient(timeouttimeout, follow_redirectsTrue) as client: resp await client.get(url) latency_ms (datetime.now(timezone.utc) - started).total_seconds() * 1000 result { ok: resp.status_code expect_code, status_code: resp.status_code, latency_ms: round(latency_ms, 1), } result.update(await check_cert(url)) return result except httpx.TimeoutException: return {ok: False, error: timeout} except httpx.RequestError as exc: return {ok: False, error: str(exc)}TCP检查更简单用socket.create_connection尝试建立连接能连上就认为服务在线。这里有个容易被忽略的细节TCP能连上不代表服务真的可用比如端口还在监听但业务线程池已经打满TCP层依然能握手成功。所以TCP检查更适合配合HTTP检查一起用——TCP做存活探针HTTP做健康探针两层都有保障。每个Target的探测逻辑全部收敛在checker里返回统一的dict结构ok、status_code、latency_ms、error、checked_at。这个结构被上层调度器、告警模块、API输出共同复用保证数据口径一致。3.2 API与前端一个够用但不过度的仪表盘引擎做好了得给用户一个操作界面。engarde816的后端API我做了四个端点GET /api/targets列出所有监控目标POST /api/targets新增目标POST /api/targets/{id}/toggle启停目标GET /api/targets/{id}/history拉取最近24小时的探测记录。前端是Vue3的单页应用启动后通过fetch定时5秒轮询一次状态接口把每个Target的当前状态用红黄绿三种色块展示出来。绿色表示正常红色表示当前处于告警状态黄色表示历史上最近一次探测失败但还没达到告警阈值。前端展示有个小设计值得说除了当前状态我会在卡片下方显示最近7天可用率计算方法有两种口径一种把每次探测当成独立样本每次探测成功算100%失败算0%取平均另一种按时间加权失败持续的时间段计为不可用。实测下来第二种更接近用户体感所以最终用了时间加权口径。3.3 告警去重与升级机制别让手机变成噪音源告警模块是我踩坑最多的部分核心要解决两个问题重复轰炸和通知过于单薄。重复轰炸的解法是连续N次失败才告警告警冷却期。连续失败计数存在Target对象里只有达到fail_threshold才发告警发完置一个alerted标记后续持续失败但不再重复发直到恢复后发一条恢复通知并清除标记。冷却期我额外设了30分钟即使服务在恢复和再次故障之间反复横跳同一小时内最多收到一次故障告警和一次恢复通知。通知内容要能直接指导行动。Telegram和钉钉的告警消息我格式化成了这样服务名、状态、故障开始时间、持续时长、最后一次错误详情、当前响应码。恢复消息则带上这次故障的总时长方便事后做复盘统计。早期版本只发xxx down五个字半夜收到还要打开电脑查日志后来加上错误详情才知道这一条信息能省多少事。告警升级我留了一个接口但默认关闭当某个Target连续告警超过30分钟自动追加一条处理人通知到邮件列表这是考虑到有些服务故障不是自动恢复能解决的需要人工介入。目前个人使用场景还没触发过升级逻辑但团队内部署时这个功能很实用。4. 踩坑记录与排查技巧我在engarde816上吃过的亏4.1 假警报的根源多半在超时参数engarde816上线后第一次半夜告警手机短信和Telegram同时响了结果爬起来一看服务好好地跑着只是当时网络出口正好在高峰期到目标服务器的延迟从平时30ms飙到了8秒触发了5秒超时限制。这就是典型的假警报。解决思路是给HTTP检查增加超时重试策略第一次请求超时后不立即记为失败等下一次调度周期再探一次连续两次超时才真正判断为目标不可用。同时在网络抖动定位上用RTT往返时延基数做动态超时如果历史平均响应时间是500ms那么超时上限可以放宽到10倍而不是固定5秒。这个改进让假警报率至少降了八成。4.2 SQLite高并发写入的坑早期的数据写入用了同步sqlite3驱动每次探测结果都直接写库。当监控目标加到几十个时问题开始显现SQLite的写锁是全局的并发写请求多了之后抛database is locked。后来做了三个改动连接池开启WAL模式、写入操作塞进单线程队列、启动参数设置busy_timeout为5000毫秒。核心配置就两行from sqlmodel import create_engine engine create_engine( sqlite:///engarde.db, connect_args{timeout: 5, check_same_thread: False}, poolclassStaticPool ) # 初始化时执行 PRAGMA journal_modeWAL;WAL模式的好处是读操作不阻塞写操作这对engarde816这种写频繁、读低频的场景非常合适。如果你也遇到过SQLite锁冲突先检查是不是这两件事没做一是用WAL二是所有写操作收紧到同一个事务队列里。4.3 容器内网络环境导致探测失败engarde816跑在Docker里出的第一个诡异问题是监控宿主机上的另一个服务明明宿主机用curl访问一切正常但容器里就是连接拒绝。排查了半天原因是不同容器不在同一个Docker网络里容器名解析失败。解决方案是在docker-compose里给engarde指定network_mode为host或让所有服务加入同一个自定义bridge网络。对于需要探测宿主机服务的场景我用了一个更简单的办法在docker-compose里加extra_hosts把宿主机Host映射到host.docker.internal。这样容器内访问宿主机服务只需要把URL写成http://host.docker.internal:8080。4.4 时区错乱导致恢复时间统计不准有段时间恢复通知里的故障时长总是差值几个小时查下来是时间戳存储不统一。MySQL和Docker默认时区不同Python的datetime.utcnow()和datetime.now()混用导致一部分数据存的UTC一部分存的是本地时间换算自然出错。后来约法三章数据库里所有时间统一存UTC毫秒时间戳展示前在API层转成Asia/Shanghai时间所有写时间字段的代码一律显式注明时区禁止依赖环境变量默认时区。改完之后时间统计再没有出过偏差。4.5 TLS证书监控里容易漏掉的一个细节HTTP检查里加了SSL证书剩余天数后有段时间发现偶尔会报警证书即将过期但浏览器访问明明正常。最后定位到问题出在有些站点用了CDNCDN边缘节点的证书才是实际返回的证书而我的检查库默认连接的是源站IP导致拉到的证书和用户实际访问的不是同一张。解决方法是给检查加一个proxy参数对使用CDN的目标统一走线上HTTPS URL去检查同时记录证书的Subject、Issuer、剩余天数三个字段。如果你做证书监控也遇到类似问题先确认你检查的是不是用户实际访问的那张证书。5. 扩展中间件与后续演化engarde816还能怎么长5.1 把告警通道换成Webhook接入钉钉/企业微信当前版本的通知模块支持Telegram、邮件以及通过通用Webhook发到任意自定义渠道。对接钉钉和企业微信的群机器人只花了一下午方法完全一样在机器人设置里拿到Webhook地址把告警消息构造成一个POST请求体。import httpx DINGTALK_WEBHOOK https://oapi.dingtalk.com/robot/send?access_tokenxxx async def send_dingtalk(title: str, text: str): payload {msgtype: markdown, markdown: {title: title, text: text}} async with httpx.AsyncClient() as client: await client.post(DINGTALK_WEBHOOK, jsonpayload)这个扩展最大的价值是团队内部不需要每个人都搭一个engarde816只需要在公共群里挂一个机器人告警自动同步到群聊到对应负责人。个人项目和团队项目可以复用同一套引擎只是通知目标不同。5.2 打通Prometheus生态用Grafana做好看的大屏engarde816自带的内置页面适合看一眼是否正常的场景但真要分析趋势、做容量规划还是得靠Grafana。我在引擎里加了一个/metrics端点用prometheus_client暴露三类指标engarde_check_success_total按Target维度统计成功次数、engarde_check_latency_seconds按Target维度统计响应时间直方图、engarde_cert_days_left证书剩余天数Gauge。然后起一个Grafana实例数据源指向engarde816的/metrics端点拉取Prometheus风格指标后可以画出每条服务的可用率趋势和响应时间走势图。相比内置页面Grafana版本的图标更自由而且可以叠加告警规则配上Alertmanager形成Prometheus采集engarde816探测Grafana展示三层结构。5.3 定时任务监控给cron加上心跳定时任务的监控是很多自建系统容易漏掉的部分engarde816通过Heartbeat机制实现每个需要监控的定时任务执行完最后一步时向engarde816发送一个POST心跳请求引擎记录心跳时间如果超过预设间隔比如30分钟没收到新心跳就判定任务异常并触发告警。这个功能本质上和电商系统的订单超时未支付自动关闭是同一套思维模型用最后活动时间作为存活信号。实现上只需要在Target表里加last_heartbeat_at和max_heartbeat_gap两个字段心跳接口里做个时间戳更新后台调度器周期性扫描超时目标即可。5.4 自动化恢复尝试让engarde816不止于通知做完了告警和监控我开始琢磨能不能让系统多一些自愈能力。当前实现了一个实验性的auto_restart开关当某个Target连续失败达到5次且该Target配置了restart_cmd引擎会尝试远程执行一条恢复命令比如docker restart xxx然后静默30秒重新探测。这个功能我在两台个人服务器上跑了快一个月效果还凑合但一定要加保护机制——恢复操作最多执行3次执行过多说明问题不是重启能解决的要人工介入。自动恢复是个双刃剑配置的时候慎重别让监控工具替你做盲目的操作。6. 个人使用体会与下一步计划engarde816从最初的Https检查脚本长到现在这个有调度、有告警、有展示窗的小系统前后花了一个半月的周末时间。这个实践给我最大的感受是监控的价值不完全在于第一时间发现问题更在于让我随时知道我的系统在什么状态。有了engarde816之后我半夜不用再因为某个服务会不会挂了这种猜疑睡不着觉它的夜间接管让我能把精力放在真正的开发上。最后再分享一个我在整个过程中踩过最深的坑刚开始我给engarde816加了太多花哨功能什么拓扑图、指标聚合、权限系统结果核心的探测逻辑反而写得粗枝大叶。如果你也想做一个类似的监控项目听我一句劝先跑通最小闭环——一个HTTP检查、一个告警、一个恢复通知——然后再按自己真实的痛点去加功能。工具是服务自己需求的不是证明技术有多炫的。本文还有配套的精品资源点击获取
返回列表