<?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/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>デバッグ · 仁才徳</title>
    <link>https://jared.lynskey.co.nz/ja/tags/%E3%83%87%E3%83%90%E3%83%83%E3%82%B0/</link>
    <description>ソフトウェアエンジニアリングと開発組織のリーダーシップについて。評価、採用とチームの拡大、CI/CD、DevOps モニタリング、エージェンティック AI。ソウルより。</description>
    <generator>Hugo</generator>
    <language>ja</language>
    <copyright>© 2026 仁才徳</copyright>
    <lastBuildDate>Wed, 09 Sep 2026 00:00:00 +1200</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/ja/tags/%E3%83%87%E3%83%90%E3%83%83%E3%82%B0/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>ゼロスケールと、再起動のたびに起きる30秒の障害</title>
      <link>https://jared.lynskey.co.nz/ja/posts/2026/2026-09-09-scale-to-zero-504s/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ja/posts/2026/2026-09-09-scale-to-zero-504s/</guid>
      <pubDate>Wed, 09 Sep 2026 00:00:00 +1200</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>Fly.io</category>
      <category>Django</category>
      <category>DevOps</category>
      <category>Cloudflare</category>
      <category>パフォーマンス</category>
      <category>コールドスタート</category>
      <category>Redis</category>
      <category>デバッグ</category>
      <description>ヘルスチェックはすべて緑なのに、リクエストの22%が失敗していました。追いかけた結果、33秒のコールドスタートを3秒の復帰に変えました。</description>
      <content:encoded><![CDATA[<p>Cloudflare のダッシュボードでトラフィックの急増を調べに行ったら、その下にもっとひどいものが埋まっていました。1日のうちにアプリが処理したリクエストの22%が失敗していたのです。アラートは一つも鳴っていません。ヘルスチェックはすべて緑。ログも完全に健全に見えました。失敗したリクエストが、そもそもアプリケーションに届いていなかったからです。</p>
<p>原因は自分で書いたコンテナの起動スクリプトでした。まっとうな処理を、まったく間違ったタイミングで実行していました。</p>

<h2 class="relative group">急増は囮だった
    <div id="急増は囮だった" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%80%a5%e5%a2%97%e3%81%af%e5%9b%ae%e3%81%a0%e3%81%a3%e3%81%9f" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>もともと調べたかったのは、中央値が毎時121件のところに約4,900件が集中した1時間です。ユニークな訪問者は42人。1人あたり110リクエストで、人間のブラウジングではありません。</p>
<p>スキャナーでした。ボットが1体、存在しないサブドメインをでっち上げながら、それぞれにシークレットを要求して回っていました。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">api-stage-asia.marucommunity.com/.env.local
</span></span><span class="line"><span class="cl">api-us-east-1-demo.marucommunity.com/.env
</span></span><span class="line"><span class="cl">api-eu-uat.marucommunity.com/wp-config.php~
</span></span><span class="line"><span class="cl">app-test-us-east-1.marucommunity.com/actuator/configprops
</span></span><span class="line"><span class="cl">prod-api-use2.marucommunity.com/.env.local
</span></span><span class="line"><span class="cl">relay.marucommunity.com/.env.production</span></span></code></pre></div></div>
<p>30ほどのホスト名、思いつく限りの <code>.env</code> の変種、そして Django アプリに向けた Spring Boot の actuator エンドポイント。すべて正規ホストへの301になり、何も返していません。退屈で、実際に無害でした。</p>
<p>ただ、分析画面を開いたついでに同じ日をステータスコードで集計しました。本当の問題はそこにありました。</p>
<table>
	<thead>
			<tr>
					<th>ステータス</th>
					<th>24時間のリクエスト数</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>合計</td>
					<td>11,350</td>
			</tr>
			<tr>
					<td>504 Gateway Timeout</td>
					<td>2,474</td>
			</tr>
	</tbody>
</table>
<p>22%です。しかもサイト全体に均等ではなく、いちばん痛いところに正確に集中していました。</p>
<table>
	<thead>
			<tr>
					<th>エンドポイント</th>
					<th>504</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>通知の未読数</td>
					<td>621</td>
			</tr>
			<tr>
					<td>通知一覧</td>
					<td>614</td>
			</tr>
			<tr>
					<td>メッセージの未読数</td>
					<td>469</td>
			</tr>
			<tr>
					<td>フィードカーソル</td>
					<td>210</td>
			</tr>
	</tbody>
</table>
<p>どれもモバイルアプリが30秒ごとにポーリングしているエンドポイントです。つまりこれは抽象的なエラー率の数字ではありませんでした。ユーザーの端末が1日中、おそらく何週間も、未読バッジを静かに読み込めずにいたということです。</p>

<h2 class="relative group">ログが健全に見えた理由
    <div id="ログが健全に見えた理由" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%ad%e3%82%b0%e3%81%8c%e5%81%a5%e5%85%a8%e3%81%ab%e8%a6%8b%e3%81%88%e3%81%9f%e7%90%86%e7%94%b1" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>最初はアプリが遅くなったのだろうと考えました。違いました。最悪の時間帯のアプリケーション側のリクエストログをアーカイブから取り出しました。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">[2026-09-08 01:00:29] &#34;GET /api/v1/notifications/unread_count/&#34; 304 61.615
</span></span><span class="line"><span class="cl">[2026-09-08 01:00:29] &#34;GET /api/v1/notifications/&#34;             304 63.568
</span></span><span class="line"><span class="cl">[2026-09-08 01:00:59] &#34;GET /api/v1/notifications/unread_count/&#34; 304 58.742
</span></span><span class="line"><span class="cl">[2026-09-08 01:00:59] &#34;GET /api/v1/notifications/&#34;             304 105.438</span></span></code></pre></div></div>
<p>44〜105ミリ秒、すべて304。オリジンは速かったのです。</p>
<p>これが重要な手がかりで、ルールとして書いておく価値があります。<strong>エッジがオリジンの知らないエラーを報告しているなら、リクエストは到達する前に死んでいます。</strong> アプリケーションログを読むのをやめて、その手前にあるものを見てください。</p>
<p>私の場合は Fly.io のプロキシで、答えはマシンのスケジューリング方法にありました。</p>

<h2 class="relative group">ゼロスケールはコストの約束であって、レイテンシの約束ではない
    <div id="ゼロスケールはコストの約束であってレイテンシの約束ではない" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%bc%e3%83%ad%e3%82%b9%e3%82%b1%e3%83%bc%e3%83%ab%e3%81%af%e3%82%b3%e3%82%b9%e3%83%88%e3%81%ae%e7%b4%84%e6%9d%9f%e3%81%a7%e3%81%82%e3%81%a3%e3%81%a6%e3%83%ac%e3%82%a4%e3%83%86%e3%83%b3%e3%82%b7%e3%81%ae%e7%b4%84%e6%9d%9f%e3%81%a7%e3%81%af%e3%81%aa%e3%81%84" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>私の設定はこうです。一見すると妥当でしょう。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">http_service</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="nx">auto_stop_machines</span> <span class="p">=</span> <span class="s1">&#39;stop&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="nx">auto_start_machines</span> <span class="p">=</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">  <span class="nx">min_machines_running</span> <span class="p">=</span> <span class="mi">1</span></span></span></code></pre></div></div>
<p>アイドルのマシンは停止し、トラフィックが来れば再び起動する。使った分だけ払う。トラフィックにムラのある小さなアプリならまさに望む挙動で、だからこそマシンを10台定義していても普段は1台しか動いていませんでした。</p>
<p>この方式が隠している請求書はレイテンシです。停止中のマシンにリクエストが来れば、誰かがそのマシンの起動を待つことになります。起動がプロキシの我慢より長ければ、その誰かは504を受け取ります。</p>
<p>では私のマシンの起動には何秒かかっていたのか。一度も測っていませんでした。ログアーカイブは知っています。起動スクリプトが各フェーズを出力し、すべての行にマシンIDが付いているからです。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">WITH</span><span class="w"> </span><span class="n">s</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">SELECT</span><span class="w"> </span><span class="n">fly</span><span class="p">.</span><span class="n">app</span><span class="p">.</span><span class="n">instance</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">inst</span><span class="p">,</span><span class="w"> </span><span class="k">timestamp</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">         </span><span class="k">CASE</span><span class="w"> </span><span class="k">WHEN</span><span class="w"> </span><span class="n">message</span><span class="w"> </span><span class="k">ILIKE</span><span class="w"> </span><span class="s1">&#39;%Starting Granian%&#39;</span><span class="w">    </span><span class="k">THEN</span><span class="w"> </span><span class="s1">&#39;begin&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="k">WHEN</span><span class="w"> </span><span class="n">message</span><span class="w"> </span><span class="k">ILIKE</span><span class="w"> </span><span class="s1">&#39;%Migrations complete%&#39;</span><span class="w"> </span><span class="k">THEN</span><span class="w"> </span><span class="s1">&#39;migrated&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="k">WHEN</span><span class="w"> </span><span class="n">message</span><span class="w"> </span><span class="k">LIKE</span><span class="w">  </span><span class="s1">&#39;%&#34;GET /api/health/%&#39;</span><span class="w">   </span><span class="k">THEN</span><span class="w"> </span><span class="s1">&#39;serving&#39;</span><span class="w"> </span><span class="k">END</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">phase</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">FROM</span><span class="w"> </span><span class="n">logs</span><span class="p">(</span><span class="s1">&#39;koreapost&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;2026-09-08&#39;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">inst</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">       </span><span class="k">min</span><span class="p">(</span><span class="k">timestamp</span><span class="p">)</span><span class="w"> </span><span class="n">FILTER</span><span class="w"> </span><span class="p">(</span><span class="k">WHERE</span><span class="w"> </span><span class="n">phase</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;begin&#39;</span><span class="p">)</span><span class="w">    </span><span class="k">AS</span><span class="w"> </span><span class="n">started</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">       </span><span class="k">min</span><span class="p">(</span><span class="k">timestamp</span><span class="p">)</span><span class="w"> </span><span class="n">FILTER</span><span class="w"> </span><span class="p">(</span><span class="k">WHERE</span><span class="w"> </span><span class="n">phase</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;migrated&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">migrated</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">       </span><span class="k">min</span><span class="p">(</span><span class="k">timestamp</span><span class="p">)</span><span class="w"> </span><span class="n">FILTER</span><span class="w"> </span><span class="p">(</span><span class="k">WHERE</span><span class="w"> </span><span class="n">phase</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;serving&#39;</span><span class="p">)</span><span class="w">  </span><span class="k">AS</span><span class="w"> </span><span class="n">first_serve</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="n">s</span><span class="w"> </span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">inst</span></span></span></code></pre></div></div>
<p>すべてのマシンで一貫して出た答えです。</p>
<table>
	<thead>
			<tr>
					<th>フェーズ</th>
					<th>所要時間</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>起動からマイグレーション完了まで</td>
					<td>10〜13秒</td>
			</tr>
			<tr>
					<td>マイグレーションから最初のリクエスト処理まで</td>
					<td>8〜19秒</td>
			</tr>
			<tr>
					<td><strong>合計</strong></td>
					<td><strong>25〜33秒</strong></td>
			</tr>
	</tbody>
</table>
<p>そして、それが何回起きているかを数えました。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">starts</span><span class="p">,</span><span class="w"> </span><span class="k">count</span><span class="p">(</span><span class="k">DISTINCT</span><span class="w"> </span><span class="n">fly</span><span class="p">.</span><span class="n">app</span><span class="p">.</span><span class="n">instance</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">machines</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="n">logs</span><span class="p">(</span><span class="s1">&#39;koreapost&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;2026-09-08&#39;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">WHERE</span><span class="w"> </span><span class="n">message</span><span class="w"> </span><span class="k">ILIKE</span><span class="w"> </span><span class="s1">&#39;%Starting Granian%&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1">-- starts: 92, machines: 10</span></span></span></code></pre></div></div>
<p>1日にコールドスタートが92回。そのたびに、そのマシンへ振られたリクエストがタイムアウトする30秒の窓が開いていました。22%はもう謎ではありません。</p>

<h2 class="relative group">原因は自分の entrypoint の中にあった
    <div id="原因は自分の-entrypoint-の中にあった" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%8e%9f%e5%9b%a0%e3%81%af%e8%87%aa%e5%88%86%e3%81%ae-entrypoint-%e3%81%ae%e4%b8%ad%e3%81%ab%e3%81%82%e3%81%a3%e3%81%9f" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>すべてのマシンが、リクエストを1件処理する前に実行していたものです。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="o">=</span> <span class="s2">&#34;granian-api&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;Running migrations...&#34;</span>
</span></span><span class="line"><span class="cl">    python manage.py migrate --noinput
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;Running collectstatic in background...&#34;</span>
</span></span><span class="line"><span class="cl">    python manage.py collectstatic --noinput <span class="p">&amp;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nb">exec</span> granian --interface wsgi koreapost_project.wsgi:application ...
</span></span><span class="line"><span class="cl"><span class="k">fi</span></span></span></code></pre></div></div>
<p>「1日に92回」を思い浮かべながらもう一度読んでみてください。</p>
<p><code>migrate</code> は完全な Django の起動です。全モデルをインポートし、Postgres に接続し、マイグレーションテーブルを問い合わせ、やることがないと判断する。共有CPUでは、何も達成しないために10秒かかります。<code>collectstatic</code> は2回目の完全な Django 起動で、そのうえ静的ファイルをオブジェクトストレージにアップロードし、Web サーバーが立ち上がろうとしているまさに同じ単一コアを奪い合います。Granian が3回目の起動です。</p>
<p>Python インタプリタが3つ、うち2つはすでに済んだ作業を繰り返しながら、マシンが目覚めるたびに毎回。</p>
<p>そう書いた理由はまっとうで、同じような形になっているコードは多いはずです。マイグレーションはどこかで走らせる必要があり、Fly の <code>release_command</code> は以前ハングしたことがあり、entrypoint はデプロイ時に必ず実行される唯一の場所だからです。デプロイ作業を置く場所としては正しい。ただ<strong>マシン起動ごと</strong>に置く場所としては正しくない。そしてマシンが自分で止まったり起きたりし始めた瞬間、その2つはもう同じ出来事ではありません。</p>
<p>バグの全体はこれで、よくある種類だと思います。</p>
<blockquote><p><strong>リリース</strong>に属する作業が、<strong>マシン起動</strong>ごとに実行されていました。ゼロスケールが1つの出来事を92個に変えたのです。</p>
</blockquote>
<h2 class="relative group">解法: リリースを一度だけ確保する
    <div id="解法-リリースを一度だけ確保する" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%a7%a3%e6%b3%95-%e3%83%aa%e3%83%aa%e3%83%bc%e3%82%b9%e3%82%92%e4%b8%80%e5%ba%a6%e3%81%a0%e3%81%91%e7%a2%ba%e4%bf%9d%e3%81%99%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>あるイメージで起動する8台目のマシンでマイグレーションが走る必要はありません。そのイメージについて一度走ればよく、残りはそれが済んだと知ればよいだけです。</p>
<p>つまり、デプロイごとに名前が変わるロックです。Fly はその名前を <code>FLY_IMAGE_REF</code> として無料で渡してくれますし、Redis はすでにありました。全体が Django より前に走る小さなスクリプトなので、よくあるケースの答えはフレームワークの起動ではなくミリ秒で済みます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">release_id</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;デプロイされたイメージが変わったときにちょうど変わる値。&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&#34;FLY_IMAGE_REF&#34;</span><span class="p">)</span> <span class="ow">or</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&#34;FLY_MACHINE_VERSION&#34;</span><span class="p">)</span> <span class="ow">or</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">claim</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">release</span> <span class="o">=</span> <span class="n">release_id</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">release</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">PREPARE</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">client</span> <span class="o">=</span> <span class="n">_client</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">client</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>          <span class="c1"># Redis がなければ、以前とまったく同じ挙動</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">PREPARE</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">done_key</span><span class="p">,</span> <span class="n">lock_key</span> <span class="o">=</span> <span class="n">_keys</span><span class="p">(</span><span class="n">release</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">done_key</span><span class="p">):</span>    <span class="c1"># このイメージは誰かがすでに準備済み</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">SKIP</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">owner</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&#34;FLY_MACHINE_ID&#34;</span><span class="p">,</span> <span class="s2">&#34;unknown&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">client</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">lock_key</span><span class="p">,</span> <span class="n">owner</span><span class="p">,</span> <span class="n">nx</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">ex</span><span class="o">=</span><span class="n">LOCK_TTL_SECONDS</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">PREPARE</span>          <span class="c1"># 競争に勝ったので自分が処理する</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 他のマシンが準備中なら待つ。半分だけ適用されたスキーマに</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># リクエストを受けるより、起動が遅いほうがましだから。</span>
</span></span><span class="line"><span class="cl">    <span class="n">deadline</span> <span class="o">=</span> <span class="n">_monotonic</span><span class="p">()</span> <span class="o">+</span> <span class="n">WAIT_SECONDS</span>
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="n">_monotonic</span><span class="p">()</span> <span class="o">&lt;</span> <span class="n">deadline</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">_sleep</span><span class="p">(</span><span class="n">POLL_SECONDS</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">done_key</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="n">SKIP</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">lock_key</span><span class="p">)</span> <span class="ow">and</span> <span class="n">client</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">lock_key</span><span class="p">,</span> <span class="n">owner</span><span class="p">,</span> <span class="n">nx</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">ex</span><span class="o">=</span><span class="n">LOCK_TTL_SECONDS</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="n">PREPARE</span>      <span class="c1"># 持ち主がマイグレーション中に落ちたので引き継ぐ</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">PREPARE</span>              <span class="c1"># 十分待ったので自分でやる</span></span></span></code></pre></div></div>
<p>entrypoint は <code>if</code> 一つになります。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="k">if</span> python3 koreapost_project/release_gate.py claim<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    python manage.py migrate --noinput
</span></span><span class="line"><span class="cl">    python manage.py collectstatic --noinput <span class="p">&amp;</span>
</span></span><span class="line"><span class="cl">    python3 koreapost_project/release_gate.py <span class="k">done</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">exec</span> granian --interface wsgi koreapost_project.wsgi:application ...</span></span></code></pre></div></div>

<h2 class="relative group">すべての失敗は「とにかく準備する」で終わらせる
    <div id="すべての失敗はとにかく準備するで終わらせる" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%81%99%e3%81%b9%e3%81%a6%e3%81%ae%e5%a4%b1%e6%95%97%e3%81%af%e3%81%a8%e3%81%ab%e3%81%8b%e3%81%8f%e6%ba%96%e5%82%99%e3%81%99%e3%82%8b%e3%81%a7%e7%b5%82%e3%82%8f%e3%82%89%e3%81%9b%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>コードよりも、この部分こそ真似する価値があります。</p>
<p>このゲートはユーザーとスキーマのマイグレーションの間に立ちます。<strong>スキップする</strong>方向に間違えれば、マイグレーションされていないデータベースにリクエストを受けることになり、これは本物の障害です。<strong>準備する</strong>方向に間違えれば、冪等な no-op のマイグレーションを2回走らせて10秒を無駄にするだけです。</p>
<p>この2つの結果は対称ではないので、コードも対称に扱ってはいけません。不確かな経路はすべて <code>PREPARE</code> を返します。</p>
<ul>
<li><strong>Redis がない、または到達不能。</strong> 準備します。これは以前の挙動そのものなので、キャッシュ障害は「壊れる」ではなく「遅くなる」に落ちます。</li>
<li><strong>環境にイメージ参照がない。</strong> 準備します。どのリリースか分からない以上、準備済みかどうかも分かりません。</li>
<li><strong>ロックの持ち主がマイグレーション中に落ちた。</strong> ロックが期限切れになり、次の待機者が引き継いで準備します。</li>
<li><strong>タイムアウトより長く待った。</strong> それでも準備します。永遠にサービスできないマシンのほうが、重複マイグレーションより悪いからです。</li>
</ul>
<p>逆方向の非対称を1つだけ意図的に入れてあります。他のマシンが実際にロックを保持していると分かったマシンは、サービスを始めずに<strong>待ちます</strong>。デプロイ中に起動が遅いのは構いません。半分だけ適用されたスキーマに問い合わせを受けるのは困ります。</p>
<p>耐障害性のあるキャッシュラッパーでこれを組む場合、罠が1つあります。私のラッパーは Redis に到達できないとき <code>add()</code> が <code>False</code> を返し、それは「他の誰かがロックを持っている」とまったく同じに読めます。そのままなら全マシンがマイグレーションをスキップしていました。まさに危険な方向です。ロックを読み返して区別してください。誰も保持していないロックは、競争に負けたのではなく、キャッシュが壊れているという意味です。</p>

<h2 class="relative group">Redis なしでテストする
    <div id="redis-なしでテストする" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#redis-%e3%81%aa%e3%81%97%e3%81%a7%e3%83%86%e3%82%b9%e3%83%88%e3%81%99%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>このロジックはユニットテストの価値があります。面白い経路ほど手作業では再現できないからです。メソッド2つのフェイクで足ります。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">FakeRedis</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;ゲートに必要なだけの redis-py: get と、nx/ex を受ける set。&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">store</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">):</span> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">store</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">set</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">,</span> <span class="n">nx</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">ex</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">nx</span> <span class="ow">and</span> <span class="n">key</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">store</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">store</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">True</span></span></span></code></pre></div></div>
<p>あとは時間を自分の都合で進めて厄介なケースを動かします。ゲートが <code>time.sleep</code> を直接呼ばず、モジュールレベルの <code>_sleep</code> と <code>_monotonic</code> の別名を使っている点に注目してください。おかげで同じプロセスの他のスレッドに触れずにテストで差し替えられます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_a_waiter_takes_over_when_the_owner_disappears</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">release_gate</span><span class="o">.</span><span class="n">claim</span><span class="p">(),</span> <span class="n">release_gate</span><span class="o">.</span><span class="n">PREPARE</span><span class="p">)</span>   <span class="c1"># 持ち主がロックを取る</span>
</span></span><span class="line"><span class="cl">    <span class="n">_done</span><span class="p">,</span> <span class="n">lock_key</span> <span class="o">=</span> <span class="n">release_gate</span><span class="o">.</span><span class="n">_keys</span><span class="p">(</span><span class="n">release_gate</span><span class="o">.</span><span class="n">release_id</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">owner_dies</span><span class="p">(</span><span class="n">_seconds</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">redis</span><span class="o">.</span><span class="n">store</span><span class="o">.</span><span class="n">pop</span><span class="p">(</span><span class="n">lock_key</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span>                       <span class="c1"># ロックが期限切れになった</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">patch</span><span class="o">.</span><span class="n">object</span><span class="p">(</span><span class="n">release_gate</span><span class="p">,</span> <span class="s2">&#34;_sleep&#34;</span><span class="p">,</span> <span class="n">owner_dies</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">assertEqual</span><span class="p">(</span><span class="n">release_gate</span><span class="o">.</span><span class="n">claim</span><span class="p">(),</span> <span class="n">release_gate</span><span class="o">.</span><span class="n">PREPARE</span><span class="p">)</span></span></span></code></pre></div></div>

<h2 class="relative group">効果はあったか、半分は
    <div id="効果はあったか半分は" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%8a%b9%e6%9e%9c%e3%81%af%e3%81%82%e3%81%a3%e3%81%9f%e3%81%8b%e5%8d%8a%e5%88%86%e3%81%af" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>デプロイしたあと、マシンを止めてもう一度起動し、時計を見ました。起動処理は自分の判断を出力して、すぐに道を空けます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">15:23:34  Starting Granian (API server — HTTP/1.1 + HTTP/2)...
</span></span><span class="line"><span class="cl">15:23:36  release gate: this release is already prepared, starting straight away
</span></span><span class="line"><span class="cl">15:23:44  &#34;GET /api/health/ HTTP/1.1&#34; 200</span></span></code></pre></div></div>
<p>10秒、2台目は11秒でした。以前の25〜33秒に対して、窓の3分の2が消えたことになります。ゲート自体は Django を一切インポートしないので2秒で済みます。</p>
<p>しかし10秒はやはり10秒です。その最初の1秒に届いたリクエストは相変わらず失敗します。そこで残りを探しに行きました。</p>

<h2 class="relative group">誰もキャッシュしていなかったバイトコード
    <div id="誰もキャッシュしていなかったバイトコード" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%aa%b0%e3%82%82%e3%82%ad%e3%83%a3%e3%83%83%e3%82%b7%e3%83%a5%e3%81%97%e3%81%a6%e3%81%84%e3%81%aa%e3%81%8b%e3%81%a3%e3%81%9f%e3%83%90%e3%82%a4%e3%83%88%e3%82%b3%e3%83%bc%e3%83%89" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>このイメージには、世の中のほぼすべての Python Dockerfile に書かれているあの一行があります。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="k">ENV</span> <span class="nv">PYTHONDONTWRITEBYTECODE</span><span class="o">=</span><span class="m">1</span></span></span></code></pre></div></div>
<p>良い助言です。コンテナがランタイムにレイヤーへ <code>.pyc</code> を書き散らすべきではありません。私が考え抜いていなかったのは、この取引のもう半分です。ランタイムがバイトコードを決して書かず、ビルドも書かないなら、何一つキャッシュされず、プロセスが起動するたびに依存関係ツリー全体をソースからコンパイルすることになります。</p>
<p>数えてみました。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">site-packages .py  ファイル : 5,707
</span></span><span class="line"><span class="cl">site-packages .pyc ファイル : 0</span></span></code></pre></div></div>
<p>Django も DRF もすべてのライブラリも、1日92回の起動のたびに再コンパイルされていました。本番マシンでの計測値です。</p>
<table>
	<thead>
			<tr>
					<th><code>django.setup()</code></th>
					<th>時間</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>出荷時のまま</td>
					<td>5.15秒</td>
			</tr>
			<tr>
					<td><code>compileall</code> 後</td>
					<td>3.17秒</td>
			</tr>
	</tbody>
</table>
<p>修正は一行で、<code>compileall</code> は明示的に書き込むので上の環境変数があっても動きます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="k">RUN</span> python -m compileall -q /app/.venv/lib /app/koreapost_project /app/marketplace /app/utils <span class="o">||</span> true</span></span></code></pre></div></div>
<p>ビルド時間16秒、一度きり、キャッシュされるレイヤーで。</p>
<p>ついでにもっと静かな問題も見つけました。<code>.dockerignore</code> にはこう書いてありました。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">__pycache__/
</span></span><span class="line"><span class="cl">*.pyc</span></span></code></pre></div></div>
<p>Docker はこれらをパスの各要素ではなく、相対パス<strong>全体</strong>に対して照合します。したがってアンカーされていないパターンはビルドコンテキストのルートでしか一致しません。ネストした <code>__pycache__</code> はすべてイメージに載っていました。ノートパソコンの Python 3.15 がコンパイルした <code>.pyc</code> が218個、インタプリタが3.14のイメージの中に紛れ込み、すべて静かに無視されていたのです。パターンは <code>**/__pycache__/</code> と <code>**/*.pyc</code> であるべきでした。</p>
<p>正直な結果: 全体では2秒ではなく1秒ほどの改善でした。10秒が9秒になりました。隔離されたベンチマークは、いつもどおり過大に見せていたわけです。</p>

<h2 class="relative group">サスペンドと、それを塞いでいた一行
    <div id="サスペンドとそれを塞いでいた一行" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%b5%e3%82%b9%e3%83%9a%e3%83%b3%e3%83%89%e3%81%a8%e3%81%9d%e3%82%8c%e3%82%92%e5%a1%9e%e3%81%84%e3%81%a7%e3%81%84%e3%81%9f%e4%b8%80%e8%a1%8c" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>9秒まで来て、取り除ける作業が尽きました。残っていたのは避けられない起動1回分です。Python が立ち上がり、Django がインポートされ、WSGI アプリが共有コアの上で起き上がる時間。</p>
<p>ならば起動しなければいい。Fly はマシンをシャットダウンする代わりにメモリをスナップショットできます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">http_service</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="nx">auto_stop_machines</span> <span class="p">=</span> <span class="s1">&#39;suspend&#39;</span>   <span class="c"># 以前は &#39;stop&#39;</span></span></span></code></pre></div></div>
<p>試そうとしたら Fly に拒否され、今回の作業でいちばん役に立つエラーメッセージを受け取りました。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">failed to suspend VM: failed_precondition:
</span></span><span class="line"><span class="cl">Machines with swap cannot be suspended</span></span></code></pre></div></div>
<p>私の設定の冒頭には、自分で書いて自分で信じていたコメントとともに、これがありました。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="c"># メモリスパイクが OOM kill ではなくディスクへスワップされるためのクッション。</span>
</span></span><span class="line"><span class="cl"><span class="nx">swap_size_mb</span> <span class="p">=</span> <span class="mi">2048</span></span></span></code></pre></div></div>
<p>もっともな予防策です。実際に働いていたのか。全マシンの答えはこうでした。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">MemTotal:   985220 kB
</span></span><span class="line"><span class="cl">SwapTotal: 2097148 kB
</span></span><span class="line"><span class="cl">SwapFree:  2097148 kB</span></span></code></pre></div></div>
<p>1ページたりともスワップされたことがありません。その日のログにも OOM kill はなく、&ldquo;oom&rdquo; に一致した11行は、コンテンツハッシュにたまたまその3文字を含む JavaScript バンドルへのリクエストでした。そして本当のメモリガードはまったく別の場所、サーバーの起動コマンドにありました。<code>--workers-max-rss 800</code> が、1GB の天井に届くはるか手前でワーカーを再生成していたのです。</p>
<p>つまりこのクッションは、一度も起きたことのない事象への保険であり、すでに別の仕組みが担保しており、その保険料がサスペンド機能でした。外しました。</p>
<table>
	<thead>
			<tr>
					<th>復帰の経路</th>
					<th>応答までの時間</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>今朝のコールドブート</td>
					<td>25〜33秒</td>
			</tr>
			<tr>
					<td>ゲートとバイトコード後のコールドブート</td>
					<td>9〜10秒</td>
			</tr>
			<tr>
					<td><strong>サスペンドからの復帰</strong></td>
					<td><strong>2.5〜3.1秒</strong></td>
			</tr>
	</tbody>
</table>
<p>3回試して、ばらつきは0.5秒以内。復帰直後にすべてのエンドポイントが250〜435ミリ秒で200を返しました。</p>

<h2 class="relative group">マシンが眠っている間、ソケットはどうなるのか
    <div id="マシンが眠っている間ソケットはどうなるのか" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%9e%e3%82%b7%e3%83%b3%e3%81%8c%e7%9c%a0%e3%81%a3%e3%81%a6%e3%81%84%e3%82%8b%e9%96%93%e3%82%bd%e3%82%b1%e3%83%83%e3%83%88%e3%81%af%e3%81%a9%e3%81%86%e3%81%aa%e3%82%8b%e3%81%ae%e3%81%8b" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>ろくなことになりませんし、直すためのフックもありません。Fly は VM を凍結します。プロセスにシグナルは届かず、降りていく途中で何も閉じられません。握っていた接続は目覚めたときもメモリに残り、数分前に相手が捨てたソケットを指しています。</p>
<p>これは降りるときに解く問題ではありません。上がってくるときに解く問題で、その大半は、すでに正しい判断をしてあったかどうかで決まります。</p>
<ul>
<li><strong>データベース接続はリクエストごとに開く</strong>（<code>conn_max_age = 0</code>）。古びるほど長生きする接続がありません。</li>
<li><strong>Redis キャッシュは例外を投げずに縮退する。</strong> ラッパーが接続エラーを捕まえてミスとして返します。</li>
</ul>
<p>2つ目が働く瞬間を実際に見られました。ある復帰で、予想どおりの失敗がログに出ました。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">redis.exceptions.ConnectionError: Error while reading from fly-...</span></span></code></pre></div></div>
<p>そしてそれが起きたリクエストは3.6秒で200を返しました。死んだキャッシュは遅いページであって、エラーページではないからです。以降の復帰では何も出ませんでした。</p>
<p>使っているキャッシュクライアントが死んだ接続で例外を投げるなら、サスペンドは復帰のたびに500の花火を打ち上げます。設定を切り替える<strong>前に</strong>確認してください。</p>

<h2 class="relative group">私に嘘をついたテスト
    <div id="私に嘘をついたテスト" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%a7%81%e3%81%ab%e5%98%98%e3%82%92%e3%81%a4%e3%81%84%e3%81%9f%e3%83%86%e3%82%b9%e3%83%88" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>最初のサスペンド計測は、この機能が大失敗だと告げました。502、30秒後、しかも2回続けて。</p>
<p>私は <code>fly-force-instance-id</code> でリクエストを特定のマシンに固定していました。インスタンス1台を負荷試験するには素晴らしいヘッダーですが、この用途には最悪です。インスタンスを強制すると、そのマシンを起こしてくれるはずのプロキシのロジックを迂回してしまうからです。Fly はちゃんと教えてくれていました。それをエラーではなく答えとして読んでいれば。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">machine was recently stopped and is unavailable to service request</span></span></code></pre></div></div>
<p>意味のある数字は、実トラフィックが通る経路から出てきます。エッジ経由で同時40リクエスト、ソフトリミット12を大きく超えさせ、プロキシがサスペンド中のマシンへスケールアウトせざるを得ない状況にしました。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">ステータスコード: 40 × 200
</span></span><span class="line"><span class="cl">最も遅いもの:     2.1秒</span></span></code></pre></div></div>
<p>教訓は2つ、そして高くつくのは2つ目です。計測しやすい経路ではなく、ユーザーが通る経路を測ること。そして、ツールが何かを拒否したら、その拒否文を読むこと。「Machines with swap cannot be suspended」という一文が、この午後の作業すべての突破口になりました。</p>

<h2 class="relative group">あなたのアプリで確認すべきこと
    <div id="あなたのアプリで確認すべきこと" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%81%82%e3%81%aa%e3%81%9f%e3%81%ae%e3%82%a2%e3%83%97%e3%83%aa%e3%81%a7%e7%a2%ba%e8%aa%8d%e3%81%99%e3%81%b9%e3%81%8d%e3%81%93%e3%81%a8" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>アイドルのインスタンスを停止するプラットフォームを使っているなら、次の問いに答えるのに私は半日かかりましたが、もっと早く確かめていれば、何週間も続いた静かな失敗を防げたはずです。</p>
<ol>
<li><strong>コールドスタートは何秒かかりますか。</strong> コンテナが走る時間ではなく、実際のリクエストに応答するまでの時間です。秒数で即答できないなら、必要になる前に測ってください。</li>
<li><strong>entrypoint が起動ごとに行っている処理のうち、デプロイに属するものは何ですか。</strong> マイグレーション、静的ファイル収集、キャッシュのウォームアップ、インデックス構築。1回ならどれも問題ありません。92回ならどれも高くつきます。</li>
<li><strong>バイトコードをキャッシュしているものはありますか。</strong> ビルドに <code>compileall</code> のない <code>PYTHONDONTWRITEBYTECODE</code> は、答えが「ノー」だという意味です。</li>
<li><strong>停止ではなくサスペンドにできますか。</strong> できないなら、何が邪魔していますか。私の場合は一度も使われなかったスワップでした。</li>
<li><strong>エッジには見えてオリジンには見えないエラーはありますか。</strong> 両者を比べてください。その差はレポートの癖ではなく、あいだで死んでいるリクエストであり、アプリケーションログは決してそれを見せてくれません。</li>
</ol>
<p>ちなみにヘルスチェックはその間ずっと緑でした。いつもそうです。ヘルスチェックはすでに立ち上がっているマシンに対して走るので、そのマシンが存在する前の30秒については何も報告できません。</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-09-09-scale-to-zero-504s/featured.jpg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>iOSアプリのデバッグ：Xcodeとシミュレータで普段やっていること</title>
      <link>https://jared.lynskey.co.nz/ja/posts/2026/2026-02-26-debugging-ios-mobile-apps/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ja/posts/2026/2026-02-26-debugging-ios-mobile-apps/</guid>
      <pubDate>Thu, 26 Feb 2026 00:00:00 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>iOS</category>
      <category>Xcode</category>
      <category>デバッグ</category>
      <category>Swift</category>
      <category>iOS Simulator</category>
      <category>モバイル開発</category>
      <description>条件付きブレークポイントからLLDB、Instruments、サニタイザーまで。iOSアプリが壊れたときに実際に使っている手順のメモです。</description>
      <content:encoded><![CDATA[<p>デバッグには本当に時間を取られます。誰でもそうだと思います。最近は自分のアプリのバグ追いでXcodeとシミュレータにこもる日が多く、そのたびに思うのは、Xcodeにはほとんど誰も触らない強力なデバッグ機能が山ほどある、ということです。この記事では、長年かけて落ち着いた自分のデバッグ手順のうち、実際に時間の節約になっている部分をまとめます。</p>

<h2 class="relative group">ブレークポイントは思っているより強力
    <div id="ブレークポイントは思っているより強力" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%96%e3%83%ac%e3%83%bc%e3%82%af%e3%83%9d%e3%82%a4%e3%83%b3%e3%83%88%e3%81%af%e6%80%9d%e3%81%a3%e3%81%a6%e3%81%84%e3%82%8b%e3%82%88%e3%82%8a%e5%bc%b7%e5%8a%9b" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>ほとんどの人は、ガターをクリックしてブレークポイントを置いて終わりです。でも右クリックしてみると、デバッグのやり方が変わるくらいのオプションが出てきます。</p>

<h3 class="relative group">条件付きブレークポイント
    <div id="条件付きブレークポイント" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%9d%a1%e4%bb%b6%e4%bb%98%e3%81%8d%e3%83%96%e3%83%ac%e3%83%bc%e3%82%af%e3%83%9d%e3%82%a4%e3%83%b3%e3%83%88" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>1000件を処理するループで、347番目だけがおかしいとします。346回もブレークポイントで止まるのは時間の無駄です。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="mf">0.</span><span class="p">.&lt;</span><span class="n">users</span><span class="p">.</span><span class="bp">count</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">processUser</span><span class="p">(</span><span class="n">users</span><span class="p">[</span><span class="n">i</span><span class="p">])</span>  <span class="c1">// ここにブレークポイントを設定、条件: i == 347</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>ブレークポイントを右クリックして条件に <code>i == 347</code> を入れれば、Xcodeは条件が真のときだけ止まります。<code>userId == &quot;abc123&quot;</code> や <code>error != nil</code> のような条件でも同じです。</p>

<h3 class="relative group">シンボリックブレークポイント
    <div id="シンボリックブレークポイント" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%b7%e3%83%b3%e3%83%9c%e3%83%aa%e3%83%83%e3%82%af%e3%83%96%e3%83%ac%e3%83%bc%e3%82%af%e3%83%9d%e3%82%a4%e3%83%b3%e3%83%88" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>メソッドがどこから呼ばれているのか分からないときに使います。ブレークポイントナビゲータ（Cmd+8）で「+」ボタンをクリックし、「Symbolic Breakpoint」を選びます。</p>
<p>どのビューコントローラがいつロードされるか全部見たければ、シンボルを <code>viewDidLoad</code> にします。Xcodeがすべての呼び出しで止まります。うるさすぎるなら <code>MyViewController.viewDidLoad</code> のように絞り込みましょう。</p>
<p>私が常設しているのはこの3つです。</p>
<ul>
<li><code>UIViewAlertForUnsatisfiableConstraints</code>：Auto Layoutの競合をその場で捕まえる</li>
<li><code>objc_exception_throw</code>：クラッシュする前にObjective-Cの例外で止まる</li>
<li><code>malloc_error_break</code>：メモリ割り当ての問題を見つける</li>
</ul>

<h3 class="relative group">例外ブレークポイント
    <div id="例外ブレークポイント" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%be%8b%e5%a4%96%e3%83%96%e3%83%ac%e3%83%bc%e3%82%af%e3%83%9d%e3%82%a4%e3%83%b3%e3%83%88" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>新しいプロジェクトで最初にやるべき設定だと思います。ブレークポイントナビゲータで「+」→「Exception Breakpoint」を追加しておくと、アプリが落ちた後ではなく、何かがスローされた瞬間にXcodeが止まってくれます。</p>
<p>コンソールに謎のクラッシュとしてしか出てこないnilアンラップの問題を、これで何度も捕まえてきました。</p>

<h3 class="relative group">ブレークポイントアクション
    <div id="ブレークポイントアクション" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%96%e3%83%ac%e3%83%bc%e3%82%af%e3%83%9d%e3%82%a4%e3%83%b3%e3%83%88%e3%82%a2%e3%82%af%e3%82%b7%e3%83%a7%e3%83%b3" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>ブレークポイントは実行を止めなくてもいいんです。ブレークポイントを編集して「Add Action」をクリックすると、こんなことができます。</p>
<ul>
<li>止めずに変数を出力する：<code>&quot;User count: @(users.count)@&quot;</code></li>
<li>LLDBコマンドを自動実行する</li>
<li>シェルスクリプトを実行する</li>
<li>音を鳴らす（めったに通らないコードパスの確認に使っています）</li>
</ul>
<p>「Automatically continue after evaluating actions」にチェックを入れれば、ブレークポイントはフローを止めないロガーになります。</p>

<h2 class="relative group">LLDB：実際に使うコンソールコマンド
    <div id="lldb実際に使うコンソールコマンド" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#lldb%e5%ae%9f%e9%9a%9b%e3%81%ab%e4%bd%bf%e3%81%86%e3%82%b3%e3%83%b3%e3%82%bd%e3%83%bc%e3%83%ab%e3%82%b3%e3%83%9e%e3%83%b3%e3%83%89" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>ブレークポイントで止まったとき、下のコンソールはクラッシュログを眺めるだけの場所ではありません。あれはLLDBで、本当に便利です。</p>

<h3 class="relative group">基本
    <div id="基本" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%9f%ba%e6%9c%ac" aria-label="アンカー">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) po user
▿ User
  - id: &#34;123&#34;
  - name: &#34;John Doe&#34;
  - email: &#34;john@example.com&#34;</code></pre></div>
<p><code>po</code> はオブジェクトを読みやすい形で出力します。私のLLDB利用の9割はこれです。</p>
<p>もう少し詳しく見たいときは <code>p</code> を使います。</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) p user.name
(String) $R0 = &#34;John Doe&#34;</code></pre></div>
<p>ローカル変数を一度に全部見るなら、こうです。</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) frame variable</code></pre></div>

<h3 class="relative group">実行中に値を変える
    <div id="実行中に値を変える" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%ae%9f%e8%a1%8c%e4%b8%ad%e3%81%ab%e5%80%a4%e3%82%92%e5%a4%89%e3%81%88%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>LLDBの本領はここです。再コンパイルせずに変数を書き換えられます。</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) expr user.name = &#34;Jane Doe&#34;
(lldb) expr index = 0</code></pre></div>
<p>バグを見つけて、リビルドせずに修正を試したい。そんなときは変数を変えてそのまま続行すればいいわけです。エッジケースを絞り込むとき、いつもこれで値を差し替えています。</p>
<p>メソッドの呼び出しもできます。</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) po self.refreshUI()
(lldb) expr navigationController?.popViewController(animated: true)</code></pre></div>

<h3 class="relative group">ナビゲーション
    <div id="ナビゲーション" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%8a%e3%83%93%e3%82%b2%e3%83%bc%e3%82%b7%e3%83%a7%e3%83%b3" aria-label="アンカー">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) bt              # コールスタックを表示
(lldb) frame select 3  # 別のスタックフレームにジャンプ
(lldb) continue        # 実行を続ける（または単に &#39;c&#39;）
(lldb) n               # ステップオーバー（次の行）
(lldb) s               # 関数にステップイン</code></pre></div>

<h3 class="relative group">ウォッチポイント
    <div id="ウォッチポイント" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%a6%e3%82%a9%e3%83%83%e3%83%81%e3%83%9d%e3%82%a4%e3%83%b3%e3%83%88" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>変数がいつ変わるのか知りたければウォッチポイントです。</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) watchpoint set variable user.isLoggedIn</code></pre></div>
<p>コードのどこから変更されても、その瞬間に実行が止まります。想定外の状態変更を追うときには手放せません。</p>

<h2 class="relative group">Console.app：隠れたデバッグツール
    <div id="consoleapp隠れたデバッグツール" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#consoleapp%e9%9a%a0%e3%82%8c%e3%81%9f%e3%83%87%e3%83%90%e3%83%83%e3%82%b0%e3%83%84%e3%83%bc%e3%83%ab" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>Xcodeのコンソールで十分な場面は多いですが、システムログもクラッシュレポートも全部見たい、となったらConsole.appの出番です。</p>
<p>OSLogを使っていれば（使うべきです）、Console.appでログを検索したりフィルタしたりできます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">import</span> <span class="nc">os</span><span class="p">.</span><span class="nc">log</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">logger</span> <span class="p">=</span> <span class="n">Logger</span><span class="p">(</span><span class="n">subsystem</span><span class="p">:</span> <span class="s">&#34;com.example.app&#34;</span><span class="p">,</span> <span class="n">category</span><span class="p">:</span> <span class="s">&#34;networking&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">&#34;Starting API request&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="p">.</span><span class="n">debug</span><span class="p">(</span><span class="s">&#34;Request URL: </span><span class="si">\(</span><span class="n">url</span><span class="p">.</span><span class="n">absoluteString</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="p">.</span><span class="n">error</span><span class="p">(</span><span class="s">&#34;Failed to decode: </span><span class="si">\(</span><span class="n">error</span><span class="p">.</span><span class="n">localizedDescription</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>Console.app側の手順はこうです。</p>
<ol>
<li>シミュレータか接続中のデバイスを選ぶ</li>
<li>サブシステムで絞る：<code>subsystem:com.example.app</code></li>
<li>レベルで絞る：<code>subsystem:com.example.app AND level:error</code></li>
</ol>
<p>よく使うフィルタ述語は保存できます。私はネットワークリクエスト用、データベース操作用、エラーと警告だけを出す用の3つを持っています。</p>

<h3 class="relative group">print()ではなくOSLogを使う理由
    <div id="printではなくoslogを使う理由" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#print%e3%81%a7%e3%81%af%e3%81%aa%e3%81%8foslog%e3%82%92%e4%bd%bf%e3%81%86%e7%90%86%e7%94%b1" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>OSLogは、<code>print()</code> をあちこちに撒くよりずっとましです。構造化されているのでレベルやカテゴリで絞り込めますし、ログ自体が最適化されていてアプリを遅くしません。本番では機密データを自動でマスクしてくれて、アプリが落ちた後もログは残ります。</p>
<p>とっさの確認には <code>print()</code> で構いません。後で見返す可能性があるものはOSLogに残しましょう。</p>

<h3 class="relative group">Console.appの高度なフィルタリング
    <div id="consoleappの高度なフィルタリング" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#consoleapp%e3%81%ae%e9%ab%98%e5%ba%a6%e3%81%aa%e3%83%95%e3%82%a3%e3%83%ab%e3%82%bf%e3%83%aa%e3%83%b3%e3%82%b0" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>Console.appはかなり凝った述語クエリに対応しています。私がよく使うのはこのあたりです。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># アプリ内のすべてのエラーと障害
</span></span><span class="line"><span class="cl">subsystem:com.example.app AND (level:error OR level:fault)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># 失敗したネットワークリクエスト
</span></span><span class="line"><span class="cl">subsystem:com.example.app AND category:networking AND eventMessage CONTAINS &#34;failed&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># 過去5分間のすべて
</span></span><span class="line"><span class="cl">subsystem:com.example.app AND timestamp &gt;= now(-5m)</span></span></code></pre></div></div>
<p>ログはエクスポートしてバグレポートに添付することもできます。Xcodeのコンソールからコピペするよりずっと楽です。</p>

<h2 class="relative group">ビューデバッグ
    <div id="ビューデバッグ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%93%e3%83%a5%e3%83%bc%e3%83%87%e3%83%90%e3%83%83%e3%82%b0" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>UIが壊れているのに原因が分からないとき、いちばん早いのはビューデバッガです。</p>
<p>アプリを実行して壊れた画面まで進み、Xcodeのデバッグバーで「Debug View Hierarchy」をクリックします（またはCmd+Shift+D）。ビュー階層全体が3Dで分解表示されます。</p>
<p>回してみると、いろいろ見えてきます。</p>
<ul>
<li>他のビューの裏でレンダリングされているビュー</li>
<li>画面のはるか外にいるビュー</li>
<li>サイズがゼロのビュー</li>
<li>選択したビューの制約チェーン全体</li>
</ul>
<p>私はこれで、タップイベントを吸い込んでいた透明なボタンや、0x0でレンダリングされていたラベル、いつの間にか幅10,000ピクセルになっていた画像を見つけたことがあります。</p>

<h3 class="relative group">Auto Layoutのデバッグ
    <div id="auto-layoutのデバッグ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#auto-layout%e3%81%ae%e3%83%87%e3%83%90%e3%83%83%e3%82%b0" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>ビュー階層に紫の警告アイコンが出ていたら制約の競合です。クリックすると、どの制約同士がぶつかっているのかXcodeが正確に教えてくれます。</p>
<p>制約には識別子を付けておきましょう。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">heightConstraint</span><span class="p">.</span><span class="n">identifier</span> <span class="p">=</span> <span class="s">&#34;ProfileImageHeight&#34;</span></span></span></code></pre></div></div>
<p>制約が壊れたとき、エラーにメモリアドレスではなく <code>&quot;ProfileImageHeight&quot;</code> が出るようになります。ログの読みやすさが段違いです。</p>
<p>LLDBから制約ツリーを出力することもできます。</p>
<div class="highlight-wrapper"><pre tabindex="0"><code class="language-lldb" data-lang="lldb">(lldb) po view.hasAmbiguousLayout
(lldb) po view._autolayoutTrace()</code></pre></div>

<h2 class="relative group">時間を節約してくれるシミュレータ機能
    <div id="時間を節約してくれるシミュレータ機能" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%99%82%e9%96%93%e3%82%92%e7%af%80%e7%b4%84%e3%81%97%e3%81%a6%e3%81%8f%e3%82%8c%e3%82%8b%e3%82%b7%e3%83%9f%e3%83%a5%e3%83%ac%e3%83%bc%e3%82%bf%e6%a9%9f%e8%83%bd" aria-label="アンカー">#</a>
    </span>
    
</h2>

<h3 class="relative group">スローアニメーション
    <div id="スローアニメーション" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%b9%e3%83%ad%e3%83%bc%e3%82%a2%e3%83%8b%e3%83%a1%e3%83%bc%e3%82%b7%e3%83%a7%e3%83%b3" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>Debug → Slow Animations で、すべてが1/10の速度になります。トランジション中に何が起きているのか、アニメーションがなぜ変に見えるのかを確かめるのにちょうどいいです。</p>

<h3 class="relative group">メモリ警告のシミュレート
    <div id="メモリ警告のシミュレート" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%a1%e3%83%a2%e3%83%aa%e8%ad%a6%e5%91%8a%e3%81%ae%e3%82%b7%e3%83%9f%e3%83%a5%e3%83%ac%e3%83%bc%e3%83%88" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>Debug → Simulate Memory Warning。メモリ不足のときのアプリの挙動をテストできます。これで画像キャッシュのバグをいくつも見つけました。メモリ警告が来るまでは普通に動いているのに、来た途端に画像が全部消えるやつです。</p>

<h3 class="relative group">Network Link Conditioner
    <div id="network-link-conditioner" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#network-link-conditioner" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>Xcode → Open Developer Tools → Network Link Conditioner</p>
<p>3GやLTE、高パケットロス、完全オフラインをシミュレートできます。WiFiでは平気なAPIリクエストがモバイル回線でタイムアウトする、という経験があるなら、原因はこれで再現できます。</p>
<p>私は遅延500ms・パケットロス10%の「Bad Network」プロファイルを常備しています。速いオフィスWiFiでは絶対に出ない、タイムアウトのバグやローディング表示の問題が引っかかります。</p>

<h3 class="relative group">ステータスバーのオーバーライド
    <div id="ステータスバーのオーバーライド" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%b9%e3%83%86%e3%83%bc%e3%82%bf%e3%82%b9%e3%83%90%e3%83%bc%e3%81%ae%e3%82%aa%e3%83%bc%e3%83%90%e3%83%bc%e3%83%a9%e3%82%a4%e3%83%89" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>シミュレータのステータスバーを右クリックすると、時刻、バッテリー残量、電波強度、キャリアを上書きできます。スクリーンショットを揃えたいときや、バッテリー残量ごとのUI確認に便利です。</p>

<h3 class="relative group">位置情報のシミュレーション
    <div id="位置情報のシミュレーション" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%bd%8d%e7%bd%ae%e6%83%85%e5%a0%b1%e3%81%ae%e3%82%b7%e3%83%9f%e3%83%a5%e3%83%ac%e3%83%bc%e3%82%b7%e3%83%a7%e3%83%b3" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>Debug → Location で、机から動かずにいろいろな場所を再現できます。任意の座標、街歩き、高速道路の走行まで一通り揃っています。位置情報を使う機能のテストや、特定の地域でしか起きない不具合の調査で重宝します。</p>

<h2 class="relative group">メモリデバッグ
    <div id="メモリデバッグ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%a1%e3%83%a2%e3%83%aa%e3%83%87%e3%83%90%e3%83%83%e3%82%b0" aria-label="アンカー">#</a>
    </span>
    
</h2>

<h3 class="relative group">Debug Memory Graph
    <div id="debug-memory-graph" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#debug-memory-graph" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>Debug Memory Graphボタン（Cmd+Shift+M）を押すと、メモリ上のすべてのオブジェクトとその参照関係、そして肝心の「どれがリークしているか」をXcodeが見せてくれます。</p>
<p>紫のビックリマークがリークです。クリックすると保持サイクルが見えます。</p>
<p>いちばんよく見かけるリークはこれです。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">ViewController</span><span class="p">:</span> <span class="n">UIViewController</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nv">onComplete</span><span class="p">:</span> <span class="p">(()</span> <span class="p">-&gt;</span> <span class="nb">Void</span><span class="p">)?</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">func</span> <span class="nf">setupHandler</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">onComplete</span> <span class="p">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="kc">self</span><span class="p">.</span><span class="n">dismiss</span><span class="p">(</span><span class="n">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span>  <span class="c1">// ❌ selfを強く参照</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>weak selfで直します。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">onComplete</span> <span class="p">=</span> <span class="p">{</span> <span class="p">[</span><span class="kr">weak</span> <span class="kc">self</span><span class="p">]</span> <span class="k">in</span>
</span></span><span class="line"><span class="cl">    <span class="kc">self</span><span class="p">?.</span><span class="n">dismiss</span><span class="p">(</span><span class="n">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span>  <span class="c1">// ✓ 保持サイクルなし</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>デリゲートもweakにしておきます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kr">weak</span> <span class="kd">var</span> <span class="nv">delegate</span><span class="p">:</span> <span class="n">ManagerDelegate</span><span class="p">?</span>  <span class="c1">// 単なる &#39;var&#39; ではなく</span></span></span></code></pre></div></div>

<h3 class="relative group">Instruments
    <div id="instruments" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#instruments" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>本腰を入れてメモリを調べるならInstrumentsです（Product → Profile、またはCmd+I）。</p>
<p>Allocationsを使うと、オブジェクトの割り当てすべて、時間経過に伴うメモリの増え方、どのクラスがメモリを食っているか、割り当てが起きた場所のスタックトレースまで見えます。</p>
<p>私が探すのはだいたい、時間に比例して増え続けるメモリ（ほぼリークです）、想定外に大きい割り当て（画像1000枚をうっかり一気に読んでいないか？）、解放されているはずなのに残っているオブジェクト、この3つです。</p>
<p>操作の前後で世代をマークして（小さな旗のボタン）、残ってはいけないものが残っていないか確認しましょう。</p>

<h2 class="relative group">パフォーマンスプロファイリング
    <div id="パフォーマンスプロファイリング" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%91%e3%83%95%e3%82%a9%e3%83%bc%e3%83%9e%e3%83%b3%e3%82%b9%e3%83%97%e3%83%ad%e3%83%95%e3%82%a1%e3%82%a4%e3%83%aa%e3%83%b3%e3%82%b0" aria-label="アンカー">#</a>
    </span>
    
</h2>

<h3 class="relative group">Time Profiler
    <div id="time-profiler" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#time-profiler" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>Product → Profile → Time Profiler。アプリで重い操作をしながら記録して、コールツリーを読みます。</p>
<p>「Self Weight」でソートするとボトルネックが見つかります。ある関数のself weightが40%なら、時間はそこに消えています。</p>
<p>以前、JSONのパース関数が毎秒1000回呼ばれているのを見つけたことがあります。バックグラウンドキューに移したら、UIのカクつきが止まりました。</p>

<h3 class="relative group">Main Thread Checker
    <div id="main-thread-checker" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#main-thread-checker" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>バックグラウンドスレッドでのUI更新は、Xcodeが自動で捕まえてくれます。こんな表示が出たら、</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Main Thread Checker: UI API called on a background thread: -[UILabel setText:]</span></span></code></pre></div></div>
<p>こういうことをやってしまっています。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">URLSession</span><span class="p">.</span><span class="n">shared</span><span class="p">.</span><span class="n">dataTask</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">url</span><span class="p">)</span> <span class="p">{</span> <span class="n">data</span><span class="p">,</span> <span class="n">response</span><span class="p">,</span> <span class="n">error</span> <span class="k">in</span>
</span></span><span class="line"><span class="cl">    <span class="kc">self</span><span class="p">.</span><span class="n">label</span><span class="p">.</span><span class="n">text</span> <span class="p">=</span> <span class="s">&#34;Loaded&#34;</span>  <span class="c1">// ❌ クラッシュ！</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>直すとこうなります。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">URLSession</span><span class="p">.</span><span class="n">shared</span><span class="p">.</span><span class="n">dataTask</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">url</span><span class="p">)</span> <span class="p">{</span> <span class="n">data</span><span class="p">,</span> <span class="n">response</span><span class="p">,</span> <span class="n">error</span> <span class="k">in</span>
</span></span><span class="line"><span class="cl">    <span class="n">DispatchQueue</span><span class="p">.</span><span class="n">main</span><span class="p">.</span><span class="n">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kc">self</span><span class="p">.</span><span class="n">label</span><span class="p">.</span><span class="n">text</span> <span class="p">=</span> <span class="s">&#34;Loaded&#34;</span>  <span class="c1">// ✓</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>

<h2 class="relative group">ランタイム診断
    <div id="ランタイム診断" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%a9%e3%83%b3%e3%82%bf%e3%82%a4%e3%83%a0%e8%a8%ba%e6%96%ad" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>Edit Scheme → Run → Diagnostics。ここに並んでいるチェックボックスは、手作業では絶対に見つからないバグを捕まえてくれます。</p>

<h3 class="relative group">Address Sanitizer
    <div id="address-sanitizer" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#address-sanitizer" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>メモリ破壊を見つけます。use-after-free、バッファオーバーフロー、メモリリーク。アプリは2〜3倍遅くなりますが、他の方法ではほぼ追跡不可能なバグを捕まえてくれます。たまにしか起きないクラッシュを追うときにオンにします。</p>

<h3 class="relative group">Thread Sanitizer
    <div id="thread-sanitizer" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#thread-sanitizer" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>データ競合とスレッド絡みの問題を捕まえます。有効にしてアプリを普通に触っているだけで、競合状態があればThread Sanitizerが見つけてくれます。</p>
<p>Address SanitizerとThread Sanitizerは同時に使えないので、妙なクラッシュを追うときは交互に切り替えています。</p>

<h3 class="relative group">Zombie Objects
    <div id="zombie-objects" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#zombie-objects" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>解放済みオブジェクトへのメッセージを捕まえます。有効にしておくと、解放されたメモリに触った瞬間をXcodeが正確に教えてくれます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">*** -[MyViewController viewDidLoad]: message sent to deallocated instance 0x600001234000</span></span></code></pre></div></div>
<p>つけっぱなしは禁物です。オブジェクトが解放されなくなるので、メモリは増える一方になります。ただ、特定のuse-after-freeバグを突き止めるには最高です。</p>

<h2 class="relative group">よくあるデバッグシナリオ
    <div id="よくあるデバッグシナリオ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%88%e3%81%8f%e3%81%82%e3%82%8b%e3%83%87%e3%83%90%e3%83%83%e3%82%b0%e3%82%b7%e3%83%8a%e3%83%aa%e3%82%aa" aria-label="アンカー">#</a>
    </span>
    
</h2>

<h3 class="relative group">起動時にクラッシュする
    <div id="起動時にクラッシュする" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%b5%b7%e5%8b%95%e6%99%82%e3%81%ab%e3%82%af%e3%83%a9%e3%83%83%e3%82%b7%e3%83%a5%e3%81%99%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>まず例外ブレークポイントを追加します。たいていはこれで捕まります。</p>
<p>だめなら、Info.plistのキー漏れ、初期化コードでの強制アンラップ、依存性注入の失敗あたりを疑います。</p>

<h3 class="relative group">UIが更新されない
    <div id="uiが更新されない" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#ui%e3%81%8c%e6%9b%b4%e6%96%b0%e3%81%95%e3%82%8c%e3%81%aa%e3%81%84" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>毎回、原因はこのどれかです。</p>
<ol>
<li>更新がメインスレッドで行われていない</li>
<li>アウトレットがつながっていない</li>
<li>ビューがそもそも表示されていない（LLDBで <code>po view.window</code> を確認。nilなら階層に入っていません）</li>
</ol>
<p>テーブルビューやコレクションビューなら、<code>reloadData()</code> の呼び忘れです。</p>

<h3 class="relative group">メモリ警告でアプリが落ちる
    <div id="メモリ警告でアプリが落ちる" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%a1%e3%83%a2%e3%83%aa%e8%ad%a6%e5%91%8a%e3%81%a7%e3%82%a2%e3%83%97%e3%83%aa%e3%81%8c%e8%90%bd%e3%81%a1%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>Instruments Allocationsで何がメモリを掴んでいるか確認します。たいていは、キャッシュから解放されない画像、解放されないビューコントローラ（保持サイクル）、際限なく育つ配列や辞書のどれかです。</p>
<p>シミュレータでメモリ警告をシミュレートして、クリーンアップ処理をテストしておきましょう。</p>

<h3 class="relative group">原因不明のクラッシュ
    <div id="原因不明のクラッシュ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%8e%9f%e5%9b%a0%e4%b8%8d%e6%98%8e%e3%81%ae%e3%82%af%e3%83%a9%e3%83%83%e3%82%b7%e3%83%a5" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>サニタイザーを全部オンにします。間欠的ならたぶんスレッドの問題なのでThread Sanitizerで、システムフレームワーク内で落ちているならたぶんメモリ破壊なのでAddress Sanitizerで走らせます。</p>

<h3 class="relative group">シミュレータでOTA更新をデバッグする
    <div id="シミュレータでota更新をデバッグする" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%b7%e3%83%9f%e3%83%a5%e3%83%ac%e3%83%bc%e3%82%bf%e3%81%a7ota%e6%9b%b4%e6%96%b0%e3%82%92%e3%83%87%e3%83%90%e3%83%83%e3%82%b0%e3%81%99%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>私のアプリはOTA（Over-the-Air）アップデートを配信しているのですが、このフロー全体のデバッグにはシミュレータがいちばん手軽です。</p>
<p>まずネットワークトラフィックを見ます。アプリのサブシステムで絞ったConsole.appに、マニフェストのリクエストとレスポンスがリアルタイムで流れてくるので、どのヘッダーを送っていてサーバーが何を返しているかが正確に分かります。</p>
<p>次に、わざとネットワークを悪くします。Network Link Conditionerで、遅い回線やパケットロスのときに更新フローがどう振る舞うかを確かめます。アプリは固まらないか。ローディング表示は出るか。ちゃんとフォールバックするか。</p>
<p>更新チェックの周辺にはログを仕込みます。チェック開始時、更新があったとき、ダウンロード開始時、新しいバンドルのロード時、それぞれで記録しています。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">import</span> <span class="nc">os</span><span class="p">.</span><span class="nc">log</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">logger</span> <span class="p">=</span> <span class="n">Logger</span><span class="p">(</span><span class="n">subsystem</span><span class="p">:</span> <span class="s">&#34;com.example.app&#34;</span><span class="p">,</span> <span class="n">category</span><span class="p">:</span> <span class="s">&#34;updates&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">&#34;Checking for updates...&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">update</span> <span class="p">=</span> <span class="k">try</span> <span class="n">await</span> <span class="n">Updates</span><span class="p">.</span><span class="n">checkForUpdateAsync</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">logger</span><span class="p">.</span><span class="n">info</span><span class="p">(</span><span class="s">&#34;Update available: </span><span class="si">\(</span><span class="n">update</span><span class="p">.</span><span class="n">isAvailable</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>最後に、マニフェストのエンドポイント単体もテストしておくと安心です。アプリが送るのと同じヘッダーでcurlできます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -H <span class="s2">&#34;expo-protocol-version: 1&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">     -H <span class="s2">&#34;expo-platform: ios&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">     -H <span class="s2">&#34;expo-runtime-version: 1.0.0&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">     https://your-server.com/api/expo-updates/manifest/</span></span></code></pre></div></div>
<p>クライアント側を掘り始める前に、サーバーが正しいデータを返しているかをここで確認できます。</p>

<h2 class="relative group">サードパーティツール
    <div id="サードパーティツール" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%b5%e3%83%bc%e3%83%89%e3%83%91%e3%83%bc%e3%83%86%e3%82%a3%e3%83%84%e3%83%bc%e3%83%ab" aria-label="アンカー">#</a>
    </span>
    
</h2>

<h3 class="relative group">Charles Proxy / Proxyman
    <div id="charles-proxy--proxyman" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#charles-proxy--proxyman" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>ネットワークトラフィックの全表示、リクエストとレスポンスの改変、エラー条件のテスト。APIデバッグには欠かせません。</p>
<p>ProxymanはCharlesよりUIがよく、macOSに馴染む感じがします。去年乗り換えて、戻る気はありません。</p>

<h3 class="relative group">Reveal
    <div id="reveal" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#reveal" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>Xcodeのビューデバッガに似ていますが、もっと強力です。Xcodeにつながずに実機で動くので、テスターの端末でレイアウト問題を追うのに向いています。</p>

<h2 class="relative group">実機でのデバッグ
    <div id="実機でのデバッグ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%ae%9f%e6%a9%9f%e3%81%a7%e3%81%ae%e3%83%87%e3%83%90%e3%83%83%e3%82%b0" aria-label="アンカー">#</a>
    </span>
    
</h2>

<h3 class="relative group">ワイヤレスデバッグ
    <div id="ワイヤレスデバッグ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%af%e3%82%a4%e3%83%a4%e3%83%ac%e3%82%b9%e3%83%87%e3%83%90%e3%83%83%e3%82%b0" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>一度だけUSBでつなぎ、Window → Devices and Simulators で「Connect via network」にチェックを入れて、ケーブルを抜きます。同じWiFiにいる限り、デバイスはXcodeから見え続けます。</p>

<h3 class="relative group">Device Console
    <div id="device-console" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#device-console" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>Window → Devices and Simulators → Open Console。システムログ、アプリログ、クラッシュレポートと、何でも見えます。Xcode側のコンソールよりずっと詳細です。</p>
<p>テスターから「アプリが落ちた」と言われたら、必ずデバイスをつないでコンソールログを取らせてもらいます。すべてを説明してくれるシステムレベルのエラーが残っていることが多いんです。</p>

<h2 class="relative group">実際のデバッグ手順
    <div id="実際のデバッグ手順" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%ae%9f%e9%9a%9b%e3%81%ae%e3%83%87%e3%83%90%e3%83%83%e3%82%b0%e6%89%8b%e9%a0%86" aria-label="アンカー">#</a>
    </span>
    
</h2>
<ol>
<li>例外ブレークポイントがなければ追加する</li>
<li>バグを再現する</li>
<li>怪しい場所の近くにブレークポイントを置く</li>
<li>LLDBの <code>po</code> で値を確認する</li>
<li>変数を書き換えて、再コンパイルせずに修正を試す</li>
<li>UI絡みならビュー階層を見る</li>
<li>パフォーマンス絡みならInstrumentsでプロファイルする</li>
<li>メモリかスレッド絡みならサニタイザーをオンにする</li>
</ol>
<p>コツは症状から逆算することです。クラッシュ？例外ブレークポイント。遅い？Time Profiler。メモリ？Instruments。UIが変？ビューデバッガ。</p>

<h2 class="relative group">もっと早く知りたかったこと
    <div id="もっと早く知りたかったこと" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%82%e3%81%a3%e3%81%a8%e6%97%a9%e3%81%8f%e7%9f%a5%e3%82%8a%e3%81%9f%e3%81%8b%e3%81%a3%e3%81%9f%e3%81%93%e3%81%a8" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>アサーションを使いましょう。ロジックの誤りがバグになる前に捕まります。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="bp">assert</span><span class="p">(</span><span class="n">users</span><span class="p">.</span><span class="bp">count</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">,</span> <span class="s">&#34;Users array should never be empty here&#34;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>デバッグを始める前にコミットしておくこと。仮説を試すためにあちこち書き換え始めると、必ず戻したくなります。</p>
<p>終わったらprint文は消すこと。少なくとも <code>#if DEBUG</code> で囲んでおきましょう。古いデバッグログは、次のバグを見えにくくします。</p>
<p>LLDBはちゃんと覚えること。アプリを20回リビルドするより速いです。</p>
<p>そして、シミュレータは本物の電話ではありません。実機でしか出ないバグは、ほぼ例外なくスレッドかメモリの問題です。あるいはパフォーマンスの問題。シミュレータはiPhoneのチップではなくMacのCPUで動いていますから。</p>

<h2 class="relative group">常に開いているツール
    <div id="常に開いているツール" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%b8%b8%e3%81%ab%e9%96%8b%e3%81%84%e3%81%a6%e3%81%84%e3%82%8b%e3%83%84%e3%83%bc%e3%83%ab" aria-label="アンカー">#</a>
    </span>
    
</h2>
<ul>
<li>Xcode（当然）</li>
<li>アプリのサブシステムで絞ったConsole.app</li>
<li>ネットワークデバッグ用のProxyman</li>
<li>事態が深刻になってきたらInstruments</li>
</ul>
<hr>
<p>どのツールも万能ではありません。ビューデバッガはメモリリークを見つけてくれませんし、InstrumentsはAuto Layoutの競合を解いてくれません。結局のところ、腕の見せどころは症状に合ったツールを選ぶことで、それは回数をこなすうちに身につきます。保持サイクルを何度も踏めば、スタックトレースを見た瞬間に匂いで分かるようになります。デバッグが楽しくなることはたぶんないですが、少なくとも苦行ではなくなります。</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-02-26-debugging-ios-mobile-apps/featured.jpg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
