0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vertex AI Gemini 429エラー対策:マルチリージョンフェイルオーバー(という名の力技💪) で安定運用する方法

Posted at

はじめに

Google Cloud の Vertex AI Gemini を本格的に使っていると、必ずと言っていいほどぶつかるのが 429 Resource exhausted エラー ですよね 😅

特に Pay-as-you-go(通常はこちら) プランだと、リージョンごとのクォータ制限でリクエストがバンバン弾かれて、「またかよ...」ってなること多くないですか?

従来の「エラーが出たらちょっと待って再試行」みたいな単純なリトライだと、同じリージョンでずっと待たされて効率悪すぎるんですよね。

そこで今回は、10個のリージョンを使ったマルチリージョンフェイルオーバーシステム を作って、429エラーを華麗(?)に回避する方法を紹介します!

問題を整理してみる

429エラーの厄介なところ

Vertex AI Gemini の 429エラーって、こんな特徴があるんです:

  • リージョナルクォータ制限: 各リージョンで独立してクォータが設定されている
  • 予測困難性: 明確に上限が設定されているわけではないので、予測が難しい

※リージョナルのクォータ制限については、ドキュメントに明確に記載されているわけではないのですが、推奨の回避策として、グローバルエンドポイントを使用するように指定があったので、実体としては、region毎にクォータが設定されていると言える?と私は理解しました。

Pay-as-you-go(通常はこちら)プランでも、"グローバルエンドポイントのようにリソースに余裕があるリージョンにリクエストしたい" というモチベーションの中で、単純ではありますが、Aリージョンで429が発生したら⇨Bリージョンでトライする というフェイルオーバーを自前実装したら安定するのでは?と思い、実際やってみたら、案外上手くいきましたというTips記事となります🙏

従来の方法の限界

よくある指数バックオフリトライって、こんな感じですよね:

# よくある単純なリトライ(これだと効率悪い)
for retry in range(max_retries):
    try:
        response = model.generate_content(prompt)
        return response.text
    except Exception as e:
        if "429" in str(e):
            time.sleep(2 ** retry)  # 指数バックオフ
        else:
            raise

でもこれだと、同じリージョンのクォータが回復するまでずっと待ちぼうけ... ⏰

解決策:マルチリージョン戦略で行こう!

基本的なアイデア

Vertex AI Gemini は複数のリージョンで提供されていて、各リージョンで独立したクォータ を持ってます。この特性を活かして、429エラーが出たら別のリージョンにさくっと切り替えちゃえばいいんです!

選んだ10リージョン

  • 北米: us-central1, us-east1, us-west1, us-west4
  • アジア: asia-northeast1, asia-southeast1, asia-east1
  • ヨーロッパ: europe-west1, europe-west3, europe-west4

2段階リトライシステム

  1. フェーズ1: 10リージョンで順次フェイルオーバー
  2. フェーズ2: 全リージョンで失敗したら指数バックオフ

実装してみよう!

AICommentGenerator クラスの設計

import os
import time
import random
from typing import Dict, Any, List, Optional
import vertexai
from vertexai.preview.generative_models import GenerativeModel
import logging

logger = logging.getLogger(__name__)

class AICommentGenerator:
    """AIを使用したコメント生成(マルチリージョン対応)"""
    
    def __init__(self, project_id: Optional[str] = None):
        self.project_id = project_id or os.environ.get("GOOGLE_CLOUD_PROJECT")
        
        # 使用可能なリージョンリスト
        self.available_regions = [
            "us-central1",      # メインリージョン
            "asia-northeast1",  # 東京
            "europe-west1",     # ベルギー
            "us-east1",         # サウスカロライナ
            "us-west1",         # オレゴン
            "asia-southeast1",  # シンガポール
            "europe-west4",     # オランダ
            "asia-east1",       # 台湾
            "europe-west3",     # フランクフルト
            "us-west4"          # ラスベガス
        ]
        
        self.current_region_index = 0
        self.current_region = self.available_regions[0]
        self._initialize_vertex_ai()
    
    def _initialize_vertex_ai(self):
        """現在のリージョンでVertex AIを初期化"""
        try:
            vertexai.init(project=self.project_id, location=self.current_region)
            self.model = GenerativeModel("gemini-1.5-flash")
            self.enabled = True
            logger.info(f"Vertex AI initialized: {self.current_region}")
        except Exception as e:
            logger.error(f"Failed to initialize Vertex AI: {str(e)}")
            raise

リージョン切り替えロジック

def _switch_to_next_region(self) -> bool:
    """次のリージョンに切り替え"""
    self.current_region_index += 1
    
    if self.current_region_index >= len(self.available_regions):
        logger.error("All regions exhausted")
        return False
    
    self.current_region = self.available_regions[self.current_region_index]
    logger.info(f"Switching to region: {self.current_region}")
    
    try:
        self._initialize_vertex_ai()
        return True
    except Exception as e:
        logger.error(f"Failed to switch to {self.current_region}: {str(e)}")
        return self._switch_to_next_region()

def _reset_to_primary_region(self):
    """プライマリリージョンにリセット"""
    if self.current_region_index != 0:
        logger.info("Resetting to primary region")
        self.current_region_index = 0
        self.current_region = self.available_regions[0]
        try:
            self._initialize_vertex_ai()
        except Exception as e:
            logger.warning(f"Failed to reset to primary: {str(e)}")

メインのマルチリージョンリトライ機構

def _generate_with_multi_region_retry(self, prompt: str, max_exponential_retries: int = 3) -> str:
    """マルチリージョン対応のリトライ機構"""
    original_region_index = self.current_region_index
    
    # フェーズ1: 複数リージョンでの試行
    logger.info(f"Starting multi-region generation. Available: {len(self.available_regions)} regions")
    
    for region_attempt in range(len(self.available_regions)):
        current_region = self.available_regions[self.current_region_index]
        logger.info(f"Attempting generation in region: {current_region} ({region_attempt + 1}/{len(self.available_regions)})")
        
        try:
            # リージョン間の負荷分散用遅延
            time.sleep(random.uniform(0.5, 1.0))
            
            generation_config = {
                "temperature": 0.1,
                "max_output_tokens": 4096,
                "top_p": 0.8,
                "top_k": 20
            }
            
            response = self.model.generate_content(prompt, generation_config=generation_config)
            logger.info(f"✅ Generation successful in region: {current_region}")
            
            # 成功した場合、プライマリリージョンにリセット
            if region_attempt > 0:
                self._reset_to_primary_region()
            
            return response.text.strip()
            
        except Exception as e:
            error_str = str(e)
            
            # 429エラーの場合
            if "429" in error_str or "Resource exhausted" in error_str:
                logger.warning(f"❌ Rate limit hit in region {current_region}")
                
                # 次のリージョンに切り替え
                if self._switch_to_next_region():
                    continue
                else:
                    logger.error("All regions exhausted due to rate limits")
                    break
            else:
                logger.error(f"❌ Non-rate-limit error in {current_region}: {error_str}")
                return ""
    
    # フェーズ2: 全リージョンで429エラーの場合、指数バックオフ
    logger.warning("All regions hit rate limits. Starting exponential backoff...")
    
    # プライマリリージョンにリセット
    self.current_region_index = original_region_index
    self.current_region = self.available_regions[self.current_region_index]
    self._initialize_vertex_ai()
    
    base_delay = 10.0
    
    for retry_attempt in range(max_exponential_retries):
        wait_time = base_delay * (2 * retry_attempt) * random.uniform(0.8, 1.2)
        logger.info(f"Exponential backoff retry {retry_attempt + 1}/{max_exponential_retries}. Waiting {wait_time:.1f}s...")
        time.sleep(wait_time)
        
        try:
            generation_config = {
                "temperature": 0.1,
                "max_output_tokens": 4096,
                "top_p": 0.8,
                "top_k": 20
            }
            
            response = self.model.generate_content(prompt, generation_config=generation_config)
            logger.info(f"✅ Exponential backoff retry successful")
            return response.text.strip()
            
        except Exception as e:
            error_str = str(e)
            if "429" in error_str or "Resource exhausted" in error_str:
                logger.warning(f"❌ Rate limit still hit during retry {retry_attempt + 1}")
                continue
            else:
                logger.error(f"❌ Non-rate-limit error during retry: {error_str}")
                return ""
    
    logger.error("All retry attempts failed")
    return ""

def generate_comment(self, prompt: str) -> str:
    """公開インターフェース"""
    if not self.enabled:
        return ""
    
    try:
        return self._generate_with_multi_region_retry(prompt)
    except Exception as e:
        logger.error(f"AI comment generation failed: {str(e)}")
        return ""

運用時のちょっとしたコツ

パフォーマンス最適化

ランダム遅延の導入
複数の処理が同時に同じリージョンにアクセスしないよう、0.5-1.0秒のランダム遅延を入れてます。これ地味に効果的です

プライマリリージョンの自動復帰
フェイルオーバー後に成功したら、次回のために us-central1 に自動で戻るようにしました。

実際に使ってみた結果

数字で見る改善効果

実際に運用してみると、単一リージョン x バックオフでの運用よりも格段にエラー発生率が減り、ほとんど失敗しなくなりました。

運用上のメリット

予測可能な処理時間
最大でも10リージョン分の試行時間(約10-15秒)で結果が出るので、処理時間の見積もりがしやすくなりました。

システムの安定性向上
特定リージョンでの障害やクォータ制限が、システム全体に与える影響を最小化できました。単一障害点の排除って大事ですね!

まとめ

マルチリージョンフェイルオーバーシステムを実装することで、Vertex AI Gemini の 429エラー問題をかなり効果的に解決できました!

重要なポイント

  • 地理的分散: 10リージョンの活用でリスクを分散
  • 2段階アプローチ: リージョンフェイルオーバー + 指数バックオフの組み合わせ
  • 運用性の配慮: 分かりやすいログとしっかりしたエラーハンドリング

この実装のおかげで、大規模なAI処理でも安定したサービス提供ができるようになりました。429エラーで悩んでる方の参考になれば嬉しいです!

何か質問があれば、コメントでお気軽にどうぞ 😊

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?