资讯动态

基于SQLite的轻量级分布式数据库Caveman:架构、部署与高可用实践

发布时间:2026/10/16 13:52:53 来源:尧图企业网站定制
聊个有点反主流的话题。前阵子我接了一个内部工具项目单库单表日活不过千可客户一听“SQLite”就皱眉头好像不用上分布式就不够正规。我理解大家对先进架构的执念但把整套架子搭完、养了半年之后回头看真正在扛业务的恰恰是那些看着“笨”的方案。今天想聊的Caveman就是把这个道理反过来玩的一个项目——它是个基于SQLite 3的分布式数据库用HTTP接口对外服务靠WAL日志同步多节点副本。名字听着像个玩笑实际用起来却相当能打。这篇文章会从它的设计动机、架构逻辑、部署实操、备份恢复一路讲到适用边界和踩坑清单适合那些正在做轻量级服务、内部系统、边缘节点数据汇聚或者单纯对“拿SQLite玩分布式”这件事好奇的开发者。1. 一个敢叫“穴居人”的数据库到底在解决什么问题第一次看到Caveman这个名字我以为是个复古游戏项目直到点开仓库才知道是个数据库。但这名字起得真不冤——它的定位就是“原始而可靠”剥掉现代分布式系统那层花里胡哨的外壳用最简单的东西解决高可用问题。1.1 单机SQLite的痛正好是它的起点SQLite本身是个嵌入式数据库大家普遍把它当成“移动端专用”或者“本地缓存”很少有人敢拿它当成正经服务来跑。这背后的顾虑主要有三个读写并发弱传统rollback journal模式下写操作会锁住整个库读也要等。没有网络协议应用没法直接通过端口连上去只能内嵌调用天然不适合多人共享。没有多副本数据只存在一份磁盘坏了就全没了。但SQLite有个其他数据库比不了的优势——文件即数据库备份迁移极其简单而且单机读取性能非常猛。对于读多写少的业务瓶颈往往根本不在SQLite身上而在接入方式和高可用架构上。Caveman想通的正是这一点保留SQLite作为存储引擎在外面包一层HTTP接口再通过WAL日志在多个节点之间同步同一个数据文件的状态。于是你得到的东西是——一个能通过curl执行SQL的服务同时跑在三台机器上任何一台挂了另外两台还能继续读数据还不太丢。这就解决了“单机SQLite不好用”的绝大多数痛点代价只是放弃了那种“跨机房强一致”的宏大叙事。1.2 它的使用场景和受众比想象中宽我实际用下来觉得下面这几类场景最合适内部管理后台用户量几百上千查询多、写入少需要报表页面实时查数据。边缘节点数据汇聚每台边缘机本地跑一个Caveman定时把数据同步到中心节点成本极低。原型产品快速上线一个带数据存储的服务等用户量起来了再平滑换到重型数据库。教学和实验想弄明白分布式系统到底怎样把一个单点存储变成多副本这个项目比看一万行etcd源码直观得多。反过来如果你的业务需要复杂的窗口函数、跨节点的分布式事务、极高的并发写入或者严格的强一致性——那我劝你直接去看PostgreSQL和TiDB别在Caveman上浪费时间。它就是一把瑞士军刀不是瑞士手表。2. 拆开它的架构SQLite凭什么能当分布式存储这个项目能被实际部署核心在于两层技术选型一是SQLite的WAL模式二是它自己实现的那套“环形节点互连 HTTP转发SQL”的协议。理解这两件事才算真正会用这个工具。2.1 WAL模式为什么它能做到“写者不堵读者”SQLite的默认journal模式是让写事务独占整个文件期间所有读操作全部阻塞这就显得很笨重。WALWrite-Ahead Logging预写日志模式从根本上改变了这种局面写操作不是直接修改主数据库文件而是追加写入一个单独的-wal文件。读操作仍然直接读主数据库文件遇到写入但未checkpoint的部分再顺带扫一下WAL文件即可。系统只有一个写事务其余全部是读事务读和写天然隔离。拿生活话来说传统模式相当于一间屋子只有一个门大家轮流进屋办事排队排到天荒地老。WAL模式相当于装修了一个侧门写日志的人走侧门读者走正门互不打扰。Caveman的集群同步就是把每一台机器上的“侧门”WAL文件通过HTTP广播给其他节点。这样每个节点保存的数据文件最终会收敛到同一状态但写入动作只发生在一台机器的WAL日志里。2.2 节点环到底是怎么转起来的Caveman的“分布式”结构不依赖中心协调节点它的基本组织方式是多个Caveman进程各自监听一个端口彼此知道对方的地址通过一个环状结构互连。在配置层面你只需要告诉每一个节点“集群里有哪些成员”剩下的拓扑同步是Caveman自己处理的。具体到实现有这几个关键设计Leader角色集群中一个节点被指定为同步的仲裁者所有写请求直接影响它的WAL。异步复制其他节点收到同步消息后在本地重放WAL变更。这个重放是异步的所以从节点看到的数据可能有几百毫秒的延迟。HTTP接口外部应用不直接连SQLite而是发HTTP请求给Caveman节点Caveman把SQL解析出来执行。这样的设计本质上是用“最终一致”换“实现简单”。它不保证两个节点同时读到一模一样的最新状态但它保证数据的变更会幂等地、按序地在整个集群铺开。2.3 服务发现手动配置和第三方协调可以并存Caveman支持两种集群成员发现方式手动配置启动时在命令行里直接传节点地址列表适合固定节点数的小规模部署。借助协调服务它也可以把节点注册信息塞给ZooKeeper或Hazelcast这类协调工具适合节点动态变化、需要自动选主的场景。我自己的经验是只要节点数不超过五六个直接手动配置就行少一个依赖就少一个故障点。引入ZooKeeper虽然能自动发现节点但也意味着你部署Caveman之前还得先维护一套ZooKeeper集群——这笔账怎么算都划不来。2.4 为什么这个架构对“大多数业务”足够好你必须想清楚一件事分布式系统的复杂度是跟着“强一致”要求走的。如果你要求每次写入所有节点都返回成功才给用户放行那延迟和复杂度都会直线上升。Caveman选择的是“单点写入异步广播最终一致”这在多数内部系统里完全够用因为写频率不高单点写入的瓶颈远没有触及上限。最终一致窗口很短从实测看通常在一秒以内。即使最坏情况下丢一点最近未同步的数据也远好过一个机器宕机导致整个服务不可用的局面。架构设计最忌讳好高骛远。Caveman把这个分寸拿捏得很准它用极其简单的代码路径换来了你平时根本感知不到的高可用是一笔非常划算的交易。3. 从零部署一套Caveman集群端口、节点号与那些小坑真正动手时候很多人会被文档简洁吓到。Caveman的命令行参数其实就那么几个但没有人带你过一遍很容易在“节点地址列表”上卡壳。3.1 准备环境与获取二进制Caveman是纯C实现的依赖SQLite3和其他几个基础库。部署之前先确认三件事系统环境Linux或macOS最好Windows需要用WSL或自行编译。SQLite3开发包编译时会用到头文件运行时不依赖外部SQLite服务但编译期需要libsqlite3-dev。源码获取直接clone官方仓库用make或cmake构建具体看README里的说明。我习惯在部署前把编译产物放到/usr/local/bin/下并写一个简单的systemd unit来管理进程这样机器重启后能自动拉起来。3.2 双节点启动的正确姿势假设我有两台机器内网IP分别是192.168.1.10和192.168.1.11都想监听9999端口。启动命令的核心是“节点环”的地址列表它的格式是两个成员之间用冒号分隔端口。典型命令如下# 节点A ./caveman 192.168.1.10:9999 192.168.1.11:9999 /open -p 9999 -n 1 # 节点B ./caveman 192.168.1.10:9999 192.168.1.11:9999 /open -p 9999 -n 2这里有两个关键点容易被忽略地址列表必须一致所有节点启动时传入的成员列表都要包含全部节点否则它们互相找不到。-n节点号在集群内不能重复每个节点要用唯一编号区分同步协议靠这个编号来追踪数据位置。启动完之后用自带的ping命令验证一下拓扑curl -X POST http://192.168.1.10:9999/ping如果返回的JSON里能看到节点信息说明节点间通信已经打通。不同版本的响应格式略有差异但核心是确认请求没有超时。3.3 建表、写数据和验证同步现在通过HTTP API建一张表并插入数据我习惯用curl来跑curl -X POST http://192.168.1.10:9999/db \ -d sqlcreate table if not exists users(id integer primary key, name text, age integer) curl -X POST http://192.168.1.10:9999/db \ -d sqlinsert into users(name, age) values(张三, 28)刚插入的数据在节点A上查询立刻能看到。到节点B上查就要看WAL重放的速度了curl -X POST http://192.168.1.11:9999/db \ -d sqlselect * from users我实测大多数情况下只要网络正常几百毫秒内就能看到数据。如果在业务里遇到“写完了查不到”多半是请求打到了不同的节点、同步还没追上而不是数据丢了。3.4 配置文件的选型建议Caveman也支持通过配置文件来指定节点列表、持久化路径和监听端口。相比命令行参数配置文件更便于在团队内统一管理和审阅。一个简化的YAML风格配置涉及这些核心项port节点监听端口。node_number当前节点的唯一编号。members集群全体成员的地址列表。database_pathSQLite数据库文件路径。tokenHTTP访问的认证token如果编译时开了这个功能。提示无论怎么配置我建议给每个节点配一个独立的database_path不要两个进程指向同一个SQLite文件。虽然SQLite本身能处理多进程但Caveman的同步模型假设每个节点管理自己的文件副本。部署到这里你的最小集群就算跑起来了。下一步的关键问题就是怎么保证数据不丢、以及单节点故障时到底怎么恢复——这两个问题我在实际运维中栽过跟头下面详细说。4. 数据一致性、备份与恢复的实测细节部署起来容易真正难的是面对“这台机器宕了怎么办”和“同步到底延迟多少”这类问题。这一节直接把我验证过的东西和踩过的坑讲清楚。4.1 ACID语义的边界你要做到心中有数Caveman的每个HTTP请求如果对应写操作最终都是交给SQLite以单事务方式执行的。这意味着单条写SQL的原子性是由SQLite保障的不会出现半条记录落盘的情况。但跨节点的行为要重新定义写请求只在接收节点上被事务化随后以异步方式广播到其他节点。其他节点重放时如果出错不会回滚发起节点的写操作而是在从节点本地留下一个错误状态。所以整个集群的一致性模型是“最终一致”强一致只存在于单节点内部。我有个内部系统的做法是所有写请求固定打到Leader节点读请求可以在任意节点上分摊。这样一个应用层约束就把“读到旧数据”的概率降到最低。这个技巧很土但在未来很长一段时间内都有效。4.2 备份的三种姿势别再只靠cpSQLite单机时代备份就是cp数据库文件。到了Caveman集群里因为WAL文件的存在直接cp主文件可能会漏掉最新数据。我梳理了三种安全备份方式方式命令/接口适用场景备注/save接口发送请求让SQLite把WAL checkpoint合并进主文件日常定时备份需要写权限/copy接口让Caveman生成当前节点的数据库副本文件快速迁移节点副本可直接用sqlite3打开文件系统快照LVM/Btrfs快照后再复制需要一致性备份前提是数据文件处于checkpoint后从自己运维的经验看定时任务调/copy比直接cp可靠得多。/copy会先触发WAL检查点确保主文件包含全部已提交数据避免复制出“看似可用实则有缺失”的坏备份。4.3 单节点宕机恢复的正确顺序假设节点A的磁盘坏了你需要用节点B的数据恢复出一个新节点A。操作顺序是在节点B上通过/copy拿到一份完整快照。把快照文件传到新机器的数据库路径下。新机器以原节点号-n 1启动Caveman加入集群。观察新节点是否从其他节点同步了宕机期间的增量数据。这里最容易踩的坑是新节点启动时如果本地数据库文件比集群里其他节点新它可能认为自己是“更新的那个”导致同步方向反了或触发冲突处理。所以恢复时尽量用从其他存活节点copy出来的快照而不是自己手工拼凑的文件。4.4 大事务与WAL膨胀一个容易被忽略的定时炸弹Caveman把SQLite的WAL机制当网络同步协议来用这有个副作用如果某个操作长时间不checkpointWAL文件会持续增长极端情况能把磁盘塞满。我遇到过的情况是这样的有个批量清理任务一条SQL更新了几十万行执行时间很长期间WAL不断膨胀。由于checkpoint是异步的文件一度冲到几个GB。解决办法有两个定期在Leader节点上执行PRAGMA wal_checkpoint(TRUNCATE)把WAL合并回主库并截断日志。把大更新拆成多个小批次减小单事务的WAL增长幅度。把这条写进运维手册里能少救好几次火。4.5 连接池与HTTP事务的经典坑HTTP接口本身是无状态的每个请求执行一条SQL这跟传统数据库连接池会话完全不同。坑在于如果你的一条业务逻辑需要两次写操作并且要求原子性你不能简单发两个请求因为Caveman不会把它们当作同一个事务。如果你用连接池维护到Caveman节点的长连接也要注意——连接池本身没有跨节点的路由能力写和读打在不同的节点上依然会有可见性延迟。最稳妥的做法是把多个写SQL合并成一个请求实在合并不了的再补充一套重试机制。习惯了这个模型之后你会发现它反而倒逼你把事务粒度设计得更清楚。5. 哪些场景千万别用它我的踩坑清单与边界判断任何工具都有它的天灵盖。Caveman也不例外而且它相当诚实地把边界写在脸上。这里我把真实遇到的失败场景和判断标准全部交代出来能帮你省下大量试错时间。5.1 写并发一上来单点写锁立刻暴露Caveman集群的写能力上限是“单节点SQLite的写能力”——即便你有五个节点写入还是全部汇聚到Leader的WAL锁上。一旦写并发超过SQLite的承受能力出现SQLITE_BUSY基本是意料之中的事。我的经验值是如果业务峰值写TPS超过200或者写事务经常一次更新超过万行就不要在Caveman上硬扛了转向PostgreSQL或MySQL才是正路。如果你拿不准写个小脚本压一下你的真实写模型比任何理论估算都实在。5.2 网络分区双子星脑裂的恢复有点痛前面说Caveman的同步是异步的理解到这个深度还不够。当两个节点之间的网络断开后两边都在独立接受写请求WAL各写各的等网络恢复时两边数据都产生了对方没有的变更。Caveman不会自动合并这些冲突恢复方式只能二选一牺牲一边的增量强制从另一边拉全量快照。人工导出一边的增量写脚本合并进另一边。这种痛在真正的强一致系统里是不会出现的因为它们有仲裁机制拒绝少数派落盘。所以跨机房部署Caveman或者把它用在网络不稳定的公网环境一定要慎重。最好的Caveman部署环境是同一个机房里低延迟的千兆内网。5.3 复杂查询能力被卡在了SQLite的罩门里Caveman支持的查询能力完全等于SQLite的SQL方言。没有窗口函数那是老黄历新版本SQLite已经加了不少但和PostgreSQL比还是有差距复杂的JSON操作、全文检索权重排序支持程度有限。多表连接超过三张、数据量又大性能远不如专门的OLAP引擎。并发读多了之后CPU和文件句柄的消耗会明显上升。如果你需要这些能力先确认一下自己的核心查询是否能在SQLite上简洁表达不要等项目上线再回炉。5.4 内存与文件句柄的隐性限制Caveman每个HTTP请求都会涉及SQLite语句的解析执行高并发下内存占用上升很快。此外SQLite默认打开文件数受限于系统ulimit节点数多时文件句柄吃紧也会出现莫名其妙的连接失败。我部署过一个节点本地文件句柄默认只有1024跑到60多个并发连接就开始报错。排查了很久最后无非是调ulimit -n到65535问题就消失了。任何用到HTTP服务的地方先把文件句柄数调高能省掉后面一大半烦恼。5.5 什么时候应该选Caveman什么时候应该选重型数据库我给自己画了一条简单的分界线每次选型都直接套用维度选Caveman选PostgreSQL/MySQL数据量1GB~数十GB数百GB以上核心负载读多写少读写均衡或写密集一致性要求最终一致可接受强一致必须查询复杂度简单SQL即可复杂分析、窗口函数运维能力极少专职DBA有专门团队选择Caveman的人本质上不是在挑选数据库而是在挑选“少干活、少操心、够用就行”的工程理念。这个理念没有错但前提是你真的愿意接受它的边界。6. 把它接到真实业务前我最后想啰嗦的几件事部署了几个月之后我对Caveman的总体评价是它是一个被严重低估的轻量级高可用方案但不是万能药。最重要的经验可以浓缩成三条第一写请求永远固定Leader读请求按需分发。这一个小小的应用层改动能把最终一致带来的“刚写完查不到”问题降到最低。如果你偷懒不做这层约束后面排查问题的成本会高好几倍。第二备份要当成日常任务来设计而不是出事之后才想起来。我给每个Caveman节点配了一个定时任务每小时执行一次/copy每天把快照传一份到对象存储。这个习惯救过一次急节点磁盘彻底损坏时我在十分钟内用备份文件加WAL恢复把服务拉了起来业务方完全无感。第三永远对“自动同步”保持警觉。网络分区、数据倾斜这类问题在Caveman的异步模型下不会自动治愈你需要主动加监控节点延迟、WAL文件大小、磁盘剩余空间、QPS曲线这四项指标每五分钟扫一次。最后再分享一个压测小技巧。先把你的真实读写模型抽象成脚本分别在三节点集群和单节点SQLite上各跑一遍对比延迟曲线和错误率。如果三节点的结果只比单机版本多出20%的延迟你大可以放心地上生产如果出现了明显的爬坡或抖动趁早调整同步策略或选型。这样练出来的判断力比背下任何参数表都管用。我个人在这些项目里最大的体会是**现代工程并不总是需要更复杂的系统而是需要在正确的地方用对恰当简单的工具。**Caveman这种棱角分明的项目比那些什么都能干但什么都养不动的框架更能打磨一个人对系统边界的直觉。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑