Apache Cassandra:大規模システムにおける「書き込み負荷(Write-Heavy)」対策の決定版

Database tutorial - IT technology blog
Database tutorial - IT technology blog

データの大嵐にSQLが「降参」するとき

以前、私たちのチームはPostgreSQLを使用してユーザー行動のトラッキングシステムを運用していました。トラフィックが毎秒5万ログに達するまでは順調でしたが、そのあたりでデータベースが「息切れ」し始めました。新しいデータが流れ込むたびにSQLのB-Treeインデックスを再構築する必要があり、ディスクI/Oが90%を超えてしまったのです。

MongoDBで試行錯誤したものの分散処理の課題を解決できず、最終的にApache Cassandraへの移行を決めました。これはFacebookが受信トレイ検索用に開発し、現在はNetflixやAppleのインフラを支える大規模システム向けのトップクラスの選択肢です。

IoT、ロギング、メッセージングなど、超高速な書き込み速度と、サーバーを追加するだけで拡張できるスケーラビリティ(Scale-out)が必要な場合、Cassandraこそがその答えです。

Cassandra의 アーキテクチャ:なぜこれほど書き込みが速いのか?

MySQLやRedis SentinelのようなMaster-Slave型の考え方は一度忘れてください。CassandraはPeer-to-Peer方式で動作し、すべてのノードが対等な役割を担います。この設計により「単一障害点(Single Point of Failure)」を完全に排除しています。1つのノードがダウンしても、他のノードが通常通り負荷を引き受け、新しいMasterの選出を待つ必要もありません。

データ書き込みの仕組み:秘密はLSM-Treeにあり

Cassandraの書き込みが従来のデータベースより遥かに速いことに驚く人も多いでしょう。それはLSM-Tree構造のおかげです。書き込みプロセスは以下の通りです:

  • データ損失を防ぐため、まずディスク上のCommitLogに書き込まれます。
  • 同時に、RAM上のMemTableに保存されます。
  • MemTableがいっぱいになると、Cassandraは内容をSSTable(Sorted String Table)ファイルとしてディスクにフラッシュします。

重要なポイントは、ディスクへの書き込みがシーケンシャルアクセス(連続書き込み)で行われることです。これは、修正箇所を探してページをめくる(ランダムシーク)のではなく、ノートの最後のページに追記していくようなものです。これにより、書き込み速度はハードウェアの限界に近いレベルまで達します。

データモデル設計:SQLとは真逆の思考法

Cassandraを使い始めた際によくある間違いは、SQLの正規化(Normalization)の考え方をそのまま持ち込んでしまうことです。Cassandraでは、エンティティに基づいてテーブルを設計しません。クエリに基づいて設計する(Query-driven modeling)のです。

CassandraはJOINをサポートしていません。そのため、非正規化(Denormalization)を受け入れる必要があります。データの重複を恐れないでください。現代のストレージは非常に安価であり、私たちが優先すべきは超高速なアクセス速度です。

パーティションキー(Partition Key)とクラスタリングキー(Clustering Key)

プライマリキー(Primary Key)は、以下の2つの重要な要素で構成されます:

  1. Partition Key: データがどのノードに配置されるかを決定します。「性別」のような分散度の低いキーを選ぶと、特定のノードにデータが集中(ホットスポット)し、局所的な過負荷を引き起こします。
  2. Clustering Key: ノード内でのデータの並び順を決定します。これは範囲クエリ(Range query)において非常に強力な味方となります。

実戦のヒント: statusのような一般的なフィールドではなく、user_idsensor_idのようなカーディナリティ(分散度)が高いものをPartition Keyに選びましょう。

Dockerでクイックセットアップ

最も手軽に試す方法は、Docker Composeを使って2ノードのクラスターを構築することです。

version: '3.9'
services:
  cassandra-node1:
    image: cassandra:latest
    container_name: cassandra1
    ports:
      - "9042:9042"
    environment:
      - CASSANDRA_CLUSTER_NAME=MyCluster
      - CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch

  cassandra-node2:
    image: cassandra:latest
    container_name: cassandra2
    environment:
      - CASSANDRA_CLUSTER_NAME=MyCluster
      - CASSANDRA_SEEDS=cassandra-node1
      - CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch
    depends_on:
      - cassandra-node1

docker-compose up -dを実行し、docker exec -it cassandra1 nodetool statusコマンドで確認するだけです。ステータスにUN(Up/Normal)が表示されれば、システムの準備は完了です。

CQL言語によるクエリの実践

CassandraはCQL(Cassandra Query Language)を使用します。構文はSQLに似ていますが、パフォーマンスを確保するために一部の機能が制限されています。以下は、IoTデバイスの温度ログを保存するテーブルの作成例です:

-- キースペースの作成(データベースに相当)
CREATE KEYSPACE sensor_data 
WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 2};

USE sensor_data;

-- テーブル作成:device_idがパーティションキー、captured_atがクラスタリングキー
CREATE TABLE temperature_logs (
    device_id uuid,
    captured_at timestamp,
    value double,
    PRIMARY KEY (device_id, captured_at)
) WITH CLUSTERING ORDER BY (captured_at DESC);

-- テストデータの挿入
INSERT INTO temperature_logs (device_id, captured_at, value) 
VALUES (uuid(), toTimestamp(now()), 25.5);

この例では、同一デバイスのデータがディスク上の隣接した領域に保存され、最新の時間順にソートされるため、履歴の照会が非常に効率的になります。

実戦経験から得た最適化のコツ

システムダウンという「高い授業料」を払って学んだ、4つの重要な教訓を共有します:

  • ALLOW FILTERINGにはNOを: 本番環境でこのコマンドが必要になるなら、データモデルの設計が間違っています。Cassandraにクラスター全体のフルスキャンを強いることになり、パフォーマンスが劇的に低下します。
  • パーティションサイズを制御する: 1つのパーティションは100MBを超えないようにすべきです。パーティションが大きすぎると、ノード間のデータ移動(リバランシング)が地獄になります。
  • バッチ(Batch)を多用しない: SQLとは異なり、Cassandraのバッチは複数のテーブル間での原子性(Atomicity)を確保するためのものです。書き込み速度の向上には繋がらず、レコードが複数のノードに分散している場合は逆に遅くなります。
  • Java Heapを適切に設定する: JVM HeapにはRAMの1/4から1/2程度を割り当てます(ただし32GBを超えないように)。これにより、ガベージコレクション(GC)による長時間の停止を防ぐことができます。

最後に

Cassandraは万能な解決策ではありません。書き込み負荷の高いシナリオには非常に強力ですが、複雑な統計レポート(SUM/AVG)や、頻繁なJOINが必要なクエリには不向きです。クエリ主導のモデリング思考をマスターすれば、数十億件のレコード管理も難しい課題ではなくなります。皆さんの成功を祈っています!

Share: