Node.js徹底攻略 ─ ヤフーのノウハウに学ぶ、パフォーマンス劣化やコールバック地獄との戦い方
Node.jsをうまく活用できている企業は、どのような方法でベストプラクティスを習得してきたのでしょうか。ヤフー株式会社でNode.jsの社内普及に務めてきた言語サポートチームに、同社の実施を紹介してもらいました。
Node.jsは「イベントループモデルで、ノンブロッキングI/Oを使用している」「問題発生時にHTTP/TCPやPOSIX APIなど低レイヤーの知識を求められる」といった特徴を持つ言語です。開発者が習得すべき技術領域が広いため、Node.jsらしい書き方の学習難易度は高いと言えます。
それでは、Node.jsをうまく活用できている企業は、どのような方法でNode.jsのベストプラクティスを習得してきたのでしょうか。ヤフー株式会社でNode.jsの社内普及に務めてきた言語サポートチームの方々に「パフォーマンス向上のため」「コールバック地獄をなくすため」に、同社で実施してきた手法を紹介してもらいました。
- Node.jsを使いこなすには、プリミティブな知識を習得しよう
- 〈パフォーマンス向上(1)〉同期処理のAPIをなるべく使用しない
- 〈パフォーマンス向上(2)〉Node.jsのデザインパターンやCore APIの仕様を理解する
- 〈コールバック地獄の解消(1)〉Promiseやasync/awaitを用いてフロー制御を書く
- 〈コールバック地獄を解消(2)〉モジュールの特性に応じて、フロー制御の方法を変える
- 低レイヤーの知識は、何十年たっても古くはならない
- 伊藤 康太(いとう・こうた) koh110 koh110(写真左)
- ヤフー株式会社 システム統括本部 情報システム本部 リーダー
チャットシステムなど内製基盤の開発・運用・マネジメントを担当。Node.js言語サポートチームにも所属し、サーバサイドTypeScriptの活用や、SSR、BFFのチューニングを支援している。 - 大津 繁樹(おおつ・しげき) jovi0608 shigeki(写真中央)
- ヤフー株式会社 システム統括本部 サイトオペレーション本部
CDNチームでTLSを中心としたネットワークセキュリティ技術を担当する。Node.jsの言語サポートチームにも所属。Node.js Collaborator。第8代黒帯(ネットワーク・セキュリティ)。
ブログ「ぼちぼち日記」 - 栗山 太希(くりやま・だいき) Ajido(写真右)
- ヤフー株式会社 システム統括本部 セキュリティ&コアテクノロジー本部
リアルタイムコミュニケーション基盤の設計と開発を担当。言語サポートチームでは非同期処理の基礎と最先端の扱い方を学ぶ演習を開催し、効率的かつ効果的なサービス開発を促進している。第8代黒帯(Node.js)。
Node.jsを使いこなすには、プリミティブな知識を習得しよう
──Node.jsはどんな特徴を持った言語でしょうか?
大津 Node.jsを使用した開発では、コンピューターの低レイヤーな部分を扱わなければいけないケースがよくあります。コーディングをする際に「APIの内部でどんな処理が行われているか?」についてのプリミティブな知識が求められます。言語の持つ抽象度が低いと言いますか。フレームワークが整備されており、抽象度の高い開発ができる他の言語とは、その点が根本的に異なっています。
栗山 下の図は、ヤフー社内のハンズオンで受講者によく見せている「JavaScriptや関連ライブラリがどのような要素技術から成り立っているか」を表す資料です。
この図からも分かるように、Node.jsはJavaScriptに加えて、HTTP/HTTPSやTCP/IP、OSやPOSIXなどの基礎技術が組み合わさってできています。
大津 さらに、現時点で用いられているライブラリやフレームワークにもデファクトスタンダードと呼べるものが少ないです。
フレームワークの体系に合わせて開発するのではなく、自らの用途に合わせてライブラリ・フレームワークをアラカルト的に選んでいく開発スタイルになります。そのため、開発者自身が低レイヤーの基礎知識を持ち合わせていなければ、問題が起きたときの原因特定ができなくなってしまいます。
伊藤 さらには、シングルプロセス・シングルスレッドで、ノンブロッキングI/Oであるという特徴もあります※。コーディングする際には、この特徴を最大限に生かすような実装をしていく必要があります。
※ なお、伊藤さん曰く「Node.js内部では、実際にはマルチスレッドを用いた処理が行われている。そのため正確には、表面的にはシングルプロセス・シングルスレッドの動作をしているという表現が適切」とのこと。
伊藤康太さん
〈パフォーマンス向上(1)〉
同期処理のAPIをなるべく使用しない
──具体的な実装テクニックとして「Node.jsの性能をフルに引き出すための方法」を教えてください。
栗山 初学者向けの内容として、私が必ず伝えていることは次の3つです。
- Clusterモジュールを使ってマルチプロセス化すること
- Sync(同期)という接尾辞がついたAPIを使わないこと
- JSON.parseの実行回数を減らすこと
これらに従うことが常に正しいとは限りません。ですが、パフォーマンスを損なう原因となっているケースが多いため、特に重要視すべき部分として挙げています。
──前の2つはイメージがつきやすいのですが、なぜJSON.parseは減らさないといけないのでしょうか?
大津 JavaScriptが提供しているJSON.parseのAPIが、同期処理しかサポートしていないためです。そのため、JSON.parseの処理を行っている間は、他のリクエストを受けつけなくなってしまいます。
栗山 過去事例として、Yahoo!ニュースの開発メンバーから「パフォーマンスが出ない理由を探ってほしい」と相談されたことがありました。
原因を探ってみると、あるキャッシュモジュールの内部でJSON.stringifyとJSON.parseが何度も呼ばれていたことが分かりました。他のモジュールを使うようにしてもらったり、どうしても回避できない部分ではモジュールを自作することで、パフォーマンス改善をしていきました。
伊藤 パフォーマンス劣化を引き起こすAPIは他にもあります。
例えば、日付を扱うIntl.DateTimeFormat
というAPIがあるのですが、これがホットコードで何度も呼び出され、パフォーマンス劣化を引き起こしているケースがありました。代わりに正規表現による置換に書き換えることで、約10倍の高速化ができました。
他にもヤフーの事例で、DIを実現するためのライブラリを利用していましたが、デフォルト設定のまま利用し、シングルトンとすべき実装の箇所がシングルトンになっておらず、リクエストのたびにインスタンスが生成される実装になっていました。なおかつ、そのconstructor内で同期処理が呼ばれてしまっていたため、この2つが重なり、大きなパフォーマンス劣化を引き起こしていました。
これらの事例から分かるように、Node.jsのパフォーマンス改善は、「性能をより良くする」というよりも「悪いコードによって落ちてしまった性能を元に戻す」というイメージです。具体的には次の2点が重要になります。
- どのようなAPIがパフォーマンス劣化を引き起こすのかを知ること
- 内部でどのようなAPIが呼ばれているかを把握すること
大津 ですから、検索して自分の用途に合ったライブラリが見つかったとしても、何も考えずに導入してはいけません。ソースをきちんと読みましょう。もしかしたら、そのライブラリはパフォーマンスに問題があるかもしれません。中身を把握して、どんな処理が行われているかを理解した上で使うべきなんです。
──どうすれば「このAPIは性能劣化を引き起こす」と分かるようになるでしょうか?
伊藤 丁寧にプロファイリングをして、どの箇所で性能劣化が発生しているのかボトルネックを探ることが重要になります。私たちは、プロファイルログをFlame Graphで表示してくれるflamebearerを用いて、プログラムの動作を計測することが多いです。
(23ページ)
〈パフォーマンス向上(2)〉
Node.jsのデザインパターンやCore APIの仕様を理解する
──他に、Node.jsをより深く理解するための方法はありますか?
栗山 デザインパターンを把握することをおすすめします。Node.jsの根底にあるコールバックやイベント駆動といったデザインパターンは、Node.jsの特性に適したものです。デザインパターンを理解することは、Node.jsの特性を理解することにつながります。
例えば、Node.jsはシングルプロセス・シングルスレッドで大量のリクエストを捌くため、I/Oを非同期にしています。コールバックやイベント駆動といったデザインパターンは、この特性に適しています。
また、コールバックは非同期処理が完了してからコールバック関数が呼ばれるワンショットの処理ですが、イベント駆動はワンショットではなく、情報を読み込んでいる最中に処理を差し込むという特徴があります。
このようなデザインパターンが持つ利点や違いを理解するといいでしょう。
大津 それから、Node.jsの各モジュールが持つ特徴を覚えておくと、Node.jsへの理解がかなり進みます。私たちはそれぞれのモジュールを、学習すべき優先度ごとに、次の6つのカテゴリーに分類しています。
# | 説明 | 分類 |
---|---|---|
1 | 基本的で他に依存性のないモジュール群 | Events、Modules、Buffer、Stream、Errors、Assertion Testing |
2 | Node固有のもので本体の動作に関連するモジュール群 | Globals、Timers、Process、Console、Command Line Options、Debugger |
3 | OSの機能や他のライブラリと関連するモジュール群 | File System、Net、UDP/Datagram、TLS/SSL、Crypto、DNS、ZLIB、Child Processes |
4 | アプリケーション向けの応用モジュール群 | HTTP、HTTPS、Cluster |
5 | その他 | Utilities、OS、Path、Query Strings、URL、Readline、REPL、TTY、String Decoder、V8、VM、C/C++ Addons |
6 | 廃止予定なので覚えなくてもいいモジュール群 | Domain、Punycode |
カテゴリー1や2は、Node.jsのベースとなるモジュールなので、ぜひ理解してください。
特に、Events、Modules、Buffer、Streamは、Node.js APIの基本となる最も重要なモジュールです。以前行った社内セミナーでは、1つのサンプルコードを使ってこれらのモジュールがどう使われているのかを解説し、そのモジュールを使った演習を通して、受講者にNode.jsの基礎をしっかり学んでもらいました。
カテゴリー3以降は、ここで取得したNode.jsの基礎知識をもとに、必要に応じて学んでいけばいいと思います。
これを、以前サポートチームのメンバーが作成した次のNode.jsのクラス図とともに頭に叩き込んでおけば、Node.js Core APIの土地勘を持てるようになるはずです。
栗山太希さん
〈コールバック地獄の解消(1)〉
Promiseやasync/awaitを用いてフロー制御を書く
──Node.jsにおいて多くのエンジニアが悩む課題として、コールバック地獄も挙げられます。まず、これが何かを解説していただけますか?
栗山 コールバック地獄(Callback Hell)とは、複数のコールバック関数を扱う際にネストが深くなってしまい、エラーハンドリングの記述が非常に冗長になってしまうような状態を指します。
なぜこの状態が良くないかというと、各コールバックを適切にハンドリングするのが難しいためです。また、処理フローが変わったときに(インデントの深さが変わるため)処理内容を変更した箇所以外にも差分が表示されてしまうため、差分確認のコストが高くなってしまいます。
Callback を撲滅せよ - Yahoo! JAPAN Tech Blog
──コールバック地獄を解消するには、何をすべきでしょうか?
栗山 Promiseやasync/awaitといったAPIを用いて、コールバックによるフロー制御を書き直すことが基本になります。かつてはサードパーティー製のライブラリを導入してコールバック制御していた時代があったのですが、ここ数年の間でPromiseやasync/awaitがNode.js本体に導入されたため、コールバック処理を書くのが相当に容易になりました。
伊藤 ここ4~5年の間で、コールバック処理のパラダイムシフトが起きました。Node.jsが毎年成長していて「より良いコールバックの書き方」も変化し続けています。数年前に当たり前だった書き方が、新しく登場したAPIによって古い書き方になってしまう、という事態がよく起きています。
▼コールバックからPromise、さらにasync/awaitへの書き換えを動画で示したWassim Cheghamさん(Angularチーム、Microsoftアドボケイト)によるツイート
〈コールバック地獄を解消(2)〉
モジュールの特性に応じて、フロー制御の方法を変える
──「Promiseやasync/awaitを用いてコードを書き換えましょう」とは言っても、何を基準に書き換えればいいか分からない初学者も多いかもしれません。どう判別すべきでしょうか?
大津 イベントハンドリングの設計パターンは、大きく分けて次の4つがあります。
- Promise
- コールバック
- async/await
- EventEmitter/Stream
これらはモジュールの要件に応じて、次の表のような基準でパターンを選択していきます。
設計パターン | 互換性 | フロー制御 | ストリーム処理 | 記述量 |
---|---|---|---|---|
Promise | ✔ | ✔ | N/A | |
コールバック | ✔ | N/A | ||
async/await | ✔ | N/A | ✔ | |
EventEmitter/Stream | ✔ | ✔ |
──表で挙げられている観点を、それぞれ順に解説していただけますか?
栗山 「互換性」は、過去のバージョンのNode.jsに導入可能かを意味しています。async/awaitにチェックが入っていないのは、この機能が導入されたのがNode.jsバージョン8だからです。何らかの理由で旧バージョンのNode.jsを利用しなければいけない場合、async/awaitは使えません。
ただし、Node.jsのバージョン8はもうLTS(Long Term Support)になっていますから、新規のコードは基本的にバージョン8以上を基準に考えてよいでしょう。
「フロー制御」では、先ほど解説したようにPromiseやasync/awaitの方がよりシンプルに記述できるため、チェックが入っています。そして、「記述量」はasync/awaitが最も少なくてすみます。
──「ストリーム処理」にEventEmitter/Streamだけチェックが入っているのはどうしてでしょうか?
栗山 ストリーム処理を行いたい場合にPromiseやasync/awaitは適しておらず、使用できる設計パターンがEventEmitter/Streamのみに限定されるからです。
大津 これらを踏まえて、現時点では次の設計パターンを推奨します。
- 可能な限りasync/awaitを用いる
- ストリーム処理を行いたい場合のみ、EventEmitter/Streamを導入する
ただし、Node.jsで利用できるJavaScriptの文法は日々進化し続けています。Streamと親和性が高いAsync Iteratorも、これから本格的に使えるようになります。今回おすすめした内容も将来的に変化する可能性があることには注意してください。
パフォーマンス向上やフロー制御以外にも、Node.jsを使いこなすには、まだまだいろいろな課題があります。
増大していくNode.jsアプリケーションのメモリ使用量を、どううまく設計・管理していくのか? KubernetesやPaaSなどの環境でサービスへの影響を最小限に抑えながら、Node.jsをどのように運用していくのか? そういった課題に現在取り組んでいるところです。
ヤフー社内で実績が出てきたNode.jsのベストプラクティスは、これからも積極的に外部に公開していく予定です。
大津繁樹さん
低レイヤーの知識は、何十年たっても古くはならない
──最後に、みなさんから「Node.jsを学ぶ読者に伝えたいこと」を伺いたいです。
伊藤 Node.jsを学ぶことには、即効性のある利点と長期的な利点の2つがあります。即効性のある利点としては、フロントエンドエンジニアでもともとJavaScriptを書いていた方が、Node.jsを習得することでバックエンドにも携われることです。私自身もそうでした。
フロントエンド開発をしていて「APIがこういう仕様になっていたらいいのに」と思うことはけっこう良くありますよね。でもバックエンド開発の知識がなければ、API設計に口を出すことは難しいです。Node.jsを学ぶことでAPI設計に携われるようになることは、大きなアドバンテージになります。
また、BFFやSSRなど、フロントエンドエンジニアでもサーバーサイドに手を伸ばす需要は、以前に比べて高まっています。この点でも、コンテキストスイッチの少ないNode.jsは非常に有効な選択肢です。
もうひとつの長期的な利点は、Node.jsを覚えることで低レイヤーの知識が学べることにあります。例えば、HTTPのモジュールやTLSのモジュールを使ってみると、HTTP/TLSがどう動いているかの根本的な仕組みが理解できます。もし将来的にNode.jsが使われなくなったとしても、Node.jsを学ぶ過程で得た知識は絶対に古くなりません。
──栗山さんはどうでしょうか?
栗山 私はNode.jsを学ぶ方々に「Node.js Core APIに関するドキュメントを全て読むこと」をおすすめしたいです。
Node.jsの公式ドキュメントでは全てのAPIの説明文を1つのページで閲覧できることができ、APIの仕様を網羅的に学べるようになっています。
Node.js Documentation(View on single page)
このページを読むだけでNode.jsについて相当詳しくなれますし、先ほど伊藤さんが話していたような低レイヤーの知識も身に付くはずです。
読み方にも指標があります。前半パートで解説した6つのAPIカテゴリーのうち、1や2に属するコアの部分から学んでいき、徐々に他のAPIを覚えていくといいです。
今年末に開催されるJSConf JPにおいて、ヤフー社内のエンジニア向けに開催している「Node.js ミニマムハンズオン」をワークショップとして開催する予定です。Node.jsのエッセンスを学びたい方はぜひ受講しにきてください。
──それだけの知識が身に付けば、Node.jsに相当詳しくなれそうですね。最後に大津さんお願いします!
大津 ソースコードをたくさん読みましょう。プログラミングは、書くことよりも読むことの方がずっと大切ですから。そして、漫然と読むのではなくて、前半パートでも話したように言語の土地勘を持った上で読むことが大事です。
なぜそのような設計になっているのか。どういう考えで言語が発展しているのかを理解すること。技術を正しく学んでいけば、自分のなかにブレない軸ができます。それこそが、時代を経ても変わらない本当の意味での技術力になります。
取材・執筆:中薗昴