1. トップページ>
  2. お役立ちコンテンツ一覧>
  3. node.js における stream の歴史とそれぞれの問題点

node.js における stream の歴史とそれぞれの問題点

記事画像

前史

2010年頃の初期のnode.jsは、公式サイトでTCPのechoサーバのサンプルを紹介していた。

2010年2月9日、TCPstreamのイベントは「receive」と「eof」と名付けられた。それからほどなくして、2010年2月18日までにはこれらのイベント名は現在の「data」、「end」に変更された。まだStreamクラスは無く、EventEmitterがあるのみだったが、ファイルやTCPソケットなどは基本的に同じと認識され、コンベンション(慣例)が発達した。

これが、nodeで最も重要なAPI、_Stream_の誕生である。しかし、まだ事は具体化され始めたばかりだった。

ウェブサイトに上がることはなかったが、nodejs 0.2のsys モジュール(現在はutilモジュール)にはpumpメソッドがあった。最初のnodeカンファレンスまでには、streamのための基底クラスができていて、sys.pump(readStream, writeStream, callback)はreadStream.pipe(writeStream)になっていた。

今でこそstreamはnode.js APIとしっかりつながっているが、node 0.4で私たちの知っているnode.js streamがそうであるようにpipeで連鎖が可能になるまではそうではなかった。

この頃、streamは人気になってきていた。僕がstreamを発見して、これは使えるんじゃないかと思ったとき、streamを関数型リアクティブプログラミングに似た使い方をしたevent-streamを書いた。

僕はsubstack氏とMax Ogden氏をstreamの便利さについて納得させ、その後彼らが他の人たちを納得させた。

streamはnodeのコア開発者によって作られたのだが、その後streamは、ユーザースペース、つまりnodeのコアに寄与してきたわけではないかもしれないがnodeのエコシステムには大いに貢献してきたという人々に取り入れられた。

この2つのグループは、streamがどのようなものかについて、少し異なる解釈を持っていた。主に、コア開発者のほうはstreamを生のデータに_のみ_適用するつもりだった。コア側の解釈では、streamはバッファや文字列であり得るというものだった。しかしユーザ側は、streamは直列可能なもの、つまりバッファや文字列に_なり得る_という解釈をした。

みんなstreamの全ての機能(それが一連のバッファであれ、バイトであれ、文字列であれ、オブジェクトであれ、場合によってはエンドやエラーと一緒にアイテムを一度に渡してくれる)を必要としていたはずだ。同じAPIを使ったらいいんじゃないの?これが僕の意見だ。もしjavascriptが厳密に型付けされていたら、このような議論は起こってなかっただろうが、javascriptはそれを気にも留めていなかったし、streamコードは当時、非依存的だった。つまり、文字列に機能するときと全く同じように、オブジェクトに機能した。

初期のnode streamの問題

streamはかなり抽象的な概念だと認識されているが、使うのが難しくもある。その主な難しさの一つは、データを作るや否や、それらがstreamに流れ始めてしまうことだった。streamを作成したら、何かを非同期にする前にそれをどこかにパイプしなければならなかった。

この問題は、pauseメソッドがあくまで_参考程度_にすぎず、データの流れをすぐに止めるという意味ではなかったことで、悪化した。

また、一般的に、streamを動かすのに明確にしなければならないことが多すぎたということもある。write endやイベントの「data」「end」を実装しなければならなかっただけでなく、さらに(いまだに物議を醸している)destroyメソッドや「close」イベントもあった。

僕は、二つ目のケースに対処するためにpause-streamを書いたことがあるが、もっと重要なのはthroughだ。僕は、ユーザランドでstreamを使って11か月間ほどthroughを試し、ついに、最も一般的で、ユーザランドの視点で使いやすいパターンを見つけた(コアに近いstreamのオーサーたちは、デバイスからの読み込みやデバイスへの書き込みをするstream、つまり低レベルインターフェイスを実装する傾向があるが、ユーザ側のstreamは主に変換である)。

僕がモジュールを「through」と呼んだのは、それがインプット、アウトプット…スループットだと思ったからだ。これはのちにIsaac氏によって、「through」は一般的な用語で、「transform」streamという名前のほうがよかったと指摘された。今になってみれば、僕もそう思う。throughがいよいよstreamの作成を容易にしてくれたので、僕たちがやることは関数を2つ提供するだけでよかった。そして、あらゆるエッジケースを処理してもらえた。throughには現在1601個の従属モジュールがあり、継承者にはthrough2(同じAPIだがstream2を使っている)には5341個の従属モジュールがある。(npm-dependentsを参照のこと)

面白いことに、僕がこれを始めたのは、Isaac氏がstream2の開発をし始める数週間前だった。stream2ではstream1の問題が対処されたが、これまでのAPIからはかなり一線を画したものとなった。

stream2はpipeを呼び出すまで一時停止され、read(n)関数を中心に実行された。readは任意のバイト数を取り、それと同じバイト数のバッファを返す。これは、パーサーの実装に役立つよう意図されたものだが、これを使うにはstreamを直接理解しなければならなかった。なぜなら、pipeはバイト数を特定する方法を教えてくれなかったからだ。

stream2はstream1よりさらに複雑だった。そして、さらに始末が悪いことに、stream1と下位互換性があったのだ(もっとも、stream1は「レガシー」と見なされていたけれども)。

stream1は100行くらいで読みやすく(リーダブル)、僕はそれを何度も読んだが、stream2がnode 0.10に入る頃には、Readableクラスは800行以上あった。

stream2の開発中、オブジェクトはデータかという議論が、オブジェクトのstreamがAPIに正式に認められたことで落ち着いた(そうでなければ、streamの可能性を排除していただろう)。

Stream2の問題

このようなstreamの変化の全てが、自ら問題を引き起こしていた。だが、賢明な策だったのはstream2を独自のリポジトリで開発したことだった。これにより、興味のある人がstream2を当てにできるようになり、nodeのforkを使わなくてもstream2を試すことが可能になった。

実は、もしあなたがthrough2を使っているなら(そして、あなたが使っているモジュールが使っているという理由でかなり頻繁にthrough 2を使うなら)、そのreadable-streamはレポジトリに依存している。これはnodeに組み込まれているが、レポジトリのバージョンは、バージョン間の互換性がより高い。readable-streamを使ったモジュールは、最新バージョンのstreamが入っていな可能性のあるnodeの古いバージョンでも動く!

最初は難しかった新規streamのパイプ処理や、stream作成をするためにより分かりやすいAPI取得を乗り越えたら、新たな問題が浮上した。エラーを何らかの方法で伝搬すべきだとの認識が高まっている。この(未解決の)問題では、単純なhttpファイルサーバがファイル記述子を漏出させることが分かっている。

これを防ぐには、nodeで関数がどう流れるかについて詳細に理解しておくのは言うまでもないが、fs streamのソースとエラーの起きたデスティネーションstreamの両方を理解する必要がある。

これは未解決の問題で、io.js革命ですら解決できなかった。これは確実に互換性を破る変更になるだろうから、コアstreamの動き方を変えるのは非常に難しくなる。

ユーザランドでのアプローチがある(sys.pumpを思い起させる!)。

エラー伝播をnodeのstreamに追加するという、真面目な提案があった。しかし後方互換性を提供するのが難しく、この提案は支持されなくなった。

Stream3

今のnode 5に入っている現在のstreamのバージョンは、stream3と見なされている。これはstream2を有意義にリファクタリングしたものだが、後方互換性もある。おまけに、stream2#read(n)のサポートもまだしているのだが、これはstream2形式でトリガーするまで動作がアクティブにならない。もしあなたがstreamをsource.pipe(dest)を用いて普通に使うだけなら、streams3.pipeは基本的にstream1のように機能する。

もしstream1が_クラシック・stream_でstream2が_ニュー・stream_なら、stream3は_ヴィンテージ・stream_だ。古いけど、新しい。しかし悲しいことに、stream2が回り道をしている間に拾った機能のせいで、現在のstream3はまだ複雑すぎる。

stream4?いいえ。

nodeのstreamが僕たちに教えてくれることがあるとするなら、それは、「コア」内部のstreamのような基礎的な何かを開発するのは、大変難しいということだ。何も壊さずにコアを変更することはできない。なぜなら、コアはただ当然のこととみなされ、コアのどの部分に依存するかは決して宣言されないからだ。それゆえ、コアに対し、常に後方互換性を持たせ、ただパフォーマンス改善のみを重視する非常に強い動機が生じてしまう。これはかなり良いことではあるのだが、まれにうっかり、マイナス要素のある判断がされてしまい、気づいた時にはもう手遅れになる。そんな状況では、きっぱりと中断することが必要となる。node.js自身がその好例だ。nodeはスレッドやブロッキングを中断し、スタック全部を一から書くことができたため、過去の動的言語(perl、ruby、python)のIOパフォーマンスを改善できたのだ。

しかし、stream1のユーザビリティの問題で、エラー伝播の必要性を見直すことが手遅れとなってしまった。そして今、nodeの成功それ自体が、今後さらに改善をしていく上での障害を生んでいる。

streamはnodeの長所の一つだが、まだまだ使いこなすのは難しいし、まだまだ改善の余地がある。しかし、streamがnode.jsのコアに属している限り、その改善がされることは不可能だ。

原文:http://dominictarr.com/post/145135293917/history-of-streams (2016-10-03) ※元記事の筆者には直接翻訳の許可を頂いて、翻訳・公開しております。

簡単60秒無料登録

フリーランスの経験はございますか?

新着案件

フルリモート案件

アクセスランキング

関連記事