Skip to main content
状态:已构建,尚未部署。 kira-image 代码已完成(Rust,可 cargo build / cargo test),但还未部署到 Fly.io,也未接入 kira-cdn。当前线上的图片变体路径仍然是 imgproxy(img.kira.art。本文档描述的是 kira-image 设计意图——它将取代 imgproxy 的变体生成,但在 owen 完成 Cloudflare/R2 配置 + kira-cdn 调用改造 + 部署之前,imgproxy 仍是现行方案。

概述

kira-image 是 Kira 的内部图片变体生成器,用 Rust 编写。给定一个源图(R2 key 或 URL)和一组变换规格(resize / encode / blur),它确保每个变体都已存在于 R2,未命中时即时生成,并返回公开的 cdn.kira.art URL 设计要点:
  • 内部专用 — 无公网入口,不分配公网 IP。仅在 Fly 6PN 私网上的 kira-image.internal:8083 可达。
  • 无状态 — 唯一的持久化存储是 R2,没有内存缓存或本地磁盘缓存。重启零成本、随时可扩缩。
  • 内容寻址 + 去重 — 变体 key 由 (源标识 ‖ 规范化变换 ‖ 格式) 的 keyed-hash 决定,相同输入永远落在同一个 key 上,天然幂等。
  • 能力安全(capability security) — keyed-hash 用了一个 secret,攻击者即使知道源图的 R2 key,也无法计算出(从而无法访问)它的公开变体。key 本身就是凭证,与 kira-cdn 的 ref-as-capability 模型一致。
变体一旦写入 R2,就由 Cloudflare 在 R2 前面向全球分发,kira-image 只在创建变体时被触及。因此它能极致缩容,而且老 imgproxy 那套「重新签名源 URL 会冲掉 CF 缓存」的问题在这里根本不存在——公开 URL 是一个确定的、不变的 key。

部署信息

以下为 fly.toml 中的目标配置;服务尚未实际部署。
配置
平台Fly.io(app kira-image
区域sjc (San Jose),primary_region = 'sjc'
内存2GB
CPU4 vCPU (shared)
端口8083
公网地址无(无 [http_service],无公网 IP)
内部地址kira-image.internal:8083(Fly 6PN 私网)
镜像引擎libvips 8.16.1(Debian trixie),crate libvips = "=1.7.1"
fly.toml 注释建议保持 2 台机器常驻(fly scale count 2 --region sjc)以服务同步的 ensure 调用,并 fly ips release 释放所有公网 IP 以保证完全私有。这与 kira-cms 的 zero-public 形状一致。

核心实现

  • 语言 / 框架:Rust(Tokio 多线程运行时 + Axum 0.7)
  • 镜像引擎:libvips,走 thumbnail_buffer(shrink-on-load)——JPEG 用 DCT 缩放、PNG/WebP 顺序读,150MP 的源图只占几十 MB 内存而非整张 RGBA 解码。
  • 存储:与 kira-cdn 共享同一个 R2 bucket(Cloudflare S3 兼容端点),kira-image 写在自己的 KIRA_IMAGE_VARIANT_PREFIX(默认 img)目录下。
main() 故意不用 #[tokio::main]:libvips 的 vips_init() 必须跑在真正的主线程上(glib 类型系统 + worker 线程池)。代码先在主线程初始化 libvips,再手动构建 Tokio 运行时。

处理管线(image_ops.rs)

1

Header 探测

廉价的惰性解码读取源图尺寸,校验像素数 ≤ KIRA_IMAGE_MAX_SRC_PIXELS(默认 150MP),超限直接拒绝(返回 422)。
2

Shrink-on-load 缩放

vips::thumbnail_buffer(src, target_w) 一步完成解码 + 缩放,并默认按 EXIF 自动旋转。目标宽度由 plan() 根据 resize 模式预先算出。
3

可选裁剪 (fill)

resizingType=fillwidthheight 都 > 0 时,先 cover 覆盖目标框,再 extract_area 居中裁剪到精确尺寸。
4

可选高斯模糊

blur sigma > 0 时应用 vips::gaussblur
5

编码 + 质量搜索

按目标格式编码:PNG 无损;JPEG 在有 alpha 时先 flatten 到白底;WebP/JPEG 在设置了 maxBytes 时做质量二分搜索。AVIF 不在契约内,请求 AVIF 会降级为 WebP(并按真实格式返回 key/URL)。

Resize 模式

模式行为
fit(默认)等比缩放至完全装入目标框;enlarge=false 时不放大
fill等比覆盖目标框后居中裁剪到精确 width × height(需 widthheight 均 > 0)
auto在 keying 上与 fit 区分,缩放逻辑同 fit(contain)
widthheight0 表示该轴不约束;两轴都为 0 则不缩放。

maxBytes 质量搜索

当 spec 提供 maxBytes 时,编码器在 [10, start_q] 区间内对质量做二分搜索,取「输出仍 ≤ 预算」的最高质量。主要用于把图片喂给 AI provider 之前做 input fitting。
resampling(lanczos3 / lanczos2 / cubic / linear / nearest,默认 lanczos3)会被解析并计入变体 key(影响去重身份),但当前管线统一走 libvips thumbnail_buffer 自己的重采样,并未把该参数单独传给一个独立的 resample 算子。

变体 Key 生成

变体存放在 kira-image 自己的 R2 目录下,按 (源标识 ‖ 规范化变换 ‖ 格式) 的 keyed-hash 做内容寻址(src/keying.rs):
{prefix}/{ab}/{cd}/{blake3_keyed_hex}.{ext}
例如:img/ab/cd/abcdef0123…789.webp
  • prefixKIRA_IMAGE_VARIANT_PREFIX,默认 img
  • ab / cd:取 hex 摘要的前两组各 2 字符做目录分片。
  • hashblake3::keyed_hash(key, "源标识|规范化opts|ext")。其中 key 是对 KIRA_IMAGE_VARIANT_SECRET 再做一次 blake3::hash 折叠成的 32 字节。
  • ext:实际输出格式的扩展名(webp / jpg / png)。
性质:

确定性 + 去重

相同 (源, 规格, 格式) ⇒ 相同 key ⇒ 幂等。重复 ensure 同一变体只是一次 R2 HEAD 命中,不会重新生成。

不可猜测(能力安全)

没有 secret 就无法算出 key,因此知道源图 R2 key 的攻击者也拿不到它的公开变体。key 即凭证。
若实际输出格式与请求格式不同(AVIF 降级为 WebP),服务会用真实格式重新计算 store key,确保返回的 URL 始终指向真正写入的字节。

配置

环境变量名(不含密钥真值)。R2_*KIRA_IMAGE_INTERNAL_KEYKIRA_IMAGE_VARIANT_SECRETfly secrets set,其余在 fly.toml [env]
环境变量默认值说明
BIND_ADDR0.0.0.0:8083(代码默认)监听地址;fly.toml 覆盖为 [::]:8083(IPv6,Fly 私网可达必需)
KIRA_IMAGE_INTERNAL_KEY(必填,secret)X-Internal-Key 共享密钥(kira-cdn → kira-image)
KIRA_IMAGE_VARIANT_SECRET(必填,secret)keyed-hash secret,使变体 key 不可猜测
KIRA_IMAGE_VARIANT_PREFIXimg变体在共享 bucket 中的目录前缀
KIRA_IMAGE_PUBLIC_BASEhttps://cdn.kira.art拼接公开 URL 的前缀
KIRA_IMAGE_MAX_CONCURRENCYmax(CPU 核数, 4)并发图片处理数(信号量上限),同时设给 libvips 线程数
KIRA_IMAGE_PROCESS_TIMEOUT_SECS30单张图片处理超时(秒)
KIRA_IMAGE_DOWNLOAD_TIMEOUT_SECS20源图 fetch 超时(秒)
KIRA_IMAGE_MAX_SRC_BYTES157286400(150MB)源图字节数上限
KIRA_IMAGE_MAX_SRC_PIXELS150000000(150MP)源图像素数上限
KIRA_IMAGE_DEFAULT_QUALITY80JPEG 默认质量
KIRA_IMAGE_WEBP_QUALITY75WebP 默认质量
KIRA_IMAGE_USER_AGENTMozilla/5.0 (compatible; KiraImage/1.0; +https://kira.art)URL 抓取时的 User-Agent
R2_ENDPOINT(必填,secret)R2 S3 兼容端点(与 kira-cdn 同一 bucket)
R2_BUCKET(必填,secret)R2 bucket 名
R2_ACCESS_KEY_ID(必填,secret)R2 access key
R2_SECRET_ACCESS_KEY(必填,secret)R2 secret key
RUST_LOGinfo,kira_image=infotracing 日志级别

并发与超时

  • 信号量限流:CPU 密集的图片处理由 tokio::sync::Semaphore 限流,上限 = KIRA_IMAGE_MAX_CONCURRENCY(默认 max(CPU, 4))。处理走 spawn_blocking,不阻塞 async 运行时。
  • 单图超时:默认 30s(KIRA_IMAGE_PROCESS_TIMEOUT_SECS),超时返回 504。
  • 优雅停机:捕获 SIGTERM / SIGINT,with_graceful_shutdown 排空在途请求后退出。

源图抓取(store.rs)

来源路径行为
R2 keysource.keybucket.get_object(key) 直接从共享 bucket 读,校验大小
HTTP URLsource.url(legacy / 外部)reqwest GET:5s connect 超时、最多 3 次重定向、download_timeout 整体超时、大小守卫、可配 User-Agent
写回时统一用 put_public(key, bytes, content_type),Content-Type 由格式决定(image/webp / image/jpeg / image/png / image/avif),作为公开对象写入,经 Cloudflare 在 cdn.kira.art 前缓存。

与 kira-cdn 的集成(待接入)

kira-image 上线后,kira-cdn 的 sign 路径需要:
  • buildVariantUrls(thumb / preview)与 /v1/fit 改为调用 kira-image.ensure(...),走私网 kira-image.internal:8083,请求头带 X-Internal-Key
  • 像现在一样把返回的 URL 缓存进 Redis。
  • 在 Cloudflare 为 cdn.kira.art/{prefix}/* 加一条 Cache Rule(Edge TTL ~1 年,immutable)。
变体存放在独立目录(不与源图同目录),因此不会随 thread / feed 的 cascade 删除连带清掉——按 age/heat 用 cron 单独清理。Watermark 仍留在 kira-cdn(Sharp),未来可用同样的 ensure-to-R2 模式折叠进来。

健康检查

# 仅内部可达(无公网)
curl http://kira-image.internal:8083/health
# → ok

文件结构

API 细节见 kira-image API 参考