2014-12-24

OpenSpendingを支える技術

このエントリはCivic Tech Advent Calendarの23日目の記事です。突然ですが、私が時々コミットしているOpenSpendingについて技術周りの事を書きます。
https://openspending.org

OpenSpendingとは

OpenSpendingは政府や自治体の予算・支出・財務諸表等の情報を集めてマッピングしたり分析したり、配布するプラットフォームです。誰でも登録されているデータセットを利用する事ができ、API経由での取得、ヴィジュアライゼーションの作成ができます。また手元にデータセットがあれば登録する事ができます。現在73カ国、905のデータセットが登録されています。
By understanding how governments spend money in our name can we have a say in how that money will affect our own lives.
The journey starts here.
トップページにある上の文章が示す通り、税金がどのように使われるかを知る事で、我々がそれに対してどうあるべきか発言できる事を目指しています。

一体どこで使われているのよ、と思うかもしれませんが有名な所でいうと日本の地方自治体ローカライズ版も多く作成されたWhere does my money go?というサイトがOpenSpendingのAPIを利用しています。

開発と運用はOpen Knowledge Foundationの支援を受けてコミュニティベースでされています。

OpenSpendingの開発

OpenSpendingはGitHub上で開発が進められており誰でも参加する事ができます。

アプリケーションサーバーはPython(Pylon + Celery Worker)、全文検索にはSolar、WebフロントはJavaScript、主にd3.jsが使われています。今時Pylon?と思われるでしょうが、もちろんFlaskにリプレースする作業も進行中です。

開発チーム目下の課題とこれからのアーキテクチャ

OpenSpendingの提供する機能を分類すると
  • データの保存
  • データアップロードとETLプロセス
  • データ取得と分析、クエリ
  • Webサイト
  • ヴィジュアライゼーション
といった具合になります。OpenSpending開発チームが課題としているのは、現状のアーキテクチャはこれらが1つのアプリケーションとして動作している事によるメンテナンス性の低下です。例えばデータの保存場所をAmazon S3にしようとしても、すぐにやれないといった状況です。開発チームの議論ではrisk of causing a butterfly effectといった文芸的な表現になっており、とてもそんな表現は思い付かないなと関心してしまいました。

さて、この状況を改善するために現在議論されているのがマイクロサービス化です。
https://github.com/openspending/osep/blob/gh-pages/01-approach-and-architecture-of-openspending.md

詳細は次のドキュメントにまとめられていますが、各役割毎にコンポーネント化しマイクロサービスとしてそれぞれを動かすといった方針になっています。
マイクロサービスそれぞれがDockerコンテナで動いてオーケストレーションされて全体が動作するとなると、いかにも今風な感じですね。

UI周りの開発

アーキテクチャ全体の話とは別に私が見ている所だと、ヴィジュアライゼーションの追加というタスクがあります。公会計の可視化は、よくある企業会計のそれとはまた違った目的、ターゲットとなるので別の難しさがあります。
ヴィジュアライゼーション作成画面

その他

アプリケーションのコードの他にも、OSSのプロダクトとして開発者を集め、コントリビュートしてもらうためにはドキュメントや開発環境を構築するためのスクリプトも必要になります。特にコマンド1発でローカル環境ができない様では、開発者に興味持たれたとしてもすぐに逃げられてしまうでしょう(しかし最近までVagrantfileがバグっていた) 。この辺りの作業も人が足りていない印象。

Contributor募集

というわけで開発者が足りていません。特に日本人はAPIをガツガツ叩くくせにコミュニティにコミットしてくれないと評判です。PythonかJavaScript、もしくはインフラの知識があれば大丈夫だと思うので是非開発に参加してみてください。

GitHubのISSUEには今すぐ着手できそうなライトな物も登録されているので、チラ見してから考えるのも良いと思います。



このエントリーをはてなブックマークに追加

2014-10-26

IRKitを使った簡易オレオレリモコンをHTMLとJavaScriptで作る

音声で家のテレビやオーディオを操作したいと妻からの要望があったので作る。100%仕様を満たす物は1日以上かかりそうなので、まずはiPhoneからタップでそれぞれ操作できるようにした。

完成図


学習リモコンの選定

まずは学習リモコンの選定。PlutoIRKitかどちらにしようか悩んだが、LANケーブルを引き回すのが困難なのでIRKitに決定。amazonで7,700JPY也。同時に無線LANを安定化させるために古い機器を引退させて、新たにAtermの無線LANルーターを導入した。

IRKitの設置

設置に必要な物はMicroUSBの電源供給のみ。適当な場所に設置してまずは自宅の家電が操作できるか試す。ここではApp Storeから入手したIRKit シンプルリモコンを使った。IRKitのwifiへの参加もやってくれて便利。
IRKitの赤外線は割と貧弱で、家具の配置や日光の影響を受けてしまう。設置場所は微調整を繰り返した。

HTTP APIのテスト

ここでは家にいる時に操作するユースケースのみを想定して、IRKit Device HTTP APIを使う。IPアドレス直指定で叩くため、ルーターの設定でIRKitのmacアドレスについて固定IPアドレスを割当するようにしておく。
% dns-sd -B _irkit._tcp
Browsing for _irkit._tcp
DATE: ---Sat 25 Oct 2014---
23:14:31.983  ...STARTING...
Timestamp     A/R    Flags  if Domain           Service Type     Instance Name
23:14:32.183  Add        2   4 local.           _irkit._tcp.     iRKitXXXXX

% dns-sd -G v4 irkitXXXXX.local
DATE: ---Sat 25 Oct 2014---
23:15:35.078  ...STARTING...
Timestamp     A/R Flags if Hostname            Address                  TTL
23:15:35.239  Add     2  4 irkitXXXXX.local.   192.168.10.4             10

# IRKItに向けて保存したいリモコンのボタンを押してから
% curl -i "http://192.168.10.4/messages”
HTTP/1.0 200 OK
Access-Control-Allow-Origin: *
Server: IRKit/2.1.2.0.g424fa95
Content-Type: text/plain

{"format":"raw","freq":38,"data":[5408,4107,640,4107,640,1738,663,1738,663,1738,663,4107,663,1738,640,1738,640,4107,663,1738,663,4107,710,1679,686,1679,686,1679,686,1679,686,50610,640,4107,640,4107,640,4107,640,1738,640,1738,640,1738,640,4107,663,1738,640,1738,640,4107,686,1738,640,4107,663,1738,640,1738,640,1738,640,1738,640,50610,640,4107,640,4107,640,4107,640,1679,640,1679,640,1679,640,4107,619,1738,640,1738,640,4107,640,1738,640,4107,640,1738,640,1738,640,1738,640,1738,640]}%

# 上で得られたIRデータをPOSTで投げて、リモコンと同じ動作になるかテスト
% curl -i "http://192.168.x.x/messages" -d '{"format":"raw","freq":38,"data":[686,41 ….'

HTTP/1.0 200 OK
Access-Control-Allow-Origin: *
Server: IRKit/2.1.2.0.g424fa95
Content-Type: text/plain
このあたりの手順は公式サイトに詳しくのっている。あとはここで得られたIRデータをIRKitに投げる画面を書けば良い。

UIを書く

IRKitのHTTP APIは Access-Control-Allow-Origin: * ヘッダを返してくるので、ブラウザのXMLHttpRequestで叩いてもレスポンスが確認できる。なのでHTMLとJavaScriptだけで完結する。コードはgithubにアップしました。
iPhoneのSafariで「ホームに追加」をしておくとSafariのタブの一つでは無く、個別アプリとして扱われるので便利。

音声認識の実装はどうしようかと悩みつつ今日はここまで。

IRKit - iPhone,iPadを使って外出先からエアコン等の家電を操作できる学習リモコンIRKit - iPhone,iPadを使って外出先からエアコン等の家電を操作できる学習リモコン
maaash.jp
売り上げランキング : 2255
Amazonで詳しく見る by AZlink

このエントリーをはてなブックマークに追加

2014-09-15

PyCon JP 2014 オープンスペースでインタラクティブプログラミングについて発表しました

PyCon JP行ってきた。話せるネタはあったものの、事前にトークセッションのプロポーザルを出してなかったのでオープンスペースで細々とやりました。



共用IPython Notebookサーバーの運用

共用のデータ分析環境としてIPython Notebookを運用する場合、適度に再起動をかけたりと、気にしなければならない事が多々ある。現在のv2系にはマルチユーザー機能が無い*1ので、自ずとノーガード戦法となり、ある程度のリスクを許容しつつの運用となるが、その中でもやれる事はやろうという話です。

インタラクティブ環境を意識したメソッドの作り

折角のインタラクティブ環境なので、動作はサクサクしていた方が良い。データロード用のショートカットメソッドなんか特にdocストリングとレスポンスタイムが重要。あと不意の事故防止。

脚注:
*1: v3系で追加予定がある

このエントリーをはてなブックマークに追加

2014-09-01

ハイパフォーマンスブラウザネットワーキング12章「HTTP 2.0」

社内のハイパフォーマンスブラウザネットワーキング勉強会、12章「HTTP 2.0」の担当をしたのですが、現在の仕様(draft 14)と異なる点がいくつかあったので資料にしました。



大きな所はヘッダ圧縮の手法として紹介されていた「ヘッダの差分だけ送る」という所が無くなった点かな。

 サーバープッシュのインパクト

アプリケーションエンジニア目線だと実際の工数にインパクトがありそうなのはサーバープッシュだろうか。リソースの優先ロード、遅延ロードといった処理をブラウザと通信プロトコルが受け持ってくれれば、アプリケーションの開発者はその分楽ができて良い。画像のbase64インライン化やJSファイルの結合といった処理も、modpagespeedの様なミドルウェアにまかせるアプローチに徐々に移行していくかもしれない。

中間装置まわりの話

普段は意識していない所なので中間装置がどうといった話はイメージがつかみにくい。例えばTLSの終端がHTTP/2の終端なのかどうか、例えばロードバランサの中はもうHTTP1.1となるのか、あたりは疑問が残ったのでまた調べようと思う。

ハイパフォーマンス ブラウザネットワーキング ―ネットワークアプリケーションのためのパフォーマンス最適化ハイパフォーマンス ブラウザネットワーキング ―ネットワークアプリケーションのためのパフォーマンス最適化
Ilya Grigorik,和田 祐一郎,株式会社プログラミングシステム社

オライリージャパン
売り上げランキング : 94924
Amazonで詳しく見る by AZlink

このエントリーをはてなブックマークに追加

2014-08-31

YAPCに行ってきた

YAPCに行ってきました。Perlのセッションを避けたつもりは無かったのですが、モバイルアプリ開発やデータ分析のセッションに出ていたらほとんどPerlの話はありませんでした、適材適所というか、普段Perlを書かない人間には参加の敷居がどんどん下がっている印象。

Google BigQuery で DWH 構築

BigQueryといえばGoogle Analyticsのプレミアムプランの印象が強くて、100万JPY/month払って使う物と考えていたが。自前DWHのストレージとして使うととても安く、どんなクエリでも高速に帰してくれる奴だと知った。今丁度データ分析プラットフォーム構築業をしているので、休み明けにでも検証したい。
https://speakerdeck.com/naoya/google-bigquery-falsehua-number-yapcasia

JSON SQLインジェクション脆弱性と、そこから学ぶセキュアプログラミングの原則

構造化されたクエリパラメータをパースできるようなおしゃれなWebアプリケーションフレームワーク、使ってみたいと思った。

そんなにビッグでもないデータ処理手法の話

fluentdの圧倒的人気。この分野は自分が素人なので、どんなミドルウェアがあるのか知れたのはよかった。BigQuery等の登場によりデータ保持コストとコンピューティングリソースコストがこのまま下がり続けると、サンプリング調査や信頼区間といった統計的な手法を忘れて常に全数調査で良くなる、みたいな話をHUBでssig33とした。
http://www.slideshare.net/tagomoris/handling-not-so-big-data

モバイルアプリとAPIのありかたを考える2014

良く見る画面だなと思ったらParse.comのデモだったり。
JSON-RPCのバッチリクエストの話は、これはシンプルさを捨ててパフォーマンス(バッテリー効率、処理速度)を取るというアプローチなので、モバイルアプリなら全然ありかなと思った。

curlでターミナルから打つのが大変になる等のデメリットはあるが、クライアントが任意にリクエストを一つに纏められるというのが大きい。アプリ起動時だとユーザーのステータスやイベントの有無を取得したり複数のAPIを叩くというのはよくあるし、バッテリー効率を考えたらユーザーの操作ログみたいな物は纏めて送れた方がいい。サーバー側の設計の話はあまり無かったけど、APIコールのコストをけちって、貪欲なレスポンスを返さなくても良いので、サーバーAPIの粒度を小さくできるのもメリットかな。

あと、この話を聞いてる最中に自前ライブラリのバッチリクエスト対応をした。

Mobile Application Development for Perl Mongers [ninjinkun x gfx]

いい話だった。アプリ開発にgit-flowを使うの、仕様フリーズしてQA期間がありつつも次のバージョンの開発もする場合は確かにそうだなと。MVVM重要、Reactive Cocoaは後でチェックする。

その他

YAPCサイトのトークスケジュールの画面、iPhoneから見ると [ビギナー]とか[レギュラー]の難易度表示が無いの、罠だった。
大学内にHUBがあるのやばい。

まとめ

CONBUとスタッフの皆さんありがとうございました。



このエントリーをはてなブックマークに追加

2014-08-30

jquery-jsonrpc2.0のバッチリクエストとPromise対応

オレオレJSON-RPCライブラリがバッチリクエストに対応していなかったので対応した。こんな風に書けるようになった。
$.jsonrpc.defaultUrl = '/rpc';

// Send 3 requests at once.
$.jsonrpc([{
    method: 'getEventStatus'
}, {
    method: 'getUserStatus'
}, {
    method: 'sendLoginStatus',
    params: {status: 'login'}
}]).done(function(responses) {
    results.forEach(function(response) {
        if (response.result) {
            doSomething(response.result);
        } else {
            handleError(response.error);
        }
    });
}).fail(function(error) {
    // timeout or 503 or bad response
});
なぜ今になってメンテしたかというと、モバイルアプリはクライアントが任意に複数のAPIコールを一つのHTTPリクエストに纏められた方が良いよねと最近思う様になった*1 のと、今日のYAPCのセッションでJSON-RPCのバッチリクエストについて説明があったので。YAPCの感想はまた次のエントリで。

あとbowerのリポジトリを眺めていたら、WebSocketを使う物を見つけたのでこちらの方がおすすめです。

脚注:
*1: Orreily「ハイパフォーマンスブラウザネットワーキング」の影響

このエントリーをはてなブックマークに追加

2014-08-04

ハイパフォーマンスブラウザネットワーキング輪講を社内でやっている話

ハイパフォーマンス ブラウザネットワーキング ―ネットワークアプリケーションのためのパフォーマンス最適化ハイパフォーマンス ブラウザネットワーキング ―ネットワークアプリケーションのためのパフォーマンス最適化
Ilya Grigorik,和田 祐一郎,株式会社プログラミングシステム社

オライリージャパン
売り上げランキング : 94924
Amazonで詳しく見る by AZlink
O'rreilyのハイパフォーマンスブラウザネットワーキングは日本語版が発売されたのを機に輪講形式で社内勉強会をやっている。先週の時点で8章の「モバイルネットワークの最適化」が終り、丁度折り返し地点である。

なぜ輪講形式にしたか

本の内容は、光ファイバーや無線の規格といった下のレイヤからHTTP2やWeb RTCの上のレイヤまでカバーしている。その範囲からして、アプリケーションエンジニアだけでやるよりも、インフラの人に解説してもらった方が面白いのでは、と思ったから。

例えばTLSの章に出てくる証明書チェーンの最適化のくだり、インフラとアプリで分業が進んでいるとアプリエンジニアは運営しているサービスの証明書チェーンがどうなっているかなんて意識していないだろう。TCP最適化の所には「サーバーは最新のカーネル使え」とあるがフロントのエンジニアにはどうしようも無い。本に載っているテクニックが採用可能かといった議論するには各分野に詳しい人がいた方が良い。

モバイルアプリ開発者が読んで得られる事

今の時点で書ける所を。特に5章から8章はモバイルアプリ開発者には面白いはず。TCPやUDPは他にいくらでも本はあるが、アプリ開発者向けに無線技術、モバイルネットワークの解説があるのは本書ならでは。

例えば自分の場合

  • 移動していれば接続基地局は切り変わるはずだが、TCPのレイヤで見るとサーバーと繋がったままに見えるのはなぜか
  • モバイルアプリでもGeoIPで良い感じに端末のいる都市が取れるのはなぜか
  • 位置情報を送り続けるアプリを適当に実装すると、あっという間にバッテリーを食いつくすのはなぜか

といった前からの疑問が解決した。バッテリーを無駄に消費しないためには、実際にどう実装すれば良いのかという知見と失敗例は参考になる。

あと、WebRTCに入門してみると必ず出てきて、ネットワーク初心者に混乱をもたらすSTUNやTURNなる謎のワードも前半のNATの章に解説がある。XMLHttpRequest等のブラウザに特化した話は後半になるまで出てこないが、いきなり後半をやるよりも最初から目を通していくのが良いと感じた。

普段なにげなく使ってはいるが中身の動作は知らなかった、という部分がどんどん埋められていく感覚が気持ちいい。

まとめ

カバーしている範囲が広いだけに、アプリ、セキュリティ、インフラ各々得意な分野が異なる人で集まって議論すると面白い。
後半もがんばるぞいという事で、参加者の皆様もいつもありがとうございます。

このエントリーをはてなブックマークに追加

2014-06-30

アプリケーションエンジニアとサーバープロビジョニングツール

自分はサーバープロビジョニングといえば、AWS上の本番・ステージング・共有開発機もろもろインフラチームにまかせっきりだった。しかしアプリエンジニアがローカル開発環境を作る用途ならChefやPuppetよりもAnsibleがマッチしていると思った次第。

背景

自分の職場だと、インフラチームはPuppetのマニフェストでサーバーを管理している、例えばサーバーにインストールするパッケージ やApacheの設定である。しかし、そのPuppetマニフェストはアプリエンジニアのローカルの環境を構築する用途としてそのまま使えない。

インフラチーム管理下のサーバーにおいてPuppetマニフェストで管理されている部分はアプリケーションエンジニアは触れないため、変更を加えたい場合は都度依頼を出している。

共有開発サーバーとローカル個人環境

例えばApacheとfluentdとElasticsearchはインフラチームが管理しており、KibanaでApacheログが見れるとしよう。Kibanaに思った通りのグラフが出せない時、自由にできる環境がなければどこをいじれば良いのか試行錯誤すらできない。必然的に共有開発サーバーではなく、個人環境が欲しくなる。

Ansibleを使ってみる

ここで欲しいのは、Apacheログがfluentdに回収されてElasticsearchに放りこまれてKibanaで見られる環境をコマンド一発で作れる何かである。Chefでも良かったが、もっと気軽に使える物は無いかとAnsibleを試してみた。

結果、作成が必要だったのは対象のサーバーを指定するファイルとレシピに相当するyamlファイル、コピーして配置する用の設定ファイル(httpd.conf等)だけで済んだ。至って簡潔。しかも対象サーバーにPython 2.4以上さえ入っていれば使える。

リポジトリには起動用のMakefileとREADMEを追加してコミットしておけば良い。

サーバープロビジョニングツールについて

インフラチームが使う高機能な物と、アプリケーションチームが使う簡易な物、それぞれ別の物が流行ると思う。少なくともChefは二度使った経験からすると、使い方とレシピの書き方を思い出すのが面倒すぎる。

ちなみに、本エントリの例題構成をプロビジョニングするAnsibleのPlaybookは次の通りになった。わかりやすい。
- hosts: webserver
  user: vagrant
  sudo: yes
  tasks:
    # Apache
    - name: Install apache
      apt: name=apache2 update_cache=yes
    - name: Copy /etc/apache2/apache2.conf
      copy: src=./settings/apache/apache2.conf dest=/etc/apache2/
    - name: Restart apache
      service: name=apache2 state=restarted

    # Elasticsearch
    - name: Get apt key for elasticsearch
      apt_key: url=http://packages.elasticsearch.org/GPG-KEY-elasticsearch
    - name: Add apt repository
      apt_repository: repo='deb http://packages.elasticsearch.org/elasticsearch/1.1/debian stable main'
    - name: Install JDK
      apt: name=openjdk-7-jre-headless update_cache=yes
    - name: Install elasticsearch
      apt: name=elasticsearch update_cache=yes

    # Elasticsearch Plugins
    - name: Remove elasticsearch-head
      command: /usr/share/elasticsearch/bin/plugin --remove mobz/elasticsearch-head
    - name: Install elasticsearch-head
      command: /usr/share/elasticsearch/bin/plugin --install mobz/elasticsearch-head
    - name: Remove elasticsearch-kibana
      command: /usr/share/elasticsearch/bin/plugin --remove elasticsearch/kibana3
    - name: Install elasticsearch-kibana
      command: /usr/share/elasticsearch/bin/plugin -url http://download.elasticsearch.org/kibana/kibana/kibana-latest.zip --install elasticsearch/kibana3

    - name: Restart elasticsearch
      service: name=elasticsearch state=restarted

    # td-agent
    - name: Get apt key for treasure-data
      apt_key: url=http://packages.treasure-data.com/debian/RPM-GPG-KEY-td-agent
    - name: Add apt repository
      apt_repository: repo='deb http://packages.treasure-data.com/precise/ precise contrib'
    - name: Install td-agent
      apt: name=td-agent update_cache=yes

    - name: Install libcurl-dev
      apt: name=libcurl4-openssl-dev update_cache=yes
    - name: Install plugin
      command: /usr/lib/fluent/ruby/bin/fluent-gem install fluent-plugin-elasticsearch

    - name: Copy td-agent.conf
      copy: src=./settings/td-agent/td-agent.conf dest=/etc/td-agent/
    - name: Restart td-agent
      service: name=td-agent state=restarted

まとめ

Ansibleはアプリエンジニアに向いている、というのと。以前、@voluntasの人に「御社はAnsibleやDockerを使って開発環境を作れるようにしてないんですか?」とつっこまれたのだが、これからはちゃんとできそうだ。

このエントリーをはてなブックマークに追加

2014-05-14

iOS Push通知配信サーバーのコネクション管理

前回のエラーハンドリングの話に続いてコネクション管理について。今どきPush通知配信サーバーを自前で用意する事は少ないかもしれないけど、そういう人向けの内容。

コネクションの管理

APNsのドキュメントにこうしろと書いてある。
Best Practices for Managing Connections

You may establish multiple connections to the same gateway or to multiple gateway instances. If you need to send a large number of push notifications, spread them out over connections to several different gateways. This improves performance compared to using a single connection: it lets you send the push notifications faster, and it lets APNs deliver them faster.

Keep your connections with APNs open across multiple notifications; don’t repeatedly open and close connections. APNs treats rapid connection and disconnection as a denial-of-service attack. You should leave a connection open unless you know it will be idle for an extended period of time—for example, if you only send notifications to your users once a day it is ok to use a new connection each day.

Local and Push Notification Programming Guide: Provider Communication with Apple Push Notification Service
つまり
  • 通知毎に接続をOpen/Closeするな、APNsはそれをDoSと扱う
  • Openしたまま維持しろ
  • 大量に配信したい場合は複数接続を張るといいよ
  • 1日に一回送るぐらいなら、その度に新しく接続を作ってもいいよ
 となるので、この通りに実装する。Webサーバーでユーザーからのリクエストをトリガーにして送るとその度にOpen/Closeになる、よってユーザー間等の通知を頻繁に送るアプリの場合はアプリケーションサーバーから通知配信処理のプロセスを分離する方式になるかと。

Gateway Serverからエラーレスポンスが返ってきた場合、向うから接続を切ってくる。その時は新しい接続を作る。

apns-proxy-serverでもスレッド毎にGateway Serverとの接続を持つようにして、接続の維持と並列化を実現している。

原因不明のBroken Pipeエラー

データフォーマットのエラーや、Invalid Tokenエラーとは関係無く、Broken Pipeに遭遇する事がある。Gateway Serverがなんらかの理由でコネクションを切るからだと思われるが、こちらとしては接続を張りなおして、送ろうとした通知からリトライするぐらいしかやれる事は無い。


このエントリーをはてなブックマークに追加

2014-05-12

Apple Push Notification Serviceのエラー処理について

iOSアプリにPush通知をするのに利用するApple Push Notification service(APNs)について。配信数がある程度の規模になると面倒事が増えるのでまとめた。
本稿では疎結合なサービスとして稼動させるPush通知配信サーバーを考える。

Push通知配信サーバーの機能要件

個々のアプリケーションから分離したPush通知配信サーバーを考える場合、要件は大きく分けて次の二つになるだろう。

A. デバイストークンを溜め込んでおき、配信日時を指定して一斉に配信する

  • ゲームのイベントが始まった事を全ユーザーに通知したい
  • ユーザーセグメントを指定してキャンペーンの通知をしたい

B. 都度送信対象のデバイスをアプリケーションから受け取って即時配信をする

  • チャットルームで発言がある度に、チャットルームのメンバーに通知をしたい
  • ユーザー間のmentionを通知したい
Bの場合は、アプリケーションから切り出す必要は無さそうだが、都度送信する通知の数が100 ~ 10万程になるとエラーレスポンスの取得とリトライが必要になり、アプリケーションから切り離したくなる。
どちらの場合も、非機能要件としてコネクション管理とエラーハンドリングが必要となる。

エラー処理の何が難しいか

APNsのドキュメントには、次のようにエラーレスポンスの記述がある。
If you send a notification that is accepted by APNs, nothing is returned. If you send a notification that is malformed or otherwise unintelligible, APNs returns an error-response packet and closes the connection. Any notifications that you sent after the malformed notification using the same connection are discarded, and must be resent. Figure 5-2 shows the format of the error-response packet.


Local and Push Notification Programming Guide: Provider Communication with Apple Push Notification Service
これが厄介なのは
  • 不正と判断された通知以降に送信した通知が破棄される、つまり再送が必要
  • エラーレスポンスが得られるまで、0.3 ~ 0.7秒ほどかかる
  • 正常時は何も得られない、本当に正常なのかエラーレスポンスを取りこぼしたのか判断ができない
  • エラーレスポンスを確認したかにかかわらずコネクションが切断される
といった点があるからで、エラー処理をさぼると「10万ユーザーに通知を送ったつもりが、実は1,000人にしか届いていなかった」という事態が起こる。

最初の一つしかエラーレスポンスが得られない

通知のバイナリフォーマットは複数の通知を一度に送信する事ができる。が、不正な通知が複数含まれていた場合、送信後に得られるエラーレスポンスは最初の一つだけである。複数の通知をまとめて送らずに、1個づつ送信した場合も同様で
  1. デバイストークンAに送信
  2. デバイストークンBに送信 (不正)
  3. デバイストークンCに送信
  4. デバイストークンDに送信 (不正)
  5. デバイストークンEに送信
  6. Bのエラーレスポンスが得られる
  7. コネクションを切られる  (C, D, Eは無かった事に)
といった動きになる。

エラーレスポンスの種類

Status CodeDescription
0No errors encountered 
1Processing error
2Missing device token
3Missing topic
4Missing payload
5Invalid token size
6Invalid topic size
7Invalid payload size
8Invalid token
10Shutdown
255None (unknown)
Table 5-1 Codes in error-response packet
ドキュメントによると11種類であるが、仕様通りのフォーマットで送信していて遭遇するのは8と10だけである。
8のInvalid tokenは実際に送るまでそうであると判定できない曲者。開発ビルドのアプリで取得されたデバイストークンが本番環境に混入するとInvalid Tokenになる。それ以外にも発生するパターンがあるかもしれない。

2,3,4,5,6,7は送信前のチェックで回避できる。

エラーレスポンスをチェックしてリトライ処理を実装する

リトライ処理には次の実装が必要になる
  • リトライ用に送信済み通知を保持しておく
  • 送信後エラーレスポンスを確認する
  • エラーがあれば、該当通知以降を再送する
エラーレスポンスの確認はブロッキングでrecvするとパフォーマンスに影響が出るが、selectを使ってノンブロッキングでやろうにもタイミングが難しかったので自分はブロッキングで実装した。(つまりノンブロッキングSocketプログラミング力が足りない。)

確実にエラーレスポンスを補足したい場合は、通知を1つ送る度にエラーレスポンスを待つのが良いが、それではパフォーマンス要件を確実に満たせない。0.5秒待つとして1スレッドで10,000通知を送ると83分もかかってしまうからだ。自分は500件毎にチェックするようにした。犠牲になるパフォーマンスはAPNsのドキュメントにあるベストプラクティスにならって接続を複数持つ事で補完する。

ここに書いた内容の具体的な実装はVOYAGE GROUPで利用しているPush通知配信サーバーのコードで見る事ができる。こんな事もあろうかとOSS化してある。
これは都度デバイストークンを受け取って配信を行なう、前述Bの要件にあわせて作っている。

補足:パフォーマンステストのやり方

例えば10万件の通知に不正なトークンを数個仕込みつつ30秒以内に配信できるかテストしたい場合がある、テストなので配信対象デバイスは手元の数個しか無い。この時、端末は機内モード、通知は expiry=0 で送信するとAPNsが到達しなかった通知を破棄してくれるので延々とブルブルする事を回避できる。すぐに機内モードを解除してしまうと通知を受信してしまうので、1分ぐらい空けると良い。

事前にデバイストークンをスクリーニングする

Invalid Tokenは実際に送ってみるまでそうとわからない。だが、Aのデバイストークンを溜め込むタイプのサービスであれば、事前に無音通知を配信して確認ができる。この時、alert、badge、sound無しにしておかないとユーザーからは謎の通知が届いたように見えるので注意。iOSクライアントの実装もそのような通知が届いた場合は無視するようにしておく。
送信後エラーレスポンスをチェックして、Invalid Tokenであれば消す。これをすると、配信時にエラーレスポンスの確認が不要になり爆速で配信ができる。

ASPを使うか、自前でPush通知配信サーバーを運用するか

前述のPush通知配信サーバーの要件
  • A. デバイストークンを溜め込んでおき、配信日時を指定して一斉に配信する
  • B. 都度送信対象のデバイスをアプリケーションから受け取って即時配信をする

Push通知ASPを検討する

ASPといえば最近はMixpanelやGrowth Pushといった分析ツールとセットになったAのタイプが注目を浴びている様子。

Aタイプのサービスは予算と折り合いがつけば使い易い物はあるが、100万通知を1分以内に、といった速度要件を満たせるかは不明。Bの用途で使いたい場合、通知1件毎にHTTPでやりとりする物は避ける。HTTPのオーバーヘッドがでか過ぎていつまで経っても終らないという事態になる。

OSSの何かを検討する

Invalid Tokenが混ざる環境ではリトライ処理が実装されている物でないと、そのまま使えない。apns-proxy-serverの実装に着手する前に調べたが、エラーハンドリングが丁寧なプロダクトは見つからなかった。(良い奴があったら教えてください。)

まとめ

selectを使ったノンブロッキングソケットでのエラー処理は後日試す。エラー処理のワークアラウンドは次の記事にも詳しく書いてあるので、これから実装する場合は参考になるだろう。
あと、apns-proxy-client-pyもOSSとして公開しているので、フィードバックやプルリク等お待ちしております。

このエントリーをはてなブックマークに追加

2014-05-08

バンディットアルゴリズムによる最適化手法 5章

Epsilon-Greedyの次はSoftmax。腕の実装とテスト実行のコードは前に使った物と同じ。

In [22]:
%run './shared_functions.ipynb'

Softmaxアルゴリズムの実装

Softmaxは過去に得た腕毎の報酬の期待値を元に、期待値が高い腕を多く試行する。
期待値をどれだけ利用するかは温度パラメータ $tau$ で制御する
  • $tau \rightarrow \infty$ の時、完全にランダムに腕を選択する
  • $tau \rightarrow 0$ の時、過去の期待値に従う
In [24]:
class Softmax(object):
    def __init__(self, temperature):
        self.counts = None
        self.values = None
        self.temperature = temperature
        
    def initialize(self, n_arms):
        # 腕を何回引いたか
        self.counts = zeros(n_arms, dtype=int)
        # 引いた腕の報酬の平均値
        self.values = zeros(n_arms)
    
    @staticmethod
    def categorical_draw(probs):
        z = random.random()
        cum_prob = 0.0
        for i in range(len(probs)):
            prob = probs[i]
            cum_prob += prob
            if cum_prob > z:
                return i
        return len(probs) - 1
    
    def select_arm(self):
        z = sum([exp(v/self.temperature) for v in self.values])
        probs = [exp(v / self.temperature) / z for v in self.values]
        return self.categorical_draw(probs)
    
    def update(self, chosen_arm, reward):
        # 腕を選んだ回数をインクリメント
        self.counts[chosen_arm] += 1
        n = self.counts[chosen_arm]
        
        # 腕の平均報酬額を更新
        value = self.values[chosen_arm]
        new_value = ((n-1)/float(n)) * value + (1/float(n)) * reward
        self.values[chosen_arm] = new_value
In [23]:
# 結果プロット用の処理
def plot_results(simulate_num, horizon, best_arm, results):
    fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(15, 5))
    plot1, plot2, plot3 = axes
    
    x = range(horizon)
    
    for result in test_results:
        accuracy = zeros(horizon)
        reward_ave = zeros(horizon)
        cumulative_rewards = zeros(horizon)

        param, chosen_arms_m, rewards_m, cumulative_rewards_m = result

        for i in xrange(horizon):
            best_arm_selected_count = len(filter(lambda choice: choice == best_arm, chosen_arms_m[:,i]))
            accuracy[i] = best_arm_selected_count / float(simulate_num)
            reward_ave[i] = average(rewards_m[:,i])
            cumulative_rewards[i] = average(cumulative_rewards_m[:,i])
            
        plot1.plot(x, accuracy, label='%10.2f' % param)
        plot2.plot(x, reward_ave, label='%10.2f' % param)
        plot3.plot(x, cumulative_rewards, label='%10.2f' % param)

    plot1.legend(loc=4)
    plot1.set_xlabel('Time')
    plot1.set_ylabel('Probability of Selecting Best Arm')
    plot1.set_title('Accuracy of the \nSoftmax Algorithm')
    
    plot2.legend(loc=4)
    plot2.set_xlabel('Time')
    plot2.set_ylabel('Average Reward')
    plot2.set_title('Performance of the \nSoftmax Algorithm')
    
    plot3.legend(loc=4)
    plot3.set_xlabel('Time')
    plot3.set_ylabel('Cumulative Reward of Chosen Arm')
    plot3.set_title('Cumulative Reward of the \nSoftmax Algorithm')

実行

Epsilon-Greedyより、報酬の高い腕に集中するのが速い。
In [30]:
SIMULATE_NUM = 5000
HORIZON = 250

means = [0.1, 0.1, 0.1, 0.1, 0.9]
random.shuffle(means)
arms = map(lambda mu: BernoulliArm(mu), means)
best_arm = array(means).argmax()

test_results = []
for temperature in [0.1, 0.2, 0.4, 0.8, 1.6]:
    algo = Softmax(temperature)
    chosen_arms_mat, rewards_mat, cumulative_rewards_mat = test_algorithm(algo, arms, SIMULATE_NUM, HORIZON)
    test_results.append([temperature, chosen_arms_mat, rewards_mat, cumulative_rewards_mat])
plot_results(SIMULATE_NUM, HORIZON, best_arm, test_results)

腕の報酬にわずかな差しか無い場合

期待値の差が小さくなるため、腕を選択する頻度も近くなる。よって最も報酬の高い腕を選択する確率は上がりにくい。
In [26]:
SIMULATE_NUM = 5000
HORIZON = 250

means = [0.2, 0.2, 0.2, 0.2, 0.3]
random.shuffle(means)
arms = map(lambda mu: BernoulliArm(mu), means)
best_arm = array(means).argmax()

test_results = []
for temperature in [0.1, 0.2, 0.4, 0.8, 1.6]:
    algo = Softmax(temperature)
    chosen_arms_mat, rewards_mat, cumulative_rewards_mat = test_algorithm(algo, arms, SIMULATE_NUM, HORIZON)
    test_results.append([temperature, chosen_arms_mat, rewards_mat, cumulative_rewards_mat])
plot_results(SIMULATE_NUM, HORIZON, best_arm, test_results)

このエントリーをはてなブックマークに追加