コメディ。 柔軟なスケーリングのためのNode.JSのアクター

こんにちは、habravchane! この記事では、Node.JSでのアクターの実装であるコメディフレームワークを紹介します。 アクターを使用すると、コードを変更せずにNode.JSアプリケーションの個々のモジュールをスケーリングできます。







俳優について



現在、俳優モデルは非常に人気がありますが、誰もが知っているわけではありません。 ウィキペディアの記事はやや恐ろしいものの、俳優は非常にシンプルです。







俳優とは何ですか? これは次のことができるようなものです:









画像







俳優と何かをする唯一の方法は、彼にメッセージを送ることです。 アクターの内部状態は、外部の世界から完全に隔離されています。 これにより、アクターはアプリケーションスケーリングの普遍的な単位です。 また、子アクタを生成する機能により、明確な責任分担を伴うモジュールの理解可能な構造を作成できます。







私はそれがやや抽象的に聞こえることを理解しています。 以下では、俳優やコメディーとの仕事がどのように行われるかの具体的な生きた例を見ていきます。 しかし、最初に...







なんでそんなこと



...最初の動機。







Node.JS(その中でも謙虚な人)でプログラムを作成する人は誰でも、Node.JSがシングルスレッドであることをよく知っています。 一方で、これは非常に愚かで再現しにくいバグのクラス(マルチスレッドバグ)から私たちを救うので良いです。 私たちのアプリケーションでは、原則としてそのようなバグは存在できません。これにより、コストが大幅に削減され、開発が高速化されます。







一方、これはNode.JSの適用性を制限します。 比較的負荷の少ないネットワーク集約型アプリケーションには最適ですが、CPU集約型アプリケーションにはあまり適していません。なぜなら、集約型コンピューティングは貴重な単一スレッドをブロックし、すべてが行き詰まってしまうからです。 これをよく知っています。







いずれにせよ、実際のアプリケーションはいずれにせよ一定量のCPUを消費することもわかっています(ビジネスロジックがまったくない場合でも、アプリケーションレベルでネットワークトラフィックを処理する必要があります(HTTP、データベースプロトコルなど)。 そして、負荷が増大すると、遅かれ早かれ、唯一のスレッドがコア電力の100%を消費する状況になります。 そして、この場合はどうなりますか? メッセージを処理する時間がなく、タスクのキューが蓄積され、応答時間が長くなり、バム! -メモリ不足。







そしてここで、すでにいくつかのCPUコアでアプリケーションをスケーリングする必要がある状況になります。 そして理想的には、1台のマシン上のカーネルだけに制限したくはありません。複数のマシンが必要になる場合があります。 同時に、アプリケーションをできる限り書き換えないようにします。 単純な構成変更によってアプリケーションがスケーリングされると便利です。 そしてさらに良い-負荷に応じて、自動的に。







そして、ここで俳優は私たちの助けになります。







ケーススタディ:プライムサービス



コメディがどのように機能するかを示すために、私は小さなをスケッチしました。素数を見つけるマイクロサービスです。 サービスへのアクセスはREST APIを介して行われます。







もちろん、素数を見つけることはCPUを集中的に使用するタスクです。 このようなサービスを実際に設計した場合、Node.JSを選択する前に10回考えるべきでした。 ただし、この場合、1つのコアでは不十分な場合に状況を再現しやすくするために、意図的に計算タスクを選択しました。







だから。 サービスの本質から始めましょう-素数を見つけるアクターを実装します。 彼のコードは次のとおりです。







/** * Actor that finds prime numbers. */ class PrimeFinderActor { /** * Finds next prime, starting from a given number (not inclusive). * * @param {Number} n Positive number to start from. * @returns {Number} Prime number next to n. */ nextPrime(n) { if (n < 1) throw new Error('Illegal input'); const n0 = n + 1; if (this._isPrime(n0)) return n0; return this.nextPrime(n0); } /** * Checks if a given number is prime. * * @param {Number} x Number to check. * @returns {Boolean} True if number is prime, false otherwise. * @private */ _isPrime(x) { for (let i = 2; i < x; i++) { if (x % i === 0) return false; } return true; } }
      
      





nextPrime()



メソッドは、指定された(必ずしも素数ではない)に続く素数を見つけます。 このメソッドは、Node.JS 8で正確にサポートされている末尾再帰を使用します(例を実行するには、まだasync-awaitがあるので、少なくともバージョン8を使用する必要があります)。 このメソッドは、ヘルパーメソッド_isPrime()



使用します。このメソッドは、簡単にするために番号をチェックします。 これは、このようなチェックに最適なアルゴリズムではありませんが、この例では優れています。







上記のコードで見たものは、一方では普通のクラスです。 一方、私たちにとって、これはいわゆるアクターの定義 、つまりアクターの振る舞いの記述です。 クラスは、アクターが受信できるメッセージ(各メソッドは同じトピックを持つメッセージハンドラー)、これらのメッセージを受け入れることで行うこと(メソッドの実装)、および結果を生成するメッセージ(戻り値)を記述します。







さらに、これは通常のクラスであるため、単体テストを作成して、その実装の正確性を簡単にテストできます。







単体テストは次のようになります
 describe('PrimeFinderActor', () => { it('should correctly find next prime', () => { const pf = new PrimeFinderActor(); expect(pf.nextPrime(1)).to.be.equal(2); expect(pf.nextPrime(2)).to.be.equal(3); expect(pf.nextPrime(3)).to.be.equal(5); expect(pf.nextPrime(30)).to.be.equal(31); }); it('should only accept positive numbers', () => { const pf = new PrimeFinderActor(); expect(() => pf.nextPrime(0)).to.throw(); expect(() => pf.nextPrime(-1)).to.throw(); }); });
      
      





これでアクターファインダープライムができました。







画像







次のステップは、RESTサーバーアクターを実装することです。 定義は次のようになります。







 const restify = require('restify'); const restifyErrors = require('restify-errors'); const P = require('bluebird'); /** * Prime numbers REST server actor. */ class RestServerActor { /** * Actor initialization hook. * * @param {Actor} selfActor Self actor instance. * @returns {Promise} Initialization promise. */ async initialize(selfActor) { this.log = selfActor.getLog(); this.primeFinder = await selfActor.createChild(PrimeFinderActor); return this._initializeServer(); } /** * Initializes REST server. * * @returns {Promise} Initialization promise. * @private */ _initializeServer() { const server = restify.createServer({ name: 'prime-finder' }); // Set 10 minutes response timeout. server.server.setTimeout(60000 * 10); // Define REST method for prime number search. server.get('/next-prime/:n', (req, res, next) => { this.log.info(`Handling next-prime request for number ${req.params.n}`); this.primeFinder.sendAndReceive('nextPrime', parseInt(req.params.n)) .then(result => { this.log.info(`Handled next-prime request for number ${req.params.n}, result: ${result}`); res.header('Content-Type', 'text/plain'); res.send(200, result.toString()); }) .catch(err => { this.log.error(`Failed to handle next-prime request for number ${req.params.n}`, err); next(new restifyErrors.InternalError(err)); }); }); return P.fromCallback(cb => { server.listen(8080, cb); }); } }
      
      





それで何が起こっていますか? 主な唯一のことは、 initialize()



メソッドがあることです。 このメソッドは、俳優を初期化するときにコメディによって呼び出されます。 アクターインスタンスが渡されます。 これはまさにメッセージを送信できるものです。 インスタンスには他にも便利なメソッドがいくつかあります。 getLog()



はアクターのロガーを返します(これは便利です) createChild()



メソッドを使用して、子アクターを作成します-最初に実装したPrimeFinderActor



です。 createChild()



で、アクターの定義を渡し、代わりに、子アクターが初期化されるとすぐに解決するプロミスを受け取り作成された子アクターのインスタンスを提供します







お気づきのとおり、アクターの初期化は非同期操作です。 initialize()



メソッドも非同期です(promiseを返します)。 したがって、 RestServerActor



は、 initialize()



メソッドによって指定されたプロミスが解決されたとき(まあ、「プロミスが満たされた」と書かないでください)にのみ初期化されたと見なされます。







さて、子PrimeFinderActor



を作成し、それが初期化されるのを待って、 primeFinder



フィールドへのインスタンスリンクを割り当てました。 RESTサーバーを構成するために、少し残っています。 Restify



ライブラリを使用して、 _initializeServer()



メソッド(非同期)でこれをRestify



ます。







GET /next-prime/:n



メソッド用に単一のリクエストハンドラ(「ハンドル」)を作成します。このメソッドは、指定された数の後の次の整数を計算し、子PrimeFinderActor



アクターにメッセージを送信して応答を受け取ります。 sendAndReceive()



メソッドを使用してメッセージを送信します。最初のパラメーターはトピック名(メソッド名でnextPrime



)、次のパラメーターはメッセージです。 この場合、メッセージは単なる数字ですが、文字列、データを持つオブジェクト、および配列が存在する場合があります。 sendAndReceive()



メソッドは非同期で、結果とともにpromiseを返します。







ほぼ完了。 あと1つだけ残しておきましょう。すべてを起動することです。 この例にさらに数行追加します。







 const actors = require('comedy'); actors({ root: RestServerActor });
      
      





ここで、アクターのシステムを作成します。 パラメーターとして、ルート(ほとんどの親)アクターの定義を示します。 RestServerActor



ます。







次の階層が判明します。







画像







幸運にも、この階層は非常に簡単です!







実際の階層の例

画像







さて、アプリケーションを実行してテストしますか?







 $ nodejs prime-finder.js Mon Aug 07 2017 15:34:37 GMT+0300 (MSK) - info: Resulting actor configuration: {}
      
      





 $ curl http://localhost:8080/next-prime/30; echo 31
      
      





うまくいく! 実験してみましょう:







 $ time curl http://localhost:8080/next-prime/30 31 real 0m0.015s user 0m0.004s sys 0m0.000s $ time curl http://localhost:8080/next-prime/3000000 3000017 real 0m0.045s user 0m0.008s sys 0m0.000s $ time curl http://localhost:8080/next-prime/300000000 300000007 real 0m2.395s user 0m0.004s sys 0m0.004s $ time curl http://localhost:8080/next-prime/3000000000 3000000019 real 5m11.817s user 0m0.016s sys 0m0.000s
      
      





開始番号が増えると、リクエストの処理時間が長くなります。 特に印象的なのは、3億から30億への移行です。 並列クエリを試してみましょう。







 $ curl http://localhost:8080/next-prime/3000000000 & [1] 32440 $ curl http://localhost:8080/next-prime/3000000000 & [2] 32442
      
      





上部では、1つのコアが完全に占有されています。







  PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 32401 weekens 20 0 955664 55588 20956 R 100,0 0,7 1:45.19 node
      
      





サーバーログには次のように表示されます:







 Mon Aug 07 2017 16:05:45 GMT+0300 (MSK) - info: InMemoryActor(5988659a897e307e91fbc2a5, RestServerActor): Handling next-prime request for number 3000000000
      
      





つまり、最初の要求が実行され、2番目の要求は待機中です。







 $ jobs [1]-  curl http://localhost:8080/next-prime/3000000000 & [2]+  curl http://localhost:8080/next-prime/3000000000 &
      
      





これはまさに説明された状況です。1つのコアが欠落しています。 もっとコアが必要です!







ショータイム!



だから、自慰行為をする時間です。 以降のすべてのアクションでは、コードの変更は不要です。







まず、 PrimeFinderActor



を個別のサブプロセスに分離しましょう。 それ自体では、このアクションはほとんど役に立たないが、私は徐々に紹介したい。







プロジェクトのルートディレクトリに次の内容のactors.json



ファイルを作成します。







 { "PrimeFinderActor": { "mode": "forked" } }
      
      





そして例を再起動します。 どうした プロセスのリストを確認します。







 $ ps ax | grep nodejs 12917 pts/19 Sl+ 0:00 nodejs prime-finder.js 12927 pts/19 Sl+ 0:00 /usr/bin/nodejs /home/weekens/workspace/comedy-examples/node_modules/comedy/lib/forked-actor-worker.js PrimeFinderActor $ pstree -a -p 12917 nodejs,12917 prime-finder.js ├─nodejs,12927 /home/weekens/workspace/comedy-examples/node_modules/comedy/lib/forked-actor-worker.js PrimeFinderActor │ ├─{V8 WorkerThread},12928 │ ├─{V8 WorkerThread},12929 │ ├─{V8 WorkerThread},12930 │ ├─{V8 WorkerThread},12931 │ └─{nodejs},12932 ├─{V8 WorkerThread},12918 ├─{V8 WorkerThread},12919 ├─{V8 WorkerThread},12920 ├─{V8 WorkerThread},12921 ├─{nodejs},12922 ├─{nodejs},12923 ├─{nodejs},12924 ├─{nodejs},12925 └─{nodejs},12926
      
      





現在、2つのプロセスがあることがわかります。 1つは、メインの「スタートアップ」プロセスです。 2番目は、 "forked"



モードで動作するため、 PrimeFinderActor



スピンする子プロセスです。 コードで何も変更せずに、 actors.json



ファイルを使用してこれを構成しました。







結果はそのような写真です:







画像







テストを再度実行します。







 $ curl http://localhost:8080/next-prime/3000000000 & [1] 13240 $ curl http://localhost:8080/next-prime/3000000000 & [2] 13242
      
      





ログを確認します。







 Tue Aug 08 2017 08:54:41 GMT+0300 (MSK) - info: InMemoryActor(5989504694b4a23275ba5d29, RestServerActor): Handling next-prime request for number 3000000000 Tue Aug 08 2017 08:54:43 GMT+0300 (MSK) - info: InMemoryActor(5989504694b4a23275ba5d29, RestServerActor): Handling next-prime request for number 3000000000
      
      





良いニュース:すべてがまだ機能します。 悪いニュース:ほとんどすべて以前と同じように機能します。 カーネルはまだ対処できず、リクエストはキューに入れられます。 カーネルに子プロセスがロードされるようになりました(PIDに注意してください):







  PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 12927 weekens 20 0 907160 40892 20816 R 100,0 0,5 0:20.05 nodejs
      
      





さらにプロセスを実行しましょうPrimeFinderActor



最大4つのインスタンスにクラスターPrimeFinderActor



します。 actors.json



変更しactors.json









 { "PrimeFinderActor": { "mode": "forked", "clusterSize": 4 } }
      
      





サービスを再起動します。 何が見えますか?







 $ ps ax | grep nodejs 15943 pts/19 Sl+ 0:01 nodejs prime-finder.js 15953 pts/19 Sl+ 0:00 /usr/bin/nodejs /home/weekens/workspace/comedy-examples/node_modules/comedy/lib/forked-actor-worker.js PrimeFinderActor 15958 pts/19 Sl+ 0:00 /usr/bin/nodejs /home/weekens/workspace/comedy-examples/node_modules/comedy/lib/forked-actor-worker.js PrimeFinderActor 15963 pts/19 Sl+ 0:00 /usr/bin/nodejs /home/weekens/workspace/comedy-examples/node_modules/comedy/lib/forked-actor-worker.js PrimeFinderActor 15968 pts/19 Sl+ 0:00 /usr/bin/nodejs /home/weekens/workspace/comedy-examples/node_modules/comedy/lib/forked-actor-worker.js PrimeFinderActor
      
      





4つの関連プロセスがありました。 簡単な設定変更で、階層を変更しました。これは次のようになります。







画像







つまり、コメディはPrimeFinderActor



を4つのピースに乗算し、それぞれが個別のプロセスで起動され、これらのアクターと親RestServerActor



間に、ラウンドロビンで子アクターへのリクエストを分散させる中間アクターをRestServerActor



ます。







テストを実行します。







 $ curl http://localhost:8080/next-prime/3000000000 & [1] 20076 $ curl http://localhost:8080/next-prime/3000000000 & [2] 20078
      
      





そして、2つのカーネルが占有されていることがわかります。







  PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 15953 weekens 20 0 909096 38336 20980 R 100,0 0,5 0:13.52 nodejs 15958 weekens 20 0 909004 38200 21044 R 100,0 0,5 0:12.75 nodejs
      
      





アプリケーションログには、同時に処理される2つのリクエストがあります。







 Tue Aug 08 2017 11:51:51 GMT+0300 (MSK) - info: InMemoryActor(5989590ef554453e4798e965, RestServerActor): Handling next-prime request for number 3000000000 Tue Aug 08 2017 11:51:52 GMT+0300 (MSK) - info: InMemoryActor(5989590ef554453e4798e965, RestServerActor): Handling next-prime request for number 3000000000 Tue Aug 08 2017 11:57:24 GMT+0300 (MSK) - info: InMemoryActor(5989590ef554453e4798e965, RestServerActor): Handled next-prime request for number 3000000000, result: 3000000019 Tue Aug 08 2017 11:57:24 GMT+0300 (MSK) - info: InMemoryActor(5989590ef554453e4798e965, RestServerActor): Handled next-prime request for number 3000000000, result: 3000000019
      
      





スケーリングは機能します!







より多くのコア!



サービスは、素数を見つけるための4つのリクエストを並行して処理できるようになりました。 残りの要求はキューに入れられます。 私のマシンには4つのコアしかありません。 より多くの同時要求を処理する場合は、近隣のマシンにスケールする必要があります。 やりましょう!







まず、少しの理論。 前の例では、 PrimeFinderActor



PrimeFinderActor



モードに設定しました。 各アクターは、次の3つのモードのいずれかになります。









ご理解のとおり、今度はPrimeFinderActor



"forked"



モードから"remote"



に転送する必要があります。 このスキームを取得したい:







画像







actors.json



ファイルを編集しましょう。 この場合、 "remote"



モードを指定するだけでは十分ではありません。アクターを実行するホストを指定する必要があります。 アドレス192.168.1.101



隣のマシンがあります。 私はそれを使用します:







 { "PrimeFinderActor": { "mode": "remote", "host": "192.168.1.101", "clusterSize": 4 } }
      
      





トラブルのみ:この非常に隣接したマシンは、コメディについて何も知りません。 既知のポートで特別なリスナープロセスを実行する必要があります。 これは次のように行われます。







 $ ssh weekens@192.168.1.101 ... weekens@192.168.1.101 $ mkdir comedy weekens@192.168.1.101 $ cd comedy weekens@192.168.1.101 $ npm install comedy ... weekens@192.168.1.101 $ node_modules/.bin/comedy-node Thu Aug 10 2017 19:29:51 GMT+0300 (MSK) - info: Listening on :::6161
      
      





これで、リスナープロセスは、既知のポート6161



アクター作成要求を受け入れる準備ができました。 私達は試みます:







 $ nodejs prime-finder.js
      
      





 $ curl http://localhost:8080/next-prime/3000000000 & $ curl http://localhost:8080/next-prime/3000000000 &
      
      





私たちはローカルマシンでトップに見えます。 アクティビティなし(Chromiumを除く):







 $ top PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 25247 weekens 20 0 1978768 167464 51652 S 13,6 2,2 32:34.70 chromium-browse
      
      





リモートマシンを見てみましょう。







 weekens@192.168.1.101 $ top PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 27956 weekens 20 0 908612 40764 21072 R 100,1 0,1 0:14.97 nodejs 27961 weekens 20 0 908612 40724 21020 R 100,1 0,1 0:11.59 nodejs
      
      





必要に応じて整数を計算しています。







小さなタッチが1つだけあります。ローカルマシンとリモートマシンの両方でカーネルを使用します。 それは非常に単純です: actors.json



1つのホストではなく、いくつかのホストを指定します。







 { "PrimeFinderActor": { "mode": "remote", "host": ["127.0.0.1", "192.168.1.101"], "clusterSize": 4 } }
      
      





コメディは、指定されたホスト間でアクターを均等に配布し、ラウンドロビンメッセージを提供します。 見てみましょう。







まず、ローカルマシンでリスナープロセスを追加で実行します。







 $ node_modules/.bin/comedy-node Fri Aug 11 2017 15:37:26 GMT+0300 (MSK) - info: Listening on :::6161
      
      





次に例を実行します。







 $ nodejs prime-finder.js
      
      





ローカルマシン上のプロセスのリストを見てみましょう。







 $ ps ax | grep nodejs 22869 pts/19 Sl+ 0:00 /usr/bin/nodejs /home/weekens/workspace/comedy-examples/node_modules/comedy/lib/forked-actor-worker.js PrimeFinderActor 22874 pts/19 Sl+ 0:00 /usr/bin/nodejs /home/weekens/workspace/comedy-examples/node_modules/comedy/lib/forked-actor-worker.js PrimeFinderActor
      
      





そして、リモートマシン上で:







 192.168.1.101 $ ps ax | grep node 5925 pts/4 Sl+ 0:00 /usr/bin/nodejs /home/weekens/comedy/node_modules/comedy/lib/forked-actor-worker.js PrimeFinderActor 5930 pts/4 Sl+ 0:00 /usr/bin/nodejs /home/weekens/comedy/node_modules/comedy/lib/forked-actor-worker.js PrimeFinderActor
      
      





必要に応じて、それぞれ2つ(さらに必要clusterSize



増やしclusterSize



)。 リクエストを送信します:







 $ curl http://localhost:8080/next-prime/3000000000 & [1] 23000 $ curl http://localhost:8080/next-prime/3000000000 & [2] 23002
      
      





ローカルマシンでの読み込みを確認します。







  PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 22869 weekens 20 0 908080 40344 21724 R 106,7 0,5 0:07.40 nodejs
      
      





リモートマシンでの読み込みを確認します。







  PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 5925 weekens 20 0 909000 40912 21044 R 100,2 0,1 0:14.17 nodejs
      
      





各マシンに1つのコアがロードされます。 つまり、負荷を両方のマシンに均等に分散するようになりました。 1行のコードを変更せずにこれを達成したことに注意してください。 コメディと俳優のモデルがこれを助けてくれました。







おわりに



Node.JS- Comedyでのアクターモデルとその実装を使用した、アプリケーションの柔軟なスケーリングの例を見てみました。 アクションのアルゴリズムは次のとおりです。







  1. アクターの観点からアプリケーションを説明してください。
  2. 多くの利用可能なCPUコアに負荷を均等に分散するようにアクターを構成します。


アクターの観点からアプリケーションを説明する方法は? これは、「オブジェクトとクラスの観点からアプリケーションを記述する方法」という質問に類似しています。 演技プログラミングはOOPに非常に似ています。 これはOOP ++であると言えます。 OOPには、定評のあるさまざまな成功したデザインパターンがあります。 同様に、アクターモデルには独自のパターンがあります。 ここにそれらのがあります。 これらのパターンは使用でき、確かに役立ちますが、すでにOOPを所有している場合は、アクターに問題はありません。







アプリケーションがすでに作成されている場合はどうなりますか? 「アクターに書き換える」必要がありますか? もちろん、この場合はコードの変更が必要です。 しかし、大規模なリファクタリングを行う必要はありません。 いくつかの主要な「大」アクターがあり、その後、すでにスケーリングできます。 「大きな」アクターは、最終的に小さなアクターに分割できます。 繰り返しますが、アプリケーションが既にOOPの用語で記述されている場合、アクターへの切り替えはほとんどの場合簡単です。 作業しなければならない唯一のことは、単純なオブジェクトとは異なり、アクターが互いに完全に隔離されていることです。







フレームワークの成熟度について。 コメディの最初の作業バージョンは、2016年6月にSAYMONプロジェクト内で開発されました。 最初のバージョンから、フレームワークは戦闘条件下で本番環境で機能しました。 2017年4月、ライブラリはEclipse Public Licenseの下でオープンソースでリリースされました。 同時に、コメディは引き続きSAYMONの一部であり、システムのスケーリングとフォールトトレランスの確保に使用されます。







計画されている機能のリストはこちらです。







この記事では、一連のコメディ機能については言及しませんでした。フォールトトレランス(アクターの「リスポーン」)、アクターへのリソースの注入、名前付きクラスター、カスタムクラスのマーシャリング、TypeScriptサポートについてです。 しかし、上記のほとんどはドキュメントにあり、まだ説明されていないのはテストです。 さらに、トピックが大衆向けであれば、Node.JSでコメディと俳優についての記事をもっと書くかもしれません。







コメディを使用してください! 課題を作成してください! あなたのコメントを待っています!








All Articles