うな(。・ε・。)

Android, iOS, AppEngine まわりのめもめも

App Engine + Java 覚書

  1. Spring 載せない方が良い
    • DI が遅いのでスピンアップが死ぬほど遅くなる
    • Spring MVC だけにして、Component Scan は使わず xml 直接 Bean を指定する
    • たいてい Slim3 Controller だけで済むし、APIGoogle Cloud Endpoints を使った方が良い
  2. jsp でなく Freemarker 等を使う
    • jsp を使うと Jasper が初期化に必要になり、スピンアップが遅くなります (5,600 ms ほど)
    • Freemarker などを使うようにすると、Jasper が必要ありませんのでスピンアップが遅くなりません
    • Frontend は Go や Python など軽いランタイムで動かすのも手です。API を叩いてデータを取ってくるか、または Remote API を使って直に Datastore を触る事を検討します。
  3. DatastoreCallbacks を効果的に使う
  4. TaskQueue を効果的に使う
    • とんでもなく便利です。
    • 本当に軽い処理で、URL Handler を切るまでもなければ、DeferredTask が使えます。クラスを書いて、Queue に突っ込むだけで処理が出来る便利クラスです
  5. Upload 処理は BlobstoreService.createUploadUrl() を使う
    • Google フロントエンドでファイルの保存処理を肩代わりしてくれるイケメンサービスです
    • アップロード処理はファイルが大きいとメモリが辛い事になりますが、このメソッドを使うととても省メモリです
  6. Request と Response をログに吐き出しておく
    • 標準では Request と Response の payload がログに吐き出されないので、普通に困ります。
    • ServletFilter などを使い、ログを吐き出しておくと便利です。
    • ただし Java では、ServletFilter で Request や Response を読もうとすると面倒があります。
    • コードを整理したら GitHub にでも置きます。
  7. Max Idle Instances は 1 にしておく
    • これの意味合いは「遊んでてもいいインスタンスの最大数」です。「全台数」ではありません。従って、非常に少ない数に設定しても通常パフォーマンスが落ちる事はありません。
    • この数値は標準では Auto ですが、非常に富豪的な設定です。リクエストに応答しているわけでもないインスタンスが乱造されます。また、これらは長い時間生き続けます。
    • 1 にすると最低限他のインスタンスからあふれた新しいリクエストにも対応出来るし、無駄なたくさんのインスタンスも出来ないのでちょうど良いです。
  8. Instance Size を上げておく
    • 標準の 128MB Instance では JVM と必要なクラスをロードするだけで割とかつかつです。
    • 基本的に Instance Size は大きければ大きいほど効率よくリクエストが捌けます。

さんこう

PubSub なもうそう

Entity の変更を通知したいことが多々ある。たとえば、「メッセージを受信しました」は、recipient_id が自分の Message オブジェクトの追加を検知して、通知を行なっていると言える。

このハンドラーは例えば Rails であれば Model の after_create などで管理されることになる。 しかし、このような書き方をすると "Entity の管理" と "通知" の責務が一緒くたになってしまう。

分割!分割!と叫ぶわけではないけど、モバイルの Push 通知の台頭で、通知の負う役割範囲が大きくなってきた。 以前までは Email で通知すれば十分だったものが、Email, APNS, GCM と通知サービスの幅が広くなってきている。Web においても、更新された情報を元にページをリアルタイムに更新したい、という欲求が芽生えるときがある。このような場合、WebSocket だとか Polling だとか考えだすと、「通知」と一言で呼ぶものはいかに幅広いものか。 他には、ログやアナリティクスの要望も大きい。通知が失敗したら、再送をしたいという要請だってある。そして A/B テスト…と。 場合によってはユーザの locale によって i18n したりね…

ここまでやることが増えると、分割したほうが良いというのは自然な欲求だと思う。

Entity だって after_create とか考えなくて、何でも良いから変更をぶん投げたい。

上記の理由を踏まえ、次のようなあーきてくちゃを考えてみた。

  1. DB サービスは、Entity の変更をすべて PubSub サービスに通知する。
  2. 通知フィルタサービスは、PubSub からの通知を購読し、通知すべき情報をフィルタリングし、実際に通知を行なう。
  3. 通知サービスは通知を行なう。

まあ、大仰にサービスを分けなくとも、コードだけでも分けることが出来れば十分。3. は Amazon SNS, SES にぶん投げる事が多いと思います。

A/B テストしたい!とか、管理画面で文言テンプレート変えたい!などとなってくると、流石にサービスとして分割した方が良いでしょう。

というもうそうを垂れ流すだけで記事を終わる ( ´ー`)。о

もっと読む

Riot.js を試食する

いろいろ雑いコードで動きます。jQuery 以上 Vue.js 未満くらいで考えるといいです。 Rails + HTML テンプレートで大部分が動いてて、ちょっとだけ JS を入れちゃうときに使うカンジです。 SPA には向きません。

eval とかも使われていなく、<script> に書いたものがそのまま function として使われているので、デバッグもそこそこ考慮されてると思います。

肝心の試食コードはこんな感じ

<book>
    <ul>
        <li each={books}>{title}</li>
    </ul>

    <form onsubmit={add}>
        <input type="text" name="name"/>
        <button>Add</button>
    </form>

    <script>
        this.books = [];

        add(e) {
            var input = this.name;
            this.books.push({"title": input.value});
            input.value = "";
        }
    </script>
</book>

これで <book></book> と HTML に書いてやると動きます。

riot.js 本体が 3.5KB (minify + gzip) と軽く、フレームワーク自体が重量級な Angular とかを入れたくないときとかに良いと思います。

依存関係とかどうするんだろう?と思ったけど、多分依存も無いような小さいことをするためのフレームワークなんでしょう。 やるならば、browserify で require したり、普通に出力された js と依存ライブラリと一緒に concat してやるとか。

Chrome Push Notification でメッセージに本文を載せる方法

2015/04/28 現在、Chrome (M42) では Push Notification にてメッセージの本文を変える事が出来ません。 これは、現在の仕様では GCM からの payload に任意のデータを載せる事が出来ないためです。したがって、毎回同じメッセージを表示する事になってしまいます。

これでは迷惑な通知にしかなりませんね。対策を探していたら偶然見つかったのでここに書きます。

次のような処理の流れにてメッセージの本文を変えることが出来ます。

  1. Push Notification を受け取る。
  2. Service Worker 上の push イベントが起こります。
    • Service Worker は、通知のメッセージを取得する API を叩きます。
      • ここで即座に showNotification() をするのではなく、API を叩いてデータを取ってきて、その上で表示を行なうのがみそです
      • タイトル、本文、アイコン URL、タップ時の遷移先 URL あたりを載せると良いと思います。
    • 取得した通知データを用いて、本文を表示します。
    • (パラメータとしては、subscriptionId を使うことが出来ます。)

元ネタは Roost という、まんま Chrome, Safari 向けの通知を提供しているスタートアップのコードです。 Roost の Service Worker のソースへのリンクを載っけておくので、上記の処理を実装したい方は各自参考にしてください。

https://dl.dropboxusercontent.com/u/7817937/roost-chrome/roost_worker.js

クローラー開発の知見

  1. 鍵は「リンク先を辿るか辿らないか」の処理。
    • この処理が甘ければ甘いほど、同一サイトのクロールが遅くなります。同じ URL を踏まないようにするため、以前辿った URL を除外したり、パラメータをイイ感じに省いて正規化してあげる必要があります。
    • (鍵は「クロール枠の節約」と言い換える事も出来ます)
    • 特別なことが無い限りは ?, # 移行は捨てる
    • ただし、一部サイトはまだ ?id=hoge などとやっている
    • canonical を使った URL 正規化の判断はけっこう面倒くさいです
  2. 既に辿った URL は KVS に突っ込んでおく。
    • 5 万ページ程度のクロールでも 150 万くらいのリンクがあります
    • MySQL とかに突っ込むと既に辿ったページを判定する為のクエリがとても重い
    • 素直に KVS に突っ込みましょう(AppEngine なら Datastore + Memcache)
  3. ページをクロールするときはとりあえず丸ごと S3 とかに突っ込んでおく
    • その場でスクレイピングとかしようとすると、Exception に弱過ぎるし、後で情報が追加で欲しくなったときに困ります。とても
    • とりあえず S3 とか GCS に突っ込んでおいて、あとから幾らでもリトライ出来るようにします
    • スクレイピングタスクはこの後キューに入れると良いです

同一ホストに対しては 1 秒 1 URL のリクエスト、とクロール枠を定めると、一日あたり 86400 ページしか取得出来ません。 URL 正規化をミスると簡単に本来の 10~100 倍ほどのクロール対象 URL が出来てしまうので、URL 正規化をキチンとやらないとクロールが一生終わりません。

開発のおとしあなリスト

  1. 言語リプレースは絶対にしない
  2. 書き直しは絶対にしない
  3. 絶対に VPS を使わない
  4. 特殊な言語 / FW を使わない
  5. 歴史の浅いライブラリを使わない
  6. スキーマをなるべく変えない
  7. 必ずログを見られるようにしておく。可能ならば BigQuery に入れておく
  8. リリース時には必ず github release して仕様の変更を記述しておく
  9. 分からないものを使わない分かり辛いものを使わない

カッコ良さとか最初の楽さとかを無視して技術選定すればイイ感じに罠を避けられます。 あとは、どれだけ気持ち悪くても、エンジニア的な気持ちよさで開発の方向性を変えないようにしましょう。

オチ: ない

リリース後のデバッグのために、ログと DB のダンプを送る機能を付ける

アプリはリリースするととにかくデバッグしにくいです。 Crashlytics を入れておくのは当然としても、これだけでは対応しにくいケースが沢山あります。

たとえば、データの不整合が出たり、予期せずデータが消えてしまったりするケース。 これらのケースは例外が発生しないので、とてもデバッグがしにくい。サポートもしにくい。

という事態が現在進行形なので対策を考えました。

解決策

「ログ」と「DB」のダンプを送れる機能を付ける。

容量の問題。 DB については、件数にもよりますが 1000 件のレコードがあっても ~1MB で済むと思いますので、送れない範囲ではない。 ログについては、1 レコード ~100bytes ほどだと思うので、直近のレコード数で切って送信すれば問題ないでしょう。

とりあえず実装方法を妄想するだけしてみました

ログの保管の仕方(妄想)

iOS の場合、マクロを使って専用のロガーを作って上げると良いと思います。 つまり、NSLog を直接つかうのではなく適当な Prefix を付けた SOMELog を使ってログするようにします。

#if DEBUG
#define SOMELog(...) NSLog(@"SOME: %@", [NSString stringWithFormat:__VA_ARGS__]);
#else
#define SOMELog(...) SOMEStoreLog(@"SOME: %@", [NSString stringWithFormat:__VA_ARGS__]);
#endif

これで SOMEStoreLog を実装すれば OK です。中身は適当にバッファしてファイルに保存するだけ。 CocoaLumberjack/CocoaLumberjack · GitHub とかがイイ感じに実装を提供していたと思います。

DB データの取得方法(妄想)

.sqlite ファイルを丸ごとアップロードすれば良いと思います。 CoreData を使っていれば NSPersistentCoordinator でファイルパスを指定しているはずなので、こちらを取得してアップロードします。

まとめ

こんな感じでやってみるかもしれないし、良かったらまた書くかも