背景
普段よく使っているサービスをどう設計するかとかあんまり考えたことがなかったから、これからちょくちょく調べていくことにする
関心
- 増え続けるデータに対してのテーブルの設計はどうするか
要約
元記事: Big Data in Real-Time at Twitter
Twitterが抱えるReal Time Data処理について、以下の点が問題となった
- Tweets
- Timelines
- Social graphs
- Search indices
おなじみのツイート。ユーザーが投稿するつぶやきのエントリ。
- もともとのテーブル設計はid(主キー), user_id, text, created_atをカラムに持つRDB
- Master-Slave構成で、キャッシュ層としてMemocached
問題1
ディスク容量。一時は容量の90%が使用されている状態となった
解決策1
パーティション化を工夫する。
ここで、どのようにパーティション化するかが課題
時間単位でパーティションを分けることで、2つの問題を解決した
この場合、どちらでid, user_idのどちらでクエリするにしても、単一のパーティションで収まるということはない(=O(N)になるはず)。
しかし、Twitterというサービスは直近のツイートに対してのクエリが最も多く、実質的にほぼO(1)のオーダーになる。
問題2
書き込み時のスループット。MySQLがデッドロックを起こすようになった。また、新しいシャードを作成するのはかなりの手間だった
解決策2
Cassandraの導入。パーティションキーをツイートのid, ソートキーをuser_idにする。
読み込みにはMemocachedを使う。
Cassandra+キャッシュの構成は始めて見た。実在するんだな。
Timelines
自分がフォローしている人のツイートの集合
元々のSQL(出典: Big Data in Real-Time at Twitter)
SELECT * FROM tweets
WHERE user_id IN (
(SELECT source_id
FROM followers
WHERE destination_id = ?)
ORDER BY created_at DESC
LIMIT 20
問題
メモリに載らないほど友達が多すぎる場合、サブクエリの部分でめちゃくちゃ時間がかかる
解決策
- Memocachedにツイートidのシーケンスをもたせる
- オフラインでファンアウトする
要は、Memocachedに一方的に配信するようにして、ユーザーが後にMemocachedを見に行くようにするPub/Subの構造にしたっぽい
→これで120万req/sに対応できるようになった
Social Graphs
フォロー/フォロワーや、リスト、ブロックなどのつながり
もともとの実装
以下のように中間テーブルを実装する
|source_id|destination_id
|-|-|
|20|12|
|29|12|
|34|16|
問題
- 書き込みのスループットが足りない
- RAMに載らない量のデータ
解決策
- 関係を片方向のみ保持するテーブルを作る
- ユーザーでパーティションを分け、時間でインデックスを貼る
これは、非正規化されたテーブルになるし、逆方向の同じリンクを別々のテーブルが持つようになる
Forward
source_id |
destination_id |
updated_at |
x |
20 |
12 |
20:50:14 |
x |
20 |
13 |
20:51:32 |
|
20 |
16 |
|
|
Backword
source_id |
destination_id |
updated_at |
x |
12 |
20 |
20:50:14 |
x |
12 |
32 |
20:51:32 |
|
12 |
16 |
|
|
課題
データの一貫性を持たせるのが難しい
→結果整合性のデータモデルにする
Search Indices
検索バーでの検索
元々の実装
term_id |
doc_id |
20 |
12 |
20 |
86 |
34 |
16 |
問題
インデックスがメモリに載らない
解決策
時間でパーティション分けする
将来的には、ドキュメントと時間の両方でパーティション分けし、確認するレイヤーをもたせる
感想
大規模だと素直にテーブル設計してもスケールしないことはわかるけど、解決策が時間ごとのパーティション分けになるのは目から鱗。サービスについて深く理解してないと、こういう解決策は出てこないな。
テーブルを非正規化するのは非常にシンプルな解決策だから引き出しの一つとして持っておきたい