Node.js における設計ミス By Ryan Dahl

Ryan Dahl は Node.js の original author ですが、彼の作ったプロダクト deno に関するトークが jsconf.eu 2018 でありました。 Node.js にずっと関わってきた僕が見て非常に興奮するような話だったので、しばらくぶりにブログに書き起こすことにしました。

背景

Ryan Dahl は2009年に Node.js の話を初めて公の場に公開しました。その時の「公の場」というのが「jsconf.eu 2009」です。

www.youtube.com

Video: Node.js by Ryan Dahl - JSConf.eu - 2009

この発表から Node.js が広まり、今やサーバのみならず、IoTデバイス、デスクトップアプリなど、様々なところで動作しています。

で、今回はその発表から9年の歳月が経過し、Node.jsに対しての設計不備について Ryan Dahl 自ら発表したという状況です。

2018.jsconf.eu

発表資料: http://tinyclouds.org/jsconf2018.pdf

動画:

www.youtube.com

今回の記事はこの話を超訳したものを紹介し、慣れてない方のために都度解説を挟みます。最後に古川の感想を書いて締めようかと思います。

当初のゴール

私 (Ryan Dahl) はNode.jsの初期開発と開発マネージメントを行っていました。当初の目標は「イベントドリブンなHTTP Serverを作れるようにすること」でした。 ある時点から Server side JS というゴールにスイッチしていきました。Server side JSにはイベントループモデルを取り入れることに成功しました。

WindowsでのIOCPと Linuxでのepoll、 OSXではkqueueを融和させ、libuvを作った事、それをcoreのJSレイヤでサポートしたり、npmを作ってユーザのコードを管理したりという一定の成功は得られました。

私は2012年にリーダーを引退し、Node.jsの開発を引き継ぐことにしました。『2012年の時点でもう既にゴールは達成された』と思っていました。しかし、今現在2018年にNode.jsを半年間使ってみたところそれは勘違いでした

Node に残っていたmission criticalなタスク

2012年頃にIsaacにリーダーを引き継いだ後、実際はまだまだいくつかのcriticalなタスクが残っていました。 それらのタスクは消化され、今のNode.jsは当時よりも良いプロジェクトになっています。

  • npmをnodeのcoreの中に入れること by Isaac
  • N-API というbinding API
  • libuv の構築 by Ben and Bert
  • governance と community の管理 by Mikeal Rogers
  • crypto周りのコードベースの大幅な改善 by Fedor Indutny

この他にもいろいろな物がありましたが、現メンバーの力によってかなり大幅な改善がされています。

しかし、私は「現時点の Node.js を半年間使ってみたが、自分の目的とは異なったものになってしまった」という感想を持っています。

mission critical なタスクはいくつかは解消されていましたが、いくつかのタスクはそもそもの設計の根幹に関わっており、解消しきれていませんでした。 このあとの話はそれらをリストアップし、どうやって deno が生まれたかについての話になります。

動的型付け

動的型付け言語は科学的計算を一度だけ行うのにはベストな言語です。 JavaScript は動的型付けの言語の中でもかなり良い方の言語だと今でも思っています。

ただ Node.js においては複数回トライエラーを繰り返しながら設計されることを想定しています。設計過程で起きたNode.jsのエラーの内容はわかりにくく、エラー時の解消方法もはっきりしていません。

Node.jsのこういう側面を見るたびに黒板を爪で引っ掻いた音を聞いたときのような嫌な印象を受けました。

せめて今ならもっとよくできるのではないか、という思いを持ちました。

古川注釈: 静的型付け

おそらく静的型付け言語のがエラーの内容がわかりやすく、解消方法もわかりやすいと言いたいのではないか、特にTypeScriptのように変換する事が現代は割と一般的なので。

Promise

Promiseは2009年に一度Node coreに記述されていました。しかしながら、2010年にはそれらをすべて消すということを決定しました

今でも愚かな決断だったと思っています。

async/await の抽象化を行うためには Promise は最初から入れておくべきでした。

Node.js のコアではPromiseが無いために今日まで非同期に関しての体験を悪くしてしまっています。

古川注釈: Promise

2009, 2010年のPromise騒動は覚えていて、Promiseを入れるという事が行われていたにも関わらず、当時はcallbackのほうがprimitiveでわかりやすく性能面でも利点が大きいという事になり、Promiseにするのはcoreの実装では不要という判断がされていました。

当時はここまでPromiseが今後のキーになるとRyanは思っていなかったのでしょう。

Security

V8それ自身はsecureなsandboxモデルを表現しています。この点についてもっと深く考察しておけば、もっと良いセキュリティの考察を得られ、それによって他の言語よりもより良いセキュリティを提供できたと思ってます。

例えば: ただのlinterなのに、networkアクセスやファイルへのフルアクセス権は不要ですよね。

古川注釈: Security

V8それ自身はただJavaScriptの実行エンジンであってファイルシステムへのアクセスやネットワークアクセスする機能は提供していません。標準出力を行う console.log ですら V8の対象の外です。それに対してNode.jsは fshttp といったファイル・ネットワークリソースへのアクセスを提供します。ただ現在のNode.jsの幅広いユースケースを鑑みると、Linterやbuildツールとして実行しているときにネットワークリソースへのアクセス権は不要であったり、ファイルの書き込み権限は不要であったりします。

これらをsandboxの中で適宜パーミッションを得ながら実行できればもっとより良いセキュリティモデルを提供できた、と言いたいのでしょう。

Build System (GYP)

GYPは大きな失敗でした。ビルドシステムは思ったよりも難しくて重要な根幹のシステムでした。

そもそもv8がgypを採用していて、Node.jsもその仕組に乗っかりましたが、その後 gyp から gn に build システムは移り、gypユーザーは取り残されてしまいました。

gyp のインタフェースはそこまで悪いものではないですが、JavaScriptのプロジェクト内で「JSONっぽいけどJSONじゃないPythonのsyntax」を使わされ、ユーザーにとってはひどい体験だったでしょう。

このまま gyp を未来永劫使い続けるのは Node.js のコアにとっては大きな失敗です。 V8 の C++ bindings を書くなら、今は FFI (Foreign Function Interface) を推奨します。

かつて、 FFI を推奨してくれた人たちが居ましたが、当時の私(Ryan)はそれを無視しました。

(ちなみに、 libuv が autotools をサポートしたことに関しては今でも残念に思ってます。)

古川注釈: gyp

FFI と gyp に関しては FFI のがポータブルである一方で当時はgypのが高速という事が言われていました。性能を優先したデザインを取ってgypをサポートしていました。

しかし、 gyp は python2 ベースですし、そもそも Chrome や v8 開発のために作られたビルドシステムです。Chrome や v8 のビルドシステムも現在は gn や bazel というビルドシステムに置き換わっていて、 Node.js がgypを使い続けるのはアーキテクチャ上負債になっています。

コアが特定プロジェクトのビルドシステムに依存するよりは FFI のような統一的な呼び出し方法のがまだ良かった、と言いたかったのではないかと思います。

(ちなみに Ryan は常に何かのビルドシステムに乗っかっては「失敗だったから変えるわ」って言うタイプで昔はWAFというビルドシステムにのって、それからgypに変えています。)

package.json

Node.js は package.json の中にある "main" フィールドを読んでそれを Node.js に require() でモジュールとして読み込ませることにしました。

最終的には npm は node.js の中のreleaseに含めることに成功し、それがデファクトスタンダードになっています。しかしながらそれは中央集権リポジトリを生み出してしまいました。

require("somemodule") と記述するのは明確な特定のモジュールを示しているわけではありません。 ローカルに定義されたモジュールの可能性もあります。npmのデータベースにあるモジュールなのかローカルに定義されたモジュールなのかは実は呼び出しているだけではわかりません。

f:id:yosuke_furukawa:20180604213609p:plain

また、 package.json はファイルを含んだディレクトリをモジュールの概念として扱っています。これは厳密的に言えば必要な抽象化ではありませんでした。 Web 上には少なくともその抽象化されたモジュールは存在しません。

また、不要な情報を多く含んでいます、ライセンス、リポジトリURL、description、こういう情報はプレーンなモジュールにおいては noise です。

import した時にファイルとURLsが使われていれば、パスにバージョンを定義できます。依存関係のリストも不要です。

古川注釈: package.json

いくつか示唆に富んだ話ですね。解説が難しいです。ここの話からモジュールの話やpackageの話が多く存在します。 現地で聞いていたときの印象としては Ryan Dahl は module は package.json 以下にあるディレクトリを指すのではなく、コアが提供するのは単一ファイルで十分なのではないかと思っているんだと思いました。

import の時に import foo from "https://example.com/foo/v2/index.js" とかで url で指定させたり、 import foo from "./foo/v2/index.js" などのファイルで指定できるようにするだけでよく、ディレクトリを指定させるのはアプリケーションごとにやれば良いと思っているのかと。

node_modules

f:id:yosuke_furukawa:20180604215229p:plain

moduleの解決アルゴリズムが相当複雑になってしまいました。 vendorのモジュールをデフォルトにした事は良いことではありますが、実際には $NODE_PATH は使われていません。

ブラウザのsemanticsからも外れてしまいました。

これは私のミスです、本当に申し訳ありません。でももうやり直すことは不可能です。

古川注釈: node_modules

module の解決アルゴリズムは node_modules 内の依存関係をたどって依存解決をします。また、 moduleは指定元からの相対パスでファイルを指定しますが、 $NODE_PATH 環境変数にパスがセットされているとそこからも読み込まれます。

この他にもいくつか hacky な方法がありますが、どの方法も微妙です。この状況を招いたのは node でもっとシンプルな方法を提供できなかったせいだと Ryan は語っていました。一方で今のNode.jsの仕組みをブラウザに逆輸入されているため、この意見については反対意見もあるようです。

".js" の拡張子なしで module を読み込ませられるようにしたこと

いわゆる require("somemodule").js の拡張子がなくても Node.js はモジュールとして読み込み可能です。 でも browser では src の指定に対して .js を省くようなことはしません。

module loader はユーザーの意図を表現するファイルシステムへのクエリーであるべきで、明示的な指定のが良いです。

index.js

index.js は require("./") で読み込めますが、これは require("./index.js") の省略です。 最初はこれが良いアイデアだと思ってました。ブラウザも //index.html を省略できます。

しかしながら、これも module loader を多少複雑にしました。

Node.js module 設計ミスサマリ

私の失敗の多くは module に関する部分です。どうやってユーザのコードを管理するかという部分の失敗が多くありました。

なぜこの状況になったかというと、イベントI/O に集中して注力してしまい、結果としてモジュールの設計を後回しになってしまった事にあります。

この重要性に早く気づいていればもっとこの状況をよくできたと思っています。

Deno

私はこれまでの設計ミスに基づいて新しいプロダクトを開発しました。それが deno です。

github.com

最初に言っておくと、全然まだまだプロトタイプレベルです。 動かないのが普通の状況なので、 lldb でデバッグして直したりしない限り動かないし、ましてやこれでなにか作るのは強くオススメしません。 (※ ちなみに古川はまだ osx で起動どころかビルドに成功していません。)

deno は v8 で動くセキュアなTypeScript 実行環境です。

Deno のゴール: Secure Model

deno は sandbox モデルになっています。デフォルトではネットワークアクセスもなければファイルの書き込み権限もありません。 (--allow-net --allow-writeをつけない限りはネットワークアクセスと書き込み権限が付きません。)

ユーザーが信頼していないツールをちょっと動かすみたいなケースではこれはセキュアなモデルです。(例えばlinterをちょっとだけ動かすとか)

また、 deno では「ダイレクトに任意のnative codeを実行すること」は許可していません。全ては protobuf の呼び出しによって間接的に実行されます。

以下の図を見てください。

f:id:yosuke_furukawa:20180604215242p:plain

古川解説: Secure Model

deno は OS のカーネルのごとく、 特権モードとユーザ空間を明示的に分けてるデザインなんですね。非常に面白いです。 Goの中で v8-worker と呼ばれる worker を起動して、 worker と main process の間で protocol buffer を経由して通信して特権のコードを実行するか決めてるんですね。

(面白いけど、ここまでのセキュアなものが本当に必要なのかは不明ですね。一方で最近 npm の脆弱性も増えているので必要な面もわかります。)

Deno Goal: モジュールシステムのシンプル化

Nodeの既存モジュールとの親和性は求めてません。

importは相対パス絶対パス、もしくはURLだけでしか指定できません。

import { test } from "https://unpkg.com/deno_testing@0.0.5/testing.ts"
import { log } from "./util.ts"

import には拡張子は必要で、基本的に一度読んだらキャッシュしますが、キャッシュを強制的に開放するときは --reload をつけて実行します。

vendoring に関してはデフォルトのキャッシュディレクトリの指定をしなければ実現できます。

古川解説: モジュールシステムのシンプル化

ファイルをモジュールの基本的な単位にするし、node.jsのnpmのことは完全に忘れて新しくするという潔さ。 また、 import には URL かファイルの相対・絶対パスしか用意しないので、 vendoring というかバージョン管理や固定は不要ですね。ファイルならそのままgitで管理できているし、URLの場合は絶対パスなので取得先が壊れたりしない限りは(理想的には)固定されます。

まぁただ本格的に使うなら vendoringとかpackage management 用の何かの仕組みを3rd partyが作ったりするんでしょうけど。 vgo とか bundler のように。

TypeScript コンパイラ

私はTypeScriptが大好きです。

TypeScript はとても美しく、プロトタイプレベルから巨大なシステムになっても構造化を保つことができます。

denoではTypeScriptのコンパイラをモジュールの参照解決とビルド成果物のインクリメンタルキャッシュに利用します。 TypeScriptのモジュールは変更されてない限りは再コンパイルしません。 通常のJavaScriptも使えるようになります(TypeScriptはJavaScriptのスーパーセットなのでそこまで難しくはありませんが)。

スタートアップを高速化するためにv8のsnapshotも利用する予定です(まだプロトタイプには入ってません)。

古川注釈: TypeScript コンパイラ

Ryan Dahl が TypeScript が好きなのは一つ前のRyan Dahl プロダクトである propel が TypeScript なところからも感じていました。ここはそこまで驚きはないです。 何個かのアプリケーションを作るうち、Ryan Dahl も型が必要だと思ったのでしょう。

v8 snapshot は ここで解説しましたが、所謂heapのsnapshotを事前に取っておいて起動を高速化するためのテクニックですね。

single executable file と最小限のリンク

deno それ自身が最小限の構成をすることを目指しています。 ldd で見てみると7つ程度しかファイルがありません。

 > ldd deno
linux-vdso.so.1 => (0x00007ffc6797a000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f104fa47000)
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f104f6c5000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f104f3bc000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f104f1a6000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f104eddc000)
/lib64/ld-linux-x86-64.so.2 (0x00007f104fc64000)

2018年であることを活用したモデル

Nodeのモジュールをコンパイルするのに Parcel をバンドルツールに使っています。Nodeで行っている事よりも最大限にシンプルです。

github.com

native code の中に 良いインフラストラクチャを作っています。httpサーバについても心配する必要はありません。既に動作は確認できています。 (Nodeの時はWeb Serverは今は手で自分で作る必要がありましたが、今は違います。)

また現時点ではJS以外のパートは Go を使っています。しかし、これはGoですべてやるという事でもありません。今は色々調査しています。 Rust や C++ も良いでしょう。

その他諸々

  • キャッチされない例外がPromiseで起きたら即座にシャットダウンします。
  • top-level await をサポートします。
  • 機能的に重なる場合はブラウザとの互換を取ります。

古川の感想

deno はまだまだプロトタイプレベルですが、今のNode.jsにもフィードバックできるところは多そうだなと思いました。そういう意味では使ってフィードバックをNode.jsにしていこうとは思いました。しかし今時点では積極的に使おうにも lldb 等のデバッガなしでは使えないので何かを作れるようになるのは先だなと思いました。

またNode.jsにおける module とは package のことであり、ディレクトリを指してます。一方で ES Modules や deno における module というのはファイルの事であり、最低限の単位しか持ちません。この時点で考え方において大きな違いがあると思いました。

Node.jsがディレクトリを単位とし、 package.json という定義ファイルがあったからこそ議論を進めた一方で、言語やコアのベーシックな機能として持つ最小限の module というのが何なのか、何であれば良かったのかについて考えさせられました。

いずれにせよ 9年越しに Ryan Dahl の話を聞けて大分面白かったです。他の jsconf と Node Collaborators Summit の話も面白かったのでいつか書きます

NaN === NaN が false な理由とutil.isDeepStrictEqual

NaN === NaN は false

NaN、つまりは Not a Number 同士の同値比較が false になるのは、よく JavaScript とかで罠だと言われていますが、罠でもなんでもないです。 false が返るという仕様です。仕様の経緯を追うとすぐに『 IEEE754 という浮動小数点の標準規格で決められているから』、という理由がヒットします。

では IEEE754 ではなんで NaN == NaN を false にしようという話になったのか、というのを調べてみました。 今回はそういう歴史の話です。

IEEE754

現在のプログラミング言語の処理系の多くが採用している浮動小数点の標準規格です。

この標準規格は以下のことを定義している。

- 基本形式: 二進および十進の浮動小数点数データの集合。有限な数(符号付ゼロと非正規化数を含む)、無限、特殊な「数ではない」値(NaN)から成る。二進形式3種類、十進形式2種類で、計5種類の基本形式が存在する。
- 交換形式: 浮動小数点数を効率的かつコンパクトな形で交換するのに使われる符号化形式(ビット列)
- 丸め規則: 算術や変換の際に数を丸める方式(端数処理)。5種類
- 演算: 基本形式に対する算術演算や他の演算
- 例外処理: 例外的状態の通知(ゼロ除算、オーバーフロー、その他)。5種類

またこの規格では、高度な例外処理、追加的な演算(三角関数など)、式評価、再現可能性などを強く推奨している。

wikipediaより。

wiki項目の中で NaN だけは特別な記述がされています。

NaNとの大小比較では、自分自身と比較した場合でも「大小不明な結果」を返す。

NaN - Wikipedia

ただ厳密に言うと、 NaN には signaling NaN と quiet NaN という二種類あり、 JavaScript の NaN はquiet NaN という 「NaNを比較した時に例外を上げない代わりに必ず false になる」というものですね。

なんで false なのか、という経緯

標準規格で決まっているのはわかったものの、なんで NaN === NaN を false にしたのか、というそもそもの経緯についても調べてみました。 NaNとは、『数学的に数字ではない、とされている値を計算機上で扱うときの便宜的な値』です。比較不可能な値同士を比較したという事でその時点で本来的には例外です。

ただし、 JavaScript では quiet NaN が採用されているため、例外はスローされず、 false になります。

同じように Infinity という『数学的に無限を表す便宜的な値』もあります。Infinity === Infinity は true になっていますが、これにもちゃんとした理由があります。

IEEE754 で表現しているのは丸めも含めた"近似値"になります。

IEEE754-2008 という改訂版では、 +∞や-∞への丸めも記述されています。

方向丸め
- +∞への丸め 正の無限大に近い側へ丸める。切り上げ (rounding up, ceiling) とも呼ばれる。
- −∞への丸め 負の無限大に近い側へ丸める。切り下げ (rounding down, floor) とも呼ばれる。

なので、 Infinity も他の数字と同じく丸められた近似された値です。『無限大』というNumber.MAX_VALUEですらない、上限を表すための近似値のため、 Infinity === Infinitytrue になる、それに対して、 NaN は近似された値ですらありません

近似されてもいない数字ではないもの同士の比較に対して、『 NaN === NaN が見かけ上同じものだからという理由で true になるのはおかしく、 false であるべき』、というのが IEEE 754 での主張です。ここまでが NaN === NaN が false な理由です。

+0-0 の比較演算

さて、 === の比較だと厳密等価という値比較になり、 NaN同士 を比較した場合は false になるというのは前述の通りですが、=== にも多少微妙な数字が有ります、それが +0 と -0 です。

IEEE754において、比較演算では +0-0 は等しいとされており、 +0 === -0 は true になります。しかし、実際の2進数上の表現は異なります(符号ビット部分)し、 1.0/0.01.0/-0.0 で得られる値も異なります(前者は Inifinity, 後者は -Inifinity )。ほとんどのケースでは +0 === -0 が trueでも困りませんが、比較以外の演算の時のみ+0と-0は分かれて扱われており、それらを無限小(無限大の逆)として扱うのであれば +0 === -0 が trueになるのはおかしいという話もあります。

IEEE 754における負のゼロ - Wikipedia

ES2015で追加された Object.is というのはこれを正しく処理するためのものです。 +0 と -0は Object.is() では false になります。

※ ただし、 NaN 同士の比較でも true になります。 Object.is(NaN, NaN) => true

IEEE754の制約を受けずに機能的に同一の値(SameValue)であれば true になる関数という事ですね。機能的に同一、という言葉だけでは分かりにくいのですが、要は NaN を含んだ配列で NaN が存在するかをチェックしようとするケースや、 Object.definePropertyで -0を指定するケースで利用するものです。

[1, NaN, 3].indexOf(NaN) // 必ず -1
[1, NaN, 3].findIndex((e) => Object.is(e, NaN)) //  1

Object.is は一般的な開発者が使うというよりも多少メタな領域で JavaScript を拡張したいライブラリーとかが使うもの、という認識ですが、憶えておいて損はないです。特に +0-0を分けておきたいときには有用です。

util.isDeepStrictEqual とは

object 同士の内容を比較する便利関数です。 v9 から追加されています。Node.js Advent Calendarでも紹介しました。

qiita.com

で、この util.isDeepStrictEqual が多少変な動きをするので、議論を重ねていたら、 NaN === NaN が false な理由とか JavaScript の同値には3種類あるとかそういう沼にハマって調べていた、というのがこの話を書こうと思った経緯です・・・(長かった)。

このツイートの元になったのは、 util.isDeepStrictEqualWeakMap|WeakSet の時に内容がどうであれ必ず true を返すという動きをするためです。

x y === Object.is util.isDeepStrictEqual
1 1 true true true
"foo" "foo" true true true
NaN NaN false true true
Infinity Infinity true true true
+0 -0 true false false
{ foo: 1} {foo: 1} false false true
{ foo: 0} {foo: -0} false false false
new Set([1, 2, 3]) new Set([1, 2, 3]) false false true
new Set([1, 2, 3]) new Set([4, 5, 6]) false false false
new WeakSet([{foo: 1}, {foo: 2}]) new WeakSet([{foo: 1}, {foo: 2}]) false false true
new WeakSet([{foo: 1}, {foo: 2}]) new WeakSet([{foo: 3}, {foo: 4}]) false false true

util.isDeepStrictEqualに関しては、Strict Equal という名前がついているので、厳密等価性 (=== )を表現するのかと思いきや、そうではなく、機能的に同一であるという方の Object.is と基本的には同じ動きをします(注意点1)。型が objectMapSetArray の時は中身を deep に比較するという動きを見せますが、 WeakMapWeakSetの時は内容が取れないので、 valueOf の値が空オブジェクトになるため、中身がどうであれ必ず true が返ってきます(注意点2)。

この動きについていくつか issue を上げてみて、様子を伺っていますが、 TSC としては議論中です。将来的に動きが変わるかもしれないので、v9時点では util.isDeepStrictEqualをヘビーに使うのは推奨しません。

個人的には StrictEqual という名前から想起しやすい動きをしてくれたほうが良いので、NaN 同士や +0, -0の動きも === と同じ動きのが良いです。このままにするのであれば、 util.isDeepSameValue という名前にしてほしいと思ってます。

WeakMap/WeakSet に関してはGCに依存した動きをしますし、そもそもコレクション同士を比較できるものじゃないので true よりは false もしくは例外をスローするか undefined みたいな特殊な値のが正しそうだな、と思っています。

参考資料

2017年振り返り

2017年振り返り

毎年激動の年ですが、今年も色々あったので振り返りをしていきます。

会社

リクルートテクノロジーズに転職して1年、マネージャーになりました。

codeiq.jp

正確にはマネージャーとシニアソフトウェアエンジニアの兼務なのですが、マネージャー側面の話はあまりしたこと無いのでこの CodeIQ のイベントは貴重でしたね。

チームビルディングの記事も書きました。 recruit-tech.co.jp

会社で仕事してるの?って聞かれること多いのですが、めっちゃやってます!!!!コードも書いてるし、マネジメントも一応ちゃんとしています。まだまだおもしろい仕事があるので興味あれば言って下さい!!!!

イベント・登壇

日本でのイベントや登壇はあまりカウントしてなくて、海外イベントの登壇は今回2回行いました。

How to create a local JavaScript Community in Node Interactive North America 2017 www.youtube.com speakerdeck.com

Turbo Boost Next Node.js in JSDC Taiwan 2017 www.youtube.com speakerdeck.com

英語での発表も苦手意識は薄くなってきましたが、やっぱりまだまだ緊張しますね。日本での登壇と違ってアドリブだったり失敗した時のリカバリができないので緊張しながらやってるのが分かります。

日本での登壇で一番評判がよかったのは以下の発表でしょうか。

You need to know SSR speakerdeck.com

PublicKey にも記事にしていただきました。 www.publickey1.jp

Node Community

Node 学園祭

今年も無事やれてよかったなと思いました。自分の思いが先行してていくつも失敗はあったのですが、発表・ワークショップ・ディスカッション・アフターパーティと結果としては大成功だったかなと。

yosuke-furukawa.hatenablog.com

Node Japan User Group 法人化

今年一番大変だったものの1つなんですが、Node Japan User Group を法人化し、 Japan Node.js Association にしました。

Japan Node.js Association

ぜんぜん慣れないながらも定款を書いたり、法務局を何度も訪問したりと色々やっていました。法人って時間がとにかくかかるのですが、去年からの宿題でずっとやらないといけないと思っていたのができたのがホント嬉しかったです。法人化すると法人データベースに名前が載るのですが、載った時はガッツポーズしましたね。

法人化もできて、今後も大きくなる土台作りが済んだのでNode.jsをますます発展できるようにこれからもがんばります。

インタビュー系記事

今年はインタビューで取り上げて貰う機会が多くてありがたかったです(なぜか緑の服着てることが多い)。

codezine.jp

freelance.levtech.jp

logmi.jp

codeiq.jp

app.codegrid.net

app.codegrid.net

執筆系

完全に色々滞りました。すいません、、、すいません、、、

React の記事を Web DB Press に納めましたが、こちらも厳密に言えば2016年の年末に頑張っていただけなので今年は執筆業としてはそこまで書いてこなかったかも。。。

gihyo.jp

一番大きな懸念であった法人化が終わったので来年は執筆も頑張っていきます。

OSS

Node.js の HTTP/2 に色々コントリビュートしてましたね。

https://github.com/nodejs/http2/commits?author=yosuke-furukawa

HTTP/2 周り全然最初動いてなかったので動くようにしてテスト追加してっていうのをやってたらv8でexperimentalながらリリースされました。(∩´∀`)∩ワーイ

でもこれもまだまだですねぇ、HTTP/2は次のv10までにexperimentalフラグが取れるように色々活動が進んでいますが、一番やりたいパフォーマンス面での貢献がまだまだ。。。

あと、Node.js のコアに関しては簡単なものは会社の若手ができるように社内で Node.js Core にコントリビュートさせる活動を開いています。この辺が活動の成果ですね。

https://github.com/nodejs/node/pull/17734 https://github.com/nodejs/node/pull/17699

活動していたら会社の若手が発表してくれました。嬉しかったです。

speakerdeck.com

Node.js コア以外だと agreed と呼ばれるライブラリをメンテナンスしたり、 redux 周りのライブラリをメンテナンスしたりしてました。

github.com

github.com

github.com

mizchiくんtwadaさんkoichikさん といったJavaScriptOSSが得意な同僚に囲まれて刺激的な環境で開発できるのでOSS開発を仕事として実施する事ができてすごく良い体験をしています。

ブログ系

この辺の記事がバズりましたね。

yosuke-furukawa.hatenablog.com

yosuke-furukawa.hatenablog.com

yosuke-furukawa.hatenablog.com

書いている内容もどんどん専門的な話になっていて、難しくなってきているのですが、これまで以上に分かりやすく書けるように頑張ります。

まとめ

今年もブログを書いて、Node.jsに貢献して、発表して、という一年でしたが、会社での地位も大きくなってどんどん色々な事に貢献していけるようになりました。来年は会社でエンジニアファーストな技術グループを作っていくことに一層磨きをかけつつも、今年出来なかった執筆系にも力を入れつつ、これまで以上に技術的な所を頑張っていきたいと思います。

来年もよろしくお願いいたします。

【書評】『超速! Webページ速度改善ガイド 』を読みました。

ご恵贈頂いた本である『超速! Webページ速度改善ガイド 』を読み終わったので紹介します。

gihyo.jp

あほむさんと SSR Panel Talk した時の話

いきなり関係ない話をしますが、 著者のあほむさんとPixel Gridのりぃさんと一度 Node 学園でパネルトークをやらせていただきました。その時の話で印象的だったのが、下記の話でした。

  • セキュリティやパフォーマンスも非機能要件だが、セキュリティは『実施しなかったら会社に与えるダメージが大きい』のに対してパフォーマンスは『実施しなくても会社に与えるダメージは見えにくい』
  • ただだからといって疎かにしていいわけではない、セキュリティは損害が会社のリスクとして見えやすいが、パフォーマンスはユーザビリティといった売上部分の根幹に関わる
  • 幸い、あほむさんの会社にはたまたまパフォーマンスに気をつけてる人が多く、そういう人を育てる土壌もある

という話を10月にしてました。大分端折っていますが、単純に「いいなぁ」と思ったのを覚えています

そしたら、あほむさんと泉水さん(二人共同じ会社)からパフォーマンスの知見を本にした物が出るという話でした。これはまず買うしかない。

Webページにおけるパフォーマンスの本

Webページにおけるパフォーマンスを題材にした本というのはこれまでにも数多く出版されています。教科書的な話で言うと、古くはハイパフォーマンスWebサイトから、続・ハイパフォーマンスWebサイトハイパフォーマンスブラウザネットワーキングとそれぞれ出版されています。

僕も仕事柄このあたりの本は大体抑えています。3年以上前に出版された本ですが、ハイパフォーマンスブラウザネットワーキングはネットワークのレイヤでかなり解説されているので、今読んでも学びがあるかなり良い本だと思います。

ただ、『超速! Webページ速度改善ガイド』ほどプロトコルのレイヤから画像の圧縮形式の話、ブラウザの内部処理の話、JavaScriptGC、Service Workerの話までWebに関わる古今東西と未来をきちんと1冊にまとめている本は僕は知りません。

強いてあげるとすると、パフォーマンス向上のためのデザイン設計という本が多少近いでしょうか。でもこれもプロトコルレイヤの解説とブラウザの内部処理レベルの解説は超速本よりは見劣りします。

www.oreilly.co.jp

これだけの話をまとめているという点だけでも読む価値はあるかと思います。

超速本のターゲット層

細かい所も読もうとすると、玄人向けだと思います(自分が読んでも難しいと感じる所が多かったので)。とはいえ全く知らないという人でも手元において損する本ではありません。逆に全く知らない人でもパフォーマンスの職人が何を考えているのかを知るという意味では有意義だと思います。

読み方としては、『一般読者向けに概念を把握する』ところと『Webパフォーマンス職人向けに実践するところ』を分けて読むと良いと思います。最初に概念、次に実践という丁寧な構成になっているので、まずは概念から理解し、実践は分からなかったら一旦飛ばして深く知りたければ後で抑える、という読み方もできます。これ一冊だけで著者のあほむさん、泉水さんの知識と体験が凝縮されているので、全部読んで全てが分からなくてもしょうがないと思います。

超速本関心したところ

第四章・第五章のブラウザのレンダリングの所は先程述べた通り詳しく解説してる本自体が珍しいのでまずはそこが素晴らしいと思いました。

パフォーマンス改善本としては、よくあるのは「表示されるまで」を高速にするというのを解説しているところが多いのですが、表示されるだけじゃなく、アニメーションだったりスクロール時の計算だったりの「操作してから」のところに焦点を当てているのは素晴らしかったです。

超速本もうちょいこうした方が良さそうなところ

こういうテクニックめいたところよりも『組織の中でどうやって継続的にパフォーマンスをキープしてるか』だったり、『組織の中で発生したパフォーマンスの障害』だったりといった実例に即した内容を聞いてみたかったです。

どうしてもHTTPの基礎的な内容だったりJavaScriptの基礎的な内容で言うと専門的な本にページ数の関係では敵わないので、そこの解説にページを割くよりは概要にとどめた上で、より実際に起きたリアルワールドでの知見が読めるとより良い本になりそうだと思いました。

その辺に関しては「 MANABIYA.tech 」で著者の泉水さんとパネルディスカッションするのでそこで深ぼってみようかなと。

manabiya.tech

まとめ

何はともあれ、恵贈してもらったからというだけではなく、「買い」の本ですね。難しいと感じてしまうところもあるかもしれませんが、今を生きるパフォーマンス職人である、あほむさん、泉水さんの知識と体験が詰まった良い本です。

gihyo.jp

Node.js Performance 改善ガイド

Node.js Performance 改善ガイド

この記事は Node.js 2 Advent Calender の 5日目の記事です。

qiita.com

Node.js のパフォーマンスについての話がISUCON含めて徐々に増えてきているので、僕が業務でやってるパフォーマンス改善をやる際の流れとその時の方法について説明します。

まず「なんか遅いなぁ」と思って調べると、だいたい「Memory、CPU、FIle、Network」のいずれかが遅くなっていたり、負荷がかかっていることが多いです。このリソースのいずれに負荷がかかっているのかを調べるには top だったりなんだったりで調べてもらい、その後の状況を整理する所からが勝負です。

それぞれ微妙に改善するポイントが違うのでそれぞれどうやって原因を特定して、対策を講じるかについて書いていきます。

Memory の場合

ちなみに僕が負荷計測していて一番よくあるのがメモリです。 Node.js のメモリへの負荷が高まっており、ガベージコレクションが頻発してCPUにも負荷がかかって結果として遅くなる、ということが多いです。

Node.js でも他の言語と同様、一度使ったオブジェクトの開放漏れ、所謂メモリリークが発生するとメモリへの負荷が高まり、この状況に陥ります。

f:id:yosuke_furukawa:20171202233417p:plain

(もちろんメモリリークが原因じゃなく、正当にメモリを使いすぎている可能性もあります)

メモリリークかどうかを特定する

メモリリークが発生するのはどこかのオブジェクトがルートノードと呼ばれる所からアクセス可能なままの状態で放置されており、 v8 がオブジェクトを開放できない時に発生します。

https://dt-cdn.net/assets/images/content/ebook/javabook/gc-roots-1d9223317f.png

https://www.dynatrace.com/resources/ebooks/javabook/how-garbage-collection-works/

これを判別するには heapdump と呼ばれる手法で解析する必要があります。これはヒープメモリの領域を全て取得し、dumpするという手法です。

これには node-heapdump を利用します。

github.com

node-heapdump を使うと Node.js のヒープメモリ内の状況がわかります。

基本的には require('heapdump') としておくだけで良いです。UNIX系ならば SIGUSR2 のシグナルを送れば勝手に dump ファイルを取得してくれます。 もしも GC をかけてから取得したい場合など何らかの事前処理を伴ってからプログラマブルに取得したい場合は以下の要領で組み込むことも可能です。

const heapdump = require('heapdump')

process.on('SIGUSR1', () => {
  global.gc(); // gc関数は --expose-gc フラグを付ける必要があります。
  heapdump.writeSnapshot('/var/local/' + Date.now() + '.heapsnapshot');
})

ここで取得したダンプファイルは Chrome の DevTools で解析できます。 DevTools のMemoryタブからアクセスして内容を確認します。

f:id:yosuke_furukawa:20171202235413p:plain

メモリ逼迫前と逼迫後の状態でheapdumpを取っておいてそれを比較します。DevToolsで比較操作をした結果が以下の状況です。

f:id:yosuke_furukawa:20171204122705p:plain

ちなみに最近実際に起きたので言うと、 React v15 の Server Side Rendering をしていた時に、React の createElement した時のHTMLオブジェクトがGCで回収されずにメモリを逼迫していました。 React v16にしたら解消されたのでそこまで深追いはしてませんが、特定には heapdump を使いました。

メモリリークではない場合

これは正当にメモリを多く利用するアプリということなので、メモリの拡張を検討しましょう。

Node.js では default で 1.5 GB までヒープメモリを保持しますがそれ以上になると Out of Memory でプロセスが終了します。これを拡張しましょう。

Frequently Asked Questions · nodejs/node Wiki · GitHub

A: By default, --max_old_space_size (which controls the upper limit of the V8 heap) is ~1.5GB. 

$ node --max_old_space_size=2048 index.js

この他にも --trace_gc--trace_gc_verbose といったGarbage Collectionが起きたときの詳細なログを取る方法もあります。これらを使って適切なタイミングで GC がかかっているかを調査するというのも手です。

--trace_gc // GCが起きた時にログを出力する

--trace_gc_verbose // GCが起きたときのログを詳細に出力する

これらの他にもGCのタイミングを調整する方法もあります。Node.jsではidleの時にGCを行いますが、それとは別のタイミング(メモリの状況に応じて)で実施することもできます。ただあまり使ったことはないです。

--min_semi_space_size (semispace(CopyGCにおいて、fromからtoへコピーされているがまだ残っているもの)の最小サイズ)

--max_semi_space_size (semispaceの最大サイズ)

--semi_space_growth_factor (semispaceの成長率)

CPU の場合

CPU高負荷で相談される事もメモリの次くらいに多いです。

僕がよく相談されるのは「開発中は気づかなかったけど、最大想定ユーザー数を見越して高負荷計測をした所、CPUに負荷がかかりすぎて想定ユーザー数をさばききれない」という問題です。高負荷測定ではCPUが高負荷になることも多いのですが、負荷がそこまでかかっていないのにCPUが100%になった場合はどこの処理に時間がかかっているのかを確認する必要があります。

どこの処理に時間がかかっているのかを確認する

いくつか紹介します。

  • Nodeでv8 simple profilerを使ってprofileする方法でまずは粗く計測する方法
  • flame graph を使ってどこにCPU負荷がかかっているのかを画像で確認する方法

これらで粗く問題領域を特定したあとは performance.now() だったり、 console.time だったりで関数の内部でどれだけ時間がかかるのかを計測して犯人探しをします。

v8 simple profiler

v8 が提供してくれているプロファイラをNode.jsから呼び出すという方法です。

Easy profiling for Node.js Applications | Node.js

$ node --prof index.js // これを実行すると isolate-0xNNNNNNNN-v8.log が出力される
$ node --prof-process isolate-0xNNNNNNNNN-v8.log // 注意: Node v9.0.0 - v9.2.0 では isolate-0xNNNNNNNNNN-v8.log のファイルの末尾に不正な行があり何も表示されない。最後の末尾の行を消せば動く。

これを使うとどこでCPUヘビーな処理が行われているかの概略を表示してくれます。

Statistical profiling result from isolate-0x103800000-v8.log,

 [Shared libraries]: 
   ticks  total  nonlib   name
     14    9.4%          /usr/lib/system/libsystem_platform.dylib
      9    6.0%          /usr/lib/system/libsystem_kernel.dylib
      2    1.3%          /usr/lib/system/libsystem_c.dylib
      1    0.7%          /usr/lib/system/libsystem_malloc.dylib

 [JavaScript]:
   ticks  total  nonlib   name
    109    1.2%    2.4%  LazyCompile: *EventEmitter events.js:11:22
     80    0.9%    1.8%  LazyCompile: *ReadableState _stream_readable.js:35:23
     75    0.8%    1.7%  LazyCompile: *IncomingMessage _http_incoming.js:20:25

 [C++]:
   ticks  total  nonlib   name
     24   16.1%   19.5%  t node::(anonymous namespace)::ContextifyScript::New(v8::FunctionCallbackInfo<v8::Value> const&)
     10    6.7%    8.1%  t node::Binding(v8::FunctionCallbackInfo<v8::Value> const&)
....

 [Summary]:
   ticks  total  nonlib   name
      2    1.3%    1.6%  JavaScript
     90   60.4%   73.2%  C++
      4    2.7%    3.3%  GC
     26   17.4%          Shared libraries
     31   20.8%          Unaccounted

 [C++ entry points]:
   ticks    cpp   total   name
     40   59.7%   26.8%  T v8::internal::Builtin_HandleApiCall(int, v8::internal::Object**, v8::internal::Isolate*)
     19   28.4%   12.8%  T v8::internal::Runtime_CompileLazy(int, v8::internal::Object**, v8::internal::Isolate*)
....

 [Bottom up (heavy) profile]:
  Note: percentage shows a share of a particular caller in the total
  amount of its parent calls.
  Callers occupying less than 1.0% are not shown.

   ticks parent  name
     20   17.2%  UNKNOWN
      2   10.0%    T v8::internal::Runtime_CompileLazy(int, v8::internal::Object**, v8::internal::Isolate*)
      1   50.0%      LazyCompile: ~startup bootstrap_node.js:13:19
      1  100.0%        Script: ~<anonymous> bootstrap_node.js:10:10
      1   50.0%      LazyCompile: ~emitNone events.js:113:18
      1  100.0%        LazyCompile: ~emit events.js:165:44
      1  100.0%          LazyCompile: ~resume_ _stream_readable.js:822:17
      1  100.0%            LazyCompile: ~_combinedTickCallback internal/process/next_tick.js:129:33
      1    5.0%    T v8::internal::Builtin_FunctionPrototypeBind(int, v8::internal::Object**, v8::internal::Isolate*)
      1  100.0%      Script: ~<anonymous> dns.js:1:11
      1  100.0%        LazyCompile: ~NativeModule.compile bootstrap_node.js:589:44
      1  100.0%          LazyCompile: ~NativeModule.require bootstrap_node.js:521:34
      1  100.0%            Script: ~<anonymous> net.js:1:11

....

このファイルの中で僕がまず見るのは [Summary] です。 JavaScript , C++, GC, Shared librariesのいずれで負荷がかかっているのかを見ます。 GCの場合はメモリを解析しますし、JavaScriptC++ の場合はそれぞれ [JavaScript] [C++] のカテゴリに書かれている内容を見てCPUを使ってる処理を特定します。

[JavaScript] のセクションは JavaScript の処理でCPU負荷がかかっているものがリストアップされます。その際に RegExpLazyCompile といったラベルが出てくることがありますが、これはそれぞれ処理の内容の概略を表しています。

  • RegExp: 正規表現が実行されています。正規表現は新しく RegExp オブジェクトを構築するのと、正規表現用のコンパイルが実行されるので、v8エンジンにとってのコストは軽くないです。普通に文字列を含むとかの計算をするだけなら indexOf とか includes とかを使う方がコスト的には安いです。
  • LazyCompile: 何らかの理由で遅延コンパイルになっています。v8の実行中に最適化がかかれば高速になりますがそれまでは高速化されません。ちなみに LazyCompile: *EventEmitter events.js:11:22 のように * が関数の先頭についていれば最適化された事を示し、 LazyCompile: ~NativeModule.compile bootstrap_node.js:589:44 のように ~ が関数の先頭についた場合は最適化が実行されなかったという事を示しています。
  • Builtin: ビルトインで組み込まれている JavaScript 関数です。Math.abs とか JSON.parse とかが該当します。ここがCPU 100%になってると標準関数の使い方が悪かったり、 JSON に巨大なオブジェクトを渡していたりする場合が多いです。

[C++] のセクションでは、 JavaScriptから C++に処理が遷移したあとのCPU実行状況を示しています。ここで出てくるのは v8 が実際に中で行っている処理の関数が出てくることが多いので、このレイヤで何かしようとしてもNode.jsのアプリケーションレベルでは厳しいときが多いです。

[Bottom up (heavy) profile] のカテゴリには実際にCPU負荷が高かったいくつかのポイントをリストアップし、それのコールスタックを表示しています。コールスタックが見れるのでどこの関数で負荷が高い処理をしているかが分かるようになっています。

flame graph を取得する

v8 simple profiler はどこの処理が負荷が高いのか概要をつかむときには使えますが、実際にどういう事をすると負荷がかかるのかわかっている時は flame graph でみたほうが分かりやすいです。

flame graph そのものが何なのかを分かっていない方はこの記事に詳しく書いてあるのでご一読ください。

www.brendangregg.com

日本語だとこの記事が分かりやすかったです。

http://deeeet.com/writing/2016/05/29/go-flame-graph/

Node.js には 0x と呼ばれる npm module があるのでそれを使うと MacOSUnix 環境では簡単にグラフを取得することができます。

www.npmjs.com

$ npm i 0x -g
$ 0x index.js
// sudo が必要なので password が求められることが有ります。

何回か処理をした後プログラムを終了すると下記のようなグラフが出力されます。

f:id:yosuke_furukawa:20171204180401p:plain

x軸の長さが登場した回数を表し、y軸がコールスタックの深さを示しています。コールスタックが深ければ深いほど関数呼び出しの回数が多いので負荷がかかります。ループの中で呼ばれていたりして何度も実行されているとx軸が長くなります。CPUが100%だったりして返ってこないという時はこのコールスタックの一番上が何かで特定すると分かりやすいです。

File の場合

File IOがボトルネックになるアプリケーションに遭遇したことはあまり無いです。たまに聞くケースで言うと、「Node.jsで大きなファイルの読み込み・書き込みをした時に遅い」と言われることがあります。例えば「ファイルアップロードするシステムを作っていて大きなサイズのファイルのアップロードが遅くなる」とか、「大きなサイズのファイルをダウンロードするのが遅い」とかそういう話ですね。

ただこういう大きなファイルを扱うアプリケーションはそもそもスケールさせるのが難しいのでバッチか何かで別途ファイルを生成して nginx であったり CDN に載せてアプリケーションサーバでサーブしないようにする方が良いです。アップロードもアプリケーションサーバを経由させず、S3に直接PUTするといった方法を取る事が多いです。

まずはそういう方法で回避できないか検討し、どうしてもアプリケーションサーバで扱う必要が出てきた時のみチューニングを実施します。

Node.js は御存知の通り非同期を基本としているので blocking することは基本的にはないのですが、容量が大きい場合は多少状況が異なります

大きなサイズのファイルをどうしても扱う時

Node.js は非同期を基本としていますが、大きなファイルを扱う場合は気をつける必要があります。

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

const server = http.createServer(async (req, res) => {
  res.writeHead(200, {
    "Content-Type": "application/octet-stream",
    "Content-Size": 1000000000, // 1GB のファイル
  })
  // fsのreadFileを行ってdata.txtを読み込む
  // ただこのファイルが1GBとかある場合はこうすると、一気にメモリが消費されるとともに
  // File IOも負荷がかかってしまう。
  const data = await readFile(__dirname + '/data.txt')
  res.end(data)
});
server.listen(3000)

このコードを実際に動かし、並列でファイルをダウンロードしてみると、Node.jsのプロセスがメモリをどんどん使ってしまい、結果として File IOとメモリの両方に負荷がかかって遅くなるのを感じると思います。

そこでよく使われるのが少しずつ chunked でファイルをもってくる Stream です。

const http = require('http')
const fs = require('fs')

const server = http.createServer((req, res) => { 
  res.writeHead(200, {
    "Content-Type": "application/octet-stream",
    "Content-Size": 1000000000, // 1GB のファイル
  })
  const stream = fs.createReadStream(__dirname + '/data.txt')
  stream.pipe(res) // pipe を使ってresponseに書き込む
})
server.listen(3000)

これならファイルサイズが多少大きくても少しずつファイルを読み込み、レスポンスに書き込むのでメモリにもFile I/Oにも優しいです。

ただこれもデフォルトでは64KBずつしかファイルを読み込まないので、ファイルサイズによっては遅く感じることもあります。この状況すらも避けたいのであれば、 StreamhighWaterMark パラメータを自分でチューニングし、バッファサイズを調整する必要があります。

https://nodejs.org/api/stream.html#stream_buffering

const http = require('http');
const fs = require('fs');
const bufferSize = 1000000;

const server = http.createServer((req, res) => {
  res.writeHead(200, {
    "Content-Type": "application/octet-stream",
    "Content-Size": 1000000000, // 1GB のファイル
  })

  // highWaterMark を設定する。ここでは 1MB ずつメモリにバッファリングする
  const stream = fs.createReadStream(__dirname + '/data.txt', { highWaterMark: bufferSize })
  stream.pipe(res)
})
server.listen(3000)

ここでのhighWaterMarkのサイズはレスポンス速度とリソース負荷とのトレードオフを取ることになります。

highWaterMark が増えればバッファリングされる量が増えてファイルの内容をメモリに貯めてから書き込むようになり、レスポンス速度の向上が期待できますが、その分メモリも増えますしFileIOも増えます。highWaterMarkを減らせば今度はレスポンス速度は下がりますが、 メモリやFile IOの負荷は軽減が見込めます。

Network の場合

Network がボトルネックになることは割と多いです。しかし Node.js のレイヤで何かチューニングすることはそんなに多くないです。シチュエーションとしては例えばBackendにAPIサーバが居て、そこに対してリクエストを送ってレスポンスが返るまで待つようなアプリケーション(いわゆるBFF)でBackendのAPIサーバがボトルネックになる、といったケースです。

このような状況の場合、まずは API サーバのチューニングから始まるので APIクライアントである Node.js のレイヤで何かすることはまだ無いです。あるとしたらキャッシュを持ってAPIサーバにリクエストを送る回数を減らすと行った措置でしょうか。

ただし、キャッシュは諸刃の剣です。またBackendのAPIサーバのチューニングをしたいと思っても自分達がハンドリングできない時もあります。そういった場合に Node.js のレイヤでもできることはあります。

keepalive を on にする

Node.js の http client はデフォルトでは keepalive を off に設定されています。

https://nodejs.org/api/http.html#http_new_agent_options

この状況下では、 http の接続を毎回実施している状況になります。httpkeepalive を有効にすることで接続するまでの時間を短縮することが可能です。

const http = require('http');
const keepAliveAgent = new http.Agent({ keepAlive: true });
options.agent = keepAliveAgent;
http.request(options, onResponseCallback);

弊社ではBFFから API サーバへの接続に axios を使っていますが、それも keepalive はデフォルトでは off になっているので下記の要領で on にする必要があります。

{
  url: '/user',
  baseURL: 'https://some-domain.com/api/',
  httpAgent: new http.Agent({ keepAlive: true }),
}

ちなみに https の場合も同様です。 httpsAPIを叩く場合はTLS接続の負荷を減らせるため、 keepalive の効果は特に大きいです。

しかしながら、 keepalive に対応していないAPIサーバもいますし、また1台に負荷が偏ってしまうのを防ぐため(接続するサーバを分散させるため)に keepalive を意図的に使わないようにしているAPIサーバも居たりします。そういったときにはこの施策は使えないので注意。

http.Agentkeepalive 以外にも下記のチューニングをすることが可能です。

new http.Agent({ 
  keepAlive: true,
  keepAliveMsecs: 10000 // keepaliveが有効になる期間 デフォルトは1秒 (1000)
  maxSockets: Infinity // 何個のsocketを最大同時に接続させるか デフォルトは Infinity
  maxFreeSockets: 256 // 接続している socket のうち、free 状態になってるsocketを最大何個まで確保するか デフォルトは 256
})

その他: 全体的にパフォーマンスを改善するためにやること

今挙げたリソースの観点からだけでなく、全体的にパフォーマンスを改善するために下記のことも検討します。

  • JITが効いているかを確認する
  • clusterが利用できないか
  • C++ addons vs JavaScript library

JIT が効いているかを確認する

v8 は内部で Just In Time compiler (通称JIT) を利用していますが実際に JIT が有効に働くかどうかは記述した JavaScript 次第です。

例えば

  • arguments を不適切に利用している場合
  • 引数に取り得るtypesが number だったり object だったりで一意に定まらない場合
  • ES2017+ などの新しいsyntaxを利用している場合

などなど、JITにとって好ましくない色々なケースが考えられます。

これらの JIT に好ましくないケースを回避してコードがうまく実行できるかどうかに関しては、いくつかのオプションを有効にしてログを取りながら確認する必要があります。

$ node --trace-opt --trace-deopt  --trace_ic index.js

それぞれ以下の意味があります。

--trace-opt JITがoptimizeしているかどうかをロギングする。

--trace-deopt JITがoptimizeできずにdeoptimizationが起きているかどうかをロギングする。deoptimizeが発生するとログが表示される

--trace_ic inline cache の状態をロギングする。

この他にも --print-opt-code といったoptimize後のコードを表示するオプションも存在します。

この中でよく見るのは --trace-deopt ですが、これを実行するとかなりの量のログが出力されます。 しかも Node.js の標準ライブラリ内のJavaScriptでも稀に deoptimization が発生しているので自分の関係のあるログだけフィルターして見ることをオススメします。

$ node --trace-deopt index.js | grep mysample

自分のコードであればなるべく deoptimization が発生しないようにコードを修正します。

よく見る deoptimizationの発生事例 で言うと、

  • babelREST/Spread operatorをトランスパイルした結果、 arguments に触るようになってしまい、deoptimization が起きるというケース
  • 関数が object の型のものも、 string の型のものも引数で受け付けるように柔軟な関数を定義しているケース

です。

前者の例ではServer Side では deoptimization が起きないように babel の設定を見直したりします。 後者の例は最近はFlowやTypeScriptなどで型が付けられるようになってきたので発生しにくくなりましたが、それでも union type などの柔軟な型を定義することができます。それによってdeoptimizationが発生した場合は関数定義を分けて対応するように修正します。

clusterが使えないか検討する

Node.js はシングルスレッドですが、clusterを使うとマルチプロセスで並列性が上がります。CPUコアが複数利用できるなら検討したほうがパフォーマンスが上がることが多いです。 clusterは普通のNode.jsのchild_processを使ったものでメモリの共有はできませんが、並列で計算させるのには使えます。また、最近では pm2cluster に簡単に変換させることもできるようにしているので、 pm2 を使っていれば簡単に cluster 化を行うことが可能です。

pm2.keymetrics.io

cluster も若干チューニングポイントが有ります。clusterは基本的に Round Robin されるように各種Worker プロセスに平等にリクエストを渡す作りになっています。リソース効率を上げるのであればこの方がオススメですが、どのWorkerに渡したかという情報をスケジューリングして上げる必要があるので前処理があります。この前処理すら不要でさっさと Worker に渡して速度を上げて欲しいという場合であれば、 clusterschedulingPolicy を調整する必要があります。

https://nodejs.org/dist/latest-v9.x/docs/api/cluster.html#cluster_cluster_schedulingpolicy

$ NODE_CLUSTER_SCHED_POLICY=none node cluster.js // こうすると Round Robin をやめてランダムに渡すようになる

ただ経験上、このようなスケジュールポリシーにまで手を入れるような事は無いです。むしろ Round Robin のままのが都合が良い事が多いです。

C++ addons vs JavaScript libraries

これは npm に上がっているライブラリを使う時に検討する事ですが、基本的に JavaScript で書かれたライブラリの方を選択するようにしています。

これは互換性だったり運用性もJavaScriptで書かれた物の方が上なのですが、パフォーマンスについてもよっぽどのことがない限りは Pure JavaScript のライブラリのが十分に高速ですC++ addons の場合、 JavaScript のレイヤから C++のレイヤを呼び出すときのオーバーヘッドがあるので、そのオーバーヘッドを無視できるくらい C++ で複雑な計算をする場合は利点があります。

しかしそうじゃない場合、特に頻繁にそこの計算処理を呼び出す場合においては普通に JavaScript で実装されている方がJITが効くことによって大体において高速です

まとめ

  • どの計算資源に負荷がかかっているかによって状況の整理方法は異なります。
  • Memory だったら heapdumpを取りましょう。
    • その後ヒープの差分を見ながら犯人を探しましょう。
  • CPUだったらまずは profile しましょう。
    • その後怪しい処理に目をつけたら Performance API か console.time で犯人を探しましょう。
  • File だったらまずは処理をアプリケーションの外に出せないか検討しましょう。
    • もしも外だしできなかったら Stream の利用を検討しましょう。
    • 必要に応じて Stream の highWaterMark でチューニングしましょう。
  • Network だったらまずは呼び出し先の状況を整理し、呼び出し先で解決できないか検討しましょう。
    • 呼び出し先の情報をキャッシュできないかを検討しましょう。
    • 必要に応じてkeepaliveオプションを有効にして接続にかかる負荷を軽減させましょう。
  • その他以下のことは余裕がある範囲で検討しましょう。
    • JIT が効いてるかどうか
    • cluster が使えるかどうか(これは設計時に検討したほうが良いです)
    • Pure JavaScript のモジュールを選んでいるかどうか (これも設計時に検討した方が良いです)

参考資料

NodeFest 2017 を開催しました。

NodeFest 2017 を開催しました。

f:id:yosuke_furukawa:20171125110240j:plain

f:id:yosuke_furukawa:20171125104214j:plain

参加者の皆さん、先週土日と二日間ご参加ありがとうございました。発表有り、ワークショップ有り、コラボレーション有り、Video Jockey有りととんでもなく内容の濃い二日間でした。非常に楽しい会になりました。ありがとうございます。

振り返りをしていこうと思います。

開催地

今年の開催地の1日目は法政大学の情報科学部提供の富士見ゲートというところでした。

cis.hosei.ac.jp

f:id:yosuke_furukawa:20171125115515j:plain

f:id:yosuke_furukawa:20171125131331j:plain

400人程度入る教室と200人規模入る教室が2つほど用意されてました。新しい教室なのですごくキレイでした。

f:id:yosuke_furukawa:20171125114217j:plain

f:id:yosuke_furukawa:20171125114435j:plain

2日目はリクルートグラントウキョウサウスタワーでした。

f:id:yosuke_furukawa:20171126104015j:plain

f:id:yosuke_furukawa:20171126104057j:plain

リクルートでは200人のメインホールと120人の Workshop 会場と 60人の NodeSchool会場を用意してました。

f:id:yosuke_furukawa:20171126113646j:plain

f:id:yosuke_furukawa:20171126135152j:plain

開催地が分かれてしまったのは単純に2日間続けてできる会場を抑えられなかっただけです。。。来年以降はちゃんと一つの会場でやれるようにします。

参加者

1日目: 314/365 で86%の参加率 2日目: 129/172 で75%の参加率

と参加率だけでいうと歴代最高でした。特に1日目の86%は素晴らしいですね。参加枠に余裕があったおかげで駆け込みでの参加が増えたのが要因だと思います。

アンケート

開催後にアンケートを取ったので、アンケート結果も公開します。

年齢

f:id:yosuke_furukawa:20171130134325p:plain

20代が50%を超えていてボリュームゾーンですね。若い世代が多いカンファレンスですが、徐々に30代も増えてきたように思えます。

職種

f:id:yosuke_furukawa:20171130134523p:plain

Webフロントエンドが3割ですね。ただ例年よりもWebサーバサイドが増えてきているように思いました。他にはフルスタックなエンジニアも居ますね。

何回目のNode学園祭の参加か

f:id:yosuke_furukawa:20171130134733p:plain

なんと7割を超えた人が初参加でした。 新しい人が多いのは新陳代謝が活発ということで良い傾向かなと思っております。 それとは別に3回目以上の常連の人もいらっしゃっていただきました。ありがとうございます。

Node.js の利用歴

f:id:yosuke_furukawa:20171130135132p:plain

プロダクションで利用してくれてる方が増えてきていますね。

以下の画像は2016年の時のグラフですが、それと比較しても割合が増えたなと感じました。

f:id:yosuke_furukawa:20171130140018p:plain

Node.js の利用方法

やはりというか当然と言うか、Webアプリとして利用している人、 JavaScriptのエコシステムとして利用している人が多いですね。

f:id:yosuke_furukawa:20171130140148p:plain

1日目のベストスピーカー

同率1位で下記の2つの発表が良かったというアンケート結果になりました。

Whose Community? by Tokuyama Yuka

コミュニティが誰のものであるか、というのを示した発表。エモい内容ですごく心に残りました。

今さら聞けないSPAのCORS対策の話 by Sota Sugiura

Cross Origin Resource Sharing というものが何なのか、というのを体系的にかつわかりやすく説明した資料でした。最後の Preflight チェックをキャッシュしてチューニングするのはなるほどと思いました。

2日目のベストスピーカー

Sharing is Caring… At Scale! by Sarah Saltrick Meyer

TBD: 発表資料未アップロード

BuzzFeed のエンジニアが BuzzFeed で行われているデザインシステムやコンポーネントライブラリの共有方法の知見を話してもらいました。英語と日本語ちゃんぽんで発表していてすごかったです。

この他にもいくつもの面白い発表がありました。全部は紹介できないので、下記のまとめ記事をご一読ください。

abouthiroppy.hatenablog.jp

Node Discussion

f:id:yosuke_furukawa:20171126113142j:plain f:id:yosuke_furukawa:20171126124407j:plain f:id:yosuke_furukawa:20171126121802j:plain

2日目はNode.jsの全体での Core Value/Problem/Wishlist/Question を集めて熱いディスカッションを交わしました。 MylesやFranziska、Joyeeといったコアコミッターにちゃんと意見を伝えられる良い機会でした。

Node.jsのvisionの話から欲しい機能についての話まで非常に面白い議論をしました。2時間取っていたのに時間が足りなくなってしまったのは僕のせいです。。。

Code And Learn

Node.js のコアリポジトリにコミットしてみましょう、というハンズオンです。

github.com

コアコラボレーターのひろっぴーがいくつかコントリビューション可能なポイントをリストアップしてくれていて、それらを使って contribution してみるという会を開催しました。

f:id:yosuke_furukawa:20171126163940j:plain

結果として13人のNode コミッターを作ることが出来ました!!!

f:id:yosuke_furukawa:20171130155723p:plain

Workshop

Auth0 Workshop

ゴールドスポンサーの Auth0 様にお願いして auth0 と React / Node.js を使ったworkshopをハンズオン形式で話してもらいました。

f:id:yosuke_furukawa:20171126120802j:plain

最初50人くれば十分だろうと思って60人規模の会議室しか使ってなかったのですが、60人以上来てパンパンになってしまい申し訳なかったです。

Line bot Workshop

IoT LT や Node.js 附属小学校主催の n0bisuke さんにお願いして LINE Bot のハンズオンをしていただきました。

f:id:yosuke_furukawa:20171126135818j:plain

なぜか僕のBotが。。。

Data Visualization Workshop

D3.js を使って花の絵を書いてみるというSVGハンズオンでした。 Shirley は Data を Visualizeするスペシャリストで datasketches というライブラリを使ってvisualなデモを作る人でした。参加者全員楽しそうでした。

After Party

とんでもなくすごかったです。Video JockeyしながらGLをlive cordingしていくというもの。実際には音楽とカメラの映像が流れているのですが、 Speech Synthesis API を使って音声合成したり、WebGLでカメラの映像を加工しながら VJ をしていくというのがすごかった。。。

ここまでガチのライブができるならライブ会場次回は借りたいですね!!!

開催しての感想

2014年から引き継いで2017年で3年間開催してきましたが、今年が一番VJ含めてすごく楽しい会になりました。 来年もアンケートの反省を活かして今年を超えられるように新しい事にチャレンジしていきます!!!!

NodeFest 2017を開校します。

NodeFest 2017 を開校します。

さてさて、NodeFest 2017 を開校します。今年の学園祭の紹介をここでしておきます。

去年に引き続き今年も2Days開催します。

参加はこちらから!

1日目:

nodejs.connpass.com

2日目:

nodejs.connpass.com

詳細なスケジュールに関してはここを見てみてください。

nodefest.jp

1日目

最初の発表は以下のような感じです。

f:id:yosuke_furukawa:20171024173545p:plain

この時点で目玉はいくつもありますが、あえて上げるなら、 V8 の中のコアメンバーである Franziska Hinkelmann が語る V8 のディープな話、stdlibの作者が語る WebAssembly の話、 Node.js Core memberである Myles が語る 今後のNode.jsのガバナンスモデルの話、中国最大手のオンラインマーケットサイトを持つAlibabaが Node.js をどう使ってるかという話があります。他にも React みたいな自分でフロントエンドフレームワークを作るにはどうするか (by Jorge)、という話もあります。

f:id:yosuke_furukawa:20171024175636p:plain

ここにもいくつも目玉が有ります。特に個人的にはkosamari さんの「標準化ってなに? Webプラットフォーム/ESNextを作る方法 -What is standardization? How to create Web Platform / ESNext -」という発表が楽しみです。TC39やWHATWGの裏側では何が行われているのかを話していただけるそうです。 他にも GraphQLの話、Bitcoinの話、量子コンピューティングの話(!)があります。

Node.js を普通に使った話というよりもコアの話や標準化などの新しい話、「JavaScriptをそんな所で使うの?!?!」っていうマッドサイエンスな話も多いです。

f:id:yosuke_furukawa:20171024181921p:plain

LTはこちら、 Angular, React, TypeScript, Visual Test, Community の話があります。 どの話も濃い話が多く、面白い話がたくさんあります。

2日目

2日目の発表は以下のとおりです。

f:id:yosuke_furukawa:20171024182754p:plain

ハッシュ関数の話、 Sharing is Caring はスタイルガイドの話、また Data Sketches などのData Visualizationの話もあります。

この他にもコラボレーションを基本としたコンテンツを用意しています。

今年も Node/JavaScript への要望を言うための Node Discussion はあるし、Code And Learn 等のコミッターになってみよう活動もやる予定です。 この他にも NodeSchool / Workshop も実施します。

Node Discussion: f:id:yosuke_furukawa:20171025131643p:plain

NodeSchool / WorkShop:

f:id:yosuke_furukawa:20171025131726p:plain

Workshopはまだ全コンテンツ追加されていませんが、 jsconf.asia や cssconf.eu で登壇した、 Shriley Wu から Data Visualization Workshop を実施する予定です。

懇親会

1日目も2日目も懇親会を行います!!

懇親会はどちらも豪華にやる予定ですし、1日目はこれまでとは違った形で行う予定です。

今年はかなりAfter Party にも色々コンテンツを用意しているので是非参加をお願い致します!!!

参加はこちらから!

1日目:

nodejs.connpass.com

2日目:

nodejs.connpass.com