2025.01.14
AWS環境で稼働するWebサービスのAPIパフォーマンス改善
結論
- CloudWatch Logsへのログ出力を標準出力に変更してAPIの実行パフォーマンスを改善した
- SecretsManagerへの問い合わせ時にキャッシュを導入してAPIの実行パフォーマンスを改善した
はじめに
こんにちは。次世代システム研究室のT.Tです。
昨年12月に現在開発運用に携わっているWebサービスのシステム基盤の一部をAWSに移行しました。AWSへの移行は全体では5フェーズに分けた移行を予定していて、今回は第1フェーズとなるWebサービスのフロントエンド/APIを移行しました。移行後のAWS環境では、フロントエンド/APIにEKSを実行環境として利用しています。
本記事では、AWS移行の検証時に発生したパフォーマンス上の問題について、その原因と解決策についてご紹介します。
1.EKSのシステム構成
Webサービスのフロントエンド/APIで利用しているEKSでは、ログ出力にCloudWatch Logs、秘匿情報の管理にSecretsManagerを利用しています。
2.APIの実行パフォーマンス検証
ユーザーが所持している情報を取得する参照系のAPIを、AWSの本番環境と移行前のシステム基盤環境のZcom CloudのSTG環境でパフォーマンステストを実行した結果の比較です。VMスペックの詳細は省略しますが、AWSの本番環境のVMスペックがZcom CloudのSTG環境より高いシステム構成となっています。100qpsで実行結果を比較したところ次のような結果になりました。
100qps
最小レスポンス時間(ms) | 最大レスポンス時間(ms) | 平均レスポンス時間(ms) | |
AWS | 2386 | 28355 | 13835 |
Zcom Cloud | 193 | 732 | 319 |
AWS環境では、100qpsでパフォーマンスが出ていないので10qpsと50qpsで実行して結果を計測してみたところ以下の結果となりました。
最小レスポンス時間(ms) | 最大レスポンス時間(ms) | 平均レスポンス時間(ms) | |
10qps | 843 | 1790 | 1051 |
50qps | 1193 | 11422 | 5649 |
100qps | 2386 | 28355 | 13835 |
10qpsの低い負荷でもパフォーマンスが出ていないので、単純にAPIを1回実行する際のパフォーマンスが悪そうです。テスト対象のAPIの処理で時間が掛かっている箇所を調査した結果、CloudWatch Logsへのログ出力とSecretsManagerへの問い合わせで時間が掛かっていることが分かりました。
3.APIの実行パフォーマンス改善
3.1.CloudWatch Logsへのログ出力の改善
CloudWatch Logsへのログ出力で時間が掛かっている原因は、API経由でログを出力しているためでした。修正前の実装では、以下のようにライブラリ経由でログを出力していますが、このライブラリではAPI経由でログを出力しているためパフォーマンスが低下していました。
<?php use Aws\CloudWatchLogs\CloudWatchLogsClient; use Yii; use yii\log\Target; class CloudWatchLogTarget extends Target { public $logGroup; public $logStream; public $clientParams; public $client; public function init() { parent::init(); $this->client = new CloudWatchLogsClient($this->clientParams); } public function export() { ... $params = [ 'logGroupName' => $this->logGroup, 'logStreamName' => $this->logStream, 'logEvents' => $logEvents, ]; $this->client->putLogEvents($params); } }
この処理を標準出力にログを出力するように変更して、FluentBitでCloudWatch Logsに転送するように修正します。
ログ出力
<?php use yii\log\Target; class StreamTarget extends Target { private $stream; public function __construct($config) { $this->stream = @fopen('php://stdout', 'ab'); parent::__construct($config); } public function export(): void { $this->stream = $this->createStream(); @flock($this->stream, LOCK_EX); @fwrite($this->stream, $this->messages); @flock($stream, LOCK_UN); @fclose($stream); } }
FluentBitの設定
containerLogs: fluentBit: config: customParsers: | [PARSER] Name container_firstline Format regex Regex (?<log>...) Time_Key time Time_Format %Y-%m-%dT%H:%M:%S.%LZ extraFiles: application-log.conf: | [INPUT] Name tail Tag application.* Exclude_Path /var/log/containers/cloudwatch-agent*, /var/log/containers/fluent-bit*, /var/log/containers/aws-node*, /var/log/containers/kube-proxy* Path /var/log/containers/*.log multiline.parser docker, cri DB /var/fluent-bit/state/flb_container.db Mem_Buf_Limit 50MB Skip_Long_Lines On Refresh_Interval 10 Rotate_Wait 30 storage.type filesystem Read_from_Head ${READ_FROM_HEAD} [FILTER] Name rewrite_tag Match application.* Rule $log ... Emitter_Name re_emitted_app [OUTPUT] Name cloudwatch_logs Match app.* region ${AWS_REGION} log_group_name /app log_stream_name default auto_create_group true log_retention_days 30 log_stream_template $filter_tag
<h3″>3.2.SecretsManagerへの問い合わせの改善
SecretsManagerへの問い合わせで時間が掛かっている原因は、PHPでリクエストを受け付ける度にSecretsManagerへの問い合わせが発生しているためでした。
修正前のSecretsManagerへの問い合わせ
<?php use Aws\SecretsManager\SecretsManagerClient; $secretName = 'app-secrets'; $cacheTtl = 3600; $client = new SecretsManagerClient([ 'version' => 'latest', 'region' => 'ap-northeast-1' ]); $result = $client->getSecretValue([ 'SecretId' => $secretName, ]); if (isset($result['SecretString'])) { $secrets = $result['SecretString']; } else { $secrets = base64_decode($result['SecretBinary']); } $secrets = json_decode($secrets, true);
コンテナ内でSecretsManagerの値をキャッシュすることでパフォーマンスが改善出来ます。以下のようにapcuを利用してコンテナ内のメモリにキャッシュを保持するように修正しました。
修正後のSecretsManagerへの問い合わせ
<?php use Aws\SecretsManager\SecretsManagerClient; $secretName = 'app-secrets'; $cacheTtl = 3600; $secrets = apcu_fetch($secretName); if (!$secrets) { $client = new SecretsManagerClient([ 'version' => 'latest', 'region' => 'ap-northeast-1' ]); $result = $client->getSecretValue([ 'SecretId' => $secretName, ]); if (isset($result['SecretString'])) { $secrets = $result['SecretString']; } else { $secrets = base64_decode($result['SecretBinary']); } $secrets = json_decode($secrets, true); apcu_store($secretName, $secrets, $cacheTtl); }
<h3″>3.3.改善後のAPIパフォーマンス検証結果
CloudWatch Logsへのログ出力とSecretsManagerへの問い合わせを改善して、再度APIのパフォーマンスを検証しました。AWSの本番環境での実行結果は以下のようになりました。
最小レスポンス時間(ms) | 最大レスポンス時間(ms) | 平均レスポンス時間(ms) | |
100qps | 29 | 397 | 57 |
200qps | 30 | 421 | 57 |
300qps | 31 | 469 | 71 |
改善により最小レスポンス時間が速くなり、300qpsまでスケールすることが確認出来ました。SecretsManagerをキャッシュが切れた際のパフォーマンスを計測したところ、キャッシュが切れるタイミングで100ms程度の遅延が確認出来ましたが、この程度の遅延であれば許容範囲です。このパフォーマンスであれば本番環境の運用上問題ないのでパフォーマンス改善としては完了としました。
4.まとめ
CloudWatch Logsへのログ出力を標準出力に変更して、SecretsManagerへの問い合わせ時にキャッシュを利用することでAPIのパフォーマンスを改善することが出来ました。現在フロントエンド/APIはAWSへの移行が完了していますが、実行パフォーマンスの問題は発生しておらず、本記事でのパフォーマンスチューニングがうまく機能したようです。
AWS移行は引き続き第2フェーズ以降の計画を立てながら設計開発を進めています。第2フェーズ以降も様々な問題に直面しながら解決していくことになると思いますが、そうした問題への取り組みを今後も共有して行きたいと思います。
次世代システム研究室では、アプリケーション開発や設計を行うアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。
皆さんのご応募をお待ちしています。
グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。
Follow @GMO_RD