一句话
100 万+ 内容页,依然秒开。
古书屋(gushuwu.com)是一个刚上线的公版古籍站:站内已发布内容 1,054,521 篇、全文合计 12.17 亿字、36,217 位作者,另有 139,319 条繁体译文。在整页缓存命中时,首页、分类页与内容页都在 20–30 毫秒量级返回。
本文所有数字都来自实测:规模数字取自站点数据库的 SQL,性能数字取自对源站的现场重测。每个数字的口径与取数命令见下方「口径与取数」。 ⚠️ 一个重要前提:本文写作时,该站正在并行导入内容(实测约 100 条/秒)。所以「内容行总数」「全文总量」「关联数」这类累计值在持续增长——下表是 2026-09-11 15:21 UTC 的快照,不是长期常量。相对的,分类规模(诗 843,621 / 词 37,304 / 古籍 12,049 …)在这一时刻是稳定的。
站点简介
古书屋(gushuwu.com)做的是公版古籍的阅读与检索——把经史子集、历代诗文、字典韵书整理成人可读、机器可检的形态,全部内容为公版文献。
内容版图(顶层三条内容轴 + 一条工具轴):
- 诗轴:诗总目 843,684 条,下含楚辞(63)
- 词轴:词 37,304 + 曲 34,705 = 72,009 条
- 古籍轴:书目 12,049 部,下含四部(经 2,639 / 史 3,199 / 子 4,026 / 集 1,109)、另编(道教部 83 / 佛教部 5 / 小说部 8 / 丛书部 3)与文类(蒙学 9 / 文言文 531 / 散文 218 / 随笔 219)
- 工具轴:名句 5,684、成語 30,906、部首 214、甲骨文 6,361、古籍導航 502,以及字典三书(漢語字典 14,810 / 康熙字典 46,814 / 說文解字 9,833)
阅读与检索形态:
- 服务端繁简双语:简体为默认,全站有 139,319 条繁体版本,走
/tw/…独立 URL(服务端渲染,不是浏览器端转换) - 多入口检索:全文检索、书名拼音索引(
/pinyin.html)、作者三轴索引(朝代 35,481 位 / 拼音 36,217 位 / 部首 184 部、35,697 位)、分类与标签总目、年月归档 - 阅读工具:内容页「▶ 朗读」(在线合成 + 逐句高亮跟读)、阅读轨迹与划线笔记、收藏书架
- 校勘公示:校勘类目 57 部 / 57 条校勘条目
规模数据
取数时间:2026-09-11 15:21 UTC(北京时间 23:21),数据库 gushu_stage,快照前后 4 秒内取完。
| 指标 | 数值 | 口径 | 此刻是否在变 |
|---|---|---|---|
| 内容行总数 | 1,054,539 | ye_contents 全部行(COUNT(*)) | 在增长(约 100 行/秒) |
| 已发布内容 | 1,054,521 | type='post' AND status='publish' | 在增长 |
| 独立页面 | 15 | type='page' AND status='publish'(10 个专题 + 关于 / 隐私 / 版权 + 作者索引 + 拼音索引) | 稳定 |
| 全文总量 | 1,217,087,332 字(12.17 亿) | SUM(words),限 post + publish + 未删除 | 在增长 |
| 作者 | 36,217 | 侧表 ye_gushu_authors 行数 | 稳定 |
| 繁体译文 | 139,319 | ye_poly_contents 中 lang='tw' AND status='publish' | 稳定 |
| 分类 / 标签 | 72 / 38,598 | ye_metas 按 type 分组 | 标签在增长 |
| 分类标签关联 | 3,134,868 | ye_relationships 行数 | 在增长 |
| 诗文篇目 | 915,693 | 诗轴 843,684 + 词轴 72,009 | 稳定 |
| · 诗 | 843,621 | 分类「诗与未分类诗文」 | 稳定 |
| · 楚辞 | 63 | 诗轴下级 | 稳定 |
| · 词 | 37,304 | — | 稳定 |
| · 曲 | 34,705 | 词轴下级 | 稳定 |
| 古籍(书目) | 12,049 | 古籍轴:本级 + 一级子类之和 | 稳定 |
| · 经部 / 史部 / 子部 / 集部 | 2,639 / 3,199 / 4,026 / 1,109 | 四部分类 | 稳定 |
| 字典三书 | 71,457 | 康熙字典 46,814 + 漢語字典 14,810 + 說文解字 9,833 | 稳定 |
| 名句 | 5,684 | 名句类目 | 稳定 |
| 成語 | 30,906 | 成語类目 | 稳定 |
| 部首 | 214 | 部首类目 | 稳定 |
| 甲骨文 | 6,361 | 甲骨文类目 | 稳定 |
| 古籍導航 | 502 | 导航类目 | 稳定 |
| 校勘 | 57 | 校勘类目(另有 57 条校勘条目) | 稳定 |
| 专题 | 10 | 另有 10 个子专题各 1 部 | 稳定 |
| 蒙学 / 文言文 / 散文 / 随笔 | 9 / 531 / 218 / 219 | 文类 | 稳定 |
| 数据库体积 | 9,518.7 MB | information_schema:数据 8,777.3 MB + 索引 741.4 MB | 缓慢增长 |
| 数据目录磁盘占用 | 10 GB | du -sh 数据目录 gushu_stage | 缓慢增长 |
| 站点目录 | 333 MB | 其中 uploads/ 占 110 MB | 稳定 |
| Sitemap | 27 个分片 + 1 个索引,共 1,240,531 条 URL | 每片 ≤ 5 万条 / ≤ 50 MB(静态文件,尚未随导入重建) | 待重建 |
口径说明:站点的规模数字有两套——分类维度读的是ye_metas.count(随导入线维护的去规范化计数列),内容维度是COUNT(*)。本次逐一用COUNT(DISTINCT content_id)对分类数字做了复核,并排除了「分类计数是旧值」的可能。
站点自己显示的数字,与库内实测逐个对账
古书屋首页有一条统计条。把它显示的六个数字,与同一时刻用 SQL 查出来的值并列:
| 首页统计条显示 | 同刻库内实测 | 对应取数 |
|---|---|---|
| 11,045 部 四部典籍 | 11,045 | COUNT(DISTINCT content_id),限 8 个部类 slug(经部 / 史部 / 子部 / 集部 / 道教部 / 佛教部 / 小说部 / 丛书部) |
| 915,693 篇 詩文篇目 | 915,693 | 诗轴 843,684 + 词轴 72,009 |
| 36,217 位 作者小傳 | 36,217 | SELECT COUNT(*) FROM ye_gushu_authors |
| 12.16 億字 全文總量 | 12.17 億(1,217,087,332 字) | SUM(words),限 post + publish + 未删除 |
| 139,319 條 繁體可讀 | 139,319 | ye_poly_contents 中 lang='tw' AND status='publish' |
| 5,584 部 存目待補 | 5,584 | 标签 cunmu 下的 COUNT(DISTINCT content_id) |
6 项里 5 项逐位一致,第 4 项差 0.01 亿——这一项对不上的原因已经查清,而且不是数据错误:站点首页的统计条结果有 1 小时缓存,而该站此刻正在以约 100 条/秒导入内容。也就是说,统计条显示的是「缓存生成那一刻」的 12.16 亿,而我现在实时查出来是 12.17 亿。缓存过期重算后就会追上。
这里正是「口径要先写清楚」的意义:最后一项「存目待补」指的是只存目录、没有正文的那 5,584 部书(站点把它们和「有全文」的 12,049 部分开计)。如果只看数字,很容易把它误读成「待补的书有多少」。
性能实测
所有请求从源站本机发起(curl --resolve gushuwu.com:443:127.0.0.1),绕开 Cloudflare,所以下表不含公网与 CDN 的传输时间——量的是站点自身的渲染与缓存成本。热/冷由响应头 X-Cache: HIT / MISS 判定,不靠推断。
| 页型(路径) | 热态:整页缓存命中 | 冷态:清空整页缓存后的首次请求 | 走整页缓存 |
|---|---|---|---|
首页 / | 0.012 – 0.027 s | 0.061 s | 是 |
分类页 /category/诗/ | 0.012 – 0.035 s | 0.250 s | 是 |
分类页 /category/jiaokan/ | 0.020 – 0.023 s | 0.302 s | 是 |
内容页 /lun-yu.html(论语) | 0.017 – 0.025 s | 0.517 s | 是 |
内容页 /shi-jing.html(诗经) | 0.016 – 0.028 s | 未单独取到 | 是 |
繁体页 /tw/lun-yu.html | 0.015 – 0.026 s | 0.548 s | 是 |
年月归档 /archives/2026/08/ | 0.017 – 0.030 s | 0.407 s | 是 |
书名拼音索引 /pinyin.html | 0.021 – 0.044 s | 0.047 s | 是 |
全文搜索 /search?q=古 | 0.058 – 0.065 s | 0.446 s | 是 |
阅读器 /reader | 0.045 – 0.152 s | 0.018 s | 否(插件页) |
专题 /topics | 0.023 – 0.084 s | 未单独取到 | 否(插件页) |
校勘公示 /corrections | 0.066 – 0.087 s | 0.105 s | 否(插件页) |
作者索引 /author.html | 1.046 – 1.131 s | 未单独取到 | 否 |
在线朗诵 /tts?text=… | 0.025 s(磁盘缓存命中) | 1.368 s(首次合成) | 否(二进制端点) |
测量条件(请连着数字一起看):
- 取数时间:2026-09-11 14:39–15:10 UTC(北京时间 22:39–23:10)
- 请求间隔 0.8 s——该站 nginx 有
limit_req zone=gushuwu_dyn rate=6r/s,不留间隔会被 503 干扰 - 「冷态」= 清空
var/cache/pages后的首次请求。此时站点的统计缓存仍是热的,所以这不是「整机重启后的第一次」——真正的整机冷启动会更慢 - 测量期间同一台机器上还有其它任务在跑,1 分钟负载在 1.8 – 11.7 之间波动:热态给的是多次测量的区间(最小值对并发队列最不敏感),冷态取的是负载 1.8–5.5 那一段的实测值
- 作为对照:当机器负载升到 12 时,同一批请求里连整页缓存命中都会排队到 5–14 秒。那是机器被占满,不是渲染路径的成本——所以我把热态按区间给出,而不是挑一次最好看的数
两处如实记录的波动与短板:
- 繁体内容页
/tw/lun-yu.html的首次渲染曾观测到 12.0 s(那一刻它的辅助缓存也全是冷的),紧接着重测是 0.55 s,之后还有一次 7.7 s 的离群值。这一页的冷态波动是全站最大的,我没有把它从数据里拿掉。 - 作者索引
/author.html(36,217 位作者的三轴总目)稳定在 1.05 s 以上,是全站最慢的页型,目前还没有做针对性优化。
用到的 YeCha 能力
| 能力 | 在古书屋上的用法 |
|---|---|
| 全页缓存 | 公开页 GET 走文件整页缓存,命中即直出;文档里带 X-Cache: HIT / MISS 响应头,可直接用 curl 断言 |
| 服务端繁简双语(Poly 插件) | 139,319 条繁体译文成组存于 ye_poly_contents,前台以 /tw/… 独立 URL 服务端渲染;不是前端 JS 转换 |
| 双主题皮肤 + 访客切换 | gushu 主题内置 A 書林 / B 丹鉛 两套皮肤 × 明暗,右下角控件即时切换、写入 cookie 全站继承 |
| 阅读器插件 | 阅读轨迹(可「继续阅读」回到上次位置)、正文划线 + 笔记、收藏书架;未登录存本机、登录后并集同步 |
| 在线朗诵 | /tts?text=… 走 Edge TTS 合成,本地磁盘缓存 + LRU 上限;前端逐句高亮跟读、逐字注音可关 |
| Sitemap 分片 | 1,240,531 条 URL 切成 27 片,每片 ≤ 5 万条,索引页 /sitemap.xml 直出静态文件 |
| 插件机制 | 站内共装 11 个插件:poly(多语言)、reader、recite、zhconv(繁简转换)、zidian(字典)、jiaokan(校勘)、gushuauthors(作者库)、autosave、subscribe、links、market |
| 归档与计数 | 年月归档(/archives/2026/08/)与自然日去重的阅读量计数 |
三个技术要点
三个都是「问题 → 做法 → 实测收益」的结构。
要点 1:两步取 id,消除跨表 filesort
问题。 繁体列表原先一条 SQL 写完:
FROM ye_poly_contents p JOIN ye_contents c ON c.id = p.group_id
WHERE p.lang = ? AND p.status = 'publish' AND ...
ORDER BY c.published_at DESC, p.id DESC LIMIT n排序键跨两张表,MySQL 无法用索引排序,只能对 13.9 万行 join 结果做 filesort;而 SELECT 列表里还带着正文 LONGTEXT(全站 3.2 GB),filesort 阶段还要把正文一起物化。实测后果:
/tw/<slug>.html:单条 SQL 1,689 ms(当时页面 1.71 s)/tw/category/<部>/:COUNT 6,831 ms,随后的 SELECT 撞上max_execution_time=8000被杀 → HTTP 500/tw/archives/:GROUP BY 被 8 s 上限杀掉 → HTTP 500/tw/page/2/:同样 500
做法。 统一改成两步:① 只用窄列(id / published_at)走索引拿到本页所需的 id 集合;② 只按这批 id 回表取要展示的列,在很小的结果集里再排序。分页 OFFSET 语义不变(先按 id 过滤再 OFFSET),候选窗口不足时自动放大重试。
实测收益。 原先被 8 秒上限杀掉、直接 500 的 /tw/archives/ 冷查询,降到亚秒级。按项目内 core/Front.php 的补丁记录,两条路径返回的 id 序列已实测逐行相同——所以这是纯性能改动,没有语义变化。(这一条一致性结论来自项目自身的补丁记录,不是本轮我对页面的重测。)
要点 2:列表页不拖正文,字数去规范化成一列
问题。 列表页其实只需要「标题 + 摘要 + 字数」,但原始写法会把整列正文一起取回来。更隐蔽的一处:主题在展示字数时,如果行里没有 words 列,就会回退去正文里用正则数字数——一篇 16 MB 的正文跑一次正则是秒级操作。这正是 /tw/archives/2026/08/ 要 3.4 s 的真凶。
做法。 两条一起改:
words(字数)在入库时就算好并持久化进ye_contents.words列,列表页直接读列,不再数正文;- 列表页回表时显式排除
content列(用information_schema拼出「除正文外的全部列」),正文根本不出现在结果集里。
实测收益。 20 行的回表从 906 ms → 1 ms(实测样本:某一页 20 条正文合计 35.6 MB);繁体归档页从 3.4 s → 0.2 s。
要点 3:窗口化 + 密度守卫
问题。 分类 / 标签这类术语页,如果一律从「关联表」出发(relationships 按 meta_id 驱动),对超大型术语就要对 9 万+ 命中行做跨表 filesort:「唐」有 95,003 条关联、「诗」有 105,378 条。实测 /tag/dyn-1/ 278 ms,全冷时 1.1–2.5 s。
做法。 按术语规模分两条路:
- 小术语(命中 ≤ 15,000):继续用关联表驱动 + 跨表排序——命中行少,filesort 很便宜;
- 大术语:改成「窗口化」——先按
(published_at DESC, id DESC)从内容表取一个候选窗口(初始 = 本页所需条数 × 8,上限 8,192),再只在这个窗口里按归属过滤。窗口是全局有序序列的前缀,所以结果一定正确;窗口内命中不够本页时按 8 倍放大重试。
另加一条密度守卫:先用「术语命中数 ÷ 站内总数」估算这页需要多大的窗口,如果所需窗口超过 4,096(也就是至少要重试一轮),就不再白试——因为重试那一步的 IN (最多 8192 个 id) + ORDER BY + OFFSET 实测要 0.4–2 s(例如「宋」在第 2000 条处实测 1,895 ms),而直接走关联表只要 245 ms。此时径直走关联表。
实测收益。 大术语页从「对 9 万+ 行 filesort」变成「在千级窗口里过滤」;密度守卫那一条,把注定要重试的深翻页从 1,895 ms 拉回 245 ms 的路径。守卫只决定要不要去试窗口,不改变任何返回数据——按项目内 core/Front.php 的补丁记录,两条路径的 id 序列已实测逐行相同;拿不到总数或参数异常时一律不拦、保持原行为。(同上,一致性结论来自项目补丁记录。)
顺带修掉的一处。 首页统计条要用「全站已发布内容总数」,原写法每次 COUNT(*) 实测 519.8 ms(走覆盖索引全扫 132 万行)。改成一个带 1 小时缓存的总数方法后,这 519.8 ms 变成约 0。
边界:还没做完的事
一个案例页如果只讲成绩就不值得看,所以这里如实列出已知的口径边界与未完成项:
- 体裁细分还粗。「诗」这一轴目前是 843,621 条,其中混着「未分类诗文」——诗、赋、骈文、杂文暂时没有拆开。按朝代、按体裁的细分还只覆盖了常见朝代。
- 民国及以后的诗文口径待定。目前站内绝大部分内容落在
published_at = 2026-08(导入日期),不是作品的真实年代;真实年代存在标签(如「朝代:宋」)而不是发布时间的维度里。所以「按年月归档」在这站上反映的是入库时间,不是文学史时间——这一点在页面上没有被伪装成后者。 - 作者只在「已系年」的范围内完整。36,217 位作者里有 736 位未系年,这部分先按拼音 / 部首走。
- 繁体是「可读」而不是「已校」。139,319 条繁体为机器转换结果,尚未逐条人工校对;站点把它标注为「繁體可讀」而没有说成「定本」。
- Sitemap 与内容的条数有微小差:繁体分片合计 139,316 条,
ye_poly_contents里lang='tw' AND status='publish'是 139,319 条,差 3 条——分片生成器与计数口径存在未查明的 3 条差异,已如实保留、未做抹平。 - 性能数字的测量条件:本轮实测在源站本机(
127.0.0.1,绕开 Cloudflare)完成,测量期间机器上还有其它任务在跑,负载见下方实测表。冷态数字是「清掉整页缓存后的首次请求」,热点会被同时段的其它负载放大——同一 URL 重复测会出现明显波动,这是同一台机器上的真实情况,没有挑最好看的那一次。 - 数据是有时间戳的快照,而且写的时候还在变。上表的取数时间是 2026-09-11 15:21 UTC;那一刻该站正在并行导入内容(实测约 100 条/秒),所以「内容行总数 / 已发布内容 / 全文总量 / 关联数 / 标签数」这类累计值会继续增长。它们是那一刻的真实值,不是长期常量——本文没有把它们写成「截至目前为止」。
- 站点统计条与实时 SQL 有 0.01 亿的差,原因是统计条有 1 小时缓存(详见上文对账小节)。这是缓存行为,不是数据错误;但读者若在不同时刻两边对照,会看到这个差。
- 本文写作期间,站点的作者数据搬了一次家:36,217 行作者页从
ye_contents迁到了侧表ye_gushu_authors。「内容行总数」因此一度由 1,079,206 变为 1,042,989(差额恰为 36,217)。内容一条没少,只是换了存放位置;本文采用搬迁后的口径(作者数直接报侧表行数)。
一句话总结:这套系统的能力边界是「把百万级公版文本稳定地读出来、检出来」;它还不是一个已经做完的学术整理平台,体裁细分、系年、繁体校勘都还有明确的工作量。
口径与取数
以下是本文数字对应的取数命令(在源站执行,gushu_stage 为只读来源):
-- 内容行数 / 已发布内容 / 独立页面
SELECT COUNT(*) FROM ye_contents;
SELECT COUNT(*) FROM ye_contents WHERE type='post' AND status='publish';
SELECT COUNT(*) FROM ye_contents WHERE type='page' AND status='publish';
-- 全文总量(post + publish + 未删除)
SELECT SUM(words) FROM ye_contents
WHERE type='post' AND status='publish' AND deleted_at IS NULL;
-- 作者 / 繁体译文 / 关联
SELECT COUNT(*) FROM ye_gushu_authors;
SELECT COUNT(*) FROM ye_poly_contents WHERE lang='tw' AND status='publish';
SELECT COUNT(*) FROM ye_relationships;
-- 分类与标签规模 / 分类计数复核
SELECT type, COUNT(*) FROM ye_metas GROUP BY type;
SELECT COUNT(DISTINCT r.content_id) FROM ye_relationships r
JOIN ye_metas m ON m.id = r.meta_id
WHERE m.type='category' AND m.slug='诗';
-- 数据库体积(数据 + 索引)
SELECT ROUND(SUM(data_length+index_length)/1024/1024,1) FROM information_schema.tables
WHERE table_schema='gushu_stage';# 磁盘占用
du -sh /www/server/data/gushu_stage
du -sh /www/wwwroot/xuecha-yecha-stage
# Sitemap:分片文件数 / URL 总条数
ls -1 sitemap*.xml | wc -l
cat sitemap-*.xml | grep -c '<loc>'
# 性能:热态(读整页缓存)与冷态(强制渲染,不读不写整页缓存)
curl -k --resolve gushuwu.com:443:127.0.0.1 -D - -o /dev/null \
-w '%{http_code} %{time_total}\n' https://gushuwu.com/
curl -k --resolve gushuwu.com:443:127.0.0.1 -H 'X-YC-NOCACHE: 1' -D - -o /dev/null \
-w '%{http_code} %{time_total}\n' https://gushuwu.com/