FriendFeed 早期使用 MySQL 存儲所有數據,隨用戶增長數據超 2.5 億條。傳統 RDBMS 面臨擴展瓶頸:
評估 CouchDB 等 NoSQL 方案後,因穩定性、成熟度不足,團隊決定在 MySQL 上構建無模式存儲層,保留 MySQL 可靠的復制、備份生態,僅放棄關係特性。
id (BINARY(16) PK)、body (MEDIUMBLOB)、updated (TIMESTAMP)、自增 added_id(物理順序寫入優化)。zlib 壓縮後存入 body。index_user_id(user_id, entity_id PK)。entity_id 列表,再回實體表取 body 反序列化。shard_on 屬性哈希分片(如 user_id % num_shards),實體表同樣分片。CREATE TABLE entities (
added_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
id BINARY(16) NOT NULL,
updated TIMESTAMP NOT NULL,
body MEDIUMBLOB,
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;
# 定義索引
user_id_index = Index(table="index_user_id", properties=["user_id"], shard_on="user_id")
datastore = DataStore(mysql_shards=["shard1:3306", "shard2:3306"], indexes=[user_id_index])
# 寫入實體(自動維護所有索引)
entity = {"id": uuid, "user_id": uid, "title": "Hello", ...}
datastore.put(entity)
# 查詢:先命中索引表,再回實體表
entities = user_id_index.get_all(datastore, user_id=uid)
CREATE TABLE index_link (
link VARCHAR(735) NOT NULL,
entity_id BINARY(16) NOT NULL UNIQUE,
PRIMARY KEY (link, entity_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
# 代碼加入新索引
link_index = Index(table="index_link", properties=["link"], shard_on="link")
datastore = DataStore(..., indexes=[user_id_index, link_index])
# 異步回填歷史數據(不影響線上服務)
./rundatastorecleaner.py --index=index_link
下集預告: 深入剖析「清潔工」回填算法、分片一致性、故障恢復與容量規劃細節。