1. はじめに
なぜ今、分散ID生成が重要なのか?
マイクロサービスやクラウドネイティブなアーキテクチャが一般化する中で、「一意なIDをどのように生成・管理するか」は、システム設計において避けて通れない課題です。
例えば、データベースやメッセージングキュー、ログトレース、外部連携APIなど、さまざまなサービス間で「同じIDで同じエンティティを識別できる」ことが求められます。
これまでのように、1台のデータベースでオートインクリメントIDを使う手法では、サービスがスケールアウトしたときにIDの衝突やボトルネックを引き起こします。
そこで登場するのが、分散ID生成システムです。
マイクロサービスアーキテクチャと一意なIDの必要性
マイクロサービスは、各機能を独立したサービスとして分離・スケーラブルに運用することを目指します。
その際、サービス横断で一意なIDを生成する仕組みがないと、次のような課題が発生します。
- サービス間で同一エンティティを特定できない
- データのマージや集計時に衝突する
- ログやトレース情報の整合性が保てない
これを解決するのが、「時系列でソート可能」「グローバルにユニーク」「高速」「冗長化しやすい」といった性質を持つ Snowflake アルゴリズムです。
この記事で構築するもの
本記事では、Twitterが設計した「Snowflake アルゴリズム」をベースにした、チケットサーバー+Snowflakeによる分散ID生成システムを構築します。
Docker Composeで簡単に動作検証可能な環境を構築しながら、アーキテクチャ設計・実装のポイントを詳しく解説します
2. 分散ID生成システムに求められる要件
分散ID生成を正しく行うためには、以下のような要件を満たす必要があります。
グローバルな一意性 (Uniqueness)
全サービス・全ノードを通じて、IDが重複しないこと。重複はデータ破壊に直結するため、最優先で保証されるべき要素です。
順序性 (Ordering)
生成されたIDが、おおよそ時間の経過に沿って増加することで、データの整列・集計・ソートが容易になります。
完全な時系列性ではなく「概ねソート可能」であることが実運用では重要です。
高可用性 (High Availability)
一部のID生成ノードが落ちても、他ノードが稼働し続け、システム全体としてID発行が止まらないよう設計すべきです。
低遅延 (Low Latency)
ID生成はあらゆる操作の前提となるため、できる限り高速でレスポンスが良いことが求められます。
耐障害性とスケーラビリティ (Fault Tolerance & Scalability)
多数の同時リクエストに対してもスループットが落ちず、将来的にノード数やトラフィックが増えても、水平スケーリングが容易であるべきです。
3. 代表的な分散ID生成アルゴリズムの比較
ここでは、代表的な4種類のID生成方法を比較し、それぞれの適用シーンや課題を明らかにします。
UUID (Universally Unique Identifier)
仕組み:ランダム(またはハッシュ)をベースに生成される128bitのID
メリット:世界中で衝突しにくい、単一ノードで完結、依存が少ない
デメリット:ソートできない(順序性がない)、長いため、DBインデックスの性能に悪影響を与える
データベースの自動採番 (Auto-increment)
仕組み:RDBMSのAUTO_INCREMENTやSERIALによる連番生成
メリット:シンプルで直感的、ソートが容易
デメリット:スケールしない(中央集権的)、単一障害点になる
Flickrのチケットサーバー方式
仕組み:中央に「チケットサーバー」を設けてIDの連番を払い出す
メリット:一意性と順序性の両立が可能、UUIDよりも短く、扱いやすい
デメリット:チケットサーバーが単一障害点となる、負荷分散・冗長化には追加設計が必要
TwitterのSnowflakeアルゴリズム
仕組み:IDを64bitにエンコードし、ビットごとに以下の情報を含む
timestamp | datacenter ID | worker ID | sequence
メリット:時系列順にソート可能、高速・スケーラブルな分散生成、オフラインでも生成可能
デメリット:時計のずれによる順序逆転の可能性、ワーカーIDの重複を防ぐ仕組みが必要(→本記事ではチケットサーバーで解決)
4. 【実装編】Docker Composeによる環境構築
services:
snowflake:
build: ./snowflake
volumes:
- ./snowflake:/app
ports:
- "9001:8080"
ticket-server:
build: ./ticket_server
volumes:
- ./ticket_server:/app
ports:
- "9002:8080"
client:
build: ./client
volumes:
- ./client:/app
command: ["python", "app.py"]
from flask import Flask
from flask import jsonify
import time
import threading
app = Flask(__name__)
# Snowflakeパラメータ(簡易版)
EPOCH = 1609459200000 # 2021-01-01
machine_id = 1
sequence = 0
last_timestamp = -1
lock = threading.Lock()
def current_millis():
return int(time.time() * 1000)
def next_id():
global sequence, last_timestamp
with lock:
timestamp = current_millis()
if timestamp == last_timestamp:
sequence += 1
else:
sequence = 0
last_timestamp = timestamp
id = ((timestamp - EPOCH) << 22) | (machine_id << 12) | sequence
return id
@app.route("/generate", methods=["GET"])
def generate_id():
id = next_id()
return jsonify({"id": id})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=True)
from flask import Flask, jsonify
import threading
app = Flask(__name__)
counter = 100000
lock = threading.Lock()
@app.route("/generate", methods=["GET"])
def generate_id():
global counter
with lock:
counter += 1
return jsonify({"id": counter})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=True)
import requests
import time
snowflake_url = "http://snowflake:8080/generate"
ticket_url = "http://ticket-server:8080/generate"
if __name__ == "__main__":
print("Starting ID generation client...")
time.sleep(2) # Wait for services to start
print("Generating IDs from Snowflake and Ticket Server:")
for _ in range(5):
sf = requests.get(snowflake_url).json()
tk = requests.get(ticket_url).json()
print(f"Snowflake ID: {sf['id']}, Ticket ID: {tk['id']}")
time.sleep(1)
print("ID generation completed.")
5. テストと考察
動作確認:実際にシステムを起動し、IDを生成
Docker Composeで立ち上げた以下の3つのサービス:
ticket-server:ワーカーIDを払い出す中央サーバ
snowflake:SnowflakeアルゴリズムによるID生成器
client:ID生成のリクエストを出す簡易クライアント
を使用して、実際にIDを生成してみます。以下は、クライアントログの出力例です:
client-1 | Starting ID generation client...
client-1 | Generating IDs from Snowflake and Ticket Server:
client-1 | Snowflake ID: 607039340006084608, Ticket ID: 100001
client-1 | Snowflake ID: 607039344259108864, Ticket ID: 100002
client-1 | Snowflake ID: 607039348512133120, Ticket ID: 100003
client-1 | Snowflake ID: 607039352765157376, Ticket ID: 100004
client-1 | Snowflake ID: 607039357030764544, Ticket ID: 100005
client-1 | ID generation completed.
課題と改善案:
現状の実装ではシンプルに動作しますが、プロダクション環境での運用にはいくつかの課題があります。以下に代表的な懸念点と改善アプローチをまとめます。
1.チケットサーバーの単一障害点 (SPOF): どう克服するか? (e.g., ZooKeeper, etcdによる管理)
現在、ticket-server は1台構成で、ダウンすると新たなワーカーがIDを取得できません。これは典型的な単一障害点 (Single Point of Failure)です。
改善案:
分散KVSとの連携:
etcd や ZooKeeper などを利用して、ワーカーIDを分散管理する。
リーダー選出やロック処理によって、ワーカーIDの重複割り当てを防ぐ。
キャッシュとリトライ機構の導入:
ワーカーが一度取得したIDをローカルにキャッシュし、障害時に再利用する。
2.時計の同期問題: NTPの重要性と、時計が逆行した場合の対策
Snowflake IDの最上位ビットはタイムスタンプを表しており、これが正しくないとIDが逆行(前のIDよりも小さい)する可能性があります。
改善案:
NTPの設定の徹底:
全てのノードで NTP サービスを有効にし、時刻同期を保証。
時計逆行時の対策:
一定時間のリトライ(数ミリ秒間待って再生成)
スタンバイ中のワーカーモード切替(例:フェイルオーバー)
3.ワーカーIDの再利用: ワーカーが再起動した際のワーカーIDの扱い
Snowflakeでは、ワーカーIDが重複すると、同一ミリ秒内に同一シーケンスで生成されたIDが衝突する可能性があります。
ワーカーが再起動して以前のIDを再取得した場合、衝突のリスクがあります。
改善案:
ワーカーIDのリース制管理:
ワーカーIDを一度割り当てた後、一定時間で期限切れとなる仕組みを導入。
再起動時のID再取得と検証:
再起動時に ticket-server に問い合わせて新しいIDを取得し、キャッシュをクリア。
クラッシュレジリエンス:
Redisやetcdに「使用中のID一覧」を保存し、リスタート後も状態を復元できるようにする。
結論
この章では、構築した分散ID生成システムの動作を検証し、その上でプロダクション運用に向けた課題と改善ポイントを洗い出しました。
Snowflakeのようなアルゴリズムは「速くて一意」であると同時に、インフラ設計と運用体制がその信頼性を支えています。