Cassandraのはじめ方─手を動かしてNoSQLを体感しよう

第8回 Cassandraで検索するには[後編]

この記事を読むのに必要な時間:およそ 5.5 分

get_range_slicesメソッドで注意すべきところ

ここまでの感じだとCassandraでもレンジクエリが簡単にできている感じがありますが,注意すべきところがあります。それはレンジクエリの順序の問題です。

Cassandraはデータをノード間に分散させる方式として,デフォルトではRandomPartitionerを使います。これは「パーティショナクラス」と呼ばれるもので,キーのハッシュ値を使って,値をノードにランダムに分散させます。

これを使う最大のメリットは,データが均等に分散しやすいため,各ノード間にかかる負担が均等になり,全体として効率がよいことです。ただしその一方で,⁠レンジクエリの順序が保障されない」というデメリットがあります。つまり,データの挿入順序が保障されず,結果がばらばらに返ってきてしまうのです。

以下はリスト1を実行した結果です。一目瞭然ですね。キーによる順序が一貫せずに値が返ってきています。

0 key = 1680082
        address -> 東京都 杉並区 久我山(1281276409219)
        addressYomi -> トウキョウト スギナミク クガヤマ(1281276409219)
        postalCode -> 1680082(1281276409219)
1 key = 1780062
        address -> 東京都 練馬区 大泉町(1281276409219)
        addressYomi -> トウキョウト ネリマク オオイズミマチ(1281276409219)
        postalCode -> 1780062(1281276409219)
2 key = 1750081
        address -> 東京都 板橋区 新河岸(1281276409219)
        addressYomi -> トウキョウト イタバシク シンガシ(1281276409219)
        postalCode -> 1750081(1281276409219)
3 key = 1540003
        address -> 東京都 世田谷区 野沢(1281276409219)
        addressYomi -> トウキョウト セタガヤク ノザワ(1281276409219)
        postalCode -> 1540003(1281276409219)

パーティショナクラスを使い分ける

では,他に方法がないのでしょうか?

実はCassandraは別の手段を準備してあります。それはOrderPreservingPartitionerというパーティショナを使う方法です。この方法ならば,レンジクエリも想定どおり正しく動いてくれます。

ただしメリットの裏にはデメリットもあるものです。OrderPreservingPartitionerを使うと,データの投入順序を保障するために,Cassandraはデータを均等に分散させるのではなく,決まった順序でデータをノードに投入していきます。

そのため,⁠あるノードにはデータが大量に存在し,あるノードにはあまりデータがないためにクエリの検索スピードや運用によるノードのリバランスなど考慮しなくてはいけない点が増える可能性があります。

実行すると,皆さんの予想どおり,順序が保障されて表示されます。

0 key = 1000000
        address -> 東京都 千代田区 (1281196753776)
        addressYomi -> トウキョウト チヨダク (1281196753776)
        postalCode -> 1000000(1281196753776)
1 key = 1000001
        address -> 東京都 千代田区 千代田(1281196753776)
        addressYomi -> トウキョウト チヨダク チヨダ(1281196753776)
        postalCode -> 1000001(1281196753776)
2 key = 1000002
        address -> 東京都 千代田区 皇居外苑(1281196753776)
        addressYomi -> トウキョウト チヨダク コウキョガイエン(1281196753776)
        postalCode -> 1000002(1281196753776)
3 key = 1000003
        address -> 東京都 千代田区 一ツ橋(1丁目)(1281196753776)
        addressYomi -> トウキョウト チヨダク ヒトツバシ(1チョウメ)(1281196753776)
        postalCode -> 1000003(1281196753776)

RandomPartitionerとOrderPreservingPartitionerのどちらを使うかは,Cassandraを使う場合に非常に難しい問題で,一概には答えられません。しかし,まずは両者の違いを把握しておくことが大事です。

また,パーティショナはCassandraではグローバルな設定でデータ構造にも影響を与えるので,storage-conf.xml全体で1つの設定になっています。どちらを使うか,注意深く考えて設計する必要があります。

特定のキーの行を複数取得する ~multiget_sliceメソッド

ある特定キーの集合を取得したいときに使うのがmultiget_sliceメソッドです。まずはコードを見てみましょう。

リスト2 特定のキーの集合データを取得する

public class SimpleAddressSearchMultiGetSlice {

    public static final String KEYSPACE = "Keyspace1";

    public static final String COLUMN_FAMILY = "SimpleAddress";

    public static void main(String[] args) throws IOException {
        TSocket transport = new TSocket("localhost", 9160);
        TProtocol protocol = new TBinaryProtocol(transport);
        try {
            transport.open();
        } catch (TTransportException e) {
            throw new RuntimeException(e);
        }
        try {
            Cassandra.Client client = new Cassandra.Client(protocol);
            SlicePredicate predicate = new SlicePredicate();
            predicate.setColumn_names(Arrays.asList("postalCode".getBytes(),
                    "address".getBytes()));
            Map<String, List<ColumnOrSuperColumn>> ret = client.multiget_slice(
                    KEYSPACE, Arrays.asList("1080074", "1080075"),
                    new ColumnParent(COLUMN_FAMILY), predicate,
                    ConsistencyLevel.ONE);

            for (Map.Entry<String, List<ColumnOrSuperColumn>> e : ret
                    .entrySet()) {
                String key = e.getKey();
                System.out.println("key = " + key);
                for (ColumnOrSuperColumn csc : e.getValue()) {
                    Column c = csc.getColumn();
                    String name = new String(c.getName());
                    String value = new String(c.getValue());
                    System.out.println(name + " -> " + value + "("
                            + c.getTimestamp() + ")");
                }
                System.out.println();
            }
        } catch (InvalidRequestException e) {
            throw new RuntimeException(e);
        } catch (UnavailableException e) {
            throw new RuntimeException(e);
        } catch (TimedOutException e) {
            throw new RuntimeException(e);
        } catch (TException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                transport.flush();
            } catch (TTransportException e) {
                throw new RuntimeException(e);
            } finally {
                transport.close();
            }
        }
    }
}

リスト2の例は

  • キーが1080074,1080075のロウを検索
  • その中の郵便番号と住所のカラムを取得
  • という処理を行うものです。⁠get_sliceを複数キーに対応させたAPI」だと思っていただければ間違いありません。

    カラム数を取得するには ~get_countメソッド

    カラム数を取得するには,get_countメソッドを使います。シンプルに「キーに対するロウ内にどれだけのカラムがあるか」を返します。以下はコードの抜粋です。

    Cassandra.Client client = new Cassandra.Client(protocol);
    
    int count = client.get_count(KEYSPACE, "1500002", new ColumnParent(
            COLUMN_FAMILY), ConsistencyLevel.ONE);
    

    今回は,簡単ですが,get_range_slices,multiget_slice,get_countの3つのメソッドの使い方を見ていきました。

    この中でやはり重要なのが,レンジクエリを実現するget_range_slicesです。レンジクエリを多用するシステムではCassandraのパーティショナを変更する必要があるでしょう。ただしその場合,運用保守で定期的にノードを再バランシングする必要が出てくるかもしれません。

    今回までは,ThriftによるCassandraのネイティブなAPIを見ていきました。次回からThrift APIを隠蔽してくれるフレームワークやライブラリを見ていきます。

    著者プロフィール

    大谷晋平(おおたにしんぺい)

    オープンソースプログラマ。WebフレームワークT2の開発をしながらHadoop/NoSQLミドルウェアにも手を出す。最近ではもっぱらHadoop,Cassandra,Avro,kumofsなどに興味津々。

    blog:http://d.hatena.ne.jp/shot6/

    Twitter:http://twitter.com/shot6/