<?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>Backend on AI2CORE - AI技術ブログ</title>
    <link>https://www.ai2core.com/tags/backend/</link>
    <description>Recent content in Backend on AI2CORE - AI技術ブログ</description>
    <generator>Hugo -- 0.146.4</generator>
    <language>ja</language>
    <lastBuildDate>Thu, 05 Mar 2026 09:12:00 +0900</lastBuildDate>
    <atom:link href="https://www.ai2core.com/tags/backend/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>PostgreSQL接続プール枯渇の実戦対処：再発防止までつなげる調査・改善プレイブック</title>
      <link>https://www.ai2core.com/posts/2026-03-05-postgresql-connection-pool-exhaustion-playbook/</link>
      <pubDate>Thu, 05 Mar 2026 09:12:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-05-postgresql-connection-pool-exhaustion-playbook/</guid>
      <description>本番で頻発するPostgreSQL接続枯渇を、発生時の初動から原因切り分け、設定改善、監視強化まで具体手順で解説。</description>
      <content:encoded><![CDATA[<h1 id="postgresql接続プール枯渇の実戦対処再発防止までつなげる調査改善プレイブック">PostgreSQL接続プール枯渇の実戦対処：再発防止までつなげる調査・改善プレイブック</h1>
<p>本番障害でよくあるのが、<code>too many clients already</code> や <code>remaining connection slots are reserved</code> です。アプリ側から見ると「急にDBに繋がらない」、ユーザー側から見ると「全機能が遅い・失敗する」という最悪の体験になります。</p>
<p>厄介なのは、接続枯渇が「DBサーバー性能不足」だけで起こるわけではない点です。リーク、タイムアウト設定、長時間トランザクション、プールサイズ不整合など、複数要因が重なって起きます。</p>
<p>この記事では、接続枯渇に対して <strong>発生時の初動 → 根本原因の特定 → 恒久対策</strong> の順で、手順を実務レベルでまとめます。</p>
<h2 id="1-まず初動サービス継続を優先する">1. まず初動：サービス継続を優先する</h2>
<p>障害対応では、完璧な原因究明より「止血」が先です。以下を順番に実施します。</p>
<ol>
<li>直近リリース有無を確認（機能フラグ含む）</li>
<li>アプリの接続数・待機数・エラー率を確認</li>
<li>DB側で <code>pg_stat_activity</code> を取得</li>
<li>長時間実行クエリを必要に応じて停止</li>
<li>一時的にアプリ Pod 数を制限して雪だるま増幅を止める</li>
</ol>
<p><code>pg_stat_activity</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></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>
</span></span><span style="display:flex;"><span>  pid,
</span></span><span style="display:flex;"><span>  usename,
</span></span><span style="display:flex;"><span>  application_name,
</span></span><span style="display:flex;"><span>  client_addr,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">state</span>,
</span></span><span style="display:flex;"><span>  wait_event_type,
</span></span><span style="display:flex;"><span>  wait_event,
</span></span><span style="display:flex;"><span>  now() <span style="color:#f92672">-</span> query_start <span style="color:#66d9ef">AS</span> query_duration,
</span></span><span style="display:flex;"><span>  now() <span style="color:#f92672">-</span> xact_start  <span style="color:#66d9ef">AS</span> xact_duration,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">left</span>(query, <span style="color:#ae81ff">120</span>) <span style="color:#66d9ef">AS</span> query_head
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> pg_stat_activity
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> datname <span style="color:#f92672">=</span> current_database()
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> xact_start NULLS <span style="color:#66d9ef">LAST</span>, query_start NULLS <span style="color:#66d9ef">LAST</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><p>ここで見るべきは、<code>state='idle in transaction'</code> と異常に長い <code>xact_duration</code> です。これがあるとコネクションを握ったまま解放されず、枯渇の引き金になります。</p>
<h2 id="2-典型原因を4パターンで切り分ける">2. 典型原因を4パターンで切り分ける</h2>
<h3 id="パターンa-アプリ接続プールサイズが過大">パターンA: アプリ接続プールサイズが過大</h3>
<p>よくあるのが、以下のような構成です。</p>
<ul>
<li>Pod 20個</li>
<li>各Podのプール max 20</li>
<li>理論最大接続 400</li>
<li>PostgreSQL <code>max_connections=300</code></li>
</ul>
<p>この時点で設計破綻です。さらに管理接続やメンテ接続を引くと余裕ゼロになります。</p>
<p><strong>対策:</strong></p>
<ul>
<li>プールサイズ設計は「全インスタンス合計」で管理</li>
<li><code>max_connections</code> の70〜80%以内に通常運用を収める</li>
<li>ピーク時はワーカ数/Pod数で制御</li>
</ul>
<h3 id="パターンb-コネクションリーク">パターンB: コネクションリーク</h3>
<p><code>finally</code> で close していない、ORMセッションの寿命が長い、例外時に返却されない、といった実装ミスです。</p>
<p>Python（SQLAlchemy）の悪い例:</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></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>session <span style="color:#f92672">=</span> SessionLocal()
</span></span><span style="display:flex;"><span>user <span style="color:#f92672">=</span> session<span style="color:#f92672">.</span>query(User)<span style="color:#f92672">.</span>filter(User<span style="color:#f92672">.</span>id <span style="color:#f92672">==</span> user_id)<span style="color:#f92672">.</span>first()
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 例外時にcloseされない可能性</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">return</span> user
</span></span></code></pre></td></tr></table>
</div>
</div><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></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> contextlib <span style="color:#f92672">import</span> contextmanager
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@contextmanager</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">get_session</span>():
</span></span><span style="display:flex;"><span>    session <span style="color:#f92672">=</span> SessionLocal()
</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> session
</span></span><span style="display:flex;"><span>        session<span style="color:#f92672">.</span>commit()
</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>        session<span style="color:#f92672">.</span>rollback()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">raise</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">finally</span>:
</span></span><span style="display:flex;"><span>        session<span style="color:#f92672">.</span>close()
</span></span></code></pre></td></tr></table>
</div>
</div><p>FastAPI なら dependency でセッションスコープを統一し、endpointごとに確実に返却させるのが安全です。</p>
<h3 id="パターンc-長時間トランザクション">パターンC: 長時間トランザクション</h3>
<p>バッチ処理で 1トランザクションに大量更新を詰め込むと、ロック競合と接続占有が同時発生します。</p>
<p><strong>対策:</strong></p>
<ul>
<li>バッチをチャンク分割（例: 500件単位）</li>
<li><code>statement_timeout</code>、<code>idle_in_transaction_session_timeout</code> を設定</li>
<li>長時間処理はキュー化して非同期実行</li>
</ul>
<h3 id="パターンd-プールとdbのタイムアウト不一致">パターンD: プールとDBのタイムアウト不一致</h3>
<p>アプリの接続再利用時間が長すぎると、DB側で切断済み接続を使って失敗し、リトライで更に接続圧を上げます。</p>
<p><strong>対策:</strong></p>
<ul>
<li>プールの <code>maxLifetime</code> を DB/NLB timeout より短く</li>
<li>接続取得待ち時間（acquire timeout）を短くし、早めに失敗させる</li>
<li>失敗時リトライは指数バックオフ + ジッター</li>
</ul>
<h2 id="3-具体的な設定例pgbouncer--postgresql">3. 具体的な設定例（PgBouncer + PostgreSQL）</h2>
<p>高トラフィック環境では、アプリ直結より PgBouncer を挟むのが安定します。</p>
<p><code>pgbouncer.ini</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></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-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[pgbouncer]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">listen_addr</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">0.0.0.0</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">listen_port</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">6432</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">auth_type</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">md5</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">pool_mode</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">transaction</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">max_client_conn</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">5000</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">default_pool_size</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">80</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">reserve_pool_size</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">20</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">server_reset_query</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">DISCARD ALL</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">server_idle_timeout</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">30</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>PostgreSQL 側の最小設定例:</p>
<pre tabindex="0"><code class="language-conf" data-lang="conf">max_connections = 300
shared_buffers = 4GB
idle_in_transaction_session_timeout = 60000
statement_timeout = 30000
log_min_duration_statement = 1000
</code></pre><p><code>pool_mode=transaction</code> は接続効率が高い一方、セッション依存機能（一時テーブルや session variable）の扱いに注意が必要です。導入前に該当クエリを洗い出してください。</p>
<h2 id="4-kubernetes運用での落とし穴">4. Kubernetes運用での落とし穴</h2>
<p>Kubernetes では、HPA がスケールアウトすると同時に接続数が急増しやすいです。</p>
<h3 id="41-設計式を最初に決める">4.1 設計式を最初に決める</h3>
<p><code>最大接続見積もり = maxPods × perPodPoolMax + 管理余白</code></p>
<p>例:</p>
<ul>
<li>maxPods=30</li>
<li>perPodPoolMax=8</li>
<li>管理余白=30</li>
<li>合計 270 → <code>max_connections=320</code> なら許容</li>
</ul>
<p>この計算を IaC へコメントで残しておくと、後任が壊しにくくなります。</p>
<h3 id="42-readiness-に-db接続必須チェックを入れすぎない">4.2 readiness に DB接続必須チェックを入れすぎない</h3>
<p>Pod起動時に全Podが同時にDBへ接続テストすると、再起動時にスパイクが起きます。readiness は軽量化し、重い初期化はバックグラウンドへ逃がすのが無難です。</p>
<h2 id="5-監視項目再発防止の最低ライン">5. 監視項目：再発防止の最低ライン</h2>
<p>以下を可視化し、閾値アラートを設定します。</p>
<ul>
<li><code>numbackends / max_connections</code></li>
<li>接続待ち時間（アプリメトリクス）</li>
<li><code>idle in transaction</code> セッション数</li>
<li>95/99パーセンタイルのクエリ時間</li>
<li>DBエラー率（connection refused, timeout）</li>
</ul>
<p>推奨アラート例:</p>
<ul>
<li><code>numbackends &gt; 85%</code> が 5分継続で Warning</li>
<li><code>numbackends &gt; 92%</code> が 2分継続で Critical</li>
<li><code>idle in transaction &gt; 10</code> が 3分継続で調査開始</li>
</ul>
<h2 id="6-障害後レビューポストモーテムで必ず決めること">6. 障害後レビュー（ポストモーテム）で必ず決めること</h2>
<p>「接続が足りませんでした」で終えると再発します。次の観点を明文化します。</p>
<ol>
<li>なぜ早期検知できなかったか</li>
<li>どの設定が設計値と乖離していたか</li>
<li>コード修正・設定修正・監視修正の担当と期限</li>
<li>次回同様インシデント時の runbook 更新点</li>
</ol>
<p>運用改善は「学びをコード化する」ことです。ドキュメントだけでなく、Terraform や Helm values に反映して初めて再発率が下がります。</p>
<h2 id="7-実務向けチェックリスト">7. 実務向けチェックリスト</h2>
<p>最後に、現場で使いやすいチェックリストを置いておきます。</p>
<ul>
<li><input disabled="" type="checkbox"> <code>max_connections</code> と総プール上限の関係を計算済み</li>
<li><input disabled="" type="checkbox"> 接続リーク防止（close/return保証）が実装されている</li>
<li><input disabled="" type="checkbox"> 長時間トランザクションに timeout が設定済み</li>
<li><input disabled="" type="checkbox"> PgBouncer の pool_mode が要件に適合している</li>
<li><input disabled="" type="checkbox"> <code>numbackends</code> と接続待ち時間のアラートがある</li>
<li><input disabled="" type="checkbox"> 障害時 runbook に SQL コマンドと判断基準が書かれている</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>PostgreSQL 接続枯渇は、単なるパラメータ不足ではなく、アプリ実装・スケーリング設計・監視不足が重なって起こる複合障害です。だからこそ、</p>
<ul>
<li>初動で止血する</li>
<li>パターン別に切り分ける</li>
<li>設定と実装を同時に直す</li>
<li>監視と runbook に落とし込む</li>
</ul>
<p>この流れを徹底するだけで、同じ障害の再発率は大きく下げられます。次に障害が起きたとき、慌てず順番に潰せる状態を今日作っておくのが最善です。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>PostgreSQL</category>
      <category>Performance</category>
      <category>SRE</category>
      <category>Troubleshooting</category>
      <category>Backend</category>
    </item>
    <item>
      <title>Redisキャッシュスタンピード対策ガイド：高負荷時にDBを守る設計と実装</title>
      <link>https://www.ai2core.com/posts/2026-03-02-redis-cache-stampede-mitigation-guide/</link>
      <pubDate>Mon, 02 Mar 2026 09:16:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-02-redis-cache-stampede-mitigation-guide/</guid>
      <description>Redisで発生するキャッシュスタンピードを防ぐための実践ガイド。singleflight、TTLジッター、非同期再生成を具体コード付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="redisキャッシュスタンピード対策ガイド高負荷時にdbを守る設計と実装">Redisキャッシュスタンピード対策ガイド：高負荷時にDBを守る設計と実装</h1>
<p>Redis を使っていても、ピークトラフィック時に DB が突然落ちることがあります。原因の多くはキャッシュスタンピードです。人気キーの TTL が同時に切れると、大量リクエストが一斉に DB へ流れ、接続プールが飽和します。</p>
<p>「Redis を入れたのに遅い」「ピーク時だけ 500 が増える」という現象は、このパターンで説明できることが非常に多いです。</p>
<p>本記事では、キャッシュスタンピードを実運用で防ぐために、<strong>設計原則・実装パターン・監視方法</strong>を順に解説します。</p>
<h2 id="1-キャッシュスタンピードとは何か">1. キャッシュスタンピードとは何か</h2>
<p>典型シナリオ:</p>
<ol>
<li>商品ランキング API が <code>ranking:daily</code> を Redis に 300 秒で保存</li>
<li>300 秒後、人気時間帯にキー期限切れ</li>
<li>同時に 1000 リクエストが miss</li>
<li>1000 回 DB 集計が走ってレイテンシ急増</li>
</ol>
<p>このとき Redis 自体は正常でも、背後の DB が壊れます。つまり、問題はキャッシュ障害ではなく「再生成の同時実行制御」です。</p>
<h2 id="2-防御の基本は三層構え">2. 防御の基本は三層構え</h2>
<p>スタンピード対策は単一施策では不十分です。次の三層を組み合わせると安定します。</p>
<ol>
<li><strong>同時再生成の抑制</strong>（singleflight / 分散ロック）</li>
<li><strong>期限切れの分散</strong>（TTL ジッター）</li>
<li><strong>期限切れ後の挙動制御</strong>（stale-while-revalidate）</li>
</ol>
<h2 id="3-パターン1-singleflight-で同時再生成を止める">3. パターン1: singleflight で同時再生成を止める</h2>
<p>同一キーの miss が同時発生しても、1 リクエストだけ再生成し、他は待つ設計です。</p>
<p>TypeScript 例:</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></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-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">inflight</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Map</span>&lt;<span style="color:#f92672">string</span>, <span style="color:#a6e22e">Promise</span>&lt;<span style="color:#f92672">string</span>&gt;&gt;();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getOrCompute</span>(<span style="color:#a6e22e">key</span>: <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">ttlSec</span>: <span style="color:#66d9ef">number</span>, <span style="color:#a6e22e">compute</span><span style="color:#f92672">:</span> () <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">Promise</span>&lt;<span style="color:#f92672">string</span>&gt;) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">cached</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">redis</span>.<span style="color:#66d9ef">get</span>(<span style="color:#a6e22e">key</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">cached</span>) <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">cached</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">inflight</span>.<span style="color:#a6e22e">has</span>(<span style="color:#a6e22e">key</span>)) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">p</span> <span style="color:#f92672">=</span> (<span style="color:#66d9ef">async</span> () <span style="color:#f92672">=&gt;</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">const</span> <span style="color:#a6e22e">value</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">compute</span>();
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">redis</span>.<span style="color:#66d9ef">set</span>(<span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">value</span>, { <span style="color:#a6e22e">EX</span>: <span style="color:#66d9ef">ttlSec</span> });
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">value</span>;
</span></span><span style="display:flex;"><span>      } <span style="color:#66d9ef">finally</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">inflight</span>.<span style="color:#66d9ef">delete</span>(<span style="color:#a6e22e">key</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:#a6e22e">inflight</span>.<span style="color:#66d9ef">set</span>(<span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">p</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 style="color:#66d9ef">await</span> <span style="color:#a6e22e">inflight</span>.<span style="color:#66d9ef">get</span>(<span style="color:#a6e22e">key</span>)<span style="color:#f92672">!</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>単一プロセスではこれで十分ですが、複数インスタンス構成では分散ロックも必要です。</p>
<h2 id="4-パターン2-分散ロックset-nx-ex">4. パターン2: 分散ロック（SET NX EX）</h2>
<p>複数 Pod で同時 miss が起きる場合、Redis ロックで再生成担当を 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></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-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">lockKey</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`lock:</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">key</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">lock</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">redis</span>.<span style="color:#66d9ef">set</span>(<span style="color:#a6e22e">lockKey</span>, <span style="color:#a6e22e">instanceId</span>, { <span style="color:#a6e22e">NX</span>: <span style="color:#66d9ef">true</span>, <span style="color:#a6e22e">EX</span>: <span style="color:#66d9ef">10</span> });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">lock</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// ロック獲得: 再生成担当
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">value</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">compute</span>();
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">redis</span>.<span style="color:#66d9ef">set</span>(<span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">value</span>, { <span style="color:#a6e22e">EX</span>: <span style="color:#66d9ef">ttlSec</span> });
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">redis</span>.<span style="color:#a6e22e">del</span>(<span style="color:#a6e22e">lockKey</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">value</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">// ロック未獲得: 少し待って再取得
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">await</span> <span style="color:#a6e22e">sleep</span>(<span style="color:#ae81ff">40</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">return</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">redis</span>.<span style="color:#66d9ef">get</span>(<span style="color:#a6e22e">key</span>);
</span></span></code></pre></td></tr></table>
</div>
</div><p>注意点は、ロック TTL が短すぎると再生成中に失効し、二重計算になることです。処理時間の p99 を見て余裕を持って設定してください。</p>
<h2 id="5-パターン3-ttl-ジッターで期限切れを分散">5. パターン3: TTL ジッターで期限切れを分散</h2>
<p>同系統キーが同時に切れると負荷波形が尖ります。TTL にランダム幅を持たせるだけでかなり改善します。</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-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">ttlWithJitter</span>(<span style="color:#a6e22e">baseSec</span>: <span style="color:#66d9ef">number</span>, <span style="color:#a6e22e">jitterSec</span>: <span style="color:#66d9ef">number</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">baseSec</span> <span style="color:#f92672">+</span> Math.<span style="color:#a6e22e">floor</span>(Math.<span style="color:#a6e22e">random</span>() <span style="color:#f92672">*</span> <span style="color:#a6e22e">jitterSec</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">await</span> <span style="color:#a6e22e">redis</span>.<span style="color:#66d9ef">set</span>(<span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">value</span>, { <span style="color:#a6e22e">EX</span>: <span style="color:#66d9ef">ttlWithJitter</span>(<span style="color:#ae81ff">300</span>, <span style="color:#ae81ff">90</span>) });
</span></span></code></pre></td></tr></table>
</div>
</div><p>300 秒固定より、300〜390 秒の分布にすると、同時失効が目に見えて減ります。</p>
<h2 id="6-パターン4-stale-while-revalidate">6. パターン4: stale-while-revalidate</h2>
<p>高可用性が重要な API では、期限切れ直後でも古い値を短時間返しつつ、裏で更新する手法が有効です。</p>
<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></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;value&#34;</span>: {<span style="color:#f92672">&#34;items&#34;</span>: [<span style="color:#960050;background-color:#1e0010">...</span>]},
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;hardExpireAt&#34;</span>: <span style="color:#ae81ff">1700000000</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;softExpireAt&#34;</span>: <span style="color:#ae81ff">1699999700</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><ul>
<li><code>now &lt; softExpireAt</code>: 新鮮データ</li>
<li><code>softExpireAt &lt;= now &lt; hardExpireAt</code>: 古い値を返しつつ非同期更新</li>
<li><code>hardExpireAt &lt;= now</code>: 同期再生成</li>
</ul>
<p>この方式はピーク帯の体感性能を維持しやすく、DB 保護にも強いです。</p>
<h2 id="7-失敗時フォールバックを設計する">7. 失敗時フォールバックを設計する</h2>
<p>キャッシュ再生成が失敗したときの動作を明確にしておくと、障害時の揺れが減ります。</p>
<p>推奨順序:</p>
<ol>
<li>stale データがあれば返す</li>
<li>stale もなければ軽量な代替レスポンス</li>
<li>最後に明示エラー（再試行可能）</li>
</ol>
<p>「毎回 DB にフォールバック」は危険です。障害時に DB をさらに追い込むため、回路遮断（circuit breaker）とセットで設計すべきです。</p>
<h2 id="8-監視で見るべき指標">8. 監視で見るべき指標</h2>
<p>対策の効果はメトリクスで評価します。</p>
<ul>
<li>cache hit ratio</li>
<li>key 単位の miss burst（短時間 miss 件数）</li>
<li>再生成処理の同時実行数</li>
<li>DB クエリ QPS と接続待ち時間</li>
</ul>
<p>Prometheus を使う場合、以下のようなカウンタを実装すると分析しやすいです。</p>
<ul>
<li><code>cache_requests_total{key_group, result=&quot;hit|miss|stale&quot;}</code></li>
<li><code>cache_rebuild_total{key_group, result=&quot;ok|error&quot;}</code></li>
<li><code>cache_lock_contention_total{key_group}</code></li>
</ul>
<h2 id="9-導入手順既存システム向け">9. 導入手順（既存システム向け）</h2>
<h3 id="step-1-熱いキーを特定">Step 1: 熱いキーを特定</h3>
<p>アクセス上位 20 キーを抽出し、まずそこだけ対策します。</p>
<h3 id="step-2-singleflight--ジッター導入">Step 2: singleflight + ジッター導入</h3>
<p>実装コストが低く、効果が高い組み合わせです。</p>
<h3 id="step-3-分散ロック導入">Step 3: 分散ロック導入</h3>
<p>複数インスタンス環境で必須。ロック競合率も計測する。</p>
<h3 id="step-4-stale-while-revalidate-追加">Step 4: stale-while-revalidate 追加</h3>
<p>高トラフィック API へ段階適用。UX と DB 安定性が両立しやすい。</p>
<h2 id="10-よくある失敗例">10. よくある失敗例</h2>
<h3 id="失敗1-ttl-を長くしてごまかす">失敗1: TTL を長くしてごまかす</h3>
<p>データ鮮度要件を満たせず、別の問題が出ます。期限延長は応急処置に留める。</p>
<h3 id="失敗2-分散ロックだけで安心する">失敗2: 分散ロックだけで安心する</h3>
<p>ロックが取れない側の待機戦略がないと、スパイクは残ります。待機・再取得・stale 応答まで設計が必要です。</p>
<h3 id="失敗3-miss-率しか見ない">失敗3: miss 率しか見ない</h3>
<p>miss が少なくても、特定キーへの burst が強ければ障害は起きます。キーグループ単位で観測してください。</p>
<h2 id="11-実運用チェックリスト">11. 実運用チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> 熱いキーを定義し、キーグループで計測している</li>
<li><input disabled="" type="checkbox"> singleflight もしくは同等の同時実行制御がある</li>
<li><input disabled="" type="checkbox"> TTL にジッターを導入している</li>
<li><input disabled="" type="checkbox"> 分散ロックの TTL が処理時間 p99 を上回る</li>
<li><input disabled="" type="checkbox"> stale-while-revalidate の返却条件が明確</li>
<li><input disabled="" type="checkbox"> 障害時フォールバック（回路遮断含む）がある</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>Redis キャッシュスタンピード対策は、キャッシュを置くことではなく「miss 後の世界を設計する」ことです。</p>
<ul>
<li>同時再生成を止める</li>
<li>失効タイミングを分散する</li>
<li>期限切れ直後の応答を制御する</li>
</ul>
<p>この3点を実装すれば、ピーク時の DB 崩壊リスクは大幅に下げられます。まずはアクセス上位キーから段階導入し、メトリクスで効果を確認してください。仕組みとして回り始めると、トラフィック増加に対する耐性が目に見えて改善します。</p>
<h2 id="12-キャッシュキー設計の実務ポイント">12. キャッシュキー設計の実務ポイント</h2>
<p>スタンピード対策では、アルゴリズム以前にキー設計が重要です。<code>user:123:timeline</code> のような粒度が粗すぎるキーは、人気ユーザーにアクセスが集中したとき一気にホットスポットになります。可能なら <code>page</code> や <code>segment</code> を分割し、巨大レスポンスを小さく分けると miss 時の再生成コストを抑えられます。</p>
<p>また、キーの命名規約をチームで統一しておくと観測しやすくなります。<code>service:domain:resource:variant</code> 形式で揃えれば、メトリクス集計時に <code>key_group</code> を自動分類しやすく、どの領域で burst が起きているかを短時間で判断できます。運用性を上げるキー設計は、性能改善と同じくらい価値があります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>Redis</category>
      <category>Caching</category>
      <category>Backend</category>
      <category>Performance</category>
    </item>
    <item>
      <title>PostgreSQLデッドロック調査プレイブック：再現・可視化・恒久対策までの実践手順</title>
      <link>https://www.ai2core.com/posts/2026-03-02-postgresql-deadlock-troubleshooting-playbook/</link>
      <pubDate>Mon, 02 Mar 2026 09:12:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-02-postgresql-deadlock-troubleshooting-playbook/</guid>
      <description>本番で発生するPostgreSQLデッドロックの調査と対処を、再現SQL・ログ設定・アプリ修正パターンまで具体例付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="postgresqlデッドロック調査プレイブック再現可視化恒久対策までの実践手順">PostgreSQLデッドロック調査プレイブック：再現・可視化・恒久対策までの実践手順</h1>
<p>本番運用で厄介なのは、エラーが「たまに」しか出ない障害です。PostgreSQL のデッドロックはその代表で、発生頻度は低くてもビジネス影響が大きいことが多いです。決済や在庫更新で発生すると、リトライが雪だるま式に増え、アプリ全体の遅延を引き起こします。</p>
<p>本記事では、デッドロック発生時に現場でそのまま使える手順を、<strong>初動対応・再現・恒久対策</strong>の順で整理します。</p>
<h2 id="1-まず理解すべき前提">1. まず理解すべき前提</h2>
<p>デッドロックは「どちらかが悪い」ではなく、<strong>ロック順序が循環したときに必ず起きる現象</strong>です。PostgreSQL は循環を検出すると、どちらか一方のトランザクションを強制中断します。</p>
<p>典型的な症状:</p>
<ul>
<li><code>ERROR: deadlock detected</code></li>
<li>API の一部がランダムに 500 を返す</li>
<li>リトライ実装により DB 負荷が上振れ</li>
</ul>
<p>ここで重要なのは、単純なタイムアウトと混同しないことです。タイムアウトは待ち時間超過、デッドロックは循環待ちです。対策が違います。</p>
<h2 id="2-初動でやること515分">2. 初動でやること（5〜15分）</h2>
<h3 id="2-1-エラーログの採取">2-1. エラーログの採取</h3>
<p>まず、DB 側ログに詳細を出す設定があるか確認します。</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-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SHOW</span> log_lock_waits;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SHOW</span> deadlock_timeout;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SHOW</span> log_min_error_statement;
</span></span></code></pre></td></tr></table>
</div>
</div><p>推奨設定（本番）:</p>
<pre tabindex="0"><code class="language-conf" data-lang="conf">log_lock_waits = on
deadlock_timeout = &#39;1s&#39;
log_min_error_statement = error
</code></pre><p><code>deadlock_timeout</code> を短めにすることで、待ちが長引いたケースの追跡がしやすくなります。</p>
<h3 id="2-2-現在のロック状況を確認">2-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><span 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></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>
</span></span><span style="display:flex;"><span>  a.pid,
</span></span><span style="display:flex;"><span>  a.usename,
</span></span><span style="display:flex;"><span>  a.application_name,
</span></span><span style="display:flex;"><span>  a.<span style="color:#66d9ef">state</span>,
</span></span><span style="display:flex;"><span>  a.query,
</span></span><span style="display:flex;"><span>  l.locktype,
</span></span><span style="display:flex;"><span>  l.<span style="color:#66d9ef">mode</span>,
</span></span><span style="display:flex;"><span>  l.<span style="color:#66d9ef">granted</span>,
</span></span><span style="display:flex;"><span>  a.query_start
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> pg_stat_activity a
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">JOIN</span> pg_locks l <span style="color:#66d9ef">ON</span> a.pid <span style="color:#f92672">=</span> l.pid
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> a.datname <span style="color:#f92672">=</span> current_database()
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> a.query_start;
</span></span></code></pre></td></tr></table>
</div>
</div><p>見るべき点は「長く生きているトランザクション」と「<code>granted = false</code> が連鎖している箇所」です。</p>
<h2 id="3-再現手順を作る原因特定の最短ルート">3. 再現手順を作る（原因特定の最短ルート）</h2>
<p>デッドロックは再現しないと直せません。以下のような単純ケースをまず作ります。</p>
<h3 id="セッション-a">セッション A</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></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">BEGIN</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">UPDATE</span> accounts <span style="color:#66d9ef">SET</span> balance <span style="color:#f92672">=</span> balance <span style="color:#f92672">-</span> <span style="color:#ae81ff">100</span> <span style="color:#66d9ef">WHERE</span> id <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- ここで待機
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">UPDATE</span> accounts <span style="color:#66d9ef">SET</span> balance <span style="color:#f92672">=</span> balance <span style="color:#f92672">+</span> <span style="color:#ae81ff">100</span> <span style="color:#66d9ef">WHERE</span> id <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">COMMIT</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="セッション-b">セッション B</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></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">BEGIN</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">UPDATE</span> accounts <span style="color:#66d9ef">SET</span> balance <span style="color:#f92672">=</span> balance <span style="color:#f92672">-</span> <span style="color:#ae81ff">50</span> <span style="color:#66d9ef">WHERE</span> id <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>;
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- ここで待機
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">UPDATE</span> accounts <span style="color:#66d9ef">SET</span> balance <span style="color:#f92672">=</span> balance <span style="color:#f92672">+</span> <span style="color:#ae81ff">50</span> <span style="color:#66d9ef">WHERE</span> id <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">COMMIT</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><p>更新順序が逆なので、容易に循環が起きます。</p>
<p>この再現が取れたら、アプリコード上でも「同じテーブルを複数行更新する順序」が一定かどうかを調べます。</p>
<h2 id="4-原因の8割は更新順序の不一致">4. 原因の8割は「更新順序の不一致」</h2>
<p>多くのプロダクトで見つかるのは次のパターンです。</p>
<ol>
<li>バッチ処理は <code>id ASC</code> で更新</li>
<li>API リクエストは受信順で更新</li>
<li>並行処理時に順序が逆転</li>
</ol>
<p>この場合、解決策は明確で、<strong>全経路でロック取得順序を統一</strong>します。</p>
<p>例（Node.js / TypeScript）:</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-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">ids</span> <span style="color:#f92672">=</span> [<span style="color:#a6e22e">fromAccountId</span>, <span style="color:#a6e22e">toAccountId</span>].<span style="color:#a6e22e">sort</span>((<span style="color:#a6e22e">a</span>, <span style="color:#a6e22e">b</span>) <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">a</span> <span style="color:#f92672">-</span> <span style="color:#a6e22e">b</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">await</span> <span style="color:#a6e22e">tx</span>.<span style="color:#a6e22e">query</span>(<span style="color:#e6db74">&#39;SELECT id FROM accounts WHERE id = ANY($1) ORDER BY id FOR UPDATE&#39;</span>, [<span style="color:#a6e22e">ids</span>]);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// その後に更新
</span></span></span></code></pre></td></tr></table>
</div>
</div><p><code>FOR UPDATE</code> を先に順序付きで取得することで、アプリ層の揺らぎを DB で吸収できます。</p>
<h2 id="5-実装レベルの対策パターン">5. 実装レベルの対策パターン</h2>
<h3 id="5-1-トランザクションを短くする">5-1. トランザクションを短くする</h3>
<p>デッドロックは「ロック保持時間」が長いほど発生しやすくなります。トランザクション内で外部 API 呼び出しをしていないか確認してください。これは最優先で排除します。</p>
<h3 id="5-2-失敗時のリトライを制御する">5-2. 失敗時のリトライを制御する</h3>
<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></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-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">attempt</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>; <span style="color:#a6e22e">attempt</span> <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">3</span>; <span style="color:#a6e22e">attempt</span><span style="color:#f92672">++</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> <span style="color:#a6e22e">runTx</span>();
</span></span><span style="display:flex;"><span>  } <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">e</span>: <span style="color:#66d9ef">any</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span>String(<span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">message</span>).<span style="color:#a6e22e">includes</span>(<span style="color:#e6db74">&#39;deadlock detected&#39;</span>) <span style="color:#f92672">||</span> <span style="color:#a6e22e">attempt</span> <span style="color:#f92672">===</span> <span style="color:#ae81ff">3</span>) <span style="color:#66d9ef">throw</span> <span style="color:#a6e22e">e</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">sleep</span>(<span style="color:#ae81ff">50</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">2</span> <span style="color:#f92672">**</span> <span style="color:#a6e22e">attempt</span>);
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="5-3-楽観ロックの導入">5-3. 楽観ロックの導入</h3>
<p>更新競合が多い領域では <code>version</code> カラムを使った楽観ロックが有効です。衝突時にアプリで再計算できます。</p>
<h2 id="6-監視運用面の改善">6. 監視・運用面の改善</h2>
<p>恒久対策を完了させるには、再発を検知できる状態を作る必要があります。</p>
<p>推奨メトリクス:</p>
<ul>
<li>deadlock 発生回数 / 分</li>
<li>lock wait 時間 p95</li>
<li>失敗リトライ回数</li>
<li>長時間トランザクション件数</li>
</ul>
<p>SQL 例（長時間 tx 検知）:</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> pid, now() <span style="color:#f92672">-</span> xact_start <span style="color:#66d9ef">AS</span> tx_age, query
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> pg_stat_activity
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> xact_start <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">AND</span> now() <span style="color:#f92672">-</span> xact_start <span style="color:#f92672">&gt;</span> interval <span style="color:#e6db74">&#39;30 seconds&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> tx_age <span style="color:#66d9ef">DESC</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><p>この結果を可視化し、閾値超過で通知するだけでも再発時の初動が速くなります。</p>
<h2 id="7-障害対応時の意思決定フレーム">7. 障害対応時の意思決定フレーム</h2>
<p>デッドロックが継続しているとき、現場は次の順序で判断すると迷いません。</p>
<ol>
<li>影響範囲（どの API / どの機能か）を確定</li>
<li>失敗処理を一時停止できるか判断（バッチ停止、機能フラグ）</li>
<li>ロック順序統一のホットフィックス可否</li>
<li>デプロイまでの間、リトライ制御で被害抑制</li>
</ol>
<p>「根本修正が間に合わない」ケースでも、リトライと負荷制御でユーザー影響を減らせます。</p>
<h2 id="8-よくあるアンチパターン">8. よくあるアンチパターン</h2>
<h3 id="アンチパターン1-serializable-に上げて解決した気になる">アンチパターン1: <code>SERIALIZABLE</code> に上げて解決した気になる</h3>
<p>隔離レベルを上げるだけでは、設計上の競合は消えません。むしろリトライ増加で負荷が悪化することがあります。</p>
<h3 id="アンチパターン2-select--for-update-を乱用する">アンチパターン2: <code>SELECT ... FOR UPDATE</code> を乱用する</h3>
<p>広範囲ロックは別の待ちを生みます。最小対象だけをロックし、順序を統一することが本質です。</p>
<h3 id="アンチパターン3-アプリログだけ見て-db-ログを見ない">アンチパターン3: アプリログだけ見て DB ログを見ない</h3>
<p>デッドロックの循環情報は DB 側ログにしか出ないことが多いです。必ず DB ログを一次情報として扱ってください。</p>
<h2 id="9-すぐ使えるチェックリスト">9. すぐ使えるチェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> <code>deadlock detected</code> の実例ログを保存した</li>
<li><input disabled="" type="checkbox"> ロック状況を <code>pg_stat_activity</code> と <code>pg_locks</code> で取得した</li>
<li><input disabled="" type="checkbox"> 再現 SQL を作成し、原因パターンを確認した</li>
<li><input disabled="" type="checkbox"> 更新順序を全経路で統一した</li>
<li><input disabled="" type="checkbox"> リトライ回数とバックオフを制限した</li>
<li><input disabled="" type="checkbox"> 監視メトリクスを追加した</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>PostgreSQL デッドロック対策は、魔法の設定値を探す作業ではありません。ポイントは一貫しており、</p>
<ul>
<li>ログで事実を取る</li>
<li>再現を作る</li>
<li>ロック順序を統一する</li>
</ul>
<p>この 3 ステップで大半の問題は改善します。障害対応時は焦って設定を増やすより、まず「どの順序で誰がロックを取ったか」を可視化することが最短ルートです。</p>
<h2 id="10-チーム運用に落とし込むためのルール化">10. チーム運用に落とし込むためのルール化</h2>
<p>技術対策ができても、運用ルールがないと再発します。特に効果が高いのは「PR テンプレートにロック観点を入れる」ことです。たとえば <code>複数行更新の順序は統一されているか</code>、<code>トランザクション内で外部I/Oをしていないか</code> をチェック項目にするだけで、設計時点で多くの問題を防げます。</p>
<p>さらに、負荷試験シナリオに競合ケース（同時更新）を追加してください。通常の性能試験は平均応答時間を見るだけで終わりがちですが、デッドロックは並行競合を作らないと検出できません。QA と開発が協調して「再現しにくい障害を再現するテスト」を用意できると、運用品質が一段上がります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>PostgreSQL</category>
      <category>Database</category>
      <category>Troubleshooting</category>
      <category>Backend</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>WebAssemblyとWASI：ブラウザを越えてサーバーサイドへ進出するWasmの可能性</title>
      <link>https://www.ai2core.com/posts/2026-02-24-webassembly-wasi/</link>
      <pubDate>Tue, 24 Feb 2026 12:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-24-webassembly-wasi/</guid>
      <description>WASI (WebAssembly System Interface) の基本概念と、Docker代替として期待されるサーバーサイドWasmの動向。</description>
      <content:encoded><![CDATA[<h2 id="webassemblyとwasiブラウザを越えてサーバーサイドへ進出するwasmの可能性">WebAssemblyとWASI：ブラウザを越えてサーバーサイドへ進出するWasmの可能性</h2>
<h3 id="はじめに">はじめに</h3>
<p>「コンテナの起動が遅い…」「開発環境と本番環境の差異でまたハマった…」「マイクロサービスのイメージサイズが肥大化してリソースを圧迫している…」</p>
<p>もしあなたがサーバーサイド開発に携わっているなら、このような悩みに一度は直面したことがあるのではないでしょうか。Dockerをはじめとするコンテナ技術は、現代のアプリケーション開発に革命をもたらしましたが、その一方で、起動時間のオーバーヘッド、リソース消費、セキュリティの懸念といった新たな課題も生み出しています。</p>
<p>もし、コンテナよりも高速に起動し、軽量で、かつ強力なセキュリティサンドボックスを持つ技術があるとしたらどうでしょう？しかも、特定のOSやCPUアーキテクチャに依存せず、真のポータビリティを実現できるとしたら？</p>
<p>この記事では、その答えとなりうる「WebAssembly (Wasm)」と、そのエコシステムをブラウザの外へ拡張する「WASI (WebAssembly System Interface)」について、深く掘り下げていきます。単なる技術解説に留まらず、Wasmがなぜ「Dockerの次」とまで言われるのか、その理由とサーバーサイドでの具体的な活用法、そして未来の可能性までを、コード例を交えながら徹底的に解説します。この記事を読み終える頃には、あなたはWasmがサーバーサイドコンピューティングの新たなパラダイムを切り拓く可能性を確信しているはずです。</p>
<h3 id="なぜ今サーバーサイドwasmが重要なのか">なぜ今、サーバーサイドWasmが重要なのか？</h3>
<p>WebAssemblyは、もともとWebブラウザ上でネイティブコードに近いパフォーマンスを実現するために生まれました。JavaScriptの代替または補完として、C/C++/Rustなどで書かれたコードを高速に実行できるバイナリフォーマットとして注目を集め、既に多くのWebアプリケーションで活用されています。</p>
<p>しかし、Wasmの持つ4つの主要な特性は、ブラウザの世界に留めておくにはあまりにも魅力的でした。</p>
<ol>
<li><strong>高速 (Fast):</strong> ネイティブに近い速度で実行可能な、効率的なバイナリフォーマット。</li>
<li><strong>安全 (Secure):</strong> デフォルトでメモリ安全なサンドボックス内で実行され、ホストシステムへのアクセスは明示的に許可された機能に限定される（Capability-based security）。</li>
<li><strong>ポータブル (Portable):</strong> 特定のCPUアーキテクチャやOSに依存しない。Wasmランタイムがあればどこでも同じように動作する。</li>
<li><strong>コンパクト (Compact):</strong> バイナリフォーマットは非常に小さく、ネットワーク経由での配信やストレージ効率に優れる。</li>
</ol>
<p>これらの特性は、サーバーサイドやエッジコンピューティングが抱える課題、特にコンテナ技術のペインポイントを解決する大きな可能性を秘めていました。</p>
<h4 id="コンテナ技術が抱える課題">コンテナ技術が抱える課題</h4>
<p>Dockerは素晴らしい技術ですが、いくつかのトレードオフがあります。</p>
<ul>
<li><strong>起動時間とオーバーヘッド:</strong> コンテナは軽量な仮想マシンと言われますが、それでもアプリケーションの起動にはOSのプロセスを立ち上げ、ファイルシステムをマウントするなど、数秒単位の時間がかかります。これがFaaS（Function as a Service）などのコールドスタート問題の一因となります。</li>
<li><strong>リソース消費:</strong> 各コンテナは、アプリケーションの依存ライブラリだけでなく、OSのユーザーランドの一部を含むレイヤ化されたイメージを持ちます。これにより、イメージサイズが数百MBから数GBに達することも珍しくなく、ディスク容量やメモリを消費します。</li>
<li><strong>セキュリティ:</strong> コンテナはNamespaceやCgroupsといったLinuxカーネルの機能を利用してプロセスを分離しますが、ホストOSのカーネルを共有しています。そのため、カーネルの脆弱性がコンテナの分離を破壊するリスクが常に存在します。</li>
<li><strong>ポータビリティの限界:</strong> 「Linuxコンテナ」はLinuxカーネル上で動作することを前提としています。WindowsやmacOSでDockerを使う場合、内部的にはLinuxの仮想マシンが動作しており、真のクロスプラットフォームとは言えません。</li>
</ul>
<p>これらの課題に対し、WebAssemblyは全く新しいアプローチを提示します。OS全体を仮想化するのではなく、個々のアプリケーションプロセスを、OSから完全に独立した軽量なサンドボックス内で実行するのです。</p>
<p>しかし、ここで一つの大きな壁がありました。オリジナルのWebAssemblyは、ブラウザのJavaScript APIと連携することしか想定されていなかったのです。ファイルシステムへのアクセス、ネットワーク通信、現在時刻の取得といった、サーバーサイドアプリケーションに必須の機能が標準化されていませんでした。</p>
<p>この問題を解決するために登場したのが、<strong>WASI (WebAssembly System Interface)</strong> です。</p>
<h3 id="wasiwebassemblyと世界をつなぐ架け橋">WASI：WebAssemblyと世界をつなぐ架け橋</h3>
<p>WASIは、WebAssemblyモジュールがホスト環境（ブラウザ、サーバー、エッジデバイスなど）のシステム機能へアクセスするための、標準化されたAPIです。WASIを「WebAssemblyのためのOSインターフェース」あるいは「POSIXのようなもの」と考えると分かりやすいでしょう。</p>
<p>WASIの登場により、Wasmはついにブラウザという揺りかごから飛び立ち、サーバーサイドという広大な大地でその真価を発揮する準備が整いました。</p>
<h4 id="wasiの仕組み">WASIの仕組み</h4>
<p>WASIは、Capability-based security（権限ベースのセキュリティ）モデルを基本としています。これは「プログラムは、明示的に与えられた権限（ファイルディスクリプタ、ソケットなど）しか利用できない」という原則です。</p>
<p>以下の図は、Wasm/WASIアプリケーションがOSとどのように対話するかを示しています。</p>
<pre tabindex="0"><code>+--------------------------+
|  Your Application Code   |  (e.g., Rust, Go, C++)
|  (Business Logic)        |
+--------------------------+
             | (Compile Time)
             v
+--------------------------+
|    Wasm Module (.wasm)   |
| (contains WASI imports)  |
+--------------------------+
             | (Runtime)
+--------------------------+  &lt;-- Wasm Sandbox Boundary
|      Wasm Runtime        |
| (e.g., Wasmtime, Wasmer) |
+------------+-------------+
             | (WASI Implementation)
             v
+--------------------------+
|        Host OS           |
| (Linux, macOS, Windows)  |
+--------------------------+
</code></pre><ol>
<li><strong>アプリケーションコード:</strong> 開発者は使い慣れた言語（Rust, Go, C++など）でコードを書きます。</li>
<li><strong>コンパイル:</strong> 専用のツールチェイン（例: <code>wasm32-wasi</code> ターゲット）を使い、Wasmモジュール（<code>.wasm</code> ファイル）にコンパイルします。この時、ファイルI/Oなどのシステムコールは、WASIのimport関数呼び出しに変換されます。</li>
<li><strong>実行:</strong> Wasmランタイムが <code>.wasm</code> ファイルをロードします。</li>
<li><strong>権限の付与:</strong> ランタイムを起動する際に、「このディレクトリへの読み込みを許可する」「このポートでの待ち受けを許可する」といった権限を明示的に与えます。</li>
<li><strong>システムコール:</strong> WasmモジュールがWASI関数（例: <code>fd_write</code>）を呼び出すと、ランタイムがそれを捕捉し、与えられた権限の範囲内でホストOSの対応するシステムコール（例: <code>write</code>）を実行します。</li>
</ol>
<p>この仕組みにより、Wasmモジュールは悪意のあるコードを含んでいたとしても、許可されていないファイルにアクセスしたり、意図しないネットワーク接続を確立したりすることは原理的に不可能です。これは、コンテナよりも遥かにきめ細かく、強力なセキュリティモデルと言えます。</p>
<h4 id="実践rustでwasiアプリケーションを作ってみる">実践：RustでWASIアプリケーションを作ってみる</h4>
<p>百聞は一見に如かず。実際に簡単なWASIアプリケーションを作成し、動かしてみましょう。ここでは、多くのWasm/WASIプロジェクトで採用されているRustを使用します。</p>
<p><strong>ステップ1: 環境構築</strong></p>
<p>まず、RustとWasmのコンパイルターゲットをインストールします。</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></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><span style="color:#75715e"># Rustをインストール (未インストールの場合)</span>
</span></span><span style="display:flex;"><span>curl --proto <span style="color:#e6db74">&#39;=https&#39;</span> --tlsv1.2 -sSf https://sh.rustup.rs | sh
</span></span><span style="display:flex;"><span>source <span style="color:#e6db74">&#34;</span>$HOME<span style="color:#e6db74">/.cargo/env&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># wasm32-wasi ターゲットを追加</span>
</span></span><span style="display:flex;"><span>rustup target add wasm32-wasi
</span></span></code></pre></td></tr></table>
</div>
</div><p>次に、Wasmランタイムをインストールします。ここでは代表的なランタイムの一つである <code>Wasmtime</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>curl https://wasmtime.dev/install.sh -sSf | bash
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>ステップ2: コードの作成</strong></p>
<p><code>hello-wasi</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></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>cargo new hello-wasi
</span></span><span style="display:flex;"><span>cd hello-wasi
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>src/main.rs</code> を開き、以下のコードに書き換えます。このコードは、カレントディレクトリに <code>output.txt</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></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-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">use</span> std::fs::File;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> std::io::prelude::<span style="color:#f92672">*</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">use</span> std::env;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">main</span>() -&gt; <span style="color:#a6e22e">std</span>::io::Result<span style="color:#f92672">&lt;</span>()<span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>    println!(<span style="color:#e6db74">&#34;Hello from inside Wasm!&#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:#75715e"></span>    <span style="color:#66d9ef">let</span> args: Vec<span style="color:#f92672">&lt;</span>String<span style="color:#f92672">&gt;</span> <span style="color:#f92672">=</span> env::args().collect();
</span></span><span style="display:flex;"><span>    println!(<span style="color:#e6db74">&#34;I see these args: </span><span style="color:#e6db74">{:?}</span><span style="color:#e6db74">&#34;</span>, args);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// output.txt にメッセージを書き込む
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">let</span> <span style="color:#66d9ef">mut</span> file <span style="color:#f92672">=</span> File::create(<span style="color:#e6db74">&#34;output.txt&#34;</span>)<span style="color:#f92672">?</span>;
</span></span><span style="display:flex;"><span>    file.write_all(<span style="color:#e6db74">b</span><span style="color:#e6db74">&#34;Hello, WASI world!</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>)<span style="color:#f92672">?</span>;
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    println!(<span style="color:#e6db74">&#34;Successfully wrote to output.txt&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Ok(())
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>このコードは、標準的なRustのファイル操作API (<code>std::fs::File</code>) を使っているだけです。WASIの素晴らしい点は、開発者がWASI固有のAPIを意識する必要がほとんどないことです。既存の標準ライブラリが、コンパイル時に自動的にWASIの呼び出しに変換されます。</p>
<p><strong>ステップ3: コンパイル</strong></p>
<p><code>wasm32-wasi</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>cargo build --target wasm32-wasi
</span></span></code></pre></td></tr></table>
</div>
</div><p>成功すると、<code>target/wasm32-wasi/debug/hello-wasi.wasm</code> というファイルが生成されます。これが私たちのWASIアプリケーション本体です。</p>
<p><strong>ステップ4: 実行</strong></p>
<p><code>Wasmtime</code> を使って実行します。ここで重要なのが、WASIの権限設定です。</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-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># --dir=. でカレントディレクトリへのアクセスを許可する</span>
</span></span><span style="display:flex;"><span>wasmtime run --dir<span style="color:#f92672">=</span>. target/wasm32-wasi/debug/hello-wasi.wasm -- some test args
</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"># Hello from inside Wasm!</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># I see these args: [&#34;target/wasm32-wasi/debug/hello-wasi.wasm&#34;, &#34;some&#34;, &#34;test&#34;, &#34;args&#34;]</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Successfully wrote to output.txt</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>実行後、カレントディレクトリを確認すると、<code>output.txt</code> が作成され、中に &ldquo;Hello, WASI world!&rdquo; と書き込まれているはずです。</p>
<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></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><span style="color:#75715e"># --dir=. を付けずに実行</span>
</span></span><span style="display:flex;"><span>wasmtime run target/wasm32-wasi/debug/hello-wasi.wasm
</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"># Hello from inside Wasm!</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># I see these args: [&#34;target/wasm32-wasi/debug/hello-wasi.wasm&#34;]</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Error: failed to run main module `...`</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Caused by:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#    0: failed to create file `output.txt`</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#    1: open &#34;output.txt&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#    2: capability not allowed</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>capability not allowed</code> というエラーメッセージが表示され、プログラムが異常終了しました。これは、WASIのセキュリティモデルが正しく機能している証拠です。Wasmモジュールは、許可されていないリソース（この場合はカレントディレクトリへの書き込み）にアクセスしようとして、ランタイムによってブロックされたのです。</p>
<h3 id="メリットとデメリットサーバーサイドwasm-vs-コンテナ">メリットとデメリット：サーバーサイドWasm vs コンテナ</h3>
<p>Wasm/WASIがコンテナと比べてどのような利点と欠点を持つのか、表にまとめて比較してみましょう。</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">項目</th>
          <th style="text-align: left">WebAssembly (WASI)</th>
          <th style="text-align: left">コンテナ (Docker)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><strong>起動速度</strong></td>
          <td style="text-align: left"><strong>非常に高速 (サブミリ秒)</strong></td>
          <td style="text-align: left">高速 (数百ミリ秒〜数秒)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>リソース消費</strong></td>
          <td style="text-align: left"><strong>非常に軽量 (数MB)</strong></td>
          <td style="text-align: left">軽量 (数十MB〜数GB)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>セキュリティ</strong></td>
          <td style="text-align: left"><strong>強力なサンドボックス (Capability-based)</strong></td>
          <td style="text-align: left">プロセス分離 (Namespace, Cgroups)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>ポータビリティ</strong></td>
          <td style="text-align: left"><strong>OSとCPUアーキテクチャから独立</strong></td>
          <td style="text-align: left">OSカーネルに依存 (主にLinux)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>イメージサイズ</strong></td>
          <td style="text-align: left"><strong>非常に小さい (数KB〜数MB)</strong></td>
          <td style="text-align: left">大きい (数百MB〜数GB)</td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>エコシステム</strong></td>
          <td style="text-align: left">発展途上</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"><strong>事実上の標準 (OCI)</strong></td>
      </tr>
      <tr>
          <td style="text-align: left"><strong>デバッグ/監視</strong></td>
          <td style="text-align: left">ツールが限定的</td>
          <td style="text-align: left"><strong>ツールが豊富</strong></td>
      </tr>
  </tbody>
</table>
<h4 id="wasmwasiの明確なメリット">Wasm/WASIの明確なメリット</h4>
<ul>
<li><strong>圧倒的なパフォーマンス:</strong> サブミリ秒単位の起動速度は、コールドスタートが致命的となるFaaSやサーバーレス環境で絶大な効果を発揮します。また、メモリフットプリントが小さいため、同じハードウェア上でより多くのインスタンスを高密度に実行できます。</li>
<li><strong>鉄壁のセキュリティ:</strong> デフォルトで何も許可しないサンドボックスモデルは、「最小権限の原則」を強制します。これにより、サプライチェーン攻撃などで悪意のあるコードが混入した場合でも、被害を最小限に抑えることができます。</li>
<li><strong>真のポータビリティ:</strong> 一度Wasmにコンパイルすれば、Wasmランタイムが動作する環境であれば、Linuxサーバー、Windowsデスクトップ、macOSラップトップ、Raspberry Pi、さらにはIoTデバイスまで、どこでも全く同じように動作します。<code>build once, run anywhere</code> の真の実現です。</li>
</ul>
<h4 id="wasmwasiの現在の課題デメリット">Wasm/WASIの現在の課題（デメリット）</h4>
<ul>
<li><strong>エコシステムの未成熟さ:</strong> コンテナにはDocker Hub、Kubernetes、Prometheus、Helmといった巨大で成熟したエコシステムが存在します。WasmにもWasmEdge、Spin、WasmCloudといった有望なプロジェクトはありますが、ツールの成熟度や選択肢の豊富さではまだ及びません。</li>
<li><strong>標準化の途上:</strong> WASIは現在も活発に開発が進められています。ファイルシステムや標準入出力といった基本的な部分は安定していますが、非同期I/O、高度なネットワーク機能（ソケット）、スレッディングといった部分はまだ標準化の途上にあります。これにより、現状では複雑なネットワークアプリケーションの実装が難しい場合があります。</li>
<li><strong>既存資産との連携:</strong> 多くの企業は、既にコンテナベースのCI/CDパイプラインやオーケストレーションシステムを構築しています。これらの既存資産とWasmをどう連携させていくかは、導入における大きな課題です。</li>
</ul>
<p>Wasmはコンテナを完全に置き換える「銀の弾丸」ではなく、それぞれの特性を理解し、適材適所で使い分ける、あるいは組み合わせて利用することが重要です。</p>
<h3 id="現場で使える実践的なtipswasmはどこで輝くのか">現場で使える実践的なTips：Wasmはどこで輝くのか？</h3>
<p>理論は十分です。では、具体的にどのようなユースケースでWasm/WASIは真価を発揮するのでしょうか？</p>
<h4 id="1-faas--サーバーレスコンピューティング">1. FaaS / サーバーレスコンピューティング</h4>
<p>AWS LambdaなどのFaaSプラットフォームでは、コールドスタートが常に課題となります。Wasmの超高速な起動時間は、この問題を劇的に改善します。リクエストが来てからWasmインスタンスを起動しても、ユーザーが体感できるほどの遅延は発生しません。Fermyon CloudやCloudflare Workersといったプラットフォームは、既にこの利点を活かしたサービスを提供しています。</p>
<h4 id="2-エッジコンピューティング">2. エッジコンピューティング</h4>
<p>リソースが限られ、多様なハードウェアが混在するエッジ環境は、Wasmの独壇場です。軽量でポータブルなWasmモジュールは、CPUパワーやメモリが少ないデバイスでも効率的に動作し、中央のサーバーから簡単にデプロイ・更新できます。CDNのエッジでリクエストを加工したり、IoTゲートウェイでセンサーデータを処理したりといった用途に最適です。</p>
<h4 id="3-安全なプラグインシステム">3. 安全なプラグインシステム</h4>
<p>アプリケーションにサードパーティ製のプラグインやユーザー定義関数（UDF）を組み込む際、セキュリティは最大の懸念事項です。Wasmの強力なサンドボックスを使えば、プラグインコードがホストアプリケーションやシステムに悪影響を与えることを防ぎ、安全に機能を拡張できます。Envoyプロキシのフィルター、データベースのUDF、SaaSアプリケーションのカスタマイズ機能などで採用が進んでいます。</p>
<h4 id="4-軽量なマイクロサービス">4. 軽量なマイクロサービス</h4>
<p>全てのマイクロサービスがコンテナを必要とするわけではありません。単一の責務を持つ小さなサービスであれば、Wasmモジュールとして実装・デプロイすることで、リソース消費を大幅に削減し、デプロイ時間を短縮できます。Fermyonが開発する <strong>Spin</strong> は、Wasmベースのマイクロサービス開発を簡素化するための優れたフレームワークです。</p>
<p><strong>Spinを使ってみる:</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></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><span style="color:#75715e"># Spinをインストール</span>
</span></span><span style="display:flex;"><span>curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># RustベースのHTTPマイクロサービスを作成</span>
</span></span><span style="display:flex;"><span>spin new -t http-rust my-first-spin-app
</span></span><span style="display:flex;"><span>cd my-first-spin-app
</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>cargo build --target wasm32-wasi
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ビルド &amp; 実行</span>
</span></span><span style="display:flex;"><span>spin build <span style="color:#f92672">&amp;&amp;</span> spin up
</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"># curl -i http://127.0.0.1:3000</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># HTTP/1.1 200 OK</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># content-type: text/plain; charset=utf-8</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># content-length: 12</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># date: ...</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Hello, Fermyon</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>このように、フレームワークを利用することで、驚くほど簡単にWasmベースのアプリケーションを構築し、実行できます。</p>
<h3 id="まとめコンテナの次に来る波に備える">まとめ：コンテナの次に来る波に備える</h3>
<p>WebAssemblyとWASIが切り拓くサーバーサイドの世界は、まだ始まったばかりです。しかし、そのポテンシャルは計り知れません。高速な起動、軽量なフットプリント、堅牢なセキュリティ、そして真のポータビリティは、現在のクラウドネイティブ技術が抱える多くの課題に対する、エレガントな解決策となり得ます。</p>
<p>WasmがすぐにDockerやKubernetesを完全に置き換えることはないでしょう。むしろ、最初はKubernetes上でWasmワークロードを実行する <code>Krustlet</code> のようなプロジェクトを通じて共存し、徐々にその適用範囲を広げていくと考えられます。FaaS、エッジ、プラグインといった特定の領域で強みを発揮し、やがてはマイクロサービスの主要な選択肢の一つとなる未来が待っています。</p>
<p>エンジニアとして、私たちはこの新しい波に乗り遅れるわけにはいきません。まずは手元でWasmtimeやSpinを試し、簡単なWASIアプリケーションをビルドしてみてください。RustやGo、TinyGoといった言語で、その開発体験に触れてみてください。</p>
<p>ブラウザのサンドボックスから解き放たれ、サーバーサイドという広大な舞台に立ったWebAssembly。その進化は、これからのアプリケーション開発のあり方を根底から変える可能性を秘めています。コンテナがもたらした革命の次の章は、今まさに始まろうとしているのです。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>WebAssembly</category>
      <category>WASI</category>
      <category>Backend</category>
    </item>
  </channel>
</rss>
