Shiny-Serverをたった1行の変更でマルチプロセス化する方法
最近、R界隈でShinyが熱いですね。
Shinyって何かというと、Rだけで簡単にGUIの可視化・分析環境を作れるRのフレームワークです(Shiny)。
もともとRの持つ可視化・分析の手軽さはそのままに、GUIも手軽に作れるという特徴があり、Rの豊富なライブラリや手軽な分析環境を分析ツールの開発にも使えるようになりました。
Shinyというライブラリは基本的にはスタンドアローンで利用するツールなのですが、開発したツールのバージョンアップ・データの配布とかを考えたらWebアプリ化できるととても便利です。
実はそのようなプロダクトも用意されていて、それをShiny-Serverと言います。ざっくり、Rで動いているShinyの前にnode.jsで作ったサーバを立てて、そいつがリクエストをさばく感じです。
先日の Japan.R でも @wdkz さんがShiny-ServerについてLTしてたようです。
ただ、このスライドにもある通り複数ユーザが同時に利用すると1つのRのプロセスを共有するため非常に低速になるという重大な欠点があります。
(@wdkz さんスライドp. 10より)
んで、それの対策として考えられるのは以下の2つです(@wdkz さんスライドp. 11より)。
@wdkzさんのスライドでは1の方法について解説しています。
これによって同時に快適に利用できるユーザ数を増やすとともに、既存のShiny-Serverでは認証ができないという問題もある程度解決できていい感じです(分析ツールでは特定の人にしか見せられない情報を表示しないといけないので、組織でShiny-Serverを使う場合には認証ができることはとても重要だと思います)。
でも、この方法だと Rプロセスの数(= 同時に快適に利用できるユーザ数)の数だけShiny-Serverも立ち上げないといけないのでちょっと大変 & nonblockingなnode.jsのいいところを全く活かすことができ無くてJavaScript好きな私としてはちょっと (´・ω・`) です(node.js部分は大した処理をしないので実用的にはそこまで問題じゃないと思います)。
とか思いながらスライド見てると…
!?
なんか指名されたので、やってみましょう。
そのためにはShiny-Serverのソースコード(node.js)を変更する必要があります。v0.3.6で試しましたが、ざっと見た感じ現時点での最新版のv0.4.0でもいけそうです。
このブログのタイトルにある通り書き換えるのはたったの1行です。
書き換えるファイルはこちら(GitHub: shiny-server/app-spec.js at master · rstudio/shiny-server · GitHub)。
- 【Shiny-Serverのインストールディレクトリ】/lib/worker/app-spec.js
普通にインストールすると /usr/lib/node_modules/shiny-server あたりにShiny-Serverはインストールされるかと思います。
書き換える内容は24行目〜30行目ぐらいの以下の部分です。
this.getKey = function() { return this.appDir + "\n" + this.runAs + "\n" + this.prefix + "\n" + this.logDir + "\n" + JSON.stringify(this.settings) + Math.random(); //乱数を追加! };
Shiny-Serverを起動して、重い処理をするShinyアプリに複数のブラウザ/タブからアクセスしてみてください。
topコマンドなどでCPU使用率を見てみると、これまで複数のブラウザ/タブに対してRのプロセスが1つしかなかったのに対して、変更後は複数のRプロセスが立ち上がり、並列に処理をしているのがわかると思います。
なんで、乱数を1つ追加しただけでマルチプロセス化できたの?
まず、ShinyとShiny-Serverがやっていることについて見て行きましょう。
- Shiny単体で利用する場合
- Shiny-Serverで利用する場合
Shinyはローカルで利用することを前提としていて、上記のようにユーザのブラウザとShinyが1対1でWebSocketで通信して、ユーザの操作をRが知ったり、Rの処理結果をユーザの画面に表示したりしています。Shinyの起動はRコンソールからrunApp関数を叩くことで実行します。
Shiny-Serverの場合は、まずユーザがShiny-Serverのアプリにアクセスして(①)、Shiny-Serverがアクセスされたアプリに対応するR(Shiny)プロセスを起動します(②)。そして、プロセスが起動したらShiny-Serverはブラウザ・ShinyそれぞれとWebSocket通信を確立します(③・④)。あとはShiny-Serverはブラウザから来たものをそのままShinyに渡し、Shinyから来たものをブラウザに渡してるだけです。このとき裏で動いているShinyは単体で利用している場合と全く同じです。
それでキモはShiny-Serverの②のところなんですが、Shiny-ServerはShinyプロセス一つ一つにキーを割り当てて、同じキーに対応するプロセスが存在する場合は、新たにプロセスを起動せずに再利用します。つまり、複数のブラウザからのアクセスに対して同一のキーを生成するのであれば、それらブラウザのリクエストは同じR(Shiny)プロセスによって処理されることになります。要するに重いです。
んで、そのキーを生成している所が上でも挙げた
- 【Shiny-Serverのインストールディレクトリ】/lib/worker/app-spec.js
の getKey関数
this.getKey = function() { return this.appDir + "\n" + this.runAs + "\n" + this.prefix + "\n" + this.logDir + "\n" + JSON.stringify(this.settings); };
です。
この関数を見てみると、アプリケーションのディレクトリやログのディレクトリ、設定内容などなどからキーを生成している事がわかります。Shinyではアプリケーションのディレクトリ = アプリ名なので、基本的にはアプリ(+設定)ごとにR(Shiny)プロセスを立ち上げています。
なので、このキーを毎回変わるようにすれば、ブラウザのアクセスごとにR(Shiny)プロセスが起動するので複数のユーザがプロセスを共有してしまい、だれか1人の重い処理が走っている間は待たないといけないといった状況を回避できます。
それで一番最初に戻ると、とりあえず一番簡単な方法として、キーの生成時に乱数つけてしまえばいいんじゃね? ってことです。
ただし、この方法だと同時接続数 = Rプロセスの数になってしまうので、本格的に運用する場合はキーの生成方法をちょっと工夫して最大プロセス数を制御するなどやったほうがいいかと思います(私はそうしました)。
なんでかというと、各リクエストの処理高速化のために、大きいデータをShiny起動時にメモリ読み込むような作りする場合とかもあると思うのですが、そうするとプロセスが増えるたびにメモリ使用量もどんどん増えていくからです。
ちなみに、一旦、ブラウザとShinyのWebSocket通信が確立してしまえば、間に立ってるShiny-Serverはデータを受け渡すだけで、キーが接続確立後に使われることはありません(画面のボタンを押すたびにプロセスが立ち上がったりしない)し、ブラウザとShiny-ServerのWebSocketの接続が切れると対応するR(Shiny)プロセスも落ちるようになってるので、ゴミが残る心配もありません(※ ただコードに手を入れる前からたまにゴミプロセスが残ることがあります)。
ただ、Shinyはconfファイルで色々設定できるのですが、上記の変更をしたうえで凝った設定をすると動かなくなるかもしれないです(未検証なので)。
まとめ
- 作者: Sau Sheong Chang,瀬戸山雅人,河内崇,高野雅典,橋本吉治
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/04/26
- メディア: 大型本
- この商品を含むブログ (9件) を見る
データサイエンティスト養成読本 R活用編 【ビジネスデータ分析の現場で役立つ知識が満載! 】 (Software Design plus)
- 作者: 酒巻隆治,里洋平,市川太祐,福島真太朗,安部晃生,和田計也,久本空海,西薗良太
- 出版社/メーカー: 技術評論社
- 発売日: 2014/12/12
- メディア: 大型本
- この商品を含むブログ (1件) を見る
注意事項
- 本記事の内容について私は一切保証はしません。エライことになっても責任も負いません。
- Shiny-Serverのライセンスは AGPLv3 なので、ソースコードを変更してサービスを公開する場合は注意して下さい。