はじめまして。東京工業大学情報理工学院情報工学系修士1年の山崎希と申します。9月5日から10月14日までの6週間、LINE株式会社の2022年インターンシップ「技術職 就業型コース」に参加させていただきました。
今回のインターンシップでは、LINE NEWSのSREを担当しているチームに配属され、LINE NEWSに対する負荷テストの自動化やLINE NEWSの管理画面のUI改善や移植を行いました。本レポートでは、LINE NEWSの概要について説明した後、負荷テストの自動化に焦点を当てて取り組んだ内容を紹介したいと思います。
概要
LINE NEWSは、LINEが提供しているニュース配信プラットフォームです。月間利用者数7,700万人、月間154億PVを超える(当社調べ / 2021年8月時点)サービスであるLINE NEWSでは、日々大量のトラフィックを捌く必要があります。
常に機能の追加・変更・削除が行われているLINE NEWSでは、本番環境へのリリースを行う前にBeta環境で動作確認などを行なっています。しかし、Beta環境では本番環境のような大量のトラフィックが生じることがないため、本番環境にリリースした後にパフォーマンスに問題があることが発覚するケースがあります。このような問題への対策として、一般的に負荷テストが用いられます。
負荷テスト
負荷テストとは、システムがさまざまな環境下で安定して動作するかを確認するための手法です。負荷テストには、ロードテスト(性能テスト)やストレステスト(限界テスト)など、複数の手法があります。ロードテストでは、システムに一定の負荷をかけて性能条件を満たしているかをテストします。具体的な例としては、システムに100人が同時にアクセスした際の応答時間を計測し、基準を満たしているかを確認するものが挙げられます。一方ストレステストでは、システムに掛ける負荷を増やしながら限界を確認し、限界に達した時のシステムの挙動を確認します。限界に達したシステムは、勝手に停止することなく、適切なエラーを出力することが望まれます。
今回負荷テストを導入する主な目的は、システムに一定の負荷がかかっている状況でパフォーマンスに問題があるかどうかを確認することです。そのため、今回のBeta環境への負荷テストにはロードテストを用います。
業務内容
リクエストの一覧作成
まず、負荷テストのシナリオに含めるリクエストの一覧を作成します。今回はクライアント側から実際に呼び出されるリクエストをもとに作成します。クライアント側で呼び出されるリクエストには、ページ読み込み時に自動的に呼び出されるAPIと、各記事へのリンクの2種類があります。ブラウザ上で動作するBeta環境のアプリでは、これらの情報をブラウザの機能を使って取得することができます。ここではGoogle Chromeの開発者ツールを用いてリクエストを取得してみます。
Google Chromeを用いたAPI一覧の取得
開発者ツールのNetworkタブを用いると、読み込んだページから呼び出されているAPIの一覧を見ることができます。実際にLINE NEWSの「トップ」タブを読み込んだときのAPIリクエストを表示してみます。
ここでは、237件のリクエストが送信されていることが分かります。このうち、APIのリクエストは81件になります。これら81件のRequest URLを調べれば、APIの一覧を得ることができます。
Google Chromeを用いたリンクの取得
各記事へのリンクは開発者ツールのConsoleタブでJavaScriptのプログラムを実行することで取得することができます。ここでは、ページ内リンクを含む138件のリンクが取得できてきます。
以上でクライアント側から実際に呼び出されるリクエストを取得することができました。しかし、LINE NEWSでは日々機能の追加・削除が行われているため、一連の流れを自動化して常に最新のリクエスト一覧を得られるようにする必要があります。
リクエスト一覧取得の自動化
リクエスト一覧取得の自動化には、Node.jsとPuppeteerを用います。Puppeteerを用いることで、Node.jsで実行されるプログラムからGoogle Chromeを制御することができます。ここでは、先述した開発者ツールのNetworkタブからのAPIリクエスト取得と、Consoleタブからの記事リンク取得をNode.jsで実行できるようにプログラムを記述します。
Puppeteer
const pptr = require("puppeteer");
async function getRequests() {
let apiRequests = [];
const browser = await pptr.launch({ headless: true });
const page = await browser.newPage();
const client = await page.target().createCDPSession();
await client.send("Network.enable");
// API 一覧の取得
client.on("Network.responseReceived", (params) => {
apiRequests.push(params.response.url);
});
await page.goto(config.captureRequest.url, {
waitUntil: "networkidle0",
});
// 記事リンク一覧の取得
let articleRequests = await page.evaluate(() =>
Array.from(document.querySelectorAll("a[href]"), (a) =>
a.getAttribute("href")
)
);
await browser.close();
// 結果の保存
}
シナリオの生成
リクエストの一覧を取得できたら、負荷テストに用いるシナリオを生成します。負荷テストは、実際のユーザーの行動を想定しながら実行するのが望ましいです。そのため、シナリオに用いるリクエスト先は各ユーザーの行動をもとに選択します。一般的にユーザーは次のような行動をとると考えられます。
- LINE NEWSのトップページを開く
- (クライアント側でAPIリクエストが送られる)
- 気になる記事をクリックし閲覧する
- 3を複数回繰り返す
そのため、シナリオに用いるリクエストは次の順番で定義します。
- LINE NEWSのトップページへのリクエストを送る
- 約80件のAPIに対して一斉にリクエストを送る
- ランダムに1件記事を選択してリクエストを送る
- 3を10回繰り返す
以上が1人のユーザーの行動としてシナリオに定義されます。
次にLINE NEWSにアクセスするユーザー数を定義します。Beta環境は本番環境よりもサーバーの規模が小さいため、同時にLINE NEWSにアクセスするユーザー数を20人として定義します。今回負荷テストのツールとして採用するk6では、このユーザーのことをVirtual User (VU)と呼びます。
続いて負荷をかける時間やVUの増やし方を定義します。今回は以下のように定義してみました。
- 最初の30秒間でVUを0人から20人に段階的に増やす
- 次の5分間はVU20人のまま負荷をかける
- 最後に30秒間かけてVUを20人から0人まで段階的に減らす
このように段階的にVU数を変化させているのは、各ユーザーがトップページにアクセスするタイミングのズレを再現したり、段階的に負荷をかけた際のパフォーマンスの変化を観測するためです。
最後に、負荷テスト時に満たすべき条件を設定します。条件は平常時の値を満たすよう、以下のように設定します。
- HTTPエラーは1%未満
- HTTPリクエストにかかった時間が150msを下回るものが95%以上
以上の条件を満たさない場合、パフォーマンスに問題があると判断します。
K6
import http from "k6/http";
import { check, sleep } from "k6";
function checkStatus(requests, responses) {
for (let i = 0; i < responses.length; i++) {
const message = `${requests[i].url} status was 200`;
check(responses[i], {
[message]: (res) => res.status === 200,
});
}
}
// k6 options
export const options = {
stages: [
{ duration: "30s", target: vu },
{ duration: "5m", target: vu },
{ duration: "30s", target: 0 },
],
thresholds: {
http_req_failed: ["rate<0.01"], // http errors should be less than 1%
http_req_duration: ["p(95)<150"], // 95% of requests should be below 150ms
},
};
// シナリオ実行時のエントリポイント
export default function () {
// シナリオの実行
// HTTPステータスの確認
checkStatus(requests, responses);
sleep(1);
}
生成されたシナリオをもとに負荷テストを実行する
負荷テストの実行には、k6をベースに開発されたStampedeと呼ばれる社内ツールを利用します。Stampedeでは、並行して複数の負荷テストを実行したり、実行終了後に結果をSlackに通知したりすることができます。ここでは、並行して2つの負荷テストを実行してみます。生成されたシナリオをもとにStampedeで負荷テストを実行すると、以下の結果が得られました。
K6 Result
Running ./01_newstab.js
#### ##### ## # # ##### ###### ##### ######
# # # # ## ## # # # # # #
#### # # # # ## # # # ##### # # #####
# # ###### # # ##### # # # #
# # # # # # # # # # # #
#### # # # # # # ###### ##### ######
Pod: 1
data_received..................: 2.1 GB 5.8 MB/s
data_sent......................: 186 MB 517 kB/s
http_req_blocked...............: avg=41.86ms min=2.83ms med=20.39ms max=1.12s p(90)=99.88ms p(95)=127.71ms
http_req_connecting............: avg=1.03ms min=180.27µs med=394.34µs max=1.01s p(90)=1.09ms p(95)=1.53ms
✓ http_req_duration..............: avg=41.56ms min=2.87ms med=21.86ms max=3.04s p(90)=90.93ms p(95)=140.3ms
✓ http_req_failed................: 0.00% ✓ 0 ✗ 180357
http_req_receiving.............: avg=2.44ms min=31.97µs med=146.16µs max=759.89ms p(90)=4.1ms p(95)=11.76ms
http_req_sending...............: avg=91µs min=18.29µs med=53.36µs max=22.88ms p(90)=93.46µs p(95)=162.52µs
http_req_tls_handshaking.......: avg=40.76ms min=2.55ms med=19.62ms max=1.01s p(90)=98.96ms p(95)=126.56ms
http_req_waiting...............: avg=39.02ms min=2.57ms med=20.3ms max=3.04s p(90)=84.8ms p(95)=132.48ms
http_reqs......................: 180357 500.303055/s
iteration_duration.............: avg=2.9s min=1.38s med=3s max=6.28s p(90)=3.48s p(95)=3.75s
iterations.....................: 2283 6.33295/s
vus............................: 1 min=1 max=20
vus_max........................: 20 min=20 max=20
Pod: 2
data_received..................: 2.1 GB 5.7 MB/s
data_sent......................: 185 MB 513 kB/s
http_req_blocked...............: avg=42.02ms min=2.88ms med=20.51ms max=1.28s p(90)=100.44ms p(95)=128.56ms
http_req_connecting............: avg=891.26µs min=197.93µs med=382.14µs max=1s p(90)=982.52µs p(95)=1.39ms
✓ http_req_duration..............: avg=41.5ms min=2.93ms med=21.89ms max=2.84s p(90)=90.03ms p(95)=138.44ms
✓ http_req_failed................: 0.00% ✓ 0 ✗ 179409
http_req_receiving.............: avg=2.45ms min=30.04µs med=136.45µs max=922.36ms p(90)=4.07ms p(95)=11.87ms
http_req_sending...............: avg=82.5µs min=17.1µs med=51.18µs max=40.57ms p(90)=86.56µs p(95)=119.95µs
http_req_tls_handshaking.......: avg=41.06ms min=2.53ms med=19.76ms max=990.73ms p(90)=99.61ms p(95)=127.57ms
http_req_waiting...............: avg=38.97ms min=2.65ms med=20.32ms max=2.84s p(90)=83.84ms p(95)=131.18ms
http_reqs......................: 179409 496.659216/s
iteration_duration.............: avg=2.92s min=1.36s med=3.01s max=6.07s p(90)=3.48s p(95)=3.77s
iterations.....................: 2271 6.286826/s
vus............................: 1 min=1 max=20
vus_max........................: 20 min=20 max=20
結果を見てみると、HTTPリクエストのうち95%のリクエストにかかった時間が150msを下回っており、HTTPエラーも発生していないことが分かります。
また、これらの結果はGrafanaで確認することもできます。
Virtual Usersのグラフを見てみると、シナリオの定義通り段階的にVU数が変化していることが分かります。今回は並行して2つの負荷テストを実行しているため、最大で合計40VUsとなっています。
以上で、負荷テストのシナリオを自動で生成し、負荷テストを実行することができました。
一連の処理を自動化し定期実行する
負荷テストの実装は完了しましたが、これを手作業で定期的に実行するのは大変です。そこで、CIツールを使って一連の処理を自動化して、毎日自動で実行されるようにします。今回は実行環境の都合上、シナリオ生成をJenkinsで実行し、負荷テストをDroneで実行します。
シナリオ生成の自動化
Jenkins上でシナリオ生成のプログラムを実行するためには、Node.jsとChromiumがインストールされた環境を用意する必要があります。今回は、Dockerを用いて環境を用意します。Dockerで実行環境が構築できたら、Puppeteerをインストールした後シナリオ生成のプログラムを実行し、生成されたJSONファイルをGHEにpushします。
シナリオ生成が完了したら、JenkinsからDroneに登録された負荷テストのプログラムを呼び出します。
負荷テストの自動化
Drone上で負荷テストのプログラムを実行するためには、StampedeがインストールされたDocker環境を用います。先ほど生成したシナリオを格納したJSONファイルを含むレポジトリをcloneしたら、負荷テストを実行します。負荷テストの実行が完了すると、Slackに以下のような通知が飛びます。
また、パフォーマンスに問題が生じて指定した条件を満たさない場合は以下のような通知が飛びます。
定期実行の設定
シナリオ生成と負荷テストを定期的に実行するため、Jenkinsで平日12時にプログラムが定期実行されるように設定します。これで、平日の12時になると最新のシナリオが生成され、負荷テストを実行した後、結果がSlackに通知されるようになります。
以上でBeta環境にパフォーマンスの問題が生じた際、早期に発見することが可能となりました。
おわりに
今回のインターンシップは6週間を通じて原則リモートで開催されました。インターン開始前は特にコミュニケーションの点において不安が大きく、問題を一人で抱え込むことがないか心配に思っていましたが、メンターの方やマネージャーの方にSlackでご相談するとすぐにリアクションをくださり、大きく詰まることなく作業をすることができました。また、開発では普段触れる機会の少ないJavaやJavaScriptを利用しました。言語特有の記法や規約、ライブラリの仕組みなどを理解するまでに時間がかかりましたが、新しい技術を習得する過程を楽しんで仕事することができました。LINE NEWSのような規模が大きくユーザー数の多いシステムに触れるという貴重な経験を通じて、大規模なシステムを安定して稼働させる仕組みを理解することができたとともに、規模の大きなソースコードを読んでその概要を把握する実力がついたと強く実感することができました。
6週間という長期のインターンでしたが、毎日学びがあり充実していて、楽しく過ごすことができました。また、原則リモートで開催された今回のインターンですが、出社を希望したところ快く受け入れてくださり、四谷のオフィスで一日過ごすことができました。メンターの方、マネージャーの方、開発チームのメンバーをはじめとする社員の方々、大変お世話になりました。この6週間で得た貴重な経験を、今後の開発に活かしていきたいと思います。ありがとうございました。