2014年2月17日 (月)

DevSumiでHerokuの公開負荷テスト(?)をした話

2/14にDevSumiでHerokuとWebSocketを使ったデモをやってきました。
今回はそのデモの総括です。

 

★概要(本番前)

今回僕が担当したのは吉田パクエ氏のネタセッションでのデモです。

年末に10分の持ち時間でHerokuの良いところをアピールできるデモをなんかやってくれないかというお話をいただきました。

その頃僕はちょうど初めて自分でWebSocketアプリを実装してみたところで、この技術がどの程度実用的なものなのかという点に興味を持っていました。

なので、何かしらWebSocketを使ったデモをやろうと思い作ってみたのがこれです。

http://ws-vote.herokuapp.com/rooms/demo

 

ボタンを押すと数字がカウントアップされるだけの極単純なアプリですが、WebSocketを使っているので誰かがボタンを押すと同じ画面を見ている全員の端末で数字がカウントアップされます。
会場にいる人にスマホで画面を開いてもらい数字がくるくる回るところを見せられれば結構面白いかなぁと思いました。(^^;

もうひとつのポイントはこれがネット上に良くあるWebSocktサンプルと違いスケールアウト可能な作りになっているという点です。
裏でRedisを使っているのでHerokuのDyno数をあげれば、それだけで負荷分散できます。

それなりに負荷テストも行ってましたし、当日は20Dynoあげるつもりだったのでどっからでもかかってこーい!と、むしろ誰も参加してくれなくて数字が回らなかったらどうしようかとそっちの方を心配していました。

 

★本番

さて迎えた本番、僕は3人のデモ担当者の中でトップバッターだったんですが、前に立ったら話そうと思ってたことを予定通りすっかり忘れて何喋ったかあんまり覚えてません。。。が、多分そんなに問題はなかったです。何故なら誰も僕の話なんか聞いてなかったから。。。(^^;;;

今回プレゼン資料もHeroku上のWebアプリとして作成しました。

 

http://flect-devsumi2014.herokuapp.com/

 

で、中に埋まっているのがセッション中約2時間弱投票を受け付けた画面での結果です。(本当は1時間の予定でしたがバグがあって指定の時間に投票が締め切られませんでした。。。(--)

数字をよく見てください。なんと合計で201万5487回もクリックされています!
スループットにして実に300回/秒!!(セッション終了後はほとんど参加者がいなかったはずなので実際にはその倍近く)


想定してた数字と2桁違いますがな。。。
いったい何が起こったんだよ。。。(--

 

★テロリスト達

数字が伸びた要因はいくつかあるんですが、それはおそらく以下の内容です。

 

PCではボタンにフォーカスあててENTERキーを押しっぱなしにするだけで連打できた

。。。(--
これ言われてみればなんで気がつかなかったんだろうという話なんですが、スマホメインで考えていたのでまったく気がついていませんでした。
PCで参加した人はほとんどこの方法で連打していたと思います。
まぁ盛り上がったので結果オーライです。(^^;

一瞬でSeleniumスクリプトを組み上げるスーパーハッカーの存在

僕の隣ではSeleniumでループまわして余裕でPC画面を眺めているツワモノが。。。(--
ていうかあなたついさっきまでデモ2番手としてAzureのデモやってましたよね?(^^;
この短時間でそんなこと思いつく発想力には感動すら覚えます。

Twitter見てるとSeleniumを投入したツワモノは他にもいたようです。

何故かピンクを勝たせようと暗躍するHeroku A氏

。。。(--

言っとくけどこのデモが落ちて一番切ない思いするのはお前だからな。。。(--

しかし、これらの攻撃にも負けずリクエストを捌ききったHerokuは素晴らしかったです。(これでA氏にドヤ顔されるのはなんだか不本意ですが。。。)

 

★WebSocket連続リクエストの話

さてさて、このように予想外にクリック数の伸びた今回のデモですが、実は事前の負荷テストから1セッションでの連続リクエストはほとんど負荷にはならないだろうという予想がありました。デモの結果を受けてそれはほとんど確信に変わっています。

何故そう思うかというとソケット繋ぎっぱで連続でリクエストを投げ続けるWebSocket通信は通信の性質だけを考えるとファイルアップロードと似たようなものだろうと思うからです。
今回一回のボタンクリックで送信するデータはホンの数バイト。WebSocket自体のオーバーヘッドがあるにしても1リクエストの転送量は10バイト程度でしょう。
連続1万回のWebSocketリクエスト」というと結構な負荷のように感じますが、「100KBのファイルアップロード(10B × 10000回)」と考えると全然たいしたことないですよね。
もちろん10バイト毎に処理が挟まるのでまったく同じに考えることはできませんが、一番コストの高い通信部分において大きな優位があることは間違いありません。

今回複数の端末から連続リクエストが発行されていたと思いますが、20台もDynoをあげていたおかけでそれらは良い感じにばらけていたはずです。なので1Dynoあたりの負荷は実はそれ程大きくはなかったと思われます。

ちなみにデモサイトは現在1Dynoしかあげていませんが、それでもENTER押しっぱで数字がくるくると回る様子が確認できます。(複数端末で同時に実行するとアウトだと思いますが)

これが「Ajaxリクエスト連続1万回」であれば、コネクションの接続、Httpヘッダの解析とはるかに1リクエストの負荷が大きくなります。

連続リクエストに強いというWebSocketの性質は多くの場面で活用可能と思うので、もっと大きくクローズアップされて良いものだと思いました。

 

★Papertrailの話

すいません。。。舐めてました。。。(--

まったくログサイズの計算をせずに500MBもあれば十分だろうとタカをくくっていたらわずか15分で転送リミットに到達。。。(--

ほんの数時間だけなんだからもっと良いプラン使っておけば良かったー(--

(Herokuのアドオンは完全従量課金なので、高額のプランを一時的に使用しても請求額は使った時間分だけ秒単位で日割り計算されます。)


ていうか、むしろTreasure Dataを使っておけば良かったですね。。。時間帯ごとの各色の伸び方とかをグラフ化できれば面白かったのに。。。
惜しいことをしました。。。(--

 

★Redisの話

今回一番負荷のかかるコンポーネントはRedisでしたが、これも極めて優秀でした。
1回のボタンクリックにつきRedisリクエストは2回(数字のINCRとPUBLISH)発行されるので2時間弱で400万回以上のリクエストを捌いた計算です。
RedisGreenの管理画面で見るとSlowRequest(閾値は多分50ms)のログはかなりてていますが一番遅いものでも300ms程度なのでまぁ許容範囲でしょう。

Redisも実はそんなに高いプランを使ってないんですがこんだけ動いてくれれば十分満足です。
僕はこれまではキャッシュとしてはRedisよりもMemcacheを使うことが多かったんですが、これからは多分Redisに乗り換えます。
速度もほとんど遜色ないと思いますし、Memcacheが外からステータスを知る方法がほとんどないのに対しRedisはCLIで色々な情報が取れるのが大きな優位です。(サービス使えばWebコンソールも使えます。)

ちなみに今回HerokuのDyno数を100まであげずに20という半端な数字に抑えたのはそれ以上あげるとRedisのコネクション数が足りなくなる不安があったからです。

 

★スマホの話

すみませんすみませんすみません。m(_ _)m
今回のデモは皆さんのスマホに「赤、赤、ピンク、ピンク、黄、紫、緑。。。」みたいな感じにほとんど攻撃のようにデータを転送していました。
データ転送量は多分大きめのアプリをダウンロードする程度だったと思いますが、CPUもそれなりに喰っていたはずで僕の端末(ELUGA P02E)は17時前に電池切れました。。。(--

僕のところでは割りとクルクルと数字動いていましたが皆さんの端末はどうだったでしょうか?
こればっかりは端末性能と通信環境によって大きなバラつきがあったはずで、ものによってはかなりカクカクした動作になっていたと思います。
実際、パクエさんが途中経過を発表していた時も僕の手元とは割と大きな数字の開きがあったりもしました。
個人的にはスマホのWebSocketも十分実用レベルと思いますが、Androidの標準ブラウザが対応していないなど別の評価もあるとは思います。

 

★まとめ

以上、個人的にはかなり満足な成果がありましたが皆さん面白かったですかね?(^^;
残念だったのはしょぼいバグがあって投票が指定の時間(16時)に終了しなかったことです。タイムテーブルの変更で投票終了時間がセッション終了時間よりも後になったため、ほとんどの人は気づかなかったと思いますが、最後まで見ていた人は僕が強制的に終了させた時になんらかのエラー画面を見たかもしれません。
そのエラーはHerokuのせいではなく人為的なものであったことは書き添えておきます。

チャレンジングな所は全部ちゃんと動いただけに、日付と比較して分岐すれば良いだけみたいなところでバグを仕込んでしまったのは結構悔しいです。。。(--

以下、今回の成果のまとめです。

  • WebSocketは既に実験的な技術ではなく十分に実用レベル
  • WebSocketはリクエスト連投に強く、Ajaxの代替として使うのもアリ
  • WebSocketをRedis併用でスケールアウトさせることは可能
  • Redisは優秀。今後はMemcacheに替えて使いたい。
  • スケールアウトの効果は絶大。Herokuがなければ多分こんな実験できてない。
  • Herokuが日本にあればもっと速かったんだろうなぁ
  • テロリスト怖い
  • 本当は赤が勝ってたんですよ

軽い気持ちで引き受けたデモでしたが、予想以上に収穫のある実験(というよりむしろテロ)となりました。(^^;
セッションに誘ってくれたパクエさん、DevSumi運営者の方々に心より感謝します。


★リソース

最後に今回のデモのソース、WebSocket関連で書いたブログへのリンクをまとめておきます。

 

 

Redisを使ってWebSocketをスケーラブルにするためのモジュールはアプリとは分離して作っています。興味ある方は使ってみてください。
デモアプリはルームを作成する機能を作っちゃえばパーティアプリとしてそこそこ面白そうな気がしてますが、誰か一緒に作りたい人はいませんかー(^^;
(もう一捻りあればSalesforce Mobile Hackに出しても良いかも)

2014年1月22日 (水)

git submoduleのすすめ

またしてもWebSocketのデモアプリを作ることになりました。

それほど凝ったものを作るつもりはないので、さきに作った汎用のRedisService(のプロト)を再利用すれば簡単にできる見込みです。(^^v

が、ここでどうしたものかと考えるのはどのように再利用するか?です。

新しいプロジェクトにソースをコピーしても良いんですが、それはあまりに芸がない。。。

ちゃんとやるならプラグイン化するのが良いんでしょうが、まだプロトでしかない(=コードの改修がガンガン発生する)段階でプラグイン化しても、外側のアプリを作りつつプラグインを修正しては入れ直し。。。とやるのはかなり面倒です。。。。(--

そんな折、Heroku MeetupのLTネタを探してDevCenterをあさっていたらふとこんな文書が目に留まりました。

https://devcenter.heroku.com/articles/git-submodules

Herokuではgit submoduleが使えるらしい。。。

git submoduleって???(初耳)


★ git submoduleとは?

ググりましょう。(^^;
日本語のサイトだけでも十分な情報が得られます。

要するにあるgitプロジェクトの任意のディレクトリに別のgitプロジェクトをまるごと取り込む仕組みです。

コマンドの使い方の説明は他のサイトに譲るとしてここでは最初の課題(RedisServiceをどのように再利用するか?)に対して、どの程度有効であるかを検証します。


★ とりあえずsubmoduleプロジェクトを作ってみる

今回作成するのはPlay2で使用する(広義の)プラグインです。
submoduleとして取り込む場合はappディレクトリに任意のディレクトリを作成してそこに取り込むことになります。

なので、submoduleのプロジェクトはそれにあわせたディレクトリ構成になっていなければならない訳ですが、幸いにもScalaにはpackageとディレクトリ構成が同じでなければならないという制限はありません。

またREADME.md等コンパイルと無関係なファイルがapp以下にあってもPlayは単純にそれを無視するだけです。

これを踏まえて、

パッケージ名: flect.redis
ファイル構成:
- README.md
- src/flect/redis/RedisService.scala


という構成でsubmoduleプロジェクトを作成しました。

https://github.com/shunjikonishi/play-redis-submodule

新規のPlayプロジェクトでこれをsubmoduleとして使用する方法はREADMEに記載した通りです。
app/redisディレクトリの下にsrcというディレクトリが現れるのはあまり一般的なディレクトリ構成ではありませんが、Playでは実行に問題ありません。
(もちろんsubmodule側でこれとは異なるディレクトリ構成を採用してもOKです。)


★ 何が嬉しいの?

ずばり外側のアプリを作りながら内側のライブラリを改修できることです。

submoduleのディレクトリはそのディレクトリに入ってしまえば通常のgitのディレクトリと変わりありません。
なので、普段と同じようにそこで修正、commit、pushなどを行うことができます。

外側のgitではsubmoduleのコミットIDしか管理していないので内側のsubmoduleを更新した場合は、外側でそのsubmoduleのadd/commitが必要になります。

ネット上ではここでsubmoduleと外側のgitが連動しないのがわかりにくい、という意見もいくつか目にしましたがこれはむしろ好都合。。。というよりも完全に意図的にこういう仕様にしていると思います。

これはさらに別のプロジェクトで同じsubmoduleを使うケースを考えてみるとわかります。

別のプロジェクトに同じsubmoduleを追加して、そこでもsubmodule側になんらかの修正を加えたくなったとします。
この場合修正はどこで行うべきでしょうか?

答えはもちろんそのプロジェクトのディレクトリの中で修正してしまえば良いのです。gitのリポジトリはどこが正ということはないので、どこで修正したって構わない訳です。

ここでの修正は既存の別プロジェクトには影響を与えませんし、必要ならばそっちでもpullすればOKです。


★ どこで使えそう?

今回はPlay/Scalaで使いましたが、考え方自体はシンプルなので他のフレームワークでも応用可能です。(Javaみたいにパッケージ名の制約があるものはちょっと悩ましいですけど。。。)

ですが、本命はなんといってもJavaScriptですね。既存のライブラリに手を加えて使うこともちょいちょいありますが、それがそのままgitリポジトリであるならプルリクしようかという気にもなるしバージョンアップも楽です。
書いてて気がついたけど、プルリクするような汎用的な修正じゃない場合でもバージョンアップと同時にマージできちゃいますね。素晴らしすぎる。(^^v

他にも社内ユース専用のちょっとした小物ライブラリなんかもこっちの管理方法の方がマッチすることが多いんじゃないでしょうか。

正直SBTにはかなりうんざりしているので、これは新しい時代の依存性管理の仕方としてアリなんじゃないかとさえ思います。

2014年1月17日 (金)

Heroku Meetup #11

昨夜はHeroku Meetup #11。

新年会と銘打っただけのことはあってご飯もお酒もいつもより増量されてて凄い楽しかったです。

日本酒持ってうろうろしている女子が複数いたのには笑った。(^^;

自分のやったLTの資料はこちら

http://www.slideshare.net/shunjikonishi/heroku-tips1

小ネタ集です。(^^;

LTは予定通り途中で切られたわけだが、他の人見てると最後までやりきる人も多くて、最初から全部喋る気のなかった自分をちょっと反省した。(--

また、小ネタがたまったらLTにもトライしようと思います。

2014年1月14日 (火)

HerokuでスケーラブルWebSocket

前回の続きです。
前回Play+Redisでスケーラブルに動くWebSocketアプリケーションが完成したので今回はそれを実際にHeroku上で動かしてみます。

といってもCLIで「heroku labs:enable websockets」を叩く以外は特に変わったことをする必要はありません。
この辺は以前にも書いたのでそちらも参照してください。

http://blog.flect.co.jp/labo/2013/12/herokuwebsocket-aad6.html

実際のところはデプロイすれば特に何の問題もなく動きます


★HerokuでWebSocketアプリを動かす場合の留意点

Herokuの特性と照らしてWebSocketアプリを動かす時に気をつけなければならない点がいくつかあります。
ざっと思いつくところは以下です。

  1. 通信が無い状態が30秒続くと接続が切れる
  2. マルチDyno対応
  3. デイリー再起動がある


1番目の30秒ルールへの対応はクライアントまたはサーバから定期的にPing的なメッセージを送信することで回避できます。

2番目のマルチDyno対応は通常のスケールアウトと何ら変わるところがないのでRedisを使うことでクリアです。

やっかいなのは最後、再起動対応です。
HerokuのDynoManagerはWebSocket接続がある場合でも容赦なく再起動をかけてくると思います。(試してませんが。。。)
これはHerokuの内部仕様なのでユーザ側では制御不能です。

以下、これに対してどういう対策が可能かを考えてみます。


★接続遮断時のクライアントの動作

Dynoの再起動をローカルでシュミレートする方法は単純に起動中のPlayframeworkをCTRL+Dで止めるだけです。

この時にクライアント側のJavaScriptがどのような動作になるかをまずは検証します。検証の方法は単純にWebSocketの onopen/onerror/oncloseイベントにconsole.logを仕込むだけです。

検証に使用したブラウザはChrome, Firefox, IE10です。
サーバ強制終了時に発生するイベントとその順序は以下です。

  • Chrome: onclose
  • Firefox: onclose
  • IE10: onerror, onclose


IEのみonerrorが発生していますが、これは多分IEの問題はまたPlayのWebSocket実装の問題です。強制終了に限らずサーバ側からWebSocketを切断(EnumeratorでInput.EOFを送信)した場合、常に発生するので。

ざっと検索した感じ(Playの話ではないですが)この辺りが関連トピックかなと思いますが詳しくは調査していません。

onerrorイベントは発生していますが、例外でスクリプトが停止する等の実害はないのでここでは気にせず話を進めることにします。

重要なのは強制終了による切断であってもクライアント側からは単にサーバからWebSocketを切断されただけに見えるという点です。(PlayはShutdownHookでクリーンアップを行っているのでそれも関係あるかもしれませんが)


★oncloseで再接続してみる

安易ですがoncloseイベントが発生した場合に再接続してみることにします。

ローカルのテストでは手動で再起動を行っている関係で、切断直後はまだサーバが起動していません(※1)。当然接続はエラーになりますが、その場合以下の順序でイベントが発生しました。

  • Chrome: onerror, onclose
  • Firefox: onerror, onclose
  • IE10: onerror, onclose


すべてのブラウザでonerrorとoncloseが発生しています。
onopenとoncloseって必ず対で発生するとは限らないんですね。。。イマイチこれが実装依存なのか正規の仕様なのかがW3Cの仕様見てもわからないんですが、とりあえずoncloseを拾って時間をおいて数回再接続を試みるような実装はできそうです。(※2)


※1 HerokuでマルチDynoで動かしている場合は再接続は別のDynoにつながるのでエラーなしで再接続できます。

※2 console.logは見てませんがiOS、Android(のChrome)でもリトライでの再接続ができているので、やはり接続失敗時にoncloseは発生しています。


★Window#onunloadでのoncloseイベントのクリア

ここまでの内容を実装して動かしてみるとローカル、Herokuともにいい感じに再接続ができています。(Herokuでの再接続のテストはCLIで「heroku ps:restart web.2」のようにDyno単位での再起動を行います。)

あともう一つ注意が必要なのはWebSocket#oncloseイベントはブラウザでページを閉じた場合などにも発生するのでその場合には再接続を行ってはいけないという点です。

ここではWindow#onunloadイベントでoncloseをクリアしていますが、ページ遷移以外のWebSocket切断があるアプリでは他にも制御が必要になるでしょう。

最終的なクライアント側のコードはこうなりました。(タグをつけようか迷いましたがmasterの最新です。)

https://github.com/shunjikonishi/websocketchat-redis/blob/master/app/views/chatRoom.scala.html

★まとめ

Dyno再起動に対応した再接続はがんばればできそうです。
しかし、ここをがんばる位なら素直にHeroku以外のプラットフォームを選んだ方が良いように思います。(^^;;;

しかししかし、EC2を使うとしてその場合にサーバの異常終了による予期しない切断の可能性を考慮しなくて良いかどうかは疑問です。
個人的な感覚としてはWebアプリはエラーが発生したとしてもリロードで回復するなら許せるんですが、かなり高い確率でそれもなんとかせぇと言われそうな気がしますね。。。(--

□□□□


まぁ必要に迫られたら考えますが、ふとこんな言い回しを思いつきました。

パフォーマンスもアベイラビリティも金で買ってください。
パフォーマンスは性能正比例で性能2倍になれば価格も2倍ですが、アベイラビリティは1%向上する毎に価格は10倍です!


昔友人が言っていた言葉の焼き直しですが、あながち間違ってないと思いますね。(^^;

2014年1月10日 (金)

PlayとRedisでスケーラブルWebSocket(実装編)

前回の続きです。

実際にPlayとRedisでWebSocketアプリケーションを作成したコードサンプルを示します。


★題材

今回の調査の途中でドンピシャのサンプルを見つけています。

http://www.ryantanner.org/2013/03/using-play-iteratees-and-enumerators.html

PlayにバンドルされているサンプルのチャットをRedis対応したサンプルアプリです。
接続情報を環境変数から取ってるふりして実はlocalhost固定だったとか、チャットルームのメンバリストをローカル管理してるから厳密にはスケーラブルになってないとか、手を入れ始めるといろいろ気にはなったんですが(^^;、WebSocket/Redisのコードサンプルとしては非常に有益で必要十分な内容でした。
記してここに感謝します。


今回の作業のゴールはこれを改良して汎用的に使えるWebSocket/Redisのベースクラスを作成することです。

https://github.com/shunjikonishi/websocketchat-redis

ただし現時点ではそれを切り出して単体のjar/pluginにすることは考えていません。現実にはWebSocket/Redisを使って何かを作る予定は今のところないしPlay、Redis共に開発サイクルが早いので、実際に必要になった時にはAPIが変わっている可能性も高いからです。

ここでは実際に必要になった時のプロトタイプとなり、その考え方や注意点を示せれば十分と思っています。


そんなわけでRedisService.scalaとChatRoom.scalaはかなり丁寧に書きましたが、それ以外(JavaScriptとか)は割と適当です。(^^;

JavaScript版のWebSocketラッパーもそのうち作りたいとは思うんですけどね。。。


★PlayでのWebSocketアプリの作り方

http://www.playframework.com/documentation/2.2.x/ScalaWebSockets

Controllerでリクエストハンドラを作る時にActionの代わりにWebSocketを使って受信データをハンドルするIterateeと送信を実行するEnumeratorを返すようにすればOKです。

以上、終わり(^^;

。。。なんですがちょっとだけIterateeとEnumeratorについても触れておきます。
これらを解説するサイトは日本語のものだけでも、かなり多数ありますが抽象度の高い概念なのでなかなか理解するのは難しいです。というか抽象度の高い議論は途中からついていけません。。。(^^;;;

そうした概念的な話はさておき、WebSocketの文脈に限定して話をするなら、

- Iteratee
クライアントからデータを受信した時と接続が切れた時に何をするかを定義するもの

- Enumerator
クライアントに発信するデータを供給するもの


とだけ理解しておけば十分です。


これ、よく考えるとアプリで必要な機能以外は何ひとつ作らなくて良いインターフェースになってるんですよね。

今までPlay2を触ってきた中でこのWebSocketのAPIが一番いけてると思いました。(^^;


★RedisService

https://github.com/shunjikonishi/websocketchat-redis/blob/master/app/models/RedisService.scala

プロトなので関連するクラスを全部1ファイルにまとめています。

RedisServiceクラスの機能は

- コネクションプール
- WebSocketで使用するPubSubChannelの作成

の二つだけです。

- コネクションプール
基本的にRedisを使う場合はコネクションプールを併用した方が良いです。RedisClientは基本的にソケット繋ぎっぱなので、接続のコストをカットできます。

ただし、RedisClientをSubscriberとして使用する場合は注意が必要です。Subscribe時のRedisClientは別スレッドでSocketの入力を監視しているのでプールに戻すタイミングが難しくなります。

今回はやや強引な方法でSubscriberもプールに戻してますが、シビアにスレッドセーフに気を使う必要があるので、素直にSubscriberとして使用する場合は新しいインスタンスを作成して使い捨てた方が良いと思います。(使い捨てる場合もunsubscribe時にdisconnectすることを忘れてはいけません。)

このプロトでは試行錯誤の過程を残す意味でプールに戻すコードを残していますが、もしこれを本当にライブラリ化するのであればSubscriberでは常に新しいインスタンスを使用するようにします。(そうするとborrowClient/returnClientメソッドが不要になるのでAPI的にもすっきりします。)


- PubSubChannel
WebSocketにひもづけるIteratee/Enumeratorをラップするクラスです。
コンストラクタの必須引数は購読するチャネル名のみです。
インスタンスを作成してメンバ変数のinとoutをWebSocketに返せばそれだけで単純なEchoアプリが作れます

オプションとして

  • send: クライアントから受信した文字列をRedisに送信する前に加工する関数
  • receive: Redisから受信した文字列をクライアントに送信する前に加工する関数
  • disconnect: クライアント切断時にRedisに送信する文字列を返す関数


などを指定できます。
exception/subscribe/unsubscribeなどのSubscriber関連のイベントハンドリング関数も一応指定できるようになってますが多分使うことはないです。

実装に関して言うと

  • Publisher: Actorになっていて都度プールから取ってきたクライアントで送信
  • Subscriber: ひとつのクライアントでredis-scalaの内部スレッドにおまかせ


となっています。
WebSocketではinとoutは一対ですが、RedisのPub/SubはWebSocketだけで使うものとは限らないので本来的にはは多対多であり、Publish自体はどこから行っても構わない訳です。

こう考えるとPublisherのActorはシングルトンでも構わない気がしますが、Publishのコストは購読者数に比例する(らしい)のでチャネル毎にわけています。

あとこのクラスではsubscribeするチャネルを一つに限定しています。Redisの機能を考えるとマルチチャネル購読を使いたいケースというのは十分に考えられるわけですが、そうすると

  • -> in/outにチャネル名も入れる必要がでてくる
  • -> 入出力の型をStringからTuple(String, String)に変更
  • -> in/outを直接WebSocketに接続できるというメリットが失われる


ので止めました。
まぁマルチチャネル対応版をサブクラスなりTraitに切りだすなりして対応するのはそんなに難しくないので必要に迫られた時に考えたいと思います。


★ChatRoom

https://github.com/shunjikonishi/websocketchat-redis/blob/master/app/models/ChatRoom.scala

ChatRoomに関して具体的なアプリの内容に関する説明はここではしません。
ここではWebSocket/Redisアプリを作る際のポイントとなる部分についてのみ説明します。


- クラスで状態管理をしてはならない
元々のサンプルではチャットルームのメンバ一覧をクラスのvar変数で管理していますが、複数サーバある場合に状態を共有できないのでアウトです。
状態管理の変数は常にRedis上に置く必要があります。

RedisにはListやSetを扱う機能があるのでとても重宝します。(^^;

- WebSocketとPubSubChannelは1対1とは限らない
WebSocket/Redisアプリを作る一番簡単な方法は先に作成したPubSubChannel(のin/out)を直接WebSocketにひもづけることです。

状態管理をすべてクライアント側で行うアプリであれば、サーバ側には一切ロジックを記述する必要がありません。(このサンプルはそうなっていませんが、チャットはそういうやり方でも作成できます。)

ですが、それだと接続毎にRedisのコネクションを消費することになるので無駄が多いわけです。

チャットの場合同じ部屋にいるユーザが購読するRedisチャネルは皆同じなのでホストで一つチャネルを開いてそれを共有できれば大幅にコネクションを節約できます。

実際、PubSubChannelのoutは複数のWebSocketで共有することができます

ですがinはダメです。何故ならこのIterateeではクライアント切断時にチャネルをクローズしているからです。つまり1人のユーザが退出したら全ユーザのWebSocketが切れます

しかし、前述の通りPublishはどこから行っても構わないのでPubSubChannelのinをそのまま使う必要はなく自前のIterateeを使うことができます

この時問題となるのはRedisの接続管理です。

- いつunsubscribeするか?
今でしょ!なわけないですよ。(^^;

論理的にはWebSocket接続数を数えておいて、0になったらunsubscribeすれば良いのですが、どこでcountUp/Downするかも悩ましかったりするわけです。考慮漏れがあると即リソースリークになるので。。。(--

で。。。結論なんですがこれは変に考えすぎず、クライアントからの切断にのみ反応するようにするべきと思います。具体的にはIteratee#Doneの中。

WebSocketの切断が発生するのは多くの場合ブラウザの画面遷移やクライアントの電源断(タブレット等ののスリープ含む)です。

これらはいつ発生するかはわからないので変に状態管理するよりも確実にそれだけを捕まえるのが良いと思います。

直観ですが、WebSocketアプリではサーバーサイドからの切断もあんまりやらない方が良い気がします。

- どこで接続管理を行うか?
これは絶対にActorを建ててそこでのみ行う必要があります。
IterateeはWebリクエストのワーカースレッド内で動くので、スレッドセーフにするためにはcountUp/Downを伴う操作は同じActorの中からしか実行してはいけません。

なのでIteratee#Done内で実行するDisconnet操作もActorにDisconnectメッセージを投げるだけとなっています。

□□□□
ここまでたどり着くのに相当試行錯誤しており、AkkaのActorにも慣れてないのでイマイチ実装に確信を持ててないんですが、概ね抑えるべきところは抑えたつもりです。

何かおかしなところがあれば是非ご連絡を!



★Playの残念なお知らせ

さて最後に今回気がついたPlayframeworkの衝撃の事実を。。。。(--

Playで修正/リコンパイルを繰り返している時には、修正前に使用されていたインスタンスは回収されません。。。マジでかーーーー!!


前回書いた通り、Redisの最大接続数=10の状態で作業しているとほんの数回修正を行っただけで接続エラー。。。。

その度にPlayの再起動を繰り返すのはなかなか苦痛でした。(最初はPlayのせいとは思わず自分かredis-scalaのリソースリークを疑っていたのでなおさら。。。(--)

Global#onStopでcloseすれば大丈夫かと思ったんですが、ここのロジックが実行されるのはリコンパイルが行われた後っぽい。。。意味ねーーーー!!!

シングルトン(object)とか、どうなってるのかと思うけどこれも単なるクラスのインスタンスなのでポインタ変わって新しいインスタンスが作られるだけなのかなと思ったり。

唯一有効だったのはActorのpostStopにcloseを仕込んで置くことだけど、これも常に有効かどうかは疑わしい。。。(上の推測通りシングルトンのアドレスが変わるのであればアウトだと思う。)

修正前のインスタンスが残るのは仕方ない気がするけど、せめてonStopはリコンパイル前に走って欲しい。。。(--

□□□□

以上、こんだけ書いておけば将来ホントにライブラリ化する日が来たとしても、ちょっとこれ作った奴出てこい!とか思わずに済むでしょう。。。多分。。。(^^;


次回、「HerokuでスケーラブルWebSocket」(かもしれない)

2014年1月 9日 (木)

PlayとRedisでスケーラブルWebSocket

本日2本目。

年末から突発的に1人でWebSocketブームです。(^^v
概要レベルの知識はあったんですが、実際に触ってみると思った以上に使える技術で応用範囲も広そうな気がします。
これは楽しい!


さて、スケールアウトするWebSocketアプリを作ろうと思ったらどうやらRedisが必修らしいので、Redisにも手を出してみることにしました。

この記事はそのまとめです。

 

★Redisとは

Memcacheの亜種。というのがこれまでの理解だったんですが、まぁだいたいあってます。基本的にはいわゆるKey-Valueストアです。

ですが実際にはMemcacheよりも遥かに高機能です。
ちゃんとドキュメントを読んだわけではないですが、ざっくりとMemcacheよりも優れている点を上げてみると

  • List, Set, Mapなどの集合を値として扱える
  • キーの一覧が取得できるなど管理系のコマンドがある
  • Pub/Sub機能がある
  • 永続化できる


などがあると思います。
今回、WebSocketで使うのは主にPub/Sub機能ですがListなどの集合の値をアトミックな操作で追加/削除できる機能はスケーラブルなアプリを作る上ではいかにも欲しそうな機能です。

通信プロトコルは多分独自で、ざっとクライアントのソースを眺めた感じではとてもシンプルそうです。可能な限り素のソケット通信に近いプロトコルを使用することで速度を稼いでいるんでしょうね。


★Pub/Subとは

Publisher/Subscriberの略です。日本語だと発信/購読という単語がしっくりきますかね。
任意のキー(チャネルという)を指定して購読を登録しておくと、そのチャネルに対して発信されたメッセージがすべての購読者にプッシュで配信されます。

ひとつのSubscriberで複数のチャネル(またはパターン)を登録することが可能(「hoge」、「fuga」、「room1.*」みたいな感じで指定できる)で、配信されるメッセージにはマッチしたチャネル名も含まれます。
なので、購読者はチャネルで分岐して処理を振り分けることができるわけです。まさにWebSocketのためにあるような機能ですね。(^^v


さて、プッシュをどうやって実現しているかというとその方法は極めて単純で単にソケットを繋ぎっぱにしてるだけです。
なので購読を行っているクライアント(ソケット)はメッセージがこない限りブロックします。(ここではNon Blocking I/Oは考慮せず単純なソケット通信を前提に話しています。)
まぁ、この辺たいていはライブラリがうまいことやってくれるので利用者はあまり気にする必要はないんですが、この事実からPub/Subを使う時の重要な注意点が導き出せます。
それは以下のような点です。

  • 購読中のクライアントは購読登録/解除のメソッド以外を実行してはならない
  • もちろん発信もアウト
  • 購読解除を行わない限りそのコネクションは占有され続ける


などなど。
初めて実装を行う際にはなんとなく同じクライアントを使いまわして購読と発信をやってしまいそうになりますが普通にアウトです。

またサーバーサイドでコネクション数の上限が決まっているので購読解除のし忘れはそのままリソースリークです。(もっとも購読に限らずRedisのクライアントはソケット繋ぎっぱなしにしていることが多いと思います。なので、利用の際にはJDBCのConnectionと同じようにこまめにcloseするかコネクションプールを使用する必要があります。)


★Java/ScalaのRedisクライアント事情

調査を始めた当初はJedisとかSedisとかいう単語がいきなり目に飛び込んできてかなり混乱しました。GitHub時代の弊害で雑多なライブラリが乱立しておりどれが良いんだかさっぱりわからないんですが、以下にいくつか調べたものをあげておきます。

- Jedis
https://github.com/xetorthio/jedis

JavaのRedisクライアント実装です。
多分これがデファクトスタンダード。

JavaのRedisクライアントだからJedisですか。そうですか。。。(--

- Sedis
https://github.com/pk11/sedis

JedisのScalaラッパー。
といっても全部のAPIがラップされているわけではないので、ないものを使おうと思ったら結局JedisのAPIを直接たたくことになる。(Pub/Subとかはない)

ScalaのRedisクライアントだからSedisですか。そうですか。。。(--
なんとなく名前に騙されそうになりますが、多分これを使うくらいなら最初からJedisを直で使う方が良いです。

- scala-redis
https://github.com/debasishg/scala-redis

最初からScalaで実装されたクライアント。
多分これがScalaのデファクト。

元々は他の人が始めたプロジェクトをこの人がフォークしてガンガン作りこんでいつの間にかこっちが本流になったらしい。。。

う~ん、GitHub時代。。。(--
なんか本家と元祖が並んでるたこ焼き屋みたいで何を信じれば良いのか疑心暗鬼にかられますけど、Redisの公式ページのクライアント一覧でもこれに星がついているしMavenセントラルにも登録されているので多分これが本家(?)です。

どうでもいいけどこのクライアント一覧。

 

お前のクライアントをこのリストに載せて欲しいのか?だったらredis-doc repositoryをフォークしてプルリクだしな!

 

とか書いてあるよ。。。
凄いな、GitHub時代。(^^;

- scala-reids-nb
https://github.com/debasishg/scala-redis-nb

Akkaを使用したNonBlocking I/OのRedisクライアント。
作者は上のscala-redisと同じ人。

RedisはNonBlocking I/Oと相性良さそうだけど、残念ながらPub/SubのAPIはない。
多分まだ枯れてないので動向はチェックしつつも様子見な感じ?

- play-plugins/redis
https://github.com/typesafehub/play-plugins/tree/master/redis

Play2系のRedisPlugin。CacheインターフェースのBackendをRedisに置き換えてくれる。
内部ではSedisを使用している


- 結局どれを使えば良いの?
素直にJavaならJedis。Scalaならscala-redisが良いと思います。

Playから使おうと思ったら多分最初に見つかるのはplay-plugins/redisなのでそれをそのまま使いたくなるけど、個人的にはscala-redisの方がお勧めです。(Jedisを直に使うならそれでも良いと思う。つまりSedisの評価が低い。)

あと、scala-redisはソースがとても読みやすいところも良いです。
実のところRedisのドキュメントはほとんど見てないんだけど、これのソース見たおかげでRedisの使い方がなんとなくわかったというのもあります。

読みやすいのはあんまりScalaっぽくないコードのせいかもしれないですけどね。(^^;
個人的には自分型アノテーションの使い方とかこれ見て初めてちゃんとわかった気がするのでそれも良かったです。
いきなりPlay本体のソースとか見ちゃうと多分Scala嫌いになると思うので、最初はこれくらいのライトなライブラリから眺めるのも良いんじゃないですかね。(^^;;;


PlayのCacheインターフェースを使いたいなら、scala-redis版はないんですけどこれは自分で作っても良いと思います。
多分、

  •  Sedis版のソースをコピって、
  •  import置き換えて、
  •  コンパイルエラーがでなくなるまで直す!


とするだけで動くと思う。(^^;
もっとも、無理してCacheに合わせなくても直でRedisを使えば良いと思いますけどね。


★HerokuのRedis事情

ついでにHerokuのRedisアドオンについて。
自分でRedisサーバたてても良いんだけどそれすらもめんどくさいので、localhostから使うテストサーバもHerokuAddon(無料版)で済ませています。(^^;;;

HerokuのAddonページで「redis」を検索するとなんと5つものAddonが引っ掛かります。(2014年1月現在)
うち、4つがCloudでのRedisサーバ提供者。。。
どこに差別化ポイントがあるのか全くわかりませんが、多分見るべきポイントはそれほど多くはないです。

今回の場合

  •  価格
  •  コネクション数(最大接続数)
  •  メモリサイズ


の3つだけを見て、無料で一番メモリサイズが大きかったRedisCloud-25MBを選択しました。
各社Webコンソールの使いやすさとかが違うんだろうと予想しますけど、コマンドライン(CLI)でたいていのことはできるので多分そんなに重要ではないです。(Webコンソールを見たのはRedisCloudだけだけど、まぁ十分です。)

最重要なのは実はメモリサイズよりもコネクション数です。前述の通りPub/Subは購読者の数だけコネクションを消費するので、これが少ないとすぐに接続エラーになってしまいます。
無料で使えるAddonはいくつかありますが、どれもコネクション数は10なので実運用には耐えない。。。というか1人でテストしてるだけでも割と簡単に上限に達します。

まぁ、これがきっかけでリソースリークを気にするようになったので結果オーライではあるんですけどね。(^^;

これが許容できないなら自前でRedisサーバを動かすのも良いでしょう。
CLIを使うために結局のところRedisのインストールは必要ですし、インストールさえすれば設定なしでいきなり起動させることができます。

逆にHerokuアドオン(Paasサービス)を使うことのメリットはWebコンソールがあるということにつきます。
先にWebコンソールは重要ではないと書きましたが、それは80点と85点の差には意味が無いということであって、まったく無いのとあるのとでは大違いなわけです。

今回の場合特にコネクション数がリアルタイムにわかったのが非常にありがたかったです。

□□□□
PlayでのRedis Pub/Subの実装についても書くつもりですけど、全然そこまでたどりつかないですね。。。(^^;
続きはまた次回

JSON Schemaというのがあるらしい

あけましておめでとうございます。(遅い!)

新年一発目のブログ。昨日途中まで書いたかなり渾身のネタがあるんだけど先に今朝飛び込んできたHeroku関連のニュースを。

なんかHerokuのPlatform APIのスキーマ定義がJSONフォーマットで公開されたらしい。

https://blog.heroku.com/archives/2014/1/8/json_schema_for_heroku_platform_api

実際にどういう定義になっているかは上記ブログ内にあるcurlコマンドを叩けば確認できます。(こんなの認証不要のテキストコンテンツとして普通にhttpで公開しちゃえば良いと思うんだけど)

ていうか「JSON Schema」っていうJSONのスキーマ定義言語があるんですね。
どのくらい流行っているのかは謎ですけど。。。

こういうのって必要だとは思うけど元々XML界隈にいた身としてはなんかXML SchemaとRelaxの不毛な戦争を思いだします。(^^;

JSON Schemaについてなんの予備知識なしで定義インスタンスを見てもなんとなく意味がわかるので、仕様自体はとてもシンプルなんだと思います。少なくともXML Schemaよりは100倍読みやすいです。

ただ、何か違和感があります。。。。

。。。。

ちょっと考えて、スキーマを定義するためのキーと定義されるスキーマのキーがひとつのJSONの中に混在していて見分けがつかないせいだと気がつきました。

definitions以下の定義が、

 

"definitions": {
  "created_at": {...},
  "description": {...},
  ...
}



のようになっていて、「type」とか「description」とかの一般的なキー、かつJSON Schema自体も使用しているものが現れた場合に混乱するんですね。

構造化を優先するなら

 

"definitions": [
  { "name" : "created_at", ...},
  { "name" : "description", ...},
  ...
]



の方が良いんじゃないかなぁという気もしますが、その思考を進めていくとだんだんXML Schemaのようになっていく気もするのであんまり深入りしない方が良いでしょう。(^^;
(JSONがXMLに取って代わった理由の一つに「名前空間が無い」というのがあると思っています。)

このスキーマをベースに各種言語のモデルを自動生成することはできると思いますが、それをやる場合僕ならやっぱりXSLTが使いたいですね。

JSONに対してXSLTを適用する方法って何かあるんでしたっけ???


2013年12月27日 (金)

PapertrailのWebAPIを使う

前回記事ではHerokuのPlatformAPIを使用してリアルタイムにログを解析してグラフを作ってみたわけですが、やっぱり直近の1500行しかログが取れないとあんまりいい感じのグラフにならないわけです。(--

で、PapertrailにもWebAPIがあったことを思い出してそっちを使ってみることにしました。

 

http://help.papertrailapp.com/kb/how-it-works/http-api

 

ちなみに検索だけだけどラッパーAPIもあります。

 

http://oss.flect.co.jp/libs/ja/papertrailTool.html

 

このラッパー最初に作ったのは多分1年以上前だと思うけど、今見ると色々WebAPI進化してますね。。。

昔は検索以外のAPIなかった気がするけどいつの間にか検索条件の保存とか管理系のAPIもあるっぽい。

まぁ使うことはなさそうだけど、こうしたPaasサービスの提供する機能がなんでもAPIで叩けるようになっているのは良い傾向です。夢が広がります。(^^v

 

★APIでtail的なことを行う

これ非常に簡単です。

まず、検索APIをパラメータなしで実行するとJSONで以下のレスポンスが返ってきます。

  • 含まれるログのIDの最小値
  • 含まれるログのIDの最大値
  • 直近のログ100件(生成日時、プログラム、メッセージなどが構造化されています。)

これに対して指定できるパラメータには以下のものがあります。

  • min_id - 指定された場合このID以降のログを取得する
  • max_id - 指定された場合このIDまでのログを取得する
  • min_time - 指定された場合この日時以降のログを取得する
  • max_time - 指定された場合この日時までのログを取得する
  • q - 任意のクエリ(WebUIと同じ書式)

※min_time, max_timeはUNIX TIMEで指定

 

min/maxの指定でどこから、どこまでを取得するかが指定できるわけですね。ちなみに取得件数は常に最大100件で指定できないようです。

なので、最初にmin_timeを指定していつからのログを取得するかを決めたら、あとはそのレスポンスのmax_idを、次のリクエストのmin_idに指定してループをまわせば連続したログが取得できます。

ここでポイントとなることが2点あります。

 

min_idに指定したIDのログは結果に含まれない

min_idを指定すると指定したIDのログを先頭にしてそれ以降のログが取得されそうな気がしますが、実際には含まれません。

なので、max_idを次のリクエストのmin_idに設定する際には+1するとかの小細工は必要ありません。(PapertrailのログIDは連番ではないので+1しても抜けることはおそらくありませんが。)

 

結果が0件の場合でもレスポンスのmax_idは設定される

直近のログまで読んでしまったら、次のログがまだ存在しないということがありえますが、その場合でもレスポンスには最終行のログIDがmax_idに設定されます。

なので「結果が0件の場合に備えて前回のmax_idを取っておいて。。。」みたいなことをする必要はありません。

 

細かいことですが、この2つのおかげでtailライクなログ取得を組む際のロジックがシンプルになります。

 

★ログ解析アプリケーション

そんなこんなで、ログ解析アプリはPapertrailのAPITokenを入力すれば誰でも使えるようになりました。(^^v

 

http://flect-papertrail.herokuapp.com/

 

Herokuアカウントで使う場合は要求される権限の範囲が広すぎるため、試しに使ってみるのも抵抗がある気がします。開発環境で試してみたいけど本番環境まで一緒に見えちゃうよ!みたいな。

が、PapertrailのAPITokenであれば開発環境で試してみるということもできるので、気が向いた人は使ってみてくださいな。

 

□□□□

どうでもいいけどこのアプリ、最初に作ったのが今年の4月頃。そこからずっと放置してたのがこの数日でグラフ機能を追加。この先もまたしばらく放置の予定です。

これをトラブルドリブン開発(TDD)とでも名づけましょうか。(^^;

それでは良いお年を(^^)/

2013年12月24日 (火)

HerokuとWebSocket

WebSocketはいけてる!(^^v

というのが、初めて実際にWebSocketアプリを実装してみての感想です。

前回の予告通り今回はHerokuでWebSocketの話です。
ちなみにアプリ自体は前回のモノと同じです。

http://flect-papertrail.herokuapp.com/

OAuthでHerokuにログインして「heroku logs -t」相当のAPIでログを取得しながら、log-runtime-metricsの数字を拾ってリアルタイムにグラフ表示します。
拾う数字は<KEY>=<数字>という形式であればなんでも良いのでPostgreSQLの出力情報や自アプリで出したログもグラフ化することが可能です。

WebSocketのサンプル題材としてはそこそこ面白いと思うんですが。。。


残念なことにHerokuのログは直近1500行しか取れないので、それなりにアクセスのあるアプリだとグラフにプロットすべきポイントが数個しか取れないという落ちがあります。(^^;

お暇な人はしばらく放置してグラフを眺めてみてください。別に見なくてもここから先の本題を読むのにまったく支障はありませんが。(^^;

 


★ WebSocketアプリケーションの作成

といっても、ここでは作り方の解説はしません。。。(^^;;;
普通にNode.jsとかPlayとかでWebSocketアプリケーションを作成すれば、それがそのままHeroku上でも動きます。

唯一Herokuアプリ特有の処理が必要なのは、HerokuのWebSocketには30秒でのタイムアウトがあるので、それを回避するために定期的に通信を行わなければならないという点です。

これを実現する一番簡単な方法はクライアント側JavaScriptのsetIntervalで定期的にダミーリクエストを送信することだと思います。
 
Play2(Scala)でのWebSocketアプリケーションの作り方は気が向いたら、また別途まとめます。(^^;;;

 


★HerokuのWebSocket

現在HerokuのWebSocket機能はβ版であり、使用するためにはアプリ毎にコマンド

 

heroku labs:enable websockets -a myapp

 

を実行する必要があります。
このコマンドを実行することでWebDynoのフロントにあるLoadBalancerがWebSocket対応版に切り替わります
Dynoの機能として何かが追加されるわけではなく、入口の方が変更されるわけです。

このアーキテクチャのため、コマンドでWebSocketを有効にした後に実際にWebSocketが使用できるようになるまでに若干の時間(DNSの切り替え)を要することがあります。(とドキュメントに書いてあります。)

またAddonとしてSSL Endpointを使用している場合はWebSocketを有効にできません。これはSSL Endpointはほとんどの場合独自ドメインと併用されており、ユーザーの管理するDNSでHerokuのSSL Endpointを指すように設定されているので、そのエンドポイントを勝手に切り替えるわけにはいかないためと思われます。

この場合、一度SSLEndpointを解除して、WebSocketを有効にしてから再度SSL Endpointを追加します。もちろんDNSの再設定も必要なはずです。(試してないですけど多分。。。)


★マルチDynoとWebSocket

WebDynoを複数起動した状態でWebSocket接続を確立するとそこでの送受信は常に同じDynoに対して行われます
逆に言うと他のDynoからはその接続は見えません。

なのでWebSocketのサンプルとしてよくあるチャットルームみたいなものを作ろうとした場合、各ユーザーが同じDynoに接続されるとは限らないので困ってしまいます。。。(--

もっとも、これはHerokuに限った話ではなくスケールアウトするWebSocketアプリには常につきまとう課題です。ざっと検索したところ、この場合Redisのpub/sub機能を使用するのがセオリーのようです。


★Dyno再起動とWebSocket

普通に接続が切断されるだけでしょうね。。。
少なくともコマンドで「heroku restart」を叩いたところでは問答無用で切断されました。

これはよくよく考えると結構悩ましい問題です。Dynoにはデイリー再起動があるので、だいたい1日に1回再起動があるわけですが、このタイミングに存在した接続を救済する方法は多分ありません

対処方法としてはクライアント側でサーバからの切断を検知したら再接続するとか?
けっこうめんどくさそう。。。(--

だけど、この辺どうなんでしょうね?これもHerokuに限らず他の環境でもWebSocketサーバからでも不当に切断されることはありそうな気がするので、まっとうなWebSocketアプリケーションはサーバーサイドからの切断に備えて必ずリトライを実装しておくべきなのかもしれません。

経験者のお話を伺いたいですな。


★モバイルとWebSocket

つねづねかねがね思ってたんですが、劣悪な通信環境や端末がスリープした場合にはWebSocket接続はどうなるの?という疑問があったので何パターンか試してみました。
テストに使用した端末はELUGA P-02E(Docomo, Android 4.1.2, 3G/LTE)とiPad2(iOS7, WiFi)です。
Androidは標準ブラウザがWebSocketに対応していないのでChromeを使用しています。(Can I UseによるとAndroid標準ブラウザも4.4からはWebSocketに対応しているらしいです)


- Android スリープ
スリープしても通信は継続されます。
数分放置しても復帰時に接続は継続されておりおそらくメッセージの欠落もありません。(てことは電池の消費も早くなる気も。。。)

ただ、何回かテストした中で一度だけ接続切れました。

- Android 劣悪通信環境
ほとんどネット接続ができなくなる通信砂漠を経過しても通信は継続されます。
おそらくメッセージの欠落もありません。
どこかでバッファリングされているってことですかね。

どうでもいいですがここで言う通信砂漠は東急日吉駅近辺のことです。

- Android 再起動
切断され、再度ブラウザを開いた時にページ自体がリロードされます。

- iOS スリープ
切断されます。

- iOS WiFi電源断
切断されます。

- iOS 再起動
切断され、再度ブラウザを開いた時にページ自体がリロードされます。

□□□□
だいたいこんな感じです。もっと接続切れるかと思ってましたが、Androidは予想以上に頑張ってました。(^^;

網羅的にテストする気はまったく無いんですが、この結果だけからでもだいたいの傾向は見えてきます。

まぁ、切れる時には切れる(雑)ってことですね。(^^;
そしてそうであれば、やはりクライアント側からのリトライは必要な気がします。

2013年12月20日 (金)

HerokuとJavaのメモリのお話

Heroku上のJavaアプリでNewRelicで見る限りメモリはまだ余裕がありそうなのにR14(Memory quota exceeded)がログに出力されるようになり、アプリの応答が極端に悪くなる(というより1台のDynoがほぼ無応答になる)という事象が発生したので、原因を調査しました。

結論から言うとエラーの原因は特定できなかった(いや、まぁメモリ不足であることはわかっている(^^;)んですが、なかなか興味深いこともわかったのでここにまとめておきます。

 


★ JavaVMのメモリ管理

Javaで使用中のメモリ量を調べる方法としてはRuntimeのメソッドを使用する方法とMemoryMXBeanを使用する方法があります。

http://docs.oracle.com/javase/jp/7/api/java/lang/Runtime.html
http://docs.oracle.com/javase/jp/7/api/java/lang/management/MemoryMXBean.html

前者はHeapメモリのみを対象とし、後者はHeapとNonHeapの両方を対象としています。
なので、Runtimeからとれる値はMemoryMXBeanからも取得できるわけですが、その関係は以下のようになっています。

  • Runtime#maxMemory == HeapMemorUsage#getMax
  • Runtime#totalMemory == HeapMemoryUsage#getCommitted
  • Runtime#freeMemory == HeapMemoryUsage#getCommitted - HeapMemoryUsage#getUsed


ログ等にメモリ使用量を出力する場合、お手軽なのでRuntimeのメソッドの方が使用されることが多いと思いますが、この方法だとNonHeapの容量が含まれていないことに注意する必要があります。

ちなみに今回テストで使用したアプリ(Play1)ではNonHeapは80MB - 100MB位確保されていました。意外と多いです。

 


★ NewRelicのInstancesページに表示されるメモリ使用量


NewRelic自体もMXBeanを使用して情報収集しているはずなので、ここでの各グラフはMXBeanで取得できる値と対応関係があるはずです。

その対応は多分以下です。

  • Physical = HeapMemoryUsage#getCommitted + NonHeapMemoryUsage#getCommitted
  • CommittedHeap = HeapMemoryUsage#getCommitted
  • UsedHeap = HeapMemoryUsage#getUsed

多分というのは、今回アプリの中からMXBeanの値を定期的にログ出力するようにしてその値をグラフ化するということをやったんですが、そのグラフと自前のグラフでいくらか傾向が異なるからです。

ちなみに自前のグラフはこんな感じ。(1分間隔で出力)

 

Memory

 

見ての通り、UsedHeapがかなりギザギザしたグラフとなっていますが、これは定期的にgcがかかっているためと推測されます。

しかし、NewRelicのグラフではこのようなギザギザは観測されません。
複数Dynoの平均値だからかとも思ったんですが、1Dynoの場合でもこのようなグラフとはならないので謎です。。。(--

ただ全体的には同じようなグラフとなるので、ベースとしている値は上記であっていると思います。。。

が、先にも書いた通り複数Dynoを使用している場合はNewRelicのグラフは平均値になるのでなので各Dynoがどれだけメモリを使用しているかはNewRelicではわかりません

 

あと今回の調査とは直接関係ありませんが、NewRelicのagentのjarファイルはちょいちょいアップデートされているので、たまには見直した方が良いかもしれません。

ほとんどの場合、最初にNewRelicを追加した時にDownloadしたAgentをそのまま使用していると思いますが、2系から3系へのバージョンアップではAgentのオーバーヘッドが減っている、とCHANGELOGに書いてあります。(まぁ実感できるものではありませんが。。。この辺Addonが自動的にやってくれると嬉しんだけれど。)

 


★ heroku labs:enable log-runtime-metrics

labsの機能、log-runtime-metricsを有効にすると20秒に一回各Dynoのメモリ使用量がログに記録されます

 

https://devcenter.heroku.com/articles/log-runtime-metrics

 

ここで出力される「memory_total」がHerokuが監視しているメモリ使用量で、この値が512MBを越えるとR14が出力されます。

ちなみにR14が出力される間隔も約20秒おきなので、このスイッチで切り替わるのはログ出力の有無だけでメモリ監視自体は常に実行されているのでしょう。

ちなみに記録対象はすべてのDynoなので、heroku run bashとかSchedulerで動いたDynoのメモリ使用量も記録されます。

各Dynoのメモリ使用量が個別にわかるし、オーバーヘッドもほとんどないと思うのでlabsだからと敬遠せずに全部のアプリで有効にした方が良いと思います。

 


★ Javaのメモリ使用量とDynoのメモリの関係

さて、今回の調査を始めた元々の動機は「NewRelicで見るメモリグラフではそれほどメモリを使用しているようには見えないのにR14警告が出ることがある(気がする)」というものでした。

先のグラフではわざと載せませんでしたが、log-runtime-metricsの値ももちろんログから拾えるのでグラフ化できます。これらを重ねればJavaのメモリ使用量とDynoのメモリの関係がわかるはずです。

その衝撃の計測結果はテストアプリのでのグラフを公開しているので是非直接ご覧ください。(12月16日以降はグラフが見られます。)

 

https://flect-papertrail.herokuapp.com/app/fexp/metrics/2013-12-19?key=memory_rss,memory_total,HeapUsed,HeapCommitted,Physical,PsRss

 

なにが衝撃だったかって、Herokuの計測しているmemory_totalがJavaのHeapCommitted(+ NonHeapCommitted)よりも小さいことがあるということです。

そんなことってある???

僕の認識ではCommittedというのはJavaによって確保済みのメモリなので、それはプロセスが使用しているメモリとほぼ等しいはずと思っていたんですが、外から見たメモリがそれよりも小さいってどういうこと???

そんなはずはねーと思って、アプリ上にpsを叩いた結果を表示するコントローラを付けてみたところ。。。

。。。マジでかーーー。。。ここでもCommittedよりも小さい値が表示されてるよ。。。(--

誰かこの現象を説明してください。m(_ _)m

 

(12/24 追記)

その後ログに「ps aux」の結果から抜いたメモリ使用量を出力するようにしてグラフに追加したところ、ほぼmemory_rssと重なりました。なので外から見た場合のメモリ使用量はJavaのCommittedメモリよりも大きいことも小さいこともあり、その数字が実際のメモリ使用量(Herokuの監視対象)と考えるのが良いようです。

□□□□

話を戻すとグラフを見るとmemory_totalはJavaから見たメモリ使用量よりも小さい時も大きい時もあります。ただグラフの上がり方を見るとアプリの負荷と相関関係があるのは間違いないです。

アプリ以外にメモリを使っているものというのがイマイチ想像できないんですけど、通信バッファとかもあるんだろうか?psにでてこないプロセスも何かある気がする。(根拠なく言ってます。。。)

これはもうこういうものだと思って飲み込んだ方が良いのかもしれません。。。深入りしても満足のいく結果が得られそうな気がしない。。。(敗北宣言)

ログからDyno毎の正確なメモリ使用量を可視化するツールができただけでもまぁ良しとしますかね。。。(--

 

★なぜか次回予告

さて、今回作成したグラフツールはまぁまぁ便利です。(ドキュメントとかまったく書いてないけど)

仕組みとしてはPapertraiがS3にアーカイブしたログを引っ張ってきて解析しているので、前日以前のログは見られるわけだけどPapertrailのログがアーカイブされるのはだいたいお昼過ぎ(昔はもっと早かった気がするけど)なので、「今R14出てるよ!」みたいな時には使えない訳です。

そこはやっぱりリアルタイムで見たいのが人情!

。。。かどうかは知りませんが。(^^;

 

そういやPlatformAPIで直近のログを取得できたな。。。

 

そういやHerokuってWebSocketサポートしたんだよな。。。

 

じゃリアルタイムグラフも作れるんじゃね?(^^v

 

ということで、次回はHerokuとWebSocketの話です。

採用情報

株式会社フレクトでは、事業拡大のため、
Salesforce/Force.comのアプリケーション
開発
HerokuやAWSなどのクラウドプラッ
トフォーム上でのWebアプリケーション開発

エンジニア、マネージャーを募集中です。

未経験でも、これからクラウドをやってみた
い方、是非ご応募下さい。

フレクト採用ページへ

会社紹介

株式会社フレクトは、
認定コンサルタント
認定上級デベロッパー
認定デベロッパー
が在籍している、
セールスフォースパートナーです。
heroku partnersにも登録されています。
herokuパートナー
株式会社フレクトのSalesforce/Force.com
導入支援サービス
弊社の認定プロフェッショナルが支援致します。
・Visualforce/Apexによるアプリ開発
・Salesforceと連携するWebアプリ開発
も承っております。
セールスフォースご検討の際は、
お気軽にお問合せください。
Powered by Six Apart