<?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>Offline on Tarragon</title><link>https://tarrragon.github.io/blog/tags/offline/</link><description>Recent content in Offline on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 26 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/offline/index.xml" rel="self" type="application/rss+xml"/><item><title>斷網環境的 infra：沒有網路時怎麼做</title><link>https://tarrragon.github.io/blog/infra/air-gapped/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/air-gapped/</guid><description>&lt;p>斷網環境（air-gapped）是跟網際網路完全隔離的執行環境——沒有 &lt;code>apt install&lt;/code>、沒有 &lt;code>terraform init&lt;/code> 自動下載 provider、沒有 Docker Hub 可以 pull image、沒有 GitHub Actions 可以跑 CI。這個約束不改變 infra 的原則（可重建、可追蹤、可審查），但改變了幾乎所有工具的使用方式。&lt;/p>
&lt;p>常見的斷網情境：政府或軍事機密網路（實體隔離）、工控與 OT 環境（工廠、電廠、SCADA）、金融交易系統的高安全隔離區、醫療設備網路、以及地端機房裡刻意不開 internet access 的 private zone。&lt;/p>
&lt;p>這個模組是橫切約束——它影響&lt;a href="https://tarrragon.github.io/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一（IaC 選型）&lt;/a>到&lt;a href="https://tarrragon.github.io/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七（PR 流程）&lt;/a>的每一個操作步驟。每篇文章處理一個被斷網影響的主要面向。&lt;/p>
&lt;h2 id="章節文章">章節文章&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文章&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-principles/" data-link-title="斷網環境的通用原則" data-link-desc="離線套件管理、內容搬運、變更追蹤的共通操作模式 — 所有斷網情境都要先建立的基礎能力">斷網環境的通用原則&lt;/a>&lt;/td>
 &lt;td>離線套件管理、內容搬運、變更追蹤的共通操作模式&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-iac/" data-link-title="斷網環境的 IaC" data-link-desc="Terraform provider mirror、離線 plugin cache、本地 state backend、沒有雲端時的 plan/apply 流程與內網 CI">斷網環境的 IaC&lt;/a>&lt;/td>
 &lt;td>Terraform provider mirror、離線 state backend、plan/apply 流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-container/" data-link-title="斷網環境的容器與映像管理" data-link-desc="Private registry 架設、映像搬運（docker save/load、skopeo）、base image 更新週期、離線漏洞掃描">斷網環境的容器與映像管理&lt;/a>&lt;/td>
 &lt;td>Private registry、映像搬運、離線 base image 更新&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-monitoring/" data-link-title="斷網環境的監控與可觀測性" data-link-desc="Self-hosted 監控（Prometheus &amp;#43; Grafana）、離線 log 收集（Loki / ELK）、不能 phone home 的告警、NTP 時間同步">斷網環境的監控與可觀測性&lt;/a>&lt;/td>
 &lt;td>Self-hosted 監控工具、離線告警、log 收集&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-self-hosted-services/" data-link-title="斷網環境要自建的服務清單" data-link-desc="正常環境消費的 SaaS（GitHub、Docker Hub、npm、Datadog）在斷網環境全部要自建 — 服務清單、選型、部署順序、統一管理取捨與維護的隱藏成本">斷網環境要自建的服務清單&lt;/a>&lt;/td>
 &lt;td>10 類服務的選型、部署順序、統一管理 vs 個別部署、維護成本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-vcs-ci/" data-link-title="斷網環境的版本控制與 CI/CD" data-link-desc="在沒有 GitHub、沒有 Docker Hub 的隔離網路裡，怎麼部署版本控制、設定 CI runner、跨邊界傳輸 commit、以及讓 PR review 流程運作">斷網環境的版控與 CI/CD&lt;/a>&lt;/td>
 &lt;td>GitLab CE / Gitea 離線安裝、CI runner、git bundle 跨邊界傳輸&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-package-registry/" data-link-title="斷網環境的套件與容器映像 Registry" data-link-desc="斷網環境裡每一個 apt install、npm install、docker pull 都需要內部來源 — 用 Nexus Repository 統一管理套件、用 Harbor 管理容器映像、建立定期搬運與安全掃描的更新週期">斷網環境的套件與容器 Registry&lt;/a>&lt;/td>
 &lt;td>Nexus 統一 proxy、Harbor 容器 registry、映像搬運 SOP、Helm 離線&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-infrastructure-services/" data-link-title="斷網環境的基礎服務：DNS、NTP、CA 與 Secret Management" data-link-desc="斷網環境裡其他所有服務的前提——內部 DNS 做名稱解析、NTP 做時間同步、內部 CA 簽發 TLS 憑證、Vault 管理機密值。這四個服務先部署、其他才能跟上。">斷網環境的基礎服務&lt;/a>&lt;/td>
 &lt;td>DNS (CoreDNS) + NTP (chrony) + CA (step-ca) + Vault&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-security-access/" data-link-title="斷網環境的資安與權限控管" data-link-desc="斷網環境的威脅模型從外部攻擊轉向內部人員與供應鏈——實體安全、離線認證、稽核日誌、更新延遲窗口、跨邊界傳輸審查各有專屬的操作方式">斷網環境的資安與權限控管&lt;/a>&lt;/td>
 &lt;td>威脅模型轉變、實體安全、離線認證、稽核日誌、跨邊界安全審查&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="跟其他模組的關係">跟其他模組的關係&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一：最小可行 IaC&lt;/a>：斷網時 IaC 工具選型和 state backend 的替代做法&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/05-core-services/" data-link-title="模組五：核心服務上 IaC" data-link-desc="資料庫、運算、儲存、load balancer 怎麼寫進基礎設施程式碼，以及上線順序">模組五：核心服務上 IaC&lt;/a>：容器映像和套件依賴的離線管理&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/06-observability-logging/" data-link-title="模組六：可觀測性與 log 一併寫進 code" data-link-desc="log group、metric、alarm 跟基礎設施同生命週期管理，出事時追得到查得到">模組六：可觀測性&lt;/a>：斷網環境的監控不能 phone home&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：PR 流程&lt;/a>：CI/CD 在內網怎麼跑&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/infra/takeover/" data-link-title="接手維運：別人建的環境怎麼接管" data-link-desc="接手前人的專案時，怎麼在不搞壞東西的前提下盤點現況、建立維運能力、逐步正規化 — 從無 SSH 的 FTP 環境到有半套 IaC 的雲端環境都適用">接手維運&lt;/a>：接手斷網環境的額外約束&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>斷網環境（air-gapped）是跟網際網路完全隔離的執行環境——沒有 <code>apt install</code>、沒有 <code>terraform init</code> 自動下載 provider、沒有 Docker Hub 可以 pull image、沒有 GitHub Actions 可以跑 CI。這個約束不改變 infra 的原則（可重建、可追蹤、可審查），但改變了幾乎所有工具的使用方式。</p>
<p>常見的斷網情境：政府或軍事機密網路（實體隔離）、工控與 OT 環境（工廠、電廠、SCADA）、金融交易系統的高安全隔離區、醫療設備網路、以及地端機房裡刻意不開 internet access 的 private zone。</p>
<p>這個模組是橫切約束——它影響<a href="/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一（IaC 選型）</a>到<a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七（PR 流程）</a>的每一個操作步驟。每篇文章處理一個被斷網影響的主要面向。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/infra/air-gapped/air-gapped-principles/" data-link-title="斷網環境的通用原則" data-link-desc="離線套件管理、內容搬運、變更追蹤的共通操作模式 — 所有斷網情境都要先建立的基礎能力">斷網環境的通用原則</a></td>
          <td>離線套件管理、內容搬運、變更追蹤的共通操作模式</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/air-gapped/air-gapped-iac/" data-link-title="斷網環境的 IaC" data-link-desc="Terraform provider mirror、離線 plugin cache、本地 state backend、沒有雲端時的 plan/apply 流程與內網 CI">斷網環境的 IaC</a></td>
          <td>Terraform provider mirror、離線 state backend、plan/apply 流程</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/air-gapped/air-gapped-container/" data-link-title="斷網環境的容器與映像管理" data-link-desc="Private registry 架設、映像搬運（docker save/load、skopeo）、base image 更新週期、離線漏洞掃描">斷網環境的容器與映像管理</a></td>
          <td>Private registry、映像搬運、離線 base image 更新</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/air-gapped/air-gapped-monitoring/" data-link-title="斷網環境的監控與可觀測性" data-link-desc="Self-hosted 監控（Prometheus &#43; Grafana）、離線 log 收集（Loki / ELK）、不能 phone home 的告警、NTP 時間同步">斷網環境的監控與可觀測性</a></td>
          <td>Self-hosted 監控工具、離線告警、log 收集</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/air-gapped/air-gapped-self-hosted-services/" data-link-title="斷網環境要自建的服務清單" data-link-desc="正常環境消費的 SaaS（GitHub、Docker Hub、npm、Datadog）在斷網環境全部要自建 — 服務清單、選型、部署順序、統一管理取捨與維護的隱藏成本">斷網環境要自建的服務清單</a></td>
          <td>10 類服務的選型、部署順序、統一管理 vs 個別部署、維護成本</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/air-gapped/air-gapped-vcs-ci/" data-link-title="斷網環境的版本控制與 CI/CD" data-link-desc="在沒有 GitHub、沒有 Docker Hub 的隔離網路裡，怎麼部署版本控制、設定 CI runner、跨邊界傳輸 commit、以及讓 PR review 流程運作">斷網環境的版控與 CI/CD</a></td>
          <td>GitLab CE / Gitea 離線安裝、CI runner、git bundle 跨邊界傳輸</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/air-gapped/air-gapped-package-registry/" data-link-title="斷網環境的套件與容器映像 Registry" data-link-desc="斷網環境裡每一個 apt install、npm install、docker pull 都需要內部來源 — 用 Nexus Repository 統一管理套件、用 Harbor 管理容器映像、建立定期搬運與安全掃描的更新週期">斷網環境的套件與容器 Registry</a></td>
          <td>Nexus 統一 proxy、Harbor 容器 registry、映像搬運 SOP、Helm 離線</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/air-gapped/air-gapped-infrastructure-services/" data-link-title="斷網環境的基礎服務：DNS、NTP、CA 與 Secret Management" data-link-desc="斷網環境裡其他所有服務的前提——內部 DNS 做名稱解析、NTP 做時間同步、內部 CA 簽發 TLS 憑證、Vault 管理機密值。這四個服務先部署、其他才能跟上。">斷網環境的基礎服務</a></td>
          <td>DNS (CoreDNS) + NTP (chrony) + CA (step-ca) + Vault</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/air-gapped/air-gapped-security-access/" data-link-title="斷網環境的資安與權限控管" data-link-desc="斷網環境的威脅模型從外部攻擊轉向內部人員與供應鏈——實體安全、離線認證、稽核日誌、更新延遲窗口、跨邊界傳輸審查各有專屬的操作方式">斷網環境的資安與權限控管</a></td>
          <td>威脅模型轉變、實體安全、離線認證、稽核日誌、跨邊界安全審查</td>
      </tr>
  </tbody>
</table>
<h2 id="跟其他模組的關係">跟其他模組的關係</h2>
<ul>
<li>→ <a href="/blog/infra/01-minimal-iac/" data-link-title="模組一：最小可行 IaC — state 地基與 Console 唯讀鐵律" data-link-desc="Terraform / OpenTofu 選型、remote state 與 lock，以及「Console 只能看不能改」鐵律">模組一：最小可行 IaC</a>：斷網時 IaC 工具選型和 state backend 的替代做法</li>
<li>→ <a href="/blog/infra/05-core-services/" data-link-title="模組五：核心服務上 IaC" data-link-desc="資料庫、運算、儲存、load balancer 怎麼寫進基礎設施程式碼，以及上線順序">模組五：核心服務上 IaC</a>：容器映像和套件依賴的離線管理</li>
<li>→ <a href="/blog/infra/06-observability-logging/" data-link-title="模組六：可觀測性與 log 一併寫進 code" data-link-desc="log group、metric、alarm 跟基礎設施同生命週期管理，出事時追得到查得到">模組六：可觀測性</a>：斷網環境的監控不能 phone home</li>
<li>→ <a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：PR 流程</a>：CI/CD 在內網怎麼跑</li>
<li>→ <a href="/blog/infra/takeover/" data-link-title="接手維運：別人建的環境怎麼接管" data-link-desc="接手前人的專案時，怎麼在不搞壞東西的前提下盤點現況、建立維運能力、逐步正規化 — 從無 SSH 的 FTP 環境到有半套 IaC 的雲端環境都適用">接手維運</a>：接手斷網環境的額外約束</li>
</ul>
]]></content:encoded></item><item><title>斷網環境的通用原則</title><link>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-principles/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-principles/</guid><description>&lt;p>斷網環境的 infra 原則跟連網環境相同——可重建、可追蹤、可審查。差別在於連網環境用網路解決的事情（下載套件、推送 code、拉取映像、發送告警），斷網環境要用替代路徑解決。這些替代路徑有一個共通模式：把內容在有網路的環境準備好，經過安全審查後搬進隔離網路。本篇建立這個共通模式的操作框架，後續的 IaC、容器、監控各篇在這個框架上展開各自的細節。&lt;/p>
&lt;h2 id="內容搬運模式content-ferry">內容搬運模式（Content Ferry）&lt;/h2>
&lt;p>斷網環境裡的所有外部依賴（套件、映像、工具、更新）都要經過一條可控的搬運路徑進入。這條路徑的設計決定了環境的安全性和維護效率。&lt;/p>
&lt;h3 id="搬運路徑的三種形態">搬運路徑的三種形態&lt;/h3>
&lt;p>&lt;strong>離線媒介搬運&lt;/strong>：用 USB 隨身碟、外接硬碟或光碟把檔案從有網路的工作站搬進隔離網路。適合高安全環境（軍事、政府機密網路），搬運頻率通常是週或月級。每次搬運的內容要經過掃毒和完整性驗證。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 外部工作站：準備搬運包&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">mkdir -p ferry/&lt;span class="k">$(&lt;/span>date +%Y%m%d&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># 把需要的套件、映像、工具複製進去&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">cp -r packages/ images/ tools/ ferry/&lt;span class="k">$(&lt;/span>date +%Y%m%d&lt;span class="k">)&lt;/span>/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 產生 checksum&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">find ferry/&lt;span class="k">$(&lt;/span>date +%Y%m%d&lt;span class="k">)&lt;/span> -type f -exec sha256sum &lt;span class="o">{}&lt;/span> &lt;span class="se">\;&lt;/span> &amp;gt; ferry/&lt;span class="k">$(&lt;/span>date +%Y%m%d&lt;span class="k">)&lt;/span>/manifest.sha256&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 隔離網路內：驗證搬運包完整性&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> /mnt/usb/ferry/20260626
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">sha256sum -c manifest.sha256&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>跨網段閘道搬運&lt;/strong>：在隔離網路的邊界放一台 staging gateway（跳板機），它有兩張網卡——一張連外部網路（或 DMZ）、一張連內部隔離網路。外部的內容先傳到閘道、經過掃描和審查後再推進內部。適合金融和工控環境，搬運頻率可以是日級。&lt;/p>
&lt;p>閘道的安全約束：只允許特定的檔案類型通過、所有傳入的檔案經過掃毒、傳輸記錄要保留 audit log、閘道本身定期更新安全軟體。&lt;/p>
&lt;p>&lt;strong>單向資料二極體（Data Diode）&lt;/strong>：硬體層面只允許資料單向流動（外 → 內），物理上無法從內部網路傳資料出去。用在最高安全等級的環境。搬運頻率和內容由二極體的設定決定。&lt;/p>
&lt;h3 id="搬運的操作紀律">搬運的操作紀律&lt;/h3>
&lt;p>每次搬運都要記錄：日期、搬運者、搬運內容清單（檔名 + 版本 + checksum）、搬運理由。這份紀錄存在內部網路的版本控制裡，讓「這個套件是誰、什麼時候、為什麼帶進來的」事後可追溯。&lt;/p>
&lt;p>搬運內容的安全審查至少包含：掃毒（ClamAV 或商業掃毒）、checksum 驗證（確認搬運過程沒有被竄改）、版本確認（確認搬進來的版本跟預期的一致、不是被降級的舊版）。&lt;/p>
&lt;p>時程參考：建立搬運流程（含閘道設定、掃描工具安裝、紀錄模板）約需 2-3 天。之後每次搬運操作約 1-2 小時（含準備、掃描、驗證、紀錄）。&lt;/p>
&lt;h2 id="離線套件管理">離線套件管理&lt;/h2>
&lt;p>連網環境的 &lt;code>apt install&lt;/code>、&lt;code>yum install&lt;/code>、&lt;code>npm install&lt;/code> 背後都在連線到公開的套件倉庫。斷網環境需要在內部建立這些倉庫的離線鏡像。&lt;/p>
&lt;h3 id="作業系統套件">作業系統套件&lt;/h3>
&lt;p>&lt;strong>Debian/Ubuntu&lt;/strong>：用 &lt;code>apt-mirror&lt;/code> 或 &lt;code>aptly&lt;/code> 在有網路的環境建立 mirror，把整個 mirror 搬進內部網路，內部機器的 &lt;code>/etc/apt/sources.list&lt;/code> 指向內部 mirror。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 外部：建立 mirror（首次約 50-200GB，後續增量）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">apt-mirror /etc/apt/mirror.list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 內部：設定 sources.list 指向內部 mirror&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;deb http://internal-mirror.local/ubuntu jammy main restricted&amp;#34;&lt;/span> &amp;gt; /etc/apt/sources.list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">apt update&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>RHEL/CentOS&lt;/strong>：用 &lt;code>reposync&lt;/code> 把 yum repo 同步到本地，搬進內部後用 &lt;code>createrepo&lt;/code> 建立 repo metadata。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 外部：同步 repo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">reposync --repoid&lt;span class="o">=&lt;/span>baseos --download-metadata -p /path/to/mirror/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 內部：建立 repo 並設定&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">createrepo /path/to/mirror/baseos&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="應用層套件">應用層套件&lt;/h3>
&lt;p>&lt;strong>Node.js（npm）&lt;/strong>：&lt;code>npm pack&lt;/code> 把每個依賴打包成 .tgz，搬進內部後用 &lt;code>npm install --offline&lt;/code> 或建立 Verdaccio private registry。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 外部：打包所有依賴&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">npm pack --pack-destination ./offline-packages/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># 或用 npm-offline-mirror&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">npm install --prefer-offline --cache ./npm-cache&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Python（pip）&lt;/strong>：&lt;code>pip download&lt;/code> 把依賴下載成 wheel 或 tarball，搬進內部後 &lt;code>pip install --no-index --find-links=./packages/&lt;/code>。&lt;/p>
&lt;p>&lt;strong>PHP（Composer）&lt;/strong>：&lt;code>composer install&lt;/code> 後整個 &lt;code>vendor/&lt;/code> 目錄打包搬進去。或建立 Satis 作為 private Packagist mirror。&lt;/p>
&lt;h3 id="套件鏡像的維護節奏">套件鏡像的維護節奏&lt;/h3>
&lt;p>離線 mirror 需要定期更新——安全補丁、版本升級都要透過搬運流程進入。更新頻率取決於安全需求：高安全環境至少月更（安全補丁）、一般環境季更可接受。每次更新都是一次搬運操作，要走完整的審查流程。&lt;/p>
&lt;h3 id="多格式統一nexus-repository">多格式統一：Nexus Repository&lt;/h3>
&lt;p>上面的做法是每個套件生態各自建 mirror（apt-mirror + Verdaccio + Satis + pip local index）。Nexus Repository 是多格式統一的 artifact proxy，同時支援 apt / yum / npm / Maven / PyPI / Docker / Helm——在企業級斷網環境裡，用一個 Nexus 實例取代多個獨立的離線 repo mirror，維護成本較低。代價是 Nexus 本身的安裝和維運（Java 應用、需要磁碟空間和記憶體），小團隊各自建 mirror 可能反而更簡單。&lt;/p></description><content:encoded><![CDATA[<p>斷網環境的 infra 原則跟連網環境相同——可重建、可追蹤、可審查。差別在於連網環境用網路解決的事情（下載套件、推送 code、拉取映像、發送告警），斷網環境要用替代路徑解決。這些替代路徑有一個共通模式：把內容在有網路的環境準備好，經過安全審查後搬進隔離網路。本篇建立這個共通模式的操作框架，後續的 IaC、容器、監控各篇在這個框架上展開各自的細節。</p>
<h2 id="內容搬運模式content-ferry">內容搬運模式（Content Ferry）</h2>
<p>斷網環境裡的所有外部依賴（套件、映像、工具、更新）都要經過一條可控的搬運路徑進入。這條路徑的設計決定了環境的安全性和維護效率。</p>
<h3 id="搬運路徑的三種形態">搬運路徑的三種形態</h3>
<p><strong>離線媒介搬運</strong>：用 USB 隨身碟、外接硬碟或光碟把檔案從有網路的工作站搬進隔離網路。適合高安全環境（軍事、政府機密網路），搬運頻率通常是週或月級。每次搬運的內容要經過掃毒和完整性驗證。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 外部工作站：準備搬運包</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">mkdir -p ferry/<span class="k">$(</span>date +%Y%m%d<span class="k">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 把需要的套件、映像、工具複製進去</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">cp -r packages/ images/ tools/ ferry/<span class="k">$(</span>date +%Y%m%d<span class="k">)</span>/
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 產生 checksum</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">find ferry/<span class="k">$(</span>date +%Y%m%d<span class="k">)</span> -type f -exec sha256sum <span class="o">{}</span> <span class="se">\;</span> &gt; ferry/<span class="k">$(</span>date +%Y%m%d<span class="k">)</span>/manifest.sha256</span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 隔離網路內：驗證搬運包完整性</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">cd</span> /mnt/usb/ferry/20260626
</span></span><span class="line"><span class="ln">3</span><span class="cl">sha256sum -c manifest.sha256</span></span></code></pre></div><p><strong>跨網段閘道搬運</strong>：在隔離網路的邊界放一台 staging gateway（跳板機），它有兩張網卡——一張連外部網路（或 DMZ）、一張連內部隔離網路。外部的內容先傳到閘道、經過掃描和審查後再推進內部。適合金融和工控環境，搬運頻率可以是日級。</p>
<p>閘道的安全約束：只允許特定的檔案類型通過、所有傳入的檔案經過掃毒、傳輸記錄要保留 audit log、閘道本身定期更新安全軟體。</p>
<p><strong>單向資料二極體（Data Diode）</strong>：硬體層面只允許資料單向流動（外 → 內），物理上無法從內部網路傳資料出去。用在最高安全等級的環境。搬運頻率和內容由二極體的設定決定。</p>
<h3 id="搬運的操作紀律">搬運的操作紀律</h3>
<p>每次搬運都要記錄：日期、搬運者、搬運內容清單（檔名 + 版本 + checksum）、搬運理由。這份紀錄存在內部網路的版本控制裡，讓「這個套件是誰、什麼時候、為什麼帶進來的」事後可追溯。</p>
<p>搬運內容的安全審查至少包含：掃毒（ClamAV 或商業掃毒）、checksum 驗證（確認搬運過程沒有被竄改）、版本確認（確認搬進來的版本跟預期的一致、不是被降級的舊版）。</p>
<p>時程參考：建立搬運流程（含閘道設定、掃描工具安裝、紀錄模板）約需 2-3 天。之後每次搬運操作約 1-2 小時（含準備、掃描、驗證、紀錄）。</p>
<h2 id="離線套件管理">離線套件管理</h2>
<p>連網環境的 <code>apt install</code>、<code>yum install</code>、<code>npm install</code> 背後都在連線到公開的套件倉庫。斷網環境需要在內部建立這些倉庫的離線鏡像。</p>
<h3 id="作業系統套件">作業系統套件</h3>
<p><strong>Debian/Ubuntu</strong>：用 <code>apt-mirror</code> 或 <code>aptly</code> 在有網路的環境建立 mirror，把整個 mirror 搬進內部網路，內部機器的 <code>/etc/apt/sources.list</code> 指向內部 mirror。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 外部：建立 mirror（首次約 50-200GB，後續增量）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">apt-mirror /etc/apt/mirror.list
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 內部：設定 sources.list 指向內部 mirror</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;deb http://internal-mirror.local/ubuntu jammy main restricted&#34;</span> &gt; /etc/apt/sources.list
</span></span><span class="line"><span class="ln">6</span><span class="cl">apt update</span></span></code></pre></div><p><strong>RHEL/CentOS</strong>：用 <code>reposync</code> 把 yum repo 同步到本地，搬進內部後用 <code>createrepo</code> 建立 repo metadata。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 外部：同步 repo</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">reposync --repoid<span class="o">=</span>baseos --download-metadata -p /path/to/mirror/
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 內部：建立 repo 並設定</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">createrepo /path/to/mirror/baseos</span></span></code></pre></div><h3 id="應用層套件">應用層套件</h3>
<p><strong>Node.js（npm）</strong>：<code>npm pack</code> 把每個依賴打包成 .tgz，搬進內部後用 <code>npm install --offline</code> 或建立 Verdaccio private registry。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 外部：打包所有依賴</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm pack --pack-destination ./offline-packages/
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 或用 npm-offline-mirror</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">npm install --prefer-offline --cache ./npm-cache</span></span></code></pre></div><p><strong>Python（pip）</strong>：<code>pip download</code> 把依賴下載成 wheel 或 tarball，搬進內部後 <code>pip install --no-index --find-links=./packages/</code>。</p>
<p><strong>PHP（Composer）</strong>：<code>composer install</code> 後整個 <code>vendor/</code> 目錄打包搬進去。或建立 Satis 作為 private Packagist mirror。</p>
<h3 id="套件鏡像的維護節奏">套件鏡像的維護節奏</h3>
<p>離線 mirror 需要定期更新——安全補丁、版本升級都要透過搬運流程進入。更新頻率取決於安全需求：高安全環境至少月更（安全補丁）、一般環境季更可接受。每次更新都是一次搬運操作，要走完整的審查流程。</p>
<h3 id="多格式統一nexus-repository">多格式統一：Nexus Repository</h3>
<p>上面的做法是每個套件生態各自建 mirror（apt-mirror + Verdaccio + Satis + pip local index）。Nexus Repository 是多格式統一的 artifact proxy，同時支援 apt / yum / npm / Maven / PyPI / Docker / Helm——在企業級斷網環境裡，用一個 Nexus 實例取代多個獨立的離線 repo mirror，維護成本較低。代價是 Nexus 本身的安裝和維運（Java 應用、需要磁碟空間和記憶體），小團隊各自建 mirror 可能反而更簡單。</p>
<h3 id="離線-configuration-managementansible">離線 Configuration Management：Ansible</h3>
<p>斷網環境的 OS 設定、套件安裝、服務啟動等 configuration management 需求，Ansible 是運作良好的工具——它不需要在目標機器安裝 agent、透過 SSH 推送 playbook 執行，playbook 本身是 YAML 可版本控制。在沒有雲端 IaC（Terraform 管的是雲端資源 API）的地端斷網環境裡，Ansible 負責 configuration management 層。Ansible 自身的安裝只需要 Python，控制端安裝後即可透過 SSH 管理內部所有機器。</p>
<h2 id="變更追蹤沒有-github-怎麼辦">變更追蹤：沒有 GitHub 怎麼辦</h2>
<p>斷網環境不能 push 到 GitHub、不能開 PR、不能用 GitHub Actions。但 git 本身是離線工具——git 的所有操作（commit、branch、merge、log、diff）都不需要網路。</p>
<h3 id="內部-git-server">內部 Git Server</h3>
<p>在隔離網路內架設 git server：Gitea（輕量、單一二進位、適合小團隊）、GitLab CE（功能完整、含 CI/CD runner、適合中大團隊）、或最簡單的 bare repo on NFS。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 最簡單的方式：bare repo on 共用檔案系統</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">git init --bare /shared/repos/infra.git
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 開發者 clone</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">git clone /shared/repos/infra.git</span></span></code></pre></div><h3 id="git-bundle-跨網段傳遞">Git Bundle 跨網段傳遞</h3>
<p>如果需要在有網路的環境開發、完成後搬進隔離網路，用 <code>git bundle</code> 把 commit 打包成單一檔案：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 外部：把 main branch 的所有 commit 打包</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">git bundle create infra-<span class="k">$(</span>date +%Y%m%d<span class="k">)</span>.bundle main
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 搬運後，在內部 clone 或 pull</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">git clone infra-20260626.bundle infra-repo
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># 或增量更新</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">git pull infra-20260626.bundle main</span></span></code></pre></div><p>bundle 檔案可以用 <code>git bundle verify</code> 驗證完整性。增量 bundle（只包含某個 tag 之後的 commit）可以減少搬運的資料量：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">git bundle create incremental.bundle last-imported-tag..main</span></span></code></pre></div><h3 id="code-review-的替代方案">Code Review 的替代方案</h3>
<p>沒有 GitHub PR，code review 可以用：</p>
<ul>
<li>GitLab CE / Gitea 的內建 merge request（如果架了內部 git server）</li>
<li><code>git format-patch</code> 產出 patch 檔 + email review（傳統做法、不需要 web UI）</li>
<li><code>git diff main..feature | less</code> 直接在終端機 review（最簡陋但可行）</li>
</ul>
<h2 id="staging-gateway-的設計">Staging Gateway 的設計</h2>
<p>staging gateway 是搬運路徑的關鍵節點——它決定了什麼能進、什麼不能進。設計要點：</p>
<p><strong>最小安裝</strong>：閘道上只裝搬運需要的工具（scp、rsync、掃毒軟體、checksum 工具），不裝開發工具、不跑應用服務。攻擊面越小越好。</p>
<p><strong>雙網卡隔離</strong>：一張網卡連外部（或 DMZ）、一張連內部。兩張網卡之間沒有自動路由——檔案必須經過人工或腳本從外部目錄搬到內部目錄，中間經過掃描。</p>
<p><strong>審計紀錄</strong>：閘道上的所有檔案操作（建立、複製、刪除）都要記錄。<code>auditd</code> 或等價工具提供核心層級的操作追蹤。</p>
<p><strong>定期輪替</strong>：閘道本身的 OS 和掃毒軟體需要更新。這是一個遞迴問題（用什麼搬運閘道的更新？）——通常用離線媒介搬運閘道自身的更新，或用另一台更上游的閘道。</p>
<p>時程參考：閘道的初次設定（含 OS 安裝、雙網卡配置、掃描工具、審計設定）約需 1-2 天。搬運流程文件化約需半天。</p>
<h2 id="安全審查什麼能跨越隔離邊界">安全審查：什麼能跨越隔離邊界</h2>
<p>每一筆跨越隔離邊界的內容都是潛在的攻擊向量。審查的原則是：預設拒絕，逐項允許。</p>
<p>審查清單：</p>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>檢查方式</th>
          <th>通過條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>掃毒</td>
          <td>ClamAV / 商業掃毒</td>
          <td>0 偵測</td>
      </tr>
      <tr>
          <td>完整性</td>
          <td>sha256sum 比對</td>
          <td>checksum 與外部記錄一致</td>
      </tr>
      <tr>
          <td>版本</td>
          <td>比對預期版本號</td>
          <td>跟申請單的版本一致</td>
      </tr>
      <tr>
          <td>來源</td>
          <td>驗證下載來源</td>
          <td>來自官方 repo 或已知 mirror</td>
      </tr>
      <tr>
          <td>必要性</td>
          <td>申請理由審查</td>
          <td>有明確的使用場景</td>
      </tr>
  </tbody>
</table>
<p>對決策者的重點：斷網環境的安全不是「隔離就安全」——搬運路徑是唯一的攻擊面，這條路徑的安全審查品質決定了整個隔離環境的安全水位。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-iac/" data-link-title="斷網環境的 IaC" data-link-desc="Terraform provider mirror、離線 plugin cache、本地 state backend、沒有雲端時的 plan/apply 流程與內網 CI">斷網環境的 IaC</a>：Terraform provider 和 module 的離線管理</li>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-container/" data-link-title="斷網環境的容器與映像管理" data-link-desc="Private registry 架設、映像搬運（docker save/load、skopeo）、base image 更新週期、離線漏洞掃描">斷網環境的容器管理</a>：映像搬運用的是本篇的 content ferry 模式</li>
<li>→ <a href="/blog/infra/08-governance-habits/" data-link-title="模組八：治理好習慣 — 規模長大後不失控的最小節奏" data-link-desc="tagging 規範、secrets 不進 code、成本可見性、最小可行節奏，規模長大後不失控">模組八：治理好習慣</a>：斷網環境的搬運紀錄是治理的一部分</li>
</ul>
]]></content:encoded></item><item><title>Degraded mode 設計</title><link>https://tarrragon.github.io/blog/ux-design/04-error-recovery/degraded-mode-design/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/04-error-recovery/degraded-mode-design/</guid><description>&lt;p>Degraded mode 是指系統的部分功能因為外部依賴不可用（網路斷線、服務故障、權限缺失）而暫時無法運作，但其他功能仍可正常使用。設計重點是讓使用者清楚知道哪些功能可用、哪些暫時不可用，而非讓整個 app 因為一個功能的失敗而停擺。&lt;/p>
&lt;h2 id="三種處理方式">三種處理方式&lt;/h2>
&lt;h3 id="靜默隱藏">靜默隱藏&lt;/h3>
&lt;p>把不可用的功能從 UI 上移除 — 按鈕消失、選單項目隱藏。使用者看不到這些功能，自然不會嘗試使用。&lt;/p>
&lt;p>靜默隱藏的風險是使用者困惑。經常使用的功能突然消失，使用者會以為是 bug 或 app 更新移除了功能。如果功能在恢復後重新出現，使用者的困惑加劇。&lt;/p>
&lt;p>靜默隱藏只適合使用者從未使用過的功能（新使用者尚未配對時隱藏連線按鈕）。已經使用過的功能突然隱藏會破壞使用者的心理模型。&lt;/p>
&lt;h3 id="明確標示">明確標示&lt;/h3>
&lt;p>功能的 UI 元素保留在畫面上，但加上不可用的視覺標示 — 灰色按鈕、「離線不可用」標籤、禁用狀態。使用者能看到功能存在，也知道目前暫時無法使用。&lt;/p>
&lt;p>明確標示的設計要點：&lt;/p>
&lt;p>&lt;strong>說明原因&lt;/strong>。「搜尋功能需要網路連線」比單純的灰色按鈕提供更多資訊。使用者知道原因後能自行判斷要等還是離開。&lt;/p>
&lt;p>&lt;strong>說明恢復條件&lt;/strong>。「連上網路後自動恢復」讓使用者知道什麼時候功能會回來。「重新啟動 app 後可用」讓使用者知道需要採取行動。&lt;/p>
&lt;p>&lt;strong>避免只靠顏色傳達狀態&lt;/strong>。灰色按鈕對色盲使用者可能不明顯。搭配文字標籤或圖示。&lt;/p>
&lt;h3 id="替代方案">替代方案&lt;/h3>
&lt;p>提供不需要失敗依賴的替代功能。線上搜尋不可用時提供離線搜尋（本地快取的資料）。即時同步不可用時提供本地儲存（恢復連線後自動同步）。&lt;/p>
&lt;p>替代方案的 UX 需要讓使用者知道目前使用的是替代版本。「離線模式 — 搜尋結果可能不是最新的」讓使用者對結果的準確度有正確預期。&lt;/p>
&lt;h2 id="全域-vs-功能級降級">全域 vs 功能級降級&lt;/h2>
&lt;h3 id="全域降級">全域降級&lt;/h3>
&lt;p>整個 app 進入降級模式 — 頂部顯示「離線模式」橫幅，所有需要網路的功能統一標示為不可用。適合網路連線是 app 核心依賴的場景。&lt;/p>
&lt;p>全域降級的 UI 實作簡單（一個全域狀態控制所有功能的可用性），但可能過度限制 — 部分功能不依賴網路也能運作。&lt;/p>
&lt;h3 id="功能級降級">功能級降級&lt;/h3>
&lt;p>每個功能獨立判斷自己的可用狀態。搜尋需要網路但筆記不需要 — 網路斷線時搜尋不可用，筆記正常。&lt;/p>
&lt;p>功能級降級更精確但實作更複雜 — 每個功能需要宣告自己的依賴，並在依賴不可用時提供對應的 UI 狀態。&lt;/p>
&lt;h2 id="降級狀態的進入和退出">降級狀態的進入和退出&lt;/h2>
&lt;h3 id="進入">進入&lt;/h3>
&lt;p>依賴不可用時自動進入降級狀態。進入時通知使用者（Snackbar、橫幅、狀態變更）。&lt;/p>
&lt;p>避免頻繁切換 — 網路訊號不穩定時，每秒在正常和降級之間切換會讓 UI 閃爍。加入穩定性判斷（連續 N 秒不可用才進入降級，連續 N 秒可用才退出降級）。&lt;/p>
&lt;h3 id="退出">退出&lt;/h3>
&lt;p>依賴恢復後自動退出降級狀態。退出時通知使用者（「已恢復連線」），並自動執行待完成的操作（同步、補發事件）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>錯誤訊息的撰寫 → &lt;a href="https://tarrragon.github.io/blog/ux-design/04-error-recovery/error-message-principles/" data-link-title="錯誤訊息撰寫原則" data-link-desc="錯誤訊息的兩個職責：使用者能讀懂發生什麼、使用者能決定下一步做什麼">錯誤訊息撰寫原則&lt;/a>&lt;/li>
&lt;li>重試循環的逃生口 → &lt;a href="https://tarrragon.github.io/blog/ux-design/04-error-recovery/error-loop-escape/" data-link-title="error → retry → error 循環的逃生口設計" data-link-desc="當重試持續失敗時，使用者需要第二條路 — 逃生口設計讓使用者能離開失敗循環而非被困住">error → retry → error 循環的逃生口&lt;/a>&lt;/li>
&lt;li>網路 gate 的 UX 處理 → &lt;a href="https://tarrragon.github.io/blog/ux-design/02-gate-fallback/network-offline-ux/" data-link-title="網路斷線 UX 模式" data-link-desc="Offline-first / retry / degraded mode 三種網路 gate 的處理策略 — 取決於功能是否依賴即時連線">ux-design 模組二 網路斷線 UX 模式&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Degraded mode 是指系統的部分功能因為外部依賴不可用（網路斷線、服務故障、權限缺失）而暫時無法運作，但其他功能仍可正常使用。設計重點是讓使用者清楚知道哪些功能可用、哪些暫時不可用，而非讓整個 app 因為一個功能的失敗而停擺。</p>
<h2 id="三種處理方式">三種處理方式</h2>
<h3 id="靜默隱藏">靜默隱藏</h3>
<p>把不可用的功能從 UI 上移除 — 按鈕消失、選單項目隱藏。使用者看不到這些功能，自然不會嘗試使用。</p>
<p>靜默隱藏的風險是使用者困惑。經常使用的功能突然消失，使用者會以為是 bug 或 app 更新移除了功能。如果功能在恢復後重新出現，使用者的困惑加劇。</p>
<p>靜默隱藏只適合使用者從未使用過的功能（新使用者尚未配對時隱藏連線按鈕）。已經使用過的功能突然隱藏會破壞使用者的心理模型。</p>
<h3 id="明確標示">明確標示</h3>
<p>功能的 UI 元素保留在畫面上，但加上不可用的視覺標示 — 灰色按鈕、「離線不可用」標籤、禁用狀態。使用者能看到功能存在，也知道目前暫時無法使用。</p>
<p>明確標示的設計要點：</p>
<p><strong>說明原因</strong>。「搜尋功能需要網路連線」比單純的灰色按鈕提供更多資訊。使用者知道原因後能自行判斷要等還是離開。</p>
<p><strong>說明恢復條件</strong>。「連上網路後自動恢復」讓使用者知道什麼時候功能會回來。「重新啟動 app 後可用」讓使用者知道需要採取行動。</p>
<p><strong>避免只靠顏色傳達狀態</strong>。灰色按鈕對色盲使用者可能不明顯。搭配文字標籤或圖示。</p>
<h3 id="替代方案">替代方案</h3>
<p>提供不需要失敗依賴的替代功能。線上搜尋不可用時提供離線搜尋（本地快取的資料）。即時同步不可用時提供本地儲存（恢復連線後自動同步）。</p>
<p>替代方案的 UX 需要讓使用者知道目前使用的是替代版本。「離線模式 — 搜尋結果可能不是最新的」讓使用者對結果的準確度有正確預期。</p>
<h2 id="全域-vs-功能級降級">全域 vs 功能級降級</h2>
<h3 id="全域降級">全域降級</h3>
<p>整個 app 進入降級模式 — 頂部顯示「離線模式」橫幅，所有需要網路的功能統一標示為不可用。適合網路連線是 app 核心依賴的場景。</p>
<p>全域降級的 UI 實作簡單（一個全域狀態控制所有功能的可用性），但可能過度限制 — 部分功能不依賴網路也能運作。</p>
<h3 id="功能級降級">功能級降級</h3>
<p>每個功能獨立判斷自己的可用狀態。搜尋需要網路但筆記不需要 — 網路斷線時搜尋不可用，筆記正常。</p>
<p>功能級降級更精確但實作更複雜 — 每個功能需要宣告自己的依賴，並在依賴不可用時提供對應的 UI 狀態。</p>
<h2 id="降級狀態的進入和退出">降級狀態的進入和退出</h2>
<h3 id="進入">進入</h3>
<p>依賴不可用時自動進入降級狀態。進入時通知使用者（Snackbar、橫幅、狀態變更）。</p>
<p>避免頻繁切換 — 網路訊號不穩定時，每秒在正常和降級之間切換會讓 UI 閃爍。加入穩定性判斷（連續 N 秒不可用才進入降級，連續 N 秒可用才退出降級）。</p>
<h3 id="退出">退出</h3>
<p>依賴恢復後自動退出降級狀態。退出時通知使用者（「已恢復連線」），並自動執行待完成的操作（同步、補發事件）。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>錯誤訊息的撰寫 → <a href="/blog/ux-design/04-error-recovery/error-message-principles/" data-link-title="錯誤訊息撰寫原則" data-link-desc="錯誤訊息的兩個職責：使用者能讀懂發生什麼、使用者能決定下一步做什麼">錯誤訊息撰寫原則</a></li>
<li>重試循環的逃生口 → <a href="/blog/ux-design/04-error-recovery/error-loop-escape/" data-link-title="error → retry → error 循環的逃生口設計" data-link-desc="當重試持續失敗時，使用者需要第二條路 — 逃生口設計讓使用者能離開失敗循環而非被困住">error → retry → error 循環的逃生口</a></li>
<li>網路 gate 的 UX 處理 → <a href="/blog/ux-design/02-gate-fallback/network-offline-ux/" data-link-title="網路斷線 UX 模式" data-link-desc="Offline-first / retry / degraded mode 三種網路 gate 的處理策略 — 取決於功能是否依賴即時連線">ux-design 模組二 網路斷線 UX 模式</a></li>
</ul>
]]></content:encoded></item><item><title>網路斷線 UX 模式</title><link>https://tarrragon.github.io/blog/ux-design/02-gate-fallback/network-offline-ux/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/02-gate-fallback/network-offline-ux/</guid><description>&lt;p>網路 &lt;a href="https://tarrragon.github.io/blog/ux-design/knowledge-cards/gate/" data-link-title="Gate（UX）" data-link-desc="說明使用者操作流程中「必須通過才能繼續」的關卡，以及成功/失敗/不確定三條路徑的設計責任">gate&lt;/a> 和其他 gate 的差異在於狀態的連續性。生物辨識是二元結果（通過或不通過），網路狀態是連續的 — 連線中、已連線、斷線、重新連線、連線但延遲高、連線但頻繁斷開。處理策略取決於功能對即時連線的依賴程度。&lt;/p>
&lt;h2 id="三種處理策略">三種處理策略&lt;/h2>
&lt;h3 id="offline-first">Offline-first&lt;/h3>
&lt;p>功能的核心操作在本地完成，網路用於同步。斷線時使用者仍可操作，重新連線後自動同步差異。&lt;/p>
&lt;p>Offline-first 適合的前提是資料可以本地存儲且衝突可以解決。筆記 app、待辦事項、表單填寫 — 使用者的操作產生本地資料，網路只負責把資料同步到 server。&lt;/p>
&lt;p>Offline-first 的 UX 設計重點是讓使用者知道同步狀態：已同步、待同步、同步失敗。不需要 gate — 網路狀態不阻擋使用者操作。&lt;/p>
&lt;h3 id="retry-with-feedback">Retry with feedback&lt;/h3>
&lt;p>功能需要網路但可以等待。斷線時顯示狀態和重試選項，使用者決定要等還是離開。&lt;/p>
&lt;p>app_tunnel 的 terminal 連線屬於這個模式。WebSocket 連線需要網路，斷線時使用者無法操作終端機。error 和 disconnected 狀態提供重連按鈕讓使用者手動重試。&lt;/p>
&lt;p>Retry 策略的 UX 設計重點：&lt;/p>
&lt;ul>
&lt;li>告知使用者發生什麼（「連線中斷」而非空白畫面）&lt;/li>
&lt;li>提供手動重試（重連按鈕）&lt;/li>
&lt;li>提供退出路徑（返回首頁 — app_tunnel 原本缺少這個）&lt;/li>
&lt;li>自動重試要有上限和間隔遞增（避免無限重試消耗電量）&lt;/li>
&lt;/ul>
&lt;h3 id="degraded-mode">Degraded mode&lt;/h3>
&lt;p>功能部分依賴網路。核心功能離線可用，進階功能需要網路。斷線時自動切換到降級模式，不阻擋使用者操作但功能受限。&lt;/p>
&lt;p>降級模式的 UX 設計重點是清楚標示哪些功能可用、哪些不可用。「離線模式 — 搜尋功能暫時不可用」比靜默隱藏搜尋按鈕更透明。&lt;/p>
&lt;h2 id="網路狀態的-ui-呈現">網路狀態的 UI 呈現&lt;/h2>
&lt;h3 id="全域指示器">全域指示器&lt;/h3>
&lt;p>在 app 頂部或狀態列顯示「離線」標示。適合網路狀態影響全域功能的 app。&lt;/p>
&lt;h3 id="功能級指示器">功能級指示器&lt;/h3>
&lt;p>在需要網路的功能旁邊顯示不可用狀態。適合只有部分功能依賴網路的 app。&lt;/p>
&lt;h3 id="非侵入式通知">非侵入式通知&lt;/h3>
&lt;p>用 Snackbar 或 Toast 短暫顯示「已恢復連線」或「網路中斷」。適合網路狀態偶爾變化的場景。不適合頻繁斷開重連的場景（通知太多會干擾使用者）。&lt;/p>
&lt;h2 id="連線但品質差的場景">連線但品質差的場景&lt;/h2>
&lt;p>網路存在但延遲高或頻繁斷開，比完全離線更難處理。完全離線時 app 可以立即切換到離線模式；連線不穩定時，每次請求可能成功也可能逾時，使用者體驗是「有時候行有時候不行」。&lt;/p>
&lt;p>處理策略：&lt;/p>
&lt;ul>
&lt;li>設定合理的逾時時間（太短會把慢回應判定為失敗，太長讓使用者等太久）&lt;/li>
&lt;li>逾時後顯示狀態和重試選項，不自動重試（避免在不穩定網路上累積重試）&lt;/li>
&lt;li>在 loading 狀態提供取消選項，讓使用者可以中斷等待&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>Gate 設計的通用方法論 → &lt;a href="https://tarrragon.github.io/blog/ux-design/02-gate-fallback/gate-three-questions/" data-link-title="Gate 分類與三問設計法" data-link-desc="每個 gate 設計時問三個問題：成功時做什麼、失敗時做什麼、使用者不知道發生什麼時做什麼">Gate 分類與三問設計法&lt;/a>&lt;/li>
&lt;li>權限請求的 UX 設計 → &lt;a href="https://tarrragon.github.io/blog/ux-design/02-gate-fallback/permission-request-timing/" data-link-title="Permission 請求時機與措辭" data-link-desc="系統權限請求的時機選擇（首次開啟 vs 功能使用時）和說明文字的設計 — 使用者只有一次機會理解為什麼需要這個權限">Permission 請求時機與措辭&lt;/a>&lt;/li>
&lt;li>畫面狀態矩陣中的網路狀態 → &lt;a href="https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/" data-link-title="模組一：畫面狀態機設計" data-link-desc="畫面狀態矩陣（顯示 / 操作 / 進入 / 退出）— 退出路徑為空 = UX 死胡同">ux-design 模組一 畫面狀態機&lt;/a>&lt;/li>
&lt;li>Server 端背壓如何影響 client UX → &lt;a href="https://tarrragon.github.io/blog/devops/03-traffic-management/backpressure/" data-link-title="背壓機制" data-link-desc="下游處理慢時上游怎麼減速 — 有限 buffer &amp;#43; 回壓訊號的設計、和 rate limit 的區別">DevOps 背壓機制&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>網路 <a href="/blog/ux-design/knowledge-cards/gate/" data-link-title="Gate（UX）" data-link-desc="說明使用者操作流程中「必須通過才能繼續」的關卡，以及成功/失敗/不確定三條路徑的設計責任">gate</a> 和其他 gate 的差異在於狀態的連續性。生物辨識是二元結果（通過或不通過），網路狀態是連續的 — 連線中、已連線、斷線、重新連線、連線但延遲高、連線但頻繁斷開。處理策略取決於功能對即時連線的依賴程度。</p>
<h2 id="三種處理策略">三種處理策略</h2>
<h3 id="offline-first">Offline-first</h3>
<p>功能的核心操作在本地完成，網路用於同步。斷線時使用者仍可操作，重新連線後自動同步差異。</p>
<p>Offline-first 適合的前提是資料可以本地存儲且衝突可以解決。筆記 app、待辦事項、表單填寫 — 使用者的操作產生本地資料，網路只負責把資料同步到 server。</p>
<p>Offline-first 的 UX 設計重點是讓使用者知道同步狀態：已同步、待同步、同步失敗。不需要 gate — 網路狀態不阻擋使用者操作。</p>
<h3 id="retry-with-feedback">Retry with feedback</h3>
<p>功能需要網路但可以等待。斷線時顯示狀態和重試選項，使用者決定要等還是離開。</p>
<p>app_tunnel 的 terminal 連線屬於這個模式。WebSocket 連線需要網路，斷線時使用者無法操作終端機。error 和 disconnected 狀態提供重連按鈕讓使用者手動重試。</p>
<p>Retry 策略的 UX 設計重點：</p>
<ul>
<li>告知使用者發生什麼（「連線中斷」而非空白畫面）</li>
<li>提供手動重試（重連按鈕）</li>
<li>提供退出路徑（返回首頁 — app_tunnel 原本缺少這個）</li>
<li>自動重試要有上限和間隔遞增（避免無限重試消耗電量）</li>
</ul>
<h3 id="degraded-mode">Degraded mode</h3>
<p>功能部分依賴網路。核心功能離線可用，進階功能需要網路。斷線時自動切換到降級模式，不阻擋使用者操作但功能受限。</p>
<p>降級模式的 UX 設計重點是清楚標示哪些功能可用、哪些不可用。「離線模式 — 搜尋功能暫時不可用」比靜默隱藏搜尋按鈕更透明。</p>
<h2 id="網路狀態的-ui-呈現">網路狀態的 UI 呈現</h2>
<h3 id="全域指示器">全域指示器</h3>
<p>在 app 頂部或狀態列顯示「離線」標示。適合網路狀態影響全域功能的 app。</p>
<h3 id="功能級指示器">功能級指示器</h3>
<p>在需要網路的功能旁邊顯示不可用狀態。適合只有部分功能依賴網路的 app。</p>
<h3 id="非侵入式通知">非侵入式通知</h3>
<p>用 Snackbar 或 Toast 短暫顯示「已恢復連線」或「網路中斷」。適合網路狀態偶爾變化的場景。不適合頻繁斷開重連的場景（通知太多會干擾使用者）。</p>
<h2 id="連線但品質差的場景">連線但品質差的場景</h2>
<p>網路存在但延遲高或頻繁斷開，比完全離線更難處理。完全離線時 app 可以立即切換到離線模式；連線不穩定時，每次請求可能成功也可能逾時，使用者體驗是「有時候行有時候不行」。</p>
<p>處理策略：</p>
<ul>
<li>設定合理的逾時時間（太短會把慢回應判定為失敗，太長讓使用者等太久）</li>
<li>逾時後顯示狀態和重試選項，不自動重試（避免在不穩定網路上累積重試）</li>
<li>在 loading 狀態提供取消選項，讓使用者可以中斷等待</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Gate 設計的通用方法論 → <a href="/blog/ux-design/02-gate-fallback/gate-three-questions/" data-link-title="Gate 分類與三問設計法" data-link-desc="每個 gate 設計時問三個問題：成功時做什麼、失敗時做什麼、使用者不知道發生什麼時做什麼">Gate 分類與三問設計法</a></li>
<li>權限請求的 UX 設計 → <a href="/blog/ux-design/02-gate-fallback/permission-request-timing/" data-link-title="Permission 請求時機與措辭" data-link-desc="系統權限請求的時機選擇（首次開啟 vs 功能使用時）和說明文字的設計 — 使用者只有一次機會理解為什麼需要這個權限">Permission 請求時機與措辭</a></li>
<li>畫面狀態矩陣中的網路狀態 → <a href="/blog/ux-design/01-screen-state-machine/" data-link-title="模組一：畫面狀態機設計" data-link-desc="畫面狀態矩陣（顯示 / 操作 / 進入 / 退出）— 退出路徑為空 = UX 死胡同">ux-design 模組一 畫面狀態機</a></li>
<li>Server 端背壓如何影響 client UX → <a href="/blog/devops/03-traffic-management/backpressure/" data-link-title="背壓機制" data-link-desc="下游處理慢時上游怎麼減速 — 有限 buffer &#43; 回壓訊號的設計、和 rate limit 的區別">DevOps 背壓機制</a></li>
</ul>
]]></content:encoded></item><item><title>離線 buffer 與重試</title><link>https://tarrragon.github.io/blog/monitoring/03-sdk-design/offline-buffer/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/03-sdk-design/offline-buffer/</guid><description>&lt;p>離線 buffer 處理的是「事件產生時網路不可用」的場景。記憶體 buffer 有容量上限，離線時間超過 buffer 容量時需要決策：丟棄舊事件、持久化到本地儲存、或兩者混合。每種策略有不同的複雜度和資料保留量的取捨。&lt;/p>
&lt;h2 id="三種策略">三種策略&lt;/h2>
&lt;h3 id="fifo-丟棄最簡單">FIFO 丟棄（最簡單）&lt;/h3>
&lt;p>Buffer 滿時丟棄最舊的事件，保留最新的。整個 buffer 在記憶體中，不做本地 persistence。&lt;/p>
&lt;p>優點：實作最簡單（array + 容量檢查），不需要檔案系統存取，不增加磁碟 I/O。&lt;/p>
&lt;p>代價：離線超過 buffer 容量時，較舊的事件永久遺失。如果離線 30 分鐘、buffer 容量 200 筆、事件產生速率每分鐘 10 筆，前 100 筆（前 10 分鐘）的事件被丟棄。&lt;/p>
&lt;p>適合場景：自用工具（離線場景少、遺失部分事件影響低）、SDK 初期版本（先用最簡單的策略上線）。&lt;/p>
&lt;h3 id="本地-persistence最完整">本地 persistence（最完整）&lt;/h3>
&lt;p>Buffer 滿時把事件寫入本地檔案（SQLite、JSONL 檔案、SharedPreferences / UserDefaults）。網路恢復後從本地檔案讀取並補發。&lt;/p>
&lt;p>優點：離線期間的事件不會遺失（在本地儲存容量內）。&lt;/p>
&lt;p>代價：實作複雜度高 — 需要處理檔案讀寫、並發存取（多執行緒安全）、本地儲存容量管理（磁碟空間上限）、補發時的去重（同一筆事件可能已在記憶體 buffer 中被 flush 過）。&lt;/p>
&lt;p>適合場景：商業產品（使用者在地鐵、電梯、飛航模式下使用）、離線時間長且事件不可遺失的需求。&lt;/p>
&lt;h3 id="混合策略">混合策略&lt;/h3>
&lt;p>記憶體 buffer 處理正常情況和短暫離線。離線超過記憶體 buffer 容量時，溢出的事件寫入本地檔案。網路恢復後先 flush 記憶體 buffer（最新事件），再補發本地檔案中的事件（較舊事件）。&lt;/p>
&lt;p>混合策略的實作複雜度介於兩者之間。本地檔案只在溢出時使用，正常情況下不產生磁碟 I/O。&lt;/p>
&lt;h2 id="恢復後補發">恢復後補發&lt;/h2>
&lt;p>網路恢復後補發離線期間累積的事件，需要處理三個問題：&lt;/p>
&lt;h3 id="補發順序">補發順序&lt;/h3>
&lt;p>離線事件按 timestamp 順序補發，保持事件的時間順序。Collector 端收到的事件 timestamp 可能比當前時間早數小時 — 這是正常的離線補發，collector 應該根據事件的 timestamp 處理，不依賴收到時間。&lt;/p>
&lt;h3 id="補發速率">補發速率&lt;/h3>
&lt;p>一次送出大量離線事件可能讓 collector 過載。分批補發（每批 50-100 筆，間隔 1-2 秒），讓 collector 有時間處理。&lt;/p>
&lt;h3 id="去重">去重&lt;/h3>
&lt;p>同一筆事件可能同時存在於記憶體 buffer 和本地檔案中（寫入本地檔案時 buffer 中也有一份）。Collector 端用事件的唯一識別（timestamp + session_id + name 的組合，或 SDK 產生的 event_id UUID）做去重。&lt;/p>
&lt;h2 id="本地儲存容量管理">本地儲存容量管理&lt;/h2>
&lt;p>本地 persistence 需要設定磁碟使用上限。上限取決於事件大小和保留時間。&lt;/p>
&lt;p>以平均每筆事件 500 bytes 估算：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>上限&lt;/th>
 &lt;th>可儲存事件數&lt;/th>
 &lt;th>備註&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1 MB&lt;/td>
 &lt;td>~2,000&lt;/td>
 &lt;td>約 3 小時（每分鐘 10 筆）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>10 MB&lt;/td>
 &lt;td>~20,000&lt;/td>
 &lt;td>約 33 小時&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>50 MB&lt;/td>
 &lt;td>~100,000&lt;/td>
 &lt;td>約 7 天&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>自用工具 1 MB 足夠（離線場景少）。行動 app 10-50 MB 合理（使用者可能整天離線）。超過上限時用 FIFO 丟棄最舊的本地檔案。&lt;/p>
&lt;h2 id="各平台的本地儲存路徑">各平台的本地儲存路徑&lt;/h2>
&lt;p>本地 persistence 的檔案路徑和格式因平台而異。MVP 階段全用記憶體 FIFO（最簡單策略），本地 persistence 標為第二階段。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>平台&lt;/th>
 &lt;th>建議路徑&lt;/th>
 &lt;th>檔案格式&lt;/th>
 &lt;th>備註&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Flutter&lt;/td>
 &lt;td>&lt;code>getApplicationSupportDirectory()&lt;/code>&lt;/td>
 &lt;td>JSONL&lt;/td>
 &lt;td>不會被 iCloud 備份（和 Documents 不同）、不會被系統自動清理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Python&lt;/td>
 &lt;td>&lt;code>~/.cache/monitor/&lt;/code> 或 &lt;code>platformdirs.user_cache_dir('monitor')&lt;/code>&lt;/td>
 &lt;td>JSONL&lt;/td>
 &lt;td>遵循 XDG 標準、&lt;code>platformdirs&lt;/code> 套件處理跨平台&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>JS/Web&lt;/td>
 &lt;td>&lt;code>localStorage&lt;/code> 或 &lt;code>IndexedDB&lt;/code>&lt;/td>
 &lt;td>JSON&lt;/td>
 &lt;td>localStorage 有 5MB 限制、IndexedDB 更大但 API 較複雜&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>App 被強制終止時（iOS 的 &lt;code>kill&lt;/code>、Android 的 process death），記憶體 buffer 中未 flush 的事件會遺失。Flutter 的 &lt;code>AppLifecycleState.detached&lt;/code> 不保證有時間執行 flush。接受這個遺失 — 強制終止是極端情境，下次啟動時 SDK 重新開始收集。&lt;/p></description><content:encoded><![CDATA[<p>離線 buffer 處理的是「事件產生時網路不可用」的場景。記憶體 buffer 有容量上限，離線時間超過 buffer 容量時需要決策：丟棄舊事件、持久化到本地儲存、或兩者混合。每種策略有不同的複雜度和資料保留量的取捨。</p>
<h2 id="三種策略">三種策略</h2>
<h3 id="fifo-丟棄最簡單">FIFO 丟棄（最簡單）</h3>
<p>Buffer 滿時丟棄最舊的事件，保留最新的。整個 buffer 在記憶體中，不做本地 persistence。</p>
<p>優點：實作最簡單（array + 容量檢查），不需要檔案系統存取，不增加磁碟 I/O。</p>
<p>代價：離線超過 buffer 容量時，較舊的事件永久遺失。如果離線 30 分鐘、buffer 容量 200 筆、事件產生速率每分鐘 10 筆，前 100 筆（前 10 分鐘）的事件被丟棄。</p>
<p>適合場景：自用工具（離線場景少、遺失部分事件影響低）、SDK 初期版本（先用最簡單的策略上線）。</p>
<h3 id="本地-persistence最完整">本地 persistence（最完整）</h3>
<p>Buffer 滿時把事件寫入本地檔案（SQLite、JSONL 檔案、SharedPreferences / UserDefaults）。網路恢復後從本地檔案讀取並補發。</p>
<p>優點：離線期間的事件不會遺失（在本地儲存容量內）。</p>
<p>代價：實作複雜度高 — 需要處理檔案讀寫、並發存取（多執行緒安全）、本地儲存容量管理（磁碟空間上限）、補發時的去重（同一筆事件可能已在記憶體 buffer 中被 flush 過）。</p>
<p>適合場景：商業產品（使用者在地鐵、電梯、飛航模式下使用）、離線時間長且事件不可遺失的需求。</p>
<h3 id="混合策略">混合策略</h3>
<p>記憶體 buffer 處理正常情況和短暫離線。離線超過記憶體 buffer 容量時，溢出的事件寫入本地檔案。網路恢復後先 flush 記憶體 buffer（最新事件），再補發本地檔案中的事件（較舊事件）。</p>
<p>混合策略的實作複雜度介於兩者之間。本地檔案只在溢出時使用，正常情況下不產生磁碟 I/O。</p>
<h2 id="恢復後補發">恢復後補發</h2>
<p>網路恢復後補發離線期間累積的事件，需要處理三個問題：</p>
<h3 id="補發順序">補發順序</h3>
<p>離線事件按 timestamp 順序補發，保持事件的時間順序。Collector 端收到的事件 timestamp 可能比當前時間早數小時 — 這是正常的離線補發，collector 應該根據事件的 timestamp 處理，不依賴收到時間。</p>
<h3 id="補發速率">補發速率</h3>
<p>一次送出大量離線事件可能讓 collector 過載。分批補發（每批 50-100 筆，間隔 1-2 秒），讓 collector 有時間處理。</p>
<h3 id="去重">去重</h3>
<p>同一筆事件可能同時存在於記憶體 buffer 和本地檔案中（寫入本地檔案時 buffer 中也有一份）。Collector 端用事件的唯一識別（timestamp + session_id + name 的組合，或 SDK 產生的 event_id UUID）做去重。</p>
<h2 id="本地儲存容量管理">本地儲存容量管理</h2>
<p>本地 persistence 需要設定磁碟使用上限。上限取決於事件大小和保留時間。</p>
<p>以平均每筆事件 500 bytes 估算：</p>
<table>
  <thead>
      <tr>
          <th>上限</th>
          <th>可儲存事件數</th>
          <th>備註</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1 MB</td>
          <td>~2,000</td>
          <td>約 3 小時（每分鐘 10 筆）</td>
      </tr>
      <tr>
          <td>10 MB</td>
          <td>~20,000</td>
          <td>約 33 小時</td>
      </tr>
      <tr>
          <td>50 MB</td>
          <td>~100,000</td>
          <td>約 7 天</td>
      </tr>
  </tbody>
</table>
<p>自用工具 1 MB 足夠（離線場景少）。行動 app 10-50 MB 合理（使用者可能整天離線）。超過上限時用 FIFO 丟棄最舊的本地檔案。</p>
<h2 id="各平台的本地儲存路徑">各平台的本地儲存路徑</h2>
<p>本地 persistence 的檔案路徑和格式因平台而異。MVP 階段全用記憶體 FIFO（最簡單策略），本地 persistence 標為第二階段。</p>
<table>
  <thead>
      <tr>
          <th>平台</th>
          <th>建議路徑</th>
          <th>檔案格式</th>
          <th>備註</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Flutter</td>
          <td><code>getApplicationSupportDirectory()</code></td>
          <td>JSONL</td>
          <td>不會被 iCloud 備份（和 Documents 不同）、不會被系統自動清理</td>
      </tr>
      <tr>
          <td>Python</td>
          <td><code>~/.cache/monitor/</code> 或 <code>platformdirs.user_cache_dir('monitor')</code></td>
          <td>JSONL</td>
          <td>遵循 XDG 標準、<code>platformdirs</code> 套件處理跨平台</td>
      </tr>
      <tr>
          <td>JS/Web</td>
          <td><code>localStorage</code> 或 <code>IndexedDB</code></td>
          <td>JSON</td>
          <td>localStorage 有 5MB 限制、IndexedDB 更大但 API 較複雜</td>
      </tr>
  </tbody>
</table>
<p>App 被強制終止時（iOS 的 <code>kill</code>、Android 的 process death），記憶體 buffer 中未 flush 的事件會遺失。Flutter 的 <code>AppLifecycleState.detached</code> 不保證有時間執行 flush。接受這個遺失 — 強制終止是極端情境，下次啟動時 SDK 重新開始收集。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>攢批送出策略 → <a href="/blog/monitoring/03-sdk-design/batch-flush/" data-link-title="攢批送出策略" data-link-desc="flush interval / buffer size / flush on close 三個控制點決定事件何時離開 SDK — 平衡即時性和網路效率">攢批送出策略</a></li>
<li>SDK 端的資料脫敏 → <a href="/blog/monitoring/03-sdk-design/redaction-helper/" data-link-title="SDK redaction helper" data-link-desc="在事件離開 SDK 前移除敏感資訊 — 預設 redaction rule 處理常見 pattern，自訂 rule 處理業務特定的 secret">SDK redaction helper</a></li>
<li>Collector 端如何處理補發事件 → <a href="/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">模組四 Collector 設計</a></li>
<li>從 SDK 到 storage 的端到端資料損失地圖 → <a href="/blog/monitoring/04-collector/data-integrity/" data-link-title="端到端資料完整性" data-link-desc="從 SDK 到 storage 的資料損失地圖 — 每個環節的損失類型、控制策略、完整性指標、被自己 SDK DDoS 的防護">端到端資料完整性</a></li>
</ul>
]]></content:encoded></item></channel></rss>