例としてFirebirdを使用した同期と非同期

画像



この出版物では、いくつかの目標を設定しました。







その後、多くのコード...





テストタスクは非常に単純に見えます。

クエリ結果を返す

select * from rdb$relations
      
      





JSON形式。 私のテストベースでは、これは約19 kbのサイズの回答になります。



このタスクを実装するためのさまざまなオプションを作成し、Apache Benchmarkを使用して、最適なオプションを見つけます。 次のコマンドでパフォーマンスをテストします。

  ab -n 10000 -c 5 http:// localhost:1337 / 




記事を読み過ぎないように、ベンチマークの結論全体を引用するつもりはありません。 1秒あたりの処理済みリクエストの平均数(RPS)は、ほとんどの場合、結論を引き出すのに十分です。



したがって、アクセスライブラリへのすべての呼び出しが同期である場合、最も単純なオプションから始めましょう。



 var cfg = require("../config").cfg; var fb = require('../../firebird'); var util = require('util'); var http = require('http'); http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); var con = fb.createConnection(); con.connectSync(cfg.db, cfg.user, cfg.password, cfg.role); var rs = con.querySync('select * from rdb$relations'); var rows = rs.fetchSync("all",true); con.disconnect(); res.write('['); rows.forEach(function(r){ res.write(JSON.stringify(r)+','); }); res.end(']'); }).listen(1337, "127.0.0.1"); console.log('Server running at http://127.0.0.1:1337/');
      
      







転送された合計:199310000バイト

転送されるHTML:198670000バイト

1秒あたりのリクエスト:52.47 [#/秒](平均)



すべてのスクリプトバリアントが同じ結果を生成するように、転送バイト数を残しました。

Firebirdアクセスライブラリを使用すると、すべての呼び出しを非同期に置き換えることができます。 これがどのように見えるかです:



 var cfg = require("../config").cfg; var fb = require('../../firebird'); var util = require('util'); var http = require('http'); http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); var con = fb.createConnection(); con.connect(cfg.db, cfg.user, cfg.password, cfg.role,function(){ con.query('select * from rdb$relations',function(err,rs){ res.write('['); rs.fetch("all",true,function(r){ res.write(JSON.stringify(r)+','); }, function(err){ con.disconnect(); res.end(']'); }); }); }); }).listen(1337, "127.0.0.1"); console.log('Server running at http://127.0.0.1:1337/');
      
      







転送された合計:199310000バイト

転送されるHTML:198670000バイト

1秒あたりのリクエスト:129.26 [#/秒](平均)



同期呼び出しを非同期呼び出しに簡単に置き換えることで、最大で生産性が向上しました

1秒あたり129リクエスト対52。非同期アプローチでは、リクエストを並行して実行し、クライアントごとに個別の接続を確立できます。 さらに、同期バージョンでは、クエリ結果全体が最初にメモリに書き込まれ、次にクライアントに渡されます。 非同期では、結果がデータベースからオンザフライで選択されるのと同じ結果の行がクライアントに返されます。



ただし、このコードにはまだ最適化できる次善のポイントがあります。 たとえば、これは各クライアント接続が新しいデータベース接続を作成することです。 極端な場合として、単一の接続でうまくやってみましょう。 最初は同期バージョンの場合:



 var cfg = require("../config").cfg; var fb = require('../../firebird'); var util = require('util'); var http = require('http'); var con = fb.createConnection(); con.connectSync(cfg.db, cfg.user, cfg.password, cfg.role); http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); if(!con.inTransaction) con.startTransactionSync(); var rs = con.querySync('select * from rdb$relations'); var rows = rs.fetchSync("all",true); res.write('['); rows.forEach(function(r){ res.write(JSON.stringify(r)+','); }); res.end(']'); con.commitSync(); }).listen(1337, "127.0.0.1"); console.log('Server running at http://127.0.0.1:1337/');
      
      







転送された合計:199310000バイト

転送されるHTML:198670000バイト

1秒あたりのリクエスト:159.55 [#/秒](平均)



ご覧のとおり、このオプションは以前の非同期オプションよりも高速です。 データベースへの接続の確立は、かなり時間がかかる操作です。 接続の再利用は、よく知られた確立された手法です。 サービスを利用する価値があります。



次に、非同期を追加してみてください。 次のコードは明らかに最適ではありません。単一の接続と非同期を使用すると、リクエストを並行して実行できません。 しかし、実験の純度のために...



 var cfg = require("../config").cfg; var fb = require('../../firebird'); var util = require('util'); var http = require('http'); var con = fb.createConnection(); con.connectSync(cfg.db, cfg.user, cfg.password, cfg.role), busy = false, next = []; http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); function doReq(){ con.query('select * from rdb$relations',function(err,rs){ res.write('['); rs.fetch("all",true,function(r){ res.write(JSON.stringify(r)+','); }, function(err){ res.end(']'); con.commit(function(){ busy = false; var n = next.pop(); if(n) n(); }); }); }); } function process(){ busy = true; if(!con.inTransaction) con.startTransaction(doReq); else doReq(); } if(busy) next.push(function(){ process(); }); else process(); }).listen(1337, "127.0.0.1"); console.log('Server running at http://127.0.0.1:1337/');
      
      







転送された合計:199310000バイト

転送されるHTML:198670000バイト

1秒あたりのリクエスト:129.85 [#/秒](平均)



非同期オプションはかなり大きく見えます。 そして、毎秒合計129のクエリを提供します。 問題は、Firebirdクライアントライブラリが設計による非同期呼び出しをサポートしていないため、単一の接続を使用しても非同期フェッチのメリットが得られないことです。 NodeJSのFirebirdモジュールは、libuvに組み込まれたプールを使用して、並列スレッドですべての非同期呼び出しを開始します。 単一の接続を使用して同時クエリを実行することはできません。 したがって、コードにはビジー接続のチェックと呼び出しのキューが含まれます。 どうやら、この場合、キューの編成が減速要因でした。 結局、同期バージョンはより高速でした。 ただし、クライアントごとに個別のデータベース接続を使用することも常に最適とは限りません。 論理的な解決策は、接続プールを使用することです。



 var cfg = require("../config").cfg; var fb = require('../../firebird'); var util = require('util'); var events = require('events'); var http = require('http'); function ConnectionPool() { events.EventEmitter.call(this); this.conns = []; this.busy = []; this.MaxConns = 5; this.newConn = function(){ var c = fb.createConnection(); c.connectSync(cfg.db, cfg.user, cfg.password, cfg.role); this.conns.push(c); }; this.get = function(cb) { var self = this; var c = this.conns.pop(); if(c) { this.busy.push(c); cb(c); } else if((this.busy.length) >= this.MaxConns){ this.once('release',function(){ self.get(cb); }); } else { this.newConn(); this.get(cb); } }; this.release = function(con){ for(var i=0;i<this.busy.length;i++) { if(this.busy[i] == con){ this.conns.push(this.busy[i]); this.busy.splice(i,1); var self = this; process.nextTick(function(){ self.emit('release'); }); return; } } }; } util.inherits(ConnectionPool, events.EventEmitter); var pool = new ConnectionPool(); pool.setMaxListeners(2000); function exec(con,res){ con.query('select * from rdb$relations',function(err,rs){ res.write('['); rs.fetch("all",true,function(r){ res.write(JSON.stringify(r)+','); },function(err){ res.end(']'); con.commit(function(){ pool.release(con); }); }); }); }; http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); pool.get(function(con){ if(!con.inTransaction) con.startTransaction(function(err){ if(!err) exec(con,res); }); else exec(con,res); }); }).listen(1337, "127.0.0.1"); console.log('Server running at http://127.0.0.1:1337/');
      
      







転送された合計:199310000バイト

転送されるHTML:198670000バイト

1秒あたりのリクエスト:187.82 [#/秒](平均)



多くのコードがありましたが、パフォーマンスの向上は明らかです。 さらに、接続プーリングコードは再利用のために別のモジュールに移動でき、データベース操作コードはシンプルでコンパクトなままです。



しかし、さらに先へ進むことができます。 多くのクライアントが同じリクエストを使用する場合、Firebirdでは、リソースを割り当ててリクエストを解析する操作を繰り返すことなく、リクエストのリソースを準備して何度も使用できます。 そのため、準備済みクエリのプールを使用する方が最適なようです。



 var cfg = require("../config").cfg; var fb = require('../../firebird'); var util = require('util'); var events = require('events'); var http = require('http'); function StatementPool() { events.EventEmitter.call(this); this.conns = []; this.busy = []; this.MaxConns = 5; this.newConn = function(){ var c ={ conn: fb.createConnection() }; c.conn.connectSync(cfg.db, cfg.user, cfg.password, cfg.role); c.stmt = c.conn.prepareSync('select * from rdb$relations'); this.conns.push(c); }; this.get = function(cb) { var self = this; var c = this.conns.pop(); if(c) { this.busy.push(c); cb(c); } else if((this.busy.length) >=this.MaxConns){ this.once('release',function(){ self.get(cb); }); } else { this.newConn(); this.get(cb); } }; this.release = function(con){ for(var i=0;i<this.busy.length;i++) { if(this.busy[i] == con){ this.conns.push(this.busy[i]); this.busy.splice(i,1); var self = this; process.nextTick(function(){ self.emit('release'); }); return; } } }; } util.inherits(StatementPool, events.EventEmitter); var pool = new StatementPool(); pool.setMaxListeners(2000); http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); pool.get(function(con){ var exec = function(){ con.stmt.exec(); con.stmt.once('result',function(err){ res.write('['); con.stmt.fetch("all",true,function(r){ res.write(JSON.stringify(r)+','); }, function(err){ res.end(']'); con.conn.commit(function(){ pool.release(con); }); }); }); }; if(!con.conn.inTransaction) con.conn.startTransaction(function(err){ if(!err) exec(); }); else exec(); }); }).listen(1337, "127.0.0.1"); console.log('Server running at http://127.0.0.1:1337/');
      
      







転送された合計:199310000バイト

転送されるHTML:198670000バイト

1秒あたりのリクエスト:214.37 [#/ sec](平均)



大量のコードにもかかわらず、このオプションは1秒あたり210を超えるリクエストを提供します。



複数の接続を同時に使用すると、準備された要求と非同期呼び出しにより、完全同期バージョンと比較して少なくとも4倍のゲインが得られます。 ただし、この数値はテスト環境にのみ当てはまります。 また、NodeJSの非同期性だけではデータベースサーバーのパフォーマンスが向上するわけではなく、それを最大限にロードするのに役立つことを理解する必要があります。 この場合、256 MBのメモリと1つのプロセッサコアがデータベースサーバーに割り当てられました。これは、一般に小さなRPSについて説明しています。 ただし、メモリ負荷は3%を超えませんでした。 また、非同期アプローチにより、データベースクエリに関連しない他のクエリの並列処理が可能になります。 これらのテストでは、そのような要求はありませんでした。 RPSの数に最も影響を与えた要因は、クエリサンプルのサイズです。 そのため、サンプルを10レコード(結果のJSONで約1 kb)に制限し、4つのプロセッサコアをデータベースサーバーに割り当てながら、約1000 RPSを取得できましたが、この場合の同期バージョンも約200 RPSを生成しました。 1秒間に1000個のデータベースクエリを実行する非同期オプションは、テンプレートに従って同時にHTMLを描画できますが、同期オプションはそうではないことを考慮する価値があります。

すべてのテストスクリプトはNodeJSのFirebirdアクセスモジュールの一部です

githubで入手できます。



All Articles