Zenn
📝

YouTube作業BGM動画作成サービスの舞台裏〜第6章 ゾンビ接続との闘い:スケーラビリティ戦略〜

2025/03/29に公開

あなたがブラウザで「生成開始」ボタンをクリックした瞬間、BGM動画生成システムの裏側では壮絶な戦いの準備が始まります。

一方ではユーザーの期待を裏切らない高品質な動画を届けたい。他方では「接続タイムアウト」という名の悪魔と戦わなければなりません。今日はそんな舞台裏の物語をお届けします。

タイムアウトという名の魔物

まず知っておくべきは「タイムアウト」という恐ろしい存在です。API Gatewayの世界では、すべての処理に29秒という厳格な制限時間が設けられています。なぜそんな制限があるのでしょうか?

最近では引き上げ制限も行われたらしいです👇
https://aws.amazon.com/jp/about-aws/whats-new/2024/06/amazon-api-gateway-integration-timeout-limit-29-seconds/

想像してみてください。もしタイムアウトがなければ、インターネットは「ゾンビ接続」であふれかえります。これは一度開始されたものの、決して完了せず、決して死なず、ただサーバーのリソースを食い尽くす不死の接続たちです。このゾンビの大群がサーバーを攻め滅ぼす前に、タイムアウトという銀の弾丸で対処するのです。

「29秒で動画を生成しろというのか!」と思われるかもしれませんが、ご安心ください。私たちは賢く立ち回る方法を見つけました。

AWS Lambdaの千の手による同時処理

私たちのシステムの中核は、AWS Lambdaという優れた技術です。これは1000の手を持つ仏像のように、同時に最大1000のリクエストを捌くことができます。しかもこの仏像、呼ばれた時だけ現れるため、静かな時間帯は電気代もかかりません。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-concurrency.html

def lambda_handler(event, context):
    # リクエストボディの解析
    body = json.loads(event['body'])
    
    # ジョブIDの生成 - これが魔法の呪文となる
    job_id = str(uuid.uuid4())
    
    # 非同期処理の準備
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table(STATUS_TABLE)
    table.put_item(
        Item={
            'jobId': job_id,
            'status': 'queued',
            'progress': 0,
            'createdAt': int(time.time()),
            # 他の必要情報
        }
    )
    
    # さっさと返事をする - ここがポイント
    return {
        'statusCode': 202,  # 「受け付けたよ」というHTTPコード
        'body': json.dumps({
            'jobId': job_id,
            'status': 'queued',
            'message': '動画生成処理がキューに追加されました'
        })
    }

非同期処理:「お待たせしません作戦」

私たちのシステムの真髄は、「お待たせしません作戦」とも呼ぶべき非同期処理にあります。レストランで例えると、注文を受けたウェイターがキッチンに伝えて、すぐにあなたのテーブルに戻ってきて 「承りました、準備ができましたらお持ちします」 と言うようなものです。

従来型のシステムでは、ウェイターが注文を受けた後、キッチンでじっと料理が完成するのを待ち、それからやっとテーブルに戻ってくるという、まるで拷問のような体験を強いていました。

動画のような重い処理では、この間ブラウザはずっと「くるくる」を回し続け、最悪の場合にはタイムアウトというレストランの閉店時間に追い出されることになります。

私たちの非同期処理モデルでは:

  1. 即レス原則 - ユーザーのリクエストを受けたら、即座に「了解しました」という返事とともに、追跡用のジョブIDを発行します。タイムアウトの悪魔が襲いかかる前に、さっさと応答を返すのです。

  2. バックグラウンドの職人技 - 実際の動画生成は舞台裏で黙々と進めます。時間のかかる処理をLambdaの職人たちに任せるのです。

  3. 進捗お知らせサービス - フロントエンドは定期的に「まだですか?」と小さなリクエストを送り、現在の進捗状況を確認します。「材料を刻んでいます」「火を通しています」「盛り付け中です」といった具合に。

この手法のおかげで、ブラウザは常に新鮮な状態を保ち、ユーザーはリアルタイムに進捗を確認できます。まるで高級レストランのような体験です。

ポーリング:小まめな確認が平和をもたらす

「なぜ細切れでデータを返さないの?」と思われる方もいるでしょう。技術的には可能ですが、それは電話をかけたまま「まだ終わってない...まだ終わってない...」と聞き続けるようなものです。電話代も莫大になりますし、もし一瞬電波が悪くなったら最初からやり直しです。

対してポーリングは、5秒ごとに短い電話をかけて「今どうなってる?」と確認するようなもの。一度の通話が切れても、次の通話で再開できます。AWS的には、29秒のタイムアウトを気にせず、効率的にリソースを使えるという利点があります。

// 5秒ごとに進捗確認の電話をかける
const pollStatus = async (jobId) => {
  try {
    const status = await checkStatus(jobId);
    
    if (status.status === 'completed') {
      // おっ!できたぞ
      setResult(status.result);
      stopPolling();
    } else if (status.status === 'failed') {
      // しまった、何かがおかしい
      setError('処理中にエラーが発生しました: ' + status.message);
      stopPolling();
    } else {
      // まだかかりそう、でも進捗は出てる
      updateProgress(status.progress, status.message);
    }
  } catch (error) {
    console.error('確認中に問題が発生しました', error);
  }
};

DynamoDBによるステータス管理:記憶係の重要性

物語のもう一人の主役は、DynamoDBという記憶力抜群のキャラクターです。彼は各ジョブの状態、進捗、結果をすべて記録します。まるで図書館司書のように、どんな情報もすぐに取り出せる能力を持っています。

def update_status(job_id, status, progress, message=None):
    """進捗メモを更新する"""
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table(STATUS_TABLE)
    
    update_expr = "SET #status = :status, #progress = :progress, #updatedAt = :updated_at"
    # 中略
    
    table.update_item(
        Key={'jobId': job_id},
        UpdateExpression=update_expr,
        ExpressionAttributeNames=expr_attr_names,
        ExpressionAttributeValues=expr_attr_values
    )

S3による片付け上手の整理術

動画ファイルは大きい。放っておくとすぐにストレージは満杯になります。そこで登場するのが、S3のライフサイクルポリシーという自動お片付けシステム。

  • 入力ファイル:「7日経ったら捨てていいよ」タグ付き
  • 出力動画:「30日経ったら捨てていいよ」タグ付き

これにより、ストレージはいつも整理整頓された状態を保ちます。

# S3のお片付けルール
LifecycleConfiguration:
  Rules:
    - Id: DeleteAfter7Days
      Status: Enabled  # 有効化
      ExpirationInDays: 7  # 7日後に自動削除

バーストトラフィック対策:急な来客にも動じない

人気YouTuberが私たちのサービスを紹介したら?突然のアクセス殺到に備え、以下の対策を講じています:

  1. API Gatewayのドアマン - 入場制限をかけて、システムが倒れないようにします
  2. CloudWatchの見張り役 - 異常を検知したら即座に管理者に通知
  3. SQSという待合室 - 混雑時には整理券を配って順番に処理

負荷テストによる耐久訓練

「平時の備えが戦時の強さ」という言葉の通り、私たちは定期的に負荷テストという名の耐久訓練を実施しています。

async def run_load_test(audio_key, image_key):
    async with aiohttp.ClientSession() as session:
        tasks = []
        for i in range(TEST_COUNT):
            # 並列でリクエストを送りまくる
            duration = 1 + i
            tasks.append(generate_video(session, audio_key, image_key, duration))
        
        results = await asyncio.gather(*tasks)
        
        # 成績発表
        success_count = sum(1 for r in results if r['success'])
        print(f"成功率: {success_count/TEST_COUNT*100:.2f}%")

将来の拡張戦略:さらなる冒険への地図

今回紹介したポーリングベースの仕組みは、中規模までのトラフィックには効果的ですが、爆発的な人気を得たらどうする?その時のために、私たちは次の冒険への地図もすでに用意しています。

ポーリング負荷対策:データベース守護作戦

ポーリングは便利ですが、すべてのクライアントが時間間隔で一斉にデータベースを叩くことになります。これは「ブラックフライデーに店の前に大行列ができる」ようなものです。そこで考えられる対策をいくつか紹介します。

  1. 高速道キャッシュ層の導入

    # ElastiCacheなどのキャッシュを導入
    def check_status(job_id):
        # まずキャッシュをチェック
        cached_status = cache.get(f"job_status:{job_id}")
        if cached_status:
            return cached_status
            
        # キャッシュになければDBから取得し、指定時間キャッシュに保存
        status = dynamodb.get_item(...)
        cache.set(f"job_status:{job_id}", status, ex=10)
        
        return status
    
  2. 賢いポーリング間隔の設計
    進捗に応じて確認頻度を変える「アダプティブポーリング」も効果的です。

    // 進捗に応じて確認間隔を変える
    const getPollingInterval = (progress) => {
      if (progress < 20) return 10000; // 初期段階は10秒間隔
      if (progress > 80) return 2000;  // 完了に近づくと2秒間隔
      return 5000; // 通常は5秒間隔
    };
    

究極形態:イベント駆動型リアルタイム通知

さらに大規模になれば、ポーリングモデルからプッシュモデルへの転換も視野に入れています。

処理ステップ完了 → SNSトピック発行 → Lambda → WebSocketを通じてクライアントに通知

AWS API GatewayのWebSocketサポートを活用すれば、状態変化があった時だけ通知する仕組みが実現可能です。これは「お客様のお料理ができました」とウェイターが直接あなたのテーブルにお知らせに来るようなものです。

// WebSocketクライアント例
const socket = new WebSocket('wss://api.example.com/ws');
socket.onmessage = (event) => {
  const data = JSON.parse(event.data);
  if (data.type === 'STATUS_UPDATE') {
    updateProgress(data.progress, data.message);
  } else if (data.type === 'COMPLETION') {
    setResult(data.result);
    // 完了通知
  }
};

大規模分散処理:ビデオファーム

動画生成リクエストが激増した場合は、専用のビデオ処理ファームへの拡張も計画しています。これはEC2インスタンスのオートスケーリンググループか、または専用のECSクラスタで実現可能です。Lambdaでは処理しきれない超大型動画の生成も、このアプローチなら対応できます。

結論:技術の裏にある人間味

最後に最も重要なことをお伝えします。このすべての技術的な工夫の目的は、ユーザーに「魔法のような体験」を提供することです。裏側ではタイムアウトとの死闘、ゾンビ接続の駆除、リソース管理の綱渡りが行われていても、ユーザーの目には「簡単に、速く、確実に」動画が生成されるという魔法だけが見えるのです。

「BGM動画を作りたい」と思ったユーザーが、技術的な制約を気にせず、クリエイティブな作業に集中できる世界。それが私たちの目指す場所であり、このような技術的工夫の真の価値なのです。

タイムアウトとの戦い、ポーリングという小さな電話、そして千の手を持つLambda。これらすべてが舞台裏で協力し合い、ユーザーの創造性を支える舞台を作り上げているのです。

Discussion

ログインするとコメントできます