前言:为什么自托管拨测

boce.com 是国内老牌的商业拨测平台,HTTP / DNS / PING / TCPing / 劫持 / 被墙检测样样齐全,按波点计费

问题在于:

  1. 平台 UI 是公版,告警格式没法定制,群里来一长串原始 JSON,10 条 URL 就是 6000+ 字符,眼睛都看花了
  2. 波点余额监控没有,经常到第三天发现「余额不足,已停止拨测」,业务告警全断
  3. 多项目隔离没有,一个公司一套配置,谁都能改别人的 URL
  4. 数据不能导出,历史曲线只能用它的图表看,没法和我们自己的 Grafana 拼起来
  5. 告警通道单一,只有邮件 / 钉钉,没有 TG / 飞书

于是就有了 boce-site —— 把 boce.com 的 API 包成自托管的小工具:

  • 玻璃态 Web 控制台,UI 自己写
  • Prometheus 抓取端点,直接并入现有监控体系
  • 周期任务调度,APScheduler 跑 12 种检测类型
  • 告警通道分发,TG + 飞书 webhook,自己定义紧凑格式

全部跑在一个 Docker 容器里,SQLite 存数据,没有 MySQL、没有 Redis、没有 worker 节点


架构:单容器装下所有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
┌───────────────────────────── boce-site 容器 ─────────────────────────────┐
│ │
│ ┌──────────────────────────┐ ┌──────────────────────────┐ │
│ │ Flask + APScheduler │ │ SQLite │ │
│ │ /login / / /config │ │ urls · url_probe_types │ │
│ │ /channels / /api/* │◄──►│ url_regions · regions │ │
│ │ /metrics / /probe/now │ │ probes_recent │ │
│ │ │ │ probe_results │ │
│ │ │ │ notification_channels │ │
│ └──────────────────────────┘ │ app_state │ │
│ │ └──────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────┐ ┌──────────────────────────┐ │
│ │ 玻璃态 Web UI │ │ Prometheus text │ │
│ │ sidebar + 3 个页面 │ │ exposition(从 DB 读) │ │
│ │ (dashboard / config / │ │ labels: probe_type, │ │
│ │ channels) │ │ source │ │
│ └──────────────────────────┘ └──────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 告警通道(TG / Lark) │ │
│ │ 每渠道独立 cron → 遍历 active URL → 取 probe_results │ │
│ │ → 紧凑格式(失败/慢/健康折叠) → sendMessage / webhook │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ boce.com API │ │ Prometheus │
task/create │ │ scrape /metrics │
task/info │ │ │
└──────────────────┘ └──────────────────┘

单容器的设计取舍

  • Web + Scheduler + Prometheus exporter 全在同一个 Flask 进程,APScheduler 用 BackgroundScheduler 起在主线程
  • SQLite 单文件,跟容器一起打包,docker-compose up 起来就能用,不需要外挂存储
  • 整容器内存占用 < 200MB,1 核 1G 机器就能跑

核心能力

🎯 玻璃态 Dashboard · 10s 自动刷新

打开 / 第一眼看到的是:

  • 实时余额(波点余额 / 已消耗 / 预计可用天数)
  • 日均消耗(过去 7 天平均)
  • 监控 URL 总数 / 异常数
  • 最近一轮结果表格(按 URL 分行,单元格按节点 + 探测类型切片)

每 10 秒自动拉一次 /api/state,不用刷新页面。

⚡ 手动拨测 · 单条 + 批量

单条:点列表里的「⚡ 拨测」按钮 → 后端立刻调 boce.com task/create → 1s 轮询 ≤ 30s → 进度条贴在卡片顶部 → 完成时按节点切片显示详情。

批量:多选勾选 → 「⚡ 拨测选中」→ 后端守护线程批量跑 → 前端 1.5s 轮询 /api/probes/batch/<id> → 进度条按 URL 切片 → 完成时逐行解锁「⚡ 拨测」按钮。

1
2
3
4
5
6
7
8
URL 上勾选 N 种 probe_type → 每个类型一个线程
├─ HTTP → boce.com task/create1s 轮询 ≤ 30s → save + gauges
├─ DNS → boce.com task/create1s 轮询 ≤ 30s → save + gauges
├─ PING → boce.com task/create1s 轮询 ≤ 30s → save + gauges
├─ TCPing → boce.com task/create1s 轮询 ≤ 30s → save + gauges
└─ 劫持检测 → ...(同步类型:create_task 已 inline 返回 data,省一轮轮询)

probe_results[(url_id, source, probe_type) UPSERT] — 每 (URL × source × type) 一行

进度条跨页面保留 —— 即使中途切到 /config/channels 页面,再切回 Dashboard 进度条还在(DB 字段 + SSR data-* 恢复)。

⏰ 周期任务 · 三种调度

每个 URL 独立 APScheduler job,三种调度方式:

类型 用途 示例
interval 固定间隔 interval=300 → 每 5 分钟一次
cron Cron 表达式 cron="0 */1 * * *" → 每整点一次
bigweek ISO 奇数周 bigweek=86400 → 隔一周(ISO week 奇数)执行一次

新增 URL 的 next_run_at 默认 = now + interval(不会立即补跑一次,避免刚加监控就把余额烧光)。

🧬 12 种检测类型 · 多类型并行

Type 标签 同步/异步 类型专属参数 每次波点
http HTTP 异步 method / post_data / headers 1 / 节点
dns DNS 异步 dns_type / dns_ns_server 1 / 节点
ping PING 异步 1 / 节点
tcping TCPing 异步 1 / 节点
hijack 劫持检测 异步 hijack_except_domain 10 / 节点
ipv6 IPv6 异步 100 / 域名
pollute DNS 污染 同步 10 / 域名
wall 被墙检测 同步 10 / 域名
qq QQ 拦截 同步 10 / 域名
wechat 微信拦截 同步 10 / 域名
blacklist 备案黑名单 同步 10 / 域名
ssl SSL 证书 同步 10 / 域名

同步类型is_async=False):create_task 直接 inline 返回结果,省一轮 30s 轮询 —— 这种类型大部分是”域名级”检测(DNS 污染 / 被墙 / QQ 拦截),不需要按节点切片。

每个 URL 可以同时勾选 N 种类型,并行执行。例:给一个生产 API 配置 HTTP + DNS + PING + 劫持检测 + SSL 证书,一次任务 5 种维度全覆盖。

🗺️ 多区域拨测

  • 默认 6 个南方节点(广东电信 / 广东联通 / 广东移动 / 福建电信 / 杭州电信 / 上海联通)
  • 节点自定义选择(按省份 + ISP 精确匹配)
  • 节点列表存在 regions 表里,可在线增删

📊 Prometheus 集成 · /metrics 端点

容器暴露 /metrics,从 SQLite 读后转成 Prometheus text exposition 格式:

1
2
3
4
5
6
7
8
9
# HELP probe_success Result of probe (1 = success, 0 = failure)
# TYPE probe_success gauge
probe_success{url="https://api.example.com",probe_type="http",source="periodic",node_name="广东电信",tag="prod"} 1
probe_success{url="https://api.example.com",probe_type="http",source="periodic",node_name="广东联通",tag="prod"} 1
probe_success{url="https://api.example.com",probe_type="http",source="periodic",node_name="福建电信",tag="prod"} 0

# HELP probe_latency_ms Probe latency in milliseconds
# TYPE probe_latency_ms gauge
probe_latency_ms{url="https://api.example.com",probe_type="http",source="periodic",node_name="广东电信",tag="prod"} 287

labels 包含:

  • probe_type — http / dns / ping / hijack …
  • sourceperiodic / manual
  • tag — 用户自定义标签(prod / staging / 灰度 …)
  • url — 完整 URL
  • node_name — 拨测节点

容器重启不丢历史 —— 所有数据在 SQLite 里,restart 一下 Prometheus 还能继续抓同一份数据。

📡 告警通道 · TG + 飞书

每渠道独立 alert_cron(默认 0 */1 * * * 每小时一次),到点遍历所有 active URL 取最新结果。

告警格式从 6000+ 字符压到 1500 字符(10 URL 场景):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
🚨 拨测告警 · 2026-08-24 12:34:56 · 3 个 URL 异常
──
<b>URL</b>: <a href="https://example.com">https://example.com</a> <code>[prod]</code> <code>2026-08-24 12:30:11</code>
<b>HTTP</b> ⚠️6节点 ✓32 avg=512ms 🐢慢1
✗ 福建电信 502 timeout
🐢 上海联通 1823ms
✓ 广东移动 200 359ms
✓ 北京联通 200 287ms
✓ 杭州电信 200 415ms
──
<b>URL</b>: <a href="https://b.example.com">https://b.example.com</a> <code>[prod]</code>
<b>劫持检测</b> ⚠️4节点 ✓13
✗ 福建电信 🚫 被劫持
...
── 3节点正常(折叠)

压缩要点

  • 类型标签中文化:劫持检测 / DNS 污染 / 被墙检测 / 备案黑名单 / SSL 证书
  • 健康节点 ≤3 直接展开,>3 折叠为「N 节点正常(折叠)」
  • 延迟整数 ms、单空格分隔、无 <pre>
  • TG 走 HTML,飞书走 markdown,两套模板各自紧凑

🔐 鉴权 · admin + viewer 双角色

  • admin 账户:完整权限(ADMIN_USER / ADMIN_PASSWORD
  • viewer 账户:只读(VIEWER_USER / VIEWER_PASSWORD,留空 = 不启用)
    • 服务端拦截任何非 GET/HEAD/OPTIONS 请求(/api/* 返 403 JSON,普通路径返 403 HTML)
    • UI 层把所有按钮 disabled + 加 viewer-mode body class 用 CSS 置灰
    • 默认收起侧边栏,禁用任何编辑 / 拨测 / 删除

给老板 / 外部审计 / 客户演示监控数据用,一个 viewer 账号搞定,任何写操作服务端硬拒绝


快速开始

1
2
3
4
git clone <repo> ~/data/boce-site
cd ~/data/boce-site
# 编辑 docker-compose.yml 配置 API_KEY / ADMIN_USER / ADMIN_PASSWORD
docker compose up -d --build

打开 http://localhost:9191/,用配置的账号登录。

入口 URL
Dashboard http://localhost:9191/
监控配置 http://localhost:9191/config
告警通道 http://localhost:9191/channels
登录 http://localhost:9191/login
Prometheus 抓取 http://localhost:9191/metrics
实时余额 API curl http://localhost:9191/api/quota
单条手动拨测 curl -X POST -H 'Content-Type: application/json' -d '{"url_id":N}' http://localhost:9191/probe/now
批量手动拨测 curl -X POST -H 'Content-Type: application/json' -d '{"url_ids":[1,2,3]}' http://localhost:9191/urls/probe-batch → 返回 {"ok":true,"batch_id":"<uuid>"}
批量进度查询 curl http://localhost:9191/api/probes/batch/<batch_id>

技术栈

  • Web:Flask 3.x + 原生 Jinja2 模板(无前后端分离,玻璃态 CSS 直接写在模板里)
  • 调度:APScheduler 3.x(BackgroundScheduler 跑在 Flask 主线程)
  • 存储:SQLite 3(单文件,200MB 内存可支撑 1000+ URL × 12 类型 × 6 节点历史)
  • 指标导出prometheus-client
  • 告警图(可选):WeasyPrint + pypdfium2 + Pillow(把告警 HTML 转 PNG 发群)
  • 容器化:单个 Docker 镜像,docker-compose 一行起

依赖列表(requirements.txt):

1
2
3
4
5
6
7
flask>=3.0,<4.0
apscheduler>=3.10,<4.0
requests>=2.31,<3.0
prometheus-client>=0.19,<1.0
weasyprint>=62.0,<70.0
pypdfium2>=4.0,<6.0
Pillow>=10.0,<12.0

不是什么

跟上一篇文章 Consul Register Manager 一样,先把边界说清楚:

  • 不是 boce.com 替代品:底层拨测还是调 task/create + task/info,自托管只解决 UI / 告警 / 集成
  • 不是 Prometheus 替代品:自己不存时序数据,只暴露 /metrics 给外部 Prometheus 抓
  • 不是商业级 SLA 平台:单容器 SQLite,几十万 URL 量级需要换 Postgres + 多 worker
  • 不是 DNS 权威服务器:不做权威解析,只监测权威解析是否被篡改 / 污染
  • 不是 APM:不追踪应用层调用链,只做”端到端可达性”

总结成一句话

把商业拨测平台的”标准 UI + 固定告警”换成”自定义玻璃态控制台 + 紧凑告警格式 + Prometheus 原生集成”,单容器装下所有,5 分钟跑起来。