概述
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 模型一致。
部署信息
以下为
fly.toml 中的目标配置;服务尚未实际部署。| 配置 | 值 |
|---|---|
| 平台 | Fly.io(app kira-image) |
| 区域 | sjc (San Jose),primary_region = 'sjc' |
| 内存 | 2GB |
| CPU | 4 vCPU (shared) |
| 端口 | 8083 |
| 公网地址 | 无(无 [http_service],无公网 IP) |
| 内部地址 | kira-image.internal:8083(Fly 6PN 私网) |
| 镜像引擎 | libvips 8.16.1(Debian trixie),crate libvips = "=1.7.1" |
核心实现
- 语言 / 框架: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)
Shrink-on-load 缩放
vips::thumbnail_buffer(src, target_w) 一步完成解码 + 缩放,并默认按 EXIF 自动旋转。目标宽度由 plan() 根据 resize 模式预先算出。Resize 模式
| 模式 | 行为 |
|---|---|
fit(默认) | 等比缩放至完全装入目标框;enlarge=false 时不放大 |
fill | 等比覆盖目标框后居中裁剪到精确 width × height(需 width、height 均 > 0) |
auto | 在 keying 上与 fit 区分,缩放逻辑同 fit(contain) |
width 或 height 为 0 表示该轴不约束;两轴都为 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):
img/ab/cd/abcdef0123…789.webp
- prefix:
KIRA_IMAGE_VARIANT_PREFIX,默认img。 ab/cd:取 hex 摘要的前两组各 2 字符做目录分片。- hash:
blake3::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 即凭证。
配置
环境变量名(不含密钥真值)。R2_*、KIRA_IMAGE_INTERNAL_KEY、KIRA_IMAGE_VARIANT_SECRET 走 fly secrets set,其余在 fly.toml [env]。
| 环境变量 | 默认值 | 说明 |
|---|---|---|
BIND_ADDR | 0.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_PREFIX | img | 变体在共享 bucket 中的目录前缀 |
KIRA_IMAGE_PUBLIC_BASE | https://cdn.kira.art | 拼接公开 URL 的前缀 |
KIRA_IMAGE_MAX_CONCURRENCY | max(CPU 核数, 4) | 并发图片处理数(信号量上限),同时设给 libvips 线程数 |
KIRA_IMAGE_PROCESS_TIMEOUT_SECS | 30 | 单张图片处理超时(秒) |
KIRA_IMAGE_DOWNLOAD_TIMEOUT_SECS | 20 | 源图 fetch 超时(秒) |
KIRA_IMAGE_MAX_SRC_BYTES | 157286400(150MB) | 源图字节数上限 |
KIRA_IMAGE_MAX_SRC_PIXELS | 150000000(150MP) | 源图像素数上限 |
KIRA_IMAGE_DEFAULT_QUALITY | 80 | JPEG 默认质量 |
KIRA_IMAGE_WEBP_QUALITY | 75 | WebP 默认质量 |
KIRA_IMAGE_USER_AGENT | Mozilla/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_LOG | info,kira_image=info | tracing 日志级别 |
并发与超时
- 信号量限流: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 key | source.key | bucket.get_object(key) 直接从共享 bucket 读,校验大小 |
| HTTP URL | source.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 模式折叠进来。