カテゴリ A: コーディング — Python デコレータとメタプログラミング
以下の要件を満たす @retry デコレータを実装せよ。
要件
- 関数が例外を raise した場合、指定回数まで自動リトライする
- リトライ間隔は指数バックオフ(
initial_wait × 2^n秒) - 最大リトライ回数を超えた場合は最後の例外をそのまま raise する
- リトライのたびに
"Retry N/max: <exception>"をログ出力する @retry(max_retries=3, initial_wait=1.0)の形式で使えること
使用例
@retry(max_retries=3, initial_wait=1.0)
def call_flaky_api():
import random
if random.random() < 0.7:
raise ConnectionError("API timeout")
return "success"
カテゴリ B: インフラ — Docker マルチステージビルドとイメージ最適化
以下の要件を満たす Dockerfile を作成せよ。
要件
- Python 3.12 ベース
- アプリケーション:
main.py(FastAPI)とrequirements.txt - マルチステージビルドを使って最終イメージのサイズを最小化
- 本番イメージには
pipもgccも含めない - 非 root ユーザー
appuserで実行 - ヘルスチェック:
/healthエンドポイントを 30 秒ごとに確認
マルチステージビルド × K8s Retry 連携図
モデル解答
import time
import functools
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def retry(max_retries: int = 3, initial_wait: float = 1.0, exceptions: tuple = (Exception,)):
"""
指数バックオフ付きリトライデコレータ
Args:
max_retries: 最大リトライ回数
initial_wait: 初回待機秒数(以降 2倍ずつ増加)
exceptions: リトライ対象の例外タプル
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_retries + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
if attempt < max_retries:
wait = initial_wait * (2 ** attempt)
logger.warning(f"Retry {attempt + 1}/{max_retries}: {e} (waiting {wait:.1f}s)")
time.sleep(wait)
else:
logger.error(f"Max retries ({max_retries}) exceeded: {e}")
raise last_exception
return wrapper
return decorator
# 使用例
@retry(max_retries=3, initial_wait=0.1, exceptions=(ConnectionError, TimeoutError))
def call_flaky_api():
import random
if random.random() < 0.7:
raise ConnectionError("API timeout")
return "success"
# === ステージ 1: ビルド ===
FROM python:3.12-slim AS builder
WORKDIR /app
# 依存関係をインストール(キャッシュ最適化: requirements.txt を先にコピー)
COPY requirements.txt .
# 仮想環境にインストール(本番イメージにコピーするため)
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
RUN pip install --no-cache-dir -r requirements.txt
# === ステージ 2: 本番 ===
FROM python:3.12-slim AS production
# セキュリティ: 非 root ユーザーを作成
RUN useradd --no-create-home --shell /bin/false appuser
WORKDIR /app
# ビルドステージから仮想環境のみコピー(pip, gcc は不要)
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# アプリケーションコードをコピー
COPY --chown=appuser:appuser main.py .
USER appuser
EXPOSE 8000
# ヘルスチェック
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" \
|| exit 1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Dockerfile テクニック解説
| テクニック | 理由 |
|---|---|
| マルチステージビルド | builder に gcc/pip が残るが production イメージには含めない |
python -m venv /opt/venv | システムの Python を汚さず、コピー可能な形で依存関係を隔離 |
COPY requirements.txt を先に | コードが変わっても依存関係レイヤーをキャッシュ利用できる |
--no-cache-dir | pip のキャッシュをイメージに含めない(サイズ削減) |
| 非 root ユーザー | コンテナ脱出時の被害を最小化 |
--chown=appuser | コピー時に所有者を変更(RUN chown より効率的) |
複合問題: A × B 統合
問い: K8s上のFastAPIサービスに
1. リトライとKubernetesのProbe(LivenessProbe/ReadinessProbe)の競合
2. タイムアウト設定の重要性
@retry デコレータを導入しようとしている。以下の観点から問題点を指摘せよ。1. リトライとKubernetesのProbe(LivenessProbe/ReadinessProbe)の競合
2. タイムアウト設定の重要性
✗ 問題のある設定
# max_retries=3, initial_wait=1.0 の場合
# 合計待機: 1s + 2s + 4s = 7秒
# もし ReadinessProbe が
# periodSeconds=5, failureThreshold=1 なら
# → Probe timeout (5s) < retry timeout (7s)
# → Pod が unhealthy 判定されてしまう!
✓ 正しい設計
# retry の合計タイムアウトを
# ReadinessProbe より短く設定
@retry(max_retries=2, initial_wait=0.5)
# 合計: 0.5s + 1.0s = 1.5秒
# ReadinessProbe:
# periodSeconds=10, failureThreshold=3
# → 30秒以内に回復すればOK
# Circuit Breaker(tenacity/Istio)と
# 組み合わせて Thundering Herd 防止
1
Probe との競合
対策: retry の合計タイムアウトを ReadinessProbe のタイムアウトより短く設定する。
@retry(max_retries=3, initial_wait=1.0) は最大 1+2+4=7秒の待機を発生させる。K8s の ReadinessProbe の failureThreshold × periodSeconds 以内にリトライが完了しない場合、Pod が unhealthy と判定されて Traffic が切られる。
対策: retry の合計タイムアウトを ReadinessProbe のタイムアウトより短く設定する。
2
タイムアウトの重要性
リトライは「一時的な障害」に対して有効だが、下流サービスがダウンしている場合はリトライが逆効果(Thundering Herd問題)。
対策: Circuit Breaker パターン(
リトライは「一時的な障害」に対して有効だが、下流サービスがダウンしている場合はリトライが逆効果(Thundering Herd問題)。
対策: Circuit Breaker パターン(
tenacity ライブラリや Istio のサービスメッシュ機能)と組み合わせる。
ポイント解説
1
@functools.wraps(func): デコレータ適用後も func.__name__ や __doc__ を保持する。デバッグ時にラッパー関数名ではなく元の関数名が見える。
2
指数バックオフ:
initial_wait * 2^attempt → 1s → 2s → 4s。フラットなリトライより下流サービスへの負荷を分散できる。
3
exceptions タプルで対象例外を絞る: ConnectionError のみリトライ、ValueError はリトライしないなど、ビジネスロジックに合わせた細かい制御が可能。