面對 2.5 億條記錄、頻繁 Schema 變更、傳統 ALTER TABLE 鎖表數小時的痛點,FriendFeed 團隊選擇在 MySQL 之上實作一套「無 Schema」存儲系統:實體表僅存 id + body(JSON blob),索引完全外置到獨立表,並開發「清潔工」異步維護索引一致性。本文深度解析其架構設計、一致性模型、效能優化與運維經驗。
-- 實體表:僅存 id、updated、壓縮後的 body
CREATE TABLE entities (
added_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, -- 物理順序寫入,利於分區讀取
id BINARY(16) NOT NULL,
updated TIMESTAMP NOT NULL,
body MEDIUMBLOB, -- zlib 壓縮的 Python dict / JSON
UNIQUE KEY (id),
KEY (updated)
) ENGINE=InnoDB;
-- 索引表:每個需查詢的屬性一張表
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;
關鍵設計決策:
added_id 自增主鍵確保新實體物理順序寫入,熱數據集中,減少隨機 I/O。body 經 zlib 壓縮,節省 70%+ 空間,CPU 成本極低。(屬性值, entity_id) 支援範圍查詢與去重。shard_on 屬性哈希路由,擴展性線性增長。entities 表(InnoDB ACID 保證)。entity_id,再回 entities 取完整實體,最後在應用層(Python)用真實屬性值過濾,修正索引不一致導致的誤判。此設計犧牲強一致性,換取寫入零鎖、Schema 變更零停機。索引不一致窗口通常僅秒級(清潔工優先處理最新更新)。
# 為新屬性 link 建立索引表後,啟動清潔工回填歷史資料 ./rundatastorecleaner.py --index=index_link
entities 表,比對索引表,補寫遺漏、刪除失效索引。上線後頁面載入延遲顯著下降並趨於穩定,即使午間尖峰也無抖動。新增索引從「數小時鎖表」變為「一天內完成,零業務影響」。
| 優勢 | 劣勢/限制 |
|---|---|
| Schema 變更極致靈活(加/刪索引無鎖) | 無 JOIN、外鍵、複雜事務支援 |
| 寫入延遲極低、可線性擴展 | 讀取需兩次查詢 + 應用層過濾,延遲略高 |
| 利用成熟 MySQL 生態(備份、複制、監控) | 存儲空間放大(索引表冗余屬性值) |
| 避免遷移至陌生 NoSQL 系統的風險 | 需自研資料存取層(Datastore SDK) |
FriendFeed 方案證明:在關係型資料庫上構建文檔存儲,是「不想換引擎、又要 Schema 自由」團隊的務實選擇。核心思想——實體與索引解耦、寫入路徑極簡、異步修復一致性——值得所有面對大表 DDL 痛點的團隊參考。