React v16 Performance Inspection

React v16 がリリースされた。詳しい記事は以下のエントリを参考にすると良い。

facebook.github.io

安心と信頼の日本語版、koba04記事もあるので見てみて欲しい。

React v16 changes - blog.koba04.com

さて、リリースされたので、実際に会社で作ってるSSRを実施するboilerplateに対して組み込んでみてどれだけ効果があるかを計測してみた。ただし、まだ React の renderToStream は実施していない。ひとまずアップグレードしただけでどれだけの効果があるかを見たかったので renderToStream の効果については今後計測予定。

2017/09/29 追記: renderToNodeStreamの結果を計測結果をまとめに追記した。

Response Benchmark

Spec: Macbook Pro 13 Processor 3.1 GHz Intel Core i5, 16 GB 2133 MHz LPDDR3

100同時接続で5秒間リクエストを集中的に送ってどれだけ捌けるか、捌いた際にどれだけのレイテンシで返ってきたかを計測した。ちなみに計測ツールは autocannon を使った。

renderするコンポーネントが数個程度の単純なページ

React v15:

Running 5s test @ http://localhost:3000
100 connections

Stat         Avg     Stdev   Max
Latency (ms) 447.44  139.85  1049
Req/Sec      214     14.32   231
Bytes/Sec    22.5 MB 1.48 MB 25.2 MB

1k requests in 5s, 113 MB read

React v16:

Running 5s test @ http://localhost:3000
100 connections

Stat         Avg    Stdev  Max
Latency (ms) 423.13 121.59 969
Req/Sec      227.6  8.09   237
Bytes/Sec    24 MB  839 kB 25.2 MB

1k requests in 5s, 120 MB read

大体レイテンシが 440ms(v15) => 420 ms(v16)210Req/Sec(v15) => 220 Req/Sec(v16) で多少改善されているが劇的な変化ではなかった。これはなんとなく想定通りで単純なページほど render にかかる時間がそもそも低いのでこんなもんだろうと思っていた。

render するコンポーネントが十数個以上ある複雑なページ

React v15:

Running 5s test @ http://localhost:3000/largeform
100 connections

Stat         Avg     Stdev  Max
Latency (ms) 1005.66 454.75 2731
Req/Sec      88.8    5.27   94
Bytes/Sec    17.1 MB 932 kB 18.9 MB

444 requests in 5s, 85.6 MB read

React v16:

Running 5s test @ http://localhost:3000/largeform
100 connections

Stat         Avg     Stdev   Max
Latency (ms) 804.6   237.79  1698
Req/Sec      114     6.79    121
Bytes/Sec    22.1 MB 1.42 MB 24.1 MB

570 requests in 5s, 109 MB read

これはアップグレードの効果が分かり、パフォーマンスの改善が見込めた。レイテンシーが平均で200ms短縮されているのと、Maxのレイテンシが落ち着いているので、パフォーマンスの振れ幅も小さくなっているのがわかると思う。 また、 Req/Seq も30程度増加しており、全体的にパフォーマンス改善傾向であることが分かる。

フレームグラフ比較

Server Side Rendering が高速化されているのは分かったので、さらに奥に入ってフレームグラフがどうなっているのかを調べてみた。これによってCPUの負荷やどこにどれだけ時間がかかっているかを粗く見ることができる。

ちなみにフレームグラフは 0x というツールで作成した*1

React v15:

f:id:yosuke_furukawa:20170927230917p:plain

React v16:

f:id:yosuke_furukawa:20170927231110p:plain

React v15 と比較して、 React v16 だと v15 の時にあったcomponentをマウントする際の深いコールスタックが低くなり、renderingにかかるコストが改善されていることが分かる。

ブラウザのロード時間

画像が全くない純粋にJSとCSSとHTMLだけのページで計測すると次のような結果が出た(複数回実施したが大体毎回こういう計測結果だった)。

f:id:yosuke_furukawa:20170927224657p:plain

Server Side Rendering が高速化されているため、 DOMContentLoaded までの時間は短縮されているが、全体のLoadまでの時間はそれほど効果はなかった。 react-dom が v16 になり、 これまで 480KB ほどあった JavaScriptのサイズが 120KBほどに縮小されているため、もっとLoadまでの時間が速くなっても良さそうだが、弊社の環境ではJavaScriptgzipやBrotli等で圧縮されてしまっている上に、JavaScriptもCodeSplitにより分割されているので、バンドルされる1つのライブラリのサイズが4分の1になったとしても実際には誤差の範囲になってしまうようだ。

f:id:yosuke_furukawa:20170927225444p:plain

「サイズじゃなくて、 ブラウザの script 実行にかかる時間は短縮されてるのでは?」と考えてみたが、これも実際には変化はなかった。しかしクライアントのフレームグラフを見てみると、こちらもコールスタックは低くなっており、CPU使用率での改善は見込める可能性がありそうだと感じた。

React v15:

f:id:yosuke_furukawa:20170927230241p:plain

Reavt v16:

f:id:yosuke_furukawa:20170927230304p:plain

おそらく、React Fiber によってクライアントで実行するための全体的な時間は延びているものの、CPU使用率には改善が与えられるようになったのではないかと仮説を立てている。

まとめ

React v16 でどの程度性能向上が見込めるのかをひとまず計測してみた。Server Side Rendering までの時間が短縮されており、CPUコストも軽くなった。結果としてはかなり大幅な改善が見込めた。

各種React依存のライブラリのアップグレード対応は必要になるものの、migrationもそこまで大変ではなかった。

この上にまだ renderToStream やら新しく切り出された機能もあるのでそれもtryしていく予定。ひとまず本日の調査はここまで。

2017/09/29 追記: renderToNodeStreamの結果を計測したが速くなってなかった。

*1:余談だが、 この 0x と autocannon という組み合わせは Node のコンサル会社の一つであり、数多くのコアコミッターを抱える nearForm 社のメンバーが作っているコラボツール

Ayo.js について

Ayo.js とは

「Node.js の fork です。」と言ってもまだできたばかりで正直このタイミングで記事にしてもまだ語ることはそんなに多くないです。

ただし、JavaScript界隈が騒ぎになりかけていることは確かです。日本でも発言が増えてきたので自分なりにまとめて今時点での話をしようと思います。

ちなみに読み方は好きに読んでくれ、と言われてます。 「アイ・オー」でもいいし、「エイ・ヨー」でも良いとのことです。ネーミング的には昔あった io.js fork騒動を想起させるネーミングになってます。もしも io.js についてご存じない方もいるのであれば、こちらをご参照ください。

yosuke-furukawa.hatenablog.com

Ayo.js の目的

https://github.com/ayojs/ayo/blob/zkat/values/VALUES.md

ここを見るとわかりやすいです。序文を見てみましょう。

Ayo.js is about the humans that help make it happen. It's a project aimed at creating a new foundation of project governance and management that brings humans and their concerns front and center.

You can pronounce it however you want: be it “ey-yo”, “I-oh”, or “Awooooo” — the Ayo.js project strives to create an environment where you can feel at home and focus on your contributions to this nice shared space that we all benefit from.

翻訳はこんな感じでしょうか。

Ayo.js は皆の成功を助けるものです。皆と皆の最も重要な関心事に対して、プロジェクトガバナンスとマネジメントをするための新しい foundation を構築することを目的としたプロジェクトです。

Ayo.js の発音は 好きな発音でいいです。 "えいよー"でも "アイ・オー"でも "アウーーーー"でもいいです。
Ayo.js プロジェクトはアットホームに感じることができて、全員がメリットを得られる共有スペースとするため、皆さんのコントリビューションに集中できる環境を構築することに取り組んでいきます。

Ayo.js の提供する価値の全文を読むとなんで Ayo.js を fork しようとしたのかの考え方が少しずつ分かってきます。曰く、

  • 人(Humans) は会議や書類以上に重要なものがある
  • 人(Humans) はAPIベンチマーク以上の重要なものがある
  • 人(Humans) は企業よりも重要なものがある

というコード以上に “人” に注目したポリシーを持ったプロジェクトとして価値を提供したいという旨の事が書いてあります。 Node.js と技術的な所でforkしたというよりもモチベーションやそこで活躍するメンバーに注目した fork になっています。

Ayo.js を fork した理由は何か

色々読むと分かってきますが、ひとえにまだ Node のコアグループ内部が一枚岩になりきれておらず、未成熟な所がある事が挙げられます。考え方が違う人もいれば言う必要がない言葉をうっかり言ってしまうような人もいます。

こういう事でぶつかり合ってしまうことを防ぐために Code of Conduct と呼ばれる行動規範が有ります。

行動規範とは “みんなが居心地よく活動するためのルール” です。言ってしまえば “法律” のようなものです。Node.js の場合はここに行動規範が書いてあります。

github.com

行動規範に則っていないメンバーがいた場合はこの Code of Conduct の Enforcement に従った措置が適用されます。

罵倒や嫌がらせ、またその他受け入れがたい事が発生した場合は report@nodejs.org に連絡することで報告することができます。全ての報告はレビューされ、調査された上でその状況に必要であり適切であるとされるレスポンスを得ることができます。このプロジェクトチームは報告者に対して機密を保護する義務があります。さらに特定の Enforcement ポリシーの詳細は別途報告されることもあります。

プロジェクトメンテナーが行動規範を遵守しておらず、また誠意を持って実施できていない場合はプロジェクトのリーダーシップを有する他のメンバーによって一時的あるいは永続的に追及されることになる可能性があります。

モデレータが Code of Conduct をどうやって施行するかやモデレーションポリシーやモデレーション要求に関してはこれらのリンクで確認することができます。

https://github.com/nodejs/TSC/blob/master/Moderation-Policy.md#moderation-policy
https://github.com/nodejs/TSC/blob/master/Moderation-Policy.md#requesting-moderation

Code of Conduct に沿っていないとなると、モデレーションポリシーに従って issue が立ち上がり、そこでメンバーがどういう行いをしているのか、githubであれば issue が watch される事になります。結果として Github の organization から ban されることもあれば、WGから除名されることもあります。

今回のAyo.jsの fork した流れの中で、「Code of Conductを功績を上げてきたコアメンバーが違反した場合にどうするべきか」という難しい議論が行われています。

アメンバーの一人である Rod Vagg は io.js と Node.js のフォーク/マージ騒動を起こし、最終的に全員をまとめて終止符を打ったメンバーの一人です。 ですが、Rod Vagg の Github 上の発言で特定の個人を攻撃するような発言があったり特定の企業を貶めるような発言があったことが Github 上で報告されています。Github以外でも「アンチ Code of Conduct 」のような発言をしたり、 Twitter 上で煽るような行動が見られていました。

これを受けて、 Code of Conductに従って審査され、その結果として Rod Vagg への除名要求をするかどうかがコアメンバー内のミーティングで議題に上がりました

github.com

しかし上記の issue の通り、 投票の末に反対票 (Rod Vagg の除名を要請しない票) が 60% あったため、結果除名要求されないことになっています。

これを受けて Code of Conduct の moderation policy にも update される流れが起こっています。

github.com

しかし、Code of Conduct が徹底されないとなるとそれはそれで問題です。先程言ったとおり、 Code of Conduct というのは “みんなが居心地よく活動するためのルール” です。たとえ過去の功績がどうであれ特定の企業や個人に対して offensive な態度をとって他者の居心地を悪くする行為は Code of Conduct 違反であり、違反したにも関わらず除名されない事に対して受け入れがたいと思っている人たちはいます。実際この流れで数名が TSC から外れています。

このように Code of Conduct 違反があったにも関わらず徹底できていないガバナンス体制を見限った人たちが発起人となって fork したのが Ayo.js です

Ayo.js では Code of Conduct の徹底だけではなく、ガバナンスモデルもBDFL任期制にするなどの体制変更まで含めて議論が始まっています。

github.com

序文に書きましたが、コードの内容よりも “人(Humans)” にフォーカスを当てているプロジェクトを目指しているため、ガバナンスモデルや CoC といった議論が最初に始まっています。

Rod Vagg 側からの意見

Rod Vagg もこの流れに対して意見を述べています。

medium.com

長文ですが、Rod Vagg としては問題が解決されたと勘違いしていたこと、自分としては人を傷つけるようなつもりで言ったわけではなく、たまたま自分の言葉選びが悪いせいで人を傷つけてしまったこと、またそれとは別に根拠のない話が多く不正に追求されていることを訴えています。

Rod Vaggとしてはこれを認めてしまうと Node.js の今後のプロジェクトにおいて無根拠な訴えでリーダーを解任できるという不都合な前例を作ってしまうため、なんとしても辞任要請を拒否するという姿勢を見せています。

Ayo.js は Node.js に再統合されることがあるのか

現時点ではわかりません。ただ、再統合というゴールもあり得るとは思います。実際それを示唆する話もありますが、今のところは再統合は検討していません。

Node.js 側も TSC でちゃんと価値を表明したり、また TSC (政治と基金面の管理) と CTC (技術開発) に分かれていた組織体制を元に戻して意見統一しやすくしたりといった改善は図られようとしています。

github.com

github.com

github.com

ただ「1つ2つの問題が解決できたら終わり」というような単純な問題ではなく、既に Node の根幹にあるオープンガバナンスモデルの是非にまで話が入り組んでしまっています。個人的には平和的な解決を望んでいますが、意見を無理に集約しなくても OSS には fork するという権利があると思っているので、わだかまりを抱えたまま merge するよりは fork されたままでも良いと思います。

まとめ

io.js の時はES2015を書けるようにするであったり、 LTS であったりと技術的な話を争点として始まった fork でした。しかし、Ayo.jsは政治的な話を争点として始まった fork になっています。ソースコード的な部分では Node.js も Ayo.js も変わりません(現時点では変更点は実行ファイルが node だけじゃなく ayo にも symlink が貼ってあるという点だけです)。

Code of Conduct というのはルールであり、「法律」だと話しました。逆説的ですが法律は違反する人が居なければ必要ありません。法律無しでもフレンドリーで楽しくやれるのであればこういった話はおそらく起こっていません。しかし、 Node.js に限らず OSS という社会は多様性を求めて色々な人から意見を吸収しようとします。色んな文化圏の人が共通のプロジェクトに取り組む際に普通にやっているつもりでもお互いの想像力が少し不足していると、ぶつかってしまうことがあります。

OSS が多様性を要求する以上、想像力を働かせてなるべく offensive な発言を避けたり、うっかり発してしまった場合であってもすぐ訂正して全体が居心地よくするように活動を心がけていく必要があります。

OSS というのはマンパワーやコードだけが重要なのではなく、何かを解決したいというモチベーションが一番大切だと思っています。モチベーションを Node に対する不信感から維持できないと思ってしまう人たちがいるのはしょうがない上に、そういう時であっても OSS には fork するという権利があります。

Ayo.js と Node.js が今後どうなるのかに関しては iojs-jp.slack.com の #ayo でリアルタイムに今 ayo.js で何が起きてるのかを@about_hiroppy が実況してくれています。

iojs-jp slack に入りたい場合は以下の リンクからジョインしてください。

Join node/iojs-jp on Slack!

また Node 学園 27時限目でも同様の話をする予定です。直接質問や話がある場合は是非参加をお願いします。

nodejs.connpass.com

Node.js で発生した Hash flooding DoS とその内容について

Node.js のセキュリティアップデート

7/11 に Node.js のセキュリティアップデートがリリースされました。

Security updates for all active release lines, July 2017 | Node.js

これには複数の脆弱性が報告されており、今回はそのうちの1つの Hash flooding DoS という脆弱性が何なのか、それに対して採用された対策が何なのかについてお話します。

Hash flooding DoS (hashdos)

Denial Of Service 、つまりサービス拒否攻撃の一種です。 JavaScript のオブジェクトは内部的にハッシュテーブルとして表現されています。

図はこちらから引用

f:id:yosuke_furukawa:20170715091518p:plain

ハッシュ関数は同じkeyなら同じ値を返しますが、別なkeyなら通常は別な値になります。

f:id:yosuke_furukawa:20170715091946p:plain

ハッシュテーブルのinsert, get, update, removeそれぞれ、通常時は O(1) でアクセスできることが期待されていますが、例外があります。ハッシュ値がぶつかり、 別なkeyにも関わらず、同じ値になった際(コリジョンした際) に、内部的にリストとして扱い、リストに追加させて持つ、という動きをします。ちなみにこれをハッシュの連鎖法と言います。

f:id:yosuke_furukawa:20170715092104p:plain

ハッシュ関数の偏りを狙って意図的に同じハッシュ値を持つ key を発生させる事で、O(1) で取得できていたはずの key の取得が O(n) となり、 複数(n個)のkeyを一度に取得しようとした場合は O(n2) になります。こうなるとアクセスする度に CPU使用率が向上し、結果として DoS になるという攻撃です。

f:id:yosuke_furukawa:20170715102018p:plain

V8 の Hash flooding DoS 対策 について

V8 の hash はこの Hash flooding DoS に関しては既に対策されており、ハッシュ関数の seed 値を乱数化することで、ハッシュ関数の偏りを推測することは困難になるように設計されています。

その対策は Node.js の過去の対応でも対応されていました。

github.com

しかしながら、今回はこの Hash flooding DoS「条件付きで発生してしまう」 という事になりました。

v8-snapshot という機能

Node.js のJavaScriptエンジンである v8 には heap のスナップショットを取るという機能が実装されています。 JavaScript には builtin object として色々なオブジェクトを作成するため、そもそもそのsetupに時間がかかります。このsetup時間を短縮させるために、 heap のスナップショットを build 時に作って serialized した状態で管理し、起動時にはこれを deserialized する改善が行われています。

これにより、起動時間を短縮する効果を産んでいますが、今回はこの機能が結果として hash の乱数化したはずの seed もserializedしてしまい、対策した結果が無効になる、という問題が起きてしまいました。

ソースからビルドすれば、ビルドのタイミングでは乱数化されるので、hashのseed値を推測することは困難になりますが、 Node.js はビルド済みのバイナリを公式で配布しており、この配布されたバイナリの中にはseed値を含んだsnapshotが入っているため、 Hash Flooding DoS が起きうるということで、 high severity vulnerabirity として対策されることになりました。

セキュリティアップデートでは既にbuild時に v8-snapshot を取らないように対策がされています。

どうしたらいいのか

既にパッチが公開されているのでアップデートして下さい。

Node v8.1.4 (Current) | Node.js

Node v6.11.1 (LTS) | Node.js

Node v4.8.4 (Maintenance) | Node.js

v4 以前の方は早めにv4以上にアップデートをおすすめします。

まとめ

  • Hash Flooding DoS (通称hashdos) の紹介
  • V8 の対策とv8-snapshotの話
  • セキュリティ対策

special thanks

今回はConstさんからv8 snapshotの話の解説をいただき記事にしました。

Node v8.0 がリリースされた

Node v8.0 is released!!!!!!

f:id:yosuke_furukawa:20170605234126p:plain

Node v7 から半年経過して次のLTS対象になる可能性が高い Node v8.0 がリリースされました。 いくつか Notable Changes を話そうかなと。ちなみに Node v8 と言うと内部で使っている JS エンジンの V8 と混同されるので、みんな Node8 とか呼んでるときが多いです。このブログの中ではまだ出たばかりとあって、 v8.0 と minor バージョン付きで紹介します。

LTS 候補

v6.x 依頼の一年ぶりの LTS 候補になります。 LTS になるのは 10月以降と予定されています。 Current から LTS になるためにはコアの成熟を待つ必要があり、リリースから半年経過させる予定です。

https://talks.continuation.io/nodeweek-4-17/images/lts.png

Notable Changes

いくつか変更点をピックアップして紹介します。

  • npm5
  • util.promisify
  • V8 5.8
  • async_hooks
  • N-API
  • buffer improves more secure API
  • WHATWG URL is not experimental

npm5

npm5がNode v8.0にバンドルされることになりました。

yosuke-furukawa.hatenablog.com

先日紹介したとおりですね。Node v8.0 ではnpm5がデフォルトになるのでNode8でnpm installとかやるとpackage-lock.jsonがデフォルトで作られます。 package-lock.json 自身はファイルを作った時にリポジトリへコミットすることを推奨されています。 package.jsonpackage-lock.json が同一階層に並ぶのが Node v8.0 の標準プロジェクト構成ということになるでしょう。

util.promisify

これも先日紹介した util.promisify 関数ですね、 Promise をもう少し Node.js フレンドリーに扱えるようになりました。

yosuke-furukawa.hatenablog.com

Promise自身は unhandledRejection の取扱い等で Node で使うには注意点が必要ですが、 PromiseがNodeで使いやすくなったという変更は朗報と言えるでしょう。

V8 5.8

V8 のバージョンが新しくなりました。これに伴いEcmaScript の新機能やエンジンの最適化で下記の機能が追加されました。

function trailing commas

function trailing commas という機能が追加されました。これは関数の定義と呼び出しでケツカンマを許容するというものです。

function clownPuppiesEverywhere(
  param1,
  param2, // 今まではここのカンマが許容されてなかった
) { /* ... */ }

clownPuppiesEverywhere(
  'foo',
  'bar', // 呼び出し時にもケツカンマ OK
);

String padStart/padEnd

文字列の行揃えをするための関数である、 padStart / padEnd が追加されました。

console.log('hello'.padStart(10));  // '     hello'
console.log('hello'.padEnd(10));    // 'hello     '

個人的には leftpad 問題で一気に有名になり、脚光を浴びた機能なので感慨深いですね。

yosuke-furukawa.hatenablog.com

--harmony でフラグ付きで有効になる機能

--harmony フラグありだと下記の機能が有効になります。

template literal revision (–harmony)

ES2015 の template literal ではバックスラッシュ付きの文字列が与えられた際に「特別な役割を持った文字列」として動いてしまうことが多いです。例えば、

  • \uunicode 文字列のエスケープ用の接頭語 例: \u{1F4A4} や \u004B
  • \x は16進数文字列のエスケープ用の接頭語 例: \x4B
  • \ に数字がつくと、8進数文字列のエスケープ用の接頭語 例: \121

として扱われていたりします。 こうなると、以下の文字は tagged template literal では扱えません。

latex`\unicode`
windowsPath`C:\uuu\xxx\111`

これを解決するのが今回の template literal revision です、これにより、 DSL 用途で使いやすくなります。

// --harmony
function tagFunc(tmplObj, substs) {
    return {
        Cooked: tmplObj,
        Raw: tmplObj.raw,
    };
}

tagFunc`\uu ${1} \xx`
// { Cooked: [ undefined, undefined ], Raw: [ '\\uu ', ' \\xx' ] }

Object rest/spread properties (–harmony)

ES2015 の時点では 配列にしか無かった Rest / Spread 演算子が Object でも使えるようになりました。

  • Rest properties
// --harmony
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x; // 1
y; // 2
z; // { a: 3, b: 4 }
  • Spread properties
// --harmony
let n = { x, y, ...z };
n; // { x: 1, y: 2, a: 3, b: 4 }

これによって、 Object同士を merge するのには assign を使わなくても、以下のように書けるようになりました。

// --harmony
const merged = {...obj1, ...obj2};
// same: const merged = Object.assign({}, obj1, obj2);

async-await

また、v7.x の時点で既に追加されてましたが、 async-await も有効です。

util.promisify と組み合わせてコアの機能を async-await で書くことができます。

const fs = require('fs');
const { promisify } = require('util');
const readFile = promisify(fs.readFile);

const read = async (path) => {
  const result = await readFile(path);
  console.log(result.toString());
};

read('test.txt');

V8 Turbofan and Ignition

V8 に新しく Ignition と呼ばれるインタプリターと Turbofan と呼ばれる新しいJIT Optimizerが追加されました。

ただし、これらもまだデフォルトでは on になっていません。試したい場合は --ignition--turbo を付ける必要があります。

これまでは Crankshaft と呼ばれるJIT エンジンだけでしたが、 Ignition と呼ばれるインタプリターが追加されたことでメモリフットプリントの軽量化とJITの高速化が図られる予定です。

docs.google.com

v8project.blogspot.jp

こちらちなみに jsconf.eu に行った時にV8チームの人達から話を聞いた所、「モバイルでのページ参照の時に低スペックでも高速に動くように検討された結果生まれた」という話を聞きました。 Node では IoT などの環境で動く際には役立つ可能性が高いです。

この話はもう少し詳しく別なエントリで語ります。

async_hooks

Node.js 内部のtraceやdiagnosticに使うための新しいAPIである async_hooks が追加されました。 async_hooks はNode.js 内のイベントループで起きているイベントを監視できるようにするための API です。 まだ、 API 一覧に内容は書かれていませんが、以下の Pull Request には上がっているのでそこから抜粋します。

node/async_hooks.md at 5d7a544bd19d46f24f58c20a4530334ea9ada64d · thlorenz/node · GitHub

以下のコードは async_hooks を使って Node.js 内部のオブジェクトとコールバックの発火有無を監視するためのものです。

const async_hooks = require('async_hooks');
const net = require('net');

let ws = 0;
async_hooks.createHook({
  // Node.js の内部オブジェクトが初期化される際に実行される関数
  // この時点ではまだ実体は存在せず、リソースは確保されていない
  init (id, type, triggerId) {
    const cId = async_hooks.currentId();
    process._rawDebug(' '.repeat(ws) +
                      `${type}(${id}): trigger: ${triggerId} scope: ${cId}`);
  },
  // コールバックが実行される前に呼び出される関数
  before (id) {
    process._rawDebug(' '.repeat(ws) + 'before: ', id);
    ws += 2;
  },
  // コールバックが実行された後に呼び出される関数
  after (id) {
    ws -= 2;
    process._rawDebug(' '.repeat(ws) + 'after:  ', id);
  },
  // 実体が削除された時に呼び出される関数
  destroy (id) {
    process._rawDebug(' '.repeat(ws) + 'destroy:', id);
  },
}).enable();

net.createServer(() => {}).listen(8080, () => {
  // 10ms 待ってから async_hooks を使って今のEventのIDを表示する。
  setTimeout(() => {
    console.log('>>>', async_hooks.currentId());
  }, 10);
});

これを実行すると下記のようなログが出力されます。

TCPWRAP(2): trigger: 1 scope: 1
TickObject(3): trigger: 2 scope: 1
before:  3
  Timeout(4): trigger: 3 scope: 3
  TIMERWRAP(5): trigger: 3 scope: 3
after:   3
destroy: 3
before:  5
  before:  4
    TTYWRAP(6): trigger: 4 scope: 4
    SIGNALWRAP(7): trigger: 4 scope: 4
    TTYWRAP(8): trigger: 4 scope: 4
>>> 4
    TickObject(9): trigger: 4 scope: 4
  after:   4
after:   5
before:  9
after:   9
destroy: 4
destroy: 9
destroy: 5

TCPWRAP や Timer などのNode.js の内部オブジェクトが実行されてる様子が見て取れるかと思います。 これをうまく使えばエラーのデバッグに役立てる情報を出力できます。ログを毎回出したくない場合は enable/disable メソッドを使ってhookをオンオフさせることも可能です。

const async_hooks = require('async_hooks');
const net = require('net');

let ws = 0;
const hook = async_hooks.createHook({
  init (id, type, triggerId) {
    const cId = async_hooks.currentId();
    process._rawDebug(' '.repeat(ws) +
                      `${type}(${id}): trigger: ${triggerId} scope: ${cId}`);
  },
  before (id) {
    process._rawDebug(' '.repeat(ws) + 'before: ', id);
  },
  after (id) {
    process._rawDebug(' '.repeat(ws) + 'after:  ', id);
  },
  destroy (id) {
    process._rawDebug(' '.repeat(ws) + 'destroy:', id);
  },
});

net.createServer(() => {}).listen(8080, () => {
  setTimeout(() => {
   // ここで改めてログを出す。
    hook.enable();
    console.log('>>>', async_hooks.currentId());
  }, 10);
});

N-API

N-API は新しく Native Addon 用の機能を提供するラッパーAPIです。これまでの Node.js ではコアでサポートしているラッパーAPIは存在せず、NANと呼ばれる ライブラリがサポートされていました。

これを利用して、 node-sass や leveldown や imagemagick-native といった Native Addon のライブラリが提供されています。今回からは NAN だけではなく、 N-API というコアが新しくサポートした機能を使って Native Addon を書くことが可能です。

詳しくはここの資料を見ると良いでしょう。

medium.com

また既に N-API を使った leveldown を使って Native Addon を活用するためのデモも存在します。

github.com

buffer improves more secure API

Buffer の API を実行する際に必ず初期ヒープ領域が 0 埋めされるようになりました。 この変更の重要さを語るためにはまず、 Buffer(number) を実行したときの問題点から語る必要があります。

Buffer をコンストラクトするには Buffer(number) のように数字を渡す方法と Buffer(string) などの文字列を渡す方法、配列を渡したり、 TypedArray を渡す方法などの複数の種類がありました。僕がよく使うのは new Buffer(string, encoding) で文字列をエンコードする時でしょうか。 new Buffer(number) を使うとその number に渡したサイズ分のバッファを事前に確保して使うことが可能です。

しかしながら、 以前の new Buffer(number) は初期化段階ではヒープメモリ中の中身を 0 埋めしていません。つまりヒープメモリ中の値の中身を見ようと思えば見れてしまいます。

var token = 'paSsWord!ASD, totally secret!';
for (var step = 0; step < 100000; step++) {
    // ここで token の中身を取れる
    var buf = (new Buffer(200)).toString('ascii');
    if (buf.indexOf(token) !== -1) {
        console.log('Found at step ' + step + ': ' + buf);
    }
}

github.com

昔の ws という WebSocket のためのモジュールがこの問題をもろに引き起こし、 攻撃を引き起こす可能性があるという脆弱性を持っていました。

https://nodesecurity.io/advisories/67

この問題に対処するため、 new Buffer(number) を呼び出す際、初期値を Buffer.fill(0) として0埋めされたバッファとして確保してしまう対応がなされました。

これにはパフォーマンスの問題が伴う可能性があります。つまり、必ずサイズ分の全ての値を 0 埋めする結果、パフォーマンス的に遅延してしまう可能性も考えられます。

セキュリティよりもパフォーマンスを優先させたい場合は専用の Buffer.allocUnsafe メソッドを使って実行する必要があります。

// 必ず最初に 0 で初期化され、安全にメモリアロケートされる
const safeBuffer1 = Buffer.alloc(10);
const safeBuffer2 = new Buffer(10);

// 初期化されずにいきなりアロケートされる
const unsafeBuffer = Buffer.allocUnsafe(10);

それに伴い、 Buffer(number) での呼び出しは pending deprecated 扱いになってます。今後はエラーになる可能性もあるので、 Buffer.allocBuffer.allocUnsafe に移行できるようにしておいてください。

WHATWG URL is not experimental

WHATWG URL は v7 で追加された新しい URL Parser ですが、それの扱いが格上げされ、 stable な API として扱われることになりました。

const URL = require('url').URL;

const myUrl = new URL('/a/path', 'https://example.org/');

v7 の初期までは URLSearchParams などにも対応していませんでしたが、それらのすべてのspecに対応されました。

そのため、以下のコードも正常に動作するようになりました。

const URL = require('url').URL;

const u = new URL('https://jxck.io?log=warn&lang=ja');
const searchParams = u.searchParams;
searchParams.get('log') // "warn"
searchParams.getAll('log') // ["warn"]
searchParams.delete('log') // undefined
searchParams.has('log') // false
searchParams.append('debug', true) // undefined
searchParams.toString() // "lang=ja&debug=true"

for ([k, v] of searchParams) {
  console.log(k, v);
  // lang ja
  // debug true
}

以下より抜粋

blog.jxck.io

他にも

と盛りだくさんです。すべてをちゃんと紹介しきれる気がしないので、いくつか抜粋しましたが、ちゃんと変更点を追いたい方は、以下の記事をご一読ください。

Node v8.0.0 (Current) | Node.js

まとめ

Node.js v8.0 が日本時間の 6/1 朝に公開されました。今回はv8.0の主だった機能を紹介しました。一週間経過して、今のところ問題も散見されておりますが、概ね動作しているように見えます。

ちなみに今週中に v8.1 が出るようなのでその時また少し変わるかもしれません。

ひとまず、自分の目の届くプロダクトでは Node v8.0 を試すことにして、実績を積み、問題があったらフィードバックする形にしていこうと思っています。

この他にも V8 の変更であったり、 N-API であったりは実は別枠で語らないといけないような大きな変更なので、随時紹介して行こうと思います。

npm v5 がリリースされた

npm v5

f:id:yosuke_furukawa:20170530032206p:plain

The npm Blog — v5.0.0

npm に v5 がやっとリリースされました。この npm v5 は既に明日リリース予定の Node v8 にバンドルされる予定です。 かいつまんで、機能を紹介します。

Notable Changes

  • package-lock.json!!!
  • faster than npm v4
  • no more --save option
  • Offline mode
  • sha512 support

package-lock.json!!!

npm v4 まで問題だった npm-shrinkwrap の問題 を解消するための新しい lock ファイルが生まれました。

shrinkwrap は依存ライブラリを固定するための機能です。npm v4 までは shrinkwrap で固定していましたが、新しく npm v5 になってからはshrinkwrap は不要です。

shrinkwrap は現在の自分の node_modules フォルダ以下にある情報を元に shrinkwrap.json ファイルを作成します。 単純に今の自分の依存モジュールのスナップショットとして作るだけならいいのですが、実際には環境の差異で開発中にしかいらないモジュール(devDependencies)やOSX環境ではインストールできたけど、Linux環境ではインストールできないモジュール(optionalDependencies)などがあり、きちんと環境に合わせてshrinkwrapを構築する必要があります。これを回避するために色々 hack していましたが、今回の変更でそれらは全て不要になります。

package-lock.json は package.json に何か変更があったらそれと完全にsyncしてファイルが変更されます。 要は npm installnpm updatenpm uninstall などをした場合は毎回 package-lock.json にも同様の変更が行われます。

facebookのメンバーが作った yarn と同じ動きですね。

もしもこの動きを止めたければ --no-save オプションが追加されているのでそれを付けると package-lock.json には反映されなくなります。 --no-save を付ける時はちょっとだけ試してみたい時ですね、基本は何も付けずに npm install foobar とやるだけで lock ファイルもpackage.json も一緒に更新されていきます。

faster than npm v4

npm v4 よりも高速化されました。試しに手元で 適当なモジュール を使った所、下記のような結果になりました。

  • npm4 3.14sec
  • npm5 1.52sec
  • yarn 0.78sec
  • pnpm 0.75sec

手元では pnpm > yarn > npm v5 > npm v4 の順で速いのが観測されました。ただ正直 pnpm と yarn の差は誤差の範囲内です。 高速にはなりましたが、 npm5 と yarn, pnpm はまだ yarn, pnpmのが高速です。

こちらの資料にはもう少し詳細なデータが載っています。

docs.google.com

no more --save option

npm install でモジュールをインストールする際に --save オプションがデフォルトで付くようになりました。つまり、何らかのモジュールをインストールした場合デフォルトで package.json に変更が入ります。

--save-dev もしくは -D オプションを付けてインストールすれば devDependencies に入ります。--save-optional もしくは -O をつければ optionalDependencies に入ります。

これらモジュールをインストールした時には必ず package.json とともに package-lock.json にも変更が入ります。

offline mode

npm install --prefer-offlinenpm install --offline などのモードが追加されました。

--prefer-offline をつけると、npmのcacheがstaleしているかどうかを確認するためのrequestを発行しなくなり、今持っているローカルキャッシュを優先して実行するようになります。無かったら npm リポジトリに取りに行きます。

--offline をつけるとローカルキャッシュからしか取りません。もしもローカルキャッシュに見つからなかったらエラーになって終わりです。

この offline mode自身は「高速化というよりもnetwork 利用率を下げるために使っているオプションで、直接速度に影響するものではない」とのことを npm の中にいる Kat Marchan から教えてもらいました。

sha512 support

これまで sha-1 でハッシュを計算していましたが、 sha-1 はcollision の懸念もあるためよりcollisionが起きにくいsha512もサポートされることになりました。

現時点では、 sha512 と sha1 両方で併用される形になるようです。 sha512 だけにすると既存モジュール全てハッシュ値を再計算する必要があるからだと想定されます。なので既存のsha1も併用する形になるんじゃないかと。ただ npm v5 以降からは sha512 のフィールドに値が入っていれば優先的にチェックされます。

この他にも

cache の機能がいくつか deprecated になっていたり、 package.json や package-lock.json がデフォルトでインデントがされるように改修が入っています。

まとめ

npm v5 のリリースに関してざっくり紹介しました。 yarn や pnpm の速度は魅力的ですが、 npm v5 は Node v8 にバンドルされて標準的なツールとして組み込まれる事になるでしょう。まだ v5.0.0 が出たばかりですが、今のうちからでも npm i npm@5 -g で入れて試してみるのも悪くないでしょう。

util.promisify が追加された

Node.js のコアに util.promisify が追加された。 github.com

今回は util.promisify が持つ役割を中心に Node.js における Promise の立場についても話していけるといいと思う。

util.promisify とは

読んで字のごとく関数を Promise に変換してくれるユーティリティメソッド。 下記のような要領で変換できる。

const util = require('util');
const fs = require('fs');

const stat = util.promisify(fs.stat);
stat('.').then((stats) => {
  console.log(stats);
}).catch((error) => {
  console.error(error);
});

async-awaitを使いたい場合(Node.js v7の最新では既にenabled)は下記の通り

const util = require('util');
const fs = require('fs');

const stat = util.promisify(fs.stat);

async function callStat() {
  try {
    const stats = await stat('.');
    console.log(stats.uid);
  } catch(e) {
    console.error(e);
  }
}

util.promisify 注意事項

Node.js のコアメソッドに限らず、他のメソッドもPromiseに変換できる、ただし、変換する場合はその関数がpromisifyの規約に従っている必要がある。

その規約というのは、

  • コールバック関数を引数の最後に取ること ( function(arg1, arg2, cb){} )
  • コールバック関数の最初の引数はエラーであること ( cb(err, res) )

である、これに従わない関数の場合はうまくPromisifyされないので要注意、特に2つ目の規約に違反しているとエラーじゃないものがPromise.reject の対象になってしまうことがある。

Node.js のコアメソッドのほとんどは上記の規約に従ったコールバック関数を取るが、Node.jsの規約から外れたコールバックの使い方をしているメソッドの場合は一工夫が必要になる。

例えば、 setTimeoutsetImmediate の場合がそうなる、これらはコールバック関数を最初の引数に要求するし、コールバック関数の最初の引数はエラーとは限らない。

こういった関数を Promise に変換したい場合は util.promisify.custom をプロパティにしてカスタマイズされたPromisify関数を提供してあげる必要がある。

const test = function(cb, arg){ cb(arg) }
test[util.promisify.custom] = (arg) => { return new Promise((resolve, reject) => { test(resolve, arg); }) };

const p = util.promisify(test);
p('foo').then((arg) => console.log(arg));

Node.js の setTimeoutsetImmediate はこの util.promisify.custom を使ってcustomizeされたPromisifyを作っている。

Node.js における Promise の位置付け

フロントエンドでは async await が採用されたり、 Web 標準のAPIが採用していたりと、ほぼスタンダードな印象を受ける Promise だが、 Node.js の中では実はまだまだ議論の余地がある。

util.promisify を採用するかどうかを議論していた時にその場に居たので、要点をまとめると

unhandledRejectionの時の振る舞いが決まっていない

という一点につきる。

議論で話している感じは以下の通り:

  • Promise のニーズは高い、特に async await のような構文サポートまであるので強力
  • しかしながら、Promise の unhandledRejection が起きた時にNodeのデフォルトをどう動かすようにするかが未定
  • 現時点では warnings が出る。しかし、今やってる unhandledRejection はただ単に例外発生時に .catch をする Promise が その時点で いなかっただけであり、Promiseが例外をキャッチするのは仕様上いつでも良いので、この時点で出るwarningsとしては適切ではない(非同期にキャッチされる可能性があるため)。
  • 現在の仕様で Promies を使ったとして、例外をキャッチしなかった際に容易にメモリリークやファイルディスクリプタのリークが起きることは想像しやすく、やはりNodeコアの中でも簡単にリークが作り込めてしまうような状況にするべきではない、リークが気づきにくくなる位なら異常終了した方がマシ

というのが議論ポイントだった。

もう少し噛み砕くと、『 Promise を簡単に使えるようにする(util.promisifyを提供する)なら、 Promise を安全に使える手段として提供してあげる(リークを起こさないようにする)べき』という感じだろうか。

これに対しては現時点で提案中のデフォルトの unhandledRejection の動きで既に3,4候補存在する。

unhandledRejection が起きたら:

まだこの部分の議論は続いている。

Node.js Collaborators Summitにおける、約一時間の議論は発散して終わった感じがするが、 util.promisify を追加する件に関してはある程度の有用性、コアでやることの意義が認められてマージされた。

Promise とどう付き合っていくか

僕らアプリケーションをNode.jsで書いている側としては気をつけるべきなのは、 Promise が使いやすくなってきているが、まだまだ運用面での知見が少ないという点だと思う。

もしかしたらリークが起きてるけど気づいていないとか、例外がスローされていたけどそのまま放置されていたとかそういう事がないように Promise は気をつけて使うべきだろう。

実際に Node.js v4 では Promise でメモリリークが起きていた(現在は修正済み)

更に言うと、現時点の Promise には core-dump を出す仕組みもない(processが死んだ時の --abort-on-uncaught-exception 相当)。Promiseを使ってしまうとエラーになって死んだ時に解析がしにくいという側面もある。

www.joyent.com

util.promisifyができたことで、Promiseが使いやすくなっているが、この辺りはまだ仕様検討中なのでv8.0次第ではPromiseの使い勝手は変わる可能性もある。

2017/05/12 追記: Promiseを無限ループさせるとリークが起きるというのは仕様の問題であって、Node.jsの問題ではありませんでした。

まとめ

  • util.promisify の説明
  • Promise と Node の位置付け
  • Promise とどう付き合っていくか

node の security checkをするなら nsp が便利

nspとは

先日たまたま会社で Vulnerability の話になって色々と Node.js だとこういうのあるんですよって言ったら知らなかった方も多かったので紹介。 nsp は node security platform の頭文字を取ったプロジェクトである。

Node Security Platform はサイト上で脆弱性を公開している。 Node.js のコアの脆弱性というよりも npm モジュールなどのモジュールの脆弱性だ。

nsp に挙げられてる脆弱性の一例

例えばこの脆弱性なんかは2017年2月11日に公開された脆弱性である。

https://nodesecurity.io/advisories/313

github.com

どういう脆弱性かというと、このモジュールはJavaScript Objectをシリアライズするためのモジュールだが、そのserializeする時に関数までも変換してくれる、JSONよりも少しだけやってることが複雑である。問題はdeserializeする時で、deserializeする時はnew Function 等で括って eval として関数を実行している、こうすると不正な即時関数 {e: (function(){ eval('console.log("exploited")') })() } をserializeしたオブジェクトが渡された場合にdeserializeした側の環境で勝手に実行されてしまう。この例題コードはconsole.logだから良いが、child_processのexecFileやらなんやらがサーバで実行されたら目も当てられない。

さて、この手の脆弱性は実は週単位のペースで上っている。これをいちいちチェックしてたらキリがない。ツールで自動化させようというのがこの nsp である。

nsp 使い方

インストールはとりあえず簡単。

$ npm install nsp -g

別にローカルモジュールに入れて npm run security とかでチェックできるようにしても良い。

$ nsp check

(+) 2 vulnerabilities found
┌───────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│               │ Code Execution Through IIFE                                                                                                                                                     │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Name          │ serialize-to-js                                                                                                                                                                 │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Installed     │ 0.5.0                                                                                                                                                                           │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Vulnerable    │ <=0.5.0                                                                                                                                                                         │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Patched       │ >=1.0.0                                                                                                                                                                         │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Path          │ server-timing@1.1.0 > serialize-to-js@0.5.0                                                                                                                                     │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ More Info     │ https://nodesecurity.io/advisories/313                                                                                                                                          │
└───────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌───────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│               │ Regular Expression Denial of Service                                                                                                                                            │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Name          │ ms                                                                                                                                                                              │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Installed     │ 0.7.0                                                                                                                                                                           │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Vulnerable    │ <=0.7.0                                                                                                                                                                         │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Patched       │ >0.7.0                                                                                                                                                                          │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Path          │ server-timing@1.1.0 > ms@0.7.0                                                                                                                                                  │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ More Info     │ https://nodesecurity.io/advisories/46                                                                                                                                           │
└───────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

これだけ。これだけでshrinkwrap もしくは package.json の定義を眺めて脆弱性のあるモジュールがないかを Node Security Platform の API に投げて確認してくれる。

もう少し凝った使い方をしたければ、 .nsprc に 例外ルールを追加して無視することも可能。

{
  "exceptions": ["https://nodesecurity.io/advisories/12"]
}

もちろんこれだけで脆弱性が防げるわけではない。これは自分の依存モジュールに脆弱性が報告されていないことを見つけるための道具でしか無い。

自分のサイトの脆弱性や自分のモジュールがうっかりSQL Injectionしていたなんて事にならないようにしたい。

ちなみに yarn 対応なんかはまだの様子。

github.com