Puppeteer + Fargate で負荷テストするお話
今回は他のWebRTC負荷テストツールやサービスについては扱いません。
それなりにコストがかかるのと、やっぱりブラウザでの動作するか検証したいからです。
「テストするのでこのURL開いてください。ひとり10タブで」
一般的なWebサイトと違い、WebRTCや動画配信サービスで負荷テストをするのはなかなか大変です。
人数が多い会社では、上記のようにアナウンスして協力して貰えば100接続くらいはなんとかなります。
ただし、社内LANに負荷をかけても大丈夫か、あるいはいろんな経路で試したい、など痒いところに手が届きません。
AWSBatch + Fargateでお手軽に300接続したい!
経路はアマゾンからになってしまいますが、Batch + Fargateを使えばお手軽にヘッドレスChromeで接続することができます。
EC2インスタンスだと100台制限の緩和申請など必要ですが、Fargateだともっとたくさん起動できます。
dockerファイルが書きたい!
puppeteer docker で検索すると大体同じです。
FROM alpine:edge
RUN apk update
# japanese font
RUN apk add --no-cache curl fontconfig font-noto-cjk \
&& fc-cache -fv
# Installs latest Chromium (76) package.
RUN apk add --no-cache \
chromium \
nss \
freetype \
freetype-dev \
harfbuzz \
ca-certificates \
ttf-freefont \
nodejs \
yarn
# timezone
RUN apk add --update --no-cache tzdata && \
cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
echo "Asia/Tokyo" > /etc/timezone && \
apk del tzdata
# Tell Puppeteer to skip installing Chrome. We'll be using the installed package.
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
WORKDIR /app
COPY ./package.json /app/package.json
COPY ./yarn.lock /app/yarn.lock
RUN yarn
COPY main.js /app
CMD ["yarn", "start"]
あとは ECRにpushしておきます。
Puppeteerで接続したい!
ちなみに、AWSBatchでは引数を渡すことはできず、他プロセスと識別するにはインデックスを使います。
const uid = process.env.AWS_BATCH_JOB_ARRAY_INDEX
? "test_"+process.env.AWS_BATCH_JOB_ARRAY_INDEX
: "local";
puppeteerで接続する処理は基本的なものと同じです。
const puppeteer = require('puppeteer'),
WebinarURL = "動作確認したいページのURL"
... なんか初期化とか
(async() => {
try{
const browser = await puppeteer.launch({
executablePath: "/usr/bin/chromium-browser",
headless: true,
ignoreHTTPSErrors: true,
defaultViewport: null,
dumpio: false,
args: ['--disable-setuid-sandbox', '--no-sandbox'] // ←これ大事
});
const page = await browser.newPage();
// 必要ならログインとかする
// WebRTCのページにアクセスする
await page.goto(WebinarURL);
await sleep(500);
...
while(true) {
// 動画が動いているか確認
// ログ出力
sleep(60); // 一分毎にチェックする
}
await browser.close();
} catch (err) {
// catchしておかないとプロセスが終了しない
console.log(err);
}
})();
package.jsonはとてもシンプル
{
"name": "client_sama_sama",
"version": "1.0.0",
"main": "main.js",
"dependencies": {
"puppeteer": "5.5.0",
"aws-sdk": "2.824.0"
},
"scripts": {
"start": "node main.js"
},
"license": "UNLICENSED"
}
音声が出ているか確認したい!
無理でした。。
いや、音声データが流れていることを確認すればそれでも良いのですが
本当に音が出てるの? というのを確認したかったのです。
ところが、headless chromeだとオーディオキャプチャの実装がないとかで難しそうでした。
/* デスクトップオーディオを取得して、音がととしるかチェックする。だが、DOMException: Not supported
* issue : https://github.com/puppeteer/puppeteer/issues/4404
* やり方 : https://paul.kinlan.me/ja/screen-recorderrecording-microphone-and-the-desktop-audio-at-the-same-time/
if (!window.analyser) {
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
// ノード作成
const desktopStream = await navigator.mediaDevices.getDisplayMedia({ video:true, audio: true });
const audioSource = audioCtx.createMediaStreamSource(desktopStream);
const desktopGain = context.createGain();
desktopGain.gain.value = 0.7;
const analyser = audioCtx.createAnalyser();
// 接続
audioSource.connect(desktopGain).connect(analyser).connect(audioCtx.destination);
window.analyser = analyser;
}
const analyser = window.analyser;
// データから音声レベルを取得
const frequencies = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(frequencies);
const audioLevel = frequencies.reduce(function(previous, current) {
return previous + current;
}) / analyser.frequencyBinCount;
// analyser.frequencyBinCount
*/
動画が止まっていないか確認したい!
キャンパスにコピーして、0(真っ暗でないか)、前回と同じでないか確認します。
最初の一回、固まった後の復帰、などなど考えると意外と面倒ですがそこは割愛します。
// ブラウザ上でjavascriptを実行する
const {ログ出力したい戻り値をここで受け取る...} = await page.evaluate(async() => {
// ここはブラウザ上
// 0出ないピクセルがあればなにか写ってる
function isCanvasSome(ctx) {
return canvas.getContext("2d")
.getImageData(0, 0, canvas.width, canvas.height).data
.some(channel => channel !== 0);
}
const video = document.getElementById("remotevideo-Camera");
if (!video || video.videoWidth === 0) {
return ...;
}
if (!window.tempCanvas) {
window.tempCanvas = document.createElement("canvas");
}
const canvas = window.tempCanvas;
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const videoCtx = canvas.getContext("2d").drawImage(video, 0, 0, video.videoWidth, video.videoWidth);
// 音声チェックしたかったけど断念
return {videoWidthとかgetStats()した戻り値とか}
});
// 戻り値をログ出力する
CloudWatchでグラフ化がしたい!
cloudwatchに投げてやればいい感じでグラフにできます。
60秒単位より細かく撮ろうとするとコストが上がるので、とりあえず60秒で良いと思います。
- 動画の横幅 width
- ビットレート
- 動画が動いているかフラグ
- その他 RTCPeerConnection.getStats() で取れるパケロスとかジッターとかお好きなもの
すみません。
あまりディメンジョンやら概念が分かっておらず、
とりあえず以下の設定で、動いているか? とwidthがインスタンス毎にグラフ化されます。
大体、配信サーバーを再起動したり帯域上限に引っかかるとwidthが安定しないのが見てとれます。
const cw = new AWS.CloudWatch({apiVersion: '2010-08-01'});
cw.putMetricData({
MetricData: [{
MetricName: 'VideoOkCount',
Dimensions: [
{
Name: 'name',
Value: uid,
},
],
Timestamp: new Date(),
Unit: 'Count',
Value: (videoOK && isMoving ? 1 : 0),
},{
MetricName: 'VideoWidth',
Dimensions: [
{
Name: 'name',
Value: uid,
},
],
Timestamp: new Date(),
Unit: 'Count',
Value: videoWidth,
}],
Namespace: 'ClinetSamaSama/LoadTest'
}, (err, data) => { if (err) { console.log(err); } });
AWSの環境構築がしたい!
長くなるので要点だけ記載します。
- API用のアクセスキーを作る
- Batch用のロールを作る
- VPCを作る。
- ステージング環境のアクセス制限がかかっている場合は、VPCエンドポイントで繋ぐなりピアで繋ぐなりセキュリティグループ当てるなりして疎通させる
- Batchのコンピューティング環境を作る
- vCPU : インスタンスの上限数にする (1インスタンス1vCPUで良い)
- ジョブ定義を作る
- ロールを作る AmazonECSTaskExecutionRolePolicyをアタッチ
- パブリックIPを有効にしておく
- イメージ : pushしたdocker
- mem : 2GBは欲しいところ
- ジョブキューを作る
- タスク数をお好きなものにして実行する
- CloudWatchにアクセスし、グラフを見て思いを巡らせる。
- 注意 : タスクが終了していても課金され続けることがあるので、コンピューティング環境も削除すると安心です。(使い方を間違っているだけかもしれません)
独り言
2,3年のブランクを経て再びWebRTCをやる機会をいただきました。
手が掛かる子ほど可愛いといいますか、やっぱWebRTCいいよなーと思います。
Unix Network Programing と TCP Illustrated と マスタリングTCP/IP RTP編 を読みつつ (全て絶版。。)
そろそろRustでなんか書こうかなー
音声や映像をリアルタイムにサーバー側でAIと繋ぎ込む需要ありそうだなー
クラウドネイティブでスケールするミドルウェアも書きたいなー
と思う今日この頃です。