拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Open Library Coverstore 封面归档全解:从 localdisk 到 archive.org 的存储布局、归档流程与源码实现

Open Library Coverstore 封面归档全解:从 localdisk 到 archive.org 的存储布局、归档流程与源码实现
  • 后端
  • 前端
  • 搜索引擎

【免费下载链接】openlibrary

One webpage for every book ever published!

项目地址:https://gitcode.com/gh_mirrors/op/openlibrary
点击查看免费下载

Open Library 的 Coverstore 是一个独立的封面仓储服务:负责接收封面上传、生成多尺寸缩略图、维护封面元数据,并在本地磁盘即将写满时把封面文件批量"归档"(打包压缩)后上传到 archive.org 的 item 中,由 CDN 就近分发。本文以 openlibrary/coverstore/README.md 为主线,结合 archive.py、code.py、coverlib.py 与 schema.sql 等源码,完整讲清封面编号的分层命名规则、归档触发的批量流程、数据库状态机,以及服务端如何借助 tar 索引(index)与 zip 路径回源 archive.org。读完你既能照着 Recipe 手动跑一次归档,也能读懂 Coverstore 每行核心逻辑的底层依据。

封面归档到哪里:archive.org 上的三层存储布局

README 的第一节直接回答了"封面归档后放在哪"。截止文档记载的时间点,归档封面分布在 archive.org 的如下 item 中:

  • 封面 ID 0 ~ 7,139,999:以zip文件形式存储在olcovers1~olcovers713这些 item 中,这些 item 归属于 archive.org 的ol_exports集合;
  • 封面 ID 8,000,000 ~ 8,819,999:以tar文件形式存储在covers_0008这个 item 中;
  • 封面 ID 8,820,000 ~ 8,829,999:以zip文件形式同样存放在covers_0008item 中。

注意中间的7,140,000 ~ 7,999,999区间既不在旧 zip item 中、也不在新 tar item 中——这正是后文要讲到的"归档历史欠账"留下的空洞:归档自 2014 年中断,恢复归档时直接从上界 8M 起跳,重新从covers_0008起步。

从当前源码看,归档工具 archive.py 的Uploader.upload()在向 archive.org 上传时固定写入如下元数据:

md = { "title": "Open Library Cover Archive", "mediatype": "data", "collection": ["ol_data", "ol_exports"], }

也就是新归档产生的 item 会同时进入ol_data与ol_exports两个集合(见Uploader.upload(),archive.py)。

Coverstore 的工作流:上传 → localdisk → 归档 → archive.org

README 的 "How it works" 一节描述了整个服务的运转方式,结合源码可以还原出完整链路。

1. 上传与缩略图生成:写入 localdisk

新封面上传到 Open Library 后,save_image()(coverlib.py)负责落盘:按YYYY/MM/DD/目录层级写入 config.data_root 下的localdisk目录,文件名形如YYYY/MM/DD/{olid}-{随机5字符}.jpg(见make_path_prefix(),coverlib.py)。原图之外还会按config.image_sizes生成 S/M/L 三种缩略图:

image_engine = "pil" image_sizes = {"S": (116, 58), "M": (180, 360), "L": (500, 500)}

(config.py)。缩略图由resize_image()用 PIL 的 LANCZOS 重采样生成,保持纵横比、quality=90(coverlib.py)。

2. 数据库登记:cover 表

每张封面(及其三个尺寸变体)都会在cover表中登记一条记录(db.new(),db.py),初始时archived=false、uploaded=false、deleted=false。表结构见 schema.sql:cover表包含filename、filename_s、filename_m、filename_l四个文件名列(对应原图与 S/M/L),以及failed、archived、uploaded、deleted四个布尔状态列;另有category表(books/authors/works 三类,对应 code.py 中的CoverCategory = Literal["a", "b", "w"])和用于审计的log表。

3. 归档:打包并更新路径

随着 localdisk 逐渐写满,在某个"合适的定期间隔"触发归档:封面文件被压缩打包进 tar/zip 归档,移动到data_root/items/下的 "staging item" 文件夹(如covers_0007),同时archive.py更新数据库中这些封面文件的路径引用(README "How it works")。

README 推测的封面查找逻辑,在源码中可以得到印证:服务端查库拿到 filename,若是 tar/zip 归档,则先看本地items/目录下是否有同名 staging item;没有则假定该 staging item 已作为同名 archive.org item 上传,转而回源 archive.org 解析请求。find_image_path()(coverlib.py)与read_file()(coverlib.py)就是这一策略的实现:文件名含:时按路径:offset:size从 tar 中定点读取,否则回退本地磁盘。

封面 ID 的分层命名方案:10 位编号切 4 / 2 / 4

README 用一个非常具体的例子解释了 item 名称的由来:covers_0007是前缀covers加上web.numify("%010d.jpg" % cover.id)[:4]。以cover.id = 7315539为例,%010d将其左补零成 10 位0007315539,取前 4 位得到0007,因此归属covers_0007。同理,ID 低于 1,000,000 的封面进covers_0000,之后每 1M 进下一个 item。该方案理论上可容纳接近 100 亿张封面。

README 还引述了 2022-12-03 Anand 的说明:"封面 ID 视为 10 位数字,前 4 位归入 item,接下来 2 位归入 tar 文件,剩余 4 位是文件名。"

当前源码把这一规则实现为Cover.id_to_item_and_batch_id()(archive.py),并配套两个常量:

ITEM_SIZE = 1_000_000 BATCH_SIZE = 10_000
>>> Cover.id_to_item_and_batch_id(987_654_321) ('0987', '65')

即:百万位 → 4 位 item_id,十万位 → 2 位 batch_id。于是归档路径呈现为covers_0008/covers_0008_87.zip、s_covers_0008/s_covers_0008_87.zip这种{尺寸前缀}covers_{item}_{batch}.zip的形态(见Batch.get_relpath(),archive.py,以及 test_archive.py 中的对应断言)。

README 中那条"经典"的数据库查询结果,正好演示了 tar 路径的:offset:size格式:

coverstore=# select id, olid, filename, last_modified from cover where archived=true order by id desc limit 1; id | olid | filename | last_modified ---------+-------------+--------------------------------------+---------------------------- 7315539 | OL25645665M | covers_0007_31.tar:1849729536:247493 | 2014-11-29 22:34:37.329315

covers_0007_31.tar:1849729536:247493的意思是:该封面位于covers_0007item 的covers_0007_31.tar中,在 tar 内的字节偏移 1849729536、大小 247493。服务端get_tar_filename()(code.py)正是按f"{prefix}_{name[:4]}_{name[4:6]}.tar:{offset}:{imgsize}"拼接这类路径。

配置与数据库:coverstore.yml 与 schema.sql

配置加载入口是 server.py 的load_config()——用yaml.safe_load读入配置文件后逐键setattr到 config.py 模块。仓库自带的 conf/coverstore.yml 展示了最小配置:

db_parameters: dbn: "postgres" db: "coverstore" host: db data_root: "/var/lib/coverstore" default_image: "static/images/empty.gif" sentry: enabled: false dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' traces_sample_rate: 1.0 environment: 'local'

其中db_parameters是 web.py 数据库连接参数(db.getdb()据此建立到 coverstore PostgreSQL 库的连接,db.py);data_root是本地磁盘根目录(README 中生产环境为/1/var/lib/openlibrary/coverstore/,配置中则是/var/lib/coverstore),localdisk/存新封面、items/存 staging item;default_image是兜底占位图,_serve_default()(code.py)在找不到封面时按default参数返回占位图、302 重定向或 404。

手动运行封面归档

README 给出了手动归档的标准操作:

ssh -A ol-covers0 docker exec -it openlibrary_covers_1 bash

然后在容器内启动 Python 终端执行:

from openlibrary.coverstore import config from openlibrary.coverstore.server import load_config from openlibrary.coverstore import archive load_config("/olsystem/etc/coverstore.yml") archive.archive(test=False)

其中load_config()负责把 coverstore.yml 读入全局 config;archive.archive()是归档主函数。若只需试跑不落库,可用archive.archive(test=True)之类的 dry-run 模式——实际上脚本入口main()(archive.py)支持--dry-run参数,会先执行archive()再以test=dry_run调用Batch.process_pending()做上传与收尾演练。

归档历史状态与 2022 年的告警

README 记录了 2022-11 时点的关键告警信息:

  • ol-covers0上有5,692,598 张未归档封面,本地磁盘开始吃紧;
  • 归档实际已停滞:最后一次成功归档是2014-11-29(上表那条记录);
  • 正因积压量巨大,直接执行旧版archive()查询全部未归档封面时,超过 5 分钟仍无响应(挂起)。

因此 README 建议给"查询未归档封面"的 SQL 加上批次上限(例如 1000),并显式指定起点id(即 2014-11-29 最后一次成功归档的 ID):

covers = _db.select('cover', where='archived=$f and id>6708293', order='id', vars={'f': False}, limit=1000)

这个"分批限流"的思路如今已在源码层面落地:archive()的函数签名变为archive(limit=None, start_id=None, end_id=None)(archive.py),内部通过CoverDB.get_unarchived_covers(limit)或get_batch_unarchived(start_id, end_id)拉取——get_covers()(archive.py)在指定start_id时会自动算出批次结束 ID(_get_batch_end_id,取整到BATCH_SIZE边界),避免再发生全表扫挂起。

此外 README 特别说明:虽然 2014-11-29 之前也识别到未归档封面,但早期归档流程可能尚未标准化,因此团队决定以最后一次成功归档日期为基准恢复归档。

实战 Recipe:一次 10k 封面的归档

README 给出了"每次把一个 10k 批次移入 archive.org tar"的完整配方,是运维该服务的核心操作手册,完整保留如下(路径前缀items/均指data_root/items/):

  1. 在 ol-covers0 的 docker 容器内运行 archive.py,从稳定 ID 8M 起点开始打包约 10k 封面,生成新批次(如covers_0008_00):

    from openlibrary.coverstore import config from openlibrary.coverstore.server import load_config from openlibrary.coverstore import archive load_config("/olsystem/etc/coverstore.yml") archive.archive(test=False)
  2. 用ia upload把各个尺寸的产物分别上传到 4 个 item:

    • covers_0008→covers_0008_00.index和covers_0008_00.tar
    • s_covers_0008→s_covers_0008_00.index和s_covers_0008_00.tar
    • m_covers_0008→m_covers_0008_00.index和m_covers_0008_00.tar
    • l_covers_0008→l_covers_0008_00.index和l_covers_0008_00.tar
  3. 更新 code.py 约 L290 处的上界值 +10k(在 ol-covers0 的容器 1 与容器 2 上)并重启:

    if (8100000 > int(value) >= 8000000):
  4. 重启容器并测试,确保服务对全部尺寸都能解析到 archive.org。

  5. 删除已完成的 partial(只删已完成批次的文件,如每个文件夹里的00产物):

    rm /1/var/lib/openlibrary/coverstore/items/cover_0008/covers_0008_00.* rm /1/var/lib/openlibrary/coverstore/items/s_cover_0008/s_covers_0008_00.* rm /1/var/lib/openlibrary/coverstore/items/m_cover_0008/m_covers_0008_00.* rm /1/var/lib/openlibrary/coverstore/items/l_cover_0008/l_covers_0008_00.*

recipe 中的.tar产物对应 README 记载时点(2022 年前后)的归档形态;而当前仓库的 archive.py 已演进为zip产物(如covers_0008/covers_0008_80.zip,见 test_archive.py),这正对应 README "Covers 8,820,000 - 8,829,999 live in azipfile also in thecovers_0008item" 的新格式。tar 与 zip 并行存在于不同 ID 区间,是历史演进的正常结果。

源码级剖析:archive.py 的四个关键构件

archive.py 的 docstring 直白地写着它的职责:"把文件从本地磁盘移动到 zip 文件,并更新数据库中的路径"。四个核心类/函数各司其职:

Batch:批次路径与上传编排

  • get_relpath(item_id, batch_id, ext, size):生成{size}_covers_{item}/{size}_covers_{item}_{batch}.zip这类相对路径;
  • get_abspath():拼上config.data_root/items/得到磁盘绝对路径;
  • process_pending(upload, finalize, test):轮询data_root/items/covers_*下所有.zip(get_pending()),对每个批次用Uploader.is_uploaded()(ia list {item} | grep ...)检查 4 个尺寸是否都上传成功;若未上传但is_zip_complete()通过(无未归档残留、zip 内 jpg 数与库中已归档数一致),则Uploader.upload()上传;若全部上传且finalize=True,则调用finalize()收尾并删除本地 zip;
  • finalize(start_id, test):逐张校验cover.has_valid_files(),失败则标failed=True,否则删除本地原图,最终update_completed_batch()一次性把该批封面更新为uploaded=True并把四个 filename 列指向 zip 相对路径(archive.py)。

CoverDB:封面状态查询

CoverDB围绕cover表的failed / archived / uploaded三个状态键提供批查询:get_unarchived_covers、get_batch_unarchived、get_batch_archived、get_batch_failures,全部按id asc排序。_get_current_batch_start_id()会把任意封面 ID 对齐到 10k 批次边界。

Cover:单张封面的文件清单

Cover(web.Storage)在初始化时用get_files()建立原图 + S/M/L 四件套(文件名形如%010d.jpg、%010d-S.jpg...)并解析出各自在localdisk下的真实路径;has_valid_files()校验四件都在;delete_files()在收尾阶段清掉本地原图。类方法get_cover_url()(archive.py)负责为已上传封面构造https://archive.org/download/{item}/{zip}/{图片文件名}形式的公开访问 URL,供服务端 302 重定向使用。

ZipManager 与 archive() 主流程

archive()遍历待归档封面:文件不齐的标failed=True;齐的则把四件套逐一add_file()追加进 ZipManager 维护的 zip(zipfile.ZipFile(path, "a")追加写),并标记archived=True。ZipManager.add_file()使用ZIP_STORED存储方式——jpg 本身已高度压缩,再压缩徒耗 CPU,所以归档 zip 不做二次压缩(archive.py)。zip 命名同样遵循 4/2 划分:cid[:4]是 item、cid[4:6]是批次(get_zipfile())。

ZipManager还提供contains()、get_last_file_in_zip()、count_files_in_zip()(unzip -l | grep jpg | wc -l)等校验工具,供is_zip_complete()做"期望文件数 vs 实际文件数"的一致性检查。

服务端如何解析归档封面:tar 索引与 zip 重定向

coverstore 的服务端路由(code.py)展示了生产环境真实的读路径,分为三档:

  1. ID < 6,000,000 的 S/M/L 尺寸:优先走get_details()中的 tar 索引快路径(get_tar_filename()),避免查库。get_tar_index()(带functools.cache)读取items/{size}_covers_{xxxx}/{...}.index文件,parse_tarindex()(code.py)按名称\t偏移\t大小三列解析出 10000 个槽位的array,从而直接算出一个tar:offset:size三元组。这也是 README 中 tar 文件名后跟:1849729536:247493的由来。

  2. L 尺寸与原图且命中 cluster:is_cover_in_cluster()依据配置项max_coveritem_index判断,命中则 302 重定向到zipview_url_from_id()生成的https://archive.org/download/olcovers{idx}/{olcovers}{idx}{size}.zip/{id}{size}.jpg(code.py)——对应 README 中 0 ~ 7.14M 封面所在的olcovers1~olcovers713系列 item(每 item 1 万张,IMAGES_PER_ITEM = 10_000)。

  3. ID ≥ 8,000,000 且已上传:重定向到archive.Cover.get_cover_url()构造的covers_0008_XX.zip路径(code.py),即 README 所述 8M+ 区间的 tar/zip 归档产物。

整个响应链路还带有细致的缓存策略:以id直接请求时返回ETag+Last-Modified(命中可回 304)与"百年不过期"的Expires;按 isbn/ia 等间接 key 请求则只给 10 分钟缓存(code.py)。

运维辅助与测试保障

audit(item_id, batch_ids, sizes)(archive.py)是一个运维利器:给定 4 位 item_id(每 1M 一个)与 2 位批次范围,逐尺寸检查{size}_covers_{item}_{batch}.zip是否都已在 archive.org 上,输出.(已上传)与X(缺失),缺失时直接打印可复制的ia upload ... --retries 10命令。

归档相关的核心纯函数都有单元测试覆盖:test_archive.py 验证了get_relpath()的 4 种尺寸/扩展名组合、_get_batch_end_id()的批次取整、以及id_to_item_and_batch_id(987_654_321) == ('0987', '65')的分层映射;CoverDB._get_batch_end_id(start_id=8820500) == 8830000则保证了任何起点 ID 都会被对齐到 10k 边界——这正是 README 中"以 10k 为一个批次"这一运维纪律的代码化表达。

小结

Coverstore 的归档体系可以用三句话概括:ID 分层定归属(10 位编号切成 item 4 位、批次 2 位、文件 4 位),状态机驱动流转(archived → uploaded两阶段,failed兜底坏图),本地磁盘 + archive.org 双层寻址(tar 索引定点读、zip 路径 302 回源)。README 里 2014 年的那 570 万未归档封面提醒我们:归档不能长期停摆,而当前源码中的limit/start_id分批机制与audit()工具,正是为这类大规模积压场景准备的弹药。

  • 后端
  • 前端
  • 搜索引擎

【免费下载链接】openlibrary

One webpage for every book ever published!

项目地址:https://gitcode.com/gh_mirrors/op/openlibrary
点击查看免费下载

相关推荐

上一篇:emexDE未来路线图:这个革命性iOS IDE将如何改变移动开发格局
下一篇:如何快速掌握OptiScaler:面向游戏爱好者的完整画质优化教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

返回列表