最近手头几个本地轻量服务(SQLite 打底)在并发小写入的场景下都开始喘。但这玩意是本地轻量服务,不能塞一堆重依赖,PG/MySQL 那一套直接 pass。借着给两个具体场景做选型的机会,我把市面上还能用的本地轻量方案摸了一遍,把结论和判断过程记下来,也算给你做决策时省点查资料的时间。
两个场景长这样:
- 一个局域网 CMDB,监控 1 万多个 IoT 嵌入式服务的端口心跳,大约每 30 秒一跳,存 7 天用来展示。
- 局域网里几十路摄像头,一直在录 30 秒一段的短视频,攒了 30~90 天的量要处理。
先说结论,后面再展开为什么。
先说结论
一句话:场景一 SQLite WAL 调优兜底、上量上 VictoriaMetrics(可选 NATS 缓冲);场景二 MinIO 存视频 + SQLite 存元数据。 全程不碰 PG/MySQL,轻量约束守住。
先别急着换库,看看瓶颈到底在哪
动手加任何组件之前,先确认瓶颈的性质。我见过太多「SQLite 并发瓶颈」其实都是下面这些能改的写法,不是 SQLite 的硬上限:
| 每条心跳独立 autocommit | 每次提交都触发一次 fsync,磁盘 IOPS 成了天花板,几十 TPS 就卡死 | 攒批:单写线程 + 队列,每 1000 条一个事务 |
| journal_mode=DELETE + synchronous=FULL | 回滚日志 + 每事务刷盘,写写互斥,并发时疯狂 SQLITE_BUSY | 改 WAL + synchronous=NORMAL |
| 多线程共用一个连接 | SQLite 连接不是线程安全的,串行化 + 锁竞争 | 每线程/协程独立连接,或单写者模型 |
| 没设 busy_timeout | 一冲突就报错而不是重试,要么丢数据要么重试风暴 | PRAGMA busy_timeout=5000 |
| 万级设备全在 :00/:30 打点 | 瞬时万级写入尖峰,单连接扛不住 | 设备端加随机 jitter(±几秒)+ 服务端批量缓冲 |
说个我自己踩过的坑:SQLite 在 WAL 模式 + 批量事务下,单写者持续 5 万~10 万次插入/秒是能跑到的。你的场景平均才 333 次/秒,即便最坏情况下「万级尖峰」也只需在秒级内消化掉——这完全落在 SQLite 的能力圈里。所以我个人的建议是,换库前务必先做这一轮调优,大概率零成本解决。
调优清单就这几个 PRAGMA,连接建立后执行一次:
PRAGMA journal_mode=WAL; — 写前日志:读不阻塞写,可以并发读
PRAGMA synchronous=NORMAL; — WAL 下只需 WAL 文件 fsync,不必每事务刷盘
PRAGMA busy_timeout=5000; — 遇锁等 5s 重试,而不是立刻 SQLITE_BUSY
PRAGMA wal_autocheckpoint=2000; — 控制 checkpoint 频率,降低抖动
PRAGMA temp_store=MEMORY;
— 写入侧:单写协程 + 批量事务(伪代码)
BEGIN;
INSERT INTO heartbeat(device_id,ts,port,status,latency) VALUES (?,?,?,?,?); — × 1000,一次 COMMIT
COMMIT;
两个场景的负载画像(先算清楚账)
选型之前得先知道自己在扛多大的量,不然容易被厂商宣传带跑。
场景一:局域网 IoT 心跳监控(CMDB)
| 被监控的 IoT 服务数 | 10,000+ |
| 平均心跳写入速率 | ~333/s |
| 7 天滚动窗口内行数 | ~2.0 亿 |
| 列式压缩后存储 | 2~4 GB(原始约 16GB) |
算法:1 万服务 ×(每 30s 一次)= 333 次/秒;单日 1 万 × 2880(每天 30s 间隔数)= 2880 万行;7 天 ≈ 2.0 亿行。每行(设备ID + 时间戳 + 端口 + 状态 + 延迟)约 80 字节 → 原始约 16GB,时序库列存压缩后一般压到 2~4GB。
查询特征大致三类:① 每个服务「最新一次心跳」状态(last-value);② 时间范围趋势图(需要降采样);③ 7 天自动过期(retention)。
场景二:局域网摄像头短视频
| 单段视频时长 | 30s |
| 单摄像头 / 天 段数 | 2,880 |
| 20 路 × 30 天 的片段数 | ~5.2M |
| 视频体量 | 5~15 TB(看路数和天数) |
这俩场景本质不一样,别混为一谈。场景一是「高频小写入 + 时间查询」,走时序库或批量 SQLite;场景二是「海量大文件 + 低频元数据」,走对象存储 + 轻量元数据库。把 30 秒视频塞进任何 SQL/NoSQL 的「值」里,结果都是数据库膨胀、备份灾难、查询变慢。视频进对象存储,数据库只管元数据,这是铁律。
产品全景
按「需不需要独立进程」分成两条主线。先泼盆冷水:几乎所有「嵌入式」方案本质仍是单写者,真要多个进程同时高频写,必须走单二进制服务或者更重的分布式层(而分布式层写也是串行的,只为高可用)。
A. 先优化 SQLite(零迁移)
- SQLite(WAL 调优) —— 单写者 + WAL 并发读;单文件 .db,零依赖;场景一调优后够用,场景二元数据首选。先做这步,我估计 80% 的情况不用换库。
B. 嵌入式库(进程内)
- LMDB —— MVCC:读永不阻塞写、写永不阻塞读;单写者,mmap 直读,读性能极强(OpenLDAP 同款)。适合读多写少、last-value 点查。Key-Value,没 SQL。
- RocksDB —— LSM-Tree,为写入优化,单写者但写吞吐极高(Kafka/TiKV 底层同款思路)。适合进程内写洪流,但 C++ 依赖偏重、没 SQL。
- bbolt(Go)/ sled(Rust) —— 单写 + 多读;如果你的服务本身是 Go / Rust 栈,直接引库最顺手。
- DuckDB —— 单进程内可读写并发(无冲突的写,尤其 append,可以并行);多进程写入不支持。列式引擎,分析查询、降采样、报表极佳,不适合做高频并发写入的主存储。
C. SQLite 衍生 / 复制层
- libSQL —— SQLite 的 fork,继承了单写者模型(官方明确:并发写入是 Turso 另一套 Rust 重写的数据库才解决)。新增了嵌入式副本、HTTP Server 远程访问。它不解决写并发瓶颈,只有当你需要「SQLite + 边缘副本 / 远程访问」时才值得换。别被「SQLite fork」这五个字骗去治并发。
- rqlite / dqlite —— 通过 Raft 做复制与高可用;写入仍要经过 leader 串行化——提升的是可用性 / 读扩展,不是写吞吐。需要多机 HA/容灾时才上。
- Litestream —— 后台进程把 SQLite 变更增量复制到文件 / S3,是容灾备份,不是并发方案。
D. 单二进制服务(sidecar)
- VictoriaMetrics(vmsingle) —— 场景一首选。为时序写入高度优化,单节点就能吃下百万级 samples/s;列存压缩,7 天 retention 自动过期。一个静态二进制 ./victoria-metrics-prod,无外部依赖;兼容 InfluxDB / Prometheus / OpenTSDB / JSON 行协议。
- QuestDB / InfluxDB v3 —— 同为单二进制时序库;QuestDB 用 SQL + InfluxDB 行协议、ingestion 极快;InfluxDB 生态成熟。场景一备选。
- MinIO —— 场景二首选。自托管 S3 兼容对象存储,一个二进制起服务;为海量文件设计,支持生命周期过期(正好匹配 30~90 天留存)。视频文件的归宿,配合 SQLite/LMDB 存元数据。
- NATS JetStream —— 单二进制消息系统,JetStream 提供持久化、at-least-once、重放。场景一的写入缓冲层(设备 → JetStream → 批量落库);也能做场景二「摄像头 → 存储 → 处理」的事件通知总线。
E. 更重(为什么不选)
- ClickHouse —— 分析能力炸裂,但对你的规模偏重,且偏 OLAP 而非实时 ingestion;场景一若要做复杂聚合报表可备选。
- TimescaleDB / PostgreSQL / MySQL —— 需要运行时 + 初始化 + 服务账号,违反「不能装太多依赖」的约束,列出来只是说明为什么 pass。
横向对比
评分用文字,避免看图说话:极高 / 高 / 中 / 可用 / 偏弱 / 不适用。
| SQLite(WAL 调优) | 嵌入式 | 单写者 + WAL 并发读 | 零依赖·单文件 | 高 | 极高 | 先调优,多数情况够用 |
| LMDB | 嵌入式 KV | MVCC,读不阻塞写,单写 | 单 C 库 | 中 | 极高 | 读性能极强,写吞吐一般 |
| RocksDB | 嵌入式 KV | LSM,高写吞吐,单写 | C++ 库 | 高 | 中 | 写洪流进程内首选 |
| bbolt / sled | 嵌入式 KV | 单写 + 多读 | Go / Rust 库 | 中 | 高 | 按技术栈选 |
| DuckDB | 嵌入式 OLAP | 进程内并发写(append 无冲突),多进程只读 | 单 C 库·零依赖 | 中 | 不适用 | 分析查询强,非主存储 |
| libSQL | SQLite fork | 单写者(继承) | 嵌入式 + HTTP server | 中 | 高 | 加副本/远程,不改写并发 |
| rqlite / dqlite | 分布式 SQLite | 写串行过 leader(HA) | 单二进制(Go)/ C 库 | 可用 | 中 | 为高可用,非写吞吐 |
| VictoriaMetrics | 单二进制 TSDB | 高并发写 + 列存压缩 | 单个静态二进制 | 极高 | 不适用 | 场景一首选 |
| QuestDB / InfluxDB | 单二进制 TSDB | 高并发写 | 单二进制 | 极高 | 不适用 | 场景一备选 |
| NATS JetStream | 单二进制 消息 | 高吞吐 pub/sub + 持久 | 单二进制 | 高 | 中 | 做写入缓冲 / 事件总线 |
| MinIO | 单二进制 对象存储 | 高吞吐 blob | 单二进制 | 不适用 | 极高 | 场景二视频归宿 |
决策树
照着走就行,不用硬记:
#mermaid-svg-uph37iMzduypZtnc{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-uph37iMzduypZtnc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uph37iMzduypZtnc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uph37iMzduypZtnc .error-icon{fill:#552222;}#mermaid-svg-uph37iMzduypZtnc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uph37iMzduypZtnc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uph37iMzduypZtnc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uph37iMzduypZtnc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uph37iMzduypZtnc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uph37iMzduypZtnc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uph37iMzduypZtnc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uph37iMzduypZtnc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uph37iMzduypZtnc .marker.cross{stroke:#333333;}#mermaid-svg-uph37iMzduypZtnc svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uph37iMzduypZtnc p{margin:0;}#mermaid-svg-uph37iMzduypZtnc .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-uph37iMzduypZtnc .cluster-label text{fill:#333;}#mermaid-svg-uph37iMzduypZtnc .cluster-label span{color:#333;}#mermaid-svg-uph37iMzduypZtnc .cluster-label span p{background-color:transparent;}#mermaid-svg-uph37iMzduypZtnc .label text,#mermaid-svg-uph37iMzduypZtnc span{fill:#333;color:#333;}#mermaid-svg-uph37iMzduypZtnc .node rect,#mermaid-svg-uph37iMzduypZtnc .node circle,#mermaid-svg-uph37iMzduypZtnc .node ellipse,#mermaid-svg-uph37iMzduypZtnc .node polygon,#mermaid-svg-uph37iMzduypZtnc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-uph37iMzduypZtnc .rough-node .label text,#mermaid-svg-uph37iMzduypZtnc .node .label text,#mermaid-svg-uph37iMzduypZtnc .image-shape .label,#mermaid-svg-uph37iMzduypZtnc .icon-shape .label{text-anchor:middle;}#mermaid-svg-uph37iMzduypZtnc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-uph37iMzduypZtnc .rough-node .label,#mermaid-svg-uph37iMzduypZtnc .node .label,#mermaid-svg-uph37iMzduypZtnc .image-shape .label,#mermaid-svg-uph37iMzduypZtnc .icon-shape .label{text-align:center;}#mermaid-svg-uph37iMzduypZtnc .node.clickable{cursor:pointer;}#mermaid-svg-uph37iMzduypZtnc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-uph37iMzduypZtnc .arrowheadPath{fill:#333333;}#mermaid-svg-uph37iMzduypZtnc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-uph37iMzduypZtnc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-uph37iMzduypZtnc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uph37iMzduypZtnc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-uph37iMzduypZtnc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uph37iMzduypZtnc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-uph37iMzduypZtnc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-uph37iMzduypZtnc .cluster text{fill:#333;}#mermaid-svg-uph37iMzduypZtnc .cluster span{color:#333;}#mermaid-svg-uph37iMzduypZtnc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-uph37iMzduypZtnc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-uph37iMzduypZtnc rect.text{fill:none;stroke-width:0;}#mermaid-svg-uph37iMzduypZtnc .icon-shape,#mermaid-svg-uph37iMzduypZtnc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uph37iMzduypZtnc .icon-shape p,#mermaid-svg-uph37iMzduypZtnc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-uph37iMzduypZtnc .icon-shape .label rect,#mermaid-svg-uph37iMzduypZtnc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uph37iMzduypZtnc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-uph37iMzduypZtnc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-uph37iMzduypZtnc :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
是
否 / 已调优仍不够
能接受(推荐)
能接受(推荐)
必须纯进程内零额外进程
写洪流
读重 / last-value
视频元数据
需要
单机即可
瓶颈是架构层还是 SQLite 硬伤?
每条都 autocommit + FULL + 无 WAL?
§1 WAL 调优:单写攒批 + busy_timeout
能否接受多跑一个单二进制 sidecar?
场景一:VictoriaMetrics + 可选 NATS 缓冲
场景二:MinIO + SQLite 元数据
读写特征?
RocksDB / DuckDB / SQLite+WAL
LMDB
SQLite / LMDB / bbolt + 文件系统
需要多机 HA / 容灾?
rqlite / dqlite(提升可用性,写仍串行)
前述选型直接落地
分场景推荐架构
场景一:IoT 心跳监控
#mermaid-svg-xkCUgCLSSVccSO3T{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xkCUgCLSSVccSO3T .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xkCUgCLSSVccSO3T .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xkCUgCLSSVccSO3T .error-icon{fill:#552222;}#mermaid-svg-xkCUgCLSSVccSO3T .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xkCUgCLSSVccSO3T .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xkCUgCLSSVccSO3T .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xkCUgCLSSVccSO3T .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xkCUgCLSSVccSO3T .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xkCUgCLSSVccSO3T .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xkCUgCLSSVccSO3T .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xkCUgCLSSVccSO3T .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xkCUgCLSSVccSO3T .marker.cross{stroke:#333333;}#mermaid-svg-xkCUgCLSSVccSO3T svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xkCUgCLSSVccSO3T p{margin:0;}#mermaid-svg-xkCUgCLSSVccSO3T .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-xkCUgCLSSVccSO3T .cluster-label text{fill:#333;}#mermaid-svg-xkCUgCLSSVccSO3T .cluster-label span{color:#333;}#mermaid-svg-xkCUgCLSSVccSO3T .cluster-label span p{background-color:transparent;}#mermaid-svg-xkCUgCLSSVccSO3T .label text,#mermaid-svg-xkCUgCLSSVccSO3T span{fill:#333;color:#333;}#mermaid-svg-xkCUgCLSSVccSO3T .node rect,#mermaid-svg-xkCUgCLSSVccSO3T .node circle,#mermaid-svg-xkCUgCLSSVccSO3T .node ellipse,#mermaid-svg-xkCUgCLSSVccSO3T .node polygon,#mermaid-svg-xkCUgCLSSVccSO3T .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xkCUgCLSSVccSO3T .rough-node .label text,#mermaid-svg-xkCUgCLSSVccSO3T .node .label text,#mermaid-svg-xkCUgCLSSVccSO3T .image-shape .label,#mermaid-svg-xkCUgCLSSVccSO3T .icon-shape .label{text-anchor:middle;}#mermaid-svg-xkCUgCLSSVccSO3T .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xkCUgCLSSVccSO3T .rough-node .label,#mermaid-svg-xkCUgCLSSVccSO3T .node .label,#mermaid-svg-xkCUgCLSSVccSO3T .image-shape .label,#mermaid-svg-xkCUgCLSSVccSO3T .icon-shape .label{text-align:center;}#mermaid-svg-xkCUgCLSSVccSO3T .node.clickable{cursor:pointer;}#mermaid-svg-xkCUgCLSSVccSO3T .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xkCUgCLSSVccSO3T .arrowheadPath{fill:#333333;}#mermaid-svg-xkCUgCLSSVccSO3T .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xkCUgCLSSVccSO3T .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xkCUgCLSSVccSO3T .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xkCUgCLSSVccSO3T .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xkCUgCLSSVccSO3T .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xkCUgCLSSVccSO3T .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xkCUgCLSSVccSO3T .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xkCUgCLSSVccSO3T .cluster text{fill:#333;}#mermaid-svg-xkCUgCLSSVccSO3T .cluster span{color:#333;}#mermaid-svg-xkCUgCLSSVccSO3T div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xkCUgCLSSVccSO3T .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xkCUgCLSSVccSO3T rect.text{fill:none;stroke-width:0;}#mermaid-svg-xkCUgCLSSVccSO3T .icon-shape,#mermaid-svg-xkCUgCLSSVccSO3T .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xkCUgCLSSVccSO3T .icon-shape p,#mermaid-svg-xkCUgCLSSVccSO3T .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xkCUgCLSSVccSO3T .icon-shape .label rect,#mermaid-svg-xkCUgCLSSVccSO3T .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xkCUgCLSSVccSO3T .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xkCUgCLSSVccSO3T .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xkCUgCLSSVccSO3T :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
10,000 IoT 设备每 30s 心跳
NATS JetStream持久化缓冲 · 削峰填谷
VictoriaMetrics vmsingle单二进制 · 7d 留存
DashboardGrafana / 自研
设备心跳 → NATS JetStream(持久化、抗尖峰)→ 消费者批量写入 VictoriaMetrics(vmsingle,-retentionPeriod=7)→ 仪表盘用 MetricsQL 查。心跳写入走 InfluxDB 行协议:
heartbeat,device=iot-001,port=8080 status=1,latency_ms=12
不想引入 NATS 的话,可以简化成「设备直接批量写 VM」,或者退回「SQLite WAL 调优」兜底。我倾向于小流量先 SQLite 顶着,真看到瓶颈再上 VM,没必要一上来就铺。
场景二:摄像头短视频
#mermaid-svg-J6qZoCjkx93XjHnA{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-J6qZoCjkx93XjHnA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-J6qZoCjkx93XjHnA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-J6qZoCjkx93XjHnA .error-icon{fill:#552222;}#mermaid-svg-J6qZoCjkx93XjHnA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-J6qZoCjkx93XjHnA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-J6qZoCjkx93XjHnA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-J6qZoCjkx93XjHnA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-J6qZoCjkx93XjHnA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-J6qZoCjkx93XjHnA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-J6qZoCjkx93XjHnA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-J6qZoCjkx93XjHnA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-J6qZoCjkx93XjHnA .marker.cross{stroke:#333333;}#mermaid-svg-J6qZoCjkx93XjHnA svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-J6qZoCjkx93XjHnA p{margin:0;}#mermaid-svg-J6qZoCjkx93XjHnA .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-J6qZoCjkx93XjHnA .cluster-label text{fill:#333;}#mermaid-svg-J6qZoCjkx93XjHnA .cluster-label span{color:#333;}#mermaid-svg-J6qZoCjkx93XjHnA .cluster-label span p{background-color:transparent;}#mermaid-svg-J6qZoCjkx93XjHnA .label text,#mermaid-svg-J6qZoCjkx93XjHnA span{fill:#333;color:#333;}#mermaid-svg-J6qZoCjkx93XjHnA .node rect,#mermaid-svg-J6qZoCjkx93XjHnA .node circle,#mermaid-svg-J6qZoCjkx93XjHnA .node ellipse,#mermaid-svg-J6qZoCjkx93XjHnA .node polygon,#mermaid-svg-J6qZoCjkx93XjHnA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-J6qZoCjkx93XjHnA .rough-node .label text,#mermaid-svg-J6qZoCjkx93XjHnA .node .label text,#mermaid-svg-J6qZoCjkx93XjHnA .image-shape .label,#mermaid-svg-J6qZoCjkx93XjHnA .icon-shape .label{text-anchor:middle;}#mermaid-svg-J6qZoCjkx93XjHnA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-J6qZoCjkx93XjHnA .rough-node .label,#mermaid-svg-J6qZoCjkx93XjHnA .node .label,#mermaid-svg-J6qZoCjkx93XjHnA .image-shape .label,#mermaid-svg-J6qZoCjkx93XjHnA .icon-shape .label{text-align:center;}#mermaid-svg-J6qZoCjkx93XjHnA .node.clickable{cursor:pointer;}#mermaid-svg-J6qZoCjkx93XjHnA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-J6qZoCjkx93XjHnA .arrowheadPath{fill:#333333;}#mermaid-svg-J6qZoCjkx93XjHnA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-J6qZoCjkx93XjHnA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-J6qZoCjkx93XjHnA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-J6qZoCjkx93XjHnA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-J6qZoCjkx93XjHnA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-J6qZoCjkx93XjHnA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-J6qZoCjkx93XjHnA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-J6qZoCjkx93XjHnA .cluster text{fill:#333;}#mermaid-svg-J6qZoCjkx93XjHnA .cluster span{color:#333;}#mermaid-svg-J6qZoCjkx93XjHnA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-J6qZoCjkx93XjHnA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-J6qZoCjkx93XjHnA rect.text{fill:none;stroke-width:0;}#mermaid-svg-J6qZoCjkx93XjHnA .icon-shape,#mermaid-svg-J6qZoCjkx93XjHnA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-J6qZoCjkx93XjHnA .icon-shape p,#mermaid-svg-J6qZoCjkx93XjHnA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-J6qZoCjkx93XjHnA .icon-shape .label rect,#mermaid-svg-J6qZoCjkx93XjHnA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-J6qZoCjkx93XjHnA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-J6qZoCjkx93XjHnA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-J6qZoCjkx93XjHnA :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
拉取视频
摄像头每 30s 一段
MinIO S3 对象存储key: cam/date/time.mp4ILM 自动过期 30-90 天
处理 Worker转码 / 识别
SQLite / LMDB 元数据路径/摄像头/时长/状态
CMDB 面板只查元数据
摄像头写 30s 片段 → MinIO(S3,key 按 摄像头/日期/时间.mp4 组织,配生命周期规则自动删 30~90 天前的片段)→ 处理 Worker 拉视频做转码 / 识别 → 结果(缩略图、检测结果)回写 → SQLite 存元数据(路径、摄像头、时长、处理状态)。面板只查元数据,绝不扫视频本体。
落地配置示例
① SQLite WAL 调优(零成本兜底,两场景通用)
— 连接初始化(各语言 SDK 都支持 PRAGMA)
PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
PRAGMA busy_timeout=5000;
— 写入:单写协程 + 每批 1000 条一个事务
— 设备端打点加 jitter:sleep(random(0, 4000))ms,错峰
② VictoriaMetrics 单节点(场景一上量方案)
# 下载单个二进制后直接运行
./victoria-metrics-prod -retentionPeriod=7 -storageDataPath=./vmdata
# 设备/网关用 InfluxDB 行协议写入(HTTP POST :8428/write)
heartbeat,device=iot-001,port=8080 status=1,latency_ms=12 1717833600000000000
# 查询最新状态(MetricsQL)
last_over_time(heartbeat{device="iot-001"}[5m])
# 趋势图(自动降采样)
count_over_time(heartbeat[1h])
③ MinIO 对象存储(场景二视频归宿)
# 单个二进制起服务
minio server /data/videos –console-address :9001
# 上传(对象 key 即元数据里的 path)
mc cp clip_20260708_120000.mp4 local/cam-01/2026/07/08/
# 元数据表(SQLite / LMDB 均可)
CREATE TABLE clip_meta (
id INTEGER PRIMARY KEY,
cam TEXT, start_ts INTEGER, end_ts INTEGER,
obj_key TEXT, size INTEGER, status TEXT
);
落地清单
- 先量后换: 上线 WAL 调优,用真实流量压 24 小时,确认 QPS / 延迟达标再决定要不要引 sidecar。
- 写入侧加缓冲: 不管选哪个库,入口都放一个内存队列 + 单写协程,把并发写变成串行批量写。
- 设备端错峰: 心跳时间戳加 ±几秒随机 jitter,消掉 :00/:30 的万级尖峰。
- 场景二绝不存视频进库: 视频只在 MinIO / 文件系统,库里只留 obj_key 字符串。
风险与权衡
老实说几个坑:
- 嵌入式 ≠ 多写者。 LMDB / RocksDB / DuckDB / libSQL / bbolt 本质都是单写者。所谓「并发」多是「多读 + 单写」或「进程内无冲突并发」。如果你真需要多个独立进程同时高频写,纯嵌入路线无解,得上单二进制服务(VM/QuestDB)或分布式层(rqlite,但写仍串行)。
- 单二进制也是要运维的。 VictoriaMetrics / MinIO 虽是一个文件,但仍是一个独立进程:要管启动、监控、磁盘、升级。好处是远比 PG/MySQL 轻——无运行时、无初始化、无账号体系。动手前先确认你的「不能装依赖」包不包含「可跑一个 sidecar」。
- 视频体量要算账。 20 路 × 90 天 ≈ 5~15 TB。确认磁盘容量和生命周期过期策略(MinIO ILM 或脚本清理),否则存储会悄悄涨满。冷数据可以迁到更大但更慢的盘。
- DuckDB 别当主库。 DuckDB 强在分析查询,多进程写入不支持。它适合做「SQLite/VM 存原始,DuckDB 做离线报表」,而不是 ingestion 主存储。
最后给个优先级:① 先 SQLite WAL 调优(零成本,两场景元数据都覆盖);② 场景一若上量,VictoriaMetrics(单二进制)+ 可选 NATS 缓冲;③ 场景二 MinIO + SQLite 元数据。全程不碰 PG/MySQL,轻量约束守住。
参考来源
- SQLite WAL 官方文档
- libSQL GitHub
- VictoriaMetrics GitHub
- DuckDB 并发模型
- MinIO 官网
- NATS JetStream 文档
- QuestDB 官网
- LMDB (Symas)
基于公开产品文档与社区实践整理(2026-07)。产品能力持续演进,落地前请以各项目最新官方文档为准。





