ストレスツールといえば、ab、jmeter、gatlingなどといろいろあるようだが、例えば既存サイトのアクセスログ一式があるとして、このアクセス負荷を再現する&できるだけ深く考えたくない&実際どのような負荷がかかっているか・サーバが応答したのかをトレースしたいとなると、いずれも帯に短かしたすきに長しという印象。
ということで悩んでいたら、最近はvegetaというのがある模様。
これはなかなか都合が良いかもしれない。
それはそれとして、vegetaを見つける前&Go言語を使うことができない状況だったため、結局のところ次のようなツールを作って対応することにした。
既製品に比べると不完全だったり、これ自体の性能がどこまで出るかはわからないが、取り回しの良さは気に入っている。
... と取り回しの良さを自画自賛(?)しておきながら、いざコードを見たら何をやっているか分からない(苦笑)。
おそらく次のようなスペックと思われる。(自分の書いたコードなのに)
- 接続先のアドレスと前の行のアクセス発生からの本行のアクセスを発生させるまでのインターバルを指定した複数行からなる負荷パターンの設定ファイルをインプットにする。このファイルはapacheのアクセスログから簡単なワンライナーで作成できるレベルのものを想定。
- インプットファイルで指定のものを1サイクルとして、何回サイクルを回すかを指定できるようにする。
- インターバルに従ってアクセスを増やしていった場合に、意図したものより負荷がかかりすぎて積滞&サチってしまうことで実際のパフォーマンスがぼやけてしまうという経験がある。この理解が正しいかはともかく、あえて最大多重度を指定することで、どこまでは確実にイケるという線引きをしやすくするよう考えた。実際の負荷のかけ方としては、最大多重度で指定した件数分だけアクセスをエントリする。これらの1束のエントリのアクセスの全てのレスポンスが返るまでは、次の1束のエントリはしない。
- 限界を確認したい場合など、元のアクセスログの数倍の負荷を与えたいことがある。これに対応できるように、インターバルの倍率を指定できるようにする。
言い忘れたが、環境・言語はnode.js。上記の特徴に、setTimeOutやPromissがちょうど良いように感じたため。
ただ、今見るとPromissの動きが全然あたまに入ってこない.....
myStressTool.js
/* 全体的にハードコーディング御免。場当たり記述でビジネスロジックがぼやけているが用途に合わせたモノリシックということで。 */ 'use strict'; const MAX_MULTIPLICITY = parseInt(process.argv[2]) || 2; const TRIAL = parseInt(process.argv[3]) || 5; const URLFILE = process.argv[4] || './DEFAULT.csv'; const BOOST = parseInt(process.argv[5]) || 1; const HTTPS = process.argv[6] === undefined ? require('https') : require('http'); /* console.logのエイリアス */ const LOG = console.log; /* アクセスログ風のINPUTファイルをCONFIG情報として読み込む */ // INPUTファイルの形式:タブ区切り3項目 // (1) WWWサーバ (2)ルート相対パス (3)1行前のアクセスからの待機時間 // example.com /foo.html 0.05 // example.com /foo.html 0.02 // example.com /foo3.html 0.08 // ... const CONFIG = (function(){ const fs = require('fs') const text = fs.readFileSync(URLFILE,'utf8').split(/\r\n|\r|\n/); LOG('最大多重度:' + MAX_MULTIPLICITY); LOG('試行件数:' + TRIAL); if (text.length < MAX_MULTIPLICITY || text.length < TRIAL ) { LOG('ERROR:INPUTファイルのレコード件数が設定条件の数値に満たないので、処理せずに終了'); process.exit(1); } const obj = { getInterval : (i) => { return parseFloat(text[i].split(/\t/)[2]); }, getOpt : (i) =>{ const line = text[i].split(/\t/); const ua = line[3] || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.109 Safari/537.36'; return { hostname: line[0], path: line[1], headers:{'User-Agent': ua} }; }, }; return obj; })(); function bytes2(str) { return(encodeURIComponent(str).replace(/%../g,"x").length); } /* 指定のurlにintrvlミリ秒後に非同期でアクセスする */ //ただし、javascriptのsetTimeoutなのであくまで目安 function access(opt,intrvl) { return new Promise(resolve => { let body = ''; setTimeout(()=>{ let s_time = (+new Date()); //ミリ秒取得 HTTPS.get(opt, (res) => { const sts = res.statusCode; res.on('data', (d) => { body += d; }); res.on('end', (res) => { LOG('Status:' + '\t' + opt.path + '\t' + sts); LOG('Bytes:' + '\t' + bytes2(body)); resolve(sts); LOG('Result:' + '\t' + opt.path + '\t' + ((+new Date()) - s_time)/1000 + '\t' + '秒'); }); }).on('error', (e) => { LOG('ERROR:' + opt.path); LOG(e); }); //HTTPS.get },intrvl); // setTimeout }); } /* 設定ファイルで指定のインターバルを空けながら非同期アクセスを行い、負荷をかける。ただし、所定の最大多重度の範囲以上のエントリはしない */ async function stress(stt,end){ if(stt + MAX_MULTIPLICITY > TRIAL){ LOG('試行終了:' + stt + '件目まで実施'); process.exit(0); } LOG('START:' + (stt + 1) + '件目以降のグループ'); let ps = []; let intrvl = 0; for(var i = stt; i < end; ++i){ // 設定ファイル中の所定の範囲の行に対応する非同期アクセスを発生させる intrvl += Math.floor(CONFIG.getInterval(i) * 1000 / BOOST ) + 1; // このセット分のintrvlを累計しつつ設定する ps[i-stt] = access(CONFIG.getOpt(i),intrvl); } var result = await Promise.all(ps); //このセットが全て完了するまで同期 LOG('END:' + end + '件目'); stress(end,end + MAX_MULTIPLICITY); } LOG('処理開始'); stress(0,MAX_MULTIPLICITY);
実行方法
$ node myStressTool.js 2 4 mini-access.log 1000 s 最大多重度:2 試行件数:4 処理開始 START:1件目以降のグループ Status: /x 404 Bytes: 199 Result: /x 0.01 秒 Status: / 200 Bytes: 45 Result: / 0.006 秒 END:2件目 START:3件目以降のグループ Status: / 200 Bytes: 45 Result: / 0.002 秒 Status: / 200 Bytes: 45 Result: / 0.001 秒 END:4件目 試行終了:4件目まで実施