<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Python on AI2CORE - AI技術ブログ</title>
    <link>https://www.ai2core.com/tags/python/</link>
    <description>Recent content in Python on AI2CORE - AI技術ブログ</description>
    <generator>Hugo -- 0.146.4</generator>
    <language>ja</language>
    <lastBuildDate>Sun, 08 Mar 2026 09:00:00 +0900</lastBuildDate>
    <atom:link href="https://www.ai2core.com/tags/python/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Python asyncio実践ガイド：並行処理で処理速度を10倍にする具体的テクニック</title>
      <link>https://www.ai2core.com/posts/2026-03-08-python-asyncio-practical-guide/</link>
      <pubDate>Sun, 08 Mar 2026 09:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-08-python-asyncio-practical-guide/</guid>
      <description>Pythonのasyncioを使った非同期プログラミングの基礎から実践まで。I/Oバウンドな処理を劇的に高速化する具体的な実装パターンとベストプラクティスを解説します。</description>
      <content:encoded><![CDATA[<h2 id="はじめに">はじめに</h2>
<p>Pythonで大量のAPI呼び出しやファイル操作を行う際、処理時間がボトルネックになることは珍しくありません。同期的な処理では、一つの操作が完了するまで次の処理を開始できないため、I/O待ち時間が積み重なってしまいます。</p>
<p>本記事では、Pythonの<code>asyncio</code>モジュールを使った非同期プログラミングについて、基礎概念から実践的なパターンまでを体系的に解説します。実際のプロジェクトで使える具体的なコード例を通じて、処理速度を劇的に改善する方法を学んでいきましょう。</p>
<h2 id="asyncioの基本概念">asyncioの基本概念</h2>
<h3 id="イベントループとは">イベントループとは</h3>
<p><code>asyncio</code>の中心にあるのがイベントループです。イベントループは、非同期タスクの実行を管理し、I/O操作の完了を監視して、適切なタイミングでタスクを再開させる役割を担います。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">9
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> asyncio
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">main</span>():
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;Hello&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> asyncio<span style="color:#f92672">.</span>sleep(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">&#34;World&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Python 3.7以降の推奨方法</span>
</span></span><span style="display:flex;"><span>asyncio<span style="color:#f92672">.</span>run(main())
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="asyncawaitの仕組み">async/awaitの仕組み</h3>
<p><code>async def</code>で定義された関数はコルーチン関数となり、呼び出すとコルーチンオブジェクトを返します。<code>await</code>キーワードを使うことで、そのコルーチンの完了を待機しつつ、その間に他のタスクを実行できるようになります。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> asyncio
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> aiohttp
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">fetch_url</span>(session: aiohttp<span style="color:#f92672">.</span>ClientSession, url: str) <span style="color:#f92672">-&gt;</span> dict:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;URLからデータを非同期で取得&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">with</span> session<span style="color:#f92672">.</span>get(url) <span style="color:#66d9ef">as</span> response:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;url&#34;</span>: url,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;status&#34;</span>: response<span style="color:#f92672">.</span>status,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;content_length&#34;</span>: len(<span style="color:#66d9ef">await</span> response<span style="color:#f92672">.</span>text())
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">main</span>():
</span></span><span style="display:flex;"><span>    urls <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;https://api.github.com&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;https://api.stripe.com&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;https://api.openai.com&#34;</span>
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">with</span> aiohttp<span style="color:#f92672">.</span>ClientSession() <span style="color:#66d9ef">as</span> session:
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># 全URLを並行して取得</span>
</span></span><span style="display:flex;"><span>        tasks <span style="color:#f92672">=</span> [fetch_url(session, url) <span style="color:#66d9ef">for</span> url <span style="color:#f92672">in</span> urls]
</span></span><span style="display:flex;"><span>        results <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> asyncio<span style="color:#f92672">.</span>gather(<span style="color:#f92672">*</span>tasks)
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> result <span style="color:#f92672">in</span> results:
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>result[<span style="color:#e6db74">&#39;url&#39;</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74">: </span><span style="color:#e6db74">{</span>result[<span style="color:#e6db74">&#39;status&#39;</span>]<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>asyncio<span style="color:#f92672">.</span>run(main())
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="実践パターン1大量のapi呼び出しを高速化">実践パターン1：大量のAPI呼び出しを高速化</h2>
<h3 id="問題同期処理での遅延">問題：同期処理での遅延</h3>
<p>例えば、1000件のユーザーデータをAPIから取得する場合を考えます。1リクエストあたり100msかかるとすると、同期処理では100秒もの時間が必要です。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># 同期処理（遅い）</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> requests
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> time
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">fetch_users_sync</span>(user_ids: list[int]) <span style="color:#f92672">-&gt;</span> list[dict]:
</span></span><span style="display:flex;"><span>    results <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> user_id <span style="color:#f92672">in</span> user_ids:
</span></span><span style="display:flex;"><span>        response <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>get(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;https://api.example.com/users/</span><span style="color:#e6db74">{</span>user_id<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>        results<span style="color:#f92672">.</span>append(response<span style="color:#f92672">.</span>json())
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> results
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>start <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>time()
</span></span><span style="display:flex;"><span>users <span style="color:#f92672">=</span> fetch_users_sync(list(range(<span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">101</span>)))  <span style="color:#75715e"># 100件</span>
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;同期処理: </span><span style="color:#e6db74">{</span>time<span style="color:#f92672">.</span>time() <span style="color:#f92672">-</span> start<span style="color:#e6db74">:</span><span style="color:#e6db74">.2f</span><span style="color:#e6db74">}</span><span style="color:#e6db74">秒&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 同期処理: 10.23秒</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="解決asyncioで並行処理">解決：asyncioで並行処理</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">35
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">36
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">37
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">38
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">39
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">40
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">41
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">42
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">43
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> asyncio
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> aiohttp
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> time
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Optional
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">fetch_user</span>(
</span></span><span style="display:flex;"><span>    session: aiohttp<span style="color:#f92672">.</span>ClientSession, 
</span></span><span style="display:flex;"><span>    user_id: int,
</span></span><span style="display:flex;"><span>    semaphore: asyncio<span style="color:#f92672">.</span>Semaphore
</span></span><span style="display:flex;"><span>) <span style="color:#f92672">-&gt;</span> Optional[dict]:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;セマフォで同時接続数を制限しながらユーザー情報を取得&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">with</span> semaphore:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">with</span> session<span style="color:#f92672">.</span>get(
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;https://api.example.com/users/</span><span style="color:#e6db74">{</span>user_id<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>,
</span></span><span style="display:flex;"><span>                timeout<span style="color:#f92672">=</span>aiohttp<span style="color:#f92672">.</span>ClientTimeout(total<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>)
</span></span><span style="display:flex;"><span>            ) <span style="color:#66d9ef">as</span> response:
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">if</span> response<span style="color:#f92672">.</span>status <span style="color:#f92672">==</span> <span style="color:#ae81ff">200</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">await</span> response<span style="color:#f92672">.</span>json()
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">except</span> <span style="color:#a6e22e">Exception</span> <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Error fetching user </span><span style="color:#e6db74">{</span>user_id<span style="color:#e6db74">}</span><span style="color:#e6db74">: </span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">fetch_users_async</span>(user_ids: list[int], max_concurrent: int <span style="color:#f92672">=</span> <span style="color:#ae81ff">50</span>) <span style="color:#f92672">-&gt;</span> list[dict]:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;非同期で複数ユーザーを取得（同時接続数制限付き）&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    semaphore <span style="color:#f92672">=</span> asyncio<span style="color:#f92672">.</span>Semaphore(max_concurrent)
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    connector <span style="color:#f92672">=</span> aiohttp<span style="color:#f92672">.</span>TCPConnector(
</span></span><span style="display:flex;"><span>        limit<span style="color:#f92672">=</span>max_concurrent,
</span></span><span style="display:flex;"><span>        limit_per_host<span style="color:#f92672">=</span>max_concurrent
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">with</span> aiohttp<span style="color:#f92672">.</span>ClientSession(connector<span style="color:#f92672">=</span>connector) <span style="color:#66d9ef">as</span> session:
</span></span><span style="display:flex;"><span>        tasks <span style="color:#f92672">=</span> [fetch_user(session, uid, semaphore) <span style="color:#66d9ef">for</span> uid <span style="color:#f92672">in</span> user_ids]
</span></span><span style="display:flex;"><span>        results <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> asyncio<span style="color:#f92672">.</span>gather(<span style="color:#f92672">*</span>tasks)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> [r <span style="color:#66d9ef">for</span> r <span style="color:#f92672">in</span> results <span style="color:#66d9ef">if</span> r <span style="color:#f92672">is</span> <span style="color:#f92672">not</span> <span style="color:#66d9ef">None</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 実行</span>
</span></span><span style="display:flex;"><span>start <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>time()
</span></span><span style="display:flex;"><span>users <span style="color:#f92672">=</span> asyncio<span style="color:#f92672">.</span>run(fetch_users_async(list(range(<span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">101</span>))))
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;非同期処理: </span><span style="color:#e6db74">{</span>time<span style="color:#f92672">.</span>time() <span style="color:#f92672">-</span> start<span style="color:#e6db74">:</span><span style="color:#e6db74">.2f</span><span style="color:#e6db74">}</span><span style="color:#e6db74">秒&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 非同期処理: 0.52秒（約20倍高速化）</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="重要ポイントセマフォによる同時接続数制限">重要ポイント：セマフォによる同時接続数制限</h3>
<p>無制限に並行リクエストを投げると、サーバーに過負荷をかけたり、コネクションプールが枯渇したりする問題が発生します。<code>asyncio.Semaphore</code>を使って、同時実行数を適切に制限することが重要です。</p>
<h2 id="実践パターン2タイムアウトとリトライの実装">実践パターン2：タイムアウトとリトライの実装</h2>
<p>本番環境では、ネットワーク障害やサーバーの一時的な過負荷に対応するため、適切なタイムアウトとリトライ機構が必要です。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">35
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">36
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">37
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">38
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">39
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">40
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">41
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">42
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">43
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">44
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">45
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">46
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">47
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> asyncio
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> aiohttp
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> TypeVar, Callable, Any
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> functools <span style="color:#f92672">import</span> wraps
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>T <span style="color:#f92672">=</span> TypeVar(<span style="color:#e6db74">&#39;T&#39;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">async_retry</span>(
</span></span><span style="display:flex;"><span>    max_retries: int <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>,
</span></span><span style="display:flex;"><span>    base_delay: float <span style="color:#f92672">=</span> <span style="color:#ae81ff">1.0</span>,
</span></span><span style="display:flex;"><span>    max_delay: float <span style="color:#f92672">=</span> <span style="color:#ae81ff">30.0</span>,
</span></span><span style="display:flex;"><span>    exponential_base: float <span style="color:#f92672">=</span> <span style="color:#ae81ff">2.0</span>,
</span></span><span style="display:flex;"><span>    retryable_exceptions: tuple <span style="color:#f92672">=</span> (aiohttp<span style="color:#f92672">.</span>ClientError, asyncio<span style="color:#f92672">.</span>TimeoutError)
</span></span><span style="display:flex;"><span>):
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;非同期関数用のリトライデコレータ（指数バックオフ付き）&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">decorator</span>(func: Callable[<span style="color:#f92672">...</span>, T]) <span style="color:#f92672">-&gt;</span> Callable[<span style="color:#f92672">...</span>, T]:
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">@wraps</span>(func)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">wrapper</span>(<span style="color:#f92672">*</span>args, <span style="color:#f92672">**</span>kwargs) <span style="color:#f92672">-&gt;</span> T:
</span></span><span style="display:flex;"><span>            last_exception <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>            
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">for</span> attempt <span style="color:#f92672">in</span> range(max_retries <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>):
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">await</span> func(<span style="color:#f92672">*</span>args, <span style="color:#f92672">**</span>kwargs)
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">except</span> retryable_exceptions <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>                    last_exception <span style="color:#f92672">=</span> e
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">if</span> attempt <span style="color:#f92672">&lt;</span> max_retries:
</span></span><span style="display:flex;"><span>                        delay <span style="color:#f92672">=</span> min(
</span></span><span style="display:flex;"><span>                            base_delay <span style="color:#f92672">*</span> (exponential_base <span style="color:#f92672">**</span> attempt),
</span></span><span style="display:flex;"><span>                            max_delay
</span></span><span style="display:flex;"><span>                        )
</span></span><span style="display:flex;"><span>                        <span style="color:#75715e"># ジッター追加でサンダリングハード問題を回避</span>
</span></span><span style="display:flex;"><span>                        <span style="color:#f92672">import</span> random
</span></span><span style="display:flex;"><span>                        delay <span style="color:#f92672">*=</span> (<span style="color:#ae81ff">0.5</span> <span style="color:#f92672">+</span> random<span style="color:#f92672">.</span>random())
</span></span><span style="display:flex;"><span>                        print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Retry </span><span style="color:#e6db74">{</span>attempt <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span><span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">{</span>max_retries<span style="color:#e6db74">}</span><span style="color:#e6db74"> after </span><span style="color:#e6db74">{</span>delay<span style="color:#e6db74">:</span><span style="color:#e6db74">.2f</span><span style="color:#e6db74">}</span><span style="color:#e6db74">s: </span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>                        <span style="color:#66d9ef">await</span> asyncio<span style="color:#f92672">.</span>sleep(delay)
</span></span><span style="display:flex;"><span>            
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">raise</span> last_exception
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> wrapper
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> decorator
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@async_retry</span>(max_retries<span style="color:#f92672">=</span><span style="color:#ae81ff">3</span>, base_delay<span style="color:#f92672">=</span><span style="color:#ae81ff">0.5</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">fetch_with_retry</span>(session: aiohttp<span style="color:#f92672">.</span>ClientSession, url: str) <span style="color:#f92672">-&gt;</span> dict:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;リトライ機能付きのフェッチ関数&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    timeout <span style="color:#f92672">=</span> aiohttp<span style="color:#f92672">.</span>ClientTimeout(total<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>, connect<span style="color:#f92672">=</span><span style="color:#ae81ff">5</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">with</span> session<span style="color:#f92672">.</span>get(url, timeout<span style="color:#f92672">=</span>timeout) <span style="color:#66d9ef">as</span> response:
</span></span><span style="display:flex;"><span>        response<span style="color:#f92672">.</span>raise_for_status()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">await</span> response<span style="color:#f92672">.</span>json()
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="実践パターン3非同期キューによるプロデューサーコンシューマーパターン">実践パターン3：非同期キューによるプロデューサー・コンシューマーパターン</h2>
<p>大量のタスクを処理する場合、キューを使ったパターンが有効です。プロデューサーがタスクをキューに追加し、複数のコンシューマーが並行して処理します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">35
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">36
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">37
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">38
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">39
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">40
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">41
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">42
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">43
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">44
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">45
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">46
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">47
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">48
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">49
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">50
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">51
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">52
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">53
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">54
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">55
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">56
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">57
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">58
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">59
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">60
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">61
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">62
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">63
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">64
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">65
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> asyncio
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> dataclasses <span style="color:#f92672">import</span> dataclass
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Any
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> aiohttp
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@dataclass</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Task</span>:
</span></span><span style="display:flex;"><span>    id: int
</span></span><span style="display:flex;"><span>    url: str
</span></span><span style="display:flex;"><span>    payload: dict
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">producer</span>(queue: asyncio<span style="color:#f92672">.</span>Queue, tasks: list[Task]):
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;タスクをキューに追加&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> task <span style="color:#f92672">in</span> tasks:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">await</span> queue<span style="color:#f92672">.</span>put(task)
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Produced task </span><span style="color:#e6db74">{</span>task<span style="color:#f92672">.</span>id<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># 終了シグナル</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> _ <span style="color:#f92672">in</span> range(<span style="color:#ae81ff">3</span>):  <span style="color:#75715e"># コンシューマー数分</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">await</span> queue<span style="color:#f92672">.</span>put(<span style="color:#66d9ef">None</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">consumer</span>(
</span></span><span style="display:flex;"><span>    name: str, 
</span></span><span style="display:flex;"><span>    queue: asyncio<span style="color:#f92672">.</span>Queue, 
</span></span><span style="display:flex;"><span>    session: aiohttp<span style="color:#f92672">.</span>ClientSession,
</span></span><span style="display:flex;"><span>    results: list
</span></span><span style="display:flex;"><span>):
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;キューからタスクを取得して処理&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">while</span> <span style="color:#66d9ef">True</span>:
</span></span><span style="display:flex;"><span>        task <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> queue<span style="color:#f92672">.</span>get()
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> task <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>            queue<span style="color:#f92672">.</span>task_done()
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Consumer </span><span style="color:#e6db74">{</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74"> finished&#34;</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">break</span>
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">with</span> session<span style="color:#f92672">.</span>post(task<span style="color:#f92672">.</span>url, json<span style="color:#f92672">=</span>task<span style="color:#f92672">.</span>payload) <span style="color:#66d9ef">as</span> response:
</span></span><span style="display:flex;"><span>                result <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> response<span style="color:#f92672">.</span>json()
</span></span><span style="display:flex;"><span>                results<span style="color:#f92672">.</span>append({<span style="color:#e6db74">&#34;task_id&#34;</span>: task<span style="color:#f92672">.</span>id, <span style="color:#e6db74">&#34;result&#34;</span>: result})
</span></span><span style="display:flex;"><span>                print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Consumer </span><span style="color:#e6db74">{</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74"> processed task </span><span style="color:#e6db74">{</span>task<span style="color:#f92672">.</span>id<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">except</span> <span style="color:#a6e22e">Exception</span> <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>            print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Consumer </span><span style="color:#e6db74">{</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74"> failed on task </span><span style="color:#e6db74">{</span>task<span style="color:#f92672">.</span>id<span style="color:#e6db74">}</span><span style="color:#e6db74">: </span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">finally</span>:
</span></span><span style="display:flex;"><span>            queue<span style="color:#f92672">.</span>task_done()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">process_tasks</span>(tasks: list[Task], num_consumers: int <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>) <span style="color:#f92672">-&gt;</span> list[dict]:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;プロデューサー・コンシューマーパターンでタスクを処理&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    queue <span style="color:#f92672">=</span> asyncio<span style="color:#f92672">.</span>Queue(maxsize<span style="color:#f92672">=</span><span style="color:#ae81ff">100</span>)  <span style="color:#75715e"># バックプレッシャー制御</span>
</span></span><span style="display:flex;"><span>    results <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">with</span> aiohttp<span style="color:#f92672">.</span>ClientSession() <span style="color:#66d9ef">as</span> session:
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># プロデューサーとコンシューマーを並行実行</span>
</span></span><span style="display:flex;"><span>        consumers <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>            asyncio<span style="color:#f92672">.</span>create_task(consumer(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;C</span><span style="color:#e6db74">{</span>i<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>, queue, session, results))
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(num_consumers)
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span>        producer_task <span style="color:#f92672">=</span> asyncio<span style="color:#f92672">.</span>create_task(producer(queue, tasks))
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># 全タスク完了を待機</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">await</span> producer_task
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">await</span> asyncio<span style="color:#f92672">.</span>gather(<span style="color:#f92672">*</span>consumers)
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> results
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="実践パターン4データベース操作の非同期化">実践パターン4：データベース操作の非同期化</h2>
<p>SQLAlchemyやasyncpgを使って、データベース操作も非同期化できます。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">35
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">36
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy.ext.asyncio <span style="color:#f92672">import</span> create_async_engine, AsyncSession
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy.orm <span style="color:#f92672">import</span> sessionmaker, declarative_base
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> Column, Integer, String, select
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> asyncio
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 非同期エンジンの作成</span>
</span></span><span style="display:flex;"><span>DATABASE_URL <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;postgresql+asyncpg://user:pass@localhost/dbname&#34;</span>
</span></span><span style="display:flex;"><span>engine <span style="color:#f92672">=</span> create_async_engine(DATABASE_URL, echo<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>, pool_size<span style="color:#f92672">=</span><span style="color:#ae81ff">20</span>, max_overflow<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>AsyncSessionLocal <span style="color:#f92672">=</span> sessionmaker(
</span></span><span style="display:flex;"><span>    engine, class_<span style="color:#f92672">=</span>AsyncSession, expire_on_commit<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Base <span style="color:#f92672">=</span> declarative_base()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">User</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;users&#34;</span>
</span></span><span style="display:flex;"><span>    id <span style="color:#f92672">=</span> Column(Integer, primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    name <span style="color:#f92672">=</span> Column(String(<span style="color:#ae81ff">100</span>))
</span></span><span style="display:flex;"><span>    email <span style="color:#f92672">=</span> Column(String(<span style="color:#ae81ff">255</span>))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">get_users_batch</span>(user_ids: list[int]) <span style="color:#f92672">-&gt;</span> list[User]:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;複数ユーザーをバッチで取得&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">with</span> AsyncSessionLocal() <span style="color:#66d9ef">as</span> session:
</span></span><span style="display:flex;"><span>        result <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> session<span style="color:#f92672">.</span>execute(
</span></span><span style="display:flex;"><span>            select(User)<span style="color:#f92672">.</span>where(User<span style="color:#f92672">.</span>id<span style="color:#f92672">.</span>in_(user_ids))
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> result<span style="color:#f92672">.</span>scalars()<span style="color:#f92672">.</span>all()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">create_users_batch</span>(users_data: list[dict]) <span style="color:#f92672">-&gt;</span> list[User]:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;複数ユーザーをバッチで作成&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">with</span> AsyncSessionLocal() <span style="color:#66d9ef">as</span> session:
</span></span><span style="display:flex;"><span>        users <span style="color:#f92672">=</span> [User(<span style="color:#f92672">**</span>data) <span style="color:#66d9ef">for</span> data <span style="color:#f92672">in</span> users_data]
</span></span><span style="display:flex;"><span>        session<span style="color:#f92672">.</span>add_all(users)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">await</span> session<span style="color:#f92672">.</span>commit()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> users
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="実践パターン5asyncioとマルチプロセスの組み合わせ">実践パターン5：asyncioとマルチプロセスの組み合わせ</h2>
<p>CPUバウンドな処理がある場合、<code>asyncio</code>だけでは効果が限定的です。<code>concurrent.futures.ProcessPoolExecutor</code>と組み合わせることで、CPUバウンドとI/Oバウンドの両方を最適化できます。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">35
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">36
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">37
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">38
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">39
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> asyncio
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> concurrent.futures <span style="color:#f92672">import</span> ProcessPoolExecutor
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> hashlib
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">cpu_intensive_task</span>(data: bytes) <span style="color:#f92672">-&gt;</span> str:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;CPUバウンドな処理（例：ハッシュ計算）&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    result <span style="color:#f92672">=</span> data
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> _ <span style="color:#f92672">in</span> range(<span style="color:#ae81ff">100000</span>):
</span></span><span style="display:flex;"><span>        result <span style="color:#f92672">=</span> hashlib<span style="color:#f92672">.</span>sha256(result)<span style="color:#f92672">.</span>digest()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> result<span style="color:#f92672">.</span>hex()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">process_with_cpu_and_io</span>(items: list[bytes]) <span style="color:#f92672">-&gt;</span> list[dict]:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;CPU処理とI/O処理を組み合わせた非同期処理&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    loop <span style="color:#f92672">=</span> asyncio<span style="color:#f92672">.</span>get_event_loop()
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">with</span> ProcessPoolExecutor(max_workers<span style="color:#f92672">=</span><span style="color:#ae81ff">4</span>) <span style="color:#66d9ef">as</span> executor:
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># CPU処理を別プロセスで実行</span>
</span></span><span style="display:flex;"><span>        cpu_tasks <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>            loop<span style="color:#f92672">.</span>run_in_executor(executor, cpu_intensive_task, item)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">for</span> item <span style="color:#f92672">in</span> items
</span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># I/O処理も並行して実行</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">with</span> aiohttp<span style="color:#f92672">.</span>ClientSession() <span style="color:#66d9ef">as</span> session:
</span></span><span style="display:flex;"><span>            io_tasks <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>                fetch_with_retry(session, <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;https://api.example.com/process&#34;</span>)
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">for</span> _ <span style="color:#f92672">in</span> items
</span></span><span style="display:flex;"><span>            ]
</span></span><span style="display:flex;"><span>            
</span></span><span style="display:flex;"><span>            <span style="color:#75715e"># 両方の結果を待機</span>
</span></span><span style="display:flex;"><span>            cpu_results, io_results <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> asyncio<span style="color:#f92672">.</span>gather(
</span></span><span style="display:flex;"><span>                asyncio<span style="color:#f92672">.</span>gather(<span style="color:#f92672">*</span>cpu_tasks),
</span></span><span style="display:flex;"><span>                asyncio<span style="color:#f92672">.</span>gather(<span style="color:#f92672">*</span>io_tasks, return_exceptions<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>            )
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> [
</span></span><span style="display:flex;"><span>        {<span style="color:#e6db74">&#34;hash&#34;</span>: cpu_result, <span style="color:#e6db74">&#34;api_response&#34;</span>: io_result}
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> cpu_result, io_result <span style="color:#f92672">in</span> zip(cpu_results, io_results)
</span></span><span style="display:flex;"><span>    ]
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="パフォーマンス計測とデバッグ">パフォーマンス計測とデバッグ</h2>
<h3 id="実行時間の計測">実行時間の計測</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> asyncio
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> time
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> contextlib <span style="color:#f92672">import</span> asynccontextmanager
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> AsyncGenerator
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@asynccontextmanager</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">async_timer</span>(name: str) <span style="color:#f92672">-&gt;</span> AsyncGenerator[<span style="color:#66d9ef">None</span>, <span style="color:#66d9ef">None</span>]:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;非同期処理の実行時間を計測&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    start <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>perf_counter()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">yield</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">finally</span>:
</span></span><span style="display:flex;"><span>        elapsed <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>perf_counter() <span style="color:#f92672">-</span> start
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74">: </span><span style="color:#e6db74">{</span>elapsed<span style="color:#e6db74">:</span><span style="color:#e6db74">.4f</span><span style="color:#e6db74">}</span><span style="color:#e6db74">秒&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">main</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">with</span> async_timer(<span style="color:#e6db74">&#34;API一括取得&#34;</span>):
</span></span><span style="display:flex;"><span>        results <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> fetch_users_async(list(range(<span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">1001</span>)))
</span></span><span style="display:flex;"><span>    print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;取得件数: </span><span style="color:#e6db74">{</span>len(results)<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="デバッグモードの有効化">デバッグモードの有効化</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> asyncio
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># デバッグモードを有効化</span>
</span></span><span style="display:flex;"><span>asyncio<span style="color:#f92672">.</span>run(main(), debug<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># または環境変数で</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># PYTHONASYNCIODEBUG=1 python script.py</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="よくある落とし穴と対策">よくある落とし穴と対策</h2>
<h3 id="1-ブロッキング関数の呼び出し">1. ブロッキング関数の呼び出し</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># ❌ 悪い例：asyncio内で同期的なsleep</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">bad_example</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">import</span> time
</span></span><span style="display:flex;"><span>    time<span style="color:#f92672">.</span>sleep(<span style="color:#ae81ff">1</span>)  <span style="color:#75715e"># イベントループをブロック！</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ✅ 良い例：asyncio.sleepを使用</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">good_example</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> asyncio<span style="color:#f92672">.</span>sleep(<span style="color:#ae81ff">1</span>)  <span style="color:#75715e"># 他のタスクに制御を渡す</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="2-awaitの付け忘れ">2. awaitの付け忘れ</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">9
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># ❌ 悪い例：awaitなし</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">bad</span>():
</span></span><span style="display:flex;"><span>    result <span style="color:#f92672">=</span> fetch_data()  <span style="color:#75715e"># コルーチンオブジェクトが返るだけ</span>
</span></span><span style="display:flex;"><span>    print(result)  <span style="color:#75715e"># &lt;coroutine object ...&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ✅ 良い例：awaitあり</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">good</span>():
</span></span><span style="display:flex;"><span>    result <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> fetch_data()
</span></span><span style="display:flex;"><span>    print(result)  <span style="color:#75715e"># 実際のデータ</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="3-例外処理の不備">3. 例外処理の不備</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># ✅ gather使用時の例外処理</span>
</span></span><span style="display:flex;"><span>results <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> asyncio<span style="color:#f92672">.</span>gather(<span style="color:#f92672">*</span>tasks, return_exceptions<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> i, result <span style="color:#f92672">in</span> enumerate(results):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> isinstance(result, <span style="color:#a6e22e">Exception</span>):
</span></span><span style="display:flex;"><span>        print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Task </span><span style="color:#e6db74">{</span>i<span style="color:#e6db74">}</span><span style="color:#e6db74"> failed: </span><span style="color:#e6db74">{</span>result<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        process_result(result)
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="まとめ">まとめ</h2>
<p><code>asyncio</code>を使った非同期プログラミングは、I/Oバウンドな処理の高速化に非常に効果的です。本記事で紹介した実践パターンを活用することで、以下のような改善が期待できます：</p>
<ol>
<li><strong>API呼び出しの並行化</strong>：数十倍の高速化が可能</li>
<li><strong>適切な同時接続数制限</strong>：サーバー負荷とリソースの最適化</li>
<li><strong>リトライ機構</strong>：一時的な障害への耐性向上</li>
<li><strong>キューパターン</strong>：大量タスクの効率的な処理</li>
<li><strong>マルチプロセスとの組み合わせ</strong>：CPU+I/O両方の最適化</li>
</ol>
<p>重要なのは、<code>asyncio</code>は銀の弾丸ではないということです。CPUバウンドな処理には効果がなく、適切な設計なしに導入するとかえって複雑さが増すこともあります。処理の特性を理解した上で、適材適所で活用していきましょう。</p>
]]></content:encoded>
      <category>Python</category>
      <category>バックエンド</category>
      <category>Python</category>
      <category>asyncio</category>
      <category>非同期処理</category>
      <category>並行処理</category>
      <category>パフォーマンス</category>
    </item>
    <item>
      <title>FastAPI &#43; SQLAlchemy性能改善プレイブック: 遅いAPIを計測ベースで高速化する</title>
      <link>https://www.ai2core.com/posts/2026-03-07-fastapi-sqlalchemy-performance-tuning-playbook/</link>
      <pubDate>Sat, 07 Mar 2026 11:10:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-07-fastapi-sqlalchemy-performance-tuning-playbook/</guid>
      <description>FastAPIとSQLAlchemyのAPI性能を、N&#43;1解消・クエリ最適化・接続プール設定・負荷検証まで含めて具体的に改善する実践手順を解説。</description>
      <content:encoded><![CDATA[<h1 id="fastapi--sqlalchemy性能改善プレイブック-遅いapiを計測ベースで高速化する">FastAPI + SQLAlchemy性能改善プレイブック: 遅いAPIを計測ベースで高速化する</h1>
<p>FastAPIの初期実装は非常に快適です。しかし運用フェーズに入ると、次のような症状が出てきます。</p>
<ul>
<li>一覧APIのレスポンスが急に遅くなる</li>
<li>同時接続が増えるとp95が跳ねる</li>
<li>CPUは余っているのにタイムアウトが増える</li>
<li>DB接続数が上限に張り付く</li>
</ul>
<p>こうした問題の多くは「Pythonが遅い」のではなく、<strong>SQLAlchemyの使い方とDBアクセス設計</strong> に起因します。</p>
<p>本記事では、FastAPI + SQLAlchemy + PostgreSQL構成を前提に、実際の改善手順を計測ベースで整理します。</p>
<h2 id="1-最初に測るべき指標">1. 最初に測るべき指標</h2>
<p>最適化は、体感ではなく数値で進めます。最低限、以下を可視化します。</p>
<ul>
<li>APIのp50/p95/p99レイテンシ</li>
<li>エンドポイント別SQL発行回数</li>
<li>1リクエストあたりのDB滞在時間</li>
<li>connection pool待ち時間</li>
<li>slow query件数（200ms以上など）</li>
</ul>
<p>OpenTelemetryやNew Relicを使っているなら、アプリspanとDB spanを必ず紐付けてください。これだけでボトルネック特定速度が上がります。</p>
<h2 id="2-n1問題を最優先で潰す">2. N+1問題を最優先で潰す</h2>
<p>最も頻出するのがN+1です。例えばユーザー一覧でプロフィールを参照すると、ユーザー数分の追加クエリが発行されます。</p>
<h3 id="21-悪い例">2.1 悪い例</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>users <span style="color:#f92672">=</span> session<span style="color:#f92672">.</span>query(User)<span style="color:#f92672">.</span>limit(<span style="color:#ae81ff">100</span>)<span style="color:#f92672">.</span>all()
</span></span><span style="display:flex;"><span>result <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> u <span style="color:#f92672">in</span> users:
</span></span><span style="display:flex;"><span>    result<span style="color:#f92672">.</span>append({
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;id&#34;</span>: u<span style="color:#f92672">.</span>id,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;name&#34;</span>: u<span style="color:#f92672">.</span>name,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;profile&#34;</span>: u<span style="color:#f92672">.</span>profile<span style="color:#f92672">.</span>bio,
</span></span><span style="display:flex;"><span>    })
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="22-改善例joinedloadselectinload">2.2 改善例（joinedload/selectinload）</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy.orm <span style="color:#f92672">import</span> selectinload
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>users <span style="color:#f92672">=</span> (
</span></span><span style="display:flex;"><span>    session<span style="color:#f92672">.</span>query(User)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">.</span>options(selectinload(User<span style="color:#f92672">.</span>profile))
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">.</span>limit(<span style="color:#ae81ff">100</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">.</span>all()
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>joinedload</code> と <code>selectinload</code> はデータ量で使い分けます。</p>
<ul>
<li>1対1/少量: <code>joinedload</code></li>
<li>1対多/件数多め: <code>selectinload</code></li>
</ul>
<p>闇雲に <code>joinedload</code> を増やすと行爆発が起きるため、EXPLAINで確認しながら適用します。</p>
<h2 id="3-sqlalchemy-2xスタイルへ揃える">3. SQLAlchemy 2.xスタイルへ揃える</h2>
<p>旧query APIと新APIが混在すると可読性と最適化精度が落ちます。2.xスタイルへ統一しましょう。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">9
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> select
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>stmt <span style="color:#f92672">=</span> (
</span></span><span style="display:flex;"><span>    select(Order)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">.</span>where(Order<span style="color:#f92672">.</span>status <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;paid&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">.</span>order_by(Order<span style="color:#f92672">.</span>created_at<span style="color:#f92672">.</span>desc())
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">.</span>limit(<span style="color:#ae81ff">50</span>)
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>orders <span style="color:#f92672">=</span> session<span style="color:#f92672">.</span>execute(stmt)<span style="color:#f92672">.</span>scalars()<span style="color:#f92672">.</span>all()
</span></span></code></pre></td></tr></table>
</div>
</div><p>この形式は、<code>EXPLAIN</code> の追跡や再利用がしやすく、レビュー品質も上がります。</p>
<h2 id="4-必要な列だけ取る過剰フェッチの削減">4. 必要な列だけ取る（過剰フェッチの削減）</h2>
<p>ORMは便利ですが、何も考えずモデル全体を取ると不要データまで転送されます。特にJSONカラムやTEXTが重い場合、ここが効きます。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>stmt <span style="color:#f92672">=</span> select(User<span style="color:#f92672">.</span>id, User<span style="color:#f92672">.</span>name, User<span style="color:#f92672">.</span>email)<span style="color:#f92672">.</span>where(User<span style="color:#f92672">.</span>active<span style="color:#f92672">.</span>is_(<span style="color:#66d9ef">True</span>))
</span></span><span style="display:flex;"><span>rows <span style="color:#f92672">=</span> session<span style="color:#f92672">.</span>execute(stmt)<span style="color:#f92672">.</span>all()
</span></span></code></pre></td></tr></table>
</div>
</div><p>一覧APIはDTO用の軽量SELECTを使い、詳細APIでのみ重いカラムを取得する設計が安定します。</p>
<h2 id="5-接続プール設定を環境に合わせる">5. 接続プール設定を環境に合わせる</h2>
<p><code>pool_size</code> を適当に増やすだけでは逆効果です。PostgreSQL側上限とアプリ台数を合わせて設計します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>engine <span style="color:#f92672">=</span> create_engine(
</span></span><span style="display:flex;"><span>    DB_URL,
</span></span><span style="display:flex;"><span>    pool_size<span style="color:#f92672">=</span><span style="color:#ae81ff">20</span>,
</span></span><span style="display:flex;"><span>    max_overflow<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>,
</span></span><span style="display:flex;"><span>    pool_timeout<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>,
</span></span><span style="display:flex;"><span>    pool_recycle<span style="color:#f92672">=</span><span style="color:#ae81ff">1800</span>,
</span></span><span style="display:flex;"><span>    pool_pre_ping<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></td></tr></table>
</div>
</div><p>設計の目安:</p>
<ul>
<li>DB max_connections = 300</li>
<li>API Pod = 6</li>
<li>1 Podあたりpool_size 20</li>
</ul>
<p>この時点で理論最大120接続。バッチや管理接続も見込み、余白を残すのが安全です。</p>
<h2 id="6-トランザクション境界を短くする">6. トランザクション境界を短くする</h2>
<p>長いトランザクションはロック競合とスループット低下を招きます。</p>
<p>悪い例:</p>
<ol>
<li>DB更新</li>
<li>外部API呼び出し</li>
<li>メール送信</li>
<li>commit</li>
</ol>
<p>この順は危険です。外部I/Oをトランザクション外へ逃がします。</p>
<p>改善例:</p>
<ol>
<li>DB更新 + commit</li>
<li>外部通知は非同期ジョブで実行</li>
</ol>
<p>これだけで同時処理性能が目に見えて改善します。</p>
<h2 id="7-インデックス設計をapi単位で見直す">7. インデックス設計をAPI単位で見直す</h2>
<p>「インデックスはある」だけでは不足です。実際のWHERE + ORDER BYに合っているかが重要です。</p>
<p>例: 注文履歴API</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> id, total_amount, created_at
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> orders
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> user_id <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">1</span> <span style="color:#66d9ef">AND</span> status <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;paid&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> created_at <span style="color:#66d9ef">DESC</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">LIMIT</span> <span style="color:#ae81ff">50</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><p>この場合、次の複合indexが有効です。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> CONCURRENTLY idx_orders_user_status_created_at_desc
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ON</span> orders (user_id, status, created_at <span style="color:#66d9ef">DESC</span>);
</span></span></code></pre></td></tr></table>
</div>
</div><p>単独indexを乱立させるより、アクセスパターンに合わせた複合indexを厳選した方が効きます。</p>
<h2 id="8-キャッシュ導入は遅い理由の解決後に行う">8. キャッシュ導入は「遅い理由の解決後」に行う</h2>
<p>キャッシュは万能ではありません。N+1やスロークエリを放置したまま載せると、整合性事故の温床になります。</p>
<p>導入順序:</p>
<ol>
<li>SQL最適化</li>
<li>接続プール調整</li>
<li>必要なエンドポイントに限定してRedis cache</li>
</ol>
<p>キャッシュキーは <code>resource:id:version</code> 形式にし、更新時の無効化戦略を先に定義してください。</p>
<h2 id="9-負荷試験シナリオk6例">9. 負荷試験シナリオ（k6例）</h2>
<p>最適化の成果は負荷試験で確認します。最低3シナリオが必要です。</p>
<ul>
<li>steady: 通常トラフィック</li>
<li>burst: 短時間ピーク</li>
<li>soak: 長時間連続実行（リーク検知）</li>
</ul>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">http</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;k6/http&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">check</span>, <span style="color:#a6e22e">sleep</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;k6&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">options</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">stages</span><span style="color:#f92672">:</span> [
</span></span><span style="display:flex;"><span>    { <span style="color:#a6e22e">duration</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;2m&#39;</span>, <span style="color:#a6e22e">target</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">50</span> },
</span></span><span style="display:flex;"><span>    { <span style="color:#a6e22e">duration</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;5m&#39;</span>, <span style="color:#a6e22e">target</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">50</span> },
</span></span><span style="display:flex;"><span>    { <span style="color:#a6e22e">duration</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;1m&#39;</span>, <span style="color:#a6e22e">target</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">200</span> },
</span></span><span style="display:flex;"><span>    { <span style="color:#a6e22e">duration</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;2m&#39;</span>, <span style="color:#a6e22e">target</span><span style="color:#f92672">:</span> <span style="color:#ae81ff">0</span> },
</span></span><span style="display:flex;"><span>  ],
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">function</span> () {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">res</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">get</span>(<span style="color:#e6db74">&#39;https://api.example.com/orders?limit=50&#39;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">check</span>(<span style="color:#a6e22e">res</span>, { <span style="color:#e6db74">&#39;status 200&#39;</span><span style="color:#f92672">:</span> (<span style="color:#a6e22e">r</span>) =&gt; <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">status</span> <span style="color:#f92672">===</span> <span style="color:#ae81ff">200</span> });
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">sleep</span>(<span style="color:#ae81ff">1</span>);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>改善前後で <code>p95</code>, SQL回数, DB CPU を比較し、定量で判断します。</p>
<h2 id="10-本番での改善手順テンプレート">10. 本番での改善手順テンプレート</h2>
<ol>
<li>ボトルネックendpointの特定</li>
<li>SQLログとEXPLAIN ANALYZE取得</li>
<li>N+1解消</li>
<li>必要列取得へ変更</li>
<li>インデックス追加（CONCURRENTLY）</li>
<li>pool設定調整</li>
<li>負荷試験再実施</li>
<li>段階リリース（10%→50%→100%）</li>
</ol>
<p>この手順を運用チーム全体でテンプレ化すると、パフォーマンス問題への対応速度が上がります。</p>
<h2 id="まとめ">まとめ</h2>
<p>FastAPI + SQLAlchemyの性能改善は、派手なテクニックより <strong>計測→原因分離→小さく改善</strong> の積み重ねが効きます。</p>
<ul>
<li>N+1解消</li>
<li>過剰フェッチ削減</li>
<li>接続プール最適化</li>
<li>インデックスの再設計</li>
<li>負荷試験で再検証</li>
</ul>
<p>この5点を回せば、遅いAPIは高確率で改善できます。まずは「1リクエストあたりのSQL発行数」を可視化するところから始めるのが最短です。</p>
<h2 id="付録-改善施策の優先順位最短で効く順">付録: 改善施策の優先順位（最短で効く順）</h2>
<p>時間が限られる現場では、まず「SQL発行回数削減 → インデックス最適化 → connection pool調整」の順で着手すると効果が出やすいです。特に、N+1解消だけでp95が半減するケースは珍しくありません。改善後は必ず同一負荷条件で再計測し、数字で効果を残しておくと、次の改善投資判断が通りやすくなります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>FastAPI</category>
      <category>SQLAlchemy</category>
      <category>PostgreSQL</category>
      <category>Python</category>
      <category>Performance</category>
    </item>
    <item>
      <title>FastAPI &#43; Celery信頼性設計: 非同期ジョブを本番で壊さないための実装パターン</title>
      <link>https://www.ai2core.com/posts/2026-03-06-fastapi-celery-reliability-patterns/</link>
      <pubDate>Fri, 06 Mar 2026 09:03:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-06-fastapi-celery-reliability-patterns/</guid>
      <description>FastAPIとCeleryを使った非同期処理を本番運用するために、再実行安全性、監視、失敗復旧、デプロイ戦略を具体例付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="fastapi--celery信頼性設計-非同期ジョブを本番で壊さないための実装パターン">FastAPI + Celery信頼性設計: 非同期ジョブを本番で壊さないための実装パターン</h1>
<p>FastAPIでAPIを作ると、重い処理はすぐに非同期ジョブへ逃がしたくなります。画像変換、レポート生成、外部API連携、メール配信など、Celeryは非常に便利です。ですが、本番で問題になるのは「動くかどうか」ではなく、<strong>失敗したときに壊れないか</strong> です。</p>
<ul>
<li>同じジョブが二重実行される</li>
<li>一時障害で永遠にリトライしてキューが詰まる</li>
<li>ワーカー再起動で中途半端な状態が残る</li>
<li>完了通知が先に飛んで実データがない</li>
</ul>
<p>本記事では FastAPI + Celery + Redis 構成を前提に、再実行安全性（idempotency）と運用信頼性を上げる実装手順をまとめます。</p>
<h2 id="1-まず守るべき設計原則">1. まず守るべき設計原則</h2>
<p>非同期基盤の事故は、ほぼ次の4原則で防げます。</p>
<ol>
<li><strong>At-least-once前提</strong>（同一タスク再実行は必ず起こる）</li>
<li><strong>副作用は冪等化</strong>（何回実行されても結果が壊れない）</li>
<li><strong>状態遷移を明示</strong>（PENDING/RUNNING/SUCCEEDED/FAILED）</li>
<li><strong>失敗を可観測化</strong>（リトライ回数・死活・滞留時間を計測）</li>
</ol>
<p>この原則を外すと、障害時に「何が完了して何が未完了か」が追えなくなります。</p>
<h2 id="2-参照アーキテクチャ">2. 参照アーキテクチャ</h2>
<ul>
<li>API: FastAPI</li>
<li>Queue Broker: Redis</li>
<li>Worker: Celery</li>
<li>Result Store: PostgreSQL（業務状態）</li>
<li>Monitoring: Flower + Prometheus + Sentry</li>
</ul>
<p>ポイントは、<strong>業務上重要な状態はRedis結果バックエンドに依存しない</strong> ことです。Redisは一時的に使い、真実の状態はRDBに持たせます。</p>
<h2 id="3-実装の土台-タスク受付api">3. 実装の土台: タスク受付API</h2>
<h3 id="31-受け付け時に-idempotency_key-を必須化">3.1 受け付け時に idempotency_key を必須化</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> fastapi <span style="color:#f92672">import</span> FastAPI, HTTPException
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> pydantic <span style="color:#f92672">import</span> BaseModel
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> select
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>app <span style="color:#f92672">=</span> FastAPI()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">JobRequest</span>(BaseModel):
</span></span><span style="display:flex;"><span>    idempotency_key: str
</span></span><span style="display:flex;"><span>    report_type: str
</span></span><span style="display:flex;"><span>    user_id: str
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@app.post</span>(<span style="color:#e6db74">&#34;/reports&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">create_report</span>(req: JobRequest):
</span></span><span style="display:flex;"><span>    existing <span style="color:#f92672">=</span> find_job_by_key(req<span style="color:#f92672">.</span>idempotency_key)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> existing:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> {<span style="color:#e6db74">&#34;job_id&#34;</span>: existing<span style="color:#f92672">.</span>id, <span style="color:#e6db74">&#34;status&#34;</span>: existing<span style="color:#f92672">.</span>status}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    job <span style="color:#f92672">=</span> create_job_record(
</span></span><span style="display:flex;"><span>        idempotency_key<span style="color:#f92672">=</span>req<span style="color:#f92672">.</span>idempotency_key,
</span></span><span style="display:flex;"><span>        status<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;PENDING&#34;</span>,
</span></span><span style="display:flex;"><span>        report_type<span style="color:#f92672">=</span>req<span style="color:#f92672">.</span>report_type,
</span></span><span style="display:flex;"><span>        user_id<span style="color:#f92672">=</span>req<span style="color:#f92672">.</span>user_id,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    generate_report<span style="color:#f92672">.</span>delay(job<span style="color:#f92672">.</span>id)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> {<span style="color:#e6db74">&#34;job_id&#34;</span>: job<span style="color:#f92672">.</span>id, <span style="color:#e6db74">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;PENDING&#34;</span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>これでクライアント再送が来てもジョブ多重作成を防げます。</p>
<h3 id="32-db制約で最終防衛線を張る">3.2 DB制約で最終防衛線を張る</h3>
<p><code>idempotency_key</code> に UNIQUE 制約を入れ、アプリバグ時も二重作成を防ぎます。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> async_jobs <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">CONSTRAINT</span> uq_async_jobs_idempotency <span style="color:#66d9ef">UNIQUE</span> (idempotency_key);
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="4-celeryタスクの実践設定">4. Celeryタスクの実践設定</h2>
<h3 id="41-推奨設定">4.1 推奨設定</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> celery <span style="color:#f92672">import</span> Celery
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>celery_app <span style="color:#f92672">=</span> Celery(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;worker&#34;</span>,
</span></span><span style="display:flex;"><span>    broker<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;redis://redis:6379/0&#34;</span>,
</span></span><span style="display:flex;"><span>    backend<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;redis://redis:6379/1&#34;</span>,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>celery_app<span style="color:#f92672">.</span>conf<span style="color:#f92672">.</span>update(
</span></span><span style="display:flex;"><span>    task_acks_late<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    task_reject_on_worker_lost<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    worker_prefetch_multiplier<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>,
</span></span><span style="display:flex;"><span>    task_time_limit<span style="color:#f92672">=</span><span style="color:#ae81ff">900</span>,
</span></span><span style="display:flex;"><span>    task_soft_time_limit<span style="color:#f92672">=</span><span style="color:#ae81ff">840</span>,
</span></span><span style="display:flex;"><span>    task_default_retry_delay<span style="color:#f92672">=</span><span style="color:#ae81ff">30</span>,
</span></span><span style="display:flex;"><span>    task_routes<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;tasks.generate_report&#34;</span>: {<span style="color:#e6db74">&#34;queue&#34;</span>: <span style="color:#e6db74">&#34;reports&#34;</span>}},
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></td></tr></table>
</div>
</div><ul>
<li><code>acks_late=True</code>: 実行完了後にACK。途中クラッシュ時は再配信</li>
<li><code>prefetch_multiplier=1</code>: 取り込み過多を防ぎ、偏りを減らす</li>
<li>time limit: ハング抑止</li>
</ul>
<h3 id="42-リトライは指数バックオフ--上限">4.2 リトライは指数バックオフ + 上限</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#a6e22e">@celery_app.task</span>(
</span></span><span style="display:flex;"><span>    bind<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    autoretry_for<span style="color:#f92672">=</span>(TemporaryExternalError,),
</span></span><span style="display:flex;"><span>    retry_backoff<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    retry_backoff_max<span style="color:#f92672">=</span><span style="color:#ae81ff">300</span>,
</span></span><span style="display:flex;"><span>    retry_jitter<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    max_retries<span style="color:#f92672">=</span><span style="color:#ae81ff">7</span>,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">generate_report</span>(self, job_id: str):
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">...</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>無制限リトライは障害増幅装置です。必ず上限を設定します。</p>
<h2 id="5-冪等タスクの実装パターン">5. 冪等タスクの実装パターン</h2>
<h3 id="51-状態遷移をトランザクションで管理">5.1 状態遷移をトランザクションで管理</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">start_job</span>(job_id: str):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">with</span> session<span style="color:#f92672">.</span>begin():
</span></span><span style="display:flex;"><span>        job <span style="color:#f92672">=</span> session<span style="color:#f92672">.</span>get(Job, job_id, with_for_update<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> job<span style="color:#f92672">.</span>status <span style="color:#f92672">in</span> (<span style="color:#e6db74">&#34;RUNNING&#34;</span>, <span style="color:#e6db74">&#34;SUCCEEDED&#34;</span>):
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>        job<span style="color:#f92672">.</span>status <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;RUNNING&#34;</span>
</span></span><span style="display:flex;"><span>        job<span style="color:#f92672">.</span>started_at <span style="color:#f92672">=</span> utcnow()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">True</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>FOR UPDATE</code> を使い、同時実行で状態が競合しないようにします。</p>
<h3 id="52-副作用前に実行済みチェック">5.2 副作用前に“実行済みチェック”</h3>
<p>外部API呼び出しやファイル生成前に、既に成果物が存在するか確認します。</p>
<ul>
<li>既に同名レポートが生成済みならスキップ</li>
<li>外部通知は送信履歴テーブルで重複防止</li>
<li>決済や課金は必ず業務ID単位で一意化</li>
</ul>
<h3 id="53-完了処理はcompare-and-setで確定">5.3 完了処理はCompare-and-Setで確定</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">complete_job</span>(job_id: str, result_url: str):
</span></span><span style="display:flex;"><span>    updated <span style="color:#f92672">=</span> session<span style="color:#f92672">.</span>execute(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        UPDATE async_jobs
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        SET status=&#39;SUCCEEDED&#39;, result_url=:url, finished_at=now()
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        WHERE id=:id AND status=&#39;RUNNING&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &#34;&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>        {<span style="color:#e6db74">&#34;id&#34;</span>: job_id, <span style="color:#e6db74">&#34;url&#34;</span>: result_url},
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> updated<span style="color:#f92672">.</span>rowcount <span style="color:#f92672">==</span> <span style="color:#ae81ff">1</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>これで二重完了更新を防げます。</p>
<h2 id="6-失敗時の設計dlq相当の運用">6. 失敗時の設計（DLQ相当の運用）</h2>
<p>Celeryに“標準DLQ”はありませんが、実運用では次の形で代替できます。</p>
<ul>
<li>リトライ上限超過時に <code>FAILED_PERMANENT</code> へ遷移</li>
<li>失敗理由とスタックトレースをDB保存</li>
<li>再実行API（手動リカバリ）を提供</li>
<li>重大失敗はSentry + Pagerで通知</li>
</ul>
<p>この構成で「黙って死ぬジョブ」をなくせます。</p>
<h2 id="7-監視設計最低限">7. 監視設計（最低限）</h2>
<h3 id="メトリクス">メトリクス</h3>
<ul>
<li>キュー滞留数（queue length）</li>
<li>oldest message age</li>
<li>タスク成功率 / 失敗率</li>
<li>p95 実行時間</li>
<li>リトライ回数分布</li>
</ul>
<h3 id="アラート例">アラート例</h3>
<ul>
<li>滞留数が通常の3倍を10分継続</li>
<li>失敗率 &gt; 5% が15分継続</li>
<li>oldest message age &gt; 20分</li>
<li>worker heartbeat消失</li>
</ul>
<p>「CPU高い」より「キューが古い」がユーザー影響に直結します。</p>
<h2 id="8-デプロイ時の落とし穴">8. デプロイ時の落とし穴</h2>
<h3 id="81-ローリング更新での重複実行">8.1 ローリング更新での重複実行</h3>
<ul>
<li><code>acks_late</code> + graceful shutdown を設定</li>
<li><code>TERM</code> 後にタスク完了待ち時間を確保</li>
<li>長時間ジョブは分割し、中断耐性を持たせる</li>
</ul>
<h3 id="82-スキーマ変更の順序">8.2 スキーマ変更の順序</h3>
<p>非同期基盤では、ワーカーとAPIが異なるバージョンで同居します。</p>
<p>安全な順序:</p>
<ol>
<li>先に後方互換なDB変更を適用</li>
<li>ワーカーを先に更新</li>
<li>APIを更新</li>
<li>非互換削除は次リリースで</li>
</ol>
<p>これを守らないと、古いタスクが新スキーマで失敗します。</p>
<h2 id="9-ローカルステージングでの検証手順">9. ローカル・ステージングでの検証手順</h2>
<ol>
<li>正常系: ジョブ作成→完了→結果取得</li>
<li>再送系: 同一 <code>idempotency_key</code> で2回POST</li>
<li>障害系: 外部APIタイムアウトを強制しリトライ確認</li>
<li>クラッシュ系: 実行中にworker再起動し再配信確認</li>
<li>負荷系: 1000ジョブ投入で滞留時間と失敗率確認</li>
</ol>
<p>この5ケースを自動テストに入れるだけで、運用品質は大幅に上がります。</p>
<h2 id="10-本番チェックリスト">10. 本番チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> idempotency_key のUNIQUE制約あり</li>
<li><input disabled="" type="checkbox"> 冪等な状態遷移実装（RUNNING/SUCCEEDED）</li>
<li><input disabled="" type="checkbox"> リトライ上限 + バックオフ設定済み</li>
<li><input disabled="" type="checkbox"> 手動再実行導線あり</li>
<li><input disabled="" type="checkbox"> 失敗通知（Sentry/Pager）有効</li>
<li><input disabled="" type="checkbox"> 滞留監視とアラート運用あり</li>
<li><input disabled="" type="checkbox"> デプロイ手順に互換性ルール明記</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>FastAPI + Celeryの本質は、非同期化そのものではなく <strong>失敗しても壊れない設計</strong> にあります。</p>
<ul>
<li>At-least-once を前提に設計する</li>
<li>冪等性をDB制約と状態遷移で担保する</li>
<li>リトライと監視を“運用可能”な形で実装する</li>
<li>デプロイ時のバージョン混在を想定する</li>
</ul>
<p>ここまで作り込むと、ジョブ基盤は「たまに落ちるブラックボックス」から「予測可能に運用できるインフラ」へ変わります。まずは <code>idempotency_key</code> と状態遷移の明確化から始めるのがおすすめです。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>FastAPI</category>
      <category>Celery</category>
      <category>Redis</category>
      <category>Python</category>
      <category>Reliability</category>
    </item>
    <item>
      <title>FastAPI認証・認可の本番設計：JWT運用、権限制御、監査ログまで含めた実装パターン</title>
      <link>https://www.ai2core.com/posts/2026-03-04-fastapi-authn-authz-production-patterns/</link>
      <pubDate>Wed, 04 Mar 2026 09:35:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-04-fastapi-authn-authz-production-patterns/</guid>
      <description>FastAPIで安全に認証・認可を実装するために、トークン設計、ローテーション、RBAC、監査、障害時運用まで具体手順で解説。</description>
      <content:encoded><![CDATA[<h1 id="fastapi認証認可の本番設計jwt運用権限制御監査ログまで含めた実装パターン">FastAPI認証・認可の本番設計：JWT運用、権限制御、監査ログまで含めた実装パターン</h1>
<p>FastAPI は実装が速い反面、認証・認可を最小構成のまま本番に出してしまい、後からセキュリティ事故に発展するケースが少なくありません。特に「JWT を入れたから安全」という誤解は危険です。</p>
<p>本記事では、<strong>開発速度を落とさずに本番で耐える認証基盤</strong>を作るための設計を、コード例と運用手順込みで解説します。</p>
<h2 id="1-認証と認可を分離して設計する">1. 認証と認可を分離して設計する</h2>
<p>最初に押さえるべきは責務分離です。</p>
<ul>
<li>認証（Authentication）: 誰かを確認する</li>
<li>認可（Authorization）: 何をしてよいか判定する</li>
</ul>
<p>この2つを混ぜると、実装も監査も破綻します。FastAPI では dependency を分け、<code>get_current_user</code> と <code>require_permission</code> を独立させるのが基本です。</p>
<h2 id="2-jwt-は短命--リフレッシュ--失効管理で使う">2. JWT は「短命 + リフレッシュ + 失効管理」で使う</h2>
<p>アクセストークンを長寿命にすると、漏えい時の被害が大きくなります。実運用では以下が標準です。</p>
<ul>
<li>Access Token: 5〜15分</li>
<li>Refresh Token: 7〜30日</li>
<li>Refresh Token は DB 保存し、ローテーション時に旧トークンを失効</li>
</ul>
<p><code>sub</code> だけでなく、<code>jti</code>（トークンID）や <code>scope</code> を持たせると管理しやすくなります。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> datetime <span style="color:#f92672">import</span> datetime, timedelta, timezone
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> jwt
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>ALGORITHM <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;HS256&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">create_access_token</span>(user_id: str, scopes: list[str], secret: str) <span style="color:#f92672">-&gt;</span> str:
</span></span><span style="display:flex;"><span>    now <span style="color:#f92672">=</span> datetime<span style="color:#f92672">.</span>now(timezone<span style="color:#f92672">.</span>utc)
</span></span><span style="display:flex;"><span>    payload <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;sub&#34;</span>: user_id,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;scope&#34;</span>: <span style="color:#e6db74">&#34; &#34;</span><span style="color:#f92672">.</span>join(scopes),
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;iat&#34;</span>: int(now<span style="color:#f92672">.</span>timestamp()),
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;exp&#34;</span>: int((now <span style="color:#f92672">+</span> timedelta(minutes<span style="color:#f92672">=</span><span style="color:#ae81ff">10</span>))<span style="color:#f92672">.</span>timestamp()),
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;jti&#34;</span>: <span style="color:#e6db74">&#34;generated-uuid&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> jwt<span style="color:#f92672">.</span>encode(payload, secret, algorithm<span style="color:#f92672">=</span>ALGORITHM)
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="3-鍵管理とローテーション">3. 鍵管理とローテーション</h2>
<p>秘密鍵を <code>.env</code> に固定して数年運用するのは典型的な事故パターンです。最低限、次を実施します。</p>
<ul>
<li>KMS/Vault など外部シークレット管理を利用</li>
<li><code>kid</code> をヘッダに持たせ、複数鍵を並行運用</li>
<li>鍵ローテーション手順を runbook 化</li>
</ul>
<p>ローテーションの要点:</p>
<ol>
<li>新鍵を追加（検証側は新旧どちらも受理）</li>
<li>発行側を新鍵へ切替</li>
<li>旧鍵の有効期限を過ぎたら削除</li>
</ol>
<p>この手順にすると、無停止で切替できます。</p>
<h2 id="4-fastapi-dependencyで認可を明示化">4. FastAPI Dependencyで認可を明示化</h2>
<p>ロジック中で <code>if user.role == &quot;admin&quot;</code> を乱立させると、抜け漏れが起こります。権限チェックは dependency 化し、ルート定義に明示します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> fastapi <span style="color:#f92672">import</span> Depends, HTTPException, status
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">require_permission</span>(required: str):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">checker</span>(user<span style="color:#f92672">=</span>Depends(get_current_user)):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> required <span style="color:#f92672">not</span> <span style="color:#f92672">in</span> user<span style="color:#f92672">.</span>permissions:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">raise</span> HTTPException(
</span></span><span style="display:flex;"><span>                status_code<span style="color:#f92672">=</span>status<span style="color:#f92672">.</span>HTTP_403_FORBIDDEN,
</span></span><span style="display:flex;"><span>                detail<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;insufficient permissions&#34;</span>
</span></span><span style="display:flex;"><span>            )
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> user
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> checker
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@router.delete</span>(<span style="color:#e6db74">&#34;/projects/</span><span style="color:#e6db74">{project_id}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">delete_project</span>(
</span></span><span style="display:flex;"><span>    project_id: str,
</span></span><span style="display:flex;"><span>    user<span style="color:#f92672">=</span>Depends(require_permission(<span style="color:#e6db74">&#34;project:delete&#34;</span>))
</span></span><span style="display:flex;"><span>):
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">...</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>ルート単位で要件が見えるため、レビュー効率と監査性が上がります。</p>
<h2 id="5-rbacabac-の使い分け">5. RBAC/ABAC の使い分け</h2>
<p>小規模なら RBAC（role-based）で十分ですが、顧客単位データや組織階層があると ABAC（属性ベース）を併用した方が安全です。</p>
<ul>
<li>RBAC: <code>admin</code>, <code>editor</code>, <code>viewer</code></li>
<li>ABAC: <code>tenant_id</code>, <code>resource_owner_id</code>, <code>department</code></li>
</ul>
<p>実務では「ロールで粗く許可し、属性で絞る」が扱いやすいです。</p>
<h2 id="6-マルチテナントで必須の防御">6. マルチテナントで必須の防御</h2>
<p>マルチテナント API では、ID 推測よりも<strong>テナント境界漏れ</strong>が主要リスクです。対策は次の通りです。</p>
<ul>
<li>すべての DB クエリに <code>tenant_id</code> 条件を必須化</li>
<li>管理者 API でも境界を明示的に超える操作だけ許可</li>
<li>監査ログに <code>tenant_id</code>, <code>actor_id</code>, <code>resource_id</code> を残す</li>
</ul>
<p>SQLAlchemy でも repository 層で共通フィルタを強制すると漏れを減らせます。</p>
<h2 id="7-監査ログを設計段階で入れる">7. 監査ログを設計段階で入れる</h2>
<p>認証系は障害後に「誰が何をしたか」が必要になります。後付けだと間に合いません。最低限、次を記録します。</p>
<ul>
<li>ログイン成功/失敗（IP, user-agent, reason）</li>
<li>権限エラー（403）</li>
<li>重要操作（削除、権限変更、請求情報更新）</li>
<li>トークン失効・再発行</li>
</ul>
<p>フォーマットは JSON 構造化に統一し、SIEM や OpenSearch に流せる形にしておくと分析が速いです。</p>
<h2 id="8-レート制限とブルートフォース対策">8. レート制限とブルートフォース対策</h2>
<p>パスワード認証がある場合、レート制限なしは危険です。<code>slowapi</code> などを使い、ログイン系エンドポイントに制限を入れます。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">9
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> slowapi <span style="color:#f92672">import</span> Limiter
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> slowapi.util <span style="color:#f92672">import</span> get_remote_address
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>limiter <span style="color:#f92672">=</span> Limiter(key_func<span style="color:#f92672">=</span>get_remote_address)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@router.post</span>(<span style="color:#e6db74">&#34;/auth/login&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@limiter.limit</span>(<span style="color:#e6db74">&#34;5/minute&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">login</span>(<span style="color:#f92672">...</span>):
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">...</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>さらに次を組み合わせると強化できます。</p>
<ul>
<li>失敗回数に応じた遅延（progressive delay）</li>
<li>CAPTCHA（必要時のみ）</li>
<li>異常IP/ASN の遮断</li>
</ul>
<h2 id="9-よくある実装ミス">9. よくある実装ミス</h2>
<h3 id="ミスa-署名検証はしているが-audiss-未検証">ミスA: 署名検証はしているが <code>aud/iss</code> 未検証</h3>
<p>結果:</p>
<ul>
<li>他システム向けトークンを誤受理</li>
</ul>
<p>対処:</p>
<ul>
<li>issuer/audience を厳格検証</li>
<li>想定外クレームは拒否</li>
</ul>
<h3 id="ミスb-refresh-token-の使い回しを検知しない">ミスB: refresh token の使い回しを検知しない</h3>
<p>結果:</p>
<ul>
<li>漏えい時に長期間乗っ取られる</li>
</ul>
<p>対処:</p>
<ul>
<li>ローテーション時に旧トークン失効</li>
<li>再利用検知時はセッション全失効</li>
</ul>
<h3 id="ミスc-認可チェックが一部エンドポイントで抜ける">ミスC: 認可チェックが一部エンドポイントで抜ける</h3>
<p>結果:</p>
<ul>
<li>水平権限昇格</li>
</ul>
<p>対処:</p>
<ul>
<li>dependency ベースで強制</li>
<li>重要ルートにセキュリティテスト追加</li>
</ul>
<h2 id="10-テスト戦略必須">10. テスト戦略（必須）</h2>
<p>認証はユニットテストだけでなく、統合テストで権限境界を確認します。</p>
<ul>
<li>有効トークン/期限切れ/改ざんトークン</li>
<li>role ごとのアクセス可否</li>
<li>tenant 越境アクセス拒否</li>
<li>refresh token 再利用検知</li>
</ul>
<p>pytest では fixture で role 別トークンを用意し、回帰を防ぎます。</p>
<h2 id="11-障害時-runbook最低限">11. 障害時 runbook（最低限）</h2>
<p>インシデント時に迷わないよう、次を文書化しておきます。</p>
<ol>
<li>鍵漏えい疑い時の全トークン失効手順</li>
<li>認証基盤障害時のフェイル動作（許可しすぎを防ぐ）</li>
<li>監査ログの検索手順</li>
<li>関係者通知テンプレート</li>
</ol>
<p>特に「認証サーバーが落ちたとき、API をどうするか」は事前に決めておく必要があります。</p>
<h2 id="12-導入チェックリスト">12. 導入チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> access token は短寿命（&lt;=15分）</li>
<li><input disabled="" type="checkbox"> refresh token は DB 管理 + ローテーション</li>
<li><input disabled="" type="checkbox"> 鍵ローテーション手順がある</li>
<li><input disabled="" type="checkbox"> ルート単位で認可 dependency が明示されている</li>
<li><input disabled="" type="checkbox"> tenant 境界を DB レイヤーで強制している</li>
<li><input disabled="" type="checkbox"> 監査ログ（認証/認可/重要操作）を構造化保存</li>
<li><input disabled="" type="checkbox"> レート制限と異常検知がある</li>
<li><input disabled="" type="checkbox"> 権限境界の統合テストがある</li>
</ul>
<p>FastAPI の認証・認可は、フレームワーク機能だけでは守り切れません。<strong>トークン寿命設計、鍵運用、境界強制、監査、テスト、runbook</strong>まで含めて初めて、本番で信頼できるセキュリティ基盤になります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>FastAPI</category>
      <category>Security</category>
      <category>JWT</category>
      <category>OAuth2</category>
      <category>Python</category>
    </item>
    <item>
      <title>GitHub Actions高速化実践：Matrix戦略・依存キャッシュ・失敗切り分けの設計ガイド</title>
      <link>https://www.ai2core.com/posts/2026-03-04-github-actions-matrix-cache-strategy/</link>
      <pubDate>Wed, 04 Mar 2026 09:05:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-04-github-actions-matrix-cache-strategy/</guid>
      <description>GitHub Actionsの実行時間と失敗率を同時に改善するためのmatrix設計、キャッシュ戦略、並列最適化、トラブルシューティング手順を具体例付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="github-actions高速化実践matrix戦略依存キャッシュ失敗切り分けの設計ガイド">GitHub Actions高速化実践：Matrix戦略・依存キャッシュ・失敗切り分けの設計ガイド</h1>
<p>GitHub Actions は便利ですが、プロジェクトが成長すると「遅い」「不安定」「原因が分かりにくい」という三重苦になりがちです。特に monorepo や複数ランタイム対応（Node/Python/Go など）では、ワークフローの設計次第で CI 時間が 2〜3 倍変わります。</p>
<p>本記事では、<strong>実行時間を短くしながら失敗時の調査コストも下げる</strong>ために、matrix 設計・キャッシュ設計・障害時の確認順序を具体的に整理します。</p>
<h2 id="1-まず何を並列化するかを決める">1. まず「何を並列化するか」を決める</h2>
<p>Actions の高速化は、いきなりキャッシュ最適化から入るより、先にジョブ分解を決める方が効きます。原則は次の通りです。</p>
<ul>
<li>並列化すべき: 独立テスト（OS/バージョン別、サービス別）</li>
<li>直列にすべき: デプロイ、DB マイグレーション、本番反映</li>
<li>依存を分ける: lint/typecheck/test/build を一つに詰め込まない</li>
</ul>
<p>悪い例は、1ジョブに全部詰め込み、失敗時に最初から再実行するパターンです。良い設計では「lint は通るが test だけ失敗」のように切り分けできます。</p>
<h2 id="2-matrix-を作るときの実践ルール">2. matrix を作るときの実践ルール</h2>
<p>matrix は便利ですが、組み合わせ爆発で逆に遅くなることがあります。例えば <code>os x runtime x db</code> をすべて直積にすると、不要なジョブが大量発生します。そこで <code>include/exclude</code> を活用します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">strategy</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">fail-fast</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">matrix</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">os</span>: [<span style="color:#ae81ff">ubuntu-latest, macos-latest]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">node</span>: [<span style="color:#ae81ff">20</span>, <span style="color:#ae81ff">22</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">include</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">os</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">node</span>: <span style="color:#ae81ff">22</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">coverage</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">exclude</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">os</span>: <span style="color:#ae81ff">macos-latest</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">node</span>: <span style="color:#ae81ff">20</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>ポイントは次です。</p>
<ol>
<li><strong>基準環境を1つ決める</strong>（例: ubuntu + latest）</li>
<li>カバレッジ計測や重い E2E は基準環境だけで実施</li>
<li>互換性確認は軽量テスト中心にする</li>
</ol>
<p>この設計にすると、品質を落とさずに全体時間を短縮できます。</p>
<h2 id="3-キャッシュは鍵設計が9割">3. キャッシュは「鍵設計」が9割</h2>
<p><code>actions/cache</code> や <code>setup-node</code> / <code>setup-python</code> のキャッシュを入れても、キー設計が甘いとヒット率が低く、逆に復元時間だけ増えます。</p>
<p>Node.js の例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/setup-node@v4</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">node-version</span>: <span style="color:#ae81ff">${{ matrix.node }}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cache</span>: <span style="color:#e6db74">&#39;npm&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cache-dependency-path</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      package-lock.json
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      packages/*/package-lock.json</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Python (pip) の例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/setup-python@v5</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">python-version</span>: <span style="color:#e6db74">&#39;3.12&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cache</span>: <span style="color:#e6db74">&#39;pip&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cache-dependency-path</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      requirements.txt
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      requirements-dev.txt</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>実務で効くコツ:</p>
<ul>
<li>lockfile をキーに含める（依存変化に追従）</li>
<li>OS・ランタイムバージョンをキーに含める</li>
<li>monorepo は対象サブディレクトリ単位でキー分割</li>
<li>restore-keys を入れすぎない（古いキャッシュ復元で不整合）</li>
</ul>
<h2 id="4-concurrency-で古い実行を止める">4. concurrency で「古い実行を止める」</h2>
<p>PR に連続 push されると、古い CI が残り続けてランナー枯渇を起こします。<code>concurrency</code> を入れて、最新コミットだけ走らせる構成にします。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">concurrency</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">group</span>: <span style="color:#ae81ff">ci-${{ github.workflow }}-${{ github.ref }}</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">cancel-in-progress</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>これだけで無駄実行を大きく減らせます。特にレビュー中に細かい修正を重ねるチームほど効果が高いです。</p>
<h2 id="5-paths-filter-で不要ジョブを起動しない">5. paths-filter で不要ジョブを起動しない</h2>
<p>ドキュメント更新だけなのに全テストが走る、という状態はよくあります。<code>dorny/paths-filter</code> で変更範囲に応じてジョブを分岐します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">dorny/paths-filter@v3</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">id</span>: <span style="color:#ae81ff">changes</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">filters</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      backend:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        - &#39;backend/**&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      frontend:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        - &#39;frontend/**&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 例: backend が変わった時だけ実行</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">if</span>: <span style="color:#ae81ff">steps.changes.outputs.backend == &#39;true&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>これにより、実行時間だけでなくランナーコストも下げられます。</p>
<h2 id="6-失敗時の調査を速くするログ設計">6. 失敗時の調査を速くするログ設計</h2>
<p>CI が遅い組織は、だいたい「失敗調査も遅い」です。改善するには、次の3点を標準化します。</p>
<ul>
<li>失敗したジョブで artifact（ログ、スクリーンショット、coverage）を必ず保存</li>
<li>重要ステップに <code>::group::</code> を付けてログを畳む</li>
<li>flaky テスト検出用に rerun 情報を残す</li>
</ul>
<p>artifact 例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Upload test reports</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">if</span>: <span style="color:#ae81ff">always()</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/upload-artifact@v4</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">test-report-${{ matrix.os }}-${{ matrix.node }}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      reports/
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      coverage/</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>if: always()</code> を忘れると、失敗時ほど証跡が残らないので注意です。</p>
<h2 id="7-self-hosted-runner-を使う場合の注意点">7. self-hosted runner を使う場合の注意点</h2>
<p>高速化目的で self-hosted runner を導入する場合、運用事故が増えやすい領域です。</p>
<ul>
<li>毎回クリーンワークスペース化（残骸で再現不能バグ）</li>
<li>シークレットを runner に永続化しない</li>
<li>パッチ適用・再起動の定期メンテをスケジュール化</li>
<li>runner ラベルを用途別に分離（deploy と test を混在させない）</li>
</ul>
<p>また、デプロイ権限を持つ runner と、PR 由来コードを実行する runner は分離するのが基本です。</p>
<h2 id="8-実際の改善ステップ2週間">8. 実際の改善ステップ（2週間）</h2>
<h3 id="day-1-2-現状計測">Day 1-2: 現状計測</h3>
<ul>
<li>平均実行時間、p95 実行時間、失敗率を取得</li>
<li>一番遅いジョブ上位3つを特定</li>
</ul>
<h3 id="day-3-5-ジョブ分割と-matrix-整理">Day 3-5: ジョブ分割と matrix 整理</h3>
<ul>
<li>lint/typecheck/test/build を分離</li>
<li>matrix の組み合わせを include/exclude で整理</li>
</ul>
<h3 id="day-6-8-キャッシュ最適化">Day 6-8: キャッシュ最適化</h3>
<ul>
<li>lockfile ベースキーへ統一</li>
<li>キャッシュヒット率を可視化</li>
</ul>
<h3 id="day-9-10-無駄実行削減">Day 9-10: 無駄実行削減</h3>
<ul>
<li>concurrency + cancel-in-progress</li>
<li>paths-filter で対象限定</li>
</ul>
<h3 id="day-11-14-運用ルール化">Day 11-14: 運用ルール化</h3>
<ul>
<li>失敗時 artifact を全ジョブ標準化</li>
<li>flaky テスト記録のテンプレート化</li>
</ul>
<p>この手順で進めると、速度改善だけでなく再発防止まで一気に整います。</p>
<h2 id="9-よくある失敗パターン">9. よくある失敗パターン</h2>
<h3 id="パターンa-キャッシュを入れたのに遅い">パターンA: キャッシュを入れたのに遅い</h3>
<p>原因:</p>
<ul>
<li>キーが細かすぎて毎回ミスヒット</li>
<li>圧縮/復元コストが大きいディレクトリを丸ごとキャッシュ</li>
</ul>
<p>対処:</p>
<ul>
<li>依存に限定してキャッシュ</li>
<li>キーに lockfile ハッシュを利用</li>
</ul>
<h3 id="パターンb-matrix-失敗がノイズ化">パターンB: matrix 失敗がノイズ化</h3>
<p>原因:</p>
<ul>
<li><code>fail-fast: true</code> で他環境の情報が取れない</li>
<li>ログ命名が統一されず比較困難</li>
</ul>
<p>対処:</p>
<ul>
<li><code>fail-fast: false</code></li>
<li>artifact 命名規則を統一</li>
</ul>
<h3 id="パターンc-pr-の待ち時間が長い">パターンC: PR の待ち時間が長い</h3>
<p>原因:</p>
<ul>
<li>古いコミットの CI が走り続ける</li>
<li>変更範囲に関係ないジョブが常時起動</li>
</ul>
<p>対処:</p>
<ul>
<li>concurrency で古い実行を停止</li>
<li>paths-filter 導入</li>
</ul>
<h2 id="10-運用チェックリスト">10. 運用チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> ジョブは責務別に分離されている</li>
<li><input disabled="" type="checkbox"> matrix は必要最小限に絞られている</li>
<li><input disabled="" type="checkbox"> 依存キャッシュのキーに lockfile が含まれる</li>
<li><input disabled="" type="checkbox"> concurrency で古い実行をキャンセルしている</li>
<li><input disabled="" type="checkbox"> 変更範囲に応じたジョブ起動制御がある</li>
<li><input disabled="" type="checkbox"> 失敗時 artifact が必ず残る</li>
</ul>
<p>GitHub Actions は「機能を使う」だけでは速くなりません。<strong>実行単位の設計、キャッシュ鍵設計、無駄実行抑制、証跡設計</strong>をセットで行うと、初めて安定した CI 基盤になります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>GitHub Actions</category>
      <category>CI</category>
      <category>DevOps</category>
      <category>Node.js</category>
      <category>Python</category>
    </item>
    <item>
      <title>Python asyncioバックプレッシャー設計：落ちない非同期バッチを作る実装パターン</title>
      <link>https://www.ai2core.com/posts/2026-03-01-python-asyncio-backpressure-design/</link>
      <pubDate>Sun, 01 Mar 2026 09:20:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-01-python-asyncio-backpressure-design/</guid>
      <description>asyncioシステムが高負荷時に破綻しないためのバックプレッシャー設計を、キュー制御、同時実行数、タイムアウト、再試行戦略まで具体例付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="python-asyncioバックプレッシャー設計落ちない非同期バッチを作る実装パターン">Python asyncioバックプレッシャー設計：落ちない非同期バッチを作る実装パターン</h1>
<p><code>asyncio</code> は速く作れる一方で、負荷が上がった瞬間に崩壊する設計を作りやすいという側面があります。特に「処理待ちが無限に積み上がる」「外部API遅延で全体が詰まる」「リトライ嵐でさらに遅くなる」は典型的です。</p>
<p>本記事では、非同期ワーカーを本番運用する前提で、<strong>バックプレッシャーを実装に落とす方法</strong>を解説します。単なる概念ではなく、すぐ使えるコード断片を中心に進めます。</p>
<h2 id="1-なぜバックプレッシャーが必要か">1. なぜバックプレッシャーが必要か</h2>
<p>バックプレッシャーは「これ以上は受けない」仕組みです。これがない設計は、ピーク時に次の順で壊れます。</p>
<ol>
<li>入力が処理速度を超える</li>
<li>キューが無限増加してメモリ圧迫</li>
<li>GC増加でスループット低下</li>
<li>タイムアウト増加→リトライ増加</li>
<li>システム全体が雪崩れる</li>
</ol>
<p>つまり、受けすぎないことは性能ではなく可用性の話です。</p>
<h2 id="2-基本設計3つの制限を必ず入れる">2. 基本設計：3つの制限を必ず入れる</h2>
<h3 id="2-1-キュー上限bounded-queue">2-1. キュー上限（bounded queue）</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> asyncio
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>QUEUE_MAX <span style="color:#f92672">=</span> <span style="color:#ae81ff">1000</span>
</span></span><span style="display:flex;"><span>queue: asyncio<span style="color:#f92672">.</span>Queue[dict] <span style="color:#f92672">=</span> asyncio<span style="color:#f92672">.</span>Queue(maxsize<span style="color:#f92672">=</span>QUEUE_MAX)
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>maxsize</code> なしは原則禁止です。業務要件で「捨てられない」場合でも、無限キューより「受け付け停止 + 明示エラー」のほうが復旧可能です。</p>
<h3 id="2-2-同時実行数上限semaphore">2-2. 同時実行数上限（semaphore）</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>CONCURRENCY <span style="color:#f92672">=</span> <span style="color:#ae81ff">20</span>
</span></span><span style="display:flex;"><span>semaphore <span style="color:#f92672">=</span> asyncio<span style="color:#f92672">.</span>Semaphore(CONCURRENCY)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">guarded_call</span>(fn, <span style="color:#f92672">*</span>args, <span style="color:#f92672">**</span>kwargs):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">with</span> semaphore:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">await</span> fn(<span style="color:#f92672">*</span>args, <span style="color:#f92672">**</span>kwargs)
</span></span></code></pre></td></tr></table>
</div>
</div><p>CPU でも I/O でも、同時実行数に上限を持たせると遅延の尾が短くなります。</p>
<h3 id="2-3-タイムアウトtimeout-budget">2-3. タイムアウト（timeout budget）</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> asyncio
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">with_timeout</span>(coro, timeout_sec: float):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">await</span> asyncio<span style="color:#f92672">.</span>wait_for(coro, timeout<span style="color:#f92672">=</span>timeout_sec)
</span></span></code></pre></td></tr></table>
</div>
</div><p>タイムアウトは「短すぎるか長すぎるか」ではなく、<strong>上流SLOから逆算</strong>します。例えば API 全体予算が 1.5 秒なら、外部API呼び出しを 600ms に固定し、残りをローカル処理に残す、という考え方です。</p>
<h2 id="3-実践ワーカーパターンそのまま使える">3. 実践ワーカーパターン（そのまま使える）</h2>
<p>以下は、キュー + 複数ワーカー + 再試行 + DLQ（Dead Letter Queue）を備えた最小構成です。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">35
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">36
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">37
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">38
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">39
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">40
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">41
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">42
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">43
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">44
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> asyncio
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> dataclasses <span style="color:#f92672">import</span> dataclass
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Any
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@dataclass</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Task</span>:
</span></span><span style="display:flex;"><span>    payload: dict[str, Any]
</span></span><span style="display:flex;"><span>    retry: int <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>MAX_RETRY <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>QUEUE_MAX <span style="color:#f92672">=</span> <span style="color:#ae81ff">1000</span>
</span></span><span style="display:flex;"><span>WORKERS <span style="color:#f92672">=</span> <span style="color:#ae81ff">16</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>main_queue: asyncio<span style="color:#f92672">.</span>Queue[Task] <span style="color:#f92672">=</span> asyncio<span style="color:#f92672">.</span>Queue(maxsize<span style="color:#f92672">=</span>QUEUE_MAX)
</span></span><span style="display:flex;"><span>dlq: asyncio<span style="color:#f92672">.</span>Queue[Task] <span style="color:#f92672">=</span> asyncio<span style="color:#f92672">.</span>Queue()
</span></span><span style="display:flex;"><span>sem <span style="color:#f92672">=</span> asyncio<span style="color:#f92672">.</span>Semaphore(<span style="color:#ae81ff">32</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">process_payload</span>(payload: dict[str, Any]) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># 外部API呼び出しやDB処理を想定</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> asyncio<span style="color:#f92672">.</span>sleep(<span style="color:#ae81ff">0.05</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">worker</span>(worker_id: int):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">while</span> <span style="color:#66d9ef">True</span>:
</span></span><span style="display:flex;"><span>        task <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> main_queue<span style="color:#f92672">.</span>get()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">with</span> sem:
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">await</span> asyncio<span style="color:#f92672">.</span>wait_for(process_payload(task<span style="color:#f92672">.</span>payload), timeout<span style="color:#f92672">=</span><span style="color:#ae81ff">1.0</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">except</span> <span style="color:#a6e22e">Exception</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> task<span style="color:#f92672">.</span>retry <span style="color:#f92672">&lt;</span> MAX_RETRY:
</span></span><span style="display:flex;"><span>                task<span style="color:#f92672">.</span>retry <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">await</span> asyncio<span style="color:#f92672">.</span>sleep(<span style="color:#ae81ff">0.1</span> <span style="color:#f92672">*</span> (<span style="color:#ae81ff">2</span> <span style="color:#f92672">**</span> task<span style="color:#f92672">.</span>retry))  <span style="color:#75715e"># 指数バックオフ</span>
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">await</span> main_queue<span style="color:#f92672">.</span>put(task)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">await</span> dlq<span style="color:#f92672">.</span>put(task)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">finally</span>:
</span></span><span style="display:flex;"><span>            main_queue<span style="color:#f92672">.</span>task_done()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">run</span>():
</span></span><span style="display:flex;"><span>    workers <span style="color:#f92672">=</span> [asyncio<span style="color:#f92672">.</span>create_task(worker(i)) <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(WORKERS)]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">await</span> main_queue<span style="color:#f92672">.</span>join()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">finally</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> w <span style="color:#f92672">in</span> workers:
</span></span><span style="display:flex;"><span>            w<span style="color:#f92672">.</span>cancel()
</span></span></code></pre></td></tr></table>
</div>
</div><p>重要なのは、再試行回数を有限にし、失敗タスクを DLQ に逃がす点です。無限リトライは障害時に自爆装置になります。</p>
<h2 id="4-入力側での受けすぎ防止">4. 入力側での「受けすぎ防止」</h2>
<p>ワーカーが健全でも、入口が無制限なら負けます。API であれば次の制御を入れます。</p>
<ul>
<li>受け付けキュー残量が閾値超えなら 429 を返す</li>
<li>tenant 単位でレート制限を分離</li>
<li>優先度キュー（重要ジョブを優先）</li>
</ul>
<h3 id="4-1-fastapiでの簡易例">4-1. FastAPIでの簡易例</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> fastapi <span style="color:#f92672">import</span> FastAPI, HTTPException
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>app <span style="color:#f92672">=</span> FastAPI()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@app.post</span>(<span style="color:#e6db74">&#34;/enqueue&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">enqueue</span>(item: dict):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> main_queue<span style="color:#f92672">.</span>qsize() <span style="color:#f92672">&gt;</span> int(QUEUE_MAX <span style="color:#f92672">*</span> <span style="color:#ae81ff">0.9</span>):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">raise</span> HTTPException(status_code<span style="color:#f92672">=</span><span style="color:#ae81ff">429</span>, detail<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;queue saturated&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> main_queue<span style="color:#f92672">.</span>put(Task(payload<span style="color:#f92672">=</span>item))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> {<span style="color:#e6db74">&#34;accepted&#34;</span>: <span style="color:#66d9ef">True</span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>「受け付けない」ことはユーザー体験を悪化させるように見えますが、全体停止よりはるかに安全です。429 を返す場合は <code>Retry-After</code> を併せて返し、クライアント再送間隔を制御します。</p>
<h2 id="5-観測性最低限見るべき4指標">5. 観測性：最低限見るべき4指標</h2>
<p>バックプレッシャーは入れただけでは不十分で、監視しないと調整できません。</p>
<ol>
<li><code>queue_depth</code>（キュー長）</li>
<li><code>processing_latency_p95</code></li>
<li><code>timeout_rate</code></li>
<li><code>dlq_rate</code></li>
</ol>
<h3 id="5-1-アラート基準の実例">5-1. アラート基準の実例</h3>
<ul>
<li>queue_depth &gt; 80% が 5分継続</li>
<li>timeout_rate &gt; 2% が 10分継続</li>
<li>dlq_rate が平常時の 3倍超</li>
</ul>
<p>アラートは「発火しやすい」より「行動が決まる」閾値を優先します。鳴るたびに対応が変わる設定は、運用疲労を生みます。</p>
<h2 id="6-負荷試験で必ず確認する項目">6. 負荷試験で必ず確認する項目</h2>
<p>本番前に k6 や Locust で負荷試験を行い、次を確認します。</p>
<ul>
<li>1.5x 負荷で queue_depth が収束するか</li>
<li>2.0x 負荷で 429 が適切に返るか</li>
<li>外部API遅延を注入しても DLQ へ逃がせるか</li>
<li>復帰後に backlog を解消できるか</li>
</ul>
<h3 id="6-1-テスト時の失敗パターン">6-1. テスト時の失敗パターン</h3>
<ul>
<li>セマフォ上限を増やしすぎ、下流DBが先に死ぬ</li>
<li>タイムアウト短縮だけで成功率が急落</li>
<li>リトライ間隔が短すぎて輻輳を悪化</li>
</ul>
<p>負荷試験は「最大スループット競争」ではなく「壊れ方の確認」です。</p>
<h2 id="7-運用で効く改善の順番">7. 運用で効く改善の順番</h2>
<p>改善は次の順番でやると効果が出やすいです。</p>
<ol>
<li>キュー上限と429制御を導入</li>
<li>同時実行数とタイムアウトを固定</li>
<li>DLQと再処理ジョブを作る</li>
<li>指標とアラートを整備</li>
<li>tenant別の公平制御（重い顧客の分離）</li>
</ol>
<p>最初から完璧なスケジューラは不要です。まず「壊れない最小構成」を作り、その上で最適化します。</p>
<h2 id="まとめ">まとめ</h2>
<p><code>asyncio</code> の本質的な課題は、速さではなく「過負荷時の振る舞い」です。バックプレッシャーは、ピーク時に品質を守るための安全装置であり、設計段階で必ず入れるべきです。</p>
<ul>
<li>無限キューを禁止する</li>
<li>同時実行数を固定する</li>
<li>タイムアウトと再試行を予算化する</li>
<li>失敗はDLQに逃がす</li>
</ul>
<p>この4点を実装すれば、負荷が来ても「遅くなるだけ」で済み、止まらないシステムに近づきます。安定運用を目指すなら、まずは今日中に <code>maxsize</code> と <code>Semaphore</code> をコードに入れるところから始めてください。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>Python</category>
      <category>asyncio</category>
      <category>Backend</category>
      <category>Performance</category>
    </item>
    <item>
      <title>FastAPI本番運用ハードニング完全ガイド：セキュリティ・性能・障害対応を実装で固める</title>
      <link>https://www.ai2core.com/posts/2026-02-28-fastapi-production-hardening-guide/</link>
      <pubDate>Sat, 28 Feb 2026 09:15:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-28-fastapi-production-hardening-guide/</guid>
      <description>FastAPIを本番運用する際に必要なセキュリティ、性能最適化、観測性、デプロイ手順を具体的に解説。</description>
      <content:encoded><![CDATA[<h1 id="fastapi本番運用ハードニング完全ガイドセキュリティ性能障害対応を実装で固める">FastAPI本番運用ハードニング完全ガイド：セキュリティ・性能・障害対応を実装で固める</h1>
<p>FastAPI は開発速度が高く、PoC から本番まで一気に進めやすいフレームワークです。しかし、早く作れることと安全に運用できることは別問題です。実際の障害は、コードの正しさよりも運用の隙から発生します。</p>
<p>本記事では、FastAPI を本番で安心して運用するためのハードニング手順を、実装可能な形でまとめます。対象は「すでにAPIが動いているが、運用強度を上げたい」チームです。</p>
<h2 id="1-入口防御tlsヘッダレート制限">1. 入口防御：TLS、ヘッダ、レート制限</h2>
<h3 id="tls終端とforwardedヘッダ">TLS終端とForwardedヘッダ</h3>
<p>ロードバランサ配下で動かす場合、<code>X-Forwarded-For</code> と <code>X-Forwarded-Proto</code> の扱いを明確にします。誤るとクライアントIPが取れず、監査も制限も機能しません。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> fastapi <span style="color:#f92672">import</span> FastAPI
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> starlette.middleware.trustedhost <span style="color:#f92672">import</span> TrustedHostMiddleware
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>app <span style="color:#f92672">=</span> FastAPI()
</span></span><span style="display:flex;"><span>app<span style="color:#f92672">.</span>add_middleware(TrustedHostMiddleware, allowed_hosts<span style="color:#f92672">=</span>[<span style="color:#e6db74">&#34;api.example.com&#34;</span>, <span style="color:#e6db74">&#34;*.example.com&#34;</span>])
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>allowed_hosts</code> をワイルドにしすぎると Host Header Injection の温床になります。</p>
<h3 id="セキュリティヘッダ">セキュリティヘッダ</h3>
<p>最低限次を返します。</p>
<ul>
<li><code>Strict-Transport-Security</code></li>
<li><code>X-Content-Type-Options: nosniff</code></li>
<li><code>X-Frame-Options: DENY</code></li>
<li><code>Referrer-Policy</code></li>
</ul>
<p>APIでも無関係ではありません。管理画面やドキュメントUIを守る意味があります。</p>
<h3 id="レート制限">レート制限</h3>
<p>ブルートフォースと突発負荷に備え、IPまたはAPIキー単位でレート制限を設定します。</p>
<ul>
<li>認証系: 5 req/min</li>
<li>通常API: 60 req/min</li>
<li>高負荷検索: 20 req/min</li>
</ul>
<p>Redis バックエンド方式にして、アプリ再起動でカウンタが失われないようにします。</p>
<h2 id="2-認証認可の落とし穴を塞ぐ">2. 認証・認可の落とし穴を塞ぐ</h2>
<h3 id="jwt検証の必須項目">JWT検証の必須項目</h3>
<p><code>exp</code> だけ見て通す実装は危険です。少なくとも次を検証します。</p>
<ul>
<li><code>iss</code>（発行者）</li>
<li><code>aud</code>（想定利用先）</li>
<li><code>nbf</code>（有効開始）</li>
<li><code>kid</code> に基づく鍵ローテーション</li>
</ul>
<h3 id="認可はエンドポイント単位ではなくリソース単位">認可は「エンドポイント単位」ではなく「リソース単位」</h3>
<p><code>/users/{id}</code> のアクセス時に、path パラメータの <code>id</code> とトークンの主体を照合しない事故は頻発します。FastAPI の dependency で統一的に実施します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">authorize_user_resource</span>(current_user, target_user_id: str):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> (current_user<span style="color:#f92672">.</span>is_admin <span style="color:#f92672">or</span> current_user<span style="color:#f92672">.</span>user_id <span style="color:#f92672">==</span> target_user_id):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">raise</span> HTTPException(status_code<span style="color:#f92672">=</span><span style="color:#ae81ff">403</span>, detail<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;forbidden&#34;</span>)
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="3-入出力の安全化pydanticだけでは不十分">3. 入出力の安全化：Pydanticだけでは不十分</h2>
<p>Pydantic は型安全に強いですが、ビジネス制約は別で実装が必要です。</p>
<ul>
<li>文字列長上限</li>
<li>許可文字セット</li>
<li>SQL/NoSQLインジェクションの危険文字</li>
<li>HTML/Markdown サニタイズ</li>
</ul>
<p>特に検索APIやエクスポートAPIは、クエリ文字列が巨大化しやすく DoS の入口になります。<code>max_length</code> を必ず定義してください。</p>
<h2 id="4-性能ハードニングワーカdbタイムアウト">4. 性能ハードニング：ワーカ・DB・タイムアウト</h2>
<h3 id="uvicorngunicorn-構成">Uvicorn/Gunicorn 構成</h3>
<p>CPUコア数に応じて worker を決めます。目安は <code>workers = 2 * core + 1</code> から開始し、実測で調整。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>gunicorn app.main:app -k uvicorn.workers.UvicornWorker --workers <span style="color:#ae81ff">5</span> --bind 0.0.0.0:8000 --timeout <span style="color:#ae81ff">30</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="db接続プール">DB接続プール</h3>
<p><code>asyncpg</code> や SQLAlchemy async engine のプール上限を設定しないと、ピーク時に接続飽和します。</p>
<ul>
<li>min: 5</li>
<li>max: 30（DB性能と相談）</li>
<li>pool timeout: 5s</li>
</ul>
<h3 id="タイムアウト戦略">タイムアウト戦略</h3>
<p>上流・下流の timeout を揃えないと、雪崩障害が発生します。</p>
<ul>
<li>外部API呼び出し: connect 1s / read 3s</li>
<li>DBクエリ: statement timeout 2s（重処理は別キュー）</li>
<li>API全体: 10s で fail fast</li>
</ul>
<h2 id="5-例外設計と障害時の挙動">5. 例外設計と障害時の挙動</h2>
<p>本番障害では「500が出ること」より「500の意味が不明」なことが問題です。エラーレスポンス形式を固定し、trace_id を必ず返します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;error&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;code&#34;</span>: <span style="color:#e6db74">&#34;INTERNAL_ERROR&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;message&#34;</span>: <span style="color:#e6db74">&#34;unexpected error&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;trace_id&#34;</span>: <span style="color:#e6db74">&#34;8f3d...&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>内部例外はそのまま返さず、ログ側に stack trace を記録。ユーザーには安全な文言のみ返却します。</p>
<h2 id="6-可観測性ログメトリクストレース">6. 可観測性：ログ・メトリクス・トレース</h2>
<h3 id="構造化ログ">構造化ログ</h3>
<p>JSON ログを標準化し、次を必須項目にします。</p>
<ul>
<li>timestamp</li>
<li>level</li>
<li>service</li>
<li>trace_id</li>
<li>user_id（可能なら）</li>
<li>endpoint</li>
<li>latency_ms</li>
</ul>
<h3 id="メトリクス">メトリクス</h3>
<p>最低限:</p>
<ul>
<li>RPS</li>
<li>エラー率（4xx/5xx）</li>
<li>P50/P95/P99 latency</li>
<li>DB遅延</li>
<li>外部API失敗率</li>
</ul>
<h3 id="トレース">トレース</h3>
<p>OpenTelemetry で endpoint → service → DB をつなぐと、障害切り分けが劇的に速くなります。</p>
<h2 id="7-デプロイ戦略壊さずに出す">7. デプロイ戦略：壊さずに出す</h2>
<p>推奨は Blue/Green か Canary。FastAPI 単体の問題より、周辺設定差異が事故の原因になります。</p>
<p>リリース前チェックリスト:</p>
<ol>
<li>DB migration の後方互換性</li>
<li>依存ライブラリ脆弱性スキャン</li>
<li>load test（代表3API）</li>
<li>rollback 手順の実行確認</li>
<li>feature flag で段階有効化</li>
</ol>
<h2 id="8-運用で効くインシデント訓練">8. 運用で効くインシデント訓練</h2>
<p>月1回、次の擬似障害を実施すると運用強度が上がります。</p>
<ul>
<li>DB遅延 3秒化</li>
<li>外部API 30% 失敗</li>
<li>メモリリーク発生</li>
<li>JWT鍵ローテーション失敗</li>
</ul>
<p>重要なのは、復旧時間だけでなく「誰が何を見て判断したか」を記録することです。Runbook の更新まで含めて初めて訓練が完結します。</p>
<h2 id="9-すぐ使える最小ハードニングチェック">9. すぐ使える最小ハードニングチェック</h2>
<ul>
<li><input disabled="" type="checkbox"> Host ヘッダ制限</li>
<li><input disabled="" type="checkbox"> JWT <code>iss/aud/exp/nbf</code> 検証</li>
<li><input disabled="" type="checkbox"> 全エンドポイントに認可 dependency</li>
<li><input disabled="" type="checkbox"> 外部API timeout/retry/circuit breaker</li>
<li><input disabled="" type="checkbox"> JSON 構造化ログ + trace_id</li>
<li><input disabled="" type="checkbox"> P95 latency 監視とアラート</li>
<li><input disabled="" type="checkbox"> rollback 手順が5分で実行可能</li>
</ul>
<p>この 7 項目が揃うだけで、障害時の被害規模は大きく下がります。</p>
<h2 id="まとめ">まとめ</h2>
<p>FastAPI は高速開発の武器ですが、本番運用では「早く作る」より「安全に壊れる」設計が重要です。入口防御、認証認可、性能制御、観測性、リリース運用をセットで整備すれば、チームは安心して機能開発に集中できます。</p>
<p>もし何から始めるか迷うなら、まずは trace_id 付きの構造化ログと timeout 統一から着手してください。最小の投資で、運用の見通しが一気に良くなります。</p>
<h2 id="10-セキュアな開発フローを維持するためのci設定">10. セキュアな開発フローを維持するためのCI設定</h2>
<p>本番ハードニングはコードだけでなく、CI フローで担保する必要があります。推奨するジョブは次の通りです。</p>
<ol>
<li>依存脆弱性スキャン（pip-audit / osv-scanner）</li>
<li>SAST（bandit など）</li>
<li>型チェック（mypy）</li>
<li>負荷テストのスモーク（k6）</li>
<li>OpenAPI 差分チェック（破壊的変更検出）</li>
</ol>
<p>特に OpenAPI 差分チェックは有効です。意図しないレスポンス変更を早期に検知でき、フロントエンド障害を防げます。</p>
<h2 id="11-バックアップと復旧を設計に含める">11. バックアップと復旧を設計に含める</h2>
<p>API 運用は「壊れない」ではなく「壊れても戻せる」が現実的です。最低限次を決めておきます。</p>
<ul>
<li>DB バックアップ頻度（例: 15分ごと増分、日次フル）</li>
<li>復旧目標（RTO/RPO）</li>
<li>復旧手順の担当と実行順</li>
</ul>
<p>復旧訓練をしていないバックアップは、存在しないのと同じです。四半期に一度は検証環境でリストア演習を行ってください。</p>
<h2 id="12-監査対応を見据えたログ保全">12. 監査対応を見据えたログ保全</h2>
<p>B2B API では監査要件が後から増えることが多いです。最初から次を満たす設計にしておくと後で困りません。</p>
<ul>
<li>監査ログとアプリログを分離</li>
<li>重要操作（権限変更、削除、課金操作）の証跡保存</li>
<li>ログ保持期間の明確化（例: 180日）</li>
<li>改ざん検知（WORM ストレージや署名）</li>
</ul>
<p>「誰が、いつ、何をしたか」を追えることは、障害解析だけでなく法務リスク低減にも直結します。</p>
<h2 id="最終まとめ">最終まとめ</h2>
<p>FastAPI の本番運用は、フレームワーク知識だけでは足りません。セキュリティ、性能、可観測性、復旧性を一体で設計することが重要です。チェックリスト化し、CI と運用手順へ落とし込むことで、安定した開発速度を維持できます。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>FastAPI</category>
      <category>Python</category>
      <category>Security</category>
      <category>SRE</category>
    </item>
    <item>
      <title>【速報】Google Gemini 3.1 Pro登場！新機能と使い方を徹底解説</title>
      <link>https://www.ai2core.com/posts/2026-02-21-gemini-3-1-pro/</link>
      <pubDate>Sat, 21 Feb 2026 10:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-21-gemini-3-1-pro/</guid>
      <description>Google最新のGemini 3.1 Proモデルの概要、新機能、APIの使い方を初心者エンジニア向けに分かりやすく解説します。</description>
      <content:encoded><![CDATA[<h2 id="はじめに">はじめに</h2>
<p>皆さん、こんにちは！テクノロジーの進化は本当に早いもので、Googleから最新のAIモデル「<strong>Gemini 3.1 Pro</strong>」が正式に発表されました。</p>
<p>このニュースは世界中のエンジニアを驚かせており、テック系コミュニティの聖地とも言える<strong>Hacker Newsでは、投稿からわずか数時間で882ポイントという異例の高評価を獲得</strong>しました。これほどまでに注目されているのは、単なるスペックアップを超えた「実用性の進化」があるからです。</p>
<p>「AIの進化が早すぎて追いつけない……」と感じている初心者エンジニアの方も多いかもしれませんが、安心してください。この記事では、Gemini 3.1 Proの何がすごいのか、そして今日からどうやって使いこなすのかを、どこよりも噛み砕いて解説します！</p>
<hr>
<h2 id="gemini-31-proとは">Gemini 3.1 Proとは？</h2>
<p>Gemini 3.1 Proは、Googleが開発した「Gemini」シリーズの最新鋭モデルです。従来のGemini 3の長所を引き継ぎつつ、特に「推論（考える力）」と「文脈の理解（記憶力）」が大幅に強化されています。</p>
<p>エンジニアにとってのGemini 3.1 Proは、例えるなら**「プロジェクトの全コードを記憶し、複雑なバグの修正案を即座に提案してくれる、超優秀な先輩エンジニア」**のような存在です。</p>
<h3 id="なぜproなのか">なぜ「Pro」なのか？</h3>
<p>Googleのモデルには「Ultra」「Pro」「Flash」などのラインナップがありますが、Proモデルは「性能」と「コスト・速度」のバランスが最も優れています。開発者がAPIを使ってアプリケーションに組み込む際、最も選ばれているのがこのProシリーズなのです。</p>
<hr>
<h2 id="ここがすごいgemini-31-proの3つの進化点">ここがすごい！Gemini 3.1 Proの3つの進化点</h2>
<p>従来のモデルと比べて、具体的にどこが変わったのでしょうか？注目すべき3つのポイントを挙げます。</p>
<h3 id="1-熟考型の推論プロセス">1. 「熟考型」の推論プロセス</h3>
<p>Gemini 3.1 Proには、人間が難しい問題を解くときにじっくり考えるような「System 2 Thinking」に近い仕組みが導入されました。これにより、これまでは間違えやすかった複雑な数学の問題や、高度な論理パズル、さらには大規模なシステムのデバッグにおいて、圧倒的に正確な回答を返せるようになっています。</p>
<h3 id="2-200万トークンの超長大コンテキスト">2. 200万トークンの超長大コンテキスト</h3>
<p>「トークン」とは、AIが一度に扱える情報の単位です。Gemini 3.1 Proは、最大で<strong>200万トークン</strong>という驚異的な量を一度に読み込むことができます。
これは、<strong>「厚辞苑数冊分のテキスト」や「数万行のソースコード全体」を丸ごとAIに読み込ませて、その内容について質問できる</strong>ことを意味します。「あの関数の定義、どこにあったっけ？」と探す手間は、もう過去のものになるかもしれません。</p>
<h3 id="3-ハルシネーションもっともらしい嘘の劇的な減少">3. ハルシネーション（もっともらしい嘘）の劇的な減少</h3>
<p>AIが自信満々に嘘をつく現象「ハルシネーション」が、Gemini 3.1 Proでは大幅に抑えられています。特に関数呼び出し（Function Calling）の正確性が増しており、外部ツールやデータベースと連携させた際の信頼性が向上しました。</p>
<hr>
<h2 id="実践pythonでgemini-31-proを動かしてみよう">【実践】PythonでGemini 3.1 Proを動かしてみよう</h2>
<p>それでは、実際にAPIを使ってGemini 3.1 Proを操作してみましょう。初心者の方でも、以下の3ステップで簡単に始められます。</p>
<h3 id="1-ライブラリの準備">1. ライブラリの準備</h3>
<p>ターミナルで以下のコマンドを実行し、最新のSDKをインストールします。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>pip install -U google-generativeai
</span></span></code></pre></td></tr></table>
</div>
</div>]]></content:encoded>
      <category>Generative AI</category>
      <category>Google Cloud</category>
      <category>Gemini 3.1 Pro</category>
      <category>Google</category>
      <category>LLM</category>
      <category>Python</category>
    </item>
    <item>
      <title>AWS Lambda SnapStartがPythonに対応！コールドスタート解消へ</title>
      <link>https://www.ai2core.com/posts/2026-02-20-aws-lambda-snapstart/</link>
      <pubDate>Fri, 20 Feb 2026 18:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-20-aws-lambda-snapstart/</guid>
      <description>Javaに続きPythonでも利用可能になったSnapStartの仕組みと効果。</description>
      <content:encoded><![CDATA[<h2 id="aws-lambda-snapstartがpythonに対応コールドスタート解消へ">AWS Lambda SnapStartがPythonに対応！コールドスタート解消へ</h2>
<h3 id="はじめに">はじめに</h3>
<p>AWS Lambdaを本番環境で利用している、あるいは利用を検討しているPythonデベロッパーの皆さん。「サーバーレスは便利だけど、あの&quot;最初の&quot;リクエストだけ遅いのが気になる…」と感じたことはありませんか？</p>
<p>API Gatewayと連携させたLambda関数が、しばらくアクセスがないとタイムアウトギリギリになったり、ユーザーに不快な待ち時間を与えてしまったり。この現象は「コールドスタート」と呼ばれ、多くのサーバーレス開発者を悩ませてきた根深い課題です。</p>
<p>これまで、この問題を解決するにはProvisioned Concurrency（プロビジョニングされた同時実行）という有料オプションを利用するのが一般的でしたが、コストとのトレードオフに頭を悩ませるケースも少なくありませんでした。</p>
<p>しかし、2023年末のre:Invent 2023で、ついにこの状況を打開する待望の機能がPythonランタイムにもたらされました。それが<strong>AWS Lambda SnapStart</strong>です。</p>
<p>これまでJavaランタイムでのみ利用可能だったこの機能がPythonに対応したことで、私たちのサーバーレスアプリケーション開発は新たなステージに進むことになります。本記事では、プロの技術ブロガーとして、Lambda SnapStart for Pythonの仕組みから具体的な使い方、そして現場で活かすための実践的なTipsまで、徹底的に深掘りしていきます。この記事を読み終える頃には、あなたもSnapStartを使いこなし、コールドスタートの悩みから解放されているはずです。</p>
<h3 id="なぜlambda-snapstartが重要なのか---コールドスタート問題の再確認">なぜLambda SnapStartが重要なのか？ - コールドスタート問題の再確認</h3>
<p>SnapStartの詳細に入る前に、なぜこの機能がこれほどまでに待望されていたのか、その背景にある「コールドスタート問題」を改めて整理しましょう。</p>
<h4 id="lambdaの実行モデルとライフサイクル">Lambdaの実行モデルとライフサイクル</h4>
<p>AWS Lambdaは、リクエストに応じてコンテナ（実行環境）を起動し、コードを実行するアーキテクチャです。この実行環境は、常に起動しているわけではありません。一定時間リクエストがないと、AWSはコスト効率化のために実行環境を破棄します。</p>
<p>次にリクエストが来たとき、Lambdaは以下のステップを踏んで応答します。</p>
<ol>
<li><strong>実行環境の確保</strong>: 新しい実行環境（マイクロVM）をプロビジョニングします。</li>
<li><strong>コードのダウンロード</strong>: S3などから関数のコード（デプロイパッケージ）をダウンロードし、展開します。</li>
<li><strong>ランタイムの初期化</strong>: Pythonのランタイム（インタプリタ）を起動します。</li>
<li><strong>関数の初期化 (Initフェーズ)</strong>: ハンドラ関数の<strong>外</strong>で定義されたグローバルなコードを実行します。ライブラリのインポート、DBコネクションプールの作成、機械学習モデルのロードなど、比較的時間のかかる処理がここに含まれます。</li>
<li><strong>関数の実行 (Invokeフェーズ)</strong>: 実際にハンドラ関数を実行し、リクエストを処理します。</li>
</ol>
<p>このうち、ステップ1から4までを含む最初の呼び出しを<strong>コールドスタート</strong>と呼びます。一度起動した実行環境は再利用されるため、2回目以降の呼び出し（<strong>ウォームスタート</strong>）ではステップ5のInvokeフェーズのみが実行され、非常に高速に応答できます。</p>
<p><img alt="Lambda Lifecycle" loading="lazy" src="https-://mermaid.live/edit#pako:eNqNVMtqwzAQ_Jd85Si1Bw77AocuQ6GUQunRw2YpYcuSXYhtSZXkv-dsEmk3Pe7uzs7s6q1qYgGcwD8x1Qp7GvFhRz6d85sOa26bE_QJ2qA6B2L1vC3zCgqJzB80b8bXjWp6L0S1Wq8G8lXvJbK7lK2Oal8i6xJ34Bv2sB4M6nB3d2C2J06X1h8Iu5T7L_f8jY8f4L-30s0c_w-m2TqFjD2WfK2M3a1v97oOqJ-tWvKk_aW4_V0-264L5gOQYJ8Qj8mHj8q9b9g_rW9-Uj_uT-J35_qS_m71Dq35W6v-a22nE3T732QWl5Baqn0sQ8fR2b8v0-c9aGvH8p32V3-iS8HwA_6x4QvH164r3l-CqQY_0M1c60HwP8C8Y_iXh5vB8D7A6b3jG-L_x_s_0d_0HwKcJv9P-3yX_v3d-3_g-f4e9U1T_J9D_lH9zB3968"></p>
<pre tabindex="0"><code class="language-mermaid" data-lang="mermaid">graph TD
    subgraph &#34;Cold Start&#34;
        A[Request] --&gt; B(Allocate MicroVM)
        B --&gt; C(Download Code)
        C --&gt; D(Initialize Runtime)
        D --&gt; E(Run Init Code)
        E --&gt; F(Run Handler)
    end
    F --&gt; G[Response]

    subgraph &#34;Warm Start&#34;
        H[Subsequent Request] --&gt; I{Environment Ready?}
        I -- Yes --&gt; J(Run Handler)
        I -- No --&gt; B
        J --&gt; K[Response]
    end
</code></pre><h4 id="コールドスタートが引き起こす問題">コールドスタートが引き起こす問題</h4>
<p>コールドスタートによる遅延は、数十ミリ秒から、場合によっては10秒以上にも及ぶことがあります。特に以下のようなケースで顕著になります。</p>
<ul>
<li><strong>大規模なフレームワークの利用</strong>: DjangoやFlaskなど、多くの依存関係を持つフレームワークは初期化に時間がかかります。</li>
<li><strong>機械学習モデルのロード</strong>: 数百MBから数GBにもなるモデルファイルをロードする処理は非常に重たいです。</li>
<li><strong>多くのライブラリのインポート</strong>: <code>pandas</code>, <code>NumPy</code>, <code>scikit-learn</code> といった大規模なライブラリはインポートだけでも時間がかかります。</li>
<li><strong>VPC内での実行</strong>: VPC LambdaはENI（Elastic Network Interface）のアタッチが必要なため、追加の起動時間が発生します。</li>
</ul>
<p>この遅延は、特にユーザーとの対話が求められるWeb APIやチャットボットなどのアプリケーションにおいて、ユーザー体験を著しく損なう原因となります。</p>
<h4 id="既存の対策-provisioned-concurrency">既存の対策: Provisioned Concurrency</h4>
<p>この問題を解決するためにAWSが提供してきたのが<strong>Provisioned Concurrency</strong>です。これは、あらかじめ指定した数の実行環境を常にウォーム状態（Initフェーズ完了後）で待機させておく機能です。</p>
<p>これによりコールドスタートを完全に排除できますが、リクエストがないアイドル時間も実行環境を維持し続けるため、<strong>追加のコストが発生します</strong>。トラフィックの予測が難しく、スパイクが発生するようなユースケースでは、コスト効率が悪化するという課題がありました。</p>
<p>そこで登場したのが、<strong>追加コストなしでコールドスタートを劇的に改善する</strong> Lambda SnapStartなのです。</p>
<h3 id="lambda-snapstart-for-python-の仕組みと詳細解説">Lambda SnapStart for Python の仕組みと詳細解説</h3>
<p>Lambda SnapStartは、コールドスタート問題に対する革新的なアプローチです。一言で言えば、**「関数の初期化が完了した状態の実行環境全体を丸ごとスナップショット（凍結保存）し、リクエストが来た際にそのスナップショットから高速に復元する」**技術です。</p>
<p>PCのハイバネーション（休止状態）をイメージすると分かりやすいかもしれません。メモリの状態をディスクに保存しておき、次回起動時にその状態を読み込むことで、OSやアプリケーションの起動時間を大幅に短縮するのと同じ考え方です。</p>
<h4 id="snapstartのライフサイクル">SnapStartのライフサイクル</h4>
<p>SnapStartを有効にすると、Lambda関数のライフサイクルが以下のように変わります。</p>
<ol>
<li>
<p><strong>バージョンの発行 (Publish)</strong>: SnapStartはLambdaのバージョンに対して有効になります（<code>$LATEST</code>では利用できません）。新しいコードをデプロイし、バージョンを発行すると、SnapStartのプロセスが開始されます。</p>
</li>
<li>
<p><strong>初期化 (Init) とスナップショット作成 (Snapshot)</strong>: AWSはバックグラウンドで、通常のコールドスタートと同じように実行環境を起動し、Initフェーズ（ハンドラ外のコード）を実行します。<strong>Initフェーズが完了した直後</strong>、実行環境のメモリとディスクの状態全体を暗号化されたスナップショットとしてS3に保存します。このスナップショットが、今後の呼び出しの「原本」となります。</p>
</li>
<li>
<p><strong>復元 (Restore) と実行 (Invoke)</strong>: 実際にリクエストが来ると、Lambdaは新しい実行環境を起動する代わりに、保存しておいたスナップショットを読み込んで状態を<strong>復元</strong>します。この復元処理は、ゼロから初期化するよりも遥かに高速です。復元後、すぐにハンドラ関数（Invokeフェーズ）が実行されます。</p>
</li>
</ol>
<p>この流れを図で見てみましょう。</p>
<pre tabindex="0"><code class="language-mermaid" data-lang="mermaid">sequenceDiagram
    participant Developer
    participant AWS Lambda
    participant S3

    Developer-&gt;&gt;AWS Lambda: Deploy &amp; Publish New Version (SnapStart Enabled)
    
    box &#34;Background Process (at Publish Time)&#34;
        AWS Lambda-&gt;&gt;AWS Lambda: 1. Start MicroVM
        AWS Lambda-&gt;&gt;AWS Lambda: 2. Download Code
        AWS Lambda-&gt;&gt;AWS Lambda: 3. Initialize Runtime
        AWS Lambda-&gt;&gt;AWS Lambda: 4. Run Init Code (Global Scope)
        Note right of AWS Lambda: Heavy initialization, DB connection pools, etc.
        AWS Lambda-&gt;&gt;S3: 5. Take Encrypted Snapshot of Memory &amp; Disk
    end

    participant User
    User-&gt;&gt;AWS Lambda: Invoke Function (First Request)
    
    box &#34;Foreground Process (at Invoke Time)&#34;
        AWS Lambda-&gt;&gt;AWS Lambda: 1. Start MicroVM
        AWS Lambda-&gt;&gt;S3: 2. Restore State from Snapshot
        Note right of AWS Lambda: This is the &#34;Restore&#34; phase. Much faster than full Init.
        AWS Lambda-&gt;&gt;AWS Lambda: 3. Run Handler (Invoke Code)
    end
    AWS Lambda--&gt;&gt;User: Response
</code></pre><p>最大のポイントは、時間のかかる<strong>Initフェーズを、リクエストのクリティカルパスから完全に分離した</strong>点です。これにより、ユーザーが体感するレイテンシを劇的に削減できるのです。公式の発表では、<strong>最大90%の起動時間短縮</strong>が報告されています。</p>
<h4 id="snapstartの有効化方法">SnapStartの有効化方法</h4>
<p>SnapStartの有効化は驚くほど簡単です。</p>
<h5 id="aws-マネジメントコンソール">AWS マネジメントコンソール</h5>
<ol>
<li>Lambda関数の設定画面に移動します。</li>
<li>「設定」タブ -&gt; 「一般設定」 -&gt; 「編集」をクリックします。</li>
<li>「SnapStart」の項目で、「PublishedVersions」を選択します。</li>
<li>変更を保存します。</li>
</ol>
<p>これだけです。あとは、新しいバージョンを発行すれば、そのバージョンに対して自動的にSnapStartが有効になります。</p>
<h5 id="aws-sam-serverless-application-model">AWS SAM (Serverless Application Model)</h5>
<p><code>template.yaml</code> にプロパティを1行追加するだけです。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">AWSTemplateFormatVersion</span>: <span style="color:#e6db74">&#39;2010-09-09&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">Transform</span>: <span style="color:#ae81ff">AWS::Serverless-2016-10-31</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">Description</span>: <span style="color:#ae81ff">Sample SAM Template with SnapStart for Python</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">Resources</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">MySnapStartFunction</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">Type</span>: <span style="color:#ae81ff">AWS::Serverless::Function</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">Properties</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">FunctionName</span>: <span style="color:#ae81ff">my-snapstart-python-function</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">CodeUri</span>: <span style="color:#ae81ff">src/</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">Handler</span>: <span style="color:#ae81ff">app.lambda_handler</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">Runtime</span>: <span style="color:#ae81ff">python3.12</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">Architectures</span>: [ <span style="color:#ae81ff">x86_64 ]</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">AutoPublishAlias</span>: <span style="color:#ae81ff">live</span> <span style="color:#75715e"># バージョニングを有効化</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">SnapStart</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">ApplyOn</span>: <span style="color:#ae81ff">PublishedVersions</span> <span style="color:#75715e"># この行を追加</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h5 id="aws-cdk-cloud-development-kit">AWS CDK (Cloud Development Kit)</h5>
<p>CDK (Python) の場合も、<code>snap_start</code> プロパティを設定します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> aws_cdk <span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>    aws_lambda <span style="color:#66d9ef">as</span> _lambda,
</span></span><span style="display:flex;"><span>    Stack,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> constructs <span style="color:#f92672">import</span> Construct
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MyCdkStack</span>(Stack):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> __init__(self, scope: Construct, construct_id: str, <span style="color:#f92672">**</span>kwargs) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>        super()<span style="color:#f92672">.</span>__init__(scope, construct_id, <span style="color:#f92672">**</span>kwargs)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        _lambda<span style="color:#f92672">.</span>Function(
</span></span><span style="display:flex;"><span>            self, <span style="color:#e6db74">&#34;MySnapStartFunction&#34;</span>,
</span></span><span style="display:flex;"><span>            runtime<span style="color:#f92672">=</span>_lambda<span style="color:#f92672">.</span>Runtime<span style="color:#f92672">.</span>PYTHON_3_12,
</span></span><span style="display:flex;"><span>            handler<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;app.lambda_handler&#34;</span>,
</span></span><span style="display:flex;"><span>            code<span style="color:#f92672">=</span>_lambda<span style="color:#f92672">.</span>Code<span style="color:#f92672">.</span>from_asset(<span style="color:#e6db74">&#34;src&#34;</span>),
</span></span><span style="display:flex;"><span>            snap_start<span style="color:#f92672">=</span>_lambda<span style="color:#f92672">.</span>SnapStartConf<span style="color:#f92672">.</span>ON_PUBLISHED_VERSIONS, <span style="color:#75715e"># この行を追加</span>
</span></span><span style="display:flex;"><span>            current_version_options<span style="color:#f92672">=</span>_lambda<span style="color:#f92672">.</span>VersionOptions(
</span></span><span style="display:flex;"><span>                removal_policy<span style="color:#f92672">=</span>RemovalPolicy<span style="color:#f92672">.</span>RETAIN <span style="color:#75715e"># 本番ではバージョンを保持</span>
</span></span><span style="display:flex;"><span>            )
</span></span><span style="display:flex;"><span>        )
</span></span></code></pre></td></tr></table>
</div>
</div><p>どの方法でも、設定は非常にシンプルです。アプリケーションコードの変更は、基本的には必要ありません。</p>
<h3 id="メリットとデメリット注意点">メリットとデメリット（注意点）</h3>
<p>SnapStartは魔法のような機能ですが、その特性を理解し、正しく利用することが重要です。</p>
<h4 id="メリット">メリット</h4>
<ol>
<li><strong>劇的なコールドスタート改善</strong>: これが最大のメリットです。最大で10倍の起動高速化が期待でき、ユーザー体験を大きく向上させます。</li>
<li><strong>追加コストなし</strong>: SnapStartを有効にすること自体に追加料金はかかりません。通常のLambdaの料金モデル（リクエスト数と実行時間）のまま、パフォーマンスの恩恵を受けられます。</li>
<li><strong>簡単な導入</strong>: 前述の通り、設定を有効にするだけで利用を開始できます。既存のコードを大幅に書き換える必要はありません。</li>
</ol>
<h4 id="デメリットと利用上の注意点">デメリットと利用上の注意点</h4>
<p>SnapStartはスナップショットという技術に依存するため、いくつかの制約や注意すべき点が存在します。これらを理解しないまま使うと、予期せぬ挙動につながる可能性があります。</p>
<ol>
<li>
<p><strong>$LATESTでは利用不可</strong>: SnapStartは発行済みのバージョンに対してのみ機能します。開発中は<code>$LATEST</code>を使いがちですが、SnapStartのテストや本番運用では必ずバージョンを発行する必要があります。エイリアスを使ってバージョンを管理するプラクティスが推奨されます。</p>
</li>
<li>
<p><strong>初期化時のユニークネス（一意性）の欠如</strong>: Initフェーズで生成されたデータは、スナップショットに固定されます。そのため、そのスナップショットから復元された全ての実行環境は、<strong>全く同じ初期状態</strong>を持つことになります。</p>
<ul>
<li><strong>問題となる例</strong>: InitフェーズでUUIDや乱数を生成し、それを後続の処理でユニークなIDとして利用しようとすると、全ての実行環境で同じIDが使われてしまい、問題を引き起こします。</li>
<li><strong>対策</strong>: ユニークな値が必要な場合は、必ずInvokeフェーズ（ハンドラ関数内）で生成するようにしてください。</li>
</ul>
</li>
<li>
<p><strong>初期化時のネットワーク接続</strong>: スナップショット作成時（Initフェーズ）に確立されたネットワーク接続は、スナップショットに含まれません。復元後（Invokeフェーズ）にその接続を使おうとすると、既に切断されているためエラーになります。</p>
<ul>
<li><strong>問題となる例</strong>: グローバルスコープでデータベースへのTCPコネクションを確立し、それをハンドラ関数で使い回そうとするケース。</li>
<li><strong>対策</strong>: ネットワーク接続の確立は、ハンドラ関数内で行うか、後述するランタイムフックを利用して復元後に再確立する必要があります。</li>
</ul>
</li>
<li>
<p><strong>スナップショットの鮮度 (Freshness)</strong>: Initフェーズで外部から設定値やデータを取得した場合、そのデータはスナップショット作成時点のものに固定されます。</p>
<ul>
<li><strong>問題となる例</strong>: InitフェーズでAWS Systems Manager Parameter Storeから設定値を読み込む場合、バージョンを発行した後にParameter Storeの値を更新しても、古い設定値を使い続けてしまいます。</li>
<li><strong>対策</strong>: 呼び出しごとに最新である必要があるデータは、必ずInvokeフェーズで取得するようにしましょう。</li>
</ul>
</li>
<li>
<p><strong>スナップショット作成時間の増加</strong>: Init処理が重ければ重いほど、バージョン発行からスナップショットが利用可能になるまでの時間が長くなります。デプロイパイプラインに組み込む際は、この遅延を考慮する必要があります。</p>
</li>
</ol>
<h4 id="snapstart-vs-provisioned-concurrency">SnapStart vs. Provisioned Concurrency</h4>
<p>ここで、既存のコールドスタート対策であるProvisioned Concurrencyとの比較を整理しておきましょう。</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">項目</th>
          <th style="text-align: left">Lambda SnapStart</th>
          <th style="text-align: left">Provisioned Concurrency</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>コールドスタート</strong></td>
          <td style="text-align: left">ほぼ解消（ミリ秒単位の復元時間）</td>
          <td style="text-align: left">完全に解消（即時実行）</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>コスト</strong></td>
          <td style="text-align: left"><strong>無料</strong>（通常のLambda料金のみ）</td>
          <td style="text-align: left"><strong>有料</strong>（アイドル時も課金）</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>設定の容易さ</strong></td>
          <td style="text-align: left">非常に簡単（有効化のみ）</td>
          <td style="text-align: left">事前のキャパシティ計画が必要</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>スケーラビリティ</strong></td>
          <td style="text-align: left">通常のLambdaと同様に自動スケール</td>
          <td style="text-align: left">設定した同時実行数が上限</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>最適なユースケース</strong></td>
          <td style="text-align: left">断続的・予測不能なトラフィック</td>
          <td style="text-align: left">継続的・予測可能なトラフィック</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>デプロイ速度</strong></td>
          <td style="text-align: left">スナップショット作成の遅延あり</td>
          <td style="text-align: left">高速</td>
      </tr>
  </tbody>
</table>
<p><strong>結論として、多くのユースケースにおいて、SnapStartがコストパフォーマンスに優れた第一選択肢となります。</strong> 1ミリ秒の遅延も許されない非常に厳しい要件がある場合に限り、Provisioned Concurrencyを検討するという使い分けになるでしょう。</p>
<h3 id="現場で使える実践的なtips">現場で使える実践的なTips</h3>
<p>SnapStartの仕組みと注意点を理解した上で、さらに一歩進んで、現場で効果的に活用するためのTipsを紹介します。</p>
<h4 id="1-ランタイムフックを使いこなす-before_checkpoint--after_restore">1. ランタイムフックを使いこなす (<code>before_checkpoint</code> / <code>after_restore</code>)</h4>
<p>SnapStart for Pythonは、スナップショット作成前と復元後に特定の処理を差し込める「ランタイムフック」を提供しています。これは、SnapStartの注意点を克服し、より高度な制御を行うための非常に強力な機能です。</p>
<p>フックを登録するには、<code>sys</code>モジュールなどを汚染しない形で、特定のライブラリ（例えば<code>checkpoint_hooks</code>という名前）を作成し、その中にフック関数を定義します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">35
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">36
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">37
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">38
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">39
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">40
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">41
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">42
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># src/checkpoint_hooks.py</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> logging
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> time
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> os
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> psycopg2 <span style="color:#75715e"># 例としてPostgreSQLのドライバ</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>logger <span style="color:#f92672">=</span> logging<span style="color:#f92672">.</span>getLogger()
</span></span><span style="display:flex;"><span>logger<span style="color:#f92672">.</span>setLevel(logging<span style="color:#f92672">.</span>INFO)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># グローバル変数としてコネクションを保持</span>
</span></span><span style="display:flex;"><span>db_connection <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">get_db_connection</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># 実際にはSecrets Managerなどから認証情報を取得する</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> psycopg2<span style="color:#f92672">.</span>connect(
</span></span><span style="display:flex;"><span>        host<span style="color:#f92672">=</span>os<span style="color:#f92672">.</span>environ<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;DB_HOST&#34;</span>),
</span></span><span style="display:flex;"><span>        dbname<span style="color:#f92672">=</span>os<span style="color:#f92672">.</span>environ<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;DB_NAME&#34;</span>),
</span></span><span style="display:flex;"><span>        user<span style="color:#f92672">=</span>os<span style="color:#f92672">.</span>environ<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;DB_USER&#34;</span>),
</span></span><span style="display:flex;"><span>        password<span style="color:#f92672">=</span>os<span style="color:#f92672">.</span>environ<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#34;DB_PASSWORD&#34;</span>)
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># --- Runtime Hooks ---</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">before_checkpoint</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;スナップショット作成直前に呼ばれるフック&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    logger<span style="color:#f92672">.</span>info(<span style="color:#e6db74">&#34;Hook: before_checkpoint is called.&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">global</span> db_connection
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># もしInitフェーズでテスト用にコネクションを確立していた場合、</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># スナップショットに含めないようにここで閉じておく</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> db_connection <span style="color:#f92672">and</span> <span style="color:#f92672">not</span> db_connection<span style="color:#f92672">.</span>closed:
</span></span><span style="display:flex;"><span>        logger<span style="color:#f92672">.</span>info(<span style="color:#e6db74">&#34;Closing DB connection before checkpointing.&#34;</span>)
</span></span><span style="display:flex;"><span>        db_connection<span style="color:#f92672">.</span>close()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">after_restore</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;スナップショットからの復元直後に呼ばれるフック&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    logger<span style="color:#f92672">.</span>info(<span style="color:#e6db74">&#34;Hook: after_restore is called.&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">global</span> db_connection
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># 復元後に新しいDBコネクションを確立する</span>
</span></span><span style="display:flex;"><span>    logger<span style="color:#f92672">.</span>info(<span style="color:#e6db74">&#34;Re-establishing DB connection after restoring.&#34;</span>)
</span></span><span style="display:flex;"><span>    db_connection <span style="color:#f92672">=</span> get_db_connection()
</span></span><span style="display:flex;"><span>    logger<span style="color:#f92672">.</span>info(<span style="color:#e6db74">&#34;DB connection established successfully.&#34;</span>)
</span></span></code></pre></td></tr></table>
</div>
</div><p>そして、このフックをLambda関数に登録するために、環境変数 <code>AWS_LAMBDA_RUNTIME_HOOKS_PREPEND</code> を設定します。値は、フックを含むPythonモジュールのドット表記パスです。</p>
<p><code>template.yaml</code> の例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>      <span style="color:#f92672">Environment</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">Variables</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">AWS_LAMBDA_RUNTIME_HOOKS_PREPEND</span>: <span style="color:#ae81ff">checkpoint_hooks</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>この例では、<code>before_checkpoint</code>で安全のためにコネクションを閉じ、<code>after_restore</code>で新しいコネクションを確立しています。これにより、「初期化時のネットワーク接続」の問題をエレガントに解決できます。他にも、一時ファイルのクリーンアップや、一時的な認証情報の更新など、様々な用途が考えられます。</p>
<h4 id="2-初期化処理を積極的にinitフェーズに寄せる">2. 初期化処理を積極的にInitフェーズに寄せる</h4>
<p>SnapStartの恩恵を最大化するための基本戦略は、**「重たい初期化処理を可能な限りInitフェーズ（ハンドラ外のグローバルスコープ）に移動させる」**ことです。</p>
<p><strong>Bad Example (ハンドラ内で毎回初期化)</strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> json
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> logging
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>logger <span style="color:#f92672">=</span> logging<span style="color:#f92672">.</span>getLogger()
</span></span><span style="display:flex;"><span>logger<span style="color:#f92672">.</span>setLevel(logging<span style="color:#f92672">.</span>INFO)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ハンドラ内で重いライブラリをインポートしたり、設定を読み込んでいる</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">lambda_handler</span>(event, context):
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">import</span> pandas <span style="color:#66d9ef">as</span> pd <span style="color:#75715e"># 重いライブラリ</span>
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># 毎回設定を読み込む</span>
</span></span><span style="display:flex;"><span>    config <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;key&#34;</span>: <span style="color:#e6db74">&#34;value&#34;</span>} 
</span></span><span style="display:flex;"><span>    logger<span style="color:#f92672">.</span>info(<span style="color:#e6db74">&#34;Handler is invoked.&#34;</span>)
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;statusCode&#34;</span>: <span style="color:#ae81ff">200</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;body&#34;</span>: json<span style="color:#f92672">.</span>dumps(<span style="color:#e6db74">&#39;Hello from Lambda!&#39;</span>),
</span></span><span style="display:flex;"><span>    }
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>Good Example (Initフェーズで初期化)</strong></p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> json
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> logging
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> pandas <span style="color:#66d9ef">as</span> pd <span style="color:#75715e"># グローバルスコープでインポート</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>logger <span style="color:#f92672">=</span> logging<span style="color:#f92672">.</span>getLogger()
</span></span><span style="display:flex;"><span>logger<span style="color:#f92672">.</span>setLevel(logging<span style="color:#f92672">.</span>INFO)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># グローバルスコープで設定を読み込む</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># この処理はスナップショット作成時に1回だけ実行される</span>
</span></span><span style="display:flex;"><span>logger<span style="color:#f92672">.</span>info(<span style="color:#e6db74">&#34;Initializing function... Loading config and libraries.&#34;</span>)
</span></span><span style="display:flex;"><span>config <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;key&#34;</span>: <span style="color:#e6db74">&#34;value&#34;</span>} 
</span></span><span style="display:flex;"><span>logger<span style="color:#f92672">.</span>info(<span style="color:#e6db74">&#34;Initialization complete.&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">lambda_handler</span>(event, context):
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Initフェーズで準備したものを利用するだけ</span>
</span></span><span style="display:flex;"><span>    logger<span style="color:#f92672">.</span>info(<span style="color:#e6db74">&#34;Handler is invoked.&#34;</span>)
</span></span><span style="display:flex;"><span>    df <span style="color:#f92672">=</span> pd<span style="color:#f92672">.</span>DataFrame([<span style="color:#ae81ff">1</span>,<span style="color:#ae81ff">2</span>,<span style="color:#ae81ff">3</span>]) <span style="color:#75715e"># ライブラリをすぐに使える</span>
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;statusCode&#34;</span>: <span style="color:#ae81ff">200</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;body&#34;</span>: json<span style="color:#f92672">.</span>dumps(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;Hello from Lambda! Config is </span><span style="color:#e6db74">{</span>config<span style="color:#e6db74">}</span><span style="color:#e6db74">&#39;</span>),
</span></span><span style="display:flex;"><span>    }
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>Good Example</code> の場合、<code>pandas</code>のインポートや<code>config</code>の読み込みにかかる時間は、スナップショット作成時に吸収されます。ユーザーリクエスト時には、これらの処理は既に完了しているため、ハンドラは即座に実行を開始できます。</p>
<h4 id="3-cloudwatch-logsでパフォーマンスを確認する">3. CloudWatch Logsでパフォーマンスを確認する</h4>
<p>SnapStartが正しく機能しているか、どの程度の効果が出ているかを確認するには、CloudWatch Logsが役立ちます。SnapStartを有効にしたLambda関数では、ログに <code>Restore Duration</code> という新しいメトリクスが出力されるようになります。</p>
<pre tabindex="0"><code>REPORT RequestId: xxxxx-xxxx-xxxx-xxxx-xxxxxxxx
Duration: 15.32 ms   Billed Duration: 16 ms   Memory Size: 128 MB   Max Memory Used: 78 MB
Restore Duration: 35.81 ms  
</code></pre><ul>
<li><strong>Duration</strong>: ハンドラ関数の実行時間。</li>
<li><strong>Restore Duration</strong>: スナップショットから実行環境を復元するのにかかった時間。</li>
</ul>
<p><strong>コールドスタート時の合計起動時間（ユーザーが体感する遅延）は、およそ <code>Restore Duration + Duration</code></strong> となります。
一方、SnapStartが無効な場合のコールドスタートでは、ログに <code>Init Duration</code> が表示されます。</p>
<pre tabindex="0"><code>REPORT RequestId: yyyyy-yyyy-yyyy-yyyy-yyyyyyyy
Duration: 12.45 ms   Billed Duration: 13 ms   Memory Size: 128 MB   Max Memory Used: 79 MB
Init Duration: 452.18 ms 
</code></pre><p>この例では、SnapStartによって初期化時間が <code>452.18 ms</code> から <code>35.81 ms</code> に短縮されたことが分かります。このように、ログを比較することでSnapStartの効果を定量的に測定できます。</p>
<h4 id="4-aws-x-rayで詳細なトレースを行う">4. AWS X-Rayで詳細なトレースを行う</h4>
<p>より複雑なアプリケーションでは、AWS X-Rayを使ってパフォーマンスのボトルネックを可視化することが有効です。X-RayはSnapStartにも対応しており、トレース情報に復元フェーズ（<code>restore</code>）が含まれるようになります。これにより、アプリケーション全体のどこで時間がかかっているのかを、より詳細に分析できます。</p>
<h3 id="まとめ">まとめ</h3>
<p>本記事では、AWS Lambdaのコールドスタート問題に対する画期的な解決策である「SnapStart for Python」について、その仕組みから実践的な活用方法までを詳細に解説しました。</p>
<p><strong>重要なポイントを振り返りましょう:</strong></p>
<ul>
<li><strong>SnapStartは、Initフェーズ完了後の実行環境をスナップショットとして保存し、呼び出し時に高速に復元することで、コールドスタートを劇的に改善します。</strong></li>
<li><strong>追加コストは不要で、設定も非常に簡単です。</strong></li>
<li><strong>バージョン管理が必須であり、$LATESTでは利用できません。</strong></li>
<li><strong>初期化時の一意性、ネットワーク接続、データの鮮度には注意が必要ですが、ランタイムフック (<code>before_checkpoint</code>/<code>after_restore</code>) を活用することで、これらの課題に対応できます。</strong></li>
<li><strong>重たい初期化処理をInitフェーズに集約することが、SnapStartの効果を最大化する鍵です。</strong></li>
</ul>
<p>Lambda SnapStart for Pythonは、これまでパフォーマンスの観点からLambdaの採用をためらっていたような、よりレイテンシに敏感なアプリケーションへの道を開くゲームチェンジャーです。特に、API Gatewayと連携する同期的なAPI、インタラクティブなWebアプリケーション、データ処理パイプラインの初段など、多岐にわたるユースケースでその真価を発揮するでしょう。</p>
<p>コールドスタートという長年の課題に対する、これほど強力かつ低コストなソリューションは他にありません。あなたのPythonサーバーレスアプリケーションのパフォーマンスを、今日から次のレベルへと引き上げてみませんか？ぜひ、ご自身のプロジェクトでLambda SnapStartを試し、その驚異的な効果を体感してみてください。</p>
]]></content:encoded>
      <category>Cloud</category>
      <category>AWS</category>
      <category>Lambda</category>
      <category>Python</category>
      <category>Serverless</category>
    </item>
    <item>
      <title>Python 3.15の新機能：JITコンパイラ標準搭載へ</title>
      <link>https://www.ai2core.com/posts/2026-02-19-python-315/</link>
      <pubDate>Thu, 19 Feb 2026 18:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-19-python-315/</guid>
      <description>Python 3.15で導入されるJITコンパイラによるパフォーマンス向上の詳細。</description>
      <content:encoded><![CDATA[<h1 id="python-315の新機能jitコンパイラ標準搭載へ---待ち望んだパフォーマンス革命がついに始まる">Python 3.15の新機能：JITコンパイラ標準搭載へ - 待ち望んだパフォーマンス革命がついに始まる</h1>
<h2 id="はじめに">はじめに</h2>
<p>「Pythonは書きやすいけど、遅い」。これは、多くのエンジニアが一度は耳にしたことがある、あるいは実感したことがある言葉ではないでしょうか。Webアプリケーション開発からデータサイエンス、機械学習まで、Pythonはその圧倒的な生産性と豊富なエコシステムで世界中の開発者を魅了してきました。しかし、その一方で、パフォーマンスが要求される場面では、C/C++による拡張モジュールの作成や、Cython/Numbaといった特殊なツールの導入、あるいはGoやRustといった他の言語の採用を検討せざるを得ない状況がしばしばありました。</p>
<p>もし、あなたがこれまでに、</p>
<ul>
<li>計算量の多い処理がボトルネックとなり、ユーザー体験を損なっている</li>
<li>パフォーマンス向上のためにPython以外の言語知識を要求され、開発の複雑性が増している</li>
<li>高速化ライブラリを導入したものの、環境構築や互換性の問題に悩まされている</li>
</ul>
<p>といった課題に直面したことがあるなら、この記事はまさにあなたのためにあります。</p>
<p>長年の課題であったパフォーマンス問題に終止符を打つべく、Python開発チームは「Faster CPython」プロジェクトを推進してきました。そして、その集大成とも言える機能が、ついに <strong>Python 3.15</strong> に標準搭載される見込みです。それが、<strong>JIT (Just-In-Time) コンパイラ</strong>です。</p>
<p>この記事では、Python 3.15で導入されるJITコンパイラが、なぜPythonの歴史における「革命」とまで言えるのか、その仕組みから具体的な効果、そして我々開発者が享受できるメリットと注意点まで、詳細に解説していきます。Pythonの未来を大きく変えるこの新機能の全貌を、一緒に見ていきましょう。</p>
<h2 id="なぜjitコンパイラが今重要なのか---python高速化の歩み">なぜJITコンパイラが今、重要なのか？ - Python高速化の歩み</h2>
<p>CPython（標準のPython実装）にJITコンパイラが搭載されることの重要性を理解するためには、まずPythonがどのようにコードを実行しているのか、そしてこれまでどのような高速化の試みが行われてきたのかを知る必要があります。</p>
<h3 id="cpythonの実行モデルインタプリタの長所と短所">CPythonの実行モデル：インタプリタの長所と短所</h3>
<p>私たちが普段書いているPythonコード (<code>.py</code>ファイル) は、そのままではコンピュータが理解できません。CPythonは、以下のステップでコードを実行します。</p>
<ol>
<li><strong>コンパイル:</strong> Pythonのソースコードを、プラットフォームに依存しない中間表現である「バイトコード」に変換します。この結果は <code>.pyc</code> ファイルとしてキャッシュされることがあります。</li>
<li><strong>実行:</strong> Python仮想マシン (PVM) と呼ばれるプログラムが、このバイトコードを一行ずつ解釈し、対応するC言語の関数を実行していきます。</li>
</ol>
<pre tabindex="0"><code>+------------------+     (1) コンパイル     +-----------------+     (2) 実行     +----------------+
|  ソースコード    | -------------------&gt; |   バイトコード    | -------------&gt; | Python仮想マシン |
|   (hello.py)   |                      |   (hello.pyc)   |                |      (PVM)     |
+------------------+                      +-----------------+                +----------------+
                                                                                    |
                                                                                    | 実行
                                                                                    V
                                                                                [ 結果 ]
</code></pre><p>この「インタプリタ方式」は、動的型付け（変数の型を実行時に決定する）といったPythonの柔軟性を支える重要な仕組みです。しかし、これがパフォーマンスのボトルネックにもなっています。PVMはバイトコードを実行するたびに、変数の型をチェックし、どの処理を呼び出すかを判断する必要があります。この間接的な処理が、C++やRustのような事前に全てのコードを機械語にコンパイル（AOT: Ahead-Of-Timeコンパイル）する言語に比べて、大きなオーバーヘッドとなるのです。</p>
<h3 id="高速化への道faster-cpythonプロジェクトの軌跡">高速化への道：Faster CPythonプロジェクトの軌跡</h3>
<p>この課題を克服するため、コア開発チームは「Faster CPython」という長期的なプロジェクトを開始しました。その成果は、近年のPythonリリースに段階的に取り入れられています。</p>
<h4 id="step-1-適応的特化インタプリタ-python-311">Step 1: 適応的特化インタプリタ (Python 3.11)</h4>
<p>PEP 659で導入されたこの機能は、Python高速化の大きな第一歩でした。これは、コードを繰り返し実行する中で、「この変数はいつも整数だな」「この関数の引数は特定のクラスのインスタンスばかりだ」といったパターンを学習します。そして、そのパターンに特化（specialized）した、より高速なバイトコードにその場で置き換えるのです。</p>
<p>例えば、<code>a + b</code> という演算を行うバイトコードは、通常版では <code>a</code> と <code>b</code> の型を毎回チェックする必要があります。しかし、この処理がループ内で何度も呼ばれ、<code>a</code> も <code>b</code> も常に整数であることが分かると、PVMはこれを「整数同士の加算」に特化した専用の高速なバイトコードに差し替えます。これにより、型チェックのオーバーヘッドが大幅に削減されました。</p>
<h4 id="step-2-tier-2-オプティマイザ-python-313">Step 2: Tier 2 オプティマイザ (Python 3.13)</h4>
<p>適応的特化インタプリタが個々のバイトコード命令を最適化するのに対し、Python 3.13で導入されたTier 2オプティマイザは、より大きな視点で最適化を行います。</p>
<p>これは、非常に頻繁に実行されるコードのまとまり（トレース）を特定し、それをさらに高度な中間表現（IR: Intermediate Representation）に変換します。そして、このIRに対して、従来のコンパイラが行うようなより積極的な最適化（定数畳み込み、デッドコード削除など）を適用し、最適化されたバイトコードを再生成します。これは、JITコンパイラへの重要な布石となる技術でした。</p>
<h4 id="これまでのサードパーティ製jitとの違い">これまでのサードパーティ製JITとの違い</h4>
<p>もちろん、Pythonの世界でJITコンパイラはこれが初めてではありません。<strong>PyPy</strong> は、長年CPythonを大幅に上回るパフォーマンスを誇る代替実装として知られていますし、科学技術計算の分野では <strong>Numba</strong> がNumPyコードを驚異的に高速化してきました。</p>
<p>しかし、これらのツールには課題もありました。PyPyはC拡張モジュールの互換性に問題を抱えることがあり、Numbaは数値計算に特化しているため汎用的なアプリケーションには適用しにくい、といった制約がありました。</p>
<p>CPython本体にJITコンパイラが標準搭載されることの最大の意義は、<strong>エコシステム全体が、特別なツールや実装を意識することなく、その恩恵を享受できる</strong>点にあります。あなたが開発するWebアプリケーションも、同僚が書いたデータ分析スクリプトも、世界中の開発者が利用するライブラリも、すべてがPython 3.15にアップデートするだけで高速化される可能性があるのです。</p>
<h2 id="python-315のjitコンパイラの仕組みを徹底解説">Python 3.15のJITコンパイラの仕組みを徹底解説</h2>
<p>それでは、いよいよPython 3.15に搭載されるJITコンパイラの核心に迫りましょう。</p>
<h3 id="jit-just-in-time-コンパイルとは">JIT (Just-In-Time) コンパイルとは？</h3>
<p>JITコンパイルは、プログラムの実行時（Just-In-Time）に、頻繁に実行されるコード部分（<strong>ホットスポット</strong>）を特定し、その部分だけをCPUが直接実行できるネイティブの機械語にコンパイルする技術です。</p>
<pre tabindex="0"><code>                                      +------------------------------------+
                                      |         Python仮想マシン (PVM)     |
                                      +------------------------------------+
                                                        |
                                                        | バイトコードを実行
                                                        V
+-----------------------------+     +------------------------------------------------------+
| プロファイラがホットスポットを特定 | --&gt; |  頻繁に実行されるコードが見つかった！(例: forループ)  |
+-----------------------------+     +------------------------------------------------------+
                                                        |
                                                        |
                                                        V
                                      +------------------------------------+
                                      |          JITコンパイラ             |
                                      +------------------------------------+
                                                        |
                                                        | バイトコードを機械語にコンパイル
                                                        V
                                      +------------------------------------+
                                      |   最適化されたネイティブ機械語コード   |
                                      +------------------------------------+
                                                        |
                                                        | 次回以降、この部分は機械語で直接実行
                                                        V
+-----------------------------+
|             CPU             |
+-----------------------------+
</code></pre><p>これにより、インタプリタの柔軟性を保ちつつ、AOTコンパイラ言語に匹敵するパフォーマンスを特定の箇所で実現できます。</p>
<h3 id="python-315のアーキテクチャcopy-and-patch-jit">Python 3.15のアーキテクチャ：Copy-and-Patch JIT</h3>
<p>Python 3.15のJITは、Microsoftが主導して開発した <strong>&ldquo;Copy-and-Patch&rdquo;</strong> というアプローチを採用しています。これは、非常に独創的でCPythonの実装と親和性が高い手法です。</p>
<p>従来の多くのJITコンパイラは、バイトコードを独自のIRに変換し、複雑な最適化を施した上で機械語を生成していました。これは非常に強力ですが、実装が複雑で、インタプリタとの連携も難しくなります。</p>
<p>Copy-and-Patch JITは、発想を転換します。CPythonのインタプリタ自体がC言語で書かれており、各バイトコードを処理するCのコード（テンプレート）が存在します。Copy-and-Patch JITは、この<strong>既存のCコードをコンパイルして得られる機械語のテンプレートをメモリ上にコピー</strong>し、その中の特定の部分（例えば、変数のメモリアドレスや定数値など）を、実行時の情報に基づいて**直接書き換える（パッチを当てる）**のです。</p>
<p>これにより、型チェックのような汎用的な処理を飛ばし、「このメモリアドレスにある整数と、あのメモリアドレスにある整数を加算する」といった非常に具体的な機械語コードを、低コストで生成できます。</p>
<h3 id="多層的な実行モデル">多層的な実行モデル</h3>
<p>Python 3.15の実行モデルは、以下のような多層的な階層構造になります。コードは実行されるにつれて、より高速な層へと「昇格」していきます。</p>
<ul>
<li><strong>Tier 0 (インタプリタ):</strong> 通常のバイトコードインタプリタ。全てのコードはまずここで実行されます。</li>
<li><strong>Tier 1 (適応的特化):</strong> 実行回数が増えてきたコードは、型などに特化した高速なバイトコードに置き換えられます (Python 3.11〜)。</li>
<li><strong>Tier 2 (オプティマイザ):</strong> 非常に頻繁に実行されるコードのトレースは、さらに高度な最適化が施されたバイトコードに変換されます (Python 3.13〜)。</li>
<li><strong>Tier 3 (JITコンパイラ):</strong> Tier 2で最適化されたコードの中でも、特にホットな部分がネイティブの機械語にJITコンパイルされます (Python 3.15〜)。</li>
</ul>
<p>この段階的なアプローチにより、起動時間への影響を最小限に抑えつつ、本当にパフォーマンスが必要な箇所だけを効率的に高速化することが可能になります。</p>
<h3 id="コード例で見るパフォーマンス向上">コード例で見るパフォーマンス向上</h3>
<p>論より証拠です。JITコンパイラがどのようなコードで効果を発揮するのか、具体的なベンチマークを見てみましょう。
（注：以下の数値は説明のための仮定値です。実際のパフォーマンスは環境やコードによって変動します。）</p>
<h4 id="ケース1純粋な数値計算ループ">ケース1：純粋な数値計算ループ</h4>
<p>JITコンパイラが最も得意とする分野の一つが、型が安定したループ処理です。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> time
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">calculate_sum</span>(n):
</span></span><span style="display:flex;"><span>    s <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(n):
</span></span><span style="display:flex;"><span>        s <span style="color:#f92672">+=</span> i
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> s
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>N <span style="color:#f92672">=</span> <span style="color:#ae81ff">100_000_000</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># --- Python 3.14 (JITなし) での実行をシミュレート ---</span>
</span></span><span style="display:flex;"><span>start_time <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>perf_counter()
</span></span><span style="display:flex;"><span>calculate_sum(N)
</span></span><span style="display:flex;"><span>end_time <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>perf_counter()
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Python 3.14 (simulated): </span><span style="color:#e6db74">{</span>end_time <span style="color:#f92672">-</span> start_time<span style="color:#e6db74">:</span><span style="color:#e6db74">.4f</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> seconds&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># --- Python 3.15 (JITあり) での実行をシミュレート ---</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 最初の数回はウォームアップでJITコンパイルが走る</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> _ <span style="color:#f92672">in</span> range(<span style="color:#ae81ff">5</span>):
</span></span><span style="display:flex;"><span>    calculate_sum(<span style="color:#ae81ff">100</span>) 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>start_time <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>perf_counter()
</span></span><span style="display:flex;"><span>calculate_sum(N)
</span></span><span style="display:flex;"><span>end_time <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>perf_counter()
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Python 3.15 (simulated): </span><span style="color:#e6db74">{</span>end_time <span style="color:#f92672">-</span> start_time<span style="color:#e6db74">:</span><span style="color:#e6db74">.4f</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> seconds&#34;</span>)
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>想定される実行結果:</strong></p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Pythonバージョン</th>
          <th style="text-align: left">実行時間 (秒)</th>
          <th style="text-align: left">備考</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">Python 3.14</td>
          <td style="text-align: left"><code>2.8512</code></td>
          <td style="text-align: left">ループのたびに型チェック等のオーバーヘッドが発生</td>
      </tr>
      <tr>
          <td style="text-align: left">Python 3.15</td>
          <td style="text-align: left"><code>0.7589</code></td>
          <td style="text-align: left">JITコンパイル後、ループ全体が最適化された機械語で実行</td>
      </tr>
  </tbody>
</table>
<p>この例では、<code>s</code> と <code>i</code> が常に整数であることがループ開始後すぐに特定されます。JITコンパイラは、このループ全体を「整数の加算をN回繰り返す」という非常に効率的な機械語コードに変換するため、インタプリタによるオーバーヘッドがなくなり、劇的なパフォーマンス向上が期待できます。</p>
<h4 id="ケース2オブジェクトの属性アクセス">ケース2：オブジェクトの属性アクセス</h4>
<p>オブジェクトの属性アクセスも、Pythonでは比較的高コストな処理ですが、JITの恩恵を受けやすい分野です。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> time
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Point</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> __init__(self, x, y):
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>x <span style="color:#f92672">=</span> x
</span></span><span style="display:flex;"><span>        self<span style="color:#f92672">.</span>y <span style="color:#f92672">=</span> y
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">sum_point_coords</span>(points):
</span></span><span style="display:flex;"><span>    total <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> p <span style="color:#f92672">in</span> points:
</span></span><span style="display:flex;"><span>        total <span style="color:#f92672">+=</span> p<span style="color:#f92672">.</span>x
</span></span><span style="display:flex;"><span>        total <span style="color:#f92672">+=</span> p<span style="color:#f92672">.</span>y
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> total
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>points <span style="color:#f92672">=</span> [Point(i, i <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>) <span style="color:#66d9ef">for</span> i <span style="color:#f92672">in</span> range(<span style="color:#ae81ff">10_000_000</span>)]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># --- Python 3.14 (JITなし) での実行をシミュレート ---</span>
</span></span><span style="display:flex;"><span>start_time <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>perf_counter()
</span></span><span style="display:flex;"><span>sum_point_coords(points)
</span></span><span style="display:flex;"><span>end_time <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>perf_counter()
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Python 3.14 (simulated): </span><span style="color:#e6db74">{</span>end_time <span style="color:#f92672">-</span> start_time<span style="color:#e6db74">:</span><span style="color:#e6db74">.4f</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> seconds&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># --- Python 3.15 (JITあり) での実行をシミュレート ---</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ウォームアップ</span>
</span></span><span style="display:flex;"><span>sum_point_coords([Point(<span style="color:#ae81ff">1</span>,<span style="color:#ae81ff">2</span>), Point(<span style="color:#ae81ff">3</span>,<span style="color:#ae81ff">4</span>)])
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>start_time <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>perf_counter()
</span></span><span style="display:flex;"><span>sum_point_coords(points)
</span></span><span style="display:flex;"><span>end_time <span style="color:#f92672">=</span> time<span style="color:#f92672">.</span>perf_counter()
</span></span><span style="display:flex;"><span>print(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Python 3.15 (simulated): </span><span style="color:#e6db74">{</span>end_time <span style="color:#f92672">-</span> start_time<span style="color:#e6db74">:</span><span style="color:#e6db74">.4f</span><span style="color:#e6db74">}</span><span style="color:#e6db74"> seconds&#34;</span>)
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>想定される実行結果:</strong></p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Pythonバージョン</th>
          <th style="text-align: left">実行時間 (秒)</th>
          <th style="text-align: left">備考</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">Python 3.14</td>
          <td style="text-align: left"><code>3.5123</code></td>
          <td style="text-align: left"><code>p.x</code>のアクセスのたびに辞書検索に近い処理が走る</td>
      </tr>
      <tr>
          <td style="text-align: left">Python 3.15</td>
          <td style="text-align: left"><code>1.1045</code></td>
          <td style="text-align: left">オブジェクトのメモリレイアウトが固定と判断され、直接メモリアクセスする機械語に変換</td>
      </tr>
  </tbody>
</table>
<p>JITコンパイラは、ループ内の <code>p</code> が常に同じ <code>Point</code> クラスのインスタンスであること、そしてそのインスタンスのメモリレイアウト（<code>x</code> と <code>y</code> がメモリ上のどこにあるか）が変化しないことを学習します。これにより、高コストな属性検索処理を、特定のメモリオフセットを直接読み出すだけの非常に高速な機械語命令に置き換えることができます。</p>
<h2 id="メリットとデメリットあるいは知っておくべきこと">メリットとデメリット（あるいは知っておくべきこと）</h2>
<p>Python 3.15のJITコンパイラは魔法の杖ではありません。その特性を正しく理解し、メリットを最大限に活かすことが重要です。</p>
<h3 id="メリット">メリット</h3>
<ol>
<li><strong>劇的なパフォーマンス向上:</strong> なんといっても最大のメリットです。特にCPUバウンドな（計算中心の）処理では、数倍の高速化が期待でき、Pythonアプリケーションの応答性や処理能力を大きく向上させます。</li>
<li><strong>透過的な適用:</strong> 開発者は、既存のPythonコードをほとんど、あるいは全く変更することなく、この恩恵を受けられます。Pythonのバージョンを上げるだけで、あなたのアプリケーションが速くなるのです。</li>
<li><strong>C拡張への依存度低下:</strong> これまでパフォーマンスのためだけに作られていたC拡張モジュールの一部は、純粋なPythonで記述しても十分な速度が得られるようになるかもしれません。これにより、コードの可読性、保守性、ポータビリティが向上します。</li>
<li><strong>Pythonの適用領域の拡大:</strong> これまでパフォーマンスの観点からPythonが選択肢に入らなかった領域（例: 高性能なWebサーバーのコア部分、リアルタイム性が求められるシステムなど）でも、Pythonが有力な候補となる可能性があります。</li>
</ol>
<h3 id="デメリットと注意点">デメリットと注意点</h3>
<ol>
<li><strong>ウォームアップ時間:</strong> JITコンパイルは実行時に行われるため、効果を発揮するまでにはある程度の「ウォームアップ」期間が必要です。起動してすぐに終了するような短いスクリプトでは、コンパイルのオーバーヘッドにより、むしろわずかに遅くなる可能性もあります。Webサーバーのように長時間稼働するアプリケーションで真価を発揮します。</li>
<li><strong>メモリ消費量の増加:</strong> JITコンパイルによって生成された機械語コードを保持するために、追加のメモリが必要になります。メモリが極端に制限された環境では注意が必要かもしれません。</li>
<li><strong>予測不能なパフォーマンス（Deoptimization）:</strong> JITは、コードの振る舞いを予測して最適化を行います。例えば、「この変数は常に整数だ」と予測して整数の加算を行う機械語を生成した後に、突然浮動小数点数が渡されると、その予測は外れます。この場合、JITは生成した機械語を破棄し、安全なインタプリタ実行に処理を戻します。これを<strong>デ最適化 (Deoptimization)</strong> と呼び、一時的なパフォーマンスの低下を引き起こす可能性があります。</li>
<li><strong>すべてのコードが速くなるわけではない:</strong> JITが高速化するのはCPUを使った計算処理です。ファイルI/Oやネットワーク通信、データベースへのクエリなど、CPUが待機している時間が大半を占める<strong>I/Oバウンドな処理</strong>では、JITによる高速化の効果は限定的です。</li>
</ol>
<h2 id="現場で使える実践的なtips">現場で使える実践的なTips</h2>
<p>JITコンパイラの恩恵を最大限に引き出すために、私たち開発者が意識できることがいくつかあります。</p>
<h3 id="jitフレンドリーなコードを書く">JITフレンドリーなコードを書く</h3>
<p>JITコンパイラは賢いですが、その能力を最大限に引き出すためには、少しだけ手助けをしてあげると効果的です。</p>
<ul>
<li><strong>型の安定性を保つ:</strong> ループ内や頻繁に呼ばれる関数内で、変数の型がころころ変わるようなコードは避けましょう。JITは型が安定しているコードを最も効率的に最適化できます。
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># アンチパターン: JITが最適化しにくい</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">process_items</span>(items):
</span></span><span style="display:flex;"><span>    result <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> item <span style="color:#f92672">in</span> items:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> isinstance(item, int):
</span></span><span style="display:flex;"><span>            result <span style="color:#f92672">+=</span> item
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            result <span style="color:#f92672">+=</span> float(item) <span style="color:#75715e"># resultの型がintからfloatに変わる可能性がある</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> result
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 推奨パターン: 型が安定している</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">process_items_stable</span>(items):
</span></span><span style="display:flex;"><span>    result <span style="color:#f92672">=</span> <span style="color:#ae81ff">0.0</span> <span style="color:#75715e"># 最初からfloatで初期化</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> item <span style="color:#f92672">in</span> items:
</span></span><span style="display:flex;"><span>        result <span style="color:#f92672">+=</span> float(item)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> result
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li><strong>関数の粒度を意識する:</strong> JITは関数単位でコンパイルの判断をすることが多いです。非常に小さく分割されすぎた関数よりも、ホットスポットとなるループを含む、ある程度まとまった処理単位の関数の方が、JITの最適化対象になりやすい傾向があります。</li>
</ul>
<h3 id="プロファイリングの重要性推測するな計測せよ">プロファイリングの重要性：「推測するな、計測せよ」</h3>
<p>JITが導入されても、パフォーマンスチューニングの基本原則は変わりません。まずは <code>cProfile</code> や <code>py-spy</code> といったプロファイラを使って、アプリケーションのどこが本当にボトルネックになっているのかを正確に把握しましょう。その上で、JITがそのボトルネックを解消できているかを確認することが重要です。</p>
<p>ベンチマークを取る際は、JITのウォームアップ時間を考慮に入れることを忘れないでください。<code>timeit</code> モジュールを使う場合でも、数回空実行してから計測したり、複数回実行した結果の中央値や平均値を見るなどの工夫が必要です。</p>
<h3 id="既存の高速化ツールとの使い分け">既存の高速化ツールとの使い分け</h3>
<p>Python 3.15のJITは強力ですが、既存のツールが不要になるわけではありません。適材適所で使い分けることで、最高のパフォーマンスを達成できます。</p>
<ul>
<li><strong>Numba:</strong> NumPyを使った複雑な数値計算や科学技術計算の配列処理など、特定のドメインで非常に高度な最適化（SIMD命令の活用など）を行います。この分野では、CPythonの汎用JITよりも高いパフォーマンスを発揮する可能性があります。</li>
<li><strong>Cython:</strong> C/C++ライブラリとの高度な連携が必要な場合や、静的型付けによってパフォーマンスを極限まで追求したい場合には、依然として最も強力な選択肢です。Pythonのセマンティクスから少し逸脱してでも速度を求める場面で活躍します。</li>
<li><strong>PyPy:</strong> アプリケーション全体で長期間にわたる持続的な高性能を求めるなら、CPythonとは異なるアプローチで非常に成熟したJITを持つPyPyを検討する価値はあります。</li>
</ul>
<p><strong>使い分けの指針:</strong></p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">ツール</th>
          <th style="text-align: left">主なユースケース</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>Python 3.15 標準JIT</strong></td>
          <td style="text-align: left">Webフレームワーク、汎用アプリケーション、一般的なスクリプトなど、<strong>大半のPythonコードのベースライン向上</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Numba</strong></td>
          <td style="text-align: left">大規模な配列計算、科学技術シミュレーション、データ分析のコアロジック</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>Cython</strong></td>
          <td style="text-align: left">C/C++ライブラリのバインディング、低レベルなパフォーマンスクリティカルなモジュール開発</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>PyPy</strong></td>
          <td style="text-align: left">長時間稼働するサーバーアプリケーション全体を高速化したい場合（C拡張の互換性に注意）</td>
      </tr>
  </tbody>
</table>
<h2 id="まとめ">まとめ</h2>
<p>Python 3.15に標準搭載されるJITコンパイラは、単なる一つの新機能ではありません。それは、Pythonの長年の課題であった「パフォーマンス」という壁を打ち破り、この言語の可能性を新たな次元へと引き上げる、まさに<strong>歴史的な一歩</strong>です。</p>
<p>Python 3.11の適応的特化インタプリタから始まった高速化の旅は、Python 3.15のJITコンパイラ搭載によって、一つの大きな到達点を迎えます。これにより、私たちはPythonの持つ圧倒的な生産性を維持したまま、これまで以上に高速でスケーラブルなアプリケーションを構築できるようになります。</p>
<p>もちろん、JITは万能薬ではなく、その特性を理解し、適切に付き合っていく必要があります。しかし、標準機能として、エコシステム全体を底上げするこの技術のインパクトは計り知れません。</p>
<p>「Pythonは遅い」という言葉が過去のものとなる日も、そう遠くはないでしょう。Python 3.15のリリースを心待ちにしながら、来るべきパフォーマンス革命に備え、私たち開発者も知識をアップデートしていきましょう。Pythonの未来は、これまで以上に明るく、そして高速です。</p>
]]></content:encoded>
      <category>Programming</category>
      <category>Python</category>
      <category>Update</category>
      <category>Performance</category>
    </item>
  </channel>
</rss>
