ヒマをみつけてWeb開発
その場の思い付きを、ヒマをみつけてWebサイトにしてみるブログ

Cassandraemon 1.0.6をリリースしました

Friday, 23 December 2011 21:01 by sabro

0.8.7をリリースしたときに1.0はしばらく先になりますと書いたのですが、Cassandra1.0はThrift APIのインターフェースにほとんど変更がなかったことと、メンバーの@kojiishiさんが頑張って新機能を作ってくれたこともあって、早くもリリースすることになりました( ̄∇  ̄ )

Cassandraemon - Download: Cassandraemon 1.0.6

オブジェクトマッピングの高速化

Cassandraemonでは、オブジェクトを登録するとき、1プロパティを1カラムに格納するのですが、これまではリフレクションを使っていました。しかし、これでは遅いのでReflection.Emitで動的に変換用のアセンブリを作る方針に変更。パフォーマンスが大幅にアップしました。他にも、値型をByte配列に変換するロジックなどにも手を入れて、細かな高速化を行っています。

Serialize方法をカスタム可能に

オブジェクトをプロパティごとにシリアライズして格納する際、たとえば特定のプロパティを格納しないなどのカスタマイズが可能になりました。

// IgnoreDataMemberAttributeがついていないプロパティを取得
var properties = type.GetProperties()
     .Where(p => !p.GetCustomAttributes(
         typeof(IgnoreDataMemberAttribute), true).Any());

// Reflection.Emitを内部で使用するCompiledCassandraSerializerFactoryを使う
var factory = CompiledCassandraSerializerFactory.Default;

// 型とシリアライズするプロパティを渡してシリアライザを作る
var serializer = factory.CreateSerializer(type, properties);

// シリアライザをセット
factory.SetSerializer(type, serializer);

より詳しくは、公式のドキュメントをお読みください。

さて、次回のリリースは本当に先になるはず。気長にお待ちください( ̄□  ̄ ||

Tags:   , , , ,
Categories:   NoSQL
Actions:   Permalink | Comments (4) | Comment RSSRSS comment feed

Cassandraemon0.8.7をリリースしました

Saturday, 3 December 2011 20:44 by sabro

Cassandraemonの初の0.8系対応バージョンをリリースしました。

Cassandraemon - Download: Cassandraemon 0.8.7

Thrift APIが0.8.7バージョンのものを使っているからこんなバージョンになってますが、実質的にCassandra0.8に対応する初のリリースです。新機能は、New Feature - ver 0.8に書いてありますが、英語なので、簡単に日本語で説明しときます。

CQL

Cassandra0.8のウリであるCQLに対応しました。

using(var context = new CassandraContext("localhost", 9160, "Keyspace1"))
{
	// insert
	string insertCql = "insert into ColumnFamily1 (KEY, name1, name2) values ('key1', 'value1', 'value2')"; 
	CqlResult insertResult = context.ExecuteCqlQuery(insertCql);
	
	// get count
	string countCql = "select count(*) from ColumnFamily1 where KEY = 'key1'";
	CqlResult countResult = context.ExecuteCqlQuery(countCql);
	Console.WriteLine(countResult.Num);	// 2
	
	// get data
	string retrieveCql = "select * from ColumnFamily1 where KEY = 'key1'";
	CqlResult retrieveResult = context.ExecuteCqlQuery(retrieveCql);
	
	var dictionary = retrieveResult.ToFlatDictionary<string, string>();
	dictionary.ToList().ForEach(kv => Console.WriteLine(kv.Key + " = " + kv.Value));
	// name1 = value1
	// name2 = value2
}

CQLの結果は、Thriftで生成したコードで定義されているCqlResultで受け取るのですが、そのままだと扱いにくいので、Object、List、Dictionaryに変換できる拡張メソッドを用意してあります。

ToCassandraEntiry ()
ToFlatNameList<T> ()
ToFlatValueList<T> ()
ToFlatDictionary<TKey, TValue> ()
ToFlatValueKeyDictionary<TKey, TValue> ()
ToNameListDictionary<TKey, TListItem> ()
ToValueListDictionary<TKey, TListItem> ()
ToObjectList<T> ()
ToObjectDictionary<TKey, TValue> ()

Counter

Cassandra0.8の、もうひとつのウリであるCounterです。普通のColumnと同様の感覚で使えます。

// insert 
var cc1 = new CounterColumn().SetNameValue("one", 1);
var cc2 = new CounterColumn().SetNameValue("two", 2);
var cc3 = new CounterColumn().SetNameValue("three", 3);

context.InsertOnSubmit("CounterColumnFamily1", "key1", new[]{cc1, cc2, cc3});
context.SubmitChanges();


// get data
var query = from x in context.CounterColumnList	// <- attention "CounterColumnList"
	    where x.ColumnFamily == "CounterColumnFamily1" &&
		  x.Key == "key1"
	    select x.ToFlatDictionary<string, long>();

query.First()
     .ToList()
     .ForEach(kv => Console.WriteLine(kv.Key + " = " + kv.Value));
// one = 1
// three = 3
// two = 2

Eventを追加

Eventをいくつか追加しました。サーバに接続したタイミング、切断したタイミングなどで処理を実行できます。

ConnectionPool.Connected += (sender, e) => Console.WriteLine("Connected");
ConnectionPool.ConnectionFailed += (sender, e) => Console.WriteLine("ConnectionFailed");

CassandraContext.Created += (sender, e) => Console.WriteLine("Created");

using(var context = new CassandraContext("localhost", 9160, "Keyspace1"))
{
	context.Executed += (sender, e) => Console.WriteLine("Executed");
	context.Disposed += (sender, e) => Console.WriteLine("Disposed");
	
	var column = new Column().SetNameValue("name1", "value1"); 
	context.InsertOnSubmit("ColumnFamily1", "event", column);
	context.SubmitChanges();
	
	Console.WriteLine("Submitted");
}
// Connected
// Created
// Executed
// Submitted
// Disposed

新しいInsert API

Insertするとき、今まではCassandraEntityオブジェクトを作成しないといけなかったのですが、ColumnFamily、Keyを別途指定できるメソッドを用意しました。いちいちCassandraEntityオブジェクトを作らなくても、Column、SuperColumnなどを直接登録できます。

using(var context = new CassandraContext("localhost", 9160, "Keyspace1"))
{
	// old version
	context.Column.InsertOnSubmit(cassandraEntity);
	context.SuperColumn.InsertOnSubmit(cassandraEntity);
	context.ColumnList.InsertOnSubmit(cassandraEntity);
	context.SuperColumnList.InsertOnSubmit(cassandraEntity);

	// new version
	context.InsertOnSubmit( "ColumnFamily1", "key1", column );
	context.InsertOnSubmit( "ColumnFamily2", "key2", superColumn );
	context.InsertOnSubmit( "ColumnFamily3", "key3", columnList );
	context.InsertOnSubmit( "ColumnFamily4", "key4", superColumnList );
}

CassandraConnectionConfig

設定項目が増えてきたので、CassandraConnectionConfigにまとめました。CassandraConnectionConfigBuildeクラスを使って生成できます。

var builder = new CassandraConnectionConfigBuilder
{
	Hosts = new [] { "localhost" }
	Port = 9160,
	ConsistencyLevel = ConsistencyLevel.QUORUM,
	Timeout = TimeSpan.FromSeconds(100),
};
var config = new CassandraConnectionConfig(builder);
using (var ctx = new CassandraContext(config)) { ... }

設定項目の一覧です

Name Description Default Value
Hosts 接続するホスト string[] { "localhost" }
Port Thriftのポート番号 9160
Keyspace 接続するKeyspace "system"
ConsistencyLevel デフォルトのConsistencyLevel ( 実行中にいつでも再設定できます ) ConsistencyLevel.One
RetryCount 接続エラー時のリトライ数 -1 ( this mean nodecount * 2 )
IsFramed フレーム転送使用 true
Timeout 接続待ちのタイムアウト値 10 seconds
MinPoolCountPerHost プールするコネクションの最小値 2
MaxPoolCountPerHost プールするコネクションの最大値 int.MaxValue
Node Node取得の方式 empty string

ConnectionPoolの修正

コネクションプーリングが不安定だったのですが、地道にバグを取り、テストコードも追加して、安定性が向上しました。

今回のリリースでは、新たにメンバーに加わった@kojiishiさんに、コネクションプーリングを中心に色々手伝って頂きました。さて、やっと0.8系をリリースできましたが、セカンドライフ向けWebサービスの開発が忙しいので、1.0系はまだ少し先になりそうな感じです( ̄□  ̄ ||

Tags:   , , , ,
Categories:   NoSQL
Actions:   Permalink | Comments (0) | Comment RSSRSS comment feed

Cassandraemon初の0.7系リリース

Wednesday, 13 April 2011 04:34 by sabro

Cassandraemonの初の0.7系対応バージョンをリリースしました。

Cassandraemon - Download: Cassandraemon 0.7.4

バージョンが0.7.4になってますが、Thrift APIが0.7.4バージョンのものを使っているからであって、実質的にCassandra0.7に対応する初のリリースです。新機能は、New Feature - ver 0.7に書いてありますが、英語なので、簡単に日本語で説明しときます。

New Link Style

Cassandraのカラム名はByte配列であるため、Cassandraemon0.6では、比較の際、ToCassandraByte関数を呼んだり、GreaterThanOrEqual関数を呼んだりする必要がありました。0.7ではCassandraBinaryという、各種演算子をオーバーロードしたByte配列のラッパークラスを作成することで、LINQをシンプルに書けるようにしました。

// ver 0.6
from x in context.ColumnList
where x.ColumnFamily == "Product" &&
      x.Key == "key1" &&
      x.SuperColumn == "sc1".ToCassandraByte() &&
      x.Column.GreaterThanOrEqual(1)
select x;

// ver 0.7
from x in context.ColumnList
where x.ColumnFamily == "Product" &&
      x.Key == "key1" &&
      x.SuperColumn == "sc1" &&
      x.Column >= 1
select x;

Secondary Index

Cassandra0.7の目玉機能、Secondary Indexに対応しました。Where句に以下のように指定することで、事前に作られたインデックスを使って値を取得できます。

from x in context.ColumnList
where x.ColumnFamily == "Product" &&
      x.Index["ColumnName1"] == "col1" &&
      x.Index["ColumnName2"] >= 1 &&
      x.Index["ColumnName3"] < DateTime.Now &&
      x.Column.In("ColumnName1", "ColumnName2")
select x;

Live Schema Update

Live Schema Updateに対応しました。といっても、ThriftのApiをラップしたものを、CassandraContextクラスに用意しただけです・・・。

class CassandraContext
{
	public string SystemAddKeyspace(KsDef ksDef)
	public string SystemUpdateKeyspace(KsDef ksDef)
	public string SystemDropKeyspace(string keyspace)
	public string SystemAddColumnFamily(CfDef cfDef)
	public string SystemUpdateColumnFamily(CfDef cfDef)
	public string SystemDropColumnFamily(string columnFamily)
	public void Truncate(string columnFamily)
}

TTLに対応

カラムの生存時間を指定するTTLに対応しました。といっても、ウチが何かしたわけじゃなくて、ThriftAPIのColumnクラスにTTLプロパティが追加されただけです。一応、List<Column>クラスに、全てのリスト内のColumnに対してTTLをセットする拡張メソッドを追加したりはしてます。

multiget_coutに対応

Keyを複数指定して、CountColumn<TKey>メソッドを呼ぶことで、複数行のCountを一気に取ってこれます。multiget_count関数はThriftApiに用意されているんですが、なぜか今のところ公式のドキュメントに記載がないみたいです。

var query = from x in context.ColumnList
            where x.ColumnFamily == "Product" &&
                  x.Key.In("key1", "key2") &&
                  x.Column >= 1
            select x;

Dictionary<string, int> d = query.CountColumn<string>();

さて、やっと0.7系をリリースできましたが、Cassandraemonは、0.8が出るまでしばらく沈黙が続きそうな感じです( ̄□  ̄ ||

Tags:   , , , ,
Categories:   NoSQL
Actions:   Permalink | Comments (15) | Comment RSSRSS comment feed

Cassandraでbatch_mutateにInsertとDeleteをセットするとどちらが先に実行されるか

Wednesday, 23 June 2010 03:40 by sabro

Cassandraの、batch_mutateには、map<key : string, map<column_family : string, list<Mutation>>>みたいな、引数を渡します。このMutationは、登録用のColumnリストか、削除用のDeletionオブジェクトを格納出来るようになっているので、同じカラムに対して、同時に登録と削除を指定することも出来るわけです。

では、実際に同時に指定すると、どうなるんでしょうか。削除されてから登録されるのか、登録されてから削除されるのか、実験してみました。

// *** 一部、CassandraemonのAPIを使用 *** //

var socket = new TSocket("localhost", 9160);
var protocol = new TBinaryProtocol(socket);
var client = new Cassandra.Client(protocol);

socket.Open();	

// Insert用のMutation作成
var column = new ColumnOrSuperColumn
	   {
		   Column = new Column().SetNameValueTimestamp
				    (
				    "c1",
				     "10",
				     DateTime.Now.Ticks
				    )
	   };
var insertMutation = new Mutation { Column_or_supercolumn = column };

// Delete用のMutation作成
var deletion = new Deletion
	   {
		   Timestamp = DateTime.Now.Ticks,
		   Predicate = new SlicePredicate
		   {
			   Column_names = new List<byte[]>
					   {
						   "c1".ToCassandraByte()
					   }
		   }
	   };
var deleteMutation = new Mutation { Deletion = deletion };


// Insert → Delete順のMap
var map1 = new Dictionary<string, Dictionary<string, List<Mutation>>>()
{
	{
		"k1",
		new Dictionary<string, List<Mutation>>()
		{
			{
			  "cf",
			  new List<Mutation>() { insertMutation, deleteMutation }
			}
		}
	}
};

// Delete → Insert順のMap
var map2 = new Dictionary<string, Dictionary<string, List<Mutation>>>()
{
	{
		"k1",
		new Dictionary<string, List<Mutation>>()
		{
			{
			  "cf",
			  new List<Mutation>() { deleteMutation, insertMutation }
			}
		}
	}
};


// ColumnPathを作っとく
var path = new ColumnPath { Column_family = "cf", Column = "c1".ToCassandraByte() };


// Insert → Delete順のMapを実行して、データを取得してみる
client.batch_mutate("ks1", map1, ConsistencyLevel.ONE);
try
{
	var CorS1 = client.get("ks1", "k1", path, ConsistencyLevel.ONE);
	Console.WriteLine("Found.");
}
catch(NotFoundException)
{
	Console.WriteLine("Not Found.");
}


// Delete → Insert順のMapを実行して、データを取得してみる
client.batch_mutate("ks1", map2, ConsistencyLevel.ONE);
try
{
	var CorS1 = client.get("ks1", "k1", path, ConsistencyLevel.ONE);
	Console.WriteLine("Found.");
}
catch(NotFoundException)
{
	Console.WriteLine("Not Found.");
}

socket.Close();

/* result
Not Found.
Not Found.
*/

Mutationのリストに、登録用のMutationから突っ込んでも、削除用から突っ込んでも、結局は登録したカラムが削除されてしまっています。さらに、上記コードをFor文で囲って100回連続で実行しても、1回も登録の方が後に行われたケースはありませんでした。つまり、削除操作が必ず後で実行されるってことでしょうか。

ソースをちらっと見た感じでは特に順序制御とかしてないような感じだったんですが、削除がメチャクチャ遅いだけだったりして( ̄∇  ̄ ) たしか、Cassandraの削除はフラグを立てるだけだった気がするので、その辺の振る舞いが関係してたりもするのかも。なんにせよ、Updateの感覚でDelete→Insertとかしたいときは、操作を分ける必要があるってことですね。Cassandraemonの場合だと、DeleteOnSubmitのあと、一回SubmitChangesしてから、InsertOnSubmitみたいな手順になります、ご注意ください。

あ、あと、これはあくまで、バージョン0.6.2での結果なんで、他では変わるかもです。

Tags:   ,
Categories:   NoSQL
Actions:   Permalink | Comments (48) | Comment RSSRSS comment feed

Cassandra0.6系では、格納時にKeyがTrimされる

Friday, 18 June 2010 01:40 by sabro

Cassandraemonの新機能、全文検索を実装してたんですが、N-Gramっていう、二文字ずつ区切ってインデックスを作るアルゴリズムで作ってみたら、スペースを含むフレーズ検索がうまくいきませんでした。

で、ちょっと調べてみたら、Cassandraの0.6系では、Keyは格納前にTrimされてるようです。RowMutation.getRowMutationの一行目で確かにTrimされてますねえ。

public static RowMutation getRowMutation(String table, String key, Map<String, List<ColumnOrSuperColumn>> cfmap)
{
    RowMutation rm = new RowMutation(table, key.trim());
    for (Map.Entry<String, List<ColumnOrSuperColumn>> entry : cfmap.entrySet())
    {
        String cfName = entry.getKey();
        for (ColumnOrSuperColumn cosc : entry.getValue())
        {
            if (cosc.column == null)
            {
                assert cosc.super_column != null;
                for (org.apache.cassandra.thrift.Column column : cosc.super_column.columns)
                {
                    rm.add(new QueryPath(cfName, cosc.super_column.name, column.name), column.value, column.timestamp);
                }
            }
            else
            {
                assert cosc.super_column == null;
                rm.add(new QueryPath(cfName, null, cosc.column.name), cosc.column.value, cosc.column.timestamp);
            }
        }
    }
    return rm;
}

つまり、Cassandra0.6系では、N-Gramで、スペースを含んだフレーズ検索は、実装不可能ってことのようです( ̄□  ̄ || 一応、インデックスをSuperColumnで作れば出来るけど、それだと偏りがえらいことなりそうだから、現実的じゃないですしねえ。

ただし、Cassandra0.7系では、Keyがバイト配列になるので、Trimは実行されません。なんで、たぶんスペースを含んでも検索できるんじゃないかなと思います。下記0.7のソースでは引数のkeyがバイト配列なんで、Trimが呼ばれてないことが確認できますね( ̄∇  ̄ )

public static RowMutation getRowMutation(String table, byte[] key, Map<String, List<ColumnOrSuperColumn>> cfmap)
{
    RowMutation rm = new RowMutation(table, key);
    for (Map.Entry<String, List<ColumnOrSuperColumn>> entry : cfmap.entrySet())
    {
        String cfName = entry.getKey();
        for (ColumnOrSuperColumn cosc : entry.getValue())
        {
            if (cosc.column == null)
            {
                assert cosc.super_column != null;
                for (org.apache.cassandra.thrift.Column column : cosc.super_column.columns)
                {
                    rm.add(new QueryPath(cfName, cosc.super_column.name, column.name), column.value, unthriftifyClock(column.clock), column.ttl);
                }
            }
            else
            {
                assert cosc.super_column == null;
                rm.add(new QueryPath(cfName, null, cosc.column.name), cosc.column.value, unthriftifyClock(cosc.column.clock), cosc.column.ttl);
            }
        }
    }
    return rm;
}

そういうわけなんで、全文検索機能のリリースは0.7系が出るまでは、おあずけということになりそうです。

Tags:   ,
Categories:   NoSQL
Actions:   Permalink | Comments (50) | Comment RSSRSS comment feed

Cassandraemon 0.6.0をリリースしました( ̄∇  ̄ )

Saturday, 12 June 2010 11:17 by sabro

自分のプロジェクトへ組み込んで、地味に検証を続けていたCassandraemonですが、先程、バージョン0.6.0をリリースしました。一応ベータ版扱いにしてありますが、バグはだいたい潰し終わったと思います。

Cassandraemon 0.6.0

ドキュメントも頑張って英語で書いてみました。日本語版のドキュメントは今のところありませんが、リクエストがあったらどこかに作ります。

リリースノートにも書いてありますが、Cassandraemonで出来ることの一覧を挙げておきます。

  • LINQクエリでCassandraデータの取得が可能
  • Cassandraへの、登録、削除操作もサポート
  • 使い易い拡張メソッドがたくさん
  • 変態的なConvertメソッドで、Cassandra ByteデータからObjectへ楽々変換
  • TimeUUIDサポート
  • Javaとの相互運用性を考慮して、Big Endian形式でデータ登録

現状の.NETクライアントの中では結構イイ線いってるような気がします。っていうか、LINQで動くCassandraクライアントでまともなやつがないですからねえ。

Cassandraemonのバージョンは、本家Cassandraと連動しています。0.6.0を試すときは、Cassandraも0.6系を使ってください。Cassandraはまだまだ変化が激しいので、多分0.7になったら動かないと思います。Cassandraコミュニティの活発さを見ると、0.7もすぐにリリースされそうなので、はやくも追従していく必要がありそうです( ̄□  ̄ ||

それとは別に、Cassandraemon独自の機能追加も考えています。具体的に上げると

  • コネクションプーリング、ロードバランシング
  • リフレクションによるマッピングを、高速マッパーライブラリで速度改善
  • 全文検索機能

などです。

結構異質なのが、全文検索機能ですが、うちのプロジェクト「クリエモン」では、今までToritonnを使った全文検索を実装していたので、Cassandraへ移行するに当たって、そこを何とかしないといけないわけです。Lucandraとかもあるけど、複数のライブラリを使いまわすのもメンドくさそうですし、開発することにしました。さぶろーは全文検索とかほとんど知らないので、仕組みとしては簡単な物になると思います。

とりあえず報告はこんなところです。それでは、充実したカサンドライフ(セカンドライフみたいだ・・・)をお過しください( ̄∇  ̄ )

Tags:   , ,
Categories:   .NET | NoSQL
Actions:   Permalink | Comments (72) | Comment RSSRSS comment feed

Javaと.NETのエンディアンの違いでCassandraの互換性崩壊( ̄□  ̄ ||

Friday, 4 June 2010 02:23 by sabro

昨日の件ですが、Javaと.NETでは、数値をバイト配列にした際の順序はやはり逆になるようです。バイト配列の格納方式はエンディアンといって、先頭から素直に並べるビッグエンディアンと、逆順に並べるリトルエンディアンがあります。

Javaの場合は常にビッグエンディアンとなり、.NETの場合は、CPUによって並べ方が変わるようです。x86系のCPUでは、リトルエンディアンになるみたいですね。.NETでは、BitConverterクラスに、IsLittleEndianというフィールドがあるので、それを見て、自前でバイト配列を並べ替えることで対処は出来るみたいです。

で、Cassandraemonで数値を格納するときにどうするか決めないといけないわけです。少なくともLongTypeのカラム名に格納する際は、ソートできるようにビッグエンディアンへの変換が必要ですね。他の場合はどうするか・・・。Thrift側でやってくれたらよかったんだが。

1、すべての数値を格納前にビッグエンディアンへ変換する

とりあえず、数値をToCassandraByte関数などでバイトへ変換するときは、常にビッグエンディアンにしてしまうというやり方がまず考えられます。このやり方だと、カラム名だけでなくカラムの値もビッグエンディアンになります。この方式のいいところは、Javaと相互運用が可能な点です。常にビッグエンディアンが格納されるので、Javaからも問題なく読みだすことができるはず。ただ、完全ではなく、数値を自前で野良バイト変換してCassandraへ格納した場合に、整合性が取れなくなることはありますね。

2、LongTypeのカラム名に格納するときだけビッグエンディアンへ変換

Cassandraemon側で、LongTypeのカラム名登録の場合だけ、自動でバイト配列を逆順にするやり方です。.NETから見た場合の相互運用性が高くなります。他の.NETクライアント、例えばhectorsharpなどから、カラム名以外は違和感なく値の取得ができると思います(そういえば、hectorsharpでは、この問題どうしてんだろ)。

3、自動的な変換はやらない

Cassandraemon側では一切変更を行わず、リトルエンディアン変換関数とかを用意しておき、LongTypeカラム名登録時は各開発者に自分で変換を行ってもらうやり方です。面倒ではありますが、どのカラムにどのエンディアンで格納されているかを開発者側が管理出来ると言うメリットがあります。ただ、値取得時に、このカラムはどのエンディアンかを意識して変換する必要もあるので、大人数での開発などでは混乱も起こるかもしれません。

以上、やり方考えてみましたが、どれにしようかなあ( ̄□  ̄ ) 個人的には1か3を考えてますが、みなさん希望とかありますでしょうか( ̄∇  ̄ )

Tags:   , , , ,
Categories:   .NET | Java
Actions:   Permalink | Comments (58) | Comment RSSRSS comment feed

Cassandraでカラム名に数値を格納するときBytesType指定は可能か?

Thursday, 3 June 2010 10:24 by sabro

追記: 以下の文章では、結局BytesTypeではうまく取得出来ないから、LongTypeを使いましょうということが書いてありますが、LongTypeでもうまく取得出来ないようです。Cassandraemonのバグの可能性もあるので、詳しく調べ中です。

追追記: 試しにJassandraで、.NETからLongTypeに登録した値を取ってみたら、なぜか「72057594037927936」みたいな値が取れました。これを16進数に変換してみると、なんと「100000000000000」。もしかして、1で登録したやつが逆になってる・・・( ̄□  ̄ || とてつもなくイヤな予感がします。

追追追記: .NETでの登録時に、Reverse関数でバイト配列を逆順にしてやると、なんか想定通りの動きをしました・・・。あれ、ほんとにJavaと.NETって、数値から作ったバイト配列が逆順になるってことでしょうか( ̄□  ̄ || 詳しい方カモン!

追追追追記: 調べてみました。→ 次のエントリへ

Cassandraでは、カラム名の比較方法として、以下の6つが選べます。(本家サイトより引用)

  • BytesType: Simple sort by byte value. No validation is performed.
  • AsciiType: Like BytesType, but validates that the input can be parsed as US-ASCII.
  • UTF8Type: A string encoded as UTF8
  • LongType: A 64bit long
  • LexicalUUIDType: A 128bit UUID, compared lexically (by byte value)
  • TimeUUIDType: a 128bit version 1 UUID, compared by timestamp

カラム名に数値を入れたいときは、普通はLongTypeを使うのですが、今日、LongTypeのカラムに4バイトのint型を突っ込んだら、8バイトじゃないとダメよってエラーがでました( ̄□  ̄ || まあ、普通に対処するなら、バイト変換する前にLong型にキャストしてやればいいんですが、数値を格納する際に毎回キャストするのも面倒なので、比較タイプをBytesTypeにすることでキャストしなくてもいい具合に動いてくれないかどうか調べてみました。

BytesTypeの動作は、「org.apache.cassandra.db.marshal.BytesTypeクラス」で規定されてるようです。

public class BytesType extends AbstractType
{
    public int compare(byte[] o1, byte[] o2)
    {
        return FBUtilities.compareByteArrays(o1, o2);
    }

    public String getString(byte[] bytes)
    {
        return FBUtilities.bytesToHex(bytes);
    }
}

FBUtilitiesクラスっていうのを使ってるみたいですね。「org.apache.cassandra.utils.FBUtilitiesクラス」のようです。

public static int compareByteArrays(byte[] bytes1, byte[] bytes2){
    if(null == bytes1){
        if(null == bytes2) return 0;
        else return -1;
    }
    if(null == bytes2) return 1;

    int minLength = Math.min(bytes1.length, bytes2.length);
    for(int i = 0; i < minLength; i++)
    {
        if(bytes1[i] == bytes2[i])
            continue;
        // compare non-equal bytes as unsigned
        return (bytes1[i] & 0xFF) < (bytes2[i] & 0xFF) ? -1 : 1;
    }
    if(bytes1.length == bytes2.length) return 0;
    else return (bytes1.length < bytes2.length)? -1 : 1;
}

むう、先頭から順番に、どちらかの配列の終わりに達するまで比較して、それでも同じだったら、配列長の長い方が勝ちとなるようです。

次に、.NETで、BitConverterを使って、バイト配列に変換したとき、どんな感じに格納されるのか知らなかったので、実験してみました。

static void Main(string[] args)
{
	uint u1 = 10;
	uint u2 = 257;
	long l1 = 10;
	long l2 = 257;

	var bu1 = BitConverter.GetBytes(u1);
	var bu2 = BitConverter.GetBytes(u2);
	var bl1 = BitConverter.GetBytes(l1);
	var bl2 = BitConverter.GetBytes(l2);

	Console.WriteLine(bu1[0] + " " + bu1[1] + " " + bu1[2] + " " + bu1[3]);
	Console.WriteLine(bu2[0] + " " + bu2[1] + " " + bu2[2] + " " + bu2[3]);
	Console.WriteLine(bl1[0] + " " + bl1[1] + " " + bl1[2] + " " + bl1[3] + " " + bl1[4] + " " + bl1[5] + " " + bl1[6] + " " + bl1[7]);
	Console.WriteLine(bl2[0] + " " + bl2[1] + " " + bl2[2] + " " + bl2[3] + " " + bl2[4] + " " + bl2[5] + " " + bl2[6] + " " + bl2[7]);
}


/*
結果
10 0 0 0
1 1 0 0
10 0 0 0 0 0 0 0
1 1 0 0 0 0 0 0
*/

どうやら、バイト配列の先頭から格納されていくようです。しかし、256単位で繰り上がるので、10を変換した場合と、257を変換した場合では、バイト配列の1つめは、前者の方がおおきくなっていますね。これだと、多分、BytesTypeの比較では上手くいかない気がします。

ためしに実験。設定ファイルを以下のように定義。

<ColumnFamily Name="BytesType" CompareWith="BytesType" />

以下がテストコード。Cassandraemonを使っています。分かりにくいかもしれないですが、通常のThriftインターフェースを使うと、とんでもなく長くなりそうだったので・・・。

public static void Main (string[] args)
{
	using(var context = new CassandraContext("localhost", 9160, "Test"))
	{
		// 1, 10, 257 の、3カラムを生成
		var list = new List<Column>();
		list.Add(1, "")
			.Add(10, "")
			.Add(257, "");
			
		// 登録用エンティティ作成
		var entity = new CassandraEntity<List<Column>>()
				.SetColumnFamily("BytesType")
				.SetKey("hoge")
				.SetData(list);
				
		// 登録
		context.ColumnList.InsertOnSubmit(entity);
		context.SubmitChanges();
		
		// 1以上のデータを取得
		var q1 = from x in context.ColumnList
				 where x.ColumnFamily == "BytesType" &&
					   x.Key == "hoge" &&
					   x.Column.GreaterThanOrEqual(1)
				 select x;
				 
		Console.Write("条件 1以上 ");
		foreach(var e in q1)
		{
			foreach(var column in e.Data)
			{
				Console.Write(column.Name.ToInt32() + " ");
			}
			Console.WriteLine();
		}
		
		// 9以上のデータを取得
		var q2 = from x in context.ColumnList
				 where x.ColumnFamily == "BytesType" &&
					   x.Key == "hoge" &&
					   x.Column.GreaterThanOrEqual(9)
				 select x;
				 
		Console.Write("条件 9以上 ");
		foreach(var e in q2)
		{
			foreach(var column in e.Data)
			{
				Console.Write(column.Name.ToInt32() + " ");
			}
			Console.WriteLine();
		}
	}
}


/*
条件 1以上 1 257 10 
条件 9以上 10 
*/

9以上の値を取得しようとしてるのに、257が取れてこないですね。検証は正しかったようです。

というわけで、数値タイプをカラム名にするときは、素直にLongTypeを使いましょうということですね( ̄∇  ̄ ) で、格納するときは毎回ちゃんとlongにキャストするか、そもそもModelクラスのフィールドには基本的に、long型を使うようにするのがいいと思います(あくまで.NETの場合)。

Tags:   , ,
Categories:   .NET | NoSQL
Actions:   Permalink | Comments (58) | Comment RSSRSS comment feed

DateTime.Ticksの精度は100ナノ秒ではない

Thursday, 6 May 2010 07:41 by sabro

DateTime.Ticksプロパティの説明には

「このプロパティの値は、0001 年 1 月 1 日午前 00:00:00 (DateTime.MinValue を表します) 以降の経過時間 (100 ナノ秒単位) を表します。」

って書いてあります。でも、これは桁数がそれだけ用意してありますよっていうだけで、実際は、動作OSによって精度が異なるみたいです。

ちょっとうちのVistaで実験

static void Main(string[] args)
{
	for (int i = 0; i < 100; i++)
	{
		Console.WriteLine(DateTime.Now.Ticks);
	}
}

こんなかんじで実行したところ、結果は以下のような感じでした。

634086940404282076
634086940404312076
634086940404312076
634086940404312076
634086940404312076
634086940404312076
634086940404312076
634086940404312076
634086940404312076
634086940404312076
634086940404312076
634086940404312076
634086940404312076
634086940404312076
634086940404312076
634086940404312076
634086940404322076
634086940404322076
634086940404322076
634086940404322076
634086940404322076
634086940404322076
634086940404322076
634086940404322076
634086940404322076
:
:

下4桁は固定で、1ミリ秒単位で増加するようです。うっかりMSDNの内容をうのみにしないように気をつけないといけませんね。

こっから蛇足。

なんで、こんな実験したかっていうと、作成中のCassandra LINQプロバイダ、Cassandraemonで、TimeUUIDをキーにする必要があったからなんです。TimeUUIDは、現在の100ナノ秒単位のタイムスタンプから生成されるIDのようなもので、かなりの確率で複数マシン間で一意になることになってます。

ところが、これをDateTime.Ticksから生成すると、1ミリ秒単位なので結構な確率で同じものが生成されてしまうんですね。これでは、テーブルのキーとして使うには厳しいわけです。

うーん、内部でミリ秒以下は適当に生成するとかで凌ぐしかないのかなあ。なんかいいアイデアないでしょうか( ̄∇  ̄ )

Tags:   , , , ,
Categories:   .NET
Actions:   Permalink | Comments (43) | Comment RSSRSS comment feed

CassandraのLINQProvider作ったよ( ̄∇  ̄ )

Friday, 23 April 2010 21:03 by sabro

今をときめくCassandraの、LINQプロバイダを作ってみました( ̄∇  ̄ )

Cassandraemon in CodePlex
Cassandraemon

こんな感じで、データを取ってこれます。

public class Product
{
	public int ID { get; set; }
	public string Name { get; set; }
}

	
using(var context = new CassandraContext("localhost", 9160, "KeySpace1"))
{
	var products = from x in context.ColumnList
		       where x.Key == "1" &&
			     x.ColumnFamily == "Product"
		       select x.ToObject<product>();
				   
	foreach(var p in products)
	{
		Console.WriteLine(p.ID.ToString());
	}
}

一応、更新系もサポートしています。より詳しい解説はドキュメントページを見てください。

まだとりあえずクエリ、更新処理が動いたことを確認できただけの段階です。全く使い込んでないのでバグもあるかも。ある程度使ってみて安定してきたら、ちゃんと正式版用意すると思います。

今後の開発方針ですが、さぶろーはセカンドライフ系サービスの開発が忙しいため、Cassandraemonに関しては、積極的なコミットはしない予定です。本家のバージョンアップで追加されたAPIくらいには対応したいですけどね。

ソースコード管理はMercurialなので、機能追加されねーって場合は、自分でForkして作ってみるのも一興かもしれません。

ちなみに、プロジェクト名の由来は、セカンドライフで絶賛稼働中の、拙作、萌え系アバターサービス「クリエモン」からです。

クリエモン

今までクリエモンのバックグラウンドではMySQLを使っていたのですが、メンテナンスが大変なのと、NoSQL使ってみたかったという理由から、Cassandraに切り替えることにしました。クリエモンで使うCassandraドライバなので、Cassandraemonというわけです。某国民的アニメとは全く関係ないのでご注意ください( ̄∇  ̄ )

Tags:   , , , ,
Categories:   .NET | NoSQL
Actions:   Permalink | Comments (63) | Comment RSSRSS comment feed