読者です 読者をやめる 読者になる 読者になる

たごもりすメモ

コードとかその他の話とか。

Node.jsなWebアプリでJobQueueなしにラクラク巨大処理を実行

node

Node.jsでWebアプリを書いてるんだけど別に大量のリクエストをさばくわけでもないしWebSocketも使ってないし、じゃあなんでそんなことしてんの、という話。

まず結論だけ書くと、
並列度が低くてよいが長時間かかることが確定的な処理を非同期的に走らせる必要がある場合、普通はそのような用途でもJobQueue/Workerを使って構成するがそういうのは管理も面倒だしインストールも面倒くさくなるのであんまりやりたくない。Node.jsなら普通のWebアプリケーションフレームワークだけでラクに書けていいんじゃね?
というひとつの提案です。

同期実行のケース

普通Webアプリケーションフレームワークというのは、一連の処理はクライアントにレスポンスを返すことで完了する。そしてひとつのプロセス/スレッドはリクエストをディスパッチされてからレスポンスを返すまでがそのリクエストに占有される。
ここで、負荷自体はそうでもないが長時間かかることが確定的な何らかの処理*1を実行する必要があるとする。なーんにも考えずにリクエストハンドラの中でこの処理をやろうとすると、こんな感じになるだろう。


"DataStore" は特定の何かじゃなくて、DBでもいいしKVSでもいいし、他の何かでもいい。何らかのデータの永続化機構とする。

  1. リクエストをディスパッチされる
  2. 対象データのなんらかのidを計算して保存する
  3. 対象データ自体の生成をがんばって処理する
  4. 完了したらレスポンスとして返す

処理時間が5秒くらいならまだ許される可能性も無くはないが、15分かかりますとか言われると絶望的ですね。

JobQueueによる非同期実行のケース

15分かかりますとか言われる場合、普通のプログラマはJobQueueおよびWorkerによる非同期化を考える。専用のミドルウェア等がどの言語にもあるので、ああアレね、とおわかりになるでしょう。


  1. リクエストをディスパッチされる
  2. 対象データのなんらかのidを計算して保存する
  3. 該当idに関するデータ生成処理をJobQueueにenqueueする
  4. レスポンスとしてidを返す

Webアプリケーションのリクエストハンドラがやるのはここまで。あとはクライアント側がそのidに関して、データ生成処理の状況を定期的に問い合わせ、完了していれば取得する、というようなことをAjaxでもなんでも使ってやればよろしい。ブラウザやスマートフォンアプリの見た目のレスポンスは極めて軽快になる。おそらく現状、ほとんどの人はこういうように処理を組むと思う。

ただこれ、めんどくさいんだよね。Webアプリケーションサーバ以外にジョブのワーカーの状態も監視しないといけなくなるし、そもそもインストールがめんどいし、ワーカー内で処理がコケたらどうするんだっけとかはミドルウェア/ジョブフレームワークの挙動などによっても異なったり、そこをあれこれ自分で書かないといけなくなったりする。
そもそもこの類のフレームワークは極めて多くの件数のジョブを高スループットで処理するために工夫を尽くして作られていたりするので、いやジョブ件数は数分に1件なんだけどさ(ただし1件のジョブが10分かかるけど)、みたいなケースにはなんというかゴツすぎる気がするよね。ね?

Node.jsによる非同期Webアプリケーション単体での実装ケース

よく考えたらなんでJobQueueに出すかっていうと、レスポンスを返したらそのハンドラの寿命はおしまい、というフレームワークに問題があるわけで、それらのフレームワークはもちろん理由があってそうなってるんだけど、Node.jsならその理由が存在しないんだからそのまま処理しちゃえばよくね? という話。
なお、CPUをもりもり使って計算するようなのはもちろんこのケースでは使えない。あくまで他ホストに処理を移譲して、その結果を延々待っている、というような処理だとしましょう。


  1. リクエストをディスパッチされる
  2. 対象データのなんらかのidを計算して保存する
  3. レスポンスとしてidを返す
  4. そのままデータ生成処理を続行
  5. 完了したらどこかにデータを保存して、あとは黙っておしまい

コード例(expressのハンドラ)としてはこんな感じかな。

app.post('/createData', function(req, res){
  var dataid = AppHandler.createId(req.body.data);

  AppHandler.saveId(dataid, function(err){

    if (err) { /* error handling and send error response ... */ }
    res.send(dataid);  /* ここでクライアントにはレスポンスを送っている */

    AppHandler.process(dataid, function(err, data){ /* 長時間かかるのはここ */
      AppHandler.saveData(data);
      /* 終わったらそのまま黙っておしまい */
    });
  });
});

クライアントは前のケースと同様、定期的にAjaxなりでデータ生成処理の状態を確認し、終わってたら取り出す。

これだけで長時間かかる処理を、ユーザへのHTTPレスポンスを軽快に返しつつ、しかもJobQueueとかWorkerとかの外部機構を使わずに実現できる。超簡単。
セットアップもただのWebアプリでしかないし、シンプルですばらしくないですか。

もちろんデメリットはある。レスポンスを返したあとの処理中に node のプロセスが落ちたりすると、そのまま処理内容が失われる。失われたまま放置されると困るような場合には、どうにかして失われた処理対象(のid)を拾い上げて再実行しないといけない。それはそれで気を使うかも。

まとめ

長時間待つような類の処理をしないといけないんだけどJobQueue/Workerのセットを準備するのもダルいなー、というような場合に、Node.jsで書いてしまえばお気楽お手軽にさくっとできるかもしれないですよ! というお話でした。

*1:他のサイトからの巨大なファイルのダウンロードとか、超重いSQLを実行して結果を待つとか、なんかそういうやつ