voidliang's zone
首页归档照片墙音乐动态杂谈友链关于
封面

照片标签存储方案调研:从千万~亿级 DAU 的检索需求出发

写作时间:2026-06-20 15:30:00
# 系统设计
# 技术调研
# 存储方案

一张照片上传到服务端。用户想按标签找到它。

这中间隔着一整套存储设计的十字路口。我最近在调研这个方向,背景不复杂:一个 日活千万~亿级的照片产品,核心就三条检索链路——GetTagList(按用户拉全部标签)、GetPicByTag(多标签 AND 求交集)、Search(多字段全文检索)。

三条链路看着像同一个功能,落到存储上,完全是三种读写模式。需求不一样,索引结构不一样,瓶颈位置也不一样。

这篇是我调研过程中的记录和思考。结合 Google Photos、iCloud 的公开架构资料,看看业界成熟方案怎么解这三道题。不是结论,是过程。

问题的起点:三条链路,三种截然不同的读写模式

方案选型之前,先把三条链路的查询特征拆清楚。问题没拆清楚,看什么方案都像正确答案。

① GetTagList:聚合是最大的敌人

GetTagList 的语义是:给定一个用户,返回他所有照片中出现过的标签,去重列表。

听起来很简单对吧?SELECT DISTINCT tag FROM photo_tags WHERE user_id = ?,加个索引就完了。

但这是千万~亿级 DAU。一个活跃用户的照片可能有几万张,每张照片 5-10 个标签。这条 SQL 要扫 几十万行,再做 DISTINCT 聚合。

更麻烦的是,标签列表是个 高频操作——打开照片页、进入搜索页,都要先拉标签列表。意味着 QPS 不会低。

GetTagList 真正的敌人不是单次查询延迟,是聚合成本 × 调用频率的乘积。

② GetPicByTag:多标签交集为什么这么痛

GetPicByTag 的语义是:用户选 N 个标签,找同时拥有这 N 个标签的所有照片。

用传统 MySQL 三表设计,SQL 大概是:

SELECT photo_id FROM photo_tags WHERE tag = 'A'
INTERSECT
SELECT photo_id FROM photo_tags WHERE tag = 'B'

两个子查询各扫几十万行,在内存里取交集。三个标签能把 MySQL 干到 2 秒超时。

不是 MySQL 不行,是交集查询和关系模型天然犯冲。关联表为"一对多 JOIN"优化,不为"多对多 AND"优化。

③ Search:多维检索的终极形态

Search 最复杂。不是按标签查,要在照片描述 + OCR 文本 + 标签 + 地点 + 人物五个维度上做全文检索。

翻译成数据库需求:一个查询同时命中文本字段、标签字段、地理位置字段、人脸聚类字段。一个索引搞不定,一个数据库也搞不定。

三条链路拆开来看,会发现 GetTagList 要聚合缓存,GetPicByTag 要倒排索引,Search 要多套索引并行——它们需要的存储结构完全不一样。这也是为什么业界方案都不是"一个数据库打天下"。

Google Photos 怎么做:元数据优先的架构思路

Google Photos 的数据量级是另一个世界。据公开资料推断,日均上传 12 亿张照片,总存储量超 4 万亿张。

但它的架构反而很干净,核心一句话:把元数据和原始文件彻底分开。

① Spanner 主存储 + 多模态索引分离

照片原始文件走对象存储(类似 GCS),通过内容哈希去重。缩略图、预览图走 CDN。

真正决定检索体验的,是那层 元数据存储。每张照片上传后,ML 管线产出一堆结构化信息:对象标签(狗、海滩、蛋糕)、场景分类(婚礼、日落)、OCR 文本、人脸 embedding、GPS 坐标、EXIF 时间戳。

这些元数据推测写入 Spanner,按用户 ID 分片。选 Spanner 而不是 Bigtable 的逻辑很直接——元数据操作经常涉及多表联动(建相册、移动照片、改标签),需要 ACID 事务保证一致性。

但 Spanner 只管存储,不管搜索。搜索走另一套独立的索引系统。

② 实时 + 批量双索引管道

Google Photos 的索引不是建在数据库上的二级索引,是一条独立的消息驱动管道:

上传 → 对象存储 → 异步消息队列 → ML 处理 → 元数据写入 Spanner → 触发索引更新

这套管道有意思的地方是分了 两条路径。实时路径负责新照片秒级可搜:上传后几秒内,倒排索引和时空索引就位。批量路径负责模型升级后的历史回填:ML 模型更新了(比如新增识别"哈士奇"这个细分类别),低峰期重新跑全量,不跟实时流量抢资源。

③ 倒排 + 向量 + 时空:三种索引各司其职

真正支撑"去年夏天在海边拍的狗狗照片"这种搜索的,是三套索引并行:

索引类型 存储内容 解决的问题
倒排索引 标签 → 照片列表、OCR 文本 → 照片列表 "狗狗""海滩"这种精确关键词匹配
向量索引 照片视觉 embedding、人脸 embedding "日落海滩"这种语义模糊查询
时空索引 时间戳 + GPS 范围 "去年夏天""在北京"这种时空约束

查询来了,三路并行召回,各自返回候选集,合并 + 个性化排序。存储和搜索完全解耦,存储管"照片有什么",搜索管"用户找什么"。

这就是 Google Photos 最核心的取舍:存储不值钱,怎么快速找到才值钱。元数据优先,索引独立。

Apple iCloud Photos:每人一个独立数据库

Google Photos 的思路是"集中存储 + 分离索引",Apple 的思路是另一个极端:把隔离做到极致。

① FoundationDB Record Layer 的基本思路

iCloud 后端核心是 CloudKit,底层跑两套存储:Cassandra(历史遗留)和 FoundationDB(主力)。

但 Apple 没直接用 FoundationDB,在上面封了一层叫 Record Layer 的东西。这是个 Java 库,在 FoundationDB 上实现了轻量关系数据库的能力:结构化类型(protobuf 定义 schema)、多种索引(值索引、排序索引、聚簇索引)、嵌套查询。

② 键范围隔离 + 无状态

Record Layer 最狠的设计是极端多租户——CloudKit 为每个用户 × 每种应用创建一个独立的记录存储,总计数十亿个逻辑数据库。

怎么做到的?每个逻辑数据库被分配一个独立的 FoundationDB 键范围。用户 A 的数据在 [A_start, A_end],用户 B 的在 [B_start, B_end]。物理上同一个集群,逻辑上完全隔离。

好处是:用户照片元数据出问题,影响范围只到这一个用户。迁移也简单——把键范围重定位到另一个集群,不需要重构存储。

Record Layer 本身无状态,加实例就能横向扩展。负载均衡只关心数据位置,不关心服务器状态。

③ 键顺序即索引:不建额外索引的搜索

FoundationDB 的键本身是有序的。利用键顺序,可以直接做前缀搜索和范围扫描,不需要再建倒排索引。

比如把照片元数据按 user_id + timestamp + photo_id 的键格式存储,"查某个用户某段时间的照片"就是一个自然的范围扫描。

当然,这个方法只适用于规则结构数据。自然语言全文搜索和向量相似度搜索,iCloud 公开资料里没详细展开,推测还需要额外索引层。

方案对比:三种技术栈的取舍逻辑

调研一圈下来,我脑子里慢慢形成了一张"技术栈选择坐标系"。横轴是数据规模,纵轴是查询复杂度。

① MySQL 系的天花板在哪里

单机 MySQL 做标签存储,三条链路的瓶颈分布完全不同:

链路 MySQL 表现 瓶颈原因
GetTagList 几千张照片时没问题 上万张后 DISTINCT 聚合开始吃力
GetPicByTag 两个标签勉强,三个超时 子查询扫大量行 + 内存 INTERSECT
Search 完全不行 多字段模糊匹配不是 MySQL 强项

MySQL 的真正天花板不在单表容量,在查询模式不匹配。三表关联为"增删改查单条记录"优化,不为"从海量记录中按多条件筛选"优化。

MySQL JSON 列 + 多值索引是一条改进路线,但也只是把天花板抬高一点——交集查询仍然要在内存做。

② MySQL + 搜索引擎的中间路线

既然 MySQL 不善长搜索,那把搜索交给专业的。

MySQL 做主存储(管元数据的增删改),Elasticsearch 做搜索加速(管三条链路的查询)。写入时双写,读取走 ES。

这个方案的性价比很高:

  • 不推翻现有 MySQL 架构,ES 是旁路加速
  • ES 的倒排索引天然适合标签交集查询(AND 就是多个 term 的倒排列表求交集)
  • ES 多字段全文搜索直接覆盖 Search 链路
  • 社区成熟,运维经验丰富

代价也明显:数据一致性需要额外处理(MySQL 和 ES 同步),多一套运维系统。

③ 原生分布式数据库的真正价值

Google 用 Spanner,Apple 用 FoundationDB,这两条路线说白了都是放弃"拼积木"式架构,直接上原生分布式数据库。

好处:存储和索引在同一个系统内,一致性有天然保证,运维复杂度反而更低。不用管 MySQL-ES 同步延迟,不用处理两套系统故障隔离。

代价:Spanner 和 FoundationDB 都不是开源产品(FoundationDB 核心开源,但 Record Layer 生产使用需大量定制)。自建团队用 TiDB 或 CockroachDB 替代,是业内常见做法。

三条路线都是对的,差别在你的系统处于什么阶段:

  • 早期几万用户,MySQL 三表方案完全够
  • 百万 DAU,MySQL + ES 是性价比最高的升级路径
  • 亿级 DAU,原生分布式数据库的价值才真正体现

写在最后

这篇调研写到这里,与其说是给了一个答案,不如说是串起了一组问题。

三条链路的存储方案不是一道单选题。GetTagList 要物化视图或缓存,GetPicByTag 要倒排索引,Search 要多模态并行召回——它们需要的存储结构完全不同。

Google Photos 把存储和搜索彻底分开,Apple iCloud 把隔离做到每人一个库。两条路线都撑起了亿级用户,说明方向比技术选型更重要。

调研过程里最大的感受是:标签存储这件事,没有银弹,只有匹配度。你系统当前是什么阶段,就该用什么方案。MySQL 三表在几万用户时一点不丢人,Google 那套分离索引也是从单库慢慢演进来的。踩着别人的脚印走,但别被别人的尺子量死。

扩展阅读:

  • Google Photos System Design Case Study — 基于公开信息的全链路架构推断
  • Google Photos System Design: A Complete Guide — 上传管道、存储分层、搜索索引完整拆解
  • Designing iCloud Photos Metadata at Scale — Apple iCloud Photos 元数据存储设计方案
  • Apple 如何构建 iCloud 来存储数十亿个数据库 — FoundationDB Record Layer 实时索引机制详解
avatar

voidliang

这个人很懒,什么也没留下

RECOMMENDED

如何给 AI 工作流出题、录题、改卷:一套自动评测系统的设计实录

2026-06-22 12:00:00

推荐系统的工程实现:一张全景地图

2026-06-24 02:33:00

你的 AI 不是不够聪明,它只是缺一套协作协议

2026-06-21 15:20:00

Table of Contents