アンチパターンばかり書いてきましたけど、たまにはアンチじゃないものも出しましょう。
PGConf.Asia2016 で Tantan の Victor Blomqvistさんもやってた、Instagram方式です。
元ネタ
- Instagram Engineering Blog
- PGConf.Asia2016公開資料:TantanでのPostgreSQL事例 – 2年で0から350billion rowへ Victor Blomqvist
DBのスケールアウト
Webサービスが順調に成長すると、顧客当たりのデータ量が徐々に増える、顧客が増えるで、DBのデータ量も増えていきます。
Apache/Tomcatの能力不足は、セッション管理だけ最初に注意して構成しておけば10や20に台数増やしていくのは容易な話で、問題はDBをどうするかです。
1台のDBServerでは処理能力が追いつかなくなってきたら?
参照中心のアプリケーションならリード・レプリカつくって負荷分散が初手になるかもしれませんが、追加・更新側のトランザクション処理が追いつかなくなったら?
顧客を2分割して、2つのDBServerに収容する水平分割となります。さらに増えたら、増えたところを分割して顧客を細分化していきます。
DB水平分割の運用問題
1台のDBServerでは処理能力が追いつかないから分割するので、それなりのデータ量が蓄積しています。移動するのは半分としても、pg_dump, psql -f でやるのは工事時間を長引かせることになります。
ストレージレベルで分割・移動
Instagramのやり方です。ここではAWS上のサービスとして話を進めます。
解決策
- 小さく多数の論理shardを、少数の物理shard サーバーに mapする
- 物理Shard = Postgres データベースクラスタ(≒ DB Server)
- 論理Shard = Postgres の schema
- ノード分割は、EBS Snapshot で丸ごとコピーして、DROP SCHEMA CASCADE で重複分を削除する。DBSizeが大きくても高速コピー可能。
8個の論理shardを2 server に格納した状態から 4server に再分割する例
// 8 logical shards on 2 machines
user_id % 8 = logical shard
logical shards -> physical shard map
{
0: A, 1: A,
2: A, 3: A,
4: B, 5: B,
6: B, 7: B
}
// 8 logical shards on 4 machines
user_id % 8 = logical shard
logical shards -> physical shard map
{
0: A, 1: A,
2: C, 3: C,
4: B, 5: B,
6: D, 7: D
}
- machineA:
- shard0
- shard1
-
shard2machineC稼働後に削除 -
shard3machineC稼働後に削除
- machineC:
-
shard0machineC稼働後に削除 -
shard1machineC稼働後に削除 - shard2
- shard3
分割引越の手順
- 対象顧客のSCHEMAをReadOnlyに
- EBS Snapshot
- 新規DBServerにSnapshotから作ったVolumeをattach
- 新規DBServerをwarmup
- 新規DBServerでSCHEMAのReadOnly制限解除
- SCHEMAとDBServerのMapを切替。(同時にReadOnly制限解除)
- 重複SCHEMAをDROP CASCADE
pg_dump/psql -f のSQLレベルだったデータ移動が、EBS SnapshotによるAWSのバックヤード処理に移りました。
warmupに少々時間がかかるので、さらに改善の余地があります。Snapshotから作ったばかりのVolumeは、新規DBServerが実際にアクセスしたところから徐々に実体化していくので、運用Webサービス用のDBではwarmupは必須です。
分割引越の手順2
- EBS Snapshot
- 新規DBServerにSnapshotから作ったVolumeをattach
- 新規DBServerをリードレプリカとして構成
- 新規DBServerをwarmup
- 対象顧客のSCHEMAをReadOnlyに
- 新規DBServerのリードレプリカ構成を解除して独立マスターにする
- 新規DBServerでSCHEMAのReadOnly制限解除
- SCHEMAとDBServerのMapを切替。(同時にReadOnly制限解除)
- 重複SCHEMAをDROP CASCADE
対象顧客のSCHEMAをReadOnlyにしたときには、対象顧客のデータはすべて新規DBServerに入っていて、warmupも済んでいる状態です。ReadOnly制限のかかる時間は、顧客データ量に比例するところのない定数時間となります。
分割DBの悩み Global Unique Key
単一DBなら SERIAL型とするだけです。複数serverでユニークかつ全体ロック・相互通信は回避したい。Hotspot問題になって、INSERT処理が遅くなってしまうので。
解決策
- ID順≒時刻順としたいので、epoch msec をID先頭に含める。
- 異なるShardのレコードをUNIONして並べたときでも、msecの分解能では ID順=時刻順
- だいたい時刻順で十分とできるときに割り切ってしまう
- Shard 毎にID生成関数をもち、ID内部に ShardIDを含めることでユニーク性を担保する。
- 1msec の中で複数挿入の事態に備えて、10bit程度はShard内で自動increment。
- 全体を64bitに納めるために epoch msec を 2011-01-01起点 の 41bit(41年間)、ShardId 13bit(8K個)、シリアル10bit とする
- 2052年問題ができちゃうけど
- 全体でのユニーク性が41年間は保つ。2PhaseCommitとか全体ロックなしでLocalの関数処理だけで済む。