离线数据集引导、更新调度与就绪状态
离线数据通过统一的更新引擎完成首次下载、版本检查、校验、文件发布和内存重载。HTTP 服务先启动,再在后台补齐缺失数据;就绪门控只在引导窗口内阻挡依赖尚未就绪数据的路由,而不是把整个后端保持在不可用状态。
目的与范围
本页覆盖数据集注册契约、启动引导、定时更新与停机补检、跨进程协调、磁盘状态及路由就绪门控。重点是运营与维护时如何判断“正在加载”“下载失败”“版本未变化”和“文件已发布但重载失败”。
IP 查询、ASN 关系图、MAC 厂商查询的业务算法,以及服务状态数据自身的采集逻辑,不在本页展开。服务状态仅作为共享引导窗口的另一个参与者说明。运行环境部署、业务查询与缓存实现应分别阅读对应专题;当前上下文未提供这些目录页的实际链接。
**证据边界:**本页依据更新引擎、注册表关键片段、就绪模块与后端启动/路由代码。PeeringDB、OUI 注册行的完整实现、各读取器内部实现及测试文件未纳入本次读取,因此不推断其内部缓存交换、完整环境变量或测试保证。
概述
系统把各数据源的差异放在 datasets 注册行中,把锁、状态、下载编排、调度和文件变化检测放在公共引擎中。
| 数据集 ID | 数据用途与发布单元 | 已核实的特殊条件 |
|---|---|---|
maxmind | GeoLite2 City 与 ASN 两个数据库共同发布 | 账号与许可证都存在才启用自动获取;两份数据库都必须能打开 |
as2org | CAIDA AS 到组织名称映射 | latest 地址的 Last-Modified 用作版本;读取器可接受非固定名称的文本快照 |
as-rel | CAIDA AS 关系数据 | 从目录中的日期文件名发现候选版本;校验 provider-to-customer 记录数量 |
peeringdb | CAIDA 镜像的 PeeringDB dump,提炼为每 ASN 索引 | 注册表说明仅在有 Cloudflare key 时启用;已读校验器要求至少 20,000 个网络 |
oui | IEEE MA-L、MA-M、MA-S、IAB、CID 注册数据 | 注册表说明这些原始文件共同发布;完整校验逻辑不在已读片段中 |
来源:datasets.js、datasets.js、datasets.js。
架构
Sources: backend-server.js、dataset-updater.js、dataset-updater.js、offline-data.js。
这不是一个全局“所有数据都成功才能接流量”的屏障:runOfflineBootstrap 标记时间窗口,路由各自提供就绪探针;同一窗口内,不依赖缺失数据的路由仍可进入处理器。
启动引导与就绪语义
启动顺序
bootBackend 的实际顺序是:
- 若 MaxMind 两个目标文件均存在,先尝试
reloadMaxMindDatabases('startup'),失败在此被捕获。 - 调用
app.listen开始监听。源码注释说明 CAIDA、PeeringDB、IEEE 的已有快照在模块导入时加载;本页未独立审查这些读取器。 - **先注册
watchDatasets,再启动下载。**否则其他进程在下载期间放入的文件可能被错误地当作监听初始基线。 - 在
runOfflineBootstrap中并行执行数据集引导与bootstrapServiceStatus。 - 数据集引导后若 MaxMind 仍未就绪,再尝试一次启动重载,失败记录凭据或手工放置文件的提示。
- 所有引导步骤结束后,启动数据集调度器和服务状态轮询。
启动“完成”表示步骤已结束,不表示每份数据都可用。来源:backend-server.js。
全局窗口与每路由探针
引导窗口通过 Promise.allSettled 关闭,某一步失败不会阻止其他步骤结束,也不会永久保留门控:
1export const runOfflineBootstrap = async (steps) => {
2 booting = true;
3 try {
4 await Promise.allSettled(steps.map((step) => Promise.resolve().then(step)));
5 } finally {
6 booting = false;
7 }
8};Source: offline-data.js。
判定公式是 booting && checks.some((isReady) => !isReady())。因此:
- 引导中且当前路由所需数据未就绪:返回 503。
- 引导中但当前路由的所有探针已就绪:立即放行,不等待其他数据集。
- 引导结束但数据仍缺失:门控放行,由业务处理器决定降级或报错。放行不是成功承诺。
1export const requireOfflineData = (checks) => (req, res, next) => {
2 if (isStillLoading(...checks)) {
3 res.setHeader('Retry-After', '30');
4 return res.status(503).json({ error: 'Offline data is loading' });
5 }
6 next();
7};Source: offline-data.js。
路由接入与缓存边界
| 路由 | 引导期就绪探针 | 门控之后的缓存配置 |
|---|---|---|
/api/maxmind | isMaxMindReady | 1 天 |
/api/asn-connectivity | isAsRelLoaded、isAsOrgLoaded | 30 天 |
/api/macchecker | isOuiLoaded | 30 天 |
/api/service-status、/api/service-status/detail | isServiceStatusPrimed | 5 分钟 |
/api/asn-profile | 不挂整条路由门控 | 完整结果 7 天;降级结果引导中为 0,引导后为 1 天 |
这些就绪中间件排在各自 cacheable 之前,门控直接返回的 503 不会进入后续缓存中间件。ASN Profile 则采用部分降级策略,启动期间不缓存降级聚合结果。实际业务降级响应格式不在本页断言范围内。
单个数据集的引导决策
bootstrapDataset 首先检查 enabled(),禁用则返回 disabled。随后读取上次发布是否留下 publishing: true;若读取器已加载可用快照且没有中断标记,返回 present。
否则进入默认 5 分钟的引导更新:wait: true 表示等待其他进程释放锁;若标准发布文件均存在,则传入 force: true,防止“版本相同”掩盖文件不可用或半发布问题。更新成功返回 downloaded;若等待期间别的进程已发布同版本,则重载文件并返回 loaded。正常更新路径中的错误被转换为 failed 并记录日志。
更新事务:从远端版本到内存快照
Source: dataset-updater.js。
版本判定与校验
跳过下载必须同时满足:非强制更新、远端 identifier 为真值、所有发布文件存在、状态版本与远端相等。没有远端版本标识时每次都会抓取,而不是把未知版本当作未修改。
fetch 返回以发布文件名为键、暂存路径为值的对象;引擎会拒绝缺少任一发布文件映射的结果,随后运行数据集校验器。只有通过校验才修改正式文件。
已读校验规则包括:
- MaxMind:City 与 ASN 两份暂存文件均能由
maxmind.open打开。 - AS 组织:至少 50,000 条 ASN 能关联到组织记录。
- AS 关系:至少 100,000 条关系字段为
-1的记录。 - PeeringDB:索引
nets.size至少 20,000。
这些阈值用来识别截断或模式变化,不是数据真实性的密码学校验。来源:datasets.js、datasets.js。
发布、回滚与崩溃恢复
发布过程先把暂存文件复制到目标旁的 .next,为已有正式文件复制 .bak,然后逐个 rename。某次替换失败时,已替换文件从备份恢复;原先不存在的目标则删除。恢复动作中的错误被吞掉,所以不能把这一机制描述为数据库式强事务。
在文件变化前,引擎原子写入 identifier: null, publishing: true;全部发布完成后才写正式版本。进程中断留下标记时,下次引导不会仅凭“文件存在且读取器已加载”跳过修复。
**多文件不是一次原子切换。**原子状态文件、锁和回滚共同降低半发布风险,但多个 rename 之间仍有时间窗口。若锁丢失,引擎停止回滚与 .next/.bak 清理,避免触碰新锁持有者可能正在操作的路径。
来源:dataset-updater.js、dataset-updater.js。
磁盘状态模型
每个数据集目录使用 .dataset-state.json 存储状态,用 .dataset.lock 协调跨进程操作。
| 字段 | 含义 | 注意事项 |
|---|---|---|
identifier | 已发布远端版本 | 发布中置 null;远端未提供版本时也可能为 null |
updatedAt | 该次更新记录的发布时间 | 实际时间值在远端发现完成后生成,不是重载完成时间 |
checkedAt | 最近检查远端的时间 | 未修改时也更新,用于停机补检 |
publishing | 文件组正在变化 | 最终状态写入时不再保留;残留 true 表示需恢复 |
主状态文件不存在时才尝试兼容旧状态;主状态 JSON 损坏等非 ENOENT 错误会继续抛出。旧状态读取/转换失败则返回空状态。MaxMind 兼容 .maxmind-update-state.json,CAIDA 兼容 .caida-update-state.json。
来源:dataset-updater.js、dataset-updater.js、datasets.js、datasets.js。
调度、停机补检与外部文件重载
固定时刻更新与开关优先级
startDatasetScheduler(rows) 在进程内复用单个 scheduler。首次调用筛选既启用又允许自动更新的行;没有符合条件的行时记录关闭日志并返回 null。默认 Cron 为 30 4 * * *,按服务器本地时间运行。
配置优先级的原始实现如下,特别注意全局与旧开关对字符串的解释不同:
1export const isAutoUpdateEnabled = (row, env = process.env) => {
2 const flag = env.DATASET_AUTO_UPDATE;
3 if (flag) return flag !== 'false';
4 const legacy = row.legacyAutoUpdateEnv && env[row.legacyAutoUpdateEnv];
5 if (legacy) return legacy === 'true';
6 return true;
7};Source: dataset-updater.js。
- 非空
DATASET_AUTO_UPDATE优先;只有精确字符串false关闭。 - 全局变量未设置或为空时,读取行声明的旧开关;非空旧开关只有精确字符串
true开启。 - 两者均无有效值时默认开启。
- **关闭自动更新不等于关闭首次下载。**引导只检查数据集是否启用,不检查这个调度开关。
每轮更新顺序遍历数据集,每行默认 30 分钟超时;一行报错会记录后继续下一行。Cron 的 protect: true 防止同一 Cron 任务上一轮尚未结束时再次执行。正常定时执行由 withCronMonitor 包装为 dataset-update 检查;监控 checkinMargin 为 60,maxRuntime 为数据集数乘 30 分钟再加 10 分钟。
来源:dataset-updater.js、dataset-updater.js。
停机补检不是固定“过期天数”判断
rowsMissingACheck 用相同 Cron 规则计算最近检查之后的下一次计划时间:优先取 checkedAt,没有则取 updatedAt;时间无效、无法得到下次时间或下次时间已经到达,则加入补检列表。
启动调度器时计算一次该列表,并在 60 秒后调用 updateDatasets(stale)。补检没有包在此处的 withCronMonitor 中,也不经过 Cron 的 protect;它可能与定时轮次重叠,但各数据集的锁仍会阻止合作更新者同时发布同一目录。
监听人工或其他进程发布的文件
watchDatasets 监听所有行,包括因为缺少凭据而禁用的行,使人工投放文件仍能触发重载。
- 用每个正式文件的
mtimeMs:size拼出指纹,缺失文件记为-。 - 使用
fs.watchFile,默认每 5 秒检查;变化后等待默认 1 秒稳定窗口。 - 指纹继续变化则重新等待;本进程仍在发布同一行时也延后检查。
- 指纹与已加载版本相同,或正式文件不齐,则不重载。
- 对正式文件组执行
row.validate,失败只记录日志。 - 校验通过后调用
reloadRow(row, 'file change');只有重载成功才更新已加载指纹。
这里不是内容哈希:保留相同修改时间与文件大小的外部替换可能无法被指纹区分。监听器已读实现没有获取跨进程锁或检查 publishing,稳定等待加校验也不构成多文件的原子读取保证。人工操作应尽量一次准备完整文件组,避免长期分批放入。
来源:dataset-updater.js、dataset-updater.js。
使用与扩展示例
以下均为仓库原始片段,不是另造的调用脚本。
复用单文件压缩下载流程
注册表使用 fetchCompressed 把数据源特有的文件名和压缩格式封装成引擎所需的 fetch:
1const fetchCompressed = (file, format) => async ({ remote, tempDir, signal }) => {
2 const archive = path.join(tempDir, 'archive');
3 const staged = path.join(tempDir, file);
4 await downloadToFile(remote.url, archive, { signal });
5 await decompressFile(archive, staged, format);
6 return { [file]: staged };
7};Source: datasets.js。
多文件版本合并
MaxMind 两份数据库的远端修改时间共同形成一个版本;任一时间缺失就返回 null,从而让引擎继续下载而不是错误跳过:
export const joinedIdentifier = (lastModifieds) =>
(lastModifieds.every(Boolean) ? lastModifieds.join(' | ') : null);Source: datasets.js。
注册行契约
| 成员 | 输入/输出 | 扩展约束 |
|---|---|---|
id、dir、files | 标识、目录、相对文件名数组 | 指纹和本进程发布状态按 id 区分;锁按目录协调,应避免无意复用 |
enabled() | 可选布尔探针 | 控制引导和调度入选,不关闭文件监听 |
findRemote({ signal }) | 返回带 identifier 的远端信息 | 整个返回对象交给 fetch;未知版本可为 null |
fetch({ remote, tempDir, signal }) | 返回文件名到暂存路径的映射 | 必须覆盖 files 中每个名字 |
validate(staged) | 失败时抛错 | 同时用于下载产物与外部正式文件验证 |
reload(reason) | 重载当前正式文件 | 成功后引擎才记录本进程已加载指纹 |
isLoaded() | 可选就绪探针 | 不提供则退化为“全部文件存在”,不能发现解析失败 |
legacyState | { file, toState } | 仅主状态不存在时读取旧状态 |
legacyAutoUpdateEnv | 旧环境变量名称 | 全局自动更新配置优先 |
来源:dataset-updater.js、dataset-updater.js。
新增数据集时应同时考虑:远端版本稳定性、完整性校验、读取器就绪探针、重载失败后的旧内存保留策略,以及依赖它的路由需要整路由门控还是部分降级。后两项需在相应读取器/处理器实现中确认,不能由更新引擎自动提供。
配置参考
| 配置或参数 | 类型 | 默认值 | 行为 |
|---|---|---|---|
DATASET_AUTO_UPDATE | 环境字符串 | 未设置时回退旧开关,否则默认开启 | 非空值中仅 false 关闭 |
DATASET_UPDATE_CRON | Cron 字符串 | 30 4 * * * | 服务器本地时间;无独立时区配置读取 |
MAXMIND_AUTO_UPDATE | 环境字符串 | 未设置时由全局规则决定 | MaxMind 旧开关,仅 true 开启 |
CAIDA_AUTO_UPDATE | 环境字符串 | 同上 | 已读 as2org、as-rel 的旧开关 |
MAXMIND_ACCOUNT_ID | 环境字符串 | 无 | 与许可证同时存在才启用 MaxMind 下载 |
MAXMIND_LICENSE_KEY | 环境字符串 | 无 | 用于 HTTP Basic 认证 |
bootstrapDataset 的 timeoutMs | 数字,毫秒 | 300,000 | 单数据集引导取消计时器 |
updateDatasets 的 timeoutMs | 数字,毫秒 | 1,800,000 | 每行更新的取消信号期限 |
watchDatasets 的 intervalMs | 数字,毫秒 | 5,000 | 文件轮询间隔 |
watchDatasets 的 settleMs | 数字,毫秒 | 1,000 | 文件稳定等待间隔 |
锁心跳为 60 秒、失效阈值为 10 分钟,引导等待锁时每 2 秒重试;这些是代码常量,不是已实现的环境配置项。下载等待 HTTP 响应的超时为 30 秒,响应体通过流写盘并受调用者信号控制。
来源:dataset-updater.js、dataset-updater.js、dataset-updater.js、datasets.js、datasets.js。
核心 API 参考
源码为 JavaScript,下面保留实际参数形式,不虚构 TypeScript 类型声明。
| API | 返回与失败语义 |
|---|---|
updateDataset(row, { signal, reason = 'auto update', wait = false, force = false } = {}) | 异步返回 { updated: true, identifier } 或 { updated: false, reason: 'locked' / 'not-modified' };锁、远端、状态、校验、发布、重载错误可能拒绝 Promise |
bootstrapDataset(row, { timeoutMs = BOOTSTRAP_TIMEOUT_MS } = {}) | 异步返回 status:disabled、present、downloaded、loaded、no-op 或 failed;捕获的失败附带 error |
bootstrapDatasets(rows) | 对各行引导执行 Promise.all,得到结果数组 |
updateDatasets(rows, { timeoutMs = UPDATE_TIMEOUT_MS } = {}) | 顺序更新,逐行捕获错误;无业务结果返回;不会自行筛选启用状态 |
rowsMissingACheck(rows, { pattern, now = new Date() }) | 异步返回需补检的行数组;状态读取失败按无历史检查处理 |
startDatasetScheduler(rows) | 返回 Cron 实例,或没有可更新行时返回 null;无效 Cron 创建错误在此未捕获 |
watchDatasets(rows, { intervalMs = 5000, settleMs = 1000 } = {}) | 源码契约说明返回停止函数;已读片段未覆盖末尾清理实现 |
runOfflineBootstrap(steps) | 等待所有零参数步骤 settled,然后关闭引导窗口;无业务结果返回 |
isOfflineBootstrapping() | 返回当前 booting 布尔值 |
isStillLoading(...checks) | 仅在引导中存在未就绪探针时返回 true |
requireOfflineData(checks) | 返回 Express 中间件,门控失败响应 503,否则 next() |
来源:dataset-updater.js、offline-data.js。
故障、并发与运维注意事项
不应混淆的失败阶段
| 现象 | 源码行为 | 排查重点 |
|---|---|---|
| 首次下载失败 | 引导记录警告并返回 failed;其他步骤继续 | 凭据、远端网络、文件权限、校验阈值 |
更新提示 locked | 非等待更新直接跳过 | 其他后端或人工更新者是否持锁;不应直接删除活跃锁 |
not-modified | 更新 checkedAt,不下载、不主动重载 | 磁盘文件齐全不等于当前进程内存已加载 |
dataset files changed on disk but failed validation | 外部文件不进入重载 | 是否只复制了部分文件、源格式变化或数据截断 |
dataset reload after a file change failed | 不更新已加载指纹 | 读取器错误;不能假设有定时重试而无需新文件变化 |
| 锁心跳失败 | 记录 compromised 警告并取消该更新 | 文件系统/锁目录问题;可能残留半发布标记 |
| 主状态文件损坏 | readState 抛错,普通更新可能持续失败 | 补检把它视为无历史不等于更新路径会自动修复 JSON |
特别注意:updateDataset 在正式文件与最终状态写入后才调用 reloadRow。因此 Promise 拒绝并不总意味着“没有发布”;重载失败可能留下已更新磁盘、旧内存或未就绪读取器。应同时检查日志、状态、正式文件和具体就绪探针,而不是只看一次调用是否失败。
来源:dataset-updater.js、dataset-updater.js、dataset-updater.js、dataset-updater.js。
并发与取消的实际保证
- 首次引导通过
Promise.all并行下载不同数据集;定时更新顺序处理以限制同时工作的数据源数量。 - 目录锁保护合作更新进程;Cron 防重入只是进程内调度保护,不能替代目录锁。
- 超时是
AbortSignal协作取消,不是强制终止整个异步函数。已读的decompressFile未接收信号,校验和重载也没有信号参数,所以不应保证所有步骤严格在 5/30 分钟整点结束。 booting是一个布尔值而非引用计数;实际启动只调用一次runOfflineBootstrap,扩展时不应随意并发嵌套调用。bootstrapDataset注释称不抛出,但enabled()与早期isLoaded()在内部try之前执行;自定义探针抛错仍可能导致拒绝。注册行应保证这些同步探针稳定返回布尔值。
来源:dataset-updater.js、dataset-updater.js、dataset-updater.js、offline-data.js。
性能与容量
下载通过流式管道写文件,避免引擎一次把整个响应体放进内存;但 AS 组织与关系校验器会读取完整文本并分行处理,内存预算仍需覆盖解压后的数据。发布还需要临时下载/解压空间、目标旁 .next 和可能存在的 .bak,不能只按最终文件大小估算峰值磁盘容量。
引导期间并发下载多个数据集可能放大磁盘与网络占用;稳定运行后版本标识与 checkedAt 可减少不必要的下载和停机补检。相关依据见 dataset-updater.js、dataset-updater.js、datasets.js。
测试与验证边界
本次未读取测试文件,不宣称任何场景已被测试覆盖。源码明确提供 timeoutMs、监听间隔、补检 now 等可注入参数,并导出补检与监控配置函数,便于验证时间相关行为。维护时建议优先验证锁竞争、同版本跳过、残留发布标记、外部文件校验失败、重载失败以及引导窗口关闭后的降级行为;这些是根据实现提出的验证重点,不是现有测试清单。
相关链接
- 启动编排与数据缺失提示:定位监听、引导、监视与调度的接线位置。
- 路由就绪门控:扩展新路由的引导期行为。
- 数据集扩展契约:新增离线源时需实现的成员。
- MaxMind 注册实现:多文件版本、认证、下载、校验与重载的完整已读示例。