はじめに
MSAを基盤としてサービスを構成するために必要なのはイベント駆動アーキテクチャです。これはイベントを登録し、そのイベントに対する応答を返すことでサービスの結合度を下げることができます。現在制作中のトイプロジェクトでは、エラーハンドリングやロギング処理などをイベントベースで作成していく予定です。
シングルスレッドはプロセス内で1つのスレッドが1つのリクエストのみを処理し、このとき他のリクエストを同時に実行できないため、シングルスレッドブロッキングモデルと呼ばれます。Node.jsはシングルスレッドですが、ノンブロッキングモデルであり、クラスタリングを通じてプロセスをフォークしマルチプロセシングを利用することができます。
イベント駆動とは?
イベントが発生したときに予め指定した作業を実行することを意味します。JavaScriptではaddEventListener
でアクションを登録し、イベントが発生したときに予め登録したコールバック関数を実行する方式で動作します。
dom.addEventListener('click', () => {
console.log("コールバック実行");
});
router.get('/getdata', (req, res, next) => {
// getdataでリクエストが入ってきたときにcallbackを処理
});
Node.jsの内部構造はJavaScriptとC++言語で構成されています。このうちLibuvは100% C++で構成されたライブラリです。V8エンジンはJavaScriptをC++に変換します。Node.jsで動作するイベントループはLibuv内で実装されていますが、Node.jsはシングルスレッドであるため、1つのイベントループを持ち、1つのスレッドがすべてを処理します。
イベントループの内部構造
イベントループは複数のフェーズを持ち、それぞれが独自のQueueを持ちます。また、ラウンドロビン方式でノードプロセスが終了するまで一定の規則に従って複数のフェーズを巡回します。**FIFO(FIRST IN FIRST OUT)**順序でコールバック関数を処理します。
ノンブロッキングIO
Node.jsではブロッキング作業をバックグラウンドで実行しますが、実際にバックグラウンドで実行される一部の機能はマルチスレッドでも実行されます。**Libuvのスレッドプールはカーネルがサポートしていない作業を実行します。**つまり、Libuvのスレッドプールはマルチスレッドです。
イベントループ内の6つのフェーズ
- timers
イベントループがフェーズを巡回し、timer段階に達すると処理可能なtimer関数を確認し、コールバック関数を実行します。 - pending callback
I/O作業が完了すると、I/O作業ブロック内のコールバック関数をpoll段階のキューに渡します。また、TCPエラーなどのシステム作業のコールバックを実行します。 - idle, prepare
- poll
I/Oに関連するコールバックを除くすべてのコールバックを実行します。timer段階での実行時間制御を担当します。 - check
setImmediate()のコールバック関数が実行されます。もし、poll段階がアイドル状態であれば、pollイベントを待たずにcheck段階に移行します。 - close callback
closeイベントに対応するコールバック関数を実行します。
以下の例では、もしイベントループが回っている時点がtimerであればsetTimeoutが先に、timerを過ぎていればsetImmediateのコールバック関数が先に実行されます。
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
イベント駆動プログラミングとは?
イベント駆動プログラミングは複数のイベントの発生を同期させ、プログラムをできるだけ簡単にするために使用されます。
イベントトリガーを受信する関数をobserverと呼びます。これはイベントが発生したときにトリガーされます。これらのイベントは'events'モジュールおよびEventEmitterクラスを通じてアクセスできます。
イベント駆動プログラミングの原則
- イベントを処理するための機能の集合であり、実装によってブロッキングまたはノンブロッキングになることがあります。
- 登録された関数をイベントにバインドします。
- 登録されたイベントが受信されるとイベントループは新しいイベントをポーリングし、一致するイベントハンドラーを呼び出します。
// 'events'モジュールをインポートします。
const events = require('events');
// EventEmitterオブジェクトをインスタンス化します。
const eventEmitter = new events.EventEmitter();
// イベントに関連するハンドラー
const connectHandler = function connected() {
console.log('Connection established.');
// 該当するイベントをトリガーします。
eventEmitter.emit('data_received');
}
// イベントをハンドラーにバインドします。
eventEmitter.on('connection', connectHandler);
// データ受信イベントをバインドします。
eventEmitter.on('data_received', function () {
console.log('Data Transfer Successful.');
});
// connectionイベントをトリガーします。
eventEmitter.emit('connection');
console.log("Finish");
上記のコードでは、connectionというイベントを発生させ、順次マッピングされたコールバックを実行します。