[开发]SplitDB 引擎控制台改造与智能扩容归档

👑Lv.11 元老 🌞 骨干会员
2026-10-24 00:18:42

本文由 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_SIZEShardRouter::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() 触发条件(全部满足才执行):
  1. 开关已启用;
  2. Settings::getTotalThreads() > 阈值;
  3. 距上次扩容超过 24 小时:auto_expand_last_at 存 settings 表(时间戳),time() - 上次 >= 86400 才放行,扩容成功后更新(防阈值过低频繁扩容);
  4. 防重入文件锁:真正执行写 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_index COUNT 与 reply_index COUNT(索引为全量,选"当前季度"时仅作参考对照);
  • 输出对比表(桶总数 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)、flock auto_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_ATtime()本次扩容时间戳(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 表不新增字段(归档状态以索引层为准,读取链路自动带出)。
  • 归档标记(双保险):
  1. 每日定时脚本 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;
  2. 读取时惰性标记(兜底):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 确认),按各自改动清单串行实施。合并去重后的改动文件清单:

#文件操作说明
1index.php✏️ 改后台组新增 /database/engine GET/POST 路由
2app/Controllers/Admin/DatabaseController.php✏️ 改engine() + 私有方法(引擎统计含 60s 缓存 / updateBucketSize / maybeAutoExpand 含 flock 防重入 / consistencyCheck / backupList);settings() 新增两类配置读写;P41 新增 maxBucketLoad() / maybeEmergencyExpand() / 静态 suggestExpandData();handleBackup/handleOptimize/handleRestore/handleImport 参数化跳转目标
3app/Views/admin/database/engine.php🆕 新建5 大模块视图(复用 admin.css 既有样式 + Alpine 确认弹窗;含刷新统计按钮、config 不可写置灰)
4app/Views/admin/_sidebar.php✏️ 改「数据管理」组内新增「引擎控制台」子链接
5app/SplitDB/ShardRouter.php✏️ 改新增 bucketForWrite()(扩容时间戳 + 新增桶范围路由;旧哈希兜底)
6app/SplitDB/Schema.php✏️ 改topic_index 建表加 is_archived 列;新增 ensureArchivedColumn() 存量库幂等 ALTER;可选 idx_topic_index_archive 索引
7app/Models/Thread.php / app/Models/Post.php✏️ 改新帖路由改 bucketForWrite;回复写入前桶负载检查(90% 紧急扩容 / 100% 拒绝抛异常)+ 60s 静态缓存
8app/Views/admin/database/settings.php✏️ 改新增「单桶容量」配置区块(下拉+提示语)、「归档设置」区块(180/365/自定义/关闭)
9app/Controllers/Admin/SettingsController.php + app/Views/admin/settings_basic.php✏️ 改注入并渲染建议扩容提示条(D5)
10app/Controllers/ThreadController.php / app/Controllers/PostController.php✏️ 改归档守卫(show/edit,非管理员拒绝)+ 读取时惰性标记
11app/Views/thread/show.php✏️ 改归档横幅(含"发布于 XXX 天前")+ 非管理员隐藏回复框
12cli/archive_mark.php🆕 新建每日归档标记脚本(分批 5000、幂等、--dry-run)
13cron_trigger.php / cli/worker.php✏️ 改末尾调用 maybeAutoExpand()(worker 静态 60s 节流);cron 消费流程内调归档标记
14config.php / install.php✏️ 改新增 SPLITDB_LAST_EXPANSION_AT(0) / SPLITDB_PREV_BUCKET_SIZE(32) 常量定义 + configTemplate 模板同步
15update/ 镜像同步 + 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 列保留无副作用;
  • 风险与注意事项:
  1. 桶数只增不减:界面与服务端双重限制,防止误操作导致数据错乱;
  2. 改 config 生效时机:当前请求仍用旧常量,新值从下一请求起生效(opcache 已失效)——页面提示语已明确"新增数据将自动按新规则路由";
  3. 自动扩容并发:engine 页与 worker 同时触发由独立 auto_expand.lock 兜底(写 config 前强制 flock,失败放弃),杜绝写竞争与跳级扩容;
  4. 跳级调整(如 32→128):SPLITDB_PREV_BUCKET_SIZE=32,新帖进 32..127(PREV=32 起 96 个桶)——按 PREV 到 NEW 计算,跳级同样成立;
  5. 新帖路由语义变更:扩容后新帖集中到最新一代桶范围(不再均匀分布到全部桶),自动扩容/建议扩容恰以最大桶负载为触发,形成闭环;
  6. 100% 拒绝写入的可用性影响:命中即用户无法回复该帖,提示语明确"请联系管理员扩容";
  7. 归档不可逆性:is_archived 置位后仅后台删除/恢复流程可影响,故保留 0=关闭选项防误配;
  8. I/O 开销:128/256 桶 × 多季度的分片统计与桶负载 COUNT 可能退化到 1s+,均以 60s 缓存(cache_engine_stats.json / cache_bucket_load.json)+ 手动刷新兜底;
  9. 严格权限 VPS(config.php 0444/0644):按钮置灰 + 友好提示,不静默失败;
  10. update/ 同步仅 PHP/app/cli,assets/ 不在镜像范围。

五、遗留事项

  • update/SplitDB引擎控制台使用说明.md 需随 P41 补充智能扩容/归档配置说明(单桶容量阈值、归档天数、90% 紧急扩容与 100% 拒绝行为);
  • auto_expand_threshold 旧键保留兼容,后续版本可清理;
  • 一致性检查选"当前季度"时,与索引的对照仅作参考(索引为全量计数);
  • 无 crontab 的虚拟主机环境,归档标记依赖 cron_trigger.php web 触发,存在未能每日执行的窗口(已由读取时惰性标记兜底)。

本篇由《SplitDB 引擎控制台改造计划》(P29)与《SplitDB 智能扩容与旧帖归档改造计划》(P41)合并去重整理而成,原文路径见文首 merged_from 列表。

轻量级、高性能、零 MySQL 依赖的PHP社区系统。
| 浏览 3 次 | 回复 0 次

全部回复 (0)

暂无回复

请 登录