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
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事