EchoAPIロードテストとの出会いで性能問題を解決する

現代のソフトウェア開発において、APIの性能と信頼性は非常に重要です。最近、バックエンド開発者として新しいAPIエンドポイントを開発し、動的なデータクエリサービスを提供することを目指していました。このAPIは、応答時間を100ミリ秒以内にし、同時に1000人のユーザーをサポートする必要がありました。しかし、初期のテストを経てもなお、ピーク時にユーザーから大幅な性能問題が報告されました。ここでは、EchoAPIがこれらのパフォーマンスのボトルネックをどのように特定し、解決する手助けをしてくれたかを詳述します。

image.png

初期要件と開発フェーズ

このAPIエンドポイントは、以下の厳しい要件を持つように設計されました。

  • 応答時間:100ミリ秒未満
  • 同時ユーザー数:最大1000人

初期の機能テストでは問題がないように見えましたが、実際の利用環境では問題が浮き彫りになりました。

  • ピーク時のフィードバック:特に夕方のピーク時に、ユーザーからレスポンスが遅い、タイムアウトが頻繁に起きるとの報告があったのです。

パフォーマンスのボトルネックを発見

image.png

ユーザーフィードバックと初期トラブルシューティング

ユーザーは高負荷期間、特に夜間において重大な遅延と時折のタイムアウトを経験していました。この問題を診断するために、初めての並行性テストスクリプトを作成しましたが、これらのテストは実際のトラフィックの複雑さと規模を再現できず、現実の性能問題を捉えるには至りませんでした。

EchoAPIへの移行による包括的な分析

最初のテストアプローチの限界を認識し、より強力なソリューションとしてEchoAPIを利用することにしました。その過程を以下に示します。

手順 1: セットアップと設定

私はEchoAPIを用いて、実際のユーザー行動をシミュレートしました。

  • 同時ユーザー数:1000
  • テスト期間:200
img_v3_02ej_64b8c047-bb9d-4ae6-a85d-55626978a5bg.jpg

手順 2: 最初のロードテスト

最初のテストにより、衝撃的な問題が明らかになりました。

  • Queries-per-second (QPS):206
  • 平均応答時間(ART):800ミリ秒
  • ピーク応答時間:3017ミリ秒
  • エラー率:5.26%
  • CPU使用率:90%にスパイク
  • メモリ使用率:85%にピーク
image.png

これらの結果とマシンのモニタリングデータは、いくつかの性能のボトルネックを明確に示しました。

  • 高い応答時間:平均応答時間が目標の100ミリ秒を大幅に超えていました。
  • データベースクエリの遅延:データベースクエリ処理における重大な遅延が観察されました。
  • 資源利用効率の悪化:高いCPUとメモリ使用率は、APIコードの非効率性を示していました。

最適化戦略と実装

image.png

最適化1: データベースインデックスとクエリの最適化

初期分析では、データベースクエリが主要なボトルネックであることが判明しました。以下の対策を講じました。

最適化前:

インデックスの不足により、完全なテーブルスキャンが発生:

SELECT * FROM Users WHERE last_login < '2024-01-01';

データセットが増加するにつれて、性能が低下していました。

最適化後:

last_login列にインデックスを追加し、クエリ性能が大幅に改善されました:

-- インデックスの追加
CREATE INDEX idx_last_login ON Users(last_login);

-- 最適化されたクエリ
SELECT id, name, email FROM Users WHERE last_login < '2024-01-01';

最適化2: コードの改良とコネクションプール

さらに最適化するために、APIコードを改善し、コネクションプールを実装しました。

最適化前:

リクエストごとに新たにデータベース接続を作成し、レスポンスを非効率に処理していました:

# 初期APIコード
@app.route('/api/v1/users', methods=['GET'])
def get_users():
    connection = create_db_connection()
    query = "SELECT * FROM Users WHERE last_login > '2024-01-01'"
    users = connection.execute(query).fetchall()
    user_list = [dict(user) for user in users]  # 辞書への変換
    return jsonify(user_list), 200

特定された問題点:

  1. データベース接続のオーバーヘッド:リクエストごとに新しい接続を行うことが、過剰なオーバーヘッドを生んでいました。
  2. 非効率なデータ処理:クエリ結果の辞書への変換は遅でした。

最適化後:

コネクションプールとデータ処理の改善を実装:

# コネクションプールを用いたデータベース接続の設定
from psycopg2 import pool

db_pool = pool.SimpleConnectionPool(
    1, 20,  # 最小および最大接続数
    user='dbuser', 
    password='dbpass', 
    host='localhost', 
    port='5432', 
    database='exampledb'
)

@app.route('/api/v1/users', methods=['GET'])
def get_users():
    connection = db_pool.getconn()
    try:
        query = "SELECT id, name, email FROM Users WHERE last_login > %s"
        users = connection.execute(query, ('2024-01-01',)).fetchall()
        user_list = [dict(user) for user in users]
    finally:
        db_pool.putconn(connection)  # プールへの接続返却
    return jsonify(user_list), 200

改善点:

  1. コネクションプールの活用:接続を再利用することにより、オーバーヘッドを削減。
  2. 効率的なクエリ実行:パラメータ化クエリの利用と、必要なカラムのみに限定した選択。

これらの変更により、API応答時間は50%減少し、CPU使用率は約60%に安定しました。

最適化3: キャッシング戦略

さらに最適化するために、頻繁にアクセスするデータに対してキャッシングメカニズムを実装しました:

実装例:

from flask_caching import Cache

# Flask-Cachingの設定
cache = Cache(config={'CACHE_TYPE': 'simple'})

@app.route('/api/v1/users', methods=['GET'])
@cache.cached(timeout=300, query_string=True)
def get_users():
    connection = db_pool.getconn()
    try:
        query = "SELECT id, name, email FROM Users WHERE last_login > %s"
        users = connection.execute(query, ('2024-01-01',)).fetchall()
        user_list = [dict(user) for user in users]
    finally:
        db_pool.putconn(connection)
    return jsonify(user_list), 200

利点:

  1. データベース負荷の軽減:結果をキャッシュすることで、同じデータへの繰り返しリクエストに対するデータベース負荷を大幅に削減。
  2. 応答時間の改善:キャッシュされたレスポンスはミリ秒単位で提供されます。

最適化4: 負荷分散

増加するトラフィックをさばくために、複数のサーバインスタンスにわたる負荷分散を実装しました:

Nginxの設定:

# Nginxの負荷分散設定
http {
    upstream api_backend {
        server backend1.example.com;
        server backend2.example.com;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://api_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

利点:

  1. 負荷の分散:リクエストが複数のバックエンドサーバーに均等に分配されます。
  2. スケーラビリティ:トラフィックが増加する際に、プールにさらに多くのサーバーを追加しやすいです。

再テストとバリデーション

これらの最適化を実施した後、再びEchoAPIを用いてロードテストを実施しました。その結果が大幅に改善されました。

  • 同時ユーザー数:1000
  • Queries-per-second (QPS):823.55
  • 平均応答時間 (ART):99ミリ秒
  • ピーク応答時間:302ミリ秒
  • エラー率:<1.1%
image.png

達成した主な改善点:

  1. 応答時間の短縮:平均応答時間が800ミリ秒から100ミリ秒に減少し、性能目標を達成。
  2. 資源利用の効率化:CPUとメモリ使用率が効率的なレベルで安定。
  3. 信頼性の向上:エラー率が大幅に低下し、安定性と信頼性が向上。

結論

EchoAPIを用いたロードテストは、新しいAPIエンドポイントでの重要な性能問題を特定し、解決する上で非常に役立ちました。EchoAPIが提供する詳細な分析と直感的なインターフェースにより、具体的な問題点を絞り込むことが容易になり、最適化の効果を評価することができました。このプロセスによって、包括的なロードテストの重要性と、EchoAPIのような高度なツールを用いる価値が強調され、APIの性能と信頼性の基準を確実に満たすことができました。

EchoAPI.png

APIを最適化しようとする開発者にとって、EchoAPIは重要なインサイトとテスト機能を提供し、効果的に性能改善を達成できるための不可欠なツールです。