首页/新闻资讯/正文详情

EmDash 404 日志上限优化解析:仅在插入新路径时执行 `MAX_404_LOG_ROWS` 上限校验

发布时间:2026/9/23 11:53:16 来源:云帆数科 栏目:资讯中心
EmDash 404 日志上限优化解析:仅在插入新路径时执行 `MAX_404_LOG_ROWS` 上限校验
EmDash 404 日志上限优化解析仅在插入新路径时执行MAX_404_LOG_ROWS上限校验【免费下载链接】emdashEmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress项目地址: https://gitcode.com/gh_mirrors/emdas/emdash导读本文围绕 EmDash 开源 CMS 中一次典型的性能回归修复展开log404之前会在每一次404 命中后都执行全表COUNT(*)来校验_emdash_404_log表是否超出MAX_404_LOG_ROWS10,000 行上限即使该路径只是重复命中也会白白扫描整张表。本次修复将上限校验收敛为仅在新唯一路径首次插入时触发从而显著降低站点在大量重复 404 场景下的 D1 行读取量。读完本文你将理解这一优化背后的数据模型、原子 upsert 实现、容量驱逐策略以及配套的回归测试是如何用 SQL 日志断言来验证重复命中不再 COUNT这一行为的。变更内容概览变更记录位于 .changeset/skip-404-cap-on-updates.md属于emdash: patch级别的补丁核心改动一句话即可概括修复 404 日志记录仅在插入新的唯一路径时强制执行MAX_404_LOG_ROWS上限重复命中跳过全表COUNT(*)显著减少为大量重复 404 服务的站点的 D1 行读取。在展开源码细节之前先交代三个关键背景知识_emdash_404_log表的结构、log404的去重机制以及MAX_404_LOG_ROWS上限的来源。_emdash_404_log表结构与常量定义建表语句_emdash_404_log表在 packages/core/src/database/migrations/029_redirects.ts 中创建其结构如下await db.schema .createTable(_emdash_404_log) .addColumn(id, text, (col) col.primaryKey()) .addColumn(path, text, (col) col.notNull()) .addColumn(referrer, text) .addColumn(user_agent, text) .addColumn(ip, text) .addColumn(created_at, text, (col) col.defaultTo(currentTimestamp(db))) .execute(); await db.schema.createIndex(idx_404_log_path).on(_emdash_404_log).column(path).execute(); await db.schema .createIndex(idx_404_log_created) .on(_emdash_404_log) .column(created_at) .execute();关键点path上有唯一索引idx_404_log_path这正是后面原子 upsert 依赖的约束基础referrer、user_agent、ip均为可空文本列后续迁移为表增加了hits、last_seen_at列见log404的 upsert 语句用于去重统计。上限与截断常量在 packages/core/src/database/repositories/redirect.ts 中定义了三组核心常量/** Hard cap on rows stored in _emdash_404_log. When exceeded, the oldest * rows (by last_seen_at) are evicted on insert. Prevents an unauthenticated * attacker from growing the table without bound by requesting unique URLs. */ export const MAX_404_LOG_ROWS 10_000; /** Max stored length for the Referer header — truncated on insert. */ export const REFERRER_MAX_LENGTH 512; /** Max stored length for the User-Agent header — truncated on insert. */ export const USER_AGENT_MAX_LENGTH 256;MAX_404_LOG_ROWS 10_000表行数硬上限超过后按last_seen_at升序驱逐最老记录防止未认证攻击者通过请求海量唯一 URL 无限撑大表REFERRER_MAX_LENGTH 512与USER_AGENT_MAX_LENGTH 256恶意客户端无法通过超大请求头撑爆存储。配套的截断工具函数truncateOrNull会保留null/undefined为null、空字符串保持为空串由调用方决定是否进一步归一化function truncateOrNull(value: string | null | undefined, max: number): string | null { if (value null || value undefined) return null; return value.length max ? value.slice(0, max) : value; }log404的调用链公共中间件中的发后即忘log404并非直接暴露给用户而是由公共重定向中间件在响应阶段调用。见 packages/core/src/astro/middleware/redirect.ts// No redirect matched -- proceed and check for 404 const response await next(); // Log misses (fire-and-forget) under the path the visitor requested. const location response.headers.get(location); const missedByRedirect isRedirectCode(response.status) (location /404 || location /404/); const missedDirectly response.status 404 pathname ! /404 pathname ! /404/; if (missedDirectly || missedByRedirect) { const referrer context.request.headers.get(referer) ?? null; const userAgent context.request.headers.get(user-agent) ?? null; repo .log404({ path: pathname, referrer, userAgent, }) .catch(() {}); }这里有两个值得注意的设计发后即忘fire-and-forgetlog404(...).catch(() {})不阻塞响应。源码注释明确说明——这个方法必须永远不对未认证调用方抛异常失败会冒泡到中间件并被吞掉见 redirect.ts 注释。两种 404 形态都会记录直接返回 404 的未匹配路由以及模板中常见的内容缺失时重定向到/404模式此时 missed path 只存在于浏览器跟随重定向之前的第一次请求中。错误页本身/404路径不会被记录因为它按设计返回 404 且不携带路径信息。从调用链可见一个站点 404 越多log404被调用的频率就越高如果每次都做全表COUNT(*)代价会被放大到难以接受——这正是本次优化要解决的问题。修复前的问题重复命中也要 COUNT在优化之前log404的逻辑大致是每次 upsert 之后无条件调用enforce404Cap()而enforce404Cap的第一步就是对整张表执行COUNT(*)private async enforce404Cap(): Promisevoid { const countRow await this.db .selectFrom(_emdash_404_log) .select((eb) eb.fn.countAllnumber().as(c)) .executeTakeFirst(); const count Number(countRow?.c ?? 0); if (count MAX_404_LOG_ROWS) return; // ...evict oldest rows... }问题在于重复命中的路径走的是ON CONFLICT DO UPDATE分支只更新已有行的hits与last_seen_at不会增加表行数。此时再执行全表COUNT(*)属于完全无效的工作——表行数根本没有变化。对于服务大量重复 404 的站点例如爬虫反复请求同一批失效链接每次命中都白扫一遍全表D1 行读取量随 404 流量线性增长既不产生任何收益又抬高了数据库成本。修复后的实现用返回值区分插入与更新修复后的log404在 packages/core/src/database/repositories/redirect.ts 中通过RETURNING id与自生成 id 的比较精确区分本次操作是新插入还是更新已有行async log404(entry: { path: string; referrer?: string | null; userAgent?: string | null; ip?: string | null; }): Promisevoid { const now new Date().toISOString(); const referrer truncateOrNull(entry.referrer, REFERRER_MAX_LENGTH); const userAgent truncateOrNull(entry.userAgent, USER_AGENT_MAX_LENGTH); const ip entry.ip ?? null; const id ulid(); // Atomic upsert by path. The UNIQUE index on path makes this safe // under concurrency: two requests for the same new path cant both // insert — the second one hits the conflict branch and increments // hits instead of failing with a uniqueness error. const result await this.db .insertInto(_emdash_404_log) .values({ id, path: entry.path, referrer, user_agent: userAgent, ip, hits: 1, last_seen_at: now, created_at: now, }) .onConflict((oc) oc.column(path).doUpdateSet({ hits: sqlhits 1, last_seen_at: now, referrer, user_agent: userAgent, ip, }), ) .returning(id) .executeTakeFirst(); // The conflict branch only updates existing rows, so repeat hits // cannot grow the table. Only enforce the row cap when we actually // inserted a new path. if (result?.id ! id) return; await this.enforce404Cap(); }关键机制拆解1. 原子 upsertON CONFLICT DO UPDATE整个写入是单条 SQL 语句完成的原子操作。path上的唯一索引保证并发安全两个请求同时命中同一个新路径时第一个执行 INSERT第二个进入冲突分支执行 UPDATEhits 1而不是抛出唯一性冲突错误。注释中特别指出这是对早期SELECT 后 INSERT/UPDATE竞态问题的回归修复——旧实现下两个并发调用都可能先 SELECT 不到记录随后第二个 INSERT 因唯一索引报错。2.RETURNING id与自生成 id 的判别新插入时返回的id就是本次生成的那个ulid()因此result?.id id继续执行enforce404Cap()冲突更新时返回的是已有行的id与本次生成的id不同result?.id ! id立即return跳过上限校验。3. 只读成本被压到最低重复命中的路径在数据库侧只发生一次带索引的 UPDATEWHERE path ?或等价的主键定位外加一次网络往返不再触碰任何全表聚合。这就是变更记录中所说的significantly reducing D1 row reads显著减少 D1 行读取的实现机理。容量驱逐enforce404Cap的边界处理当插入确实让表行数超过上限时enforce404Cap负责清理。其完整实现见 redirect.tsprivate async enforce404Cap(): Promisevoid { const countRow await this.db .selectFrom(_emdash_404_log) .select((eb) eb.fn.countAllnumber().as(c)) .executeTakeFirst(); const count Number(countRow?.c ?? 0); if (count MAX_404_LOG_ROWS) return; const excess count - MAX_404_LOG_ROWS; // Evict the oldest rows in a single SQL statement. Using a subquery // (rather than materialising the victim IDs in JS and passing them // back as bind parameters) keeps the statement bounded regardless of // how far over cap the table is — important for existing installs // that crossed the threshold before this cap was introduced. await this.db .deleteFrom(_emdash_404_log) .where( id, in, this.db .selectFrom(_emdash_404_log) .select(id) .orderBy(last_seen_at, asc) .orderBy(id, asc) .limit(excess), ) .execute(); }实现要点驱逐顺序按last_seen_at升序辅以id升序作为稳定次序选出最旧的excess行并删除因此持续被命中的路径即使首次出现很早也会保留在表中——冷路径被淘汰热路径被保留这与hits去重语义天然自洽。单语句删除使用子查询内联选择受害行 id而非在 JS 中物化 id 列表后再回传绑定参数。这样无论表超限多少SQL 语句大小都是有界的对在上限引入之前就已经超限的存量安装同样成立。何时会被调用enforce404Cap是私有方法注释明确仅由log404在新路径插入后调用。修复后它在重复命中路径上不再被触发。回归测试用 SQL 日志断言不 COUNT本次修复最有力的证据来自集成测试 packages/core/tests/integration/redirects/log404-bounded.test.ts。该测试文件标题直指unbounded 404 logging DoS无界 404 日志的 DoS回归验证了四类行为按路径去重连续三次log404({ path: /missing })后表内只有 1 行hits 3请求头截断10,000 字符的超大Referer/User-Agent被截断到REFERRER_MAX_LENGTH/USER_AGENT_MAX_LENGTH且null不会被强转成空字符串容量驱逐直接向表灌入MAX_404_LOG_ROWS行分批 500 行插入以规避 SQLite 每语句绑定参数限制随后插入新路径/brand-new断言表行数仍为上限、最老的seed-000000被删除、新路径存在并补充验证容量已满时重复命中已有路径只 bumphits不驱逐任何行并发原子性10 个并发log404({ path: /race })结束后恰好 1 行、hits 10证明 upsert 无丢失更新、无唯一性异常。其中与本次变更最直接相关的是最后一个用例only enforces the row cap on a new unique path, not on repeat hitslog404-bounded.test.ts。它用 Kysely 的查询日志钩子捕获了执行过的全部 SQLconst loggedDb new KyselyDatabase({ dialect: new SqliteDialect({ database: openNodeSqliteDatabase(:memory:) }), log(event) { if (event.level query) { captured.push(event.query.sql); } }, });随后分别统计插入与更新路径时count(*)风格 SQL 的出现次数captured.length 0; await loggedRepo.log404({ path: /new-path }); const countAfterInsert captured.filter((sql) /count\s*\(\s*\*\s*\)/i.test(sql)).length; expect(countAfterInsert).toBe(1); captured.length 0; await loggedRepo.log404({ path: /new-path }); const countAfterUpdate captured.filter((sql) /count\s*\(\s*\*\s*\)/i.test(sql)).length; expect(countAfterUpdate).toBe(0);断言语义一目了然新路径首次插入时COUNT(*)恰好执行 1 次同一路径重复命中时COUNT(*)执行 0 次。这正是skip-404-cap-on-updates这个 changeset 名称的技术注脚——用可执行测试锁死了行为契约防止未来重构把全表 COUNT 悄悄加回来。收益分析与适用前提收益量化设某站点 404 流量中重复路径的占比为r0 ≤ r 1每次 404 的 D1 行读取量近似为修复前每次命中 ≈ 1upsert 全表扫描COUNT(*) 修复后新路径命中 ≈ 1upsert 全表扫描COUNT(*)重复命中 ≈ 1UPDATE当站点由爬虫或失效外链驱动、r趋近于 1 时例如仅几个失效路径被高频请求修复后 D1 行读取量可降低一个量级以上。需要强调的是COUNT(*)是全表聚合其成本随表行数增长即使表始终远低于上限比如只有几百行重复命中时跳过 COUNT 依然节省了 404 流量 × 表大小 数量级的无效读取。适用前提与限制前提一表必须存在path唯一索引原子 upsert 才能成立。该索引在迁移 029_redirects.ts 中建立前提二enforce404Cap的驱逐语义是按last_seen_at驱逐最旧热路径优先保留。若运营者期望的是按首次出现时间created_at驱逐则需要另行调整限制修复只优化了重复命中这一主路径的读放大。新路径首次插入时仍执行一次全表COUNT(*)对于持续涌入海量全新 URL的攻击型流量读成本依然存在——MAX_404_LOG_ROWS上限机制本身才是防无界增长的最终防线本次优化解决的是正常业务场景下的无谓开销兼容性属于patch级别变更不改变log404的对外签名与行为语义去重、截断、驱逐均不变存量部署可直接升级。小结本次 changeset 是一个典型的小改动、大收益案例通过在原子 upsert 中利用RETURNING id与自生成 id 的差异将MAX_404_LOG_ROWS上限校验从每次命中必执行收紧为仅新路径插入时执行配合单语句有界驱逐与 SQL 日志断言测试同时保证了性能、安全与可回归性。对于需要高频记录 404、又担心 D1 读配额被重复命中烧光的 EmDash 站点这一实现思路同样可以迁移到其他去重计数 容量上限场景。关键代码速查关注点位置changeset 声明.changeset/skip-404-cap-on-updates.md常量与log404/enforce404Cap实现packages/core/src/database/repositories/redirect.ts中间件调用链fire-and-forgetpackages/core/src/astro/middleware/redirect.ts_emdash_404_log建表与索引packages/core/src/database/migrations/029_redirects.ts有界日志回归测试含 COUNT 断言packages/core/tests/integration/redirects/log404-bounded.test.ts【免费下载链接】emdashEmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress项目地址: https://gitcode.com/gh_mirrors/emdas/emdash创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关推荐

dsp调音软件入门到精通:版本升级API全变?老手教你3步搞定
dsp调音软件入门到精通:版本升级API全变?老手教你3步搞定

dsp调音软件入门到精通:版本升级API全变?老手教你3步搞定 昨晚加急上线音频处理模块,我盯着屏幕上的 NullPointerException 发呆。刚把 DSP 调音软件库从 2.4 升到 3.0,原本跑得好好的 setGain… · 2026/9/23 11:53:16

3步搞定开开源码:手写实现防升级踩坑指南
3步搞定开开源码:手写实现防升级踩坑指南

3步搞定开开源码:手写实现防升级踩坑指南 版本升级后 API 全变了,这种痛谁懂?很多开发者在接手老旧项目或更新依赖时,常面临“文档滞后、接口变更、底层逻辑黑盒”的三重困境。与其被动等待官方补丁,不如主动出击,通过 手写实现… · 2026/9/23 11:53:04

Webamp Track 类型详解:掌握播放列表曲目的完整数据结构与实战用法
Webamp Track 类型详解:掌握播放列表曲目的完整数据结构与实战用法

Webamp Track 类型详解:掌握播放列表曲目的完整数据结构与实战用法 【免费下载链接】webamp Winamp 2 reimplemented for the browser 项目地址: https://gitcode.com/gh_mirrors/we/webamp 在 Webamp 中,许多实例方法(如 initialTrac… · 2026/9/23 11:53:04

PyTorch Sampler完全指南:从原理到实战,解决类别不均衡与分布式训练
PyTorch Sampler完全指南:从原理到实战,解决类别不均衡与分布式训练

1. 为什么每个PyTorch新手都会在Sampler上栽跟头1.1 一次"数据顺序错乱"事故的排查全过程前阵子帮一个朋友调试训练脚本,现象非常诡异:同一个模型、同一份数据,在A机器上跑得好好的,换到B机器上loss曲线就开始抖动&… · 2026/9/23 12:39:20

SCM供应商管理全生命周期:从准入到退出的闭环实战指南
SCM供应商管理全生命周期:从准入到退出的闭环实战指南

既然聊到SCM,供应商管理是怎么也绕不开的一块。这两年被问得最多的问题里,“SCM供应商管理怎么做”一定排前三。很多人觉得供应商管理就是找货源、压价格、催交期,结果真出了事——供应商突然断供、质量事故频发、账期和交付对不上——才反应… · 2026/9/23 12:39:20

MCP协议从入门到精通:LLM与Agent工具调用标准化实践指南
MCP协议从入门到精通:LLM与Agent工具调用标准化实践指南

1. 为什么MCP值得你花时间,又为什么很多人半路就放弃了MCP这个词在最近一年里出现的频率高得离谱。如果你在开发者社区、技术群或者各种工具文档里频繁看到它,却又说不清它到底解决了什么问题,那你不是一个人。我身边不少朋友的状态是&#x… · 2026/9/23 12:39:20

JEDEC标准族实战指南:DDR5、UFS与JESD22兼容性验证
JEDEC标准族实战指南:DDR5、UFS与JESD22兼容性验证

简介:JEDEC标准族是电子元器件领域的工业标准合集,面向从事元器件可靠性设计、测试与质量验证的工程师及研究人员,帮助其系统查阅环境应力与电应力试验方法。资源包内含1个doc文档,约60KB,以文字条目形式整理JESD22系列… · 2026/9/23 12:39:20

火灾烟雾图像标注数据集实战:从格式清洗到YOLOv8部署调优
火灾烟雾图像标注数据集实战:从格式清洗到YOLOv8部署调优

简介:火灾烟雾图像标注数据集是一份面向目标检测方向的计算机视觉资源,包含2257张火灾与烟雾相关图像,可帮助研究人员和开发者训练、优化火灾和烟雾识别模型,解决安全场景中早期火情定位与预警问题。压缩包体积约266.14MB&#xf… · 2026/9/23 12:39:13

从一天10-20元起步:普通人可落地的网赚副业实操指南
从一天10-20元起步:普通人可落地的网赚副业实操指南

1. 为什么把目标定为一天10-20元:先算清这笔账1.1 一天10-20元的真实含义:单位时间产出率很多人一听到"网赚"两个字,第一反应是月入过万、日入几百的暴富故事。但说实话,那些故事要么是卖课的引流钩子,要么是… · 2026/9/23 12:39:13

3招搞定手机怎么下载微信面试难题实战项目解析
3招搞定手机怎么下载微信面试难题实战项目解析

3招搞定手机怎么下载微信面试难题实战项目解析 面试被问“手机怎么下载微信”背后的原理,90%的人答不上来。别笑,这看似弱智的问题,实则是考察你对移动应用分发机制、安全校验及网络协议理解的试金石。我带过不少校招新人,他们背了八股文,却连一个A… · 2026/9/23 0:00:03

你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型
你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型

你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 面试被问“高并发下如何保证消息不丢失”,你张口就是“用Redis”,结果面试官追问“如果Redis宕机了怎么办”,你瞬间卡壳。这种场景太常见了,很多新手在背八股文时,只记住了技术名词… · 2026/9/23 0:00:29

Win7无线热点配置工具源码解析:解决API失效的3个实战技巧
Win7无线热点配置工具源码解析:解决API失效的3个实战技巧

Win7无线热点配置工具源码解析:解决API失效的3个实战技巧 Win7无线热点配置工具在Win10/11上跑不动?不是你的问题,是版本升级后 API 全变了。很多老项目里的 netsh wlan… · 2026/9/23 0:00:36

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码