イベントソーシングとSnowflake IDパターン

1.7K Views

February 04, 26

スライド概要

profile-image

GitHubber, OSS作家。Tech SaaSのPdM、スタートアップ取締役CTOや外資スタートアップのIC等を経験後現職。好きな言語はGoとPerlと中国語で雑なOSSを200以上量産している。3 times ISUCON winner. 著書「みんなのGo言語」共著他。Podcast https://oss4.fun

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

イベントソーシングと Snowflake IDパターン Masayuki Matsuki a.k.a Songmu @ 外部キーNight #2

3.

外部キーは使いましょう ● データに適切な制約が設けられていると安心 ○ ● オーバーヘッドも全然許容できる (今ならほぼ無視できるコストでは?) ただ、CASCADE DELETE は避けよう ○ ○ ○ トリガー的な暗黙的な振る舞いになりやすい アプリケーションからすると副作用が大きい 1:N関係で大量に削除されてパフォーマンス影響がでることも

4.

DB設計の基本方針 変なデータが入ったら大変なので制約をフル活用 ● 出典: ルーク!MySQLではkamipo TRADITIONALを使え! ○ kamipo TRADITIONALとか知らなくて良くなったので良い時代になりました 「アプリケーションは適度に柔らかく、最終防衛ラインであるDBは固く」

6.

開発も似ている 「楽観的に企画し、悲観的に設計し、楽観的に実装する」 データ設計含めた設計を楽観的に片づけてしまうと、実装が最初は楽観的でも 後からどんどん悲愴的になってしまう

7.

以上 …ではなくて、PRIMARY KEYやSnowflake IDの話をします

8.

PK (Primary Key) の流儀 派閥や求めたい要件

9.

PKはサロゲートキーかナチュラルキーか ● ● 私はサロゲートキー派 多くのWebフレームワークやエコシステムがそれ前提となった ○ 残念なトレンドだと思う人もいるでしょう

10.

PKに求めたい要件 ● イミュータブル(不変)であること ○ ● ● そのためにも意味を持たない無機質な値であること 時系列に並んでいること(後述) できれば int64 に収まっていて欲しい ○ (できればint53… -> 無理)

11.

イミュータブルであって欲しい ● PKの値の変更を許容すると無駄に複雑になる ○ ○ 特に外部システム連携が絡む場合など ログにID出してあとから突き合わせたいとか ■ こういう要件もあるから「意味がない」ことも大事 世界が変化してシステムを変化させていく前提だと、不変なナチュラルキー設計は現 実的ではない

12.

時系列で並んでいて欲しい ● ● 「意味を持たない」とは矛盾するが… アプリケーションが過度にその前提に依存するのは危険ではある ○ ○ それを前提にソートすることくらいは許して欲しい… パーティション使うときに便利 (後述)

13.

結局INSERT順にソートしたいことは多い ● ● created_at, id に index 張るのは無駄感 例えば、あるユーザーに絞って降順に並べたい場合 ○ ○ user_id, created_at にINDEXを張ると、同一の created_at の順序が不定になる user_id, id へINDEXを張ればその心配は無い

14.

(余談) 外部公開IDの扱い IDを内部情報が伺い知れない値にしたいケースがある ● 連番 (Auto incrementなど) ○ ● ユーザー数規模等が推測されてしまう 生成時刻を逆引できる形式 (UUID v7やSnowflake IDなど) ○ そのエンティティの生成時刻が推測されてしまう ただ、推測されても問題ない場合もあるので必須要件ではないし、本トークではス コープ外とします。

15.

要件を満たすサロゲートキーの値の型 ● ● ● Auto incrementなint64 / uint64 (一般的) UUID v7 / ULID Snowflake ID 及びその亜種

16.

Auto Incrementでたまに困る事 ● INSERT前にIDがわからない ○ ○ ● 時系列のパーティションを切りたい時に不便 (後述) ○ ● PKのIDから時刻が逆引できるようになっている方が便利な場合がある シャーディング (水平分割) したいとき ○ ○ ● 複数テーブルや外部連携がある時にINSERTにIDを発番しておけると助かる ■ リトライなどもやりやすい 事前発番の方がDBに依存しないので設計的にも綺麗になる ❗ 令和はシャーディングしなくていい! シャード間でIDが被らないようにする必要がある (メリット: 覚えやすい)

17.

分散採番が欲しくなることがある ● ● Auto Incrementなしだとアプリケーション側でのID生成が必要 複数プロセスでIDがぶつからないようにケアする必要がある ○ ○ アプリケーションごとの分散採番 → 採番機欲しい (まあ、UUID v7とかで採番して、ユニークキー制約で衝突検知でリトライでも 良いのだけど)

18.

Snowflake ID 皆さん実は地味にお世話になっています データストアのSnowflakeとは関係ありません!

19.

Snowflake IDとは ● ● https://x.com/songmu/status/2018647700959555984 "2018647700959555984" が Snowflake ID

20.

X公式ドキュメントによる説明 ● ● https://docs.x.com/fundamentals/x-ids Snowflake と呼ばれる uint64 のID ○ ○ ○ ○ タイムスタンプとワーカー番号と連番で構成されている 大体時系列順に並ぶ Xのシステム全体でユニークである (昔はint64だったがいつの間にかuint64になっていた)

21.

IDから生成時刻が逆算できる! ● 2010-11-04 01:42:54.657 UTC を起点とする ○ ● そこから経過した時間(ミリ秒)がIDに含まれている 具体的には上位42bitがミリ秒単位のタイムスタンプになっている ○ 以下のような式で導出可能 ミリ秒タイムスタンプ = ($SnowflakeID >> 22) + timestamp_ms(2010-11-04 01:42:54.657 UTC)

22.

Snowflake IDのレイアウト ● 64bitにレイアウトされている ○ ○ ● ● ● 昔はsignedで63bitレイアウトだったが、いつの間にか64bitになっていた ■ 元々Scala実装だったためJavaのLongになっていた これによりタイムスタンプが42bitになり寿命が倍に! タイムスタンプが先頭に来るので時系列にソート可能 ワーカー(アプリケーション)毎に独立したIDを持たせて重複を回避 同一ミリ秒内でも当然複数回発番できるようにシーケンス番号を持つ ○ 同一ミリ秒内で、4096個まで発番可能 ■ 仮に4096個を超えた場合は次のミリ秒まで待つ (現実的にはレアケース) タイムスタンプ (ミリ秒 ) ワーカー ID シーケンス番号 42bit 10bit 12bit

23.

実装例 ● https://github.com/twitter/snowflake ○ ○ ● https://github.com/mackerelio/snowflake ○ ● Mackerel開発チームによるメンテナンスFork https://github.com/kayac/go-katsubushi ○ ○ ● Twitterオリジナル実装 / Scala製 アーカイブされてREADMEだけ残されている・履歴は辿れる 複数社で採用実績がありオススメ Goライブラリとしても使えるし、他言語からも採番サーバーとして利用可能 https://github.com/sony/sonyflake ○ ○ Sony製!Go製 スターも多いし、メンテナンスもアクティブ 起点のタイムスタンプはデフォルトがそれぞれ異なるし、利用者側で設定も可能。

24.

サービス利用例 ● Discord ○ ● Instagram ○ ● Discord utilizes Twitter's snowflake format for uniquely identifiable descriptors (IDs). ■ ref. https://discord.com/developers/docs/reference ref. Sharding & IDs at Instagram Mastodon ○ ○ module Mastodon::Snowflake ref. https://github.com/mastodon/mastodon/blob/main/lib/mastodon/snowflake.rb ■ タイムスタンプが48bit割り当てられている

25.

Snowflakeの利点 ● ● ● ● 分散採番できる 時系列順にソートできる IDから生成時刻を逆算できる int64 / uint64 に収まっている ○ 既存のORMでも扱いやすい

26.

Snowflakeの弱点 ● 採番機の設定まわりでアーキテクチャがやや複雑になる ○ ○ ● 案外長持ちしない ○ ○ ○ ● ● 41bitタイムスタンプ (int64) だと69年 42bitタイムスタンプ(uint64) だと139年 ■ まあ大丈夫か ■ PostgreSQLにはunsignedが無い… ● numeric等を使う手もありそうだが Mastodonみたいにレイアウトを調整しても良い 手動でINSERTしたい時に一手間必要 (何れにせよやらない方がよいけど…) ミリ秒以下のソートは同じワーカーIDじゃないと保証されない ○ ● 特にワーカーIDの管理必要になる katsubushiはRedisによるID管理の仕組みがバンドルされている 許容できる 長いので覚えられない

27.

(余談) PostgreSQLにはUUID型がある! ● UUID v7を使えば同様の要件を簡単に満たせる ○ ○ ○ ○ ○ ● 時系列順にソート可能 IDからの生成時刻の逆算も可能 PostgreSQL 18以降はDB側でのUUID v7発番も可能 アプリケーション側での採番にも切り替えられる 寿命もSnowflake IDよりも長い デメリット ○ ○ ○ データサイズが128bitにはなる 既存のORMなどがまだしっかり対応していない 将来的にはサポートされていくかも

28.

Snowflake IDとパーティション 分散採番やUUIDは必ずしもシャーディングのためだけではない

29.

イベント追記テーブル ● ● 更新を行わず、事実(イベント)を追記し続けるテーブル 以下のような設計思想とマッチする ○ ○ ● ● ● ● イベントソーシング イミュータブルデータモデル イベントを積み上げておけば、現状を再構築できる 1つのイベントを複数のユースケースで利用できる 履歴テーブルと言われることもあるがニュアンスは少し異なる メッセージブローカーを立てずともRDBMSで完結できる

30.

イベント追記テーブルにおける課題 データが増え続け・膨大になる 1. 古いデータの消し込みをローコストで行いたい 2. 必要なデータに効率的にアクセスしたい

31.

パーティションとは? ● テーブルを内部的に「区切る」RDBMS組み込みの機能 ○ ● PostgreSQLにもMySQLにもある 内部的にユニークインデックスで分割された複数テーブルがあるイメージ

32.

パーティションで課題解決 1. 古いデータの消し込みをローコストで行いたい a. → 古いパーティションをDropすればOK 2. 必要なデータに効率的にアクセスしたい a. b. → 適切なインデックスアクセスをすれば必要な内部テーブルしか見に行かない Partition Pruning (刈り込み)

33.

パーティション具体例 ● PKにsnowflake IDを使ってRangeパーティションを切る ○ (実はPostgreSQLならUUIDでも良いです) 初期テーブル作成後、未来のパーティションの追加と、過去のパーティション の削除を定期的にバッチ処理する

34.

PostgreSQLでの作成例

35.

MySQLでの作成例

36.

パーティションの追加 (PostgreSQL) 単に新たにPartitionをCREATE TABLEする

37.

パーティションの追加 (MySQL) MySQL - catch all partitionを分割(REORGANIZE)する

38.

過去のパーティションの削除 (PostgreSQL) テーブルからDETACHして取り出してDROP

39.

過去のパーティションの削除 (MySQL) ALTER TABLE DROP PARTITION 構文 PostgreSQLもMySQLも他のパーティションのインデックス再構成などが行われない のでローコストで削除可能。

40.

パーティションの制約 ● パーティションに指定するキーをユニークキーに含める必要がある ○ ● id, created_at をPKにして create_at でパーティションを切る方法 の落とし穴 ○ ○ ● 内部の格納テーブルが一意に定まる必要があるのでそれはそう 良く紹介されているが id にユニークキー制約が効かなくなる…! ■ ex. (1, '2025-12-31'), (1, '2026-01-01') のように同じIDが入りうる → 時系列IDでやるのがめちゃくちゃオススメ ○ データとしても綺麗でインデックス効率も良い

41.

パーティションと外部キー制約 ● パーティションはその仕組み上外部キー制約実現が内部実装上難しい ○ ● PostgreSQL 11以降でサポート!(2018年) ○ ● 長らくサポートされていなかった ただしパーティションキーに限る (まあそれはそう) MySQLはサポートされていない

42.

何度かSoudaiさんに怒られました ● https://x.com/soudai1025/status/1959891948682285175 ○ これを機にまとめました

43.

まとめ

44.

Snowflake IDは便利 ● int64, uint64として使えるので既存のORMなどでも扱いやすい ○ ○ ● 特にイベント追記テーブルとの相性が良い ○ ● データ効率も悪くない 特にMySQLはunsigned BIGINT が使えるので長生きさせられる 時系列のUUIDや分散IDを使いたいのはシャーディング要件だけではない PostgreSQLであればUUID v7も選択肢になる ○ ○ ORMやエコシステムの対応が望まれる 受け入れられて行くでしょう