# sqlite-server **Repository Path**: qulei123/sqlite-server ## Basic Information - **Project Name**: sqlite-server - **Description**: SQLite 数据库服务器 - **Primary Language**: Unknown - **License**: LGPL-2.1 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-18 - **Last Updated**: 2026-09-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: Sqlite, Server ## README # sqlite-server 一个在 Linux 上、用 C 手写的**客户端/服务器型 SQLite 数据库服务器**——把单个 SQLite 文件封装成一个本机服务,供多个业务进程通过 Unix 域套接字以**请求-响应** 方式访问。作为业务分离架构中的**数据库读写层**独立部署。 ## 为什么需要它(而不是让多进程直接打开库文件) SQLite 的 WAL 模式本身允许多进程直接共享一个库文件。这个 server 存在的理由不是 "共享",而是: 1. **集中访问控制**——权限落在 socket 文件上(`0600`/属组),不用把库文件本身暴露给业务进程; 2. **审计**——每个连接的对端 `uid/pid`(`SO_PEERCRED`)记入日志(journald); 3. **瘦 client**——业务进程链接 `libsipc`(几 KB)而不是整个 SQLite,协议简单到其他语言一两天即可重实现。 v1 是**显式的非目标**:引擎中立(协议就是 SQLite 方言:`err_code` 是 sqlite 错误码、 `last_insert_id` 是 rowid)。等第二个引擎真出现,让真实差异来决定抽象的形状。 ## 目录结构 ``` lib/ libcommon — 公共基础设施库(libcommon.a),模块平级,应用组装 cm_queue.{h,c} 有界阻塞队列(契约:禁止静默扩容/丢弃) cm_pool.{h,c} 固定 worker 线程组(spawn/join,局部失败降级) cm_loop.{h,c} epoll 事件循环 + 毫秒定时器 + eventfd 唤醒(fd 单一所有权规则) cm_ipc.{h,c} SEQPACKET 消息传输 + memfd/SCM_RIGHTS 大载荷旁路 cm_db.{h,c} SQLite 连接(WAL + synchronous=FULL + busy_timeout + interrupt) cm_log.{h,c} 分级线程安全日志(默认 stderr,journald 接手) cm_config.{h,c} KEY=VALUE 配置读取(与 systemd EnvironmentFile 同构) apps/ sqlite_local_server.c daemon 入口(flag > --config 文件 > 环境变量 > 默认) server_core.c server 核心 —— libcommon 的参照实现(模块如何组装) sqlite_client.c client 命令行(单条 / REPL / stdin) include/ 公共头:sipc_protocol.h(线上协议)、sipc_client.h、sipc_server.h src/ protocol.c 协议编解码 client.c client 库(libsipc 的一部分) deploy/ systemd 单元、配置模板、每日备份、安装/卸载脚本 tests/ 每模块单测 + 端到端集成测试(fork server) ``` ## 构建(需 Linux / WSL) ```sh make # 产出 sqlite-server、sqlite-client、libcommon.a make libsipc.a # 只产出可复用的静态库 make fetch-sqlite # 可选:vendor 固定版本 SQLite amalgamation ``` `CFLAGS` 默认 `-O2 -g -std=gnu11 -D_GNU_SOURCE -Wall -Wextra -Werror`。开发期建议 ASan/UBSan 再编一次(见"测试")。 ## 运行 ```sh ./sqlite-server --db /data/app.db --sock /var/run/app.sock --threads 4 # 或用配置文件(KEY=VALUE,键名与 systemd EnvironmentFile 完全一致): ./sqlite-server --config /etc/sqlite-server/sqlite-server.conf # client ./sqlite-client /var/run/app.sock "SELECT 1;" echo "CREATE TABLE t(a);" | ./sqlite-client /var/run/app.sock ``` 主要参数:`--threads N`、`--max-msg MB`(单条消息上限,默认 16 MiB,双向)、 `--shm-threshold KiB`(默认 64 KiB)、`--max-conn N`、`--idle-timeout SEC` (默认 1800,0=关)、`--query-timeout SEC`(默认关)、`--sync-normal`(默认 FULL)、 `--sock-mode`/`--sock-group`。优先级:**CLI flag > --config 文件 > 环境变量 > 默认**。 socket 文件默认权限 `0600`:谁能打开 socket 文件,谁就能访问该库。 ## Client 库用法 ```c #include "sipc_client.h" sipc_client *c = sipc_connect("/var/run/app.sock"); sipc_set_timeout(c, 30.0); /* 可选:传输超时 */ sipc_set_max_msg(c, 4 << 20); /* 可选:单消息上限(默认 256 MiB) */ sipc_response *r = sipc_query(c, "SELECT name FROM t WHERE id = ?", &(sipc_value){ .type = SIPC_INTEGER, .i64 = 42 }, 1); if (!r) /* 传输层失败(断连/超时/超限) */ fprintf(stderr, "transport: %s\n", sipc_error(c)); else if (r->status == SIPC_STATUS_ERR) /* SQL 错误,连接仍可用 */ fprintf(stderr, "sqlite(%d): %s\n", r->err_code, r->err_msg); /* 遍历 r->rows(行主序:rows[row*col_count + col]);用完 sipc_response_free(r) */ ``` 批请求(原子执行)用 `sipc_exec`。一个 `sipc_client` 句柄**非线程安全**,多线程请各建各的。 ## 语义(写 client 前必读) - **事务**:无状态。单条语句天然原子;多条语句以批发送,server 用 `BEGIN…COMMIT` 包住整体,任一句失败整体回滚。**不支持**跨请求的交互式事务。 - **`changes`**:整批所有语句累加的受影响行数。 - **`last_insert_id`**:批内**最后一条执行过的语句**的 rowid——哪怕那条是 SELECT (此时它指向更早的 INSERT)。需要确定性就用 `INSERT … RETURNING`(SQLite 3.35+)。 - **read-modify-write**:无法"读一把、算一下、再决定写"地进事务。把条件逻辑写进 单条 SQL(`UPDATE t SET v = v + 1 WHERE id = ? AND v = ?`,按 `changes` 判断成败)。 - **失败模型**:SQL 错误走错误响应、连接保活;传输层失败(断连/超时)才表现为 NULL + `sipc_error()`。结果集超过 `--max-msg` 预算 → 干净的 SQL 错误,不是断连。 ## 设计 ### 并发模型(apps/server_core.c 是参照实现) 一个 `cm_loop` 事件循环(主线程)持有全部 client fd;`cm_pool` 的 worker 各持一个 `cm_db` 连接。fd 可读 → 循环摘除注册、压入 `cm_queue` → worker 收请求/执行/回包 → `cm_loop_wake()` 唤醒循环重挂(或关闭)fd。**任意时刻一个 fd 只有一个线程在碰** (`cm_loop.h` 里写明的不变量)。并发 SELECT 并行;写由 SQLite WAL 单写者 + busy_timeout 排队。循环定时器驱动空闲回收与查询超时 watchdog(超时经 `sqlite3_interrupt` 中止,client 收到干净错误)。 ### 传输与消息 - **控制通道**:`AF_UNIX` + `SOCK_SEQPACKET`,一次 `send` 一条消息,协议无长度前缀,小端。 - **大载荷旁路**:消息超过 `--shm-threshold`(默认 64 KiB)时整条消息写入匿名 `memfd`,socket 上只传 20 字节控制头 + `SCM_RIGHTS` 传 fd。段生命周期由内核引用 计数管理——**无名字残留、client 崩溃零泄漏、权限随 fd 走**(不是文件权限)。 双向对称(大 BLOB 的 INSERT 同样受益)。SEQPACKET 单消息受 `SO_SNDBUF` 天花板 (默认 sysctl 下 ~400 KiB),阈值必须低于它——这正是旁路存在的理由。 - **预算**:`--max-msg`(默认 16 MiB)是**单条完整消息(任一方向)**的上限。请求超限 在入口拒绝;结果集在收集阶段按行预算(物化前,`zeroblob` 型攻击也不放大),超限 返回干净 SQL 错误。 - **队列契约**:`cm_queue` 有界,容量 = `max_connections + workers + 1` 一次算够; push 满则阻塞、try_push 失败,**永不静默扩容、永不静默丢弃**(这个模块的祖先 代码曾在半删扩容逻辑后静默覆写排队 fd、挂死最长等待的客户端——契约由此而来)。 ### 持久化 `journal_mode=WAL` + `synchronous=FULL`(**COMMIT 返回即已落盘**)+ `busy_timeout=5s`。 `--sync-normal`(或 conf 的 `SQLITE_SYNC_NORMAL=1`)降到 NORMAL:快,但断电可能丢 最近已提交的事务(库不损坏)。数据库层不替业务偷偷做这个决定——默认诚实。 ## 部署(systemd) ```sh sudo make install # 或 sudo bash deploy/install.sh [PREFIX] sudo systemctl start sqlite-server /usr/local/bin/sqlite-client /run/sqlite-server/app.sock 'SELECT 1;' ``` 以 `sqlitesrv` 系统用户运行,数据在 `/var/lib/sqlite-server/`,socket 在 `/run/sqlite-server/`。配置 `/etc/sqlite-server/sqlite-server.conf`(键即环境变量, 改完 `systemctl restart`)。共享访问:`SQLITE_SOCK_MODE=0660` + `SQLITE_SOCK_GROUP=组名`, 把 client 用户加进组。卸载 `sudo make uninstall`(保留数据与配置)。 ## 运维 - **备份**:安装时自动启用 `sqlite-server-backup.timer`,每天 04:17 用 sqlite3 `.backup` 在线快照到 `/var/lib/sqlite-server/backup/`(默认留 7 份, `SQLITE_BACKUP_KEEP` 可调)。恢复:停服务、拷回、启动。 - **WAL**:长读会推迟 checkpoint、`-wal` 文件变大,属正常;空闲时会自动回收。 不需要手工 checkpoint 除非 disk 紧张(`sqlite3 app.db 'PRAGMA wal_checkpoint(TRUNCATE);'`)。 - **日志**:stderr → journald。`journalctl -u sqlite-server -f` 看连接审计 (每条连接的 uid/pid)与错误。 - **VACUUM**:按需在停机窗口或低峰执行(`sqlite3 app.db 'VACUUM;'`)。 ## 测试 ```sh make test # 9 个测试二进制:协议 + 每模块单测 + 端到端集成(含并发、大 BLOB 双向、超限拒绝) ``` Sanitizer 构建(手动): ```sh make clean make CFLAGS='-O1 -g -std=gnu11 -D_GNU_SOURCE -Wall -Wextra -Iinclude -Ilib \ -fsanitize=address,undefined' LDFLAGS='-fsanitize=address,undefined' test ``` ## 非目标(v1)与 v2 开放项 - **不**做引擎中立(见上)。 - **不**做跨机器(TCP):v2 加跨设备形态时引入长度框架(TCP 是字节流,SEQPACKET 的免费消息边界不存在),且 socket 文件权限/`SO_PEERCRED` 整套安全模型届时需要 认证层替代——是 v2 的一等课题,不是顺手改。 - **不**做跨请求交互式事务(sticky session):与无状态 worker 池正面冲突。 - **不**做结果集流式;环形缓冲区模块(连续流场景)留待真实消费者出现再进库。