FaceBook 收購前的 FriendFeed 面對數億條數據、頻繁 Schema 變更、MySQL DDL 鎖表數小時的痛點,創造性地在 MySQL InnoDB 之上構建了一套**無模式存儲層**:實體序列化為 Blob 存儲,索引獨立建表維護,實現「加字段不鎖表、加索引不阻塞寫入、清理工異步回填」。本文深度解析其核心設計、一致性模型、清理機制與性能實測。
**決策**:不遷移陌生 NoSQL (CouchDB 等穩定性未驗證),而是**在熟悉、穩定的 MySQL 上實現文檔存儲語義**。
CREATE TABLE entities (
added_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, -- 物理順序寫入優化
id BINARY(16) NOT NULL, -- UUID 16 字節
updated TIMESTAMP NOT NULL,
body MEDIUMBLOB, -- zlib 壓縮後的 Python dict/JSON
UNIQUE KEY (id),
KEY (updated)
) ENGINE=InnoDB;
added_id 自增主鍵保證新實體順序寫入磁盤末尾,熱數據聚集,利於分區讀取。body 存儲完整屬性集 (如 user_id, feed_id, title, link, published...),**對資料庫不透明**,應用層反序列化後解析。-- 按 user_id 查詢用戶所有實體
CREATE TABLE index_user_id (
user_id BINARY(16) NOT NULL,
entity_id BINARY(16) NOT NULL UNIQUE,
PRIMARY KEY (user_id, entity_id) -- 覆蓋索引,避免回表
) ENGINE=InnoDB;
-- 按 link 精確查找
CREATE TABLE index_link (
link VARCHAR(735) NOT NULL, -- 767 字節限制 / utf8mb4
entity_id BINARY(16) NOT NULL UNIQUE,
PRIMARY KEY (link, entity_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
shard_on 決定索引落在哪個分片 (如 user_id % num_shards)。entities 寫入成功 (規范數據源)。一致性妥協:
entities 讀取完整實體,**在應用層用真實屬性過濾驗證**,過濾掉索引不準導致的假陽性。後台常駐進程 rundatastorecleaner.py --index=index_link 持續運行:
updated 倒序掃描 entities,對比索引表,補寫缺失、刪除殘留。# Python 偽碼
def get_by_user_id(user_id):
entity_ids = [] # 從所有分片 index_user_id 查詢
for shard in shards:
ids = shard.query("SELECT entity_id FROM index_user_id WHERE user_id=%s", user_id)
entity_ids.extend(ids)
entities = [] # 批量回 entities 表
for shard in shards:
ents = shard.query("SELECT body FROM entities WHERE id IN (%s)", entity_ids)
entities.extend(ents)
# 應用層二次過濾 (防禦索引髒數據)
return [e for e in entities if e['user_id'] == user_id]
added_id) + 覆蓋索引 + 無鎖 Schema 變更 = 寫入吞吐線性擴展,讀取延遲可控。| 維度 | 傳統 RDBMS | FriendFeed 無模式層 |
|---|---|---|
| Schema 變更 | DDL 鎖表、風險高 | 應用層新增屬性即可、零停機 |
| 索引管理 | 同表、加鎖、回填慢 | 獨立表、異步建、清理工回填 |
| 一致性模型 | 強一致 (ACID) | 最終一致 (實體強、索引弱) |
| 查詢方式 | SQL JOIN、WHERE | 索引定位 ID → 回實體 → 應用過濾 |
| 技術棧 | 純 SQL | MySQL + Python 應用層邏輯 |