
PLAIDがNode.jsを採用し、5年間で12万行書いてわかったこと
Posted on
エンジニアのgamiです。趣味はポッドキャスト配信です。
今回は、「Node.js」に焦点を当てた記事です。
PLAIDでは、約5年前から「KARTE」というサービスを開発しています。そのサーバーサイドの実装は、ほとんどがNode.jsで記述されています。
一方、国内企業の中で、サーバーサイド実装にNode.jsを採用している企業は少ない印象があります。
そこで、以下についてご紹介します。
- PLAIDがNode.jsで実現しているプロダクト
- Node.jsを5年間使ってわかった強みと弱点
- Node.jsを使いこなすための工夫
1. PLAIDがNode.jsで実現しているプロダクト
実際にPLAIDがどのようにNode.jsを使っているかを紹介します。PLAIDでは、「リアルタイムユーザー解析サービス」であるKARTEを主に開発しているので、KARTEの開発について説明します。
プロダクトの規模
まずは前提として、KARTEのプロダクトとしての規模がわかるデータを紹介します。
リクエスト数、イベント数
KARTEが解析しているデータの規模感は、2018年8月現在の値でおよそ以下の通りです。
- 最大 約20,000 イベント/秒
- 最大 約10億 イベント/日
KARTEが扱うユーザー情報は、「イベント」という単位で連携、解析されます。エンドユーザーのブラウザから飛んでくるリクエストの中に、1つ以上のイベントが含まれています。
1日で10億回ものユーザー解析を処理しているので、かなり「大規模」と言えるかと思います。
ファイル数
KARTEのメインのリポジトリのうち、Node.jsで書かれたサーバーサイドの実装に関するファイルの数と行数をカウントすると、以下の結果になりました。
- ファイル数
- 1,759ファイル
- 行数
- 126,256行
(マイグレーションバッチ、テストコード、空行、コメント行などは除く)
他にも、一部の独自ライブラリはリポジトリを分けて開発しているので、それを加えるともう少し大きくなります。
実現している機能
以下の図がKARTEの大まかな構成図です。
今後の説明のために、ざっと解説します。
track/analyze(エンドユーザーの解析とデータ送受信)
KARTEでは、解析対象のサイトにタグ(ネイティブアプリならSDK)を設置してもらいます。
そこから送られるイベントをリクエストとして受け取り解析をするのが、「track」と「analyze」です。
- タグやSDKからイベントデータを受信
- エンドユーザーのセグメントを解析
- 1秒以内にエンドユーザーにポップアップなどの最適なアクションを送信
- レスポンス後の処理も含めて、ユーザーデータを再計算
特に、エンドユーザーからのリクエストに対してポップアップなどの最適なアクションを1秒以内に返す必要があり、高いパフォーマンスが求められる部分です。後述するように、NodeのClusterモジュールを使って負荷分散しています。
なお、KARTEではエンドユーザーから送られるイベントデータのスキーマをほとんど決めていないため、大量のスキーマレスなJSONデータを受け取る必要があります。Node.jsはJSONデータの取り回しが楽である反面、大規模なJSONを大量に扱うというCPUインテンシブな処理が苦手なのが悩みです。
admin(クライアントに管理画面を提供)
「admin」では、管理画面上でユーザーデータを閲覧できる機能等を提供しています。
- ルーティング
- (SPA部分はvue-routerに任せる)
- 設定値などを保存するMongoDBの操作
- スキーマ定義
- read/write
- フロントエンドから叩くRPC APIの提供
KARTEではスキーマレスなNoSQL DBである「MongoDB」を多用しています。MongoDBはほぼJSONライクなドキュメントを扱うので、Node.jsとの相性はかなりよく、DB周りの処理を自然に書くことができます。
主に実現している機能は以上です。
KARTEは「単純なWebサーバーとしての機能」だけではなく、「エンドユーザーの解析機能」をプロダクトのコアとして提供しています。その両方の機能について、特に言語を分けずに全てNode.jsで記述しています。
なお、システムの全体像や解析サービスを実現するGCPの活用方法については、PLAIDのエンジニア陣が執筆した以下の記事をご覧ください。
大規模解析サービスを支えるGCP活用事例連載一覧:CodeZine
言語(AltJS)
言語については、場所によって使い分けています。
- 解析部分
- その他
- ES6
- CoffeeScript(一部)
解析エンジン部分だけは、固めに書きたいのでTypeScriptを使っています。大半のソースコードは、開発速度を優先して生のJavaScriptで記述しています。
元々はCoffeeScriptを使っていたのですが、世の多くの会社がそうであるように、Pure JavaScriptの機能強化に伴い、徐々に書き換えを進めています。
フレームワーク、ライブラリ
主に以下を使っています。(挙げるとキリがないので、だいぶ削っています)
- Express.js
- 薄いので拡張しやすい
- Lodash
- 主に複雑なオブジェクトの操作で利用
- async
- 非同期処理を連続/並列で実行するときに便利
- Mongoose
- MongoDB用のODM(Object Document Mapper)
- cron
- 定期バッチの実行
- bolt-rpc
- PLAID独自RPCライブラリ
- OSSとして公開中
- 内部でsocket.ioを利用
- Node.jsで記述
- PLAID独自RPCライブラリ
- Brook
- PLAIDの独自解析エンジン
- Node.jsで記述
2. Node.jsを5年間使ってわかった強みと弱点
上述したKARTEの実装を日々書いているPLAIDのエンジニアに、Node.jsのメリットとデメリットを訊いてみました。カテゴリ別に整理します。
ノンブロッキング/イベントループ
強み
Node.jsの良く知られたメリットは、KARTEの開発でも享受できています。
- 大量のリクエストが来ても、パフォーマンスが低下しにくい
弱点
逆に、非同期処理を多用するため、例外発生時やプロファイルをするときに問題の箇所を特定するのが難しかったり、そもそも記述が複雑になりやすかったりします。
- 例外が起こったときのデバッグがキツい
- プロファイルむずすぎ
- 非同期処理がわかりにくい
シングルスレッド
強み
シングルスレッドであることで設計がシンプルになりやすい点は、意外とメリットを感じる点です。
- コンテキストスイッチによるパフォーマンス低下を防げる
- ハードな制約なので、message queueなどを使って綺麗にプロセス分離したシンプルな設計になりやすい
弱点
やはりCPUバウンドな処理が苦手な点が大きな弱点です。特に弊社の場合は大規模なJSONを扱うことが多く、JSONのパージングが同一スレッドで起きることでブロックされることに悩まされたりしています。
- シングルスレッド・マルチプロセスの癖/限界
- シングルスレッドなのでハングすることがたまにあるし、原因特定が難しい
- CPUをゴリゴリ使う処理とは相性が悪い
言語としての性質
強み
特にPLAIDではフロントエンド/サーバーサイドでエンジニアを分けずに開発しているので、どちらも同じ言語で書けるというメリットは大きいです。
- フロントとサーバーサイドを同じ言語で書けるのでスイッチングコスト低い
- JSONをそのまま扱える
- 非同期処理が簡単に書ける
- 決まりごとが少なく、用途が自由
弱点
逆に、Node.js自体が自由すぎて起こる問題もあります。なお、一部では前述のようにTypeScriptを導入しています。
- シンプル過ぎるので、ファイル構成からコードの書き方まで混乱しがち
- typoなどで実行時エラーが起こる
- 型が弱い
npm/エコシステム
強み
大量のnpmライブラリがOSSとして公開されていて、CTO曰く「センスがいいモジュールが多い」ということです。
また、Node.jsが使っているJavaScriptエンジンであるV8の進化が早く、Node.jsのバージョンを上げただけでパフォーマンスが大幅に向上したことがあります。過去にはKARTEでも、Node.jsのバージョンを6から8に上げたとき、パフォーマンスが2倍以上上がった例がありました。
- ミニマムでセンスがいいライブラリが多い印象
- GoogleがV8をどんどん進化させてくれる
- Express.jsがシンプルで良い
弱点
反面、npm自体のバグや依存性解決の難しさで時間を溶かすことも多かったです。依存性解決については、以前このブログでも紹介しています。ただし、npmのバージョンアップで徐々に解消されつつあります。
- npmのバグが多い
- node_moduleの依存関係周りが辛い
3. Node.jsを使いこなすための工夫
大規模で高いパフォーマンス要求を求められるプロダクトの実現や、Node.jsの弱点の補完のために実施していることをいくつか紹介します。
Node.jsのClusterモジュールによる負荷分散
KARTEでは最大で1秒間に数千リクエストを処理する必要があります。ただし、通常は1つのポートにアクセスできるプロセスは1つだけであり、そこがボトルネックになってしまいます。
そこで、Node.jsのClusterモジュールを使うことで、Cluster内に組み込まれたロードバランサが、リクエスト処理を複数のワーカープロセスに分割してくれます。これにより、CPUのコア数を最大限活かすことができます。
KARTEでは、特にtrackのパフォーマンス向上に役立っています。
perfコマンドとFlameGraphを使ったCPUプロファイル
CPU使用率が急上昇したときに、どの処理が重いのかを調べるため、CPUのプロファイリングをすることは多いと思います。しかし一部のプロファイリング用のライブラリは実際にコードの中に組み込むため、アプリケーション自体のパフォーマンスに影響する可能性があります。
そこで、perfコマンドで外側からプロファイルを取ることで、パフォーマンスにほぼ影響を与えずにCPUリソースを無駄に使っている関数を特定できるようにしています。
Node.jsでperfを使ったプロファイルを実現するには、--perf-basic-prof-only-functions
などのプロファイル用のオプションを起動時に付ける必要があります。オプションを付けるとperf用の.map
ファイルを吐いてくれます。
プロファイル結果は、このライブラリを使ってFlameGraphとして出力しています。FlameGraphというのは、こういうやつです。
出典: Profiling Node.js | Node.js
使いどころとしては、hubotのコマンド経由でいつでも実行できるようにしている他、特定のインスタンスについて自動でプロファイル結果を出力するようにしています。KARTEではDatadogで各インスタンスのパフォーマンスを監視していますが、パフォーマンスが出ないインスタンスは自動で殺すようにしています。後からその原因を追いたい場合に備えて、殺す前に自動でperfコマンドを実行してプロファイルをしています。
なお、KARTEを支える監視サービスと構成については、以下の記事をご覧ください。
大規模解析サービスを支える監視サービスと監視構成のポイント | CodeZine(コードジン)
マルチスレッド処理へ
Node.jsは基本的にシングルスレッドで動作するので、特にCPUインテンシブな処理が苦手です。KARTEではスキーマレスな構造データを大量に行うため、その点がボトルネックになりやすいのが悩みでした。
Node.jsでもマルチスレッド処理がしたいという熱い想いは、弊社エンジニアの牧野が以前ブログで書いています。
MicrosoftのNapa.jsでJavaScriptをマルチスレッド化する | PLAID engineer blog
Node.js v10.5.0からWorker Threadsが使えるようになったので、今後は一部の処理をそちらに置き換える計画を立てています。
まとめ
Node.jsを採用して5年間ほどKARTEの開発に使い続けましたが、特に「リアルタイムユーザー解析サービス」である特性上、その恩恵を受けることが多い。
また、Node.jsやV8の進化も早く、今後の発展が非常に楽しみです。
国内企業がプロダクションでNode.jsを採用する例はまだまだ少ないですが、とても強力な言語なのでぜひ使っていきましょう!
リアルタイムユーザー解析プラットフォームの「KARTE」を運営するプレイドでは、KARTEを使ってこんなアプリケーションが作りたい! KARTE自体の開発に興味がある!というエンジニア(インターンも!)を募集しています。
詳しくは弊社採用ページまたはWantedlyをご覧ください。 もしくはお気軽に、下記の「話を聞きに行きたい」ボタンを押してください!