はじめに
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: 10リージョンで順次フェイルオーバー
- フェーズ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エラーで悩んでる方の参考になれば嬉しいです!
何か質問があれば、コメントでお気軽にどうぞ 😊