タグ「slim3」 の検索結果(1/1)

2011年11月30日 23:00

Memcacheに1MB以上のオブジェクトを格納する

AZusaar! ではDatastoreからとってきた検索結果を100件ずつMemcacheに入れているのですが、最近ちょっとDatastore Reads Oppsが増えてきました。

チューニング前

これが一番多かった時ですが、$0.11/日課金されてます。普通だとFrontEnd InstanceやDatastore Writesで課金がかかると思うんですがうちの場合これらは余裕ですw

これくらいなら微々たるものなのですがなんとかチューニングできないかと課金対策してみました。

とりあえず検索結果を100件ずつではなく全件Memcacheに入れるようにしたところ、下記のようなエラーが発生。

Uncaught exception from servlet
com.google.appengine.api.memcache.MemcacheServiceException: Memcache put: Error setting single item (dev20111122.354967843445918452_KokucheeseSearchService__ALL_201111)
	at com.google.appengine.api.memcache.AsyncMemcacheServiceImpl$7.transform(AsyncMemcacheServiceImpl.java:427)
	at com.google.appengine.api.memcache.AsyncMemcacheServiceImpl$7.transform(AsyncMemcacheServiceImpl.java:419)
	at com.google.appengine.api.memcache.MemcacheServiceApiHelper$RpcResponseHandler.convertResponse(MemcacheServiceApiHelper.java:59)
	at com.google.appengine.api.memcache.MemcacheServiceApiHelper$1.wrap(MemcacheServiceApiHelper.java:98)
	at com.google.appengine.api.memcache.MemcacheServiceApiHelper$1.wrap(MemcacheServiceApiHelper.java:92)
	at com.google.appengine.api.utils.FutureWrapper.wrapAndCache(FutureWrapper.java:57)
	at com.google.appengine.api.utils.FutureWrapper.get(FutureWrapper.java:98)
	at com.google.appengine.api.utils.FutureWrapper.get(FutureWrapper.java:90)
	at com.google.appengine.api.memcache.MemcacheServiceImpl.quietGet(MemcacheServiceImpl.java:27)
	at com.google.appengine.api.memcache.MemcacheServiceImpl.put(MemcacheServiceImpl.java:79)
	at org.slim3.memcache.MemcacheDelegate.put(MemcacheDelegate.java:551)
	at org.slim3.memcache.Memcache.put(Memcache.java:361)

エラーメッセージからは分かりづらいですが、Memcacheに入れようとしたデータが1MB超えているためのエラーのようです。(実際計算したら1.3MBくらいでした)

シリアライズした状態のバイナリは実は全然圧縮されていないため、gzip圧縮かけてMemcacheに入れるためユーティリティを作りました。

(appengine 1.6.0, slim3 1.0.15で確認済)

使い方はテストコードを見ればだいたい分かると思いますが、Slim3のMemcacheとだいたい同じです。
注意点としてはrootPackage配下に置いておかないとhotReloading時にエラーになること。

気になるProduction Server上でのパフォーマンスはこんな感じです。

put時 (1.3MB -> 286KB)
serialize:53ms
compress:121ms

get時 (286KB -> 1.3MB)
uncompressed:55ms
deserialize:19ms

※データの大部分がStringなので結構圧縮率が高いです

圧縮や再シリアライズによる多少のオーバーヘッドはありますがこれくらいなら許容範囲かと。
体感的にもそんなに変わってないです。
他のライブラリに依存していないのでspin upによるコストもありません。

普通だと圧縮してまでMemcacheに入れるということはあまりないと思いますが、もしどうしても1つのデータとしてMemcacheに入れたい場合はこういう方法もあるといういい例ですね。

このチューニングを施した後は見事Datastore Readsを$0に抑えることができました。わーい!
チューニング後


2011年10月 1日 22:00

indexの更新遅延を考慮した実装を行う

実はバリデータ関係でちょっと問題があったりします
これは会議室の名前の重複をチェックするバリデータですが、appengine固有の問題により正常に動きません。
ユニットテストでも問題ないため最悪本番稼動した後に発覚する一番質の悪いバグですね。これに関してはまた別の機会に書きます。
(これはappengineで何らかのサービスを運営してしてないと気づかないかなぁと)

前回のブログでこんなことを書いてましたが解答編です。タイトルにもあるように、インデックスの更新遅延が原因でバリデータは正常に動きません。(appengineじゃなかったらこういう問題はおきないんですがねw)

Entity自体はすぐにDatastoreに反映されるのですが、indexの更新には若干ラグがあります。厳密に調べたことはありませんがひどい時で1?2時間くらいindexが更新されなかったことがあります。
検索系は多少古い検索結果が返ってくることを許容することでいけますが、データの整合性に関してはそうもいきません。

例えば今回の場合だと登録するタイミングによっては同一名の会議室が存在する可能性があります。

よってindexやqureyを利用しない重複チェックを使用することになります。解決方法としては2つあります

1. keyに会議室の名前を入れる

keyはuniqueなのでこれがベストですね

2. Datastore.putUniqueValueを利用する

keyを利用するのがベストなんですが、運用中で容易にkeyを変えられないという場合があります。また、key以外の要素でもuniqueな要素を持たせたいというのはあると思います。
そういう場合はSlim3のDatastore.putUniqueValueを利用するのがいいです

これはソースを見ててたまたま見つけたやつなんですが、uniqueな名前を管理するためのEntityを1つ作ってしまうというやり方です。valueをDatastoreのkeyにしているので原理的には1と全く同じです。

サンプルコード

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import org.junit.Test;
import org.slim3.datastore.Datastore;
import org.slim3.tester.AppEngineTestCase;

public class PutUniqueValueTest extends AppEngineTestCase{

    private static final String UNIQUE_INDEX_NAME = "ConferenceRoomName";

    @Test
    public void test() throws Exception {
        boolean actual1 = Datastore.putUniqueValue(UNIQUE_INDEX_NAME, "会議室A");
        assertThat(actual1, is(true));

        // 重複登録しようとするとfalseが返却される
        boolean actual2 = Datastore.putUniqueValue(UNIQUE_INDEX_NAME, "会議室A");
        assertThat(actual2, is(false));

        assertThat(tester.count(UNIQUE_INDEX_NAME), is(1));
    }

}