Event SourceやWeb Socketを使うにあたっての考察
背景
Reactの記事はちとまって欲しい。gitにcodeをあげたら下書きにしたものをアップするんじゃ。
Server上のlogをtail -n 5 -F /var/log/messages
などで見るような感じで、Web Page上で見たい、という気持ちがあった(あった……)。
これをMojoで実現しようとしたのだけれど、Server Applicationの中でFile::Tailして読み出しながらEvent Source(以下ES)で接続してきたClientに配送しようとしたら、結構blockingしてしまって困った。
いやこのあたりはもっとうまく組めばいけたのかもしれないけど今のところうまくやる方法が思いつかなかったの。
いっそtail commandをMojo::IOLoopとかにいれてしまえばというのも考えたけど、監視したいlogが増えたりしたときにServerの下にchild processがいくつもぶら下がるのはあんまり楽しくない。
前にhypnotoadで立ち上げたらprocess落とせずに残っていたりしたからな……。こやつらのせいでportが使用済みになってserverを再起動しようとしたら蹴られたりしたし……。
ということをもやもやと考えながら結局どういう指針を取るのが良さそうか考察していったという話。
未検証タグがついているように結論で述べていることについてはまだ実験してない。どこかでやってみたい。
そもそもblockingさせない方法なんかないかな
調べてみる
通常Perlでファイルを読み込む処理を書く場合は、ノンブロッキングな処理ではなくてブロックする処理になります。ですから、大量に処理が必要となるコンテンツ配信サーバーを書きたい場合は、普通のファイル入出力の処理を書いてはいません。
AEをインストールして、AnyEventを使って、ファイルを読み込む部分を、ノンブロッキングで書く必要があります。こうしないと、HTTPリクエストとレスポンスの処理が並列化できても、内部がブロックしてしまうことになるからです。
そういうことなんだよな。
Mojolicious::Plugin::Webtail - search.cpan.org
まあこういうものもないわけではないけれど。websocketでtailというのもありっちゃあり。双方向にする意味ないけど。
たぶん一つの答え
これ中を見たら単にtail command打ってるだけ。でもたぶんこういう方法が一つの答え。Mojo::IOLoop::Streamでtail commandを読み取ってひたすらESで吐いていくという方法。
でもさっき言ったときたまchild processが残るとか、hypnotoadで立ち上げてdownさせるときにchild processがkillされずに残ったりしたので嫌だ。
余談: mojoでevent source
Mojolicious::Plugin::EventSource - search.cpan.org
これはes書くのに便利そうだけど今求めてるのはそうじゃない。
いっそFile::TailをIO Eventとしてwrapして、readの中でMojo::EventEmitterのemitを実行する?
たぶんこれを使うことでできるんだよな。
Mojo::Reactor · yuki-kimoto/mojolicious-guides-japanese Wiki · GitHub
Reactorでevent loopを作って、emitで発行、という感じなんだろうなあ。
これでFile::Tailをwrapするほうほうが思いつかないけど。というかFile::Tail自体無限loopで定期的にFile::Tail->read()を読み取りに行っているだけだから結局どこかでloopを実装しないことにはどうにもならない。
いっそsingleton loopのrecurringに登録するか? いやそれでも止まるし。
結論
たぶんIOLoopみたいにEvent Loopとして組み込むのは(僕の理解levelでは)無理筋。もっとReactorとか理解すれば別なのかもしれないけど、今は無理。いつかこのあたりの理解は再挑戦したい。
別の筋を考える
とりあえずESをさっくり諦めて、GETやPOSTでfileをちまちま読み込めば、このあたりはどうにかなるのはわかる。でもlogrotateとか起きたときがややめんどい。いやrotateさせている以上設定見ればなんて名前になるかわかるし、それをopenすればいいんだけど。
いやまて。なんか思いついた。
少しおさらいする。
大勢のconnectionに対して同じ内容をbroadcastできる仕組みとしてのweb socket(これはesのようにserver=>clientだけじゃなくてclient=>serverへのrequestも飛ばせる)やevent source(server sent event、server=>clientの一方通行の通信)だけど、僕がmojo使ってもそれをうまく使いこなせなかった。
なんでかといえば、mojoのserverの中でlog fileなどをtailさせて延々event sourceなどで配信しようとしたのだけれど、commandでtail -F /var/log/messages
とかさせればMojo::IOLoop::Streamでpollさせることもできるけど、その手段をとりたくなかったから。
というかforkさせてprocess起動させっぱなしにしながらevent sourceで配送するというやり方をしたくなかった。たくさんのprocessの面倒を見るとかしたくない。というかtail -Fとかwatch command的なものをperlからforkして実行しっぱなしにするって想定されているのかわからない。
いやtail -Fならともかくwatchは制御commandをはき出してscreenを更新しているだけだからうまくいかないと思う。
せめて内部でFile::Tailとかを使ったmethodをloopさせてやりたかったけど、けどこれだけのためにAnyEventみたいなものを入れるのもなんか嫌だ。
で、考えていたけど
たぶんTailというかresource monitoringさせるserverとdataをclientにbroadcastするserverを一つにしているから大変なんだな、と。
resource monitoring serverがweb serverのapiを叩いて、web serverはapiが叩かれて新しいdataを受け付けたらweb socketなりevent sourceなりでbroadcastするという仕組みにするのが、一番良い気がする。
つまりこんなん
+------------------------------+ Push +--------------+ broadcast +-----------+ | | | | | | | Resource Monitoring Daemon | +----------> | Web Server | +----------> | Client | | | | | | | +------------------------------+ +--------------+ +-----------+ + | | Watch | v +-----+ | Log | +-----+
これならESであれWeb Socketであれ話は簡単。data受付用のapiを Web Serverでlocalにだけ公開しておいて、そこに何かがpushされたら、Web Server内でeventをkickしてESで配送させればいい。まあどこかにstoreを用意しておいて、Mojo::IOLoop::singleton->recurringあたりで適宜様子見してwriteさせるなり、ping的なloopをどこかに用意しておいてstoreを確認して、とすればまあいける。仮にtail -n 100
みたいな処理もしたくなったとしても、storeをうまく使えば対処できるはず。
欲を言えばresource monitoring serverとか作らないで、syslogとかinfluxdbとかでweb serverに転送する機能があれば楽なんだけどなー(無茶言うなし
syslog-ngにあったし
7.7. Posting messages over HTTP - - The syslog-ng Open Source Edition 3.9 Administrator Guide
あった。
最近のsyslog-ngはdestにhttpをとれるのか。すごいな……。
influxdbの場合
ぱっと見そこまでぴたりというものはないけれど、考えてみればMojo::UserAgentがnon-blockingで動くから、これでcallbackの中に仕込んじゃえばいいかな……。定期実行できるなら同じだし。
たぶんそろそろ問題が違う
ESやWeb SocketでLoggingしたいみたいな処理をやるとして、Logging用のResource Monitoring Daemonを外に立てて、そこからServerにPushすることでServerにdataをStoreさせ、Serverは定期的にESなどでwriteすればいい、という流れはいけると思う。
問題はMojoの中でのLoopか。まあ500行のlogをstoreして一気にはき出すくらいなら1secもかかわらないだろうけど。
ここら辺をnon-blockingにやりたいけれど、なんかいい方法ないものかなあ。
ああ、あれか。storeは初接続用のためだけの処理として残しつつ、普通はpostされた直後にbroadcastしちゃえばいいのか。とするとESとのconnection一覧をarrayか何かで保持しといて共有しとけばいいな。
今のところこの方法が一番気楽かな。
とはいえここまでしておいてなんだけど、ここまでするくらいならいちいちfile読み込むlegacyなやり方の方が安定しそうな気がする。個人でやる範囲では。大量の接続が来るようならこういう方法も考えてもいいかも。