本文由 FlintHub 开发文档归档发布。原始文档日期见标题前缀,来源:内部文档归档。
SplitDB 引擎控制台改造与智能扩容归档
本篇由 2 篇原始文档合并去重而成:
- 《SplitDB 引擎控制台改造计划》(2026-08-16 确认,P29):新增后台页面
/admin/database/engine「SplitDB 引擎控制台」,负责分片管理监控与桶数动态调整,含引擎概览 / 桶数调整 / 自动扩容 / 备份优化 / 一致性检查五大模块;- 《SplitDB 智能扩容与旧帖归档改造计划》(2026-08-17 确认,P41,原标题"FlintHub 1.0 — SplitDB 智能扩容与旧帖归档改造计划"):在控制台基础上的智能扩容与归档策略,含最大桶负载触发、扩容路由、旧帖归档、爆火安全余量。
两篇共享的内容(写 config 四步安全流程、24h 守卫、flock
auto_expand.lock防重入、cron_trigger/worker 触发点、update/镜像同步)已合并去重,只保留一份。
一、背景与目标
- 基线:FlintHub 1.0(SplitDB 纯 SQLite 分片引擎);PHP 8.0.2(SQLite 3.33.0,无 RETURNING);数据规模 19.2 万帖 / 5 万用户。
- P29 目标:新增后台页面
/admin/database/engine「SplitDB 引擎控制台」,用于管理和监控分片状态,并支持动态调整桶数。 - P41 目标:为 SplitDB 引擎实现「智能扩容 + 扩容路由 + 旧帖归档 + 爆火安全余量」完整机制。
- 两篇共同的核心原则:旧数据零迁移、只影响新增数据——扩容与归档均不移动/改写已有数据。
二、方案设计 / 实施细则
2.1 零迁移前提、路由与权限(两篇共识)
桶数增加后旧数据原地不动,不进行任何数据迁移或重哈希:
| 场景 | 路由规则 | 依据 |
|---|---|---|
| 写入新帖 | ID % SPLITDB_BUCKET_SIZE | ShardRouter::bucketSize() 每次调用实时读常量,改 config 后下一请求即按新桶数路由 |
| 读取旧帖 | 以 main_index.sqlite 的 bucket_path 为唯一依据,不重新计算哈希 | Thread 模型 / Schema::bucketFromPath / ShardRouter::bucketFromPath,全站读取链路无二次哈希 |
- 32 → 64 桶后:旧帖继续留在旧桶,新帖进入新桶;零停机、零搬家;未来可继续 64 → 128 → 256 平滑升级;
- 不设计任何"数据重新分配 / 重哈希"脚本;
- 页面路径
/admin/database/engine(GET + POST);路由注册:index.php后台组内新增$r->get/post('/database/engine', [AdminDatabaseController::class, 'engine']); - 权限复用
BaseController构造器校验(Auth::isAdmin()+ 30 分钟二次密码验证),与既有后台页面一致。
2.2 引擎控制台页面五大模块(P29)
(1)引擎概览(只读监控)
| 项 | 实现 |
|---|---|
| 当前配置桶数 | defined('SPLITDB_BUCKET_SIZE') ? (int)SPLITDB_BUCKET_SIZE : 32 |
| 当前季度 | ShardRouter::quarter()(如 2026Q3) |
| 分片分布统计 | glob('data/bucket/active/{季度}/*.sqlite') 遍历当前季度全部桶文件 → 逐桶直接 PDO(不经 DBFactory 池,避免 LRU 8 句柄抖动;连接即置 PRAGMA query_only = true 只读句柄,读完即关)查 topic/reply 行数 → 表格(桶号 / topic 数 / reply 数 / 文件大小) |
| 统计缓存 | 60s 控制台缓存:结果落 data/runtime/cache_engine_stats.json(JSON 带 ts),避免 128/256 桶 × 多季度时每请求数百次 PDO 建连/磁盘扫描退化到 1s+;页面提供「刷新统计」按钮强制实时重算并回写缓存 |
| 当前总帖子数 | topic_index 表 SELECT COUNT(*)(经 Schema::mainIndexDb()) |
| 建议桶数 | 帖子数 <10万 → 32;10万~50万 → 64;50万~200万 → 128;>200万 → 256 |
(2)桶数动态调整(核心操作区)
- 控件:下拉框(32 / 64 / 128 / 256);
- POST 保存(
Csrf::verifyOrDie)→ intvar + 白名单in_array([32,64,128,256])→ 必须 ≥ 当前桶数(仅允许增加,禁止减少,防数据错乱); - 写 config 安全四步:① 备份
config.php→protected/backups/config.php.bak.{Ymd_His}.{随机}(沿用 install.php 模式);②preg_replace替换define('SPLITDB_BUCKET_SIZE', N);一行(保留行内注释);③file_put_contents(LOCK_EX),失败不覆盖并提示错误;④opcache_invalidate(realpath($file), true)必须传 config.php 绝对物理路径(OPcache 对相对路径可能无法精准匹配缓存键,导致旧常量残留); - 成功提示:"桶数已更新。新增数据将自动按新规则路由。旧数据不受影响。"
- 写权限前置检测:页面加载时
is_writable(config.php)结果传视图——不可写则「更新桶数」按钮置灰,并提示「配置文件 config.php 不可写,请检查文件权限」(适配严格权限 VPS 下 0444/0644 场景,不静默失败)。
(3)自动扩容设置(P29 原版,触发口径后被 P41 改为最大桶负载,见 2.3)
- 开关
auto_expand_enabled(1/0)+ 阈值auto_expand_threshold(intvar ≥ 1)→ 存 settings 表(Setting::setValue+Settings::buildCache); - 阈值输入框旁只读显示"当前总帖子数"(
Settings::getTotalThreads()实时值),方便对照设定; maybeAutoExpand()触发条件(全部满足才执行):
- 开关已启用;
Settings::getTotalThreads() > 阈值;- 距上次扩容超过 24 小时:
auto_expand_last_at存 settings 表(时间戳),time() - 上次 >= 86400才放行,扩容成功后更新(防阈值过低频繁扩容); - 防重入文件锁:真正执行写 config.php 前强制
flock('data/lock/auto_expand.lock', LOCK_EX|LOCK_NB),获取失败立即放弃(防高并发下 2 个 worker / 管理员页面同时满足条件导致 config 并发写坏或跳级扩容);
- 执行动作:桶数 ×2(上限 256)→ 走(2)的写 config 流程(同受 auto_expand.lock 保护)+
AuditLog::log记录; - 触发点(双触发):① 后台引擎控制台页加载时(GET);②
cron_trigger.php/cli/worker.php消费末尾调用——无需管理员访问也能自动扩; - 常驻进程低频节流:cli/worker 内用静态变量
$lastCheckTime,同一常驻进程距上次检查 <60s 直接跳过,避免死循环中每毫秒读 Settings 表/DB。
(4)备份与优化(维护操作)
| 功能 | 实现(复用现有私有方法) |
|---|---|
| 备份列表 | 复用 index() 的 glob 逻辑(抽成 backupList()):protected/backups/forum_backup_*.{zip,sqlite},时间倒序,显示文件名/大小/时间 |
| 立即备份 | 复用 handleBackup()(改造为可指定跳转目标 URL)→ 全 data/ 目录打包 zip + WAL checkpoint |
| 恢复备份 | 复用 handleRestore() / handleImport()(上传 .zip/.sqlite)→ 跳转回 engine 页 |
| VACUUM 优化 | 复用 handleOptimize()(全部 sqlite 库 VACUUM + WAL checkpoint) |
(5)一致性检查(诊断工具)
- 表单含检查范围下拉框:「当前季度 / 全部历史」,默认当前季度(减少历史数据全量遍历开销);当前季度仅遍历
data/bucket/active/{ShardRouter::quarter()}/.sqlite,全部历史遍历data/bucket/active//*.sqlite(全季度); - 按钮触发 POST 对拍:所选范围内逐桶 COUNT topic/reply 求和,对照
topic_indexCOUNT 与reply_indexCOUNT(索引为全量,选"当前季度"时仅作参考对照); - 输出对比表(桶总数 vs 索引记录数),不一致行红色高亮;单桶异常(表不存在等)try/catch 跳过并标注,不阻塞整体。
2.3 智能扩容(P41,触发口径语义变更)
- 新增配置项
bucket_safe_capacity(settings 表键,Setting::setValue+Settings::buildCache):单桶最大安全帖子数,选项 3 万 / 5 万 / 8 万 / 10 万(下拉框),默认 5 万; - 提示语:"该值决定每个桶在触发扩容前可容纳的主题帖数量。若服务器内存较高(如 4GB 以上),可适当调高;若在低配环境(如 600M 空间),建议保持 3~5 万。"
- 口径(D1,已确认):桶负载 = 仅 topic 行数(主题帖数),reply 行数不计入桶负载判断;
- 触发逻辑语义变更:从"总帖数 > 阈值"改为"当前桶数下数据量最大的桶的负载 ≥ 阈值(100%)→ 自动扩容";
- 开关仍用
auto_expand_enabled,触发阈值改用bucket_safe_capacity(原auto_expand_threshold总帖数阈值弃用,键保留兼容); - 其余逻辑不动:24h 守卫(
auto_expand_last_at)、flockauto_expand.lock防重入、翻倍 ≤256、AuditLog 记录; - 触发点沿用三处:后台设置页加载(GET)+
cron_trigger.php+cli/worker.php(worker 静态变量 60s 节流); - 新增私有方法
maxBucketLoad(): int:遍历data/bucket/active//.sqlite(全部活跃季度,与一致性检查全季度遍历同构),逐桶COUNT(topic)(仅主题帖,D1 口径)取最大;逐桶直接 PDO +PRAGMA query_only只读句柄(复用bucketStatsLive()模式),不经 DBFactory 池;60s 缓存结果落data/runtime/cache_bucket_load.json,避免 128/256 桶 × 多季度时每请求数百次 PDO 退化; - 建议扩容提示(80%):当
maxBucketLoad() >= 80% * bucket_safe_capacity时,在站点设置页(/admin/settings/basic)顶部显示提示条(D5):
⚠️ 建议扩容:桶 N 当前 X 帖,已达单桶容量阈值 Y 的 Z%(≥80%),可手动调整桶数或等待自动扩容。
(最大桶编号取自 maxBucketLoad 返回的明细,页面数据由 DatabaseController::suggestExpandData() 静态方法提供);
- 紧急扩容
maybeEmergencyExpand():不受auto_expand_enabled开关限制(紧急必须扩),保留 24h 守卫 + flock 防重入 + 翻倍 ≤256 + AuditLog(action=emergency_expand);与常规自动扩容共用updateBucketSize()写 config 流程; - 手动桶数调整(32/64/128/256 只增不减 + 写 config 四步流程)全部保留,与自动扩容共用同一安全流程,flock 单点持有不变。
2.4 扩容后的路由策略(P41,新帖进新桶)
扩容时间戳落盘:updateBucketSize() 写 config.php 时,在替换 SPLITDB_BUCKET_SIZE 的同时写入/更新两个新常量(同一安全流程、同一备份、同一 flock):
| 常量 | 值 | 说明 |
|---|---|---|
SPLITDB_LAST_EXPANSION_AT | time() | 本次扩容时间戳(0 = 从未扩容;新装模板默认 0) |
SPLITDB_PREV_BUCKET_SIZE | 扩容前桶数 | 新帖路由的新增桶范围计算依据 |
- 实现:正则替换两个常量行(存在则替换、不存在则追加在
SPLITDB_BUCKET_SIZE行之后),一次file_put_contents写回; - install.php 的
configTemplate()必须同步(P33 踩坑:改 config 必须同步模板,否则新装站生成旧版)——模板新增define('SPLITDB_LAST_EXPANSION_AT', 0);/define('SPLITDB_PREV_BUCKET_SIZE', 32);; - 扩容记录(audit_logs)detail 追加时间戳信息,方便追溯。
新帖写入路由:ShardRouter 新增 bucketForWrite(int $id, int $createdAt): int,Thread::insert() 改用之(ShardRouter::bucket() 保留兼容旧调用)。核心逻辑:当 SPLITDB_LAST_EXPANSION_AT > 0 且 $createdAt >= SPLITDB_LAST_EXPANSION_AT 且 0 < SPLITDB_PREV_BUCKET_SIZE < SPLITDB_BUCKET_SIZE 时,返回 SPLITDB_PREV_BUCKET_SIZE + ($id % (SPLITDB_BUCKET_SIZE - SPLITDB_PREV_BUCKET_SIZE));否则回退 id % SPLITDB_BUCKET_SIZE。
| 场景 | 路由 | 说明 |
|---|---|---|
| 新发帖(created_at=now > 扩容时间戳) | 新增桶范围 | D3(已确认):扩容后所有新主题帖全部进入新增桶范围,旧桶不再接收任何新主题帖。32→64 后新帖进桶 32..63,旧桶 0..31 冻结 |
| 历史帖导入/迁移(created_at < 扩容时间戳) | id % bucketSize | 还原历史分布语义;迁移脚本显式传 created_at |
| 从未扩容(常量 0/未定义) | id % bucketSize | 与现状完全一致,零行为变化 |
| 回复/编辑 | 跟随父帖 bucket_path | 不改,旧帖的回复和编辑天然留在旧桶 |
| 读取 | bucket_path 唯一依据 | 不改,不受路由影响 |
多次扩容累计效果:32 桶(未扩容)新帖 id % 32(桶 0..31)→ 扩容 32→64(PREV=32)新帖 32 + (id % 32)(桶 32..63)→ 再扩容 64→128(PREV=64)新帖 64 + (id % 64)(桶 64..127)。旧桶范围随扩容逐代冻结,新帖持续进入最新一代桶。
2.5 旧帖归档机制(P41,自动只读)
- 新增配置
archive_after_days(settings 表键):可选 180 / 365 / 自定义(数字输入),另加 关闭(0) 选项(D2,已确认保留防误归档全部旧帖);默认 365;提示语:"超过该天数的帖子将自动进入归档状态(只读,不可回复/编辑;管理员豁免。)" - 归档状态存储:
topic_index表新增is_archived INTEGER DEFAULT 0——Schema::ensureMainIndex()建表 SQL 增加该列(新装库);存量库幂等迁移新增ensureArchivedColumn():PRAGMA table_info(topic_index)探测缺列则ALTER TABLE topic_index ADD COLUMN is_archived INTEGER DEFAULT 0(SQLite 支持,秒级,不动存量行);可选索引idx_topic_index_archive(is_archived, create_time)加速标记脚本扫描;桶 topic 表不新增字段(归档状态以索引层为准,读取链路自动带出)。 - 归档标记(双保险):
- 每日定时脚本
cli/archive_mark.php:读archive_after_days,截止时间 =now - days*86400;分批UPDATE topic_index SET is_archived=1 WHERE is_archived=0 AND deleted_at IS NULL AND create_time < :cut,每批 LIMIT 5000(写放大硬上限),循环至 0 行;幂等可重跑,--dry-run预览影响行数;挂载点cron_trigger.php消费流程内调用(无 crontab 的虚拟主机可 web 触发),有 crontab 可独立php cli/archive_mark.php; - 读取时惰性标记(兜底):
ThreadController::show()详情页读取时,若is_archived=0且create_time < 截止时间→ 顺手单行 UPDATE 置 1(毫秒级),即使定时任务未跑,归档状态也能即时生效。
- 前台表现:
thread/show.php顶部显示归档横幅📌 该帖已归档(发布于 XXX 天前),无法回复或编辑。(XXX = floor((now-create_time)/86400));回复框隐藏:非管理员且已归档 → 渲染"该帖已归档,无法回复"空态(替换现有回复表单/无权限提示分支);内容完整可查看,引用链接可手动粘贴到新帖;列表页不隐藏归档帖(阅读不受限,仅禁止新交互)。 - 管理员豁免:
Auth::isAdmin()判定,管理员在归档帖详情页仍显示回复框、仍可编辑;服务端强制:ThreadController::show()POST 回复分支、ThreadController::edit()、PostController::edit()均加守卫——非管理员对is_archived=1帖子直接拒绝(redirect + 提示),不依赖前端隐藏。
2.6 旧帖爆火安全余量(P41)
Post::insert()(回复写入链路,已持有父帖bucket_path)在写 extern/桶/索引之前,在该桶连接(Schema::bucketFromPath)上执行SELECT COUNT(*) FROM topic(D1 口径仅 topic,不含 reply,单桶 5 万行级毫秒级);性能上加进程内静态 60s 缓存(key = bucket_path),避免高并发回复时每次 COUNT 打桶;- load ≥ 90% × bucket_safe_capacity → 触发紧急扩容(见 2.3,D4 已确认);
- load ≥ 100% × bucket_safe_capacity → 拒绝写入:抛专用异常(
RuntimeException带 code),ThreadController::show()catch 后 redirect 回帖子页并提示「当前桶已满,请联系管理员扩容」(回复内容不丢失——编辑器在错误提示后保留原文,用户可复制); - 说明:扩容只改变新帖去向,旧桶本身不迁移——90% 紧急扩容的意义是立即扩大后续新帖的落桶范围、为旧桶 100% 拒绝前的窗口争取时间;100% 拒绝写入是最终防线;
- 桶负载检查与紧急扩容全部 try/catch:计数失败放行写入(不因检查故障误伤正常回复),仅记录 error_log。
三、落地情况 / 进度
两篇均为「已确认」计划(P29 于 2026-08-16 确认;P41 按 D1~D5 修正后于 2026-08-17 确认),按各自改动清单串行实施。合并去重后的改动文件清单:
| # | 文件 | 操作 | 说明 |
|---|---|---|---|
| 1 | index.php | ✏️ 改 | 后台组新增 /database/engine GET/POST 路由 |
| 2 | app/Controllers/Admin/DatabaseController.php | ✏️ 改 | engine() + 私有方法(引擎统计含 60s 缓存 / updateBucketSize / maybeAutoExpand 含 flock 防重入 / consistencyCheck / backupList);settings() 新增两类配置读写;P41 新增 maxBucketLoad() / maybeEmergencyExpand() / 静态 suggestExpandData();handleBackup/handleOptimize/handleRestore/handleImport 参数化跳转目标 |
| 3 | app/Views/admin/database/engine.php | 🆕 新建 | 5 大模块视图(复用 admin.css 既有样式 + Alpine 确认弹窗;含刷新统计按钮、config 不可写置灰) |
| 4 | app/Views/admin/_sidebar.php | ✏️ 改 | 「数据管理」组内新增「引擎控制台」子链接 |
| 5 | app/SplitDB/ShardRouter.php | ✏️ 改 | 新增 bucketForWrite()(扩容时间戳 + 新增桶范围路由;旧哈希兜底) |
| 6 | app/SplitDB/Schema.php | ✏️ 改 | topic_index 建表加 is_archived 列;新增 ensureArchivedColumn() 存量库幂等 ALTER;可选 idx_topic_index_archive 索引 |
| 7 | app/Models/Thread.php / app/Models/Post.php | ✏️ 改 | 新帖路由改 bucketForWrite;回复写入前桶负载检查(90% 紧急扩容 / 100% 拒绝抛异常)+ 60s 静态缓存 |
| 8 | app/Views/admin/database/settings.php | ✏️ 改 | 新增「单桶容量」配置区块(下拉+提示语)、「归档设置」区块(180/365/自定义/关闭) |
| 9 | app/Controllers/Admin/SettingsController.php + app/Views/admin/settings_basic.php | ✏️ 改 | 注入并渲染建议扩容提示条(D5) |
| 10 | app/Controllers/ThreadController.php / app/Controllers/PostController.php | ✏️ 改 | 归档守卫(show/edit,非管理员拒绝)+ 读取时惰性标记 |
| 11 | app/Views/thread/show.php | ✏️ 改 | 归档横幅(含"发布于 XXX 天前")+ 非管理员隐藏回复框 |
| 12 | cli/archive_mark.php | 🆕 新建 | 每日归档标记脚本(分批 5000、幂等、--dry-run) |
| 13 | cron_trigger.php / cli/worker.php | ✏️ 改 | 末尾调用 maybeAutoExpand()(worker 静态 60s 节流);cron 消费流程内调归档标记 |
| 14 | config.php / install.php | ✏️ 改 | 新增 SPLITDB_LAST_EXPANSION_AT(0) / SPLITDB_PREV_BUCKET_SIZE(32) 常量定义 + configTemplate 模板同步 |
| 15 | update/ 镜像同步 + update/CHANGELOG.md + update/SplitDB引擎控制台使用说明.md | 📦 同步 | 上述 PHP/app/cli 全部 fc /b 逐字节一致;CHANGELOG 记 P29 / P41;使用说明为维护用交付文档 |
配套验证(两篇合计):php -l 全过;临时冒烟脚本(用后即删)覆盖:config 正则替换往返(含两个新常量一次写回、备份存在、opcache 路径)、桶统计缓存命中/过期、一致性对拍(当前季度/全部历史两种范围)、自动扩容阈值触发、24h 小时间隔守卫、flock 防重入(双进程仅一方成功)、config 不可写分支(chmod 0444 后按钮置灰)、bucketForWrite 路由矩阵(未扩容/新帖进新增范围/历史 created_at 回退/常量缺失兜底)、maxBucketLoad 跨季度取值、归档标记分批 5000 上限/幂等/--dry-run/惰性单行标记、90% 触发紧急扩容与 100% 拒绝抛异常、存量库幂等 ALTER(无 is_archived 列的库加列,存量行默认 0);smoke_install 断言更新(is_archived 列存在)。
四、影响面与回滚
- 影响面:仅影响新增数据路由与索引层归档标记;存量数据零迁移(扩容仅写 config 常量;归档仅
ALTER TABLE ADD COLUMN默认 0 + 索引行置位,桶/extern 数据不动);读取链路零改动;回复跟随父帖桶、编辑原地写;备份恢复 / VACUUM / 一致性检查零改动; - 回滚:config.php 每次写入前有
.bak备份,可回退常量(含两个新常量);bucketForWrite常量缺失/非法/PREV≥NEW 全部分支回退旧哈希,绝不抛异常中断发帖;归档只需将archive_after_days置 0 关闭;is_archived列保留无副作用; - 风险与注意事项:
- 桶数只增不减:界面与服务端双重限制,防止误操作导致数据错乱;
- 改 config 生效时机:当前请求仍用旧常量,新值从下一请求起生效(opcache 已失效)——页面提示语已明确"新增数据将自动按新规则路由";
- 自动扩容并发:engine 页与 worker 同时触发由独立
auto_expand.lock兜底(写 config 前强制 flock,失败放弃),杜绝写竞争与跳级扩容; - 跳级调整(如 32→128):
SPLITDB_PREV_BUCKET_SIZE=32,新帖进 32..127(PREV=32 起 96 个桶)——按 PREV 到 NEW 计算,跳级同样成立; - 新帖路由语义变更:扩容后新帖集中到最新一代桶范围(不再均匀分布到全部桶),自动扩容/建议扩容恰以最大桶负载为触发,形成闭环;
- 100% 拒绝写入的可用性影响:命中即用户无法回复该帖,提示语明确"请联系管理员扩容";
- 归档不可逆性:
is_archived置位后仅后台删除/恢复流程可影响,故保留 0=关闭选项防误配; - I/O 开销:128/256 桶 × 多季度的分片统计与桶负载 COUNT 可能退化到 1s+,均以 60s 缓存(cache_engine_stats.json / cache_bucket_load.json)+ 手动刷新兜底;
- 严格权限 VPS(config.php 0444/0644):按钮置灰 + 友好提示,不静默失败;
update/同步仅 PHP/app/cli,assets/不在镜像范围。
五、遗留事项
update/SplitDB引擎控制台使用说明.md需随 P41 补充智能扩容/归档配置说明(单桶容量阈值、归档天数、90% 紧急扩容与 100% 拒绝行为);auto_expand_threshold旧键保留兼容,后续版本可清理;- 一致性检查选"当前季度"时,与索引的对照仅作参考(索引为全量计数);
- 无 crontab 的虚拟主机环境,归档标记依赖
cron_trigger.phpweb 触发,存在未能每日执行的窗口(已由读取时惰性标记兜底)。
本篇由《SplitDB 引擎控制台改造计划》(P29)与《SplitDB 智能扩容与旧帖归档改造计划》(P41)合并去重整理而成,原文路径见文首 merged_from 列表。