astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

星屑のおじさま、野生のトラを撃つ

f:id:astamuse:20170220174018j:plain

こんにちは。石橋を叩き過ぎて割るタイプの福田です。

春の足音を聴きながら、雲方面への移住を進めています。

さて、寒さもピークの昼下がり、ソイラテを片手にラップトップ整理。

2年物・書き捨てのPythonスクリプトに、ふと足を止めました。

すれっからしですが、意外に役立つので取り上げておきます。

Mongo Oscillator

今から2年前の2015年3月、MongoDBのバージョン3.0がリリースされ、新しいストレージエンジンWiredTigerが選択できるようになりました。その後、同3.2で従来のMMAPv1に代わりデフォルトのエンジンとなります。

当時のホワイトペーパーには、大幅なパフォーマンス向上、透過的圧縮、フラグメンテーションからの解放とあり、食指が動きます。

しかし、直ぐにアップグレードするのは気が引けるので、諸々のテストを実施しました。

はじめに、実運用のケースに即したテストとして、一定量のデータに対して継続的な更新操作を行った際にデータの破損や肥大化、レプリケーションの破綻が起こらないかを確認するための簡単なコードを書きました。

当初、バックアップのダンプデータを入力としてシミュレーションを行っていましたが、以下の点で不満が出てきます。

  • I/Oの面での不利
  • BSONのデコードやドメイン特有の処理のオーバーヘッド
  • 複数環境でのテストツール一式の可搬性の欠如
  • 更新のシミュレーションの煩雑さと再現性の欠如

これらの問題点を解消すべく書いたのが、こちらのPythonスクリプトになります。

唐突すぎて何をやっているのか分からないと思いますので、簡単に解説します。

  • generate_object関数

seq_noで与えられた番号にkey_shift値を加えた値をシードとして生成した乱数を元に、所与の平均値と標準偏正規分布に従ったサイズのデータフィールドを持つオブジェクトを生成します。

オブジェクトの_idはseq_noのハッシュ値とし、幾つかの属性値を含む辞書を返します。

  • shoot関数

split_tasks関数で分割されたタスクを受け取り、担当分のデータの挿入/更新を行います。

並列処理の単位となります。

  • split_tasks関数

n件のデータ生成タスクを、parallel数のタスクに分割し、開始と終了番号のタプルを返します。

使い方

テストドライバのホストで、パラメータを指定して実行するだけです。

$ python mos.py -h
usage: mos.py [-h] [-n N] [-w W] [-p P] [-s S] [-z Z] [-c C] [-d D] --host
             HOST [--db DB] [--collection COLLECTION]

Mongo Oscillator

optional arguments:
 -h, --help            show this help message and exit
 -n N                  number of objects to generate
 -w W                  mongodb replicaset write concern
 -p P                  number of processes to run in parallel (split n into p
                       tasks)
 -s S                  key shift offset
 -z Z                  average object size
 -c C                  cheat ratio
 -d D                  stddev
 --host HOST           comma separated mongodb replicaset hosts
 --db DB               mongodb database name
 --collection COLLECTION

実行例

単発の実行

$ python mos.py -p 4 --host yin,yang -n 10000 -z 4096 -d 1024
Namespace(c=0.9, collection='star', d=1024, db='shooting', host='yin,yang', n=10000, p=4, s=1, w=2, z=4096)
30659   n=0 (2500)      10.1889410019 sec       245.36406674 tps
30657   n=0 (2500)      10.6970849037 sec       233.708531109 tps
30656   n=0 (2500)      11.1619198322 sec       223.975806812 tps
30658   n=0 (2500)      11.3631420135 sec       220.009571034 tps
30651   n=10000 11.3728001118 sec       879.290931145 tps

実際には、以下のように1ずつキーシフトしながら複数回のデータ上書き更新をシミュレートします。

$ for x in `seq 1 4`; do python mos.py -p 4 --host yin,yang -n 10000 -z 4096 -d 1024 -s $x; done

10000件、4分割(4プロセス)で、キーを1つずつシフトして4回書込み(初回の挿入と3回の更新)

パラメータの説明

  • -n 生成するオブジェクト数 (データの総件数)
  • -p タスク分割数(1つのタスクにつき1プロセス割り当て)、デフォルト値:2
  • -s キーシフトのオフセット(後述します)、デフォルト値:1 ※1
  • -z 平均のオブジェクトサイズ(byte)、デフォルト値:50 ※2
  • -d 標準偏差、デフォルト値:-1 (-zで指定した平均のオブジェクトサイズの10%)
  • -c ランダム生成文字列使い回し比率(生成の負荷を調整)、デフォルト値:0.9 ※3
  • –host MongoDB ホスト名(レプリカセットの場合はカンマ区切りで複数指定可)、必須
  • –db  MongoDB データベース名、デフォルト値:shooting
  • –collection MongoDB コレクション名、デフォルト値:star

※1 シーケンスで採番したキーを指定数分ずらすことで(オブジェクトからキーを切り離してシフト)、サイズの変動を伴うオブジェクトの更新をシミュレートしています。

更新時に全体の総サイズを変えずにオブジェクトサイズの変動をシミュレートすることで、フラグメンテーションとそれに伴うデータの肥大化傾向の有無や度合いを観察できることを期待しました。

以下、小さなデータを生成し、挙動を簡単に説明します。

データ件数3、平均データサイズ40、標準偏差10でデータ生成します。

$ python mos.py -p 1 --host yin,yang -n 3 -z 40 -d 10 -s 1

mongo shellでの確認

r0:PRIMARY> use shooting
r0:PRIMARY> db.star.find()
{ "_id" : "b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "length" : 52, "data" : "NnzxHPXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJjUPMVNPsZYi", "s
eq" : 0, "last_modified" : 1486707174.010212 }
{ "_id" : "356a192b7913b04c54574d18c28d46e6395428ab", "length" : 57, "data" : "ceRMIqFXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJjUPMVNPsZYiNLxB
", "seq" : 1, "last_modified" : 1486707174.016221 }
{ "_id" : "da4b9237bacccdf19c0760cab7aec4a8359010b0", "length" : 40, "data" : "tFGdXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJj", "seq" : 2, "la
st_modified" : 1486707174.01845 }

何度やっても生成結果が同じになります。

r0:PRIMARY> db.star.find()
{ "_id" : "b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "length" : 52, "data" : "NnzxHPXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJjUPMVNPsZYi", "s
eq" : 0, "last_modified" : 1486707200.27103 }
{ "_id" : "356a192b7913b04c54574d18c28d46e6395428ab", "length" : 57, "data" : "ceRMIqFXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJjUPMVNPsZYiNLxB
", "seq" : 1, "last_modified" : 1486707200.276858 }
{ "_id" : "da4b9237bacccdf19c0760cab7aec4a8359010b0", "length" : 40, "data" : "tFGdXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJj", "seq" : 2, "la
st_modified" : 1486707200.279132 }

1回目のシフト

$ python mos.py -p --host yin,yang -n 3 -z 40 -d 10 -s 2

シフト後の状態

r0:PRIMARY> db.star.find()
{ "_id" : "b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "length" : 57, "data" : "ceRMIqFXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJjUPMVNPsZYiNLxB
", "seq" : 0, "last_modified" : 1486707251.232851 }
{ "_id" : "356a192b7913b04c54574d18c28d46e6395428ab", "length" : 40, "data" : "tFGdXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJj", "seq" : 1, "la
st_modified" : 1486707251.232893 }
{ "_id" : "da4b9237bacccdf19c0760cab7aec4a8359010b0", "length" : 40, "data" : "uiduXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJj", "seq" : 2, "la
st_modified" : 1486707251.234418 }

_idの集合は据え置かれて、dataおよびlengthがシフトしているのが分かると思います。

2回目のシフト

$ python mos.py -p --host yin,yang -n 3 -z 40 -d 10 -s 3

シフト後の状態

r0:PRIMARY> db.star.find()
{ "_id" : "b6589fc6ab0dc82cf12099d1c2d40ab994e8410c", "length" : 40, "data" : "tFGdXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJj", "seq" : 0, "la
st_modified" : 1486707260.945545 }
{ "_id" : "356a192b7913b04c54574d18c28d46e6395428ab", "length" : 40, "data" : "uiduXXceRMIqFFEiwuLZXCxnbbyqtUBDmbqhAZJj", "seq" : 1, "la
st_modified" : 1486707260.942702 }
{ "_id" : "da4b9237bacccdf19c0760cab7aec4a8359010b0", "length" : 28, "data" : "PXMXXceRMIqFFEiwuLZXCxnbbyqt", "seq" : 2, "last_modified"
: 1486707260.943574 }

※2 dataフィールドのサイズ(バイト)を指定。data以外のフィールドがあるため、db.stats()のavgObjSizeとの差異が生じ、差異の影響はこの値が大きいほど小さくなります。

※3 生成のオーバーヘッドを減らすため、予めランダム文字列のテンプレートを用意しておいて使いまわし、パラメータで指定した一定の比率部分のみオブジェクト毎にランダム生成を行うようにしました(0.9という設定は各オブジェクト毎に決定されたdataフィールドのサイズのうち9割をテンプレートから使いまわすことを意味します)。

折しも

バージョン3.4を検証中のGCP環境での実行結果を上げておきます。

f:id:astamuse:20170220125143p:plain

データディレクトリのある/dev/sdb1の使用量は、既存のデータがあるため0.64Tがベースになります。

無事更新操作が完了し、ストレージサイズの肥大化がないことが確認できました。

  • MongoDBのバージョン: 3.4.1
  • 構成: 2ノード(PRI, SEC)+ ARBのレプリカセット
  • インスタンスタイプ
    • PRI,SEC: n1-highmem-4 (CPU:4, Mem:26G, SSD)
    • ARB: g1-small (CPU:1, Mem:1.7G)
    • ドライバホスト:n1-highmem-4(CPU:4, Mem:26G)

テストパラメータ

  • 件数: 10,000,000
  • 平均オブジェクトサイズ: 40960
  • σ: 10240
  • キーシフト: 1-8
  • 上書き回数: 7
  • write concern: 2
  • cheat_ratio: 0.95

まとめ

MongoDBのデータ更新をシミュレートする、Pythonスクリプトを取り上げました。

実際、バージョン3.0の初めの頃にこのテスト実行した際に良好な結果が得られず、移行を見合わせる判断材料となりました。

その後、バージョン3.2にて無事アップグレードと同時にWiredTigerへの移行を完了し、今日も正常稼働を続けています。

世の中にはY

おや、誰かきたようなのでこの辺で。

バナーデザインに最適なイメージ・ブレストとは!?(バナーレース竹葉亭うな重カップ)

f:id:astamuse:20161216160732p:plain ――――都内某所アスタミューゼ会議室。

マーケtng:「突然ですが、みなさん『イメージ・ブレスト』というのをご存じですか?」

部長srk&デザイナーkrt:「イメージ・ブレスト!?」
デザイナーkrt:(なにそれ。めんどそう。。)
部長srk:「なんすか、それ?」
マーケtng:「ご存じないですか(まあ、さっき俺が思いついたやつだし)。では説明しましょう」

イメージ・ブレストとは?

みなさん、ブレストは知ってますよね?そう、みんなで意見出し合って他人の意見に乗っかって新しいアイデアどんどん出してくアレです。平たく言うとアレを「言葉」ではなく、「クリエイティブ」でやろうってのが『イメージ・ブレスト』です。てことで、さっそくルールを説明します。

  1. まず、テーマ・目的を説明して一旦解散!
  2. 共有フォルダに作ったクリエイティブをどんどん入れてく(途中でもいいから。ローカルで作業しない!)
  3. 他人のクリエイティブをパクってもいい!(てゆーかむしろパクろう。訴求をパクって別のクリエイティブとか、クリエイティブをパクって別の訴求とか)
  4. 定期的に集まって自分のクリエイティブを説明。言葉でも軽くブレスト。で、また解散!
  5. これを繰り返してとにかくクリエイティブをいっぱい出す!

マーケtng:「つまり、デザイナーなら言葉じゃなく、クリエイティブでブレストしようってことです!」(`^´) ドヤッ!
部長srk:(お前デザイナーじゃねーじゃん)
デザイナーkrt:(やっぱめんどそう。。)
マーケtng:「今回はマンガを使ったバナー制作なんで、バナーレースも開催します!」
部長srk&デザイナーkrt:「……(なにそれ?)」

バナーレースとは?

  1. みなさんが作ったバナーから、配信したいバナー(無記名)を広告ディレクターhtnが選びます。

  2. 実際に配信してみて、最も成績のよかったバナーを制作した人に部長が銀座竹葉亭のうな重をご馳走します!

部長srk:「はぁ!?(聞いてねーし)」
デザイナーkrt:(うな重…!?)
マーケtng:「てことで、みなさんよろしくです!」
部長srk:「俺が勝ったらどーすんだよ!?」
マーケtng&デザイナーkrt:(独りで食えばいーじゃん)
マーケtng:「では一旦解散!」

こんな感じでただの思い付きから始まったイメージ・ブレストのバナーレース(竹葉亭うな重カップ)。果たしてどうなることやら……

エントリーはこちら!

★ぜひ、誰が勝つか予想しながら読み進めてください!

コンテンツ理解度100% × デザインセンス0% マーケtng
予想:〇(手堅い)オッズ:1.4倍
本企画の立案者でランディング先の設計もしてることから、理解度は間違いなく断トツ!絶望的なデザインセンスを除けばおそらく順当に勝ち上がってくることでしょう。
低血圧系無気力ガール デザイナーkrt
予想:△(やる気次第)オッズ:5.5倍
朝から晩までテンション寝起き状態で間違いなく今回もやる気はない。ただし、未確認情報でマンガ大好き腐女子との噂も…
俺には他にやるべきことがある! 部長srk
予想:▲(流れ次第で大穴)オッズ:11.0倍
そもそもバナーなんて作ってていいのか?もっと大事な仕事があるだろうに。時間がなくて作ってこないと予想されるが、いいクリエイティブが集まらない場合は部門長の責任感で仕上げてくる可能性もある大穴。

ところが、このレースで誰も予想しなかったことが起きる――――



第一回イメージ・ブレスト(第1コーナー)

――――早くも迎えた第一回ミーティング。で、集まったのがこれ。 f:id:astamuse:20161216160742p:plain ※大きな画像を見たい方はこちらよりご応募ください。

さあ迎えた第1コーナー!
マーケtng、大きくリード!
デザイナーkrt、建前程度に1コ。
部長srk、責任感で作ろうとしたけど直前で断念!

やっぱこうなるか。まぁいいや、やろう。てことで各々軽く自分のクリエイティブを説明。 ところが、デザイナーkrtが作ったクリエイティブの視点が意外とよかったんで、「こうすればいいんじゃない?」とか、「こうゆうのもアリじゃない?」など、第一回ミーティングは思ったより盛り上がりました。最後にもう一度、「他人のクリエイティブをもっとパクってとにかくアイデアを出しましょう」と念押しして会議を閉じました。

これがウォーミングアップになったのか、第二回ミーティングでは予想外の展開に――――



第二回イメージ・ブレスト(第2コーナー)

第二回ミーティングで集まったクリエイティブがまさかのこれ。 f:id:astamuse:20161216160748p:plain ※大きな画像を見たい方はこちらよりご応募ください。

第2コーナーを回って、
マーケtng、第1コーナーの優位にあぐらをかいたか伸び悩み。
デザイナーkrt、マーケtngをおさえてまさかのトップ!
部長srk、やはり脱落コースか!?

パクり合って新しいクリエイティブも生まれています。例えばこんな感じ。 f:id:astamuse:20161216160807p:plain f:id:astamuse:20161216160814p:plain ※もっと見たい方はこちらよりご応募ください。

おお~なんかほんとにブレストっぽくなってる(自分でもびっくり)。
て、喜んでる場合じゃない。
正直、相手は時間ない部長とやる気ないデザイナーなんで、うな重余裕だと思ってたのに、ちょっとピンチです。
(デザイナーkrt、まさかうな重で釣れた!?)

こうして迎えた選考会で、再び衝撃が走る――――



配信バナー選考会(最終コーナー)

――――選考会当日。何も知らない広告ディレクターhtnが選んだのはこちら。 f:id:astamuse:20161216160756p:plain ※大きな画像を見たい方はこちらよりご応募ください。

さあ、いよいよ迎えた最終コーナー!
マーケtng、再び大きくリード!
デザイナーkrt、ここでスタミナ切れか!?
部長srk、やっぱダメなのか!?

最終コーナーでなんとかリードは守れたけど、まさか全員生き残るなんて……
(選考会でみんな締め出そうと思ってた)
ここで、カンのいい読者はエントリーにあったオッズの意味に気づいたかもしれません。
そう、バナーレースの本番はここからです!

この中で最も効果のあったバナーはどれか!?
果たして部長srkはプライドにかけて追い上げることができるのか!?
このあと、配信直後にあるバナーがまさかの快進撃をみせる。
銀座竹葉亭うな重は誰の手に――――!?




配信結果発表

――――選考会で選ばれたバナーを配信して1ヵ月が経ちました。

ある程度結果が予想できたバナーも、やってみるまで誰にも予想できなかったバナーも、様々なクリエイティブを試すことができました。
そしてその中でCPAやCV数などから、優位性のある1枚のクリエイティブが決まりました。

みなさんは誰を予想しましたか?
バナーレースは、アスタミューゼ社内でも予想外の劇的な結末を迎えました。

結果を知りたい方は、ぜひ、
アスタミューゼの募集要項からご応募いただき、
「バナーレースの結果を教えてください」とお伝えください。

みなさまのご応募、心よりお待ちしております m(_ _)m

そうだAsta4dでWebアプリケーションを作ろう(第4回)

Handlerの役割と使い方

Hanlerの話です。
Handlerの役割については色々ありますが、詳しくはJavaフレームワークAsta4Dの話に書いてますのでご覧ください。

今回はコードレベルでどのような形で書けば良いかという観点で書いてます。
動的URLに対して適したHTMLを表示させるというお題をもって説明いこうと思います。

今回のURLルール

package com.astamuse.blog_sample.rules;

import static com.astamuse.asta4d.web.dispatch.HttpMethod.GET;

import com.astamuse.asta4d.web.dispatch.mapping.UrlMappingRuleInitializer;
import com.astamuse.asta4d.web.dispatch.mapping.ext.UrlMappingRuleHelper;

import com.astamuse.blog_sample.Handler;

public class UrlRules implements UrlMappingRuleInitializer{
    public void initUrlMappingRules(UrlMappingRuleHelper rules) {
        rules.add(GET, "/").forward("/html/index.html");
        rules.add(GET, "/part4/{id:[0-9]+}").handler(Handler.class);
    }
}

そんな訳で動的なURLを書いてみました。
前回まで使っていたルールに1行、動的URLを追加してるだけですが。

asta4DのURLルールでは、「{}」内に変数を記載すると動的なURLとして認識され、HandlerやSnippet側でその変数が受け取ることが出来ます。
変数部分には正規表現で変数の内容を縛ることも可能です。
見て分かりますが、今回の場合は数字の場合だけ変数「id」に値が入りURLとして効力が発揮されます。

「/part4/1」「/part4/2」のように「id」の部分が数字の場合はこのURLとして認識されるが、「/part4/a」だと認識されないって感じですね。

で、このURLは今までと違ってforwardする処理が入っていません。
今回は「動的URLに対して適したHTMLを表示させる」ということでfoward先をURLに合わせて変更させなければならないので、Handler内でforward先を指定する実装という形で説明したいと思います。

なお、今回は描画部分のお話ではないのでHTMLやSnippet部分は省略します。

Handlerの実装

package com.astamuse.blog_sample.handler;

import com.astamuse.asta4d.web.annotation.QueryParam;
import com.astamuse.asta4d.web.dispatch.request.RequestHandler;

public class Handler {

    @RequestHandler
    public String handle(Integer id) {
        if(id == null) {
            new RuntimeException();
        }
        return "/html/" + id +".html";
    }
}

ということでHandlerを実装しました。
今回は変数で入ってきたidを元にforward先を決めるだけなので簡易な実装ですね。
実際はもっと複雑と言いたいところですが、複雑な実装になっているHandlerは実使用上でもあまりありません。
基本的にView Firstでは、View以外の実装については複雑にはしないのです。

それでは、1行ずつ見ていきたいと思います。

@RequestHandler
public String handle(Integer id)

@RequestHandlerのアノテーションが記載されたメソッドが最初にコールされます。

引数には、URLルール上に記載された変数を書いておくことで勝手にインジェクションしてくれます。
URLルール以外にも引数に含めておくと勝手にインジェクションしてくれるクラスがあるので、覚えておくと便利です。
HttpServletRequestHttpServletRequestが該当します(他にもあります)。
リクエストパラメータからなんかしたい場合やレスポンスヘッダを自力でいじりたい場合に使ったりします。

インジェクションされないような変数が引数にあってもエラーにはなりませんが、値には何も入りません。

        if(id == null) {
            new RuntimeException();
        }

ここはNullpo防止対策なので、ノーコメント。

return "/html/" + id +".html";

Handlerの結果を返却します。
Handlerではreturn値に様々な値を設定することが可能となります。
forwardメソッドにはHandlerから返却されたreturn値をもって振り分ける目的のメソッドがあるので、その値を持ってfoward先を決定することも可能です。

そして、今回のようにforward先のhtmlファイルを直接指定することも可能です。
ルール側だとforward先が多岐に渡る場合に困るので、Handler側でhtmlファイルを返却する方法を使ったりした方が良い場合もあります。
まぁ、使い分けですね。

また、RedirectTargetProviderクラスを返却すると強制的に指定されたURLにリダイレクトしてくれます。

void型のHandlerを作成することも出来、その場合はExceptionが発生していなければ次の処理へ遷移します。
今回のケースでそうすると次の処理がないのでエラーになりますが。

Handlerの実使用例

実装の説明と共に、astamuse.comにて実際に使われているケースをざっくり語ることで理解を深めていただければと思います。

  • フォームから送信された内容をDBに保存する
  • ページを表示するために必要な最低限のデータが存在しているかのチェック
  • ログインが必要なページへのアクセス時のチェック
  • 外部サイトからのデータ取得(特許画像の取得や公報PDFの取得)
  • URL変更時のリダイレクト処理

起動

接続して思った通り表示されていれば大成功。
htmlファイルを用意してないURLにアクセスした場合はTmplateNotFoundExceptionが起きると思いますが、今回の場合はそれでも問題ありません。
(実際の運用上で起きたらまずいけど・・・)

終わりに

ここまでの回の内容が理解出来ればなんとなくWebアプリケーションを作ることは出来ます。
残り2回はプラスα的な内容をお送りしたいと思ってます。

次回予告

次回は、Handlerについてもう少しつっこんでみようと思います。

関連URL

第1回:環境構築して静的ページを表示するまで
第2回:Snippetを使って動的ページを作ろう
第3回:少し複雑なSnippet
第4回:Handlerの役割と使い方(今ここ)
第5回:Global Handlerとattribute
第6回:Form Flowを使おう

Copyright © astamuse company, ltd. all rights reserved.