用 SQLite 存监控历史:从 JSON 文件迁到单文件数据库

举报
yd_217417492 发表于 2026/10/27 08:48:38 2026/10/27
【摘要】 监控脚本用 latest.json 和 events.jsonl 存数据,查历史、做统计都很别扭。本文给出 SQLite 表设计、WAL 并发配置、写入与聚合查询示例,以及从 JSONL 平滑迁移的做法。

JSON 文件方案的三个瓶颈

很多小监控脚本是这么存的:latest.json 只保留最新一次结果,events.jsonl 追加补货、告警之类的事件。跑几天没问题,时间一长就会遇到三个问题。

  • 查不动。想知道“100 元档最近 24 小时的库存曲线”,得把整个 jsonl 读进内存再过滤,文件到几十 MB 就开始卡。
  • 没法聚合。“这个月检查了多少次、漏了几次”“补货集中在哪个时间段”,用文本文件只能自己写循环。
  • 并发有风险。cron 在写、你手动在查,或者两个进程同时追加,轻则读到半行,重则互相覆盖。

这些正好是 SQLite 擅长的:单文件、零服务、Python 标准库自带 sqlite3,还支持索引、事务和聚合查询。

表结构:只留必要的字段

CREATE TABLE IF NOT EXISTS checks (
  id        INTEGER PRIMARY KEY AUTOINCREMENT,
  ts        TEXT    NOT NULL,          -- UTC ISO8601
  tier      INTEGER NOT NULL,          -- 500/200/100
  stock     INTEGER NOT NULL DEFAULT 0,
  has_stock INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_checks_ts ON checks(ts);
CREATE INDEX IF NOT EXISTS idx_checks_tier_ts ON checks(tier, ts);

CREATE TABLE IF NOT EXISTS events (
  id    INTEGER PRIMARY KEY AUTOINCREMENT,
  ts    TEXT    NOT NULL,
  kind  TEXT    NOT NULL,              -- restock / stock_up / error
  tier  INTEGER NOT NULL,
  stock INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts);

两个细节值得强调。第一,时间统一存 UTC 的 ISO8601 字符串,展示时再转本地时区,避免夏令时和跨机器时区把数据搅乱。第二,has_stock 单独存一列,不要用 stock > 0 反推——平台可能返回“有货但数量未知”的状态。

并发:WAL 加 busy_timeout

cron 写入和手动查询撞在一起时,默认的 rollback journal 会直接报 database is locked。加三行配置就能大幅缓解:

import sqlite3

conn = sqlite3.connect("/var/lib/monitor/monitor.db", timeout=10)
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA synchronous=NORMAL")
conn.execute("PRAGMA busy_timeout=5000")

WAL 允许“一写多读”并行,busy_timeout 让写锁冲突时先等 5 秒而不是立刻失败。监控这种低频写入场景,这套配置足够了。

写入:一次检查一个事务

from datetime import datetime, timezone

def record(conn, snapshot):
    ts = datetime.now(timezone.utc).isoformat(timespec="seconds")
    rows = [(ts, tier, s["stock"], int(s["has_stock"]))
            for tier, s in snapshot.items()]
    with conn:
        conn.executemany(
            "INSERT INTO checks(ts, tier, stock, has_stock) VALUES (?,?,?,?)",
            rows,
        )

with conn 会自动开事务并在异常时回滚,比手动 commit() 稳。注意别在循环里逐条 commit,每 10 分钟三次插入合成一个事务,写放大和锁竞争都会小很多。

三个马上能用的查询

1)某档位最近的库存变化

SELECT ts, stock FROM checks
WHERE tier = 100
ORDER BY ts DESC LIMIT 20;

2)今天有没有漏跑——正常每 10 分钟一次,一天应该是 144 次:

SELECT substr(ts, 1, 10) AS day, COUNT(*) AS n
FROM checks
GROUP BY day
ORDER BY day DESC LIMIT 7;

3)补货事件按小时分布,用来判断平台一般什么时候放量:

SELECT substr(ts, 12, 2) AS hour, COUNT(*) AS n
FROM events
WHERE kind = 'restock'
GROUP BY hour
ORDER BY n DESC;

从 JSONL 平滑迁移

不用停机。写一个一次性脚本,把历史事件读进来逐条插入,插入前先按 (ts, kind, tier) 查重,避免重复跑导致数据翻倍:

import json

with open("events.jsonl", encoding="utf-8") as f:
    for line in f:
        e = json.loads(line)
        conn.execute(
            """INSERT INTO events(ts, kind, tier, stock)
               SELECT ?,?,?,? WHERE NOT EXISTS (
                 SELECT 1 FROM events WHERE ts=? AND kind=? AND tier=?)""",
            (e["at"], e["kind"], e["tier"], e["stock"],
             e["at"], e["kind"], e["tier"]),
        )
conn.commit()

迁移完成后先并行跑一周:新数据写库,同时继续写 jsonl 兜底;确认查询结果对得上,再把 jsonl 关掉。

保留策略与备份

SQLite 不会自动瘦身。建议按天清理明细、保留聚合结果:

DELETE FROM checks WHERE ts < datetime('now', '-90 days');
VACUUM;

备份更简单,用 sqlite3 monitor.db ".backup /backup/monitor-$(date +%F).db" 就得到一致性快照,比复制正在写入的文件安全。

什么时候不该用

如果监控对象变成几十台机器、每秒都有数据,或者需要多台机器同时写同一个库,那就该上 Prometheus、VictoriaMetrics 这类时序库。SQLite 适合的是单机、低频、要长期留历史的场景——小脚本监控正好落在这个区间。

从 JSON 换到 SQLite,改动量通常只有几十行,但之后每一次“查一下上个月”的请求,都会感谢当初的自己。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。