ここ1年くらい、仕事で node.js をいじっているのだけど、ちょっと愚痴を書いてみる。
node.js が流行したのはもう何年も前のこと。
Apache よりずっと速く処理できる、ということだったけど、仕事でそこまで処理速度が必要なことはやっていなかったし、その時は、ふぅん、そんな技術もあるんだ、という程度。
1年ほど前に、WEB socket 使いたい仕事があって調べたら、node.js を使うのが一番やりやすいとわかった。
最初はよくわからない技術だったけど、その後それなりに実績もあるようだし、使ってみようと思った。
と書いたところで、相変わらず node.js がマイナー技術だという認識なので、コイツが何者であるかを最初に説明しよう。
今これを読んでくれている人は、おそらくPCかスマホの WEB ブラウザで読んでいるだろう。
多分、そのブラウザには Javascript というプログラム言語の実行環境が備わっている。
プログラムなんてどんな言語でもそれほど大きくは変わらないものだけど、それぞれ特徴はある。
Javascript の最大の特徴は、WEB ブラウザという「利用者が直接使う部分」に密着する様に作られている、ということだ。
人が使う部分…フロントエンドとか、ユーザーインターフェイスとか言われるけど、その部分はとにかく人を待たせてはいけない。
Javascript も、「人を待たせない」ことを理念として作られている。
具体的に言うと、「1秒待つ」とか、「ユーザーの入力を待つ」とか、「ファイルを開いて内容を得る」なんて考え方が無い。
これらは、パソコンの計算速度に比べると、途方もなく遅いからだ。
代わりに存在しているのが、「1秒後に関数呼び出し」「ユーザー入力が終わったら関数呼び出し」「ファイルの内容を引数に関数呼び出し」というように、とにかく待つ代わりに「あとで呼び出して」と依頼しておく方法。
このプログラム方法、初心者にはとてもわかりづらい。
まぁ、初心者にわかりづらい、なんていうのは些細な話。
ブラウザだから、ユーザーを待たせないことが第一で、そのためにはプログラマーが苦労するのは当然なのだから。
でも、ブラウザに限らず、プログラムが「待たせたくない」場所は多い。
サーバーは人が使わないから多少遅くてもいい…と言われたのは昔の話で、今は多数のサーバーアクセスを高速にこなすことが求められている。
だって、クラウド時代だもの。昔とはサーバーの接続量がケタ違い。
で、Apache …というか、その上でよく使われる PHP は「ファイルを開いて内容を得る」とか「SQL にクエリを投げて、返事を待つ」とか、とにかく遅い動作が多い。
これ、Javascript みたいに「終わったら関数呼び出し」にしたらいいんじゃない? というのが、node.js の基本的な発想。
google が作成し、chrome に搭載されている Javascirpt のエンジンを使って、サーバーで動作できるようにしてしまった。
書ける人が多い Javascript がサーバーでも使える~というような説明も見るのだけど、これはミスリードだと思う。
主眼は Javascript を使うことではなくて、「待たせない」プログラム手法がすでに確立されている言語を使うこと。
先に WEB socket を使うなら node.js と書いたけど、これは実のところ node.js である必要はない。
WEB socket というのは、TCP/IP ソケット上で実現されている HTTP 接続の上で、TCP/IP のような「ソケット」を実現しよう…という、回りくどい技術。
でも、HTTP というのは、TCP/IP の機能の、ほんの一部を利用して作っているんだ。
それを拡張し、再度 TCP/IP のような汎用性を持たせれば、メリットは計り知れない。
じゃぁ TCP/IP 使えばいいじゃん、となりそうだけど、WEB socket だと、HTTP の延長上にあるので WEB ブラウザから使いやすい。
Apache + PHP だと、Apache が完全に HTTP を前提に作られているので、WEB socket は扱いにくい。
そこで、node.js の出番となる。
実のところ、WEB socket はまだブラウザごとに実装が一部食い違ったり、実装していないブラウザもある。
そういう場合でも、node.js 上のライブラリ、socket.io が、ブラウザごとの差異を隠してくれる。
さて、前提条件の話だけでずいぶん長くなったな…
node.js + socket.io の勉強を始めた時は、右も左もわからなかったので、分かりやすいチュートリアルに従って使い始めた。
そのチュートリアル自体が多少古いものだったので、Express ver.2.x を前提としていた。
この Express は、node.js 上で動く WEB サーバー。
当時の socket.io では、WEB サーバーが前提として必要だった。
でも、実はこの時点で Express の最新版は 3.x だった。
判っていたけど、3.x 対応の使い方説明で良いものがなかったので、「後でバージョンあげられるでしょ?」と思って、古いバージョンを使った。
…で、話は一気に最近へ。
Express は v2 と v3 と v4 で、全然互換性が無い。
いや、全然というのは間違いか。でも、ずいぶん書き変えないと動かない。
socket.io も、v0.9 が長い間使われていたけど、v1.x 系列となった。
こちらも、かなり書き換えないと動かない。
メールを扱おうと思って nodemailer というライブラリを使った。
これも、v0.7 から最近 ver1.0 になり、かなり書き換えないと動かない。
互換性を保てない設計変更が悪い、とは言わない。
最初から完璧な設計なんてできないし、作り進めるうえで変更しないといけないことも多いだろう。
でも、node.js の界隈は、互換性を気にしている人なんて誰もいない、というように思える。
理由は二つあって、node.js を使う最大の理由が「速度」だというのが一つ。
過去との互換性を気にして速度を落とすなんて馬鹿馬鹿しい。効率のためには互換性を切ったっていいだろう。
もう一つ、おそらくは「後から関数を呼んでもらう」というスタイルのプログラムは、速度効率は良いかもしれないけど、互換性を保ったまま拡張することが困難になりやすい。
多分、多くの原因はこちらだろう。
これは自分がプログラムしていて困った部分だけど、ループ処理している部分があった。
その中で、扱う値を「SQL DB に入っている値を参照して」扱いを変えなくてはならない、ということになった。
これ、絶望的なプログラム書き換えを伴う。
PHP なら、SQL に問い合わせて、答えを見るだけだ。
でも、node.js では、SQL 問い合わせは関数を呼び出してもらう必要がある。関数が増える。
この関数は、「後で」呼び出されるものだ。すぐに答えを得られるわけではない。
でも、ループしているのは「全部を処理する」ためのループではなく「順次処理する」ためのループだった。
1つの処理が終わってから次の処理に入らないといけない。
処理の中の DB 問い合わせが、すぐに結果を得られないため、ループそのものを解体する必要が出てきた。
ループの中身を関数として書き出し、関数の中で DB 問い合わせをし、結果を貰うための関数を呼び出す。
結果で呼び出される関数の中でループ変数にあたるものを操作し、必要であればループの中身にあたる関数を呼び出すことで「ループ」を構成する。
再帰プログラムのようで、再帰ではない。
だって、呼び出しは遅延して行われるのだもの。DB 問い合わせした後に関数は終了し、スタックは解放されるので、再帰階層が深くなってもメモリを馬鹿食いしたりはしない。
逆に言えば、スタック解放されるからローカル変数は使えない。
グローバルをやたら増やしたくはないから、クロージャのように関数内で変数と関数を定義して、「解放されないローカル変数」を作り出す必要がある。
複雑怪奇なプログラムになった。
こういう大幅な書き換えがあると、呼び出し方法も変わることが多い。
そう、つまり、ライブラリ作者たちも同じような理由で呼び出し方法を「変えざるを得ない」ことがあるのではないかな、と思う。
Apache + PHP だと、ロードバランサ―を使って多重化できる。
万が一サーバーが死んでも大丈夫。
PHP は、アクセスの度に呼び出される。エラーが起きても、次のユーザーの接続で再起動される。
でも、node.js はそうではない。Apache にあたる、サーバー自体を書いている。
正しくエラーハンドリングすれば止まることはないのだけど、止まったら致命的。
でも、多くの人が同じ悩みがあるから、cluster を作ることができる。
複数のプロセスを同時に起動しておいて、どれかが止まっても大丈夫なようにするのね。
この cluster 化の方法が、多くの人が考えたため、たくさんある。
プログラムを大幅に書き変える必要がある物から、簡単に cluster 化できるものまで。
最近の流行は pm2 のようだ。
クラスタ化するだけでなく、接続を出来るだけ保ったまま、順次プログラムの再起動も出来る。
#socket.io は「ずっと通信路を保つ」ため、この通信路はどうしても切れてしまう。
socket.io 自体に、通信が切れたらすぐ再接続する仕組みはあるが、再接続後に先ほどと同じ状況を整えるのはプログラムが責任を持つ必要がある。
もっとも、これは pm2 と関係なく、いつ通信が不安定になるかわからないので、作らないといけない処理だ。
で、この pm2 を使おうとしたら、socket.io はおかしくなる。
socket.io はそのままではクラスタ対応できていないのだ。
まぁ、PHP だって、ロードバランサを使う時には守らないといけない決まりがある。
これは当然だろう。そして、socket.io も、いくつかの決まりを守ればクラスタ化できる。
…うまいくいかない。
この作業、半年くらい前に一度やって、どうしてもうまくいかないので一度断念した。
最近再チャレンジして、理由がわかった。
socket.io の v0.9 までは、クラスタ化に対応いしていたのだけど、v1.0 になった時に対応を「外部に任せた」のだった。
いや、そのことは知っていたよ。
多くのページがその方法を書いているから、その通りにしたのだけどダメだった。
で、知識が増えて再チャレンジして、多くのページが勘違い記述していることを知った。
世の中の半数以上のページが v0.9 前提で書かれていて、残る半数のうち、8割以上が 0.9 から 1.0 になって、新たな記述方法となったことを伝えている。
でも、そうじゃない。v1.0 になって、概念が変わった。細分化された。
クラスタ対応は「通信」と「ハンドシェイク」の2段階に分割され、多くのページに書かれているのは「通信」の対応だけだったのだ。
通信はできるが、ハンドシェイクにすごく時間がかかるようになる。
#socket.io は、ハンドシェイクのために2回アクセスを行う。
この2回が同じサーバーなら成功して通信に入るが、違うサーバーだとリトライする。
確率問題で成功するまで、延々リトライを繰り返すため、接続に時間がかかるようになる。
socket.io の公式ページの奥底に、ハンドシェイクは外部の別プログラムに任せる、ということと、推奨するライブラリが書いてあった。
でも、このライブラリは、pm2 とは違う cluster 化ライブラリを前提としていた。
そのライブラリは、確かにクラスタ化してくれるが、低機能で役に立たない。
探し回って、やっとわずかな情報で socket.io のハンドシェイクをクラスタ対応にする方法がわかった…ように思えた。
方法は二つあった。
1つは、大幅に socket.io の接続部分を書き変える方法。
もう一つは、それをライブラリ化したもの。
ライブラリの方を使ってみたら、うまく動かない。
どうやら、socket.io やその他もろもろのライブラリに依存してしまうようだ。
バージョンが変わるごとに非互換になるので、こういうことになる。
大幅に書き換えが必要な方は、まだ試していない。
その書き換え方法が、どのバージョンでつかえるのか明記されていないため。
でも、node.js の歴史自体が浅く、まだバージョンによって互換性が致命的に失われることを理解している人が少ないため、バージョンを明記した記事はあまり見かけない。
自分が求めている方法を見つけては、試しに自分のプログラムに組み込んでみて(場合によっては非常に大きな書き換えを伴う!)、ダメだったら自分の勘違いなのかバージョンが違っているのかを理解して、多くの場合は「諦め」て別の情報を探す、という決断が必要となる。
と、以上は node.js に対する愚痴でした。
node.js を使いこなしている人から見たら、入り口付近で悩んでいるだけかもしれない。
ライブラリも全部 javascript で書かれているから、自分で解析して書き変えてしまう、という方法もなくはない。
でも、今後のバージョンアップ(と、そのたびの非互換性)を考えると、手を加えるのは得策でないように思う。
同じテーマの日記(最近の一覧)
別年同日の日記
申し訳ありませんが、現在意見投稿をできない状態にしています。 |