<?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>Budget on Tarragon</title><link>https://tarrragon.github.io/blog/tags/budget/</link><description>Recent content in Budget on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 27 Oct 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/budget/index.xml" rel="self" type="application/rss+xml"/><item><title>租用雲端主機之前的費用規劃：費用訊號的延遲、停止之前的最大超支、扣款的信用卡額度、配額與預算門檻的計算，以及停損停到哪一層</title><link>https://tarrragon.github.io/blog/devops/operations/08-cost-management/spend-planning-before-renting/</link><pubDate>Tue, 27 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/operations/08-cost-management/spend-planning-before-renting/</guid><description>&lt;p>這篇整理在雲端租用主機之前就要決定的幾個費用上的值，以及它們怎麼從同一組輸入推出來。各節依序是：費用在什麼時間點開始產生，以及免費額度以整個資源判定時超出的是多少；各種費用訊號（資源的用量計數、帳務明細、預算通知、扣款）各自落後多久；由停止延遲與配額允許的最大花費速度，推出防護機制觸發之後到費用停止之前最多再花的金額；扣款的信用卡或帳戶要留多大的額度；平台上的上限分成擋下建立、停止用量與只發通知三種，各自要決定的值怎麼算；停損要停到哪一層、由誰執行；最後把這些值依推算的先後整理成一張依賴表。依基線偵測費用突增的異常告警讀的也是帳務資料，延遲不少於帳務明細，它的設計在 &lt;a href="https://tarrragon.github.io/blog/devops/operations/08-cost-management/cost-monitoring/" data-link-title="成本監控與告警" data-link-desc="要讓雲端帳單不失控時，靠歸因把每筆花費追到擁有者、用異常告警抓突增、以及選 showback 或 chargeback 讓成本有人負責">成本監控與告警&lt;/a>。各家機制以 Oracle Cloud、Google Cloud 與 AWS 為例，出處與讀取日期寫在句子裡。帳單由哪些維度組成在 &lt;a href="https://tarrragon.github.io/blog/devops/operations/08-cost-management/billing-models/" data-link-title="計費模式理解" data-link-desc="看懂雲端帳單怎麼生成時，先認清收費的維度、on-demand/reserved/savings plan/spot 的承諾差異、以及 egress 這類最常被忽略的隱藏成本">計費模式理解&lt;/a>；預知的大流量要擴容多少、編多少預算，在 &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/planned-peak-sizing-and-budget/" data-link-title="預知的大流量要加多大、編多少預算：從業務預估到擴容規格與費用區間的換算鏈、給決策者與會計的數字，以及每次活動之後的校準" data-link-desc="行銷活動、檔期這類事先知道的大流量，容量規劃怎麼從業務預估換算成峰值請求量、各層要開幾台與擴容的計費時數，再換算成費用區間；三點估計怎麼帶著預估的誤差，給決策者的選項要附規格、費用、不足時的後果與回退方式，給會計的預算要對齊增量與付款時點，以及每次活動之後校準換算係數、沒有歷史資料時怎麼起步">預知的大流量要加多大、編多少預算：從業務預估到擴容規格與費用區間的換算鏈、給決策者與會計的數字，以及每次活動之後的校準&lt;/a>，這裡不重寫。&lt;/p>
&lt;p>這幾個值放在租用之前決定，理由在決定時的處境。租用之前，決定的人可以不受時間壓力，從計畫推出它們；等到費用失控時才決定，決定的人要在帳務明細落後、費用持續累積的情況下選處置，選得越久花得越多。&lt;/p>
&lt;h2 id="費用在什麼時間點開始產生">費用在什麼時間點開始產生&lt;/h2>
&lt;p>雲端的費用依它跟資源的關係分成兩種，兩種開始計費的時點不同：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>跟著資源存在計費&lt;/strong>：主機與磁碟這類資源，從建立成功的那一刻開始計費，跟有沒有人在用無關；保留的公開 IP 依業者，有些在沒有附在執行中的主機上時另外計費。主機停止之後運算費用是否停止依機型而定（Oracle Cloud 的 Dense I/O、HPC 與多數 GPU 機型停止後照常計費，要終止才停，見 &lt;a href="https://docs.oracle.com/en-us/iaas/Content/Compute/Tasks/resource-billing-stopped-instances.htm">Resource Billing for Stopped Instances&lt;/a>，2026-10-27 讀），磁碟照常計費；各種暫停方式各自留下哪些計費的資源，見 &lt;a href="https://tarrragon.github.io/blog/devops/operations/09-host-lifecycle/suspend-and-resume-service/" data-link-title="服務暫停與恢復上線：只停服務、停止主機、終止主機到全部刪除各自保留的資源，暫停期間仍計費與仍被指向的資源，恢復前要查的漂移與恢復的順序" data-link-desc="一台主機上的服務要暫停一段時間再恢復：只停服務、停止主機、終止主機保留設定、全部刪除靠 repo 重建各自保留什麼與付出什麼，怎麼依保留的月費、重建的成本與漂移量選擇，暫停期間仍在計費的資源（含在作業系統裡 shutdown 不停止計費），仍被 DNS 與 CDN 指向的結果（Cloudflare 521、522，釋放 IP 之後的懸空 DNS），恢復前要重新確認的漂移，以及依元件依賴排出的恢復順序與驗證">服務暫停與恢復上線：只停服務、停止主機、終止主機到全部刪除各自保留的資源，暫停期間仍計費與仍被指向的資源，恢復前要查的漂移與恢復的順序&lt;/a>〈暫停期間仍在計費的資源〉。&lt;/li>
&lt;li>&lt;strong>跟著用量計費&lt;/strong>：出站流量、API 呼叫次數、函式的執行時間，費用跟用量成正比，用量增加的速度決定費用增加的速度。&lt;/li>
&lt;/ul>
&lt;p>免費額度讓「開始計費」多了一個判定：一個資源建立時算不算在免費額度裡，而判定的單位決定了超出時多付的是多少。Oracle Cloud 的區塊儲存是以整顆磁碟判定：2026-10-27 在一個付費帳戶上，開機磁碟合計已經用掉免費額度 200 GB 裡的 141 GB 時，再建一顆 60 GB 的開機磁碟，免費額度的用量停在 141、全部磁碟的合計變成 201，那一顆整顆沒有算進免費額度，而不是只有超出的 1 GB 在額度外（額度用量的實測與帳務明細上的項目，見 &lt;a href="https://tarrragon.github.io/blog/devops/infra/cloud-providers/oracle-cloud/hands-on/private-ubuntu-egress-console-rebuild/" data-link-title="Oracle Cloud 私有子網路 Ubuntu 主機的實測紀錄：套件來源的出站（NAT gateway、路由與 NSG）、序列主控台的連線，以及被回收之後的重建" data-link-desc="要在 Oracle Cloud 沒有公開 IP 的私有子網路裡跑一台 Ubuntu 主機、讓它裝得到套件更新，或要經由序列主控台看主機的開機過程，或主機被閒置回收、停止或刪除之後要照原樣重建，或想知道 NAT gateway 要不要另外付費、開機磁碟扣掉多少區塊磁碟額度、合計超過 200 GB 時放不進額度的那一顆怎麼算、怎麼用 quota 擋下超額的建立、線上加大開機磁碟超過額度時怎麼算、空出額度之後付費磁碟多久轉成免費時，查實際跑過的指令、錯誤原文、重建腳本與每一步的讀法">Oracle Cloud 私有子網路 Ubuntu 主機的實測紀錄：套件來源的出站（NAT gateway、路由與 NSG）、序列主控台的連線，以及被回收之後的重建&lt;/a>〈費用與額度：A1、開機磁碟與區塊儲存額度、出站流量與 NAT gateway〉）。所以「還剩多少免費額度」要拿下一個資源的大小去比，不能拿超出的量去估費用。各家免費額度的範圍與試用結束後的處置，見 &lt;a href="https://tarrragon.github.io/blog/devops/infra/cloud-providers/pricing-comparison/" data-link-title="雲端免費額度與小型主機的收費比較：試用金與固定額度、計費單位、區域限制、試用結束後的處置、出站流量與查價項目" data-link-desc="Oracle Cloud、Google Cloud 與 AWS 的免費方案分別由哪幾種額度組成，比較時要換算的計費單位，免費主機能放在哪些區域，試用期結束後帳號與資源會怎樣，以及付費主機的價格該去哪裡查">雲端免費額度與小型主機的收費比較：試用金與固定額度、計費單位、區域限制、試用結束後的處置、出站流量與查價項目&lt;/a>。&lt;/p>
&lt;h2 id="費用訊號的延遲從資源建立到扣款">費用訊號的延遲：從資源建立到扣款&lt;/h2>
&lt;p>一筆費用從發生到有人收到通知、再到從信用卡扣走，中間經過幾種訊號，每一種落後的時間不同。依出現的先後：&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>資源清單與用量計數&lt;/td>
 &lt;td>現在有哪些資源、各種限制用掉多少&lt;/td>
 &lt;td>幾乎即時。Oracle Cloud 的 &lt;code>oci limits&lt;/code> 在主機建好之後幾分鐘內查到的用量已經包含它（2026-10-27 實測）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>帳務明細（逐小時或逐日）&lt;/td>
 &lt;td>每個資源在每個時段花了多少錢&lt;/td>
 &lt;td>數小時到兩天。Oracle Cloud 的 usage API 逐小時資料，2026-10-27 實測時大約落後 2 小時；Oracle Cloud 的 Cost Analysis 頁寫資料最多 48 小時才出現、最近 24 小時是預測值；Google Cloud 寫費用明細通常一天內出現、有時超過 24 小時；AWS 寫 Cost Explorer 至少每 24 小時更新一次、部分資料可能更晚&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>預算通知&lt;/td>
 &lt;td>花費達到預算門檻&lt;/td>
 &lt;td>帳務資料的延遲再加上預算的評估週期（兩者相加是推論，各家沒有寫出合計）。Oracle Cloud 的預算每 24 小時評估一次；AWS Budgets 每天最多更新三次、通常間隔 8 到 12 小時；Google Cloud 一天送好幾則，建立預算之後第一則可能要數小時&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>扣款&lt;/td>
 &lt;td>費用從信用卡或帳戶扣走&lt;/td>
 &lt;td>月結後付的帳戶在帳單週期結束之後；門檻扣款的帳戶另外在餘額達到門檻時（下面〈扣款的信用卡或帳戶要留多大的額度〉）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>官方出處（2026-10-27 讀）：Oracle Cloud 的 &lt;a href="https://docs.oracle.com/en-us/iaas/Content/Billing/Concepts/costanalysisoverview.htm">Cost Analysis&lt;/a> 與 &lt;a href="https://docs.oracle.com/en-us/iaas/Content/Billing/Concepts/budgetsoverview.htm">Budgets&lt;/a>；Google Cloud 的 &lt;a href="https://docs.cloud.google.com/billing/docs/how-to/reports">Analyze billing data and cost trends with Reports&lt;/a> 與 &lt;a href="https://docs.cloud.google.com/billing/docs/how-to/budgets">Create, edit, or delete budgets and budget alerts&lt;/a>；AWS 的 &lt;a href="https://docs.aws.amazon.com/cost-management/latest/userguide/ce-what-is.html">Analyzing your costs and usage with AWS Cost Explorer&lt;/a> 與 &lt;a href="https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-managing-costs.html">Managing your costs with AWS Budgets&lt;/a>。Google Cloud 預算通知的實測間隔，在 &lt;a href="https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/hands-on/budget-killswitch-walkthrough/" data-link-title="GCP 預算警示與停損開關的實測紀錄：建預算、Pub/Sub 主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與停用後的恢復" data-link-desc="要在 GCP 專案上建預算警示，或建一個花費超過預算就解除帳單連結的停損開關，想照一次實跑過的順序做、看每一步漏掉或換一種做法的結果時讀；也用在函式部署成功、狀態 ACTIVE 卻從沒解除過帳單連結，或專案因超支被停用之後要恢復的時候">GCP 預算警示與停損開關的實測紀錄：建預算、Pub/Sub 主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與停用後的恢復&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>這篇整理在雲端租用主機之前就要決定的幾個費用上的值，以及它們怎麼從同一組輸入推出來。各節依序是：費用在什麼時間點開始產生，以及免費額度以整個資源判定時超出的是多少；各種費用訊號（資源的用量計數、帳務明細、預算通知、扣款）各自落後多久；由停止延遲與配額允許的最大花費速度，推出防護機制觸發之後到費用停止之前最多再花的金額；扣款的信用卡或帳戶要留多大的額度；平台上的上限分成擋下建立、停止用量與只發通知三種，各自要決定的值怎麼算；停損要停到哪一層、由誰執行；最後把這些值依推算的先後整理成一張依賴表。依基線偵測費用突增的異常告警讀的也是帳務資料，延遲不少於帳務明細，它的設計在 <a href="/blog/devops/operations/08-cost-management/cost-monitoring/" data-link-title="成本監控與告警" data-link-desc="要讓雲端帳單不失控時，靠歸因把每筆花費追到擁有者、用異常告警抓突增、以及選 showback 或 chargeback 讓成本有人負責">成本監控與告警</a>。各家機制以 Oracle Cloud、Google Cloud 與 AWS 為例，出處與讀取日期寫在句子裡。帳單由哪些維度組成在 <a href="/blog/devops/operations/08-cost-management/billing-models/" data-link-title="計費模式理解" data-link-desc="看懂雲端帳單怎麼生成時，先認清收費的維度、on-demand/reserved/savings plan/spot 的承諾差異、以及 egress 這類最常被忽略的隱藏成本">計費模式理解</a>；預知的大流量要擴容多少、編多少預算，在 <a href="/blog/devops/operations/05-capacity-planning/planned-peak-sizing-and-budget/" data-link-title="預知的大流量要加多大、編多少預算：從業務預估到擴容規格與費用區間的換算鏈、給決策者與會計的數字，以及每次活動之後的校準" data-link-desc="行銷活動、檔期這類事先知道的大流量，容量規劃怎麼從業務預估換算成峰值請求量、各層要開幾台與擴容的計費時數，再換算成費用區間；三點估計怎麼帶著預估的誤差，給決策者的選項要附規格、費用、不足時的後果與回退方式，給會計的預算要對齊增量與付款時點，以及每次活動之後校準換算係數、沒有歷史資料時怎麼起步">預知的大流量要加多大、編多少預算：從業務預估到擴容規格與費用區間的換算鏈、給決策者與會計的數字，以及每次活動之後的校準</a>，這裡不重寫。</p>
<p>這幾個值放在租用之前決定，理由在決定時的處境。租用之前，決定的人可以不受時間壓力，從計畫推出它們；等到費用失控時才決定，決定的人要在帳務明細落後、費用持續累積的情況下選處置，選得越久花得越多。</p>
<h2 id="費用在什麼時間點開始產生">費用在什麼時間點開始產生</h2>
<p>雲端的費用依它跟資源的關係分成兩種，兩種開始計費的時點不同：</p>
<ul>
<li><strong>跟著資源存在計費</strong>：主機與磁碟這類資源，從建立成功的那一刻開始計費，跟有沒有人在用無關；保留的公開 IP 依業者，有些在沒有附在執行中的主機上時另外計費。主機停止之後運算費用是否停止依機型而定（Oracle Cloud 的 Dense I/O、HPC 與多數 GPU 機型停止後照常計費，要終止才停，見 <a href="https://docs.oracle.com/en-us/iaas/Content/Compute/Tasks/resource-billing-stopped-instances.htm">Resource Billing for Stopped Instances</a>，2026-10-27 讀），磁碟照常計費；各種暫停方式各自留下哪些計費的資源，見 <a href="/blog/devops/operations/09-host-lifecycle/suspend-and-resume-service/" data-link-title="服務暫停與恢復上線：只停服務、停止主機、終止主機到全部刪除各自保留的資源，暫停期間仍計費與仍被指向的資源，恢復前要查的漂移與恢復的順序" data-link-desc="一台主機上的服務要暫停一段時間再恢復：只停服務、停止主機、終止主機保留設定、全部刪除靠 repo 重建各自保留什麼與付出什麼，怎麼依保留的月費、重建的成本與漂移量選擇，暫停期間仍在計費的資源（含在作業系統裡 shutdown 不停止計費），仍被 DNS 與 CDN 指向的結果（Cloudflare 521、522，釋放 IP 之後的懸空 DNS），恢復前要重新確認的漂移，以及依元件依賴排出的恢復順序與驗證">服務暫停與恢復上線：只停服務、停止主機、終止主機到全部刪除各自保留的資源，暫停期間仍計費與仍被指向的資源，恢復前要查的漂移與恢復的順序</a>〈暫停期間仍在計費的資源〉。</li>
<li><strong>跟著用量計費</strong>：出站流量、API 呼叫次數、函式的執行時間，費用跟用量成正比，用量增加的速度決定費用增加的速度。</li>
</ul>
<p>免費額度讓「開始計費」多了一個判定：一個資源建立時算不算在免費額度裡，而判定的單位決定了超出時多付的是多少。Oracle Cloud 的區塊儲存是以整顆磁碟判定：2026-10-27 在一個付費帳戶上，開機磁碟合計已經用掉免費額度 200 GB 裡的 141 GB 時，再建一顆 60 GB 的開機磁碟，免費額度的用量停在 141、全部磁碟的合計變成 201，那一顆整顆沒有算進免費額度，而不是只有超出的 1 GB 在額度外（額度用量的實測與帳務明細上的項目，見 <a href="/blog/devops/infra/cloud-providers/oracle-cloud/hands-on/private-ubuntu-egress-console-rebuild/" data-link-title="Oracle Cloud 私有子網路 Ubuntu 主機的實測紀錄：套件來源的出站（NAT gateway、路由與 NSG）、序列主控台的連線，以及被回收之後的重建" data-link-desc="要在 Oracle Cloud 沒有公開 IP 的私有子網路裡跑一台 Ubuntu 主機、讓它裝得到套件更新，或要經由序列主控台看主機的開機過程，或主機被閒置回收、停止或刪除之後要照原樣重建，或想知道 NAT gateway 要不要另外付費、開機磁碟扣掉多少區塊磁碟額度、合計超過 200 GB 時放不進額度的那一顆怎麼算、怎麼用 quota 擋下超額的建立、線上加大開機磁碟超過額度時怎麼算、空出額度之後付費磁碟多久轉成免費時，查實際跑過的指令、錯誤原文、重建腳本與每一步的讀法">Oracle Cloud 私有子網路 Ubuntu 主機的實測紀錄：套件來源的出站（NAT gateway、路由與 NSG）、序列主控台的連線，以及被回收之後的重建</a>〈費用與額度：A1、開機磁碟與區塊儲存額度、出站流量與 NAT gateway〉）。所以「還剩多少免費額度」要拿下一個資源的大小去比，不能拿超出的量去估費用。各家免費額度的範圍與試用結束後的處置，見 <a href="/blog/devops/infra/cloud-providers/pricing-comparison/" data-link-title="雲端免費額度與小型主機的收費比較：試用金與固定額度、計費單位、區域限制、試用結束後的處置、出站流量與查價項目" data-link-desc="Oracle Cloud、Google Cloud 與 AWS 的免費方案分別由哪幾種額度組成，比較時要換算的計費單位，免費主機能放在哪些區域，試用期結束後帳號與資源會怎樣，以及付費主機的價格該去哪裡查">雲端免費額度與小型主機的收費比較：試用金與固定額度、計費單位、區域限制、試用結束後的處置、出站流量與查價項目</a>。</p>
<h2 id="費用訊號的延遲從資源建立到扣款">費用訊號的延遲：從資源建立到扣款</h2>
<p>一筆費用從發生到有人收到通知、再到從信用卡扣走，中間經過幾種訊號，每一種落後的時間不同。依出現的先後：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>它記錄的是什麼</th>
          <th>落後多久</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>資源清單與用量計數</td>
          <td>現在有哪些資源、各種限制用掉多少</td>
          <td>幾乎即時。Oracle Cloud 的 <code>oci limits</code> 在主機建好之後幾分鐘內查到的用量已經包含它（2026-10-27 實測）</td>
      </tr>
      <tr>
          <td>帳務明細（逐小時或逐日）</td>
          <td>每個資源在每個時段花了多少錢</td>
          <td>數小時到兩天。Oracle Cloud 的 usage API 逐小時資料，2026-10-27 實測時大約落後 2 小時；Oracle Cloud 的 Cost Analysis 頁寫資料最多 48 小時才出現、最近 24 小時是預測值；Google Cloud 寫費用明細通常一天內出現、有時超過 24 小時；AWS 寫 Cost Explorer 至少每 24 小時更新一次、部分資料可能更晚</td>
      </tr>
      <tr>
          <td>預算通知</td>
          <td>花費達到預算門檻</td>
          <td>帳務資料的延遲再加上預算的評估週期（兩者相加是推論，各家沒有寫出合計）。Oracle Cloud 的預算每 24 小時評估一次；AWS Budgets 每天最多更新三次、通常間隔 8 到 12 小時；Google Cloud 一天送好幾則，建立預算之後第一則可能要數小時</td>
      </tr>
      <tr>
          <td>扣款</td>
          <td>費用從信用卡或帳戶扣走</td>
          <td>月結後付的帳戶在帳單週期結束之後；門檻扣款的帳戶另外在餘額達到門檻時（下面〈扣款的信用卡或帳戶要留多大的額度〉）</td>
      </tr>
  </tbody>
</table>
<p>官方出處（2026-10-27 讀）：Oracle Cloud 的 <a href="https://docs.oracle.com/en-us/iaas/Content/Billing/Concepts/costanalysisoverview.htm">Cost Analysis</a> 與 <a href="https://docs.oracle.com/en-us/iaas/Content/Billing/Concepts/budgetsoverview.htm">Budgets</a>；Google Cloud 的 <a href="https://docs.cloud.google.com/billing/docs/how-to/reports">Analyze billing data and cost trends with Reports</a> 與 <a href="https://docs.cloud.google.com/billing/docs/how-to/budgets">Create, edit, or delete budgets and budget alerts</a>；AWS 的 <a href="https://docs.aws.amazon.com/cost-management/latest/userguide/ce-what-is.html">Analyzing your costs and usage with AWS Cost Explorer</a> 與 <a href="https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-managing-costs.html">Managing your costs with AWS Budgets</a>。Google Cloud 預算通知的實測間隔，在 <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/budget-killswitch-walkthrough/" data-link-title="GCP 預算警示與停損開關的實測紀錄：建預算、Pub/Sub 主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與停用後的恢復" data-link-desc="要在 GCP 專案上建預算警示，或建一個花費超過預算就解除帳單連結的停損開關，想照一次實跑過的順序做、看每一步漏掉或換一種做法的結果時讀；也用在函式部署成功、狀態 ACTIVE 卻從沒解除過帳單連結，或專案因超支被停用之後要恢復的時候">GCP 預算警示與停損開關的實測紀錄：建預算、Pub/Sub 主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與停用後的恢復</a>。</p>
<p>這張表的讀法是：越晚出現的訊號越接近實際的金額。用量計數沒有金額，但它是這幾種訊號裡唯一幾乎即時的；它只涵蓋跟著資源存在計費的那幾項（資源的數量與容量），跟著用量計費的出站流量、API 呼叫仍要等帳務明細。帳務明細與預算通知有金額，代價是落後數小時到兩天。所以「現在有沒有資源或用量超出計畫」看用量計數，「實際花了多少」看帳務明細，兩件事用不同的訊號判斷。</p>
<h2 id="停止之前的最大超支配額訊號延遲與處置延遲">停止之前的最大超支：配額、訊號延遲與處置延遲</h2>
<p>費用失控指的是費用以超出計畫的速度增加，而增加的部分在人或程式停止它之前持續累積。防護機制的觸發條件（預算門檻、用量超過計畫量）成立之後，費用不會當場停止，觸發條件成立到費用停止之間還會再花一段，這一段的金額由兩個量決定：</p>
<ul>
<li><strong>最大花費速度</strong>：最壞情況下每小時花多少，計畫內與超出計畫的部分都算在內，因為延遲期間兩者都在累積。跟著資源存在計費的那一部分由平台上「能開多少資源」決定，也就是配額允許的量乘上每單位每小時的價格。拿到外洩憑證的人可以直接建資源，不經過自動擴縮，所以這裡要用配額，不能用自動擴縮的最大台數；<a href="/blog/backend/knowledge-cards/autoscaling/" data-link-title="Autoscaling" data-link-desc="說明系統如何依負載自動調整服務實例數量">自動擴縮</a>的上限限得住的是服務自己出錯的那一類失控，它是跟著資源存在計費那一部分的單月上限，理由在 <a href="/blog/devops/operations/05-capacity-planning/cost-model/" data-link-title="成本模型" data-link-desc="把容量規劃的資源翻成錢時，釐清 on-demand、reserved、spot 三種計費模式的取捨、怎麼算單位請求成本、以及過度與不足配置各自的經濟代價">成本模型</a>〈Autoscaler 的 max 是財務斷路器〉。跟著用量計費的那一部分（出站流量、API 呼叫、函式執行）要分開看。API 呼叫與函式執行在部分服務上有平台的上限：請求速率的配額、函式的並行上限、Cloud Run 這類服務的最大執行個體數，例如 Google Cloud 可以把 API 配額調到預設值以下（<a href="https://docs.cloud.google.com/docs/quotas/view-manage">View and manage quotas</a>，2026-10-27 讀，不是每個服務都支援）；它們跟配額一樣是超過就拒絕，沒有延遲。出站流量多半沒有這種平台上限（推論），限住它的做法是應用層的<a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">速率限制</a>，或改用含流量的計價方式（每月固定的免費出站量、含流量的主機方案、前面放 CDN，各家出站流量的額度見 <a href="/blog/devops/infra/cloud-providers/pricing-comparison/" data-link-title="雲端免費額度與小型主機的收費比較：試用金與固定額度、計費單位、區域限制、試用結束後的處置、出站流量與查價項目" data-link-desc="Oracle Cloud、Google Cloud 與 AWS 的免費方案分別由哪幾種額度組成，比較時要換算的計費單位，免費主機能放在哪些區域，試用期結束後帳號與資源會怎樣，以及付費主機的價格該去哪裡查">雲端免費額度與小型主機的收費比較：試用金與固定額度、計費單位、區域限制、試用結束後的處置、出站流量與查價項目</a>〈出站流量與閒置回收〉）；兩者都沒有時，這一部分的最壞情況沒有上限，只能靠偵測，而且用量計數偵測不到它，要等帳務明細。</li>
<li><strong>停止延遲</strong>：偵測延遲加上處置延遲，從觸發條件實際成立的那一刻起算。偵測延遲是從觸發條件成立到偵測的訊號出現：用預算通知偵測時，是帳務明細落後的時間再加上預算的評估週期；用用量計數偵測時，是查詢的間隔加上用量計數本身落後的幾分鐘。處置延遲是從那個訊號出現到費用停止：執行處置的人或程式多久動作，加上動作之後資源多久停止計費（計費的最小單位依業者與資源而不同，要查各家的價目說明）。由人執行時，人判斷與處理的時間就算在處置延遲裡。</li>
</ul>
<p>防護機制的觸發條件有兩種：花費達到某個金額（下文稱<strong>觸發處置的門檻</strong>，由預算通知偵測），或用量超過計畫量（由用量計數偵測）。最大花費速度乘上停止延遲，下文稱為<strong>停止之前的最大超支</strong>：觸發條件成立之後，到費用停止之前最多再花的金額。一個月最壞的總花費由三段相加：觸發條件成立之前花的錢，加上停止之前的最大超支，再加上停止之後仍在累積的計畫內花費（處置停掉全部花費時是 0）。觸發條件是金額門檻時，第一段不超過門檻本身；觸發條件是用量超過計畫量時，第一段不超過計畫內每月的花費（推論）。後面的門檻與信用卡額度都從這個三段的和推出來。</p>
<p>這三段只算得到按時間累積的費用。另有兩類費用不是速度乘上時間，停損的任何一層都停不掉，要另外加進最壞的總花費：一是一次性的購買，例如預留執行個體（AWS 的 Reserved Instances 選全額預付時在期初付清、買了不能取消，見 <a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/reserved-instances-fundamentals.html">Reserved Instances for Amazon EC2 overview</a>〈Payment options〉，2026-10-27 讀）、Marketplace 訂閱、網域註冊，拿到憑證的人一個呼叫就能買下，配額擋不住；二是承諾用量（Savings Plans、承諾使用折扣、按月付的預留執行個體），合約期間每月照扣。一次性購買的上限由權限決定：用 IAM 或 SCP 拒絕這幾類購買動作，它就跟配額一樣沒有延遲；沒有拒絕時，它的上限是權限允許的購買金額。承諾用量是計畫內就知道的固定金額，算進「停止之後仍在累積的計畫內花費」。</p>
<p>示例：偵測靠預算通知，偵測延遲取〈費用訊號的延遲：從資源建立到扣款〉那張表裡 Oracle Cloud 文件寫的上限，帳務明細最多落後 48 小時、預算每 24 小時評估一次，假設兩者直接相加；處置由人執行，收到通知到處理完要 12 小時；最大花費速度每小時 5 美元。12 小時與 5 美元是為了說明算法而設的，不是任何業者的數字。停止延遲最長約 48 ＋ 24 ＋ 12 ＝ 84 小時，停止之前的最大超支約 5 × 84 ＝ 420 美元。同樣是多開資源造成的失控，改成看用量計數、由程式停止資源，假設每 10 分鐘查一次、用量計數落後約 5 分鐘、程式約 5 分鐘內停止、停止之後在 1 小時內不再計費，停止延遲最長約 1 小時 20 分，停止之前的最大超支約 5 × 1.33 ≈ 7 美元。出站流量造成的失控，用量計數偵測不到，偵測仍要靠預算通知；假設應用層的速率限制把它限在每小時 5 美元以內，處置由人執行時仍是 420 美元，改由預算通知觸發的程式處置時約 5 ×（48 ＋ 24）＝ 360 美元。</p>
<p>停止之前的最大超支是後面每一個值的輸入。依它算出的觸發處置的門檻（下面〈平台上的上限：擋下建立、停止用量與只發通知〉的算法），如果比計畫內每月的花費還小，正常使用就會觸發處置；這時要回頭降低兩個量之一：收緊配額讓最大花費速度變小，或縮短停止延遲，也就是改用落後較少的訊號偵測、改由程式處置。反過來說，下文的<strong>能接受的停止延遲</strong>，指的是每月能接受的上限、配額與計畫內花費都給定時，算出來的門檻不會落到計畫內花費以下所允許的最長停止延遲。</p>
<p>失控的來源分成兩類，兩類的花費速度與處置不同。服務自己出錯（無限重試、遞迴觸發、擴縮上限設太高）時，修好程式費用就停。資源被別人濫用（外洩的 API 金鑰、被入侵的主機拿去挖礦）時，濫用者可以把配額允許的量用滿，最壞情況下花費速度直接到最大花費速度（推論）；處置要先撤銷外洩的憑證、停掉被入侵的資源，再處理費用，只停資源不撤銷憑證，濫用者可以再建一份。兩類的細節與 Google Cloud 上的例子在 <a href="/blog/devops/infra/cloud-providers/gcp/spend-protection-gcp-budget-killswitch/" data-link-title="GCP 上的雲端費用失控防護：預算警示、強制執行支出上限與自動停用帳單的作用範圍、反應時間與判斷" data-link-desc="GCP 上為費用失控設防的機制：預算警示寄出通知的時機、範圍、抵免與通知對象、強制執行支出上限涵蓋的服務與暫停期間，以預算通知觸發函式停止用量的兩種處置（解除帳單連結、只停止花錢的資源）該選哪一種，帳單資料延遲對這些機制反應時間的影響，停損開關放在另一個專案的理由，以及驗收為什麼分成真實通知與測試訊息兩段、停用後的恢復為什麼要先調預算">GCP 上的雲端費用失控防護：預算警示、強制執行支出上限與自動停用帳單的作用範圍、反應時間與判斷</a>〈費用失控的來源與反應時間〉。</p>
<h2 id="扣款的信用卡或帳戶要留多大的額度">扣款的信用卡或帳戶要留多大的額度</h2>
<p>這一節講的是用信用卡付款的隨用隨付帳戶；先預付或簽約付款的方案（例如 Oracle Cloud 的 Universal Credits）用發票付款，扣款與額度照合約，預付的金額也會像抵免一樣讓帳務上的淨額偏低。扣款的時點決定了信用卡一次要付多少。三家的扣款方式有兩種：</p>
<ul>
<li><strong>月結後付</strong>：帳單週期結束之後，一次扣整個週期的費用。Oracle Cloud 的 Pay As You Go 依上個月的實際用量出帳，信用卡付款「立即到期」（原文「When you pay by credit card, the payment term is immediate」，<a href="https://docs.oracle.com/en/cloud/get-started/subscriptions-cloud/ocpib/understanding-your-invoice.html">Understanding Your Invoice</a>）；AWS 在每月初扣預設付款方式（<a href="https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/manage-making-a-payment.html">Making payments</a>）；Google Cloud 的每月週期帳戶在每月 2 日扣上個月的費用（出處同下一項）。</li>
<li><strong>門檻扣款</strong>：Google Cloud 的門檻週期帳戶，每月 1 日扣款，餘額達到付款門檻時也會扣。門檻由 Google 指定，使用者不能自己改，帳戶狀態良好時觸發一次之後會自動調高；費用累積很快時，實際扣的金額可能超過門檻（<a href="https://docs.cloud.google.com/billing/docs/how-to/billing-cycle">Find out your Cloud Billing account type and charging cycle</a>）。</li>
</ul>
<p>以上都是 2026-10-27 讀的。另外，註冊與升級時會有一筆暫時的預先授權：Oracle Cloud 註冊 Free Tier 時授權 1 美元、升級時授權 100 美元，Oracle 端立即撤銷，銀行多久退回由銀行決定；AWS 新註冊流程的驗證暫扣 1 美元、3 到 5 天（<a href="https://docs.oracle.com/iaas/Content/GSG/Tasks/changingpaymentmethod.htm">Managing Account Upgrades and Payment Method</a>、<a href="https://docs.aws.amazon.com/accounts/latest/reference/sign-in-new.html">Sign up for AWS (new)</a>，2026-10-27 讀）。可用額度低於這筆授權金額的卡片，在註冊或升級時就會被拒（從授權的機制推論）。</p>
<p>一個帳單週期最壞的總花費，是〈停止之前的最大超支：配額、訊號延遲與處置延遲〉那三段的和，再加上沒有被權限擋下的一次性購買；〈平台上的上限：擋下建立、停止用量與只發通知〉裡觸發處置的門檻，就是照「這個和等於每月能接受的上限」設的。所以門檻照那個算法設好時，一期雲端費用的上限是每月能接受的上限，信用卡要容得下的是：</p>
<ul>
<li><strong>一期雲端費用</strong>：每月能接受的上限，再乘上不在預算金額裡的比例項目，例如稅金（Google Cloud 寫預算門檻的計算含稅金，見 <a href="https://docs.cloud.google.com/billing/docs/how-to/budgets">Create, edit, or delete budgets and budget alerts</a>，2026-10-27 讀；其他兩家的預算是否含稅沒有查）、隨花費比例收費的支援方案；刷外幣時再加匯率與發卡行的海外交易手續費。</li>
<li><strong>同時掛在卡上的期數</strong>：信用卡的可用額度要等繳款之後才恢復。雲端在月初扣上個月的費用，而信用卡的繳款日如果落在下一次雲端扣款之後，下一期扣款時上一期還沒繳，額度要容得下兩期（從信用卡繳款的機制推論，各發卡行的規則不同）。門檻扣款的帳戶一個月可能扣好幾次，要把落在同一個信用卡帳期裡的扣款加起來算。</li>
<li><strong>其他</strong>：同一張卡的其他支出，以及一段給估錯的餘裕（最大花費速度或停止延遲比假設的大）。</li>
</ul>
<p>門檻沒有設、或設得比算出來的高時，一個月最壞的總花費就沒有上限，信用卡額度也就算不出來。另外，月結後付的「一個月扣一次」有例外：AWS 的客戶合約寫懷疑帳號遭到詐欺使用時可以提前出帳（§3.1），而濫用正是要防的那一種失控；預留執行個體這類一次性購買在購買時就產生費用（扣款的時點沒有查）。</p>
<p>額度不夠時，扣款失敗本身會停掉服務，而停的方式不是自己選的。Google Cloud 寫付款被拒時可能停止服務，自助帳戶可能整個被停權、連結的全部專案的全部服務都停，長期無效時資源可能被移除、移除之後救不回來（<a href="https://docs.cloud.google.com/billing/docs/how-to/resolve-issues">Resolve Cloud Billing issues</a>）；AWS 的客戶合約寫違反付款義務時可以在通知之後立即停權（<a href="https://aws.amazon.com/agreement/">AWS Customer Agreement</a> §4.1）；Oracle Cloud 的官方頁只寫了 Universal Credits 訂閱未付款時 tenancy 與全部資源停權，Pay As You Go 的處置沒有找到明文（<a href="https://docs.oracle.com/en-us/iaas/Content/Billing/Concepts/subscriptions-paymentfailure.htm">Subscriptions and Payment Failure</a>）。扣款失敗之後還有幾天可以補救，三家都沒有寫出天數：Google Cloud 的頁面沒有寫，AWS 合約寫的是通知之後可以立即停權（也就是不保證有寬限），Oracle Cloud 的 Pay As You Go 沒有找到。以上都是 2026-10-27 讀的。</p>
<p>所以把信用卡額度壓低、拿它當費用的上限，限住的只有「扣款這個動作」：超過額度的費用仍是欠款，帳號停權之後照樣要付，AWS 的合約還寫了逾期可以收月息 1.5%。付出的是兩件事。一是作用得晚：月結後付的帳戶要到帳單週期結束之後才扣款，失控的那一個月照樣累積到扣款那天。二是作用的方式粗：停權的範圍是整個帳單帳戶（Google Cloud 寫的是連結到那個帳單帳戶的全部專案），正式服務跟著停，資料可能被移除；把實驗拆到自己的專案而共用同一個帳單帳戶與同一張卡時，隔離不了這個範圍。額度刻意壓低的專用卡適合只放實驗資源的帳號，而且這個帳號要有自己的帳單帳戶與卡片，資料要能丟、或在業者之外另有一份；放正式服務的帳號，額度要留足，費用的上限交給〈平台上的上限：擋下建立、停止用量與只發通知〉的平台機制。</p>
<p>實驗帳號還有一個比壓低額度更嚴的做法：不升級的免費方案。AWS 的 Free account plan 不會產生費用，六個月後或抵免用完時帳號自動關閉；Oracle Cloud 不升級的 Free Tier 不會扣款；Google Cloud 的試用帳號不升級就不扣款。換到的是超支為 0，付出的是能用的服務受限、到期時帳號或付費資源被關閉，各家的處置見 <a href="/blog/devops/infra/cloud-providers/pricing-comparison/" data-link-title="雲端免費額度與小型主機的收費比較：試用金與固定額度、計費單位、區域限制、試用結束後的處置、出站流量與查價項目" data-link-desc="Oracle Cloud、Google Cloud 與 AWS 的免費方案分別由哪幾種額度組成，比較時要換算的計費單位，免費主機能放在哪些區域，試用期結束後帳號與資源會怎樣，以及付費主機的價格該去哪裡查">雲端免費額度與小型主機的收費比較：試用金與固定額度、計費單位、區域限制、試用結束後的處置、出站流量與查價項目</a>〈試用結束後帳號與資源的處置〉。</p>
<h2 id="平台上的上限擋下建立停止用量與只發通知">平台上的上限：擋下建立、停止用量與只發通知</h2>
<p>平台上的上限依它在費用超過時做什麼，分成三種，三種的反應時間與副作用不同：</p>
<ul>
<li><strong>擋下建立</strong>：配額（quota）限制的是能建多少資源，建立請求超過時直接被拒絕，費用根本不會開始。它不經過帳務資料，所以沒有延遲；代價是要擴充時得先改配額。Oracle Cloud 的 compartment quota 由管理員用 policy 設定，可以限制主機的核心數與區塊儲存的總量，超過時建立請求被拒，回 <code>400</code>（配額名稱與 policy 的寫法以 <a href="https://docs.oracle.com/en-us/iaas/Content/Quotas/Concepts/quota_policy_syntax.htm">Quota Policy Syntax</a> 為準，2026-10-27 讀）。2026-10-27 實跑時，A1 核心數的那一條被擋下時訊息寫出被超過的 quota 名稱與 policy；區塊儲存那一條回的 <code>code</code> 是 <code>LimitExceeded</code>、訊息寫成 service limit，而 Oracle 的錯誤代碼表把 <code>LimitExceeded</code> 定義成超過 Oracle 設定的 tenancy 上限；那時 <code>total-storage-gb</code> 的用量遠低於 tenancy 的上限，所以擋下建立的是 quota（指令與兩種回應的原文在 <a href="/blog/devops/infra/cloud-providers/oracle-cloud/hands-on/private-ubuntu-egress-console-rebuild/" data-link-title="Oracle Cloud 私有子網路 Ubuntu 主機的實測紀錄：套件來源的出站（NAT gateway、路由與 NSG）、序列主控台的連線，以及被回收之後的重建" data-link-desc="要在 Oracle Cloud 沒有公開 IP 的私有子網路裡跑一台 Ubuntu 主機、讓它裝得到套件更新，或要經由序列主控台看主機的開機過程，或主機被閒置回收、停止或刪除之後要照原樣重建，或想知道 NAT gateway 要不要另外付費、開機磁碟扣掉多少區塊磁碟額度、合計超過 200 GB 時放不進額度的那一顆怎麼算、怎麼用 quota 擋下超額的建立、線上加大開機磁碟超過額度時怎麼算、空出額度之後付費磁碟多久轉成免費時，查實際跑過的指令、錯誤原文、重建腳本與每一步的讀法">Oracle Cloud 私有子網路 Ubuntu 主機的實測紀錄：套件來源的出站（NAT gateway、路由與 NSG）、序列主控台的連線，以及被回收之後的重建</a>〈費用與額度：A1、開機磁碟與區塊儲存額度、出站流量與 NAT gateway〉）。tenancy 與 compartment 是 Oracle Cloud 的帳號層級與資源分組，定義在 <a href="/blog/devops/infra/cloud-providers/oracle-cloud/account-and-home-region/" data-link-title="Oracle Cloud 的帳號結構與 home region：tenancy、Always Free 的範圍與主控台登入欄位" data-link-desc="OCI 的 tenancy、home region、compartment 與 availability domain 各自管什麼，home region 為什麼在申請當下就要決定，Always Free 額度的計算與回收規則，以及主控台登入時租用戶與使用者名稱兩個欄位的分工">Oracle Cloud 的帳號結構與 home region：tenancy、Always Free 的範圍與主控台登入欄位</a>〈Tenancy、home region、compartment 與 availability domain〉。AWS 的 Service Quotas 是 AWS 定義的資源上限，提供的是查詢與申請調高；要在 AWS 上限制能建多少，用 <a href="/blog/devops/infra/knowledge-cards/scp/" data-link-title="SCP (Service Control Policy)" data-link-desc="AWS Organizations 層級的權限天花板，套用到 OU 後連管理員都越不過">SCP</a> 或下一項的 Budget actions。</li>
<li><strong>停止用量</strong>：花費達到門檻時由平台或程式停止資源，反應多快取決於它讀的是帳務資料還是費用估算。讀帳務資料的，反應時間至少是〈費用訊號的延遲：從資源建立到扣款〉那張表裡帳務明細加預算通知的延遲：AWS 的 Budget actions（套用 IAM policy、SCP，或對指定的 EC2、RDS 執行個體執行動作），以及由預算通知觸發的自寫程式。讀費用估算的比帳務明細快、但不是即時，從達到上限到暫停之間多出來的用量照常計費：Google Cloud 的強制執行支出上限（spend cap budget）屬於這一種，它只涵蓋幾項 API 型的服務、不涵蓋 Compute Engine，暫停的是新用量，持續性資源照常計費（涵蓋的服務清單與延遲的說法在 <a href="/blog/devops/infra/cloud-providers/gcp/spend-protection-gcp-budget-killswitch/" data-link-title="GCP 上的雲端費用失控防護：預算警示、強制執行支出上限與自動停用帳單的作用範圍、反應時間與判斷" data-link-desc="GCP 上為費用失控設防的機制：預算警示寄出通知的時機、範圍、抵免與通知對象、強制執行支出上限涵蓋的服務與暫停期間，以預算通知觸發函式停止用量的兩種處置（解除帳單連結、只停止花錢的資源）該選哪一種，帳單資料延遲對這些機制反應時間的影響，停損開關放在另一個專案的理由，以及驗收為什麼分成真實通知與測試訊息兩段、停用後的恢復為什麼要先調預算">GCP 上的雲端費用失控防護：預算警示、強制執行支出上限與自動停用帳單的作用範圍、反應時間與判斷</a>〈強制執行支出上限：涵蓋的服務、範圍與暫停期間〉）。AWS 改版後的註冊流程分成 Free Plan 與 Paid Plan，Paid Plan 可以替每個 project（那個流程裡的資源分組）設每月的支出上限（spend limit），達到時暫停 project、停止資源，資料保留，90 天沒有處理就永久刪除；上限的最低值是 20 美元與 AWS 對這個 project 可能花費的保守估計兩者中較大的那個，它的延遲官方頁沒有寫（<a href="https://docs.aws.amazon.com/accounts/latest/reference/create-spend-limit.html">Create a spend limit in AWS Settings</a>，2026-10-27 讀）。平台機制涵蓋不到的服務，要由預算通知觸發自己寫的程式去停。</li>
<li><strong>只發通知</strong>：預算警示在花費達到通知的門檻時寄通知，不停止任何用量。Oracle Cloud 的預算官方稱為軟上限（soft limits），頁面描述的通知方式是 email；Google Cloud 寫只發通知的預算不會封頂。通知本身的延遲是〈費用訊號的延遲：從資源建立到扣款〉那張表裡帳務明細加預算通知的延遲，費用停在哪裡，再加上收到通知的人多快處理。</li>
</ul>
<p>三種上限各自要決定的值：</p>
<ul>
<li><strong>擋下建立：配額的值</strong>。配額設在平台允許的最大值時，最大花費速度裡跟著資源存在計費的那一部分就是那個最大值；配額等於計畫要用的量時，那一部分就等於計畫內的速度，多開資源造成的失控不會發生，剩下要防的是跟著用量計費的那一部分。代價是每次擴充都要先改配額，而這一步本身就是一次有人經手的審核。配額的值還要對照它的作用範圍：配額可能以可用網域（availability domain，區域裡的一個資料中心）、區域或專案為單位，設定的值套在每一個單位上，全區的實際上限是設定值乘上單位的個數。Oracle Cloud 主機核心數與區塊儲存的配額就是以可用網域為單位，設 4 在有三個可用網域的區域，等於全區合計 12（依 Quota Policy Syntax 的說明，沒有實跑）。容量上的配額也有兩種設法：設在計畫的合計，計畫外的資源一建就被拒；設在免費額度的上限（例如 Oracle Cloud 區塊儲存的 200 GB 是整個 tenancy 的合計，以可用網域為單位的配額要讓各單位加起來不超過 200），兩者之間可以臨時建磁碟、不必先改配額，代價是這段範圍裡建的磁碟不經審核。沒有自己設配額時，最大花費速度就用平台的預設上限算（Oracle Cloud 用 <code>oci limits</code>、AWS 用 Service Quotas 查得到）。</li>
<li><strong>停止用量：觸發處置的門檻與涵蓋範圍</strong>。觸發處置的門檻＝每月能接受的上限－停止之前的最大超支－停止之後仍在累積的計畫內花費。處置停掉全部還在累積的花費時，最後一項是 0；處置只停失控的那一塊、計畫內的服務照常運作時，最後一項保守的取法是一整個月計畫內的花費（失控發生在月初的情形），承諾用量這類停不掉的固定費用也在這一項裡。示例（另設的一組數字，跟〈停止之前的最大超支：配額、訊號延遲與處置延遲〉的示例無關）：每月能接受的上限是 100 美元，停止之前的最大超支是 30 美元，處置停掉全部花費時，觸發處置的門檻設在 70 美元以下；只停失控的那一塊、計畫內每月 20 美元時，設在 50 美元以下。算出來的門檻要比計畫內每月的花費大，否則正常使用就會觸發；這個例子裡 50 大於 20，有空間。帳號有試用金或抵免時，預算比的金額可能是扣掉抵免之後的淨額（Google Cloud 的預算預設扣掉抵免與折扣，見 <a href="https://docs.cloud.google.com/billing/docs/how-to/budgets">Create, edit, or delete budgets and budget alerts</a>，2026-10-27 讀），門檻要等抵免用完才觸發，那段期間的失控吃掉的是抵免，帳務上看起來是 0；門檻要照抵免之前的金額設，Google Cloud 是在預算設定裡取消勾選抵免。涵蓋範圍要逐項對照計畫要開的資源；涵蓋不到的那幾項，由配額限制住最大花費速度，或接受只有通知。</li>
<li><strong>只發通知：通知的門檻</strong>。程式自動處置之外再設一道通知，是讓人在程式動作之前先判斷要不要改用別的處置，所以通知要比觸發處置的門檻早「人判斷期間的超支」那麼多：通知門檻＝觸發處置的門檻－最大花費速度 × 人判斷要多久。這裡的人判斷時間發生在程式處置之前，不在處置延遲裡；沒有程式處置、由人執行的情形，人的時間已經算在處置延遲裡，通知門檻就是觸發處置的門檻。這條式子的前提是通知與處置讀同一種訊號；程式由用量計數觸發、通知走預算通知時，程式會比通知先動作。算出來的通知門檻比計畫內每月的花費還低時，正常使用每個月都會收到通知，要回頭縮短人判斷的時間或收緊配額。依預測花費（平台依目前的速度推估到期末）觸發的門檻通常比依實際花費的早，代價是一次性的大筆費用會讓預測暫時偏高、提早觸發（這兩點是推論）；Oracle Cloud、AWS 與 Google Cloud 的預算都有預測的選項，而 AWS 要大約 5 週的用量資料才產生預測，新帳號在那之前只能靠實際花費的門檻（<a href="https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-best-practices.html">AWS Budgets best practices</a>，2026-10-27 讀），Google Cloud 自訂期間的預算也不能用預測門檻。</li>
</ul>
<h2 id="停損停到哪一層由誰執行">停損停到哪一層、由誰執行</h2>
<p>停損的動作有好幾層，停得越徹底，費用越確定停止，對服務與資料的影響也越大。前四層沿用 <a href="/blog/devops/operations/09-host-lifecycle/suspend-and-resume-service/" data-link-title="服務暫停與恢復上線：只停服務、停止主機、終止主機到全部刪除各自保留的資源，暫停期間仍計費與仍被指向的資源，恢復前要查的漂移與恢復的順序" data-link-desc="一台主機上的服務要暫停一段時間再恢復：只停服務、停止主機、終止主機保留設定、全部刪除靠 repo 重建各自保留什麼與付出什麼，怎麼依保留的月費、重建的成本與漂移量選擇，暫停期間仍在計費的資源（含在作業系統裡 shutdown 不停止計費），仍被 DNS 與 CDN 指向的結果（Cloudflare 521、522，釋放 IP 之後的懸空 DNS），恢復前要重新確認的漂移，以及依元件依賴排出的恢復順序與驗證">服務暫停與恢復上線：只停服務、停止主機、終止主機到全部刪除各自保留的資源，暫停期間仍計費與仍被指向的資源，恢復前要查的漂移與恢復的順序</a> 的分法，從輕到重：</p>
<ul>
<li><strong>只停服務</strong>：程序停掉，主機照常計費。</li>
<li><strong>停止主機</strong>：多數機型的運算費用停止，磁碟照常計費（例外的機型見〈費用在什麼時間點開始產生〉）。</li>
<li><strong>終止主機、保留磁碟與網路設定</strong>：主機的費用停止，保留的磁碟、IP、快照與網路元件照常計費。</li>
<li><strong>全部刪除</strong>：連磁碟一起刪，跟著資源存在計費的費用停止，資料不在了，之後要從 repo 重建；仍在計費的備份、快照與網路元件見那一篇〈暫停期間仍在計費的資源〉。</li>
<li><strong>帳單層</strong>：把專案從帳單帳戶解除連結（Google Cloud），整個專案的服務都停，資源可能被永久刪除，也不保證能恢復，見 <a href="/blog/devops/infra/cloud-providers/gcp/spend-protection-gcp-budget-killswitch/" data-link-title="GCP 上的雲端費用失控防護：預算警示、強制執行支出上限與自動停用帳單的作用範圍、反應時間與判斷" data-link-desc="GCP 上為費用失控設防的機制：預算警示寄出通知的時機、範圍、抵免與通知對象、強制執行支出上限涵蓋的服務與暫停期間，以預算通知觸發函式停止用量的兩種處置（解除帳單連結、只停止花錢的資源）該選哪一種，帳單資料延遲對這些機制反應時間的影響，停損開關放在另一個專案的理由，以及驗收為什麼分成真實通知與測試訊息兩段、停用後的恢復為什麼要先調預算">GCP 上的雲端費用失控防護：預算警示、強制執行支出上限與自動停用帳單的作用範圍、反應時間與判斷</a>〈由預算通知觸發的處置：解除帳單連結與只停止花錢的資源〉。</li>
</ul>
<p>選哪一層，比的是兩種損失：多付的費用，以及服務停止、資料遺失的代價。後者由兩件事決定，一是停機影不影響使用者，二是資料能不能重建：</p>
<ul>
<li><strong>資料可以重建、停機不影響使用者</strong>（實驗與開發環境）：停得徹底比較划算，程式可以在門檻觸發時直接刪除資源或解除帳單連結。</li>
<li><strong>停機可以接受、資料不能丟</strong>：程式只停止花錢的資源（停止主機、縮減到零），保留磁碟與資料；之後要付的是磁碟的費用。</li>
<li><strong>停機影響使用者</strong>（正式服務）：程式自動停止既有的資源會讓一筆異常的帳單變成一次停機，所以停止既有資源由人判斷；程式可以做的是不造成停機的那一種處置，也就是擋下新增：套用拒絕建立資源的 IAM policy 或 SCP（AWS 的 Budget actions 做得到）、調低自動擴縮的上限、撤銷或輪替外洩的金鑰。多開資源這一類失控的處置延遲因此縮到程式的反應時間；已經在跑的資源與出站流量這類用量，停止延遲仍要算人判斷的時間。沒有人能隨時處理時，處置延遲就是下一次有人看到通知的時間（例如隔天上班），配額要依這段時間的最大花費速度能承受的金額來設。</li>
</ul>
<p>判斷的單位是一次停損會一起停掉的範圍（一個專案、compartment 或帳號），不是單一資源：一個範圍裡同時有實驗資源與正式服務時，照正式服務判。所以把容易失控的功能拆到自己的專案或 compartment，讓那一塊可以照較寬的規則獨立停止；拆出去之前要確認正式服務沒有共用那一塊的資源或憑證，例如同一把 API 金鑰，否則停掉那一塊時，正式服務裡用同一把金鑰的功能也一起停。Google Cloud 上用解除帳單連結與只停止花錢的資源兩種做法的範圍與實測，在 <a href="/blog/devops/infra/cloud-providers/gcp/spend-protection-gcp-budget-killswitch/" data-link-title="GCP 上的雲端費用失控防護：預算警示、強制執行支出上限與自動停用帳單的作用範圍、反應時間與判斷" data-link-desc="GCP 上為費用失控設防的機制：預算警示寄出通知的時機、範圍、抵免與通知對象、強制執行支出上限涵蓋的服務與暫停期間，以預算通知觸發函式停止用量的兩種處置（解除帳單連結、只停止花錢的資源）該選哪一種，帳單資料延遲對這些機制反應時間的影響，停損開關放在另一個專案的理由，以及驗收為什麼分成真實通知與測試訊息兩段、停用後的恢復為什麼要先調預算">GCP 上的雲端費用失控防護：預算警示、強制執行支出上限與自動停用帳單的作用範圍、反應時間與判斷</a>〈由預算通知觸發的處置：解除帳單連結與只停止花錢的資源〉。</p>
<p>這幾件事要在租用之前有答案：門檻觸發時通知誰、誰有權限停止哪一層、程式自動停止的範圍是哪些資源、停之前要不要先做快照或備份、恢復時由誰決定。通知送到一個沒人看的信箱，或有權限停止的人不在，處置延遲就沒有上限，停止之前的最大超支也跟著沒有上限。停損的程式平常不動作，所以它的驗收要用合成的觸發實際跑一次；Google Cloud 上的建置，以及分成「真實的預算通知送到主題」與「測試訊息觸發函式」兩段的驗收，在 <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/budget-killswitch-walkthrough/" data-link-title="GCP 預算警示與停損開關的實測紀錄：建預算、Pub/Sub 主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與停用後的恢復" data-link-desc="要在 GCP 專案上建預算警示，或建一個花費超過預算就解除帳單連結的停損開關，想照一次實跑過的順序做、看每一步漏掉或換一種做法的結果時讀；也用在函式部署成功、狀態 ACTIVE 卻從沒解除過帳單連結，或專案因超支被停用之後要恢復的時候">GCP 預算警示與停損開關的實測紀錄：建預算、Pub/Sub 主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與停用後的恢復</a>。</p>
<h2 id="租用之前要決定的值與它們的依賴">租用之前要決定的值與它們的依賴</h2>
<p>上面幾節的值互相依賴。推算的起點有幾類：由人直接決定的每月能接受的上限與能接受的停止延遲；由容量規劃推出來的資源規格；從價目表查到的單價；以及組織能做到的反應時間，也就是有人值班時，人處理或判斷一次要多久。由人還是由程式執行處置，是從能接受的停止延遲與人的反應時間推出來的。其餘的值都從這些起點推出來，依推算的先後：</p>
<table>
  <thead>
      <tr>
          <th>要決定的值</th>
          <th>由什麼推出來</th>
          <th>沒有決定時的後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>計畫要開的資源與配額</td>
          <td>服務需要的規格（容量規劃）</td>
          <td>最大花費速度是平台允許的最大值，被濫用時以那個速度累積</td>
      </tr>
      <tr>
          <td>最大花費速度</td>
          <td>配額允許的量 × 單價，加上跟著用量計費的那一部分（沒有限制時沒有上限）</td>
          <td>停止之前的最大超支算不出來</td>
      </tr>
      <tr>
          <td>偵測用的訊號與處置方式</td>
          <td>能接受的停止延遲與人的反應時間</td>
          <td>預設只剩帳務明細與預算通知，延遲以小時到天計</td>
      </tr>
      <tr>
          <td>一次性購買的權限</td>
          <td>計畫裡有沒有要買預留執行個體、Marketplace 訂閱這類項目</td>
          <td>拿到憑證的人可以一次買下，金額不受配額與停止延遲限制</td>
      </tr>
      <tr>
          <td>停止之前的最大超支</td>
          <td>最大花費速度 ×（偵測延遲 ＋ 處置延遲）</td>
          <td>預算門檻與信用卡額度都沒有依據</td>
      </tr>
      <tr>
          <td>觸發處置的門檻</td>
          <td>每月能接受的上限 － 停止之前的最大超支（只停一部分時再扣掉停止後的計畫內花費）</td>
          <td>門檻設在上限上，費用停止時實際花費超過上限，超出的是停止之前的最大超支</td>
      </tr>
      <tr>
          <td>通知的門檻</td>
          <td>有程式處置時：觸發處置的門檻 － 最大花費速度 × 人判斷要多久；由人處置時：同觸發門檻</td>
          <td>通知跟程式處置同時到，人來不及在處置之前判斷</td>
      </tr>
      <tr>
          <td>信用卡或帳戶的額度</td>
          <td>每月能接受的上限 ＋ 同一個信用卡帳期的其他支出 ＋ 估錯的餘裕</td>
          <td>扣款失敗，帳號被停權，停的範圍與時間不是自己選的</td>
      </tr>
      <tr>
          <td>停損的層級與執行者</td>
          <td>停機的影響與資料能不能重建（由服務的負責人判斷），對上多付的費用</td>
          <td>失控時才決定，決定的人要在帳務資料落後、費用持續累積的情況下選處置</td>
      </tr>
  </tbody>
</table>
<p>租用之前推的值，有幾個是照文件或假設估的，例如帳務明細的延遲：Oracle Cloud 文件寫最多 48 小時，2026-10-27 實測的 usage API 大約落後 2 小時；AWS 的預測要大約 5 週的資料。所以第一個帳單週期跑完、量到實際的延遲之後，這張表要用實測值重算一次。預知的大流量要臨時加開資源時，也要重算：配額先調高，最大花費速度跟著變大，停止之前的最大超支、門檻與信用卡額度都要依新的值調整。擴容的規格與預算怎麼從業務預估推出來，在 <a href="/blog/devops/operations/05-capacity-planning/planned-peak-sizing-and-budget/" data-link-title="預知的大流量要加多大、編多少預算：從業務預估到擴容規格與費用區間的換算鏈、給決策者與會計的數字，以及每次活動之後的校準" data-link-desc="行銷活動、檔期這類事先知道的大流量，容量規劃怎麼從業務預估換算成峰值請求量、各層要開幾台與擴容的計費時數，再換算成費用區間；三點估計怎麼帶著預估的誤差，給決策者的選項要附規格、費用、不足時的後果與回退方式，給會計的預算要對齊增量與付款時點，以及每次活動之後校準換算係數、沒有歷史資料時怎麼起步">預知的大流量要加多大、編多少預算：從業務預估到擴容規格與費用區間的換算鏈、給決策者與會計的數字，以及每次活動之後的校準</a>；峰值的形狀與倍率在 <a href="/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算</a>，計費模式與單位請求成本在 <a href="/blog/devops/operations/05-capacity-planning/cost-model/" data-link-title="成本模型" data-link-desc="把容量規劃的資源翻成錢時，釐清 on-demand、reserved、spot 三種計費模式的取捨、怎麼算單位請求成本、以及過度與不足配置各自的經濟代價">成本模型</a>。</p>
]]></content:encoded></item><item><title>GCP 預算警示與停損開關的實測紀錄：建預算、Pub/Sub 主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與停用後的恢復</title><link>https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/hands-on/budget-killswitch-walkthrough/</link><pubDate>Sun, 25 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/hands-on/budget-killswitch-walkthrough/</guid><description>&lt;p>實測日期是 2026-10-24，在 GCP 測試專案 &lt;code>blog-verify-20261024&lt;/code> 上分兩次實跑：第一次建出停損開關並觸發一次；第二次另開一個可拋棄的目標專案，驗預算的範圍與抵免、電子郵件通知管道、停損開關放在另一個專案、拆開驗證、心跳監看與恢復。帳單帳戶的幣別是新台幣，帳戶上有抵免；測試帳號底下沒有組織。&lt;/p>
&lt;p>這篇記錄在 GCP 上建預算警示與停損開關（由預算通知觸發、把專案從帳單帳戶解除連結的函式）的實際操作。內容依操作的順序是：建立限定一個專案的預算；建接收通知的 Pub/Sub 主題並把預算接上；建服務帳號並授予解除帳單連結與讀取帳單狀態的角色；函式的程式；部署函式並指定兩個服務帳號、執行個體上限與重試；授予觸發器呼叫函式的權限；停損開關放在另一個專案時要改的指令；用第一則真實通知與兩則測試訊息做兩段驗收；設心跳監看；超支停用之後的恢復。每一步寫出這次的取值與實測結果，以及漏掉或換一種做法時的結果；替代做法與沒做時的結果沒有另外標註的是這次實測看到的，照官方文件推出的寫出文件，推論的寫明沒有實跑。費用失控的來源與帳單資料的延遲、預算警示與強制執行支出上限的作用範圍、兩種處置（解除帳單連結、只停止花錢的資源）該選哪一種、停損開關為什麼要放在另一個專案、驗收為什麼分兩段，寫在 &lt;a href="https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/spend-protection-gcp-budget-killswitch/" data-link-title="GCP 上的雲端費用失控防護：預算警示、強制執行支出上限與自動停用帳單的作用範圍、反應時間與判斷" data-link-desc="GCP 上為費用失控設防的機制：預算警示寄出通知的時機、範圍、抵免與通知對象、強制執行支出上限涵蓋的服務與暫停期間，以預算通知觸發函式停止用量的兩種處置（解除帳單連結、只停止花錢的資源）該選哪一種，帳單資料延遲對這些機制反應時間的影響，停損開關放在另一個專案的理由，以及驗收為什麼分成真實通知與測試訊息兩段、停用後的恢復為什麼要先調預算">GCP 上的雲端費用失控防護&lt;/a>（下文稱概念篇），這裡不重寫。&lt;/p>
&lt;p>停損開關的效果是一次自己造成的全面停機。建之前先照 &lt;a href="https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/spend-protection-gcp-budget-killswitch/" data-link-title="GCP 上的雲端費用失控防護：預算警示、強制執行支出上限與自動停用帳單的作用範圍、反應時間與判斷" data-link-desc="GCP 上為費用失控設防的機制：預算警示寄出通知的時機、範圍、抵免與通知對象、強制執行支出上限涵蓋的服務與暫停期間，以預算通知觸發函式停止用量的兩種處置（解除帳單連結、只停止花錢的資源）該選哪一種，帳單資料延遲對這些機制反應時間的影響，停損開關放在另一個專案的理由，以及驗收為什麼分成真實通知與測試訊息兩段、停用後的恢復為什麼要先調預算">GCP 上的雲端費用失控防護&lt;/a> 的〈由預算通知觸發的處置：解除帳單連結與只停止花錢的資源〉判斷這個專案該不該建、建哪一種。下文以停損開關為例；只停止花錢的資源那一種的取捨與實測結果（跨全部 zone 停 VM、約兩分鐘 &lt;code>TERMINATED&lt;/code>）寫在那一節，它的函式本體這篇沒有收。&lt;/p>
&lt;p>停損開關部署了卻沒有動作時，成因依呼叫鏈的段落分在這幾節：通知裡的 &lt;code>costAmount&lt;/code> 沒有越過預算（抵免、帳單資料延遲、&lt;code>--filter-projects&lt;/code> 的範圍）見〈建立限定一個專案的預算〉；通知沒有送到主題或主題接錯預算，見〈建接收通知的主題並把預算接上〉；函式收到了卻沒有解除連結，見〈建服務帳號並授予兩個角色〉〈部署函式：兩個服務帳號、執行個體上限與重試〉〈授予觸發器呼叫函式的權限〉；要確認是哪一段，照〈兩段驗收：第一則真實通知，再發兩則測試訊息〉。&lt;/p>
&lt;p>下面的指令用到三個本文取的名稱：主題 &lt;code>budget-alerts&lt;/code>、服務帳號 &lt;code>budget-killswitch-sa&lt;/code>、函式 &lt;code>budget-killswitch&lt;/code>。換成別的名稱時，同一個名稱出現的每一條指令都要一起換。&lt;code>&amp;lt;project-id&amp;gt;&lt;/code> 指的是放停損開關、同時也是要被停用帳單的那個專案；兩者分開的情形見〈停損開關放在另一個專案時要改的指令〉。&lt;/p>
&lt;h2 id="建立限定一個專案的預算">建立限定一個專案的預算&lt;/h2>
&lt;p>建立預算的人要通過兩道權限檢查：實測沒有任何角色的帳號先被 &lt;code>--billing-project&lt;/code> 那個專案擋下（缺 &lt;code>serviceusage.services.use&lt;/code>，訊息寫 &lt;code>USER_PROJECT_DENIED&lt;/code>）；補上 &lt;code>roles/serviceusage.serviceUsageConsumer&lt;/code> 約一分鐘後，才換成帳單帳戶的權限不足。所以建立預算的人要有那個專案的 &lt;code>serviceusage.services.use&lt;/code>，以及帳單帳戶上能管理預算的角色（帳單帳戶管理員或帳單帳戶費用管理員）。&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"># 查帳單帳戶的 ID 與幣別：預算金額用的是這個幣別&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">gcloud billing accounts list
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">gcloud billing accounts describe &amp;lt;billing-account-id&amp;gt; --format&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;value(currencyCode)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1"># gcloud 透過 --billing-project 指定的專案呼叫預算 API，那個專案要先啟用這個 API&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">gcloud services &lt;span class="nb">enable&lt;/span> billingbudgets.googleapis.com --project&lt;span class="o">=&lt;/span>&amp;lt;project-id&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1"># 建立預算：金額寫成「數字加幣別代碼」，幣別要與帳單帳戶相同&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">gcloud billing budgets create --billing-account&lt;span class="o">=&lt;/span>&amp;lt;billing-account-id&amp;gt; &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --display-name&lt;span class="o">=&lt;/span>&amp;lt;budget-display-name&amp;gt; --budget-amount&lt;span class="o">=&lt;/span>&amp;lt;amount&amp;gt;&amp;lt;currency-code&amp;gt; &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --filter-projects&lt;span class="o">=&lt;/span>projects/&amp;lt;project-id&amp;gt; &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --threshold-rule&lt;span class="o">=&lt;/span>&lt;span class="nv">percent&lt;/span>&lt;span class="o">=&lt;/span>0.5 --threshold-rule&lt;span class="o">=&lt;/span>&lt;span class="nv">percent&lt;/span>&lt;span class="o">=&lt;/span>0.9 --threshold-rule&lt;span class="o">=&lt;/span>&lt;span class="nv">percent&lt;/span>&lt;span class="o">=&lt;/span>1.0 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --threshold-rule&lt;span class="o">=&lt;/span>&lt;span class="nv">percent&lt;/span>&lt;span class="o">=&lt;/span>1.0,basis&lt;span class="o">=&lt;/span>forecasted-spend &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --billing-project&lt;span class="o">=&lt;/span>&amp;lt;project-id&amp;gt;&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>&lt;code>&amp;lt;billing-account-id&amp;gt;&lt;/code>：&lt;code>gcloud billing accounts list&lt;/code> 的第一欄，是三段各六個英數字、以 &lt;code>-&lt;/code> 分隔的字串，例如 &lt;code>000000-AAAAAA-111111&lt;/code>（這是佔位值）。&lt;/li>
&lt;li>&lt;code>&amp;lt;budget-display-name&amp;gt;&lt;/code>：自己取的名稱，只用來在主控台與通知裡辨識這個預算。&lt;/li>
&lt;li>&lt;code>&amp;lt;amount&amp;gt;&amp;lt;currency-code&amp;gt;&lt;/code>：例如帳單帳戶的幣別是新台幣時寫成 &lt;code>320TWD&lt;/code>。主控台建立預算的表單只顯示一個 &lt;code>$&lt;/code>，不顯示幣別；幣別是新台幣的帳戶在那裡填 &lt;code>10&lt;/code>，門檻是新台幣 10 元而不是 10 美元。&lt;/li>
&lt;li>&lt;code>--filter-projects&lt;/code>：預算只計算這些專案的花費。&lt;code>gcloud billing budgets describe&lt;/code> 的 &lt;code>budgetFilter.projects&lt;/code> 存的是專案編號，不是專案 ID。&lt;/li>
&lt;li>門檻比例：50% 與 90% 留給收到通知的人處理的時間，100% 對齊花費達到預算的那一刻，預測花費的門檻提早反映速度異常；比例依處理一次所需的時間自己調整。&lt;/li>
&lt;li>&lt;code>--billing-project&lt;/code>：gcloud 呼叫預算 API 所用的專案，不影響預算的範圍。&lt;/li>
&lt;/ul>
&lt;p>接停損開關的預算要另外建，不接到既有的帳戶層級警示預算上；它的金額設成這個專案本期能承受的最高花費，減掉帳單資料延遲期間多出的費用，估法見概念篇〈由預算通知觸發的處置：解除帳單連結與只停止花錢的資源〉。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>沒做時&lt;/strong>（不給 &lt;code>--filter-projects&lt;/code>）：&lt;code>gcloud billing budgets create --help&lt;/code> 寫著預算涵蓋所有專案，通知裡的 &lt;code>costAmount&lt;/code> 是帳單帳戶下所有專案的合計；接上停損開關時，別的專案超支會停掉這一個（從程式的判斷規則推論，這次沒有實跑）。&lt;/li>
&lt;li>&lt;strong>替代做法&lt;/strong>：
&lt;ul>
&lt;li>加 &lt;code>--credit-types-treatment=exclude-all-credits&lt;/code>，換到的是以抵免前的金額計算、大額抵免期間門檻照樣觸發，實測同一段期間扣掉抵免的帳戶層級預算報 &lt;code>costAmount&lt;/code> 0.05、&lt;code>exclude-all-credits&lt;/code> 的預算報 0.37；付出的是免費方案的用量也以原價算進花費（推論：&lt;code>FREE_TIER&lt;/code> 是預算接受的抵免類型，這次沒有量）。&lt;/li>
&lt;li>改用 &lt;code>--credit-types-treatment=include-specified-credits&lt;/code> 搭配 &lt;code>--filter-credit-types=FREE_TIER&lt;/code>，換到的是只讓免費方案的用量照常扣掉；類型寫錯時回 &lt;code>INVALID_ARGUMENT: ... is not a valid credit type string&lt;/code>，&lt;code>--help&lt;/code> 不列出可用的類型，以 Cloud Billing 文件為準。&lt;/li>
&lt;li>加 &lt;code>--notifications-rule-monitoring-notification-channels=&amp;lt;channel-resource-name&amp;gt;&lt;/code>，把通知另外寄到 Cloud Monitoring 的電子郵件通知管道，換到的是值班的人收得到，付出的是只支援電子郵件類型：接上 webhook 類型的管道時只回 &lt;code>INVALID_ARGUMENT: Request contains an invalid argument&lt;/code>，訊息不說原因。&lt;code>&amp;lt;channel-resource-name&amp;gt;&lt;/code> 形如 &lt;code>projects/&amp;lt;project-id&amp;gt;/notificationChannels/&amp;lt;channel-id&amp;gt;&lt;/code>，在主控台的 Monitoring 通知管道頁面查得到。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h2 id="建接收通知的主題並把預算接上">建接收通知的主題並把預算接上&lt;/h2>





&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">gcloud services &lt;span class="nb">enable&lt;/span> pubsub.googleapis.com cloudfunctions.googleapis.com run.googleapis.com &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> cloudbuild.googleapis.com artifactregistry.googleapis.com eventarc.googleapis.com &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> cloudbilling.googleapis.com --project&lt;span class="o">=&lt;/span>&amp;lt;project-id&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 建立主題，並讓接停損開關的預算把通知送過來&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">gcloud pubsub topics create budget-alerts --project&lt;span class="o">=&lt;/span>&amp;lt;project-id&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">gcloud billing budgets update &amp;lt;budget-resource-name&amp;gt; &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --notifications-rule-pubsub-topic&lt;span class="o">=&lt;/span>projects/&amp;lt;project-id&amp;gt;/topics/budget-alerts &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --billing-project&lt;span class="o">=&lt;/span>&amp;lt;project-id&amp;gt;&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>&amp;lt;budget-resource-name&amp;gt;&lt;/code> 是 &lt;code>gcloud billing budgets list --billing-account=&amp;lt;billing-account-id&amp;gt; --billing-project=&amp;lt;project-id&amp;gt; --format=&amp;quot;value(name)&amp;quot;&lt;/code> 印出的完整名稱，形如 &lt;code>billingAccounts/&amp;lt;id&amp;gt;/budgets/&amp;lt;uuid&amp;gt;&lt;/code>，與建立時給的顯示名稱是兩個值。設定通知目標時，GCP 會自動讓預算服務的系統帳號 &lt;code>billing-budget-alert@system.gserviceaccount.com&lt;/code> 擁有這個主題的發布權限，&lt;code>gcloud pubsub topics get-iam-policy budget-alerts --project=&amp;lt;project-id&amp;gt;&lt;/code> 看得到這條綁定。&lt;/p></description><content:encoded><![CDATA[<p>實測日期是 2026-10-24，在 GCP 測試專案 <code>blog-verify-20261024</code> 上分兩次實跑：第一次建出停損開關並觸發一次；第二次另開一個可拋棄的目標專案，驗預算的範圍與抵免、電子郵件通知管道、停損開關放在另一個專案、拆開驗證、心跳監看與恢復。帳單帳戶的幣別是新台幣，帳戶上有抵免；測試帳號底下沒有組織。</p>
<p>這篇記錄在 GCP 上建預算警示與停損開關（由預算通知觸發、把專案從帳單帳戶解除連結的函式）的實際操作。內容依操作的順序是：建立限定一個專案的預算；建接收通知的 Pub/Sub 主題並把預算接上；建服務帳號並授予解除帳單連結與讀取帳單狀態的角色；函式的程式；部署函式並指定兩個服務帳號、執行個體上限與重試；授予觸發器呼叫函式的權限；停損開關放在另一個專案時要改的指令；用第一則真實通知與兩則測試訊息做兩段驗收；設心跳監看；超支停用之後的恢復。每一步寫出這次的取值與實測結果，以及漏掉或換一種做法時的結果；替代做法與沒做時的結果沒有另外標註的是這次實測看到的，照官方文件推出的寫出文件，推論的寫明沒有實跑。費用失控的來源與帳單資料的延遲、預算警示與強制執行支出上限的作用範圍、兩種處置（解除帳單連結、只停止花錢的資源）該選哪一種、停損開關為什麼要放在另一個專案、驗收為什麼分兩段，寫在 <a href="/blog/devops/infra/cloud-providers/gcp/spend-protection-gcp-budget-killswitch/" data-link-title="GCP 上的雲端費用失控防護：預算警示、強制執行支出上限與自動停用帳單的作用範圍、反應時間與判斷" data-link-desc="GCP 上為費用失控設防的機制：預算警示寄出通知的時機、範圍、抵免與通知對象、強制執行支出上限涵蓋的服務與暫停期間，以預算通知觸發函式停止用量的兩種處置（解除帳單連結、只停止花錢的資源）該選哪一種，帳單資料延遲對這些機制反應時間的影響，停損開關放在另一個專案的理由，以及驗收為什麼分成真實通知與測試訊息兩段、停用後的恢復為什麼要先調預算">GCP 上的雲端費用失控防護</a>（下文稱概念篇），這裡不重寫。</p>
<p>停損開關的效果是一次自己造成的全面停機。建之前先照 <a href="/blog/devops/infra/cloud-providers/gcp/spend-protection-gcp-budget-killswitch/" data-link-title="GCP 上的雲端費用失控防護：預算警示、強制執行支出上限與自動停用帳單的作用範圍、反應時間與判斷" data-link-desc="GCP 上為費用失控設防的機制：預算警示寄出通知的時機、範圍、抵免與通知對象、強制執行支出上限涵蓋的服務與暫停期間，以預算通知觸發函式停止用量的兩種處置（解除帳單連結、只停止花錢的資源）該選哪一種，帳單資料延遲對這些機制反應時間的影響，停損開關放在另一個專案的理由，以及驗收為什麼分成真實通知與測試訊息兩段、停用後的恢復為什麼要先調預算">GCP 上的雲端費用失控防護</a> 的〈由預算通知觸發的處置：解除帳單連結與只停止花錢的資源〉判斷這個專案該不該建、建哪一種。下文以停損開關為例；只停止花錢的資源那一種的取捨與實測結果（跨全部 zone 停 VM、約兩分鐘 <code>TERMINATED</code>）寫在那一節，它的函式本體這篇沒有收。</p>
<p>停損開關部署了卻沒有動作時，成因依呼叫鏈的段落分在這幾節：通知裡的 <code>costAmount</code> 沒有越過預算（抵免、帳單資料延遲、<code>--filter-projects</code> 的範圍）見〈建立限定一個專案的預算〉；通知沒有送到主題或主題接錯預算，見〈建接收通知的主題並把預算接上〉；函式收到了卻沒有解除連結，見〈建服務帳號並授予兩個角色〉〈部署函式：兩個服務帳號、執行個體上限與重試〉〈授予觸發器呼叫函式的權限〉；要確認是哪一段，照〈兩段驗收：第一則真實通知，再發兩則測試訊息〉。</p>
<p>下面的指令用到三個本文取的名稱：主題 <code>budget-alerts</code>、服務帳號 <code>budget-killswitch-sa</code>、函式 <code>budget-killswitch</code>。換成別的名稱時，同一個名稱出現的每一條指令都要一起換。<code>&lt;project-id&gt;</code> 指的是放停損開關、同時也是要被停用帳單的那個專案；兩者分開的情形見〈停損開關放在另一個專案時要改的指令〉。</p>
<h2 id="建立限定一個專案的預算">建立限定一個專案的預算</h2>
<p>建立預算的人要通過兩道權限檢查：實測沒有任何角色的帳號先被 <code>--billing-project</code> 那個專案擋下（缺 <code>serviceusage.services.use</code>，訊息寫 <code>USER_PROJECT_DENIED</code>）；補上 <code>roles/serviceusage.serviceUsageConsumer</code> 約一分鐘後，才換成帳單帳戶的權限不足。所以建立預算的人要有那個專案的 <code>serviceusage.services.use</code>，以及帳單帳戶上能管理預算的角色（帳單帳戶管理員或帳單帳戶費用管理員）。</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"># 查帳單帳戶的 ID 與幣別：預算金額用的是這個幣別</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">gcloud billing accounts list
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">gcloud billing accounts describe &lt;billing-account-id&gt; --format<span class="o">=</span><span class="s2">&#34;value(currencyCode)&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># gcloud 透過 --billing-project 指定的專案呼叫預算 API，那個專案要先啟用這個 API</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">gcloud services <span class="nb">enable</span> billingbudgets.googleapis.com --project<span class="o">=</span>&lt;project-id&gt;
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># 建立預算：金額寫成「數字加幣別代碼」，幣別要與帳單帳戶相同</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">gcloud billing budgets create --billing-account<span class="o">=</span>&lt;billing-account-id&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --display-name<span class="o">=</span>&lt;budget-display-name&gt; --budget-amount<span class="o">=</span>&lt;amount&gt;&lt;currency-code&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --filter-projects<span class="o">=</span>projects/&lt;project-id&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>  --threshold-rule<span class="o">=</span><span class="nv">percent</span><span class="o">=</span>0.5 --threshold-rule<span class="o">=</span><span class="nv">percent</span><span class="o">=</span>0.9 --threshold-rule<span class="o">=</span><span class="nv">percent</span><span class="o">=</span>1.0 <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="se"></span>  --threshold-rule<span class="o">=</span><span class="nv">percent</span><span class="o">=</span>1.0,basis<span class="o">=</span>forecasted-spend <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>  --billing-project<span class="o">=</span>&lt;project-id&gt;</span></span></code></pre></div><ul>
<li><code>&lt;billing-account-id&gt;</code>：<code>gcloud billing accounts list</code> 的第一欄，是三段各六個英數字、以 <code>-</code> 分隔的字串，例如 <code>000000-AAAAAA-111111</code>（這是佔位值）。</li>
<li><code>&lt;budget-display-name&gt;</code>：自己取的名稱，只用來在主控台與通知裡辨識這個預算。</li>
<li><code>&lt;amount&gt;&lt;currency-code&gt;</code>：例如帳單帳戶的幣別是新台幣時寫成 <code>320TWD</code>。主控台建立預算的表單只顯示一個 <code>$</code>，不顯示幣別；幣別是新台幣的帳戶在那裡填 <code>10</code>，門檻是新台幣 10 元而不是 10 美元。</li>
<li><code>--filter-projects</code>：預算只計算這些專案的花費。<code>gcloud billing budgets describe</code> 的 <code>budgetFilter.projects</code> 存的是專案編號，不是專案 ID。</li>
<li>門檻比例：50% 與 90% 留給收到通知的人處理的時間，100% 對齊花費達到預算的那一刻，預測花費的門檻提早反映速度異常；比例依處理一次所需的時間自己調整。</li>
<li><code>--billing-project</code>：gcloud 呼叫預算 API 所用的專案，不影響預算的範圍。</li>
</ul>
<p>接停損開關的預算要另外建，不接到既有的帳戶層級警示預算上；它的金額設成這個專案本期能承受的最高花費，減掉帳單資料延遲期間多出的費用，估法見概念篇〈由預算通知觸發的處置：解除帳單連結與只停止花錢的資源〉。</p>
<ul>
<li><strong>沒做時</strong>（不給 <code>--filter-projects</code>）：<code>gcloud billing budgets create --help</code> 寫著預算涵蓋所有專案，通知裡的 <code>costAmount</code> 是帳單帳戶下所有專案的合計；接上停損開關時，別的專案超支會停掉這一個（從程式的判斷規則推論，這次沒有實跑）。</li>
<li><strong>替代做法</strong>：
<ul>
<li>加 <code>--credit-types-treatment=exclude-all-credits</code>，換到的是以抵免前的金額計算、大額抵免期間門檻照樣觸發，實測同一段期間扣掉抵免的帳戶層級預算報 <code>costAmount</code> 0.05、<code>exclude-all-credits</code> 的預算報 0.37；付出的是免費方案的用量也以原價算進花費（推論：<code>FREE_TIER</code> 是預算接受的抵免類型，這次沒有量）。</li>
<li>改用 <code>--credit-types-treatment=include-specified-credits</code> 搭配 <code>--filter-credit-types=FREE_TIER</code>，換到的是只讓免費方案的用量照常扣掉；類型寫錯時回 <code>INVALID_ARGUMENT: ... is not a valid credit type string</code>，<code>--help</code> 不列出可用的類型，以 Cloud Billing 文件為準。</li>
<li>加 <code>--notifications-rule-monitoring-notification-channels=&lt;channel-resource-name&gt;</code>，把通知另外寄到 Cloud Monitoring 的電子郵件通知管道，換到的是值班的人收得到，付出的是只支援電子郵件類型：接上 webhook 類型的管道時只回 <code>INVALID_ARGUMENT: Request contains an invalid argument</code>，訊息不說原因。<code>&lt;channel-resource-name&gt;</code> 形如 <code>projects/&lt;project-id&gt;/notificationChannels/&lt;channel-id&gt;</code>，在主控台的 Monitoring 通知管道頁面查得到。</li>
</ul>
</li>
</ul>
<h2 id="建接收通知的主題並把預算接上">建接收通知的主題並把預算接上</h2>





<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">gcloud services <span class="nb">enable</span> pubsub.googleapis.com cloudfunctions.googleapis.com run.googleapis.com <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  cloudbuild.googleapis.com artifactregistry.googleapis.com eventarc.googleapis.com <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  cloudbilling.googleapis.com --project<span class="o">=</span>&lt;project-id&gt;
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 建立主題，並讓接停損開關的預算把通知送過來</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">gcloud pubsub topics create budget-alerts --project<span class="o">=</span>&lt;project-id&gt;
</span></span><span class="line"><span class="ln">7</span><span class="cl">gcloud billing budgets update &lt;budget-resource-name&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="se"></span>  --notifications-rule-pubsub-topic<span class="o">=</span>projects/&lt;project-id&gt;/topics/budget-alerts <span class="se">\
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="se"></span>  --billing-project<span class="o">=</span>&lt;project-id&gt;</span></span></code></pre></div><p><code>&lt;budget-resource-name&gt;</code> 是 <code>gcloud billing budgets list --billing-account=&lt;billing-account-id&gt; --billing-project=&lt;project-id&gt; --format=&quot;value(name)&quot;</code> 印出的完整名稱，形如 <code>billingAccounts/&lt;id&gt;/budgets/&lt;uuid&gt;</code>，與建立時給的顯示名稱是兩個值。設定通知目標時，GCP 會自動讓預算服務的系統帳號 <code>billing-budget-alert@system.gserviceaccount.com</code> 擁有這個主題的發布權限，<code>gcloud pubsub topics get-iam-policy budget-alerts --project=&lt;project-id&gt;</code> 看得到這條綁定。</p>
<p>預算接上主題之後，GCP 一天會送好幾次通知過來，不論花費有沒有越過門檻。第一則送到的時間兩次實跑不同：第一次實跑約 8 分鐘，第二次新建的預算約 35 分鐘；Cloud Billing 文件說第一則可能要數小時。通知的內容如下，<code>alertThresholdExceeded</code> 這類欄位要等越過門檻才會出現：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span><span class="nt">&#34;budgetDisplayName&#34;</span><span class="p">:</span> <span class="s2">&#34;&lt;budget-display-name&gt;&#34;</span><span class="p">,</span> <span class="nt">&#34;costAmount&#34;</span><span class="p">:</span> <span class="mf">0.0</span><span class="p">,</span> <span class="nt">&#34;costIntervalStart&#34;</span><span class="p">:</span> <span class="s2">&#34;2026-10-01T07:00:00Z&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"> <span class="nt">&#34;budgetAmount&#34;</span><span class="p">:</span> <span class="mf">320.0</span><span class="p">,</span> <span class="nt">&#34;budgetAmountType&#34;</span><span class="p">:</span> <span class="s2">&#34;SPECIFIED_AMOUNT&#34;</span><span class="p">,</span> <span class="nt">&#34;currencyCode&#34;</span><span class="p">:</span> <span class="s2">&#34;TWD&#34;</span><span class="p">}</span></span></span></code></pre></div><p>主題原本接在別的預算上時，只有一種做法拿得掉：<code>gcloud billing budgets update</code> 沒有清除 Pub/Sub 通知的旗標，要對預算 API 送 PATCH，<code>updateMask</code> 設成 <code>notificationsRule.pubsubTopic</code>、<code>notificationsRule.pubsubTopic</code> 設成空字串。這次用這個方式拿掉了；送出的完整指令沒有留下來，請求的形狀以 Cloud Billing Budget API 的 <code>budgets.patch</code> 文件為準。</p>
<ul>
<li><strong>沒做時</strong>（一個主題接了好幾個預算）：本文的函式不分辨通知來自哪一個預算，任何一個超支都會讓它停掉 <code>TARGET_PROJECT_ID</code>（從程式的判斷規則推論，這次沒有實跑）。要依預期花費收通知，另建一個不接主題的預算。</li>
</ul>
<h2 id="建服務帳號並授予兩個角色">建服務帳號並授予兩個角色</h2>





<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">gcloud iam service-accounts create budget-killswitch-sa --project<span class="o">=</span>&lt;project-id&gt;
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</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">gcloud projects add-iam-policy-binding &lt;project-id&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --member<span class="o">=</span>serviceAccount:budget-killswitch-sa@&lt;project-id&gt;.iam.gserviceaccount.com <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --role<span class="o">=</span>roles/billing.projectManager
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># 讀取專案的帳單狀態（getBillingInfo 需要 resourcemanager.projects.get）</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">gcloud projects add-iam-policy-binding &lt;project-id&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --member<span class="o">=</span>serviceAccount:budget-killswitch-sa@&lt;project-id&gt;.iam.gserviceaccount.com <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --role<span class="o">=</span>roles/browser</span></span></code></pre></div><p><code>roles/billing.projectManager</code> 只包含兩個權限，建立與刪除專案的帳單連結（<code>gcloud iam roles describe roles/billing.projectManager</code> 列得出來），讀取帳單狀態不在裡面。<code>roles/browser</code> 包含 <code>resourcemanager.projects.get</code>，整個角色是讀取專案、資料夾與組織的中繼資料以及專案 IAM policy 的六個權限，不包含讀取專案內資源的權限。執行部署指令的人另外要有這個服務帳號的 <code>roles/iam.serviceAccountUser</code>，才能把它指定給函式與觸發器；專案擁有者本來就有。要管多個專案時，每個專案各建一個限定在該專案的預算、一個主題與一個函式部署，兩個角色在每個專案各綁一次。</p>
<ul>
<li><strong>沒做時</strong>（只給 <code>roles/billing.projectManager</code>）：函式在解除連結之前先讀一次帳單狀態，那一步回 403「The caller does not have permission」；函式部署成功、狀態 <code>ACTIVE</code>，停損開關卻不會解除帳單連結。</li>
<li><strong>替代做法</strong>：拿掉函式裡讀帳單狀態的那一步、只留解除連結，換到的是少一個角色，付出的是重複的通知會對已經停用的專案再送一次解除連結（推論，這次沒有實跑）。</li>
</ul>
<h2 id="判斷並解除帳單連結的函式">判斷並解除帳單連結的函式</h2>
<p>下面的程式存成 <code>&lt;source-dir&gt;/main.py</code>，判斷規則只看實際花費：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">base64</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">json</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">import</span> <span class="nn">os</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="kn">import</span> <span class="nn">functions_framework</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="kn">from</span> <span class="nn">googleapiclient</span> <span class="kn">import</span> <span class="n">discovery</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">PROJECT_ID</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="p">[</span><span class="s2">&#34;TARGET_PROJECT_ID&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">PROJECT_NAME</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;projects/</span><span class="si">{</span><span class="n">PROJECT_ID</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">def</span> <span class="nf">should_disable</span><span class="p">(</span><span class="n">notification</span><span class="p">:</span> <span class="nb">dict</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="c1"># 只看實際花費：改看預測花費時，一次短暫的尖峰就可能讓函式停掉正常的服務</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="n">cost</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">notification</span><span class="p">[</span><span class="s2">&#34;costAmount&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="n">budget</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">notification</span><span class="p">[</span><span class="s2">&#34;budgetAmount&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">except</span> <span class="p">(</span><span class="ne">KeyError</span><span class="p">,</span> <span class="ne">TypeError</span><span class="p">,</span> <span class="ne">ValueError</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">({</span><span class="s2">&#34;warning&#34;</span><span class="p">:</span> <span class="s2">&#34;unreadable notification, ignored&#34;</span><span class="p">}))</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="k">return</span> <span class="n">budget</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="ow">and</span> <span class="n">cost</span> <span class="o">&gt;=</span> <span class="n">budget</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="k">def</span> <span class="nf">billing_enabled</span><span class="p">(</span><span class="n">billing</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="n">info</span> <span class="o">=</span> <span class="n">billing</span><span class="o">.</span><span class="n">projects</span><span class="p">()</span><span class="o">.</span><span class="n">getBillingInfo</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="n">PROJECT_NAME</span><span class="p">)</span><span class="o">.</span><span class="n">execute</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="k">return</span> <span class="n">info</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;billingEnabled&#34;</span><span class="p">,</span> <span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="nd">@functions_framework.cloud_event</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="k">def</span> <span class="nf">stop_billing</span><span class="p">(</span><span class="n">cloud_event</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">    <span class="c1"># 讀不懂的通知不構成停機的理由；直接結束，不拋出例外，免得 --retry 讓它一直重送</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">        <span class="n">notification</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">base64</span><span class="o">.</span><span class="n">b64decode</span><span class="p">(</span><span class="n">cloud_event</span><span class="o">.</span><span class="n">data</span><span class="p">[</span><span class="s2">&#34;message&#34;</span><span class="p">][</span><span class="s2">&#34;data&#34;</span><span class="p">]))</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">    <span class="k">except</span> <span class="p">(</span><span class="ne">KeyError</span><span class="p">,</span> <span class="ne">TypeError</span><span class="p">,</span> <span class="ne">ValueError</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">({</span><span class="s2">&#34;warning&#34;</span><span class="p">:</span> <span class="s2">&#34;undecodable message, ignored&#34;</span><span class="p">}))</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">({</span><span class="s2">&#34;received&#34;</span><span class="p">:</span> <span class="n">notification</span><span class="p">}))</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">
</span></span><span class="line"><span class="ln">35</span><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">should_disable</span><span class="p">(</span><span class="n">notification</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">({</span><span class="s2">&#34;action&#34;</span><span class="p">:</span> <span class="s2">&#34;none&#34;</span><span class="p">}))</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">
</span></span><span class="line"><span class="ln">39</span><span class="cl">    <span class="c1"># 呼叫 Cloud Billing API 的錯誤不攔，讓 --retry 重送這則通知</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">    <span class="n">billing</span> <span class="o">=</span> <span class="n">discovery</span><span class="o">.</span><span class="n">build</span><span class="p">(</span><span class="s2">&#34;cloudbilling&#34;</span><span class="p">,</span> <span class="s2">&#34;v1&#34;</span><span class="p">,</span> <span class="n">cache_discovery</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">billing_enabled</span><span class="p">(</span><span class="n">billing</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">({</span><span class="s2">&#34;action&#34;</span><span class="p">:</span> <span class="s2">&#34;none&#34;</span><span class="p">,</span> <span class="s2">&#34;reason&#34;</span><span class="p">:</span> <span class="s2">&#34;billing already disabled&#34;</span><span class="p">}))</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">        <span class="k">return</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">
</span></span><span class="line"><span class="ln">45</span><span class="cl">    <span class="c1"># billingAccountName 給空字串，就是把專案從帳單帳戶解除連結</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">    <span class="n">billing</span><span class="o">.</span><span class="n">projects</span><span class="p">()</span><span class="o">.</span><span class="n">updateBillingInfo</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">        <span class="n">name</span><span class="o">=</span><span class="n">PROJECT_NAME</span><span class="p">,</span> <span class="n">body</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;billingAccountName&#34;</span><span class="p">:</span> <span class="s2">&#34;&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">    <span class="p">)</span><span class="o">.</span><span class="n">execute</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">({</span><span class="s2">&#34;action&#34;</span><span class="p">:</span> <span class="s2">&#34;billing disabled&#34;</span><span class="p">,</span> <span class="s2">&#34;project&#34;</span><span class="p">:</span> <span class="n">PROJECT_ID</span><span class="p">}))</span></span></span></code></pre></div><p>同一個目錄放一份 <code>requirements.txt</code>，列出 <code>functions-framework</code> 與 <code>google-api-python-client</code>。實測對主題發一則不是 JSON 的訊息，函式留下一筆 <code>undecodable message, ignored</code> 的警告、請求回 200，之後沒有重送。</p>
<ul>
<li><strong>沒做時</strong>（讀不懂的訊息拋出例外）：加了 <code>--retry</code> 之後，那則訊息會被一直重送（從重試政策的作用推論，這次沒有實跑）。</li>
<li><strong>替代做法</strong>（判斷規則）：改看 <code>forecastThresholdExceeded</code>，換到的是更早解除連結、被濫用時損失更少，付出的是一次正常的流量尖峰也可能讓整個專案停擺；改成「讀不懂就解除連結」，換到的是格式異常時偏向保護費用，付出的是通知格式變動時就會誤停（兩者都是推論，這次沒有實跑）。現在的寫法在誤停與漏停兩種錯誤裡最不會誤停，代價是被濫用時要等到實際花費寫進帳單資料才動作。</li>
</ul>
<h2 id="部署函式兩個服務帳號執行個體上限與重試">部署函式：兩個服務帳號、執行個體上限與重試</h2>





<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">gcloud functions deploy budget-killswitch --gen2 --region<span class="o">=</span>&lt;region&gt; --runtime<span class="o">=</span>python312 <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --source<span class="o">=</span>&lt;source-dir&gt; --entry-point<span class="o">=</span>stop_billing --trigger-topic<span class="o">=</span>budget-alerts <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --service-account<span class="o">=</span>budget-killswitch-sa@&lt;project-id&gt;.iam.gserviceaccount.com <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --trigger-service-account<span class="o">=</span>budget-killswitch-sa@&lt;project-id&gt;.iam.gserviceaccount.com <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --set-env-vars<span class="o">=</span><span class="nv">TARGET_PROJECT_ID</span><span class="o">=</span>&lt;project-id&gt; --max-instances<span class="o">=</span><span class="m">1</span> --retry <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --project<span class="o">=</span>&lt;project-id&gt;</span></span></code></pre></div><ul>
<li>
<p><code>&lt;region&gt;</code>：函式所在的 region，例如 <code>asia-east1</code>；它與預算或要停用帳單的專案在哪個區域無關。</p>
</li>
<li>
<p><code>--runtime</code>：Python 的版本，可用的值由 <code>gcloud functions runtimes list --region=&lt;region&gt;</code> 列出；舊版本停止支援之後部署會被拒絕。</p>
</li>
<li>
<p><code>&lt;source-dir&gt;</code>：放 <code>main.py</code> 與 <code>requirements.txt</code> 的本機目錄。</p>
</li>
<li>
<p><code>--entry-point</code>：程式裡加了 <code>@functions_framework.cloud_event</code> 的那個函式的名稱。</p>
</li>
<li>
<p><code>--service-account</code>：函式執行時的身分，函式以它呼叫 Cloud Billing API。</p>
</li>
<li>
<p><code>--trigger-service-account</code>：Eventarc 觸發器呼叫函式時的身分。不給這個旗標時用哪個帳號，<code>gcloud functions deploy --help</code> 寫的是專案的 Compute Engine 預設服務帳號，而這次實測用的是 <code>--service-account</code> 那一個，兩者不同，所以明確指定，部署後用 <code>gcloud functions describe budget-killswitch --region=&lt;region&gt; --project=&lt;project-id&gt;</code> 輸出的 <code>eventTrigger.serviceAccountEmail</code> 確認。</p>
</li>
<li>
<p><code>TARGET_PROJECT_ID</code>：要停用帳單的專案。</p>
</li>
<li>
<p><code>--max-instances=1</code>：同一時間最多只啟動一個執行個體，超支之後一天好幾則通知依序處理，不會同時送出多個解除連結的呼叫。</p>
</li>
<li>
<p><code>--retry</code>：讓 Pub/Sub 依觸發器的<a href="/blog/backend/knowledge-cards/retry-policy/" data-link-title="Retry Policy" data-link-desc="說明重試策略如何區分暫時性錯誤、永久錯誤與副作用風險">重試政策</a>在幾分鐘內重送失敗的通知，一次暫時性的錯誤（剛授予的權限還沒生效）不必等到下一則通知。不會自己恢復的錯誤（權限少了一個、專案的帳單連結被鎖定）重試也不會成功，那一類只能靠〈兩段驗收：第一則真實通知，再發兩則測試訊息〉在上線前找出來。</p>
</li>
<li>
<p><strong>沒做時</strong>（不加 <code>--retry</code>）：觸發器的重試政策是 <code>RETRY_POLICY_DO_NOT_RETRY</code>（<code>gcloud functions describe</code> 輸出的 <code>eventTrigger.retryPolicy</code>）。函式在讀取帳單狀態時拋出例外，Cloud Run 的請求紀錄卻是 HTTP 200，Pub/Sub 因此把那則超出預算的通知標為已確認收訊（<a href="/blog/backend/knowledge-cards/ack-nack/" data-link-title="Ack / Nack" data-link-desc="說明 consumer 如何向 broker 回報訊息處理結果">ack</a>），之後不再送；下一次有機會解除連結，要等數小時後的下一則預算通知，這段期間帳單照常計費。</p>
</li>
<li>
<p><strong>沒做時</strong>（不加 <code>--max-instances=1</code>）：超支之後同時到的幾則通知可能各自啟動一個執行個體、各送一次解除連結（推論，這次沒有實跑）。</p>
</li>
</ul>
<h2 id="授予觸發器呼叫函式的權限">授予觸發器呼叫函式的權限</h2>
<p>第二代的 Cloud Run functions 跑在 Cloud Run 上，Eventarc 觸發器以 <code>--trigger-service-account</code> 指定的服務帳號身分呼叫它，而那個服務帳號預設沒有呼叫權限：</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"># 第一個 budget-killswitch 是函式名稱：第二代函式在 Cloud Run 上的服務與函式同名</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud run services add-iam-policy-binding budget-killswitch --region<span class="o">=</span>&lt;region&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --member<span class="o">=</span>serviceAccount:budget-killswitch-sa@&lt;project-id&gt;.iam.gserviceaccount.com <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --role<span class="o">=</span>roles/run.invoker --project<span class="o">=</span>&lt;project-id&gt;</span></span></code></pre></div><ul>
<li><strong>沒做時</strong>：Cloud Run 以 403 擋下每一次推送，紀錄寫著「The IAM principal lacks {run.routes.invoke} permission」，函式一次都不會執行，而 <code>gcloud functions describe</code> 顯示的狀態仍是 <code>ACTIVE</code>。</li>
<li><strong>替代做法</strong>：觸發器改用另一個只有 <code>roles/run.invoker</code> 的服務帳號，換到的是呼叫權限與解除帳單連結的權限分在兩個身分上，付出的是多管一個服務帳號（推論，這次沒有實跑）。</li>
</ul>
<h2 id="停損開關放在另一個專案時要改的指令">停損開關放在另一個專案時要改的指令</h2>
<p>主題、函式與服務帳號放在一個專門放帳單工具的管理專案、要停用帳單的是另一個專案時，上面的指令照下面改，其餘不變：</p>
<ul>
<li>主題、服務帳號、函式、觸發器的呼叫權限：<code>--project=&lt;project-id&gt;</code> 與服務帳號位址裡的 <code>&lt;project-id&gt;</code> 都填管理專案。</li>
<li>服務帳號的兩個角色：<code>gcloud projects add-iam-policy-binding</code> 的第一個引數改成被保護的專案，<code>--member</code> 仍是管理專案裡的服務帳號。</li>
<li>部署：<code>--set-env-vars=TARGET_PROJECT_ID=</code> 填被保護的專案。</li>
<li>預算：<code>--filter-projects=projects/</code> 填被保護的專案。</li>
</ul>
<p>第二次實跑把函式放在 <code>blog-verify-20261024</code>、<code>TARGET_PROJECT_ID</code> 指向另一個可拋棄的專案：超出預算的訊息讓被保護的專案在 9 秒後停用，管理專案的帳單維持啟用，緊接著的低於預算訊息照常處理。這樣放的效果與代價見概念篇〈停損開關放在另一個專案〉。</p>
<ul>
<li><strong>沒做時</strong>（停損開關跑在被保護的同一個專案）：解除連結之後，查詢函式狀態的 API 直接回 403，訊息要求確認帳單帳戶；函式本身在停用後約一分鐘內仍處理了下一則通知，之後的行為這次沒有觀察。兩段驗收的兩則測試訊息要照順序發。</li>
</ul>
<h2 id="兩段驗收第一則真實通知再發兩則測試訊息">兩段驗收：第一則真實通知，再發兩則測試訊息</h2>
<p>驗收分成預算到 Pub/Sub 主題、主題之後兩段，各用不同的訊息確認，理由見概念篇〈驗收：真實通知確認預算到 Pub/Sub 主題的一段，測試訊息確認主題之後的一段〉。</p>
<p><strong>Pub/Sub 主題之前的那一段，用第一則真實的預算通知確認。</strong> 接上主題之後等第一則通知，用下面這條查詢找到一筆 <code>received</code> 紀錄，確認 <code>budgetDisplayName</code> 是接停損開關的那個預算，再拿它的 <code>costAmount</code> 對照帳單報表上這個專案本期的費用：通知的值接近 0 而報表不是，就是抵免設定或 <code>--filter-projects</code> 設錯了。</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"># 列出函式收到的通知；--freshness 依等待的時間調整</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud logging <span class="nb">read</span> <span class="s1">&#39;resource.type=&#34;cloud_run_revision&#34; AND resource.labels.service_name=&#34;budget-killswitch&#34; AND jsonPayload.received:*&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --freshness<span class="o">=</span>1d --format<span class="o">=</span><span class="s2">&#34;json(timestamp,jsonPayload.received)&#34;</span> --project<span class="o">=</span>&lt;project-id&gt;</span></span></code></pre></div><p><strong>Pub/Sub 主題之後的那一段，用兩則測試訊息確認。</strong> 低於預算的那一則確認訊息送得到、判斷邏輯會跑；停損開關跑在被保護的同一個專案時，它要排在超出預算那一則之前。</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">gcloud pubsub topics publish budget-alerts --project<span class="o">=</span>&lt;project-id&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --message<span class="o">=</span><span class="s1">&#39;{&#34;costAmount&#34;: 15.0, &#34;budgetAmount&#34;: 320.0, &#34;currencyCode&#34;: &#34;TWD&#34;}&#39;</span>
</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"># 等一兩分鐘再查：紀錄寫入有延遲；預期 action 是 none；--freshness=10m 只看最近十分鐘</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">gcloud logging <span class="nb">read</span> <span class="s1">&#39;resource.type=&#34;cloud_run_revision&#34; AND resource.labels.service_name=&#34;budget-killswitch&#34; AND jsonPayload.action:*&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --freshness<span class="o">=</span>10m --format<span class="o">=</span><span class="s2">&#34;value(timestamp,jsonPayload.action)&#34;</span> --project<span class="o">=</span>&lt;project-id&gt;</span></span></code></pre></div><p>這條查詢回傳零筆時，有三種可能：紀錄還沒寫入、觸發器被 403 擋下、查詢條件寫錯。把條件裡的 <code>AND jsonPayload.action:*</code> 換成 <code>AND logName:&quot;run.googleapis.com%2Frequests&quot;</code>、<code>--format</code> 換成 <code>&quot;value(timestamp,httpRequest.status)&quot;</code>，查同一個服務的請求紀錄：有 403 的請求代表缺〈授予觸發器呼叫函式的權限〉那條綁定，查不到任何請求代表訊息沒有送到觸發器。</p>
<p>低於預算的訊息在判斷規則那一步就結束，讀取帳單狀態與解除帳單連結的權限一次都沒有行使。超出預算的那一則才會走到它們，結果看帳單狀態：</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">gcloud pubsub topics publish budget-alerts --project<span class="o">=</span>&lt;project-id&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --message<span class="o">=</span><span class="s1">&#39;{&#34;costAmount&#34;: 350.0, &#34;budgetAmount&#34;: 320.0, &#34;currencyCode&#34;: &#34;TWD&#34;}&#39;</span>
</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"># 等一兩分鐘再查；預期 billingEnabled 變成 false、billingAccountName 變成空字串</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">gcloud billing projects describe &lt;project-id&gt;</span></span></code></pre></div><p>測試訊息裡的 <code>budgetAmount</code> 大於 0、而且小於或等於 <code>costAmount</code> 時，函式就會解除帳單連結，與預算實際設定的金額無關。實測從發出超出預算的訊息到 <code>billingEnabled</code> 變成 <code>false</code> 約十三秒。驗收完用 <code>gcloud billing projects link &lt;project-id&gt; --billing-account=&lt;billing-account-id&gt;</code> 重新連結，連結完成才算驗收結束。這次驗收會實際讓專案停擺，排在停機可以接受的時段做；之後每一次修改函式、角色或觸發器，都再做一次。</p>
<ul>
<li>
<p><strong>沒做時</strong>（只看部署成功與 <code>ACTIVE</code>）：〈建服務帳號並授予兩個角色〉的 <code>roles/browser</code>、〈部署函式：兩個服務帳號、執行個體上限與重試〉的 <code>--retry</code>、〈授予觸發器呼叫函式的權限〉的 <code>roles/run.invoker</code>，實測時各自讓停損開關在狀態顯示正常時失效過一次。</p>
</li>
<li>
<p><strong>替代做法</strong>（專案有停用之後不能遺失的資料時）：拆開驗證，第二次實跑做過。</p>
<ul>
<li>在可拋棄的專案上完整觸發一次：另外部署一份複本，函式名稱與主題都換掉（例如 <code>budget-killswitch-test</code> 與 <code>budget-alerts-test</code>），<code>TARGET_PROJECT_ID</code> 指向一個可拋棄的空專案，兩個角色綁在那個空專案上，再對它發超出預算的訊息。複本若沿用正式的函式名稱，部署會蓋掉正式的函式；訊息若發到正式的主題，被停用的就是正式專案。</li>
<li>在正式專案上只確認權限：<code>gcloud policy-intelligence troubleshoot-policy iam //cloudresourcemanager.googleapis.com/projects/&lt;project-id&gt; --principal-email=&lt;sa-email&gt; --permission=&lt;permission&gt;</code>。<code>&lt;sa-email&gt;</code> 是 <code>budget-killswitch-sa@&lt;project-id&gt;.iam.gserviceaccount.com</code>，<code>&lt;permission&gt;</code> 依序填 <code>resourcemanager.projects.deleteBillingAssignment</code> 與 <code>resourcemanager.projects.get</code>；執行的專案要先啟用 <code>policytroubleshooter.googleapis.com</code>。輸出的 <code>overallAccessState</code> 是 <code>CAN_ACCESS</code> 才算有，沒有綁角色的服務帳號回 <code>CANNOT_ACCESS</code>。</li>
</ul>
<p>拆開驗證仍沒有行使正式專案上那一次 <code>updateBillingInfo</code>，驗收紀錄要寫明缺了這一段。</p>
</li>
</ul>
<h2 id="心跳監看函式停止收到通知時告警">心跳監看：函式停止收到通知時告警</h2>
<p>預算一天會送好幾則通知，函式的紀錄長時間沒有出現帶 <code>action</code> 欄位的紀錄就通知，讓函式停止收到通知這件事本身也成為訊號。心跳監看建在第三個專案：停損開關正常觸發之後，被保護的專案整個停擺，告警放在那裡會一起停；也不放在管理專案，管理專案失效時要有人知道。實測的做法是用 <code>gcloud logging metrics create</code> 把帶 <code>action</code> 欄位的那類紀錄做成日誌指標，再在 Cloud Monitoring 建一條缺席條件（condition absent）的告警；缺席條件的時間窗上限是 23 小時 30 分，設成 24 小時會被拒，所以時間窗設 23 小時。設好之後暫時把時間窗縮到幾分鐘，確認通知送到收的人手上，再改回 23 小時：實測時間窗設 3 分鐘，最後一筆紀錄之後 4 到 9 分鐘告警送達。這次建指標與告警政策的完整指令沒有留下來，日誌指標的 filter 用〈兩段驗收：第一則真實通知，再發兩則測試訊息〉那條 <code>jsonPayload.action:*</code> 的查詢條件。</p>
<ul>
<li><strong>沒做時</strong>：別人調整了角色（例如權限盤點時收掉 <code>roles/browser</code>）或鎖定了專案的帳單連結時，部署的人不會知道要重驗，停損開關一直顯示 <code>ACTIVE</code>（推論，這次沒有實跑）。</li>
</ul>
<h2 id="超支停用之後的恢復">超支停用之後的恢復</h2>
<p>恢復之前先調預算的理由（<code>costAmount</code> 是本期累計值，只重新連結的話下一則通知會讓函式再解除一次）與依停用原因分開的順序，見概念篇〈解除連結之後恢復服務的順序〉。</p>
<p><strong>服務自己出錯</strong>：修好程式之後，先調高預算金額或刪掉函式，再重新連結：</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">gcloud billing budgets update &lt;budget-resource-name&gt; --budget-amount<span class="o">=</span>&lt;amount&gt;&lt;currency-code&gt; --billing-project<span class="o">=</span>&lt;project-id&gt;
</span></span><span class="line"><span class="ln">2</span><span class="cl">gcloud billing projects link &lt;project-id&gt; --billing-account<span class="o">=</span>&lt;billing-account-id&gt;</span></span></code></pre></div><p><strong>資源被別人濫用</strong>：重新連結之前，把預算金額調成目前的 <code>costAmount</code> 加上清理期間能承受的金額，並先撤銷外洩的 API 金鑰與服務帳號金鑰：實測帳單停用期間 <code>gcloud services api-keys list</code> 與 <code>delete</code> 都照常可用。重新連結之後列出並刪除不屬於自己的 VM、instance template 與 MIG，確認乾淨之後再把預算調回原本的金額。</p>
<p>實測停用約兩分半鐘再重新連結，Cloud Router、Cloud NAT、函式與它的觸發器都還在、設定沒變（這次沒有觀察 VM 與資料庫）；停用期間 <code>gcloud compute routers list</code> 回傳零筆，重新連結之後 Cloud Router 仍在。重新連結之後 4 分鐘內沒有再被停用：停用期間對主題發布的訊息照樣被接受，而函式在停用後的那一分鐘內就把它處理掉了（回報帳單已停用），沒有留到重新連結之後。</p>
<ul>
<li><strong>替代做法</strong>：刪掉函式再重新連結，換到的是不必估清理期間的金額，付出的是清理期間沒有停損開關；被濫用的情形下，這段時間正是攻擊者可能再開資源的時候（推論，這次沒有實跑）。</li>
</ul>
]]></content:encoded></item><item><title>GCP 上的雲端費用失控防護：預算警示、強制執行支出上限與自動停用帳單的作用範圍、反應時間與判斷</title><link>https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/spend-protection-gcp-budget-killswitch/</link><pubDate>Sat, 24 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/spend-protection-gcp-budget-killswitch/</guid><description>&lt;p>這篇整理在 GCP 上為費用失控設防的機制與判斷依據，範圍從專案已經連結到一個帳單帳戶開始。&lt;a href="https://tarrragon.github.io/blog/devops/operations/08-cost-management/dev-environment-cost/" data-link-title="開發環境成本控制" data-link-desc="壓非 production 環境的隱形浪費時，用排程關機、用完即拆的 ephemeral 環境、CI 跑 spot、以及 per-team 成本上限">開發環境成本控制&lt;/a>的〈Per-team 成本上限〉說明了為什麼要給團隊與環境設一個費用的上限；這一篇處理那個上限在 GCP 上由哪些機制執行、每一個機制在費用超過門檻時實際做了什麼，以及從費用發生到它動作之間隔了多久。三個機制分別是預算警示（花費達門檻時寄通知）、強制執行支出上限（暫停指定服務的新用量），以及由預算通知觸發的處置（由程式把專案從帳單帳戶解除連結，或只停止花錢的資源）；另外寫停損開關放在另一個專案的理由、驗收為什麼分兩段，以及停用之後的恢復順序。建預算、主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與恢復的逐步指令與實跑結果，在 &lt;a href="https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/hands-on/budget-killswitch-walkthrough/" data-link-title="GCP 預算警示與停損開關的實測紀錄：建預算、Pub/Sub 主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與停用後的恢復" data-link-desc="要在 GCP 專案上建預算警示，或建一個花費超過預算就解除帳單連結的停損開關，想照一次實跑過的順序做、看每一步漏掉或換一種做法的結果時讀；也用在函式部署成功、狀態 ACTIVE 卻從沒解除過帳單連結，或專案因超支被停用之後要恢復的時候">GCP 預算警示與停損開關的實測紀錄&lt;/a>。&lt;/p>
&lt;p>文中的觀察來自 2026 年 10 月在一個 GCP 測試專案上的實跑；標「未實跑」的做法沒有在實跑中執行；引用 Cloud Billing 文件或 &lt;code>gcloud&lt;/code> 說明的行為，出處寫在句子裡。主控台的選項與服務清單會變動，以操作當下看到的為準。&lt;/p>
&lt;h2 id="費用失控的來源與反應時間">費用失控的來源與反應時間&lt;/h2>
&lt;p>費用失控指的是費用增加的速度快過人發現它、處理它的速度。來源分成兩類，兩類的處置方式不同：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>服務自己出錯&lt;/strong>：程式對失敗的請求無限重試、一個事件觸發的函式又產生同一種事件而形成遞迴、自動擴縮允許的最大執行個體數設得太高。費用花在自己的服務上，修好程式之後費用就停止增加。&lt;/li>
&lt;li>&lt;strong>資源被別人濫用&lt;/strong>：外洩的 API 金鑰（API key）被拿去呼叫付費 API、被入侵的 VM 被拿去挖礦。費用花在攻擊者的用途上，處理的人要先切斷攻擊者能用的資源，再處理入侵。&lt;/li>
&lt;/ul>
&lt;p>兩類的共同難處在時間：費用從發生到任何機制動作，中間隔著一段延遲。一個防護機制從費用發生到動作，要經過三段：雲端把用量算成費用、寫進帳單資料；帳單資料觸發通知；通知觸發處置（人或程式）。把用量算成費用那一段的延遲由雲端決定，GCP 的帳單主控台寫著費用資料要數小時後才會顯示、有時超過 24 小時。實測時一個專案已經跑了幾個小時的 VM 與 GKE 叢集，那段期間送到的預算通知裡 &lt;code>costAmount&lt;/code>（本期累計花費）仍是 &lt;code>0.0&lt;/code>；那個預算用預設的抵免設定，所以這次分不出 &lt;code>0.0&lt;/code> 來自延遲還是抵免。&lt;/p>
&lt;p>預算警示與由預算通知觸發的處置讀的是帳單資料，反應時間至少是這段延遲。強制執行支出上限依的是 GCP 估算的費用，Cloud Billing 文件說這份估算比帳單報表上的費用快得多，但仍不是即時，延遲期間超出上限的部分照常計費。這三個機制的費用最後停在哪裡也不同：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>強制執行支出上限與解除帳單連結&lt;/strong>：費用停止增加，總額是觸發的金額再加上延遲期間已經發生的費用。&lt;/li>
&lt;li>&lt;strong>只停止花錢的資源&lt;/strong>：被停掉的資源不再計費，磁碟與儲存照常計費。&lt;/li>
&lt;li>&lt;strong>預算警示&lt;/strong>：不停止任何用量，費用停在哪裡取決於收到通知的人多快處理。&lt;/li>
&lt;/ul>
&lt;p>要在費用發生的當下限制它，得靠不經過帳單資料的限制，它們也決定了延遲期間最多能多花多少：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>能用多少&lt;/strong>：API 的配額、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">速率限制（rate limit）&lt;/a>、服務允許的最大執行個體數（例如 Cloud Run 的 &lt;code>--max-instances&lt;/code>、MIG 的最大台數），以及 Compute Engine 的區域 CPU 與 GPU 配額。配額調到實際用量附近，被入侵的帳號能開的 VM 台數就有上限。&lt;/li>
&lt;li>&lt;strong>能建什麼&lt;/strong>：組織政策（Organization Policy，GCP 繁體中文介面寫作「機構政策」）可以限制專案能啟用的 API，搭配 Compute Engine 的自訂限制條件可以限制能用的機型，前提是專案屬於一個組織（未實跑：測試帳號底下沒有組織）。&lt;/li>
&lt;li>&lt;strong>金鑰能做什麼&lt;/strong>：API 金鑰的使用範圍限定可呼叫的 API 與來源；實測 &lt;code>gcloud services api-keys create&lt;/code> 不給 &lt;code>--api-target&lt;/code> 時被拒，新建的金鑰一定帶 API 範圍。金鑰被盜用時，先撤銷或限制那一把金鑰，影響範圍比停用整個專案的帳單小。&lt;/li>
&lt;/ul>
&lt;h2 id="預算警示門檻依據範圍抵免與通知對象">預算警示：門檻依據、範圍、抵免與通知對象&lt;/h2>
&lt;p>預算（budget）是建在帳單帳戶上的一組門檻：本期花費達到預算的某個比例時，GCP 寄通知給帳單帳戶的管理員。門檻有兩種依據，&lt;strong>實際花費&lt;/strong>（已經寫進帳單資料的費用）與&lt;strong>預測花費&lt;/strong>（GCP 依目前的速度推估到期末的費用）；預測花費的門檻通常比實際花費的門檻早觸發，代價是一次短暫的尖峰也可能觸發預測花費的門檻。預算只寄通知，扣款照常進行。建立指令（&lt;code>gcloud billing budgets create&lt;/code> 加 &lt;code>--filter-projects&lt;/code>、&lt;code>--threshold-rule&lt;/code>）與它的兩道權限檢查，在 &lt;a href="https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/hands-on/budget-killswitch-walkthrough/" data-link-title="GCP 預算警示與停損開關的實測紀錄：建預算、Pub/Sub 主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與停用後的恢復" data-link-desc="要在 GCP 專案上建預算警示，或建一個花費超過預算就解除帳單連結的停損開關，想照一次實跑過的順序做、看每一步漏掉或換一種做法的結果時讀；也用在函式部署成功、狀態 ACTIVE 卻從沒解除過帳單連結，或專案因超支被停用之後要恢復的時候">GCP 預算警示與停損開關的實測紀錄&lt;/a> 的〈建立限定一個專案的預算〉。&lt;/p>
&lt;p>決定一個預算會不會在對的時候通知對的人，有三件事：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>範圍&lt;/strong>：預算用 &lt;code>--filter-projects&lt;/code> 限定要計算的專案。&lt;code>gcloud&lt;/code> 說明寫著不給這個旗標時涵蓋所有專案，通知裡的 &lt;code>costAmount&lt;/code> 就是帳單帳戶下所有專案的合計：別的專案的花費也會讓它越過門檻，而這個專案自己的超支被合計稀釋。&lt;/li>
&lt;li>&lt;strong>抵免&lt;/strong>：&lt;code>gcloud&lt;/code> 說明寫著預設把帳戶上的抵免扣掉之後才算花費。帳戶上有大額抵免（新創方案、試用額度）時，&lt;code>costAmount&lt;/code> 在抵免用完之前都接近 0，門檻在抵免被燒光的整段期間都不會觸發。抵免本身也是要保護的錢時，建立預算時加 &lt;code>--credit-types-treatment=exclude-all-credits&lt;/code>，花費以抵免前的金額計算，代價是免費方案的用量也會以原價算進去（推論：&lt;code>FREE_TIER&lt;/code> 是預算接受的抵免類型，這次沒有量）；只想讓部分抵免照常扣掉時，改用 &lt;code>include-specified-credits&lt;/code> 搭配 &lt;code>--filter-credit-types&lt;/code>。試用額度用完或試用期結束之後，帳單帳戶與資源會被停用、停止還是開始計費，GCP 與 Oracle Cloud、AWS 的規則見 &lt;a href="https://tarrragon.github.io/blog/devops/infra/cloud-providers/pricing-comparison/" data-link-title="雲端免費額度與小型主機的收費比較：試用金與固定額度、計費單位、區域限制、試用結束後的處置、出站流量與查價項目" data-link-desc="Oracle Cloud、Google Cloud 與 AWS 的免費方案分別由哪幾種額度組成，比較時要換算的計費單位，免費主機能放在哪些區域，試用期結束後帳號與資源會怎樣，以及付費主機的價格該去哪裡查">雲端免費額度與小型主機的收費比較&lt;/a> 的〈試用結束後帳號與資源的處置〉。&lt;/li>
&lt;li>&lt;strong>通知對象&lt;/strong>：通知預設寄給帳單帳戶的管理員與帳單使用者，建立預算時先確認這兩個角色的成員信箱有人在看。值班的人通常不在這兩個角色裡，要讓他們收到，把通知另外寄到 Cloud Monitoring 的電子郵件通知管道（Cloud Billing 文件寫明預算只支援電子郵件類型的管道）；要送到簡訊、聊天工具或 webhook，改由 Pub/Sub 主題接一段轉送程式。&lt;/li>
&lt;/ul>
&lt;p>預算另外可以把通知送到一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">Pub/Sub&lt;/a> &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">主題（topic）&lt;/a>，由預算通知觸發的處置就是從這個主題接收通知；接法在 &lt;a href="https://tarrragon.github.io/blog/devops/infra/cloud-providers/gcp/hands-on/budget-killswitch-walkthrough/" data-link-title="GCP 預算警示與停損開關的實測紀錄：建預算、Pub/Sub 主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與停用後的恢復" data-link-desc="要在 GCP 專案上建預算警示，或建一個花費超過預算就解除帳單連結的停損開關，想照一次實跑過的順序做、看每一步漏掉或換一種做法的結果時讀；也用在函式部署成功、狀態 ACTIVE 卻從沒解除過帳單連結，或專案因超支被停用之後要恢復的時候">GCP 預算警示與停損開關的實測紀錄&lt;/a> 的〈建接收通知的主題並把預算接上〉。預算警示沒有辦法手動送出一封測試用的門檻通知，第一封真實的門檻通知寄到之前，「通知送得到收的人」這一段沒有驗證過；寄到時確認收到的是誰、多久後有人看到。&lt;/p>
&lt;h2 id="強制執行支出上限涵蓋的服務範圍與暫停期間">強制執行支出上限：涵蓋的服務、範圍與暫停期間&lt;/h2>
&lt;p>在帳單主控台的「Budgets &amp;amp; caps」頁面（2026 年 10 月主控台顯示的名稱）建立預算時，表單裡有一個「強制執行支出上限（適用於某些服務）」的選項：花費達到上限時，GCP 暫停表單裡選定的那個專案、那一項服務的新用量，直到這個預算期間結束（以月為期的預算是到月底，下一期自動重設）或有人手動解除為止。它是帳單功能裡不必自己寫程式就能暫停用量的機制，範圍有四個限制：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>一個上限只涵蓋一個專案裡的一項服務&lt;/strong>：要保護多項服務，就每項各建一個。&lt;/li>
&lt;li>&lt;strong>只涵蓋清單上的服務&lt;/strong>：2026 年 10 月建立表單時列出的是 Cloud Run、Cloud Run functions、Gemini API 與 Vertex AI 四項，Compute Engine 與 GKE 不在清單上。表單的服務下拉選單就是涵蓋清單，建立之前打開看一次。&lt;/li>
&lt;li>&lt;strong>持續性資源的固定費用不暫停&lt;/strong>：Cloud Billing 文件說明，與持續性資源綁在一起的固定用量不會被暫停，執行也有延遲，超出的部分照常計費。&lt;/li>
&lt;li>&lt;strong>以抵免前的金額計算&lt;/strong>：Cloud Billing 文件寫明上限的花費不扣抵免與折扣，與預算的預設抵免設定相反。帳戶有抵免、而上限與預算的金額相近時，上限會先達到。&lt;/li>
&lt;/ul>
&lt;p>工作負載跑在清單上的服務時，這個選項不必寫程式；它暫停的是這項服務的新用量，所以對正式服務同樣是一次停機，只是範圍限於這一項服務。是否建立，依〈由預算通知觸發的處置：解除帳單連結與只停止花錢的資源〉比較的兩種損失（多付的費用，與服務停止、資料可能遺失）來判斷。一個專案同時有清單上的服務與不在清單上的服務時，兩種做法可以並用：清單上的服務各建一個支出上限，在估算費用達到上限時先暫停那一項服務；不在清單上的資源由只停止花錢的資源那一種處置接手。改用解除帳單連結的話，停掉的是整個專案，清單上的服務也一起停。&lt;/p></description><content:encoded><![CDATA[<p>這篇整理在 GCP 上為費用失控設防的機制與判斷依據，範圍從專案已經連結到一個帳單帳戶開始。<a href="/blog/devops/operations/08-cost-management/dev-environment-cost/" data-link-title="開發環境成本控制" data-link-desc="壓非 production 環境的隱形浪費時，用排程關機、用完即拆的 ephemeral 環境、CI 跑 spot、以及 per-team 成本上限">開發環境成本控制</a>的〈Per-team 成本上限〉說明了為什麼要給團隊與環境設一個費用的上限；這一篇處理那個上限在 GCP 上由哪些機制執行、每一個機制在費用超過門檻時實際做了什麼，以及從費用發生到它動作之間隔了多久。三個機制分別是預算警示（花費達門檻時寄通知）、強制執行支出上限（暫停指定服務的新用量），以及由預算通知觸發的處置（由程式把專案從帳單帳戶解除連結，或只停止花錢的資源）；另外寫停損開關放在另一個專案的理由、驗收為什麼分兩段，以及停用之後的恢復順序。建預算、主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與恢復的逐步指令與實跑結果，在 <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/budget-killswitch-walkthrough/" data-link-title="GCP 預算警示與停損開關的實測紀錄：建預算、Pub/Sub 主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與停用後的恢復" data-link-desc="要在 GCP 專案上建預算警示，或建一個花費超過預算就解除帳單連結的停損開關，想照一次實跑過的順序做、看每一步漏掉或換一種做法的結果時讀；也用在函式部署成功、狀態 ACTIVE 卻從沒解除過帳單連結，或專案因超支被停用之後要恢復的時候">GCP 預算警示與停損開關的實測紀錄</a>。</p>
<p>文中的觀察來自 2026 年 10 月在一個 GCP 測試專案上的實跑；標「未實跑」的做法沒有在實跑中執行；引用 Cloud Billing 文件或 <code>gcloud</code> 說明的行為，出處寫在句子裡。主控台的選項與服務清單會變動，以操作當下看到的為準。</p>
<h2 id="費用失控的來源與反應時間">費用失控的來源與反應時間</h2>
<p>費用失控指的是費用增加的速度快過人發現它、處理它的速度。來源分成兩類，兩類的處置方式不同：</p>
<ul>
<li><strong>服務自己出錯</strong>：程式對失敗的請求無限重試、一個事件觸發的函式又產生同一種事件而形成遞迴、自動擴縮允許的最大執行個體數設得太高。費用花在自己的服務上，修好程式之後費用就停止增加。</li>
<li><strong>資源被別人濫用</strong>：外洩的 API 金鑰（API key）被拿去呼叫付費 API、被入侵的 VM 被拿去挖礦。費用花在攻擊者的用途上，處理的人要先切斷攻擊者能用的資源，再處理入侵。</li>
</ul>
<p>兩類的共同難處在時間：費用從發生到任何機制動作，中間隔著一段延遲。一個防護機制從費用發生到動作，要經過三段：雲端把用量算成費用、寫進帳單資料；帳單資料觸發通知；通知觸發處置（人或程式）。把用量算成費用那一段的延遲由雲端決定，GCP 的帳單主控台寫著費用資料要數小時後才會顯示、有時超過 24 小時。實測時一個專案已經跑了幾個小時的 VM 與 GKE 叢集，那段期間送到的預算通知裡 <code>costAmount</code>（本期累計花費）仍是 <code>0.0</code>；那個預算用預設的抵免設定，所以這次分不出 <code>0.0</code> 來自延遲還是抵免。</p>
<p>預算警示與由預算通知觸發的處置讀的是帳單資料，反應時間至少是這段延遲。強制執行支出上限依的是 GCP 估算的費用，Cloud Billing 文件說這份估算比帳單報表上的費用快得多，但仍不是即時，延遲期間超出上限的部分照常計費。這三個機制的費用最後停在哪裡也不同：</p>
<ul>
<li><strong>強制執行支出上限與解除帳單連結</strong>：費用停止增加，總額是觸發的金額再加上延遲期間已經發生的費用。</li>
<li><strong>只停止花錢的資源</strong>：被停掉的資源不再計費，磁碟與儲存照常計費。</li>
<li><strong>預算警示</strong>：不停止任何用量，費用停在哪裡取決於收到通知的人多快處理。</li>
</ul>
<p>要在費用發生的當下限制它，得靠不經過帳單資料的限制，它們也決定了延遲期間最多能多花多少：</p>
<ul>
<li><strong>能用多少</strong>：API 的配額、<a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">速率限制（rate limit）</a>、服務允許的最大執行個體數（例如 Cloud Run 的 <code>--max-instances</code>、MIG 的最大台數），以及 Compute Engine 的區域 CPU 與 GPU 配額。配額調到實際用量附近，被入侵的帳號能開的 VM 台數就有上限。</li>
<li><strong>能建什麼</strong>：組織政策（Organization Policy，GCP 繁體中文介面寫作「機構政策」）可以限制專案能啟用的 API，搭配 Compute Engine 的自訂限制條件可以限制能用的機型，前提是專案屬於一個組織（未實跑：測試帳號底下沒有組織）。</li>
<li><strong>金鑰能做什麼</strong>：API 金鑰的使用範圍限定可呼叫的 API 與來源；實測 <code>gcloud services api-keys create</code> 不給 <code>--api-target</code> 時被拒，新建的金鑰一定帶 API 範圍。金鑰被盜用時，先撤銷或限制那一把金鑰，影響範圍比停用整個專案的帳單小。</li>
</ul>
<h2 id="預算警示門檻依據範圍抵免與通知對象">預算警示：門檻依據、範圍、抵免與通知對象</h2>
<p>預算（budget）是建在帳單帳戶上的一組門檻：本期花費達到預算的某個比例時，GCP 寄通知給帳單帳戶的管理員。門檻有兩種依據，<strong>實際花費</strong>（已經寫進帳單資料的費用）與<strong>預測花費</strong>（GCP 依目前的速度推估到期末的費用）；預測花費的門檻通常比實際花費的門檻早觸發，代價是一次短暫的尖峰也可能觸發預測花費的門檻。預算只寄通知，扣款照常進行。建立指令（<code>gcloud billing budgets create</code> 加 <code>--filter-projects</code>、<code>--threshold-rule</code>）與它的兩道權限檢查，在 <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/budget-killswitch-walkthrough/" data-link-title="GCP 預算警示與停損開關的實測紀錄：建預算、Pub/Sub 主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與停用後的恢復" data-link-desc="要在 GCP 專案上建預算警示，或建一個花費超過預算就解除帳單連結的停損開關，想照一次實跑過的順序做、看每一步漏掉或換一種做法的結果時讀；也用在函式部署成功、狀態 ACTIVE 卻從沒解除過帳單連結，或專案因超支被停用之後要恢復的時候">GCP 預算警示與停損開關的實測紀錄</a> 的〈建立限定一個專案的預算〉。</p>
<p>決定一個預算會不會在對的時候通知對的人，有三件事：</p>
<ul>
<li><strong>範圍</strong>：預算用 <code>--filter-projects</code> 限定要計算的專案。<code>gcloud</code> 說明寫著不給這個旗標時涵蓋所有專案，通知裡的 <code>costAmount</code> 就是帳單帳戶下所有專案的合計：別的專案的花費也會讓它越過門檻，而這個專案自己的超支被合計稀釋。</li>
<li><strong>抵免</strong>：<code>gcloud</code> 說明寫著預設把帳戶上的抵免扣掉之後才算花費。帳戶上有大額抵免（新創方案、試用額度）時，<code>costAmount</code> 在抵免用完之前都接近 0，門檻在抵免被燒光的整段期間都不會觸發。抵免本身也是要保護的錢時，建立預算時加 <code>--credit-types-treatment=exclude-all-credits</code>，花費以抵免前的金額計算，代價是免費方案的用量也會以原價算進去（推論：<code>FREE_TIER</code> 是預算接受的抵免類型，這次沒有量）；只想讓部分抵免照常扣掉時，改用 <code>include-specified-credits</code> 搭配 <code>--filter-credit-types</code>。試用額度用完或試用期結束之後，帳單帳戶與資源會被停用、停止還是開始計費，GCP 與 Oracle Cloud、AWS 的規則見 <a href="/blog/devops/infra/cloud-providers/pricing-comparison/" data-link-title="雲端免費額度與小型主機的收費比較：試用金與固定額度、計費單位、區域限制、試用結束後的處置、出站流量與查價項目" data-link-desc="Oracle Cloud、Google Cloud 與 AWS 的免費方案分別由哪幾種額度組成，比較時要換算的計費單位，免費主機能放在哪些區域，試用期結束後帳號與資源會怎樣，以及付費主機的價格該去哪裡查">雲端免費額度與小型主機的收費比較</a> 的〈試用結束後帳號與資源的處置〉。</li>
<li><strong>通知對象</strong>：通知預設寄給帳單帳戶的管理員與帳單使用者，建立預算時先確認這兩個角色的成員信箱有人在看。值班的人通常不在這兩個角色裡，要讓他們收到，把通知另外寄到 Cloud Monitoring 的電子郵件通知管道（Cloud Billing 文件寫明預算只支援電子郵件類型的管道）；要送到簡訊、聊天工具或 webhook，改由 Pub/Sub 主題接一段轉送程式。</li>
</ul>
<p>預算另外可以把通知送到一個 <a href="/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">Pub/Sub</a> <a href="/blog/backend/knowledge-cards/topic/" data-link-title="Topic" data-link-desc="說明 topic 如何把事件依主題分流給不同訂閱者">主題（topic）</a>，由預算通知觸發的處置就是從這個主題接收通知；接法在 <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/budget-killswitch-walkthrough/" data-link-title="GCP 預算警示與停損開關的實測紀錄：建預算、Pub/Sub 主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與停用後的恢復" data-link-desc="要在 GCP 專案上建預算警示，或建一個花費超過預算就解除帳單連結的停損開關，想照一次實跑過的順序做、看每一步漏掉或換一種做法的結果時讀；也用在函式部署成功、狀態 ACTIVE 卻從沒解除過帳單連結，或專案因超支被停用之後要恢復的時候">GCP 預算警示與停損開關的實測紀錄</a> 的〈建接收通知的主題並把預算接上〉。預算警示沒有辦法手動送出一封測試用的門檻通知，第一封真實的門檻通知寄到之前，「通知送得到收的人」這一段沒有驗證過；寄到時確認收到的是誰、多久後有人看到。</p>
<h2 id="強制執行支出上限涵蓋的服務範圍與暫停期間">強制執行支出上限：涵蓋的服務、範圍與暫停期間</h2>
<p>在帳單主控台的「Budgets &amp; caps」頁面（2026 年 10 月主控台顯示的名稱）建立預算時，表單裡有一個「強制執行支出上限（適用於某些服務）」的選項：花費達到上限時，GCP 暫停表單裡選定的那個專案、那一項服務的新用量，直到這個預算期間結束（以月為期的預算是到月底，下一期自動重設）或有人手動解除為止。它是帳單功能裡不必自己寫程式就能暫停用量的機制，範圍有四個限制：</p>
<ul>
<li><strong>一個上限只涵蓋一個專案裡的一項服務</strong>：要保護多項服務，就每項各建一個。</li>
<li><strong>只涵蓋清單上的服務</strong>：2026 年 10 月建立表單時列出的是 Cloud Run、Cloud Run functions、Gemini API 與 Vertex AI 四項，Compute Engine 與 GKE 不在清單上。表單的服務下拉選單就是涵蓋清單，建立之前打開看一次。</li>
<li><strong>持續性資源的固定費用不暫停</strong>：Cloud Billing 文件說明，與持續性資源綁在一起的固定用量不會被暫停，執行也有延遲，超出的部分照常計費。</li>
<li><strong>以抵免前的金額計算</strong>：Cloud Billing 文件寫明上限的花費不扣抵免與折扣，與預算的預設抵免設定相反。帳戶有抵免、而上限與預算的金額相近時，上限會先達到。</li>
</ul>
<p>工作負載跑在清單上的服務時，這個選項不必寫程式；它暫停的是這項服務的新用量，所以對正式服務同樣是一次停機，只是範圍限於這一項服務。是否建立，依〈由預算通知觸發的處置：解除帳單連結與只停止花錢的資源〉比較的兩種損失（多付的費用，與服務停止、資料可能遺失）來判斷。一個專案同時有清單上的服務與不在清單上的服務時，兩種做法可以並用：清單上的服務各建一個支出上限，在估算費用達到上限時先暫停那一項服務；不在清單上的資源由只停止花錢的資源那一種處置接手。改用解除帳單連結的話，停掉的是整個專案，清單上的服務也一起停。</p>
<h2 id="由預算通知觸發的處置解除帳單連結與只停止花錢的資源">由預算通知觸發的處置：解除帳單連結與只停止花錢的資源</h2>
<p>這一節的做法是讓預算通知觸發一段程式，由程式在花費達到預算時停止用量。停止用量有兩種做法，差別在停掉的範圍：</p>
<ul>
<li><strong>解除帳單連結</strong>：程式呼叫 Cloud Billing API 把專案從帳單帳戶解除連結。專案沒有連結帳單帳戶時，GCP 停止專案裡所有的服務（包括免費方案的服務），費用也就停止增加。這段程式在下文稱為<strong>停損開關</strong>（kill switch）。</li>
<li><strong>只停止花錢的資源</strong>：程式改成停止專案裡執行中的 VM、把 MIG 的目標台數設成 0、把 GKE node pool 縮到 0 個節點。Cloud Billing 文件的〈Control resource usage with notifications〉就是這種做法，範例是逐台停止一個 zone 裡執行中的 VM。實測改用 <code>instances().aggregatedList</code> 跨全部 zone 列出 <code>RUNNING</code> 的 VM 再逐台 <code>stop</code>，服務帳號只給 <code>roles/compute.instanceAdmin.v1</code>：訊息發出後函式立刻送出停止，VM 約兩分鐘後變成 <code>TERMINATED</code>，帳單仍是啟用、開機磁碟仍在。資料留在磁碟與儲存裡、那部分照常計費，所以費用不會歸零；代價是程式只停它列得出來的資源，漏掉一種資源型別，或攻擊者另外開的新資源，那部分照常計費。</li>
</ul>
<p>呼叫鏈上的 Eventarc 是 GCP 把事件（這裡是主題裡的訊息）轉成對函式的呼叫的服務；第二代的 Cloud Run functions 以 Eventarc 觸發器接主題。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">預算（帳單帳戶）── 通知 ──&gt; Pub/Sub 主題 ── 推送 ──&gt; Eventarc 觸發器 ── 呼叫 ──&gt; Cloud Run functions 函式 ──&gt; 解除帳單連結，或停止花錢的資源</span></span></code></pre></div><p>兩種做法的預算、主題、觸發器與重試設定相同，差別在函式本體、服務帳號的角色與驗收的終點：改成只停止花錢的資源時，函式改成呼叫 Compute Engine 與 GKE 的 API，服務帳號改給能修改那些資源的角色（VM 與 MIG 例如 <code>roles/compute.instanceAdmin.v1</code>，GKE node pool 的縮放要 container 的權限，以 <code>gcloud iam roles describe</code> 確認），驗收的終點改成超出預算的訊息之後 <code>gcloud compute instances list</code> 的狀態變成 <code>TERMINATED</code>、MIG 的目標台數與 node pool 的節點數變成 0，每一個 zone 與資源型別各確認一次（MIG 與 node pool 的部分未實跑）。</p>
<p>建之前先決定要不要建、建哪一種，因為停損開關的效果是一次自己造成的全面停機：VM、資料庫、函式、Kubernetes 叢集一起停擺，停損開關本身跑在同一個專案時也會停。Cloud Billing 文件另外警告，停用帳單時資源可能被永久刪除，也不保證服務能恢復，所以這是一次可能遺失資料的停機。判斷方式是比較兩種損失：「被濫用或程式出錯而多付一筆費用」與「這個專案的服務停止、資料可能遺失」，哪一種比較大。</p>
<p>判斷的單位是專案，因為停損開關停的是整個專案。混合了正式服務與沙箱的專案照正式服務判斷；想對沙箱建停損開關，就先把沙箱搬到自己的專案。逐個專案套下面三條：</p>
<ul>
<li><strong>停機時沒有人受影響、而且專案裡的資料都能從別處重建</strong>（只跑實驗的專案、個人的沙箱多半是這樣）：多付的費用沒有上限，建停損開關。</li>
<li><strong>停機可以接受、而資料不能遺失</strong>（團隊共用的 staging、有持久資料的實驗專案）：建只停止花錢的資源的那一種。</li>
<li><strong>停機會影響使用者或同事</strong>（正式服務）：全面停機的代價通常高於一段時間的超支，改用預算警示，寄到值班的人手上（見〈預算警示：門檻依據、範圍、抵免與通知對象〉的通知對象），並把〈費用失控的來源與反應時間〉列的即時限制設上，讓帳單資料延遲期間的花費有上限。沒有人能值班時，處理時間就是自己下一次看到通知的時間，配額要依這段時間能承受的花費來設。風險集中的功能可以拆出去，例如 API 金鑰寫在網頁或 App 裡、任何人都拿得到那把金鑰的功能：拆到自己的專案再對那個專案建停損開關。停損開關停掉那個專案時，正式服務裡用這把金鑰的功能也會一起停，正式服務的其餘部分與正式服務的資料要放在另一個專案。</li>
</ul>
<p>接停損開關的預算要另外建、範圍限定在要停用的那一個專案，金額設成這個專案本期能承受的最高花費減掉帳單資料延遲期間多出的費用。延遲費用的上限約是「每小時最多能花的費用 × 延遲的時數」，每小時最多能花多少由〈費用失控的來源與反應時間〉列的配額決定，所以先把配額壓到實際需要的量，才估得出這個上限。一個主題只接一個預算：函式不分辨通知來自哪一個預算，好幾個預算送到同一個主題時，任何一個超支都會讓它停掉同一個專案。</p>
<p>停損開關有三項設定，任何一項設錯，部署指令都回報成功、函式狀態也都顯示 <code>ACTIVE</code>，停損開關卻不會解除帳單連結：服務帳號要另外有讀取帳單狀態的 <code>roles/browser</code>（<code>roles/billing.projectManager</code> 只能建立與刪除帳單連結）、部署要加 <code>--retry</code>（不加時，函式在中途拋出例外而 Cloud Run 回 200，那則超出預算的通知被確認收訊、不再重送）、觸發器的服務帳號要有呼叫函式的 <code>roles/run.invoker</code>（缺它時每一次推送都被 403 擋下，函式一次都不執行）。三項的指令與實測時看到的錯誤，在 <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/budget-killswitch-walkthrough/" data-link-title="GCP 預算警示與停損開關的實測紀錄：建預算、Pub/Sub 主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與停用後的恢復" data-link-desc="要在 GCP 專案上建預算警示，或建一個花費超過預算就解除帳單連結的停損開關，想照一次實跑過的順序做、看每一步漏掉或換一種做法的結果時讀；也用在函式部署成功、狀態 ACTIVE 卻從沒解除過帳單連結，或專案因超支被停用之後要恢復的時候">GCP 預算警示與停損開關的實測紀錄</a> 的〈建服務帳號並授予兩個角色〉〈部署函式：兩個服務帳號、執行個體上限與重試〉〈授予觸發器呼叫函式的權限〉。</p>
<h2 id="停損開關放在另一個專案">停損開關放在另一個專案</h2>
<p>專案裡跑的工作負載可能被入侵時，攻擊者拿到的身分若有專案的編輯權限，也能刪掉同一個專案裡的函式與主題。主題、函式與服務帳號可以放在一個專門放帳單工具、不受停用影響的專案（Cloud Billing 文件稱為 FinOps administration project，下文稱管理專案；文件建議把帳單相關的主題集中在這種專案），被保護的專案只綁服務帳號的兩個角色。這樣放有三個效果：解除連結之後函式仍在運作，能記錄並通知人「哪個專案被停了」；被保護的專案裡被入侵的身分刪不掉它；驗收時函式不會跟著停，所以兩則測試訊息不必照順序發。代價是管理專案成了所有停損開關的共同依賴：它的帳單停了或權限被收掉，所有被保護的專案一起失去停損開關，所以監看停損開關有沒有持續收到通知的心跳監看要放在管理專案以外。分開放時要改的指令在 <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/budget-killswitch-walkthrough/" data-link-title="GCP 預算警示與停損開關的實測紀錄：建預算、Pub/Sub 主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與停用後的恢復" data-link-desc="要在 GCP 專案上建預算警示，或建一個花費超過預算就解除帳單連結的停損開關，想照一次實跑過的順序做、看每一步漏掉或換一種做法的結果時讀；也用在函式部署成功、狀態 ACTIVE 卻從沒解除過帳單連結，或專案因超支被停用之後要恢復的時候">GCP 預算警示與停損開關的實測紀錄</a> 的〈停損開關放在另一個專案時要改的指令〉。</p>
<h2 id="驗收真實通知確認預算到-pubsub-主題的一段測試訊息確認主題之後的一段">驗收：真實通知確認預算到 Pub/Sub 主題的一段，測試訊息確認主題之後的一段</h2>
<p>停損開關平常不動作，壞掉與「還沒輪到它動作」在任何狀態欄上都長得一樣；〈由預算通知觸發的處置：解除帳單連結與只停止花錢的資源〉最後一段列的三項設定，實測時各自讓它在狀態顯示正常時失效過一次。所以部署成功不算完成，驗收看的是呼叫鏈最後一段的結果：專案的帳單狀態。這是<a href="/blog/report/protection-proves-itself-by-firing/" data-link-title="平常不動作的防護機制的部署驗證：設定被接受、元件狀態正常、執行失敗被吞掉與測試從中途注入這些提早出現的成功訊號，驗證查詢與處置指令的假陰性，以及合成觸發本身有損害時的分段驗證" data-link-desc="部署停損開關、稽核紀錄、告警或自動處置這類平常不動作的防護機制之後，要判斷它算不算部署完成時讀；也用在驗收紀錄寫著「部署成功」「狀態 ACTIVE」「設定已套用」、或驗證查詢回傳零筆就被當成結論的時候">平常不動作的防護機制的部署驗證</a>的一個應用。</p>
<p>驗收分成兩段，因為測試訊息是從主題送進去的：主題之後的每一段（觸發器、呼叫權限、函式、角色、重試）都會被行使到，主題之前的那一段（預算的範圍、金額、抵免設定、發布到主題的權限）不會。主題之前的那一段，用第一則真實的預算通知確認：通知裡的 <code>costAmount</code> 對照帳單報表上這個專案本期的費用，接近 0 而報表不是，就是抵免設定或範圍設錯了。主題之後的那一段，用一則低於預算、一則超出預算的測試訊息確認，兩則走的是函式的兩條分支；只有超出預算那一則會行使解除連結要用到的兩個權限，結果看 <code>billingEnabled</code> 有沒有變成 <code>false</code>。</p>
<p>這次驗收會實際讓專案停擺。專案有停用之後不能遺失的資料時，改用拆開驗證：在可拋棄的專案上用一份換了名稱與主題的複本完整觸發一次，在正式專案上只用 Policy Troubleshooter 確認服務帳號的權限；拆開驗證仍沒有行使正式專案上那一次解除連結，驗收紀錄要寫明缺了這一段。部署之後別人調整了角色或鎖定了帳單連結時，部署的人不會知道要重驗，所以另設一個<a href="/blog/backend/knowledge-cards/dead-mans-switch/" data-link-title="Dead Man&#39;s Switch（心跳監看）" data-link-desc="需要知道監控或排程工作自己停掉了、而它停掉時發不出任何告警時，以「時限內沒收到訊號就通知」反過來判斷">心跳監看（dead man&rsquo;s switch）</a>：函式的紀錄長時間沒有出現就通知，讓函式停止收到通知這件事本身也成為訊號。兩段的訊息、查詢指令、拆開驗證的複本與心跳監看的設法，在 <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/budget-killswitch-walkthrough/" data-link-title="GCP 預算警示與停損開關的實測紀錄：建預算、Pub/Sub 主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與停用後的恢復" data-link-desc="要在 GCP 專案上建預算警示，或建一個花費超過預算就解除帳單連結的停損開關，想照一次實跑過的順序做、看每一步漏掉或換一種做法的結果時讀；也用在函式部署成功、狀態 ACTIVE 卻從沒解除過帳單連結，或專案因超支被停用之後要恢復的時候">GCP 預算警示與停損開關的實測紀錄</a> 的〈兩段驗收：第一則真實通知，再發兩則測試訊息〉與〈心跳監看：函式停止收到通知時告警〉。</p>
<h2 id="解除連結之後恢復服務的順序">解除連結之後恢復服務的順序</h2>
<p>實際因為超支而停用之後，<code>costAmount</code> 是本期累計值，整個預算期間都不會降回預算以下；只重新連結的話，下一則預算通知（一天會來好幾則）會讓函式再解除一次。恢復的順序依停用的原因而不同：</p>
<ul>
<li><strong>服務自己出錯</strong>：修好程式之後，先調高預算金額或刪掉函式，再重新連結。</li>
<li><strong>資源被別人濫用</strong>：重新連結之前，把預算金額調成目前的 <code>costAmount</code> 加上清理期間能承受的金額，讓停損開關在攻擊者再開資源時仍會動作，又不會在清理途中被已經累計的花費再觸發一次；並先撤銷外洩的 API 金鑰與服務帳號金鑰。重新連結之後列出並刪除不屬於自己的資源，清單要在重新連結之後列：實測停用期間 <code>gcloud compute routers list</code> 回傳零筆，而重新連結之後 Cloud Router 仍在，所以停用期間的空清單不代表資源已經刪除。確認乾淨之後再把預算調回原本的金額。</li>
</ul>
<p>指令與實測時資源在停用前後的狀態，在 <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/budget-killswitch-walkthrough/" data-link-title="GCP 預算警示與停損開關的實測紀錄：建預算、Pub/Sub 主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與停用後的恢復" data-link-desc="要在 GCP 專案上建預算警示，或建一個花費超過預算就解除帳單連結的停損開關，想照一次實跑過的順序做、看每一步漏掉或換一種做法的結果時讀；也用在函式部署成功、狀態 ACTIVE 卻從沒解除過帳單連結，或專案因超支被停用之後要恢復的時候">GCP 預算警示與停損開關的實測紀錄</a> 的〈超支停用之後的恢復〉。</p>
<h2 id="延伸閱讀逐步的實作上限的理由費用的歸因防護機制的驗收與服務帳號的兩邊權限">延伸閱讀：逐步的實作、上限的理由、費用的歸因、防護機制的驗收與服務帳號的兩邊權限</h2>
<ul>
<li>→ <a href="/blog/devops/infra/cloud-providers/gcp/hands-on/budget-killswitch-walkthrough/" data-link-title="GCP 預算警示與停損開關的實測紀錄：建預算、Pub/Sub 主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與停用後的恢復" data-link-desc="要在 GCP 專案上建預算警示，或建一個花費超過預算就解除帳單連結的停損開關，想照一次實跑過的順序做、看每一步漏掉或換一種做法的結果時讀；也用在函式部署成功、狀態 ACTIVE 卻從沒解除過帳單連結，或專案因超支被停用之後要恢復的時候">GCP 預算警示與停損開關的實測紀錄</a>：建預算、主題、服務帳號、函式與觸發器、兩段驗收、心跳監看與恢復的逐步指令、實跑結果，以及每一步漏掉或換一種做法時的結果</li>
<li>→ <a href="/blog/devops/operations/08-cost-management/dev-environment-cost/" data-link-title="開發環境成本控制" data-link-desc="壓非 production 環境的隱形浪費時，用排程關機、用完即拆的 ephemeral 環境、CI 跑 spot、以及 per-team 成本上限">開發環境成本控制</a>：〈Per-team 成本上限〉說明給每個團隊與環境設上限的理由，本篇是那個上限在 GCP 上的執行方式</li>
<li>→ <a href="/blog/devops/operations/08-cost-management/cost-monitoring/" data-link-title="成本監控與告警" data-link-desc="要讓雲端帳單不失控時，靠歸因把每筆花費追到擁有者、用異常告警抓突增、以及選 showback 或 chargeback 讓成本有人負責">成本監控與告警</a>：收到預算警示之後，靠資源標記定位是哪個團隊的哪類資源在漲；停損開關解除連結之前，要依歸因的結果選定是哪一個專案</li>
<li>→ <a href="/blog/report/protection-proves-itself-by-firing/" data-link-title="平常不動作的防護機制的部署驗證：設定被接受、元件狀態正常、執行失敗被吞掉與測試從中途注入這些提早出現的成功訊號，驗證查詢與處置指令的假陰性，以及合成觸發本身有損害時的分段驗證" data-link-desc="部署停損開關、稽核紀錄、告警或自動處置這類平常不動作的防護機制之後，要判斷它算不算部署完成時讀；也用在驗收紀錄寫著「部署成功」「狀態 ACTIVE」「設定已套用」、或驗證查詢回傳零筆就被當成結論的時候">平常不動作的防護機制的部署驗證</a>：停損開關、稽核紀錄與告警共用的驗收方法，以及「部署成功、狀態正常」為什麼不算驗收</li>
<li>→ <a href="/blog/devops/infra/knowledge-cards/service-account/" data-link-title="Service Account（服務帳號）" data-link-desc="程式、排程或雲端服務要以自己的身分呼叫雲端 API、要決定給它哪些角色、遇到「狀態正常卻呼叫失敗」的權限問題、或看到 IAM 綁定裡的 serviceAccount: 成員時，查服務帳號是什麼、它的權限要從「它能做什麼」與「誰能以它的身分行事」兩邊設，以及它和人的帳號在管理上的差別">Service Account（服務帳號）</a>：服務帳號的權限分成「它能做什麼」與「誰能以它的身分行事」兩邊，停損開關的角色與部署者的 <code>serviceAccountUser</code> 各屬一邊</li>
</ul>
]]></content:encoded></item><item><title>預知的大流量要加多大、編多少預算：從業務預估到擴容規格與費用區間的換算鏈、給決策者與會計的數字，以及每次活動之後的校準</title><link>https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/planned-peak-sizing-and-budget/</link><pubDate>Tue, 27 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/planned-peak-sizing-and-budget/</guid><description>&lt;p>這篇整理事先知道會有大流量（行銷活動、檔期、上架、直播）時，容量規劃的規格與預算怎麼從同一份業務預估推出來。各節依序是：決策者問「要加多大」與會計問「要多少預算」各自要的是什麼；從業務預估到峰值請求量、再到各層要開幾台的換算鏈，每一個換算係數從哪裡來、預估的誤差怎麼用三點估計帶著走；從台數到費用區間的算法，包括擴容的計費時數、計費模式、跟著用量計費的項目與付款時點；給決策者的選項要附哪些數字、各自適合什麼條件；以及每次活動之後怎麼校準換算係數、沒有歷史資料時怎麼起步。峰值的形狀與倍率階梯在 &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算&lt;/a>，計費模式與單位請求成本在 &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/cost-model/" data-link-title="成本模型" data-link-desc="把容量規劃的資源翻成錢時，釐清 on-demand、reserved、spot 三種計費模式的取捨、怎麼算單位請求成本、以及過度與不足配置各自的經濟代價">成本模型&lt;/a>，活動前的準備時程、預先擴容的做法與值班在 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="可預期峰值從 T-90 到 T-0 的準備時程：容量計畫、quota 申請、壓測、game day 與 pre-scaling 各排在事件前幾天、排在那裡的理由與項目之間的依賴，pre-scaling 的做法、watch room、event tier 分級與準備時間不夠時被壓縮的部分，以及事後 retro">9.11 高峰事件準備&lt;/a>，這裡不重寫。&lt;/p>
&lt;h2 id="決策者與會計各自要的數字規格與風險金額期間與付款時點">決策者與會計各自要的數字：規格與風險、金額期間與付款時點&lt;/h2>
&lt;p>事先知道的大流量，在活動之前通常會被問兩個問題，兩個問題要的數字不同：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>決策者問要加多大&lt;/strong>：要的是規格與風險，也就是加到多少台、多大的資料庫，以及不加或少加時會發生什麼（回應變慢、排隊、下單失敗的比例），才能在幾個選項之間選。&lt;/li>
&lt;li>&lt;strong>會計問要多少預算&lt;/strong>：要的是金額、期間與付款時點，也就是費用落在哪幾個月、哪一筆是一次性的、哪一筆是每月經常性的、什麼時候從信用卡或帳戶扣走，才能編進預算、確認付得出來。&lt;/li>
&lt;/ul>
&lt;p>規格與預算各估一次時，決策者核准的規格與會計核准的金額會對不上：活動前一週才發現預算買不到核准的規格，或預算編了、規格卻不是照那個預算規劃的。同一份業務預估推出來的兩份數字，改一個假設（例如預估人數加倍）時會一起變，也一起被看到，所以兩份數字從同一份預估推。預算先定下來的情形，同一條換算鏈倒過來算：從預算推出買得到的台數，再推出能服務的峰值，超出的部分交給下面〈給決策者的選項〉裡的降級或等候室。&lt;/p>
&lt;h2 id="從業務預估到台數換算鏈換算係數的來源與三點估計">從業務預估到台數：換算鏈、換算係數的來源與三點估計&lt;/h2>
&lt;p>業務預估是活動負責人給得出來的數字：預計多少人來、集中在哪個時段。台數是工程要準備的數字：每一層要開幾台。兩者之間是一條換算鏈，前三步各用一個換算係數乘或除，第四步把台數對到機型與規格，上一步的輸出是下一步的輸入：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>業務預估 → 峰值時段的使用者數&lt;/strong>：預計來的人數，乘上集中在峰值時段的比例（下文稱&lt;strong>集中比例&lt;/strong>）。預計人數要包含平常在同一時段就會來的訪客；不包含時，把平常同一時段的請求量加到第二步的結果上。集中比例依活動的形式差很多（限量搶購集中在開賣的頭幾分鐘、全館折扣分散在整天），要從同類活動的歷史量；峰值的形狀在 &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算&lt;/a>。&lt;/li>
&lt;li>&lt;strong>使用者數 → 到源站的峰值請求量&lt;/strong>：乘上每個使用者在峰值時段發出的請求數（下文稱&lt;strong>每人請求數&lt;/strong>），再乘上到源站的比例（1 減去 CDN 的命中率），再除以峰值時段的秒數，得到源站每秒要處理的請求數。每人請求數從平常的流量模型量（&lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/traffic-model/" data-link-title="流量模型建立" data-link-desc="要把「這個服務會收到什麼樣的流量」量化成容量規劃的輸入時，釐清該量哪幾個維度、峰均比為什麼是最關鍵的單一指標、以及怎麼從 production log 抽出可信的模型">流量模型建立&lt;/a>）；活動期間的行為可能跟平常不同，例如延遲上升時使用者重新整理、客戶端重試，每人請求數會跟著變大。&lt;/li>
&lt;li>&lt;strong>峰值請求量 → 各層的台數&lt;/strong>：除以一台在目標利用率下的每秒處理量，結果無條件進位成整數台。一台的處理量從壓力測試量（&lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/load-testing-tools/" data-link-title="壓力測試工具與方法" data-link-desc="要用壓測驗證容量規劃時，釐清壓測到底在找什麼、k6 與 wrk 各自的定位、怎麼讀壓測輸出的延遲分布，以及讓結果失真的幾個反模式">壓力測試工具與方法&lt;/a>）；目標利用率指膝點處理量的比例（不是 CPU 使用率），設在膝點以下，留出的那段餘裕吸收秒級的波動與故障（&lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算&lt;/a>〈安全係數的數學根據在飽和曲線〉）。故障的單位常是一個可用區：部署在 N 個可用區、要承受掉一個時，剩下的主機會吃到原本 N ÷（N－1）倍的量，目標利用率要再壓低到這個比例以下。目標利用率每一層不同：資料庫這類有狀態、有鎖競爭的元件膝點來得早，它的目標利用率要取得比無狀態的應用伺服器低（各類元件留多少的討論在 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型&lt;/a>〈Headroom budget 設計〉，概念見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/headroom-budget/" data-link-title="Headroom Budget" data-link-desc="說明容量規劃中為應付異常 burst &amp;#43; AZ 故障 &amp;#43; forecast 誤差的安全餘量">Headroom Budget&lt;/a> 卡）。資料庫、快取、第三方 API 這些不一定能水平加台的層，要再乘上每個請求觸發幾次查詢或呼叫，並對照它們的上限；從延遲目標往下反推各層的方法在〈峰值估算〉的〈從業務指標反推各層峰值〉。&lt;/li>
&lt;li>&lt;strong>台數 → 規格&lt;/strong>：台數、機型、資料庫規格、頻寬。&lt;/li>
&lt;/ol>
&lt;p>預估本身的誤差，這篇從目標利用率拆出來另外處理。〈峰值估算〉與 9.6 把預估誤差也算進膝點以下的那段餘裕（9.6 的〈Headroom budget 設計〉把預測的誤差列為 headroom 要涵蓋的第一項）；這篇改用下面的三點估計把它帶著走，所以目標利用率留下的餘裕只涵蓋秒級的波動與故障，用 9.6 的比例時要先扣掉預測誤差那一份，不重複算兩次。&lt;/p>
&lt;p>換算鏈裡誤差最大的是集中比例與每人請求數：每台處理量是壓測量的，相對可靠；集中比例與每人請求數在沒辦過同類活動時是估的。這兩個係數各取三個值，保守（偏高）、預期、樂觀（偏低），高的配高的、低的配低的，整條鏈跑三次，得到峰值請求量的&lt;strong>三點估計&lt;/strong>（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/peak-forecast/" data-link-title="Peak Forecast" data-link-desc="說明預期峰值流量的預測方法 — 容量規劃的第一個輸入">Peak Forecast&lt;/a> 卡寫的最壞、預期、最好三點）；活動負責人對預計人數也沒把握時，人數一樣取三個值配進去。保守那一點是每個係數同時取偏高值的組合，不是機率上的某個分位數：兩個係數彼此獨立時，同時偏高的機率比各自偏高低，保守點會偏寬；兩者正相關時（越集中的活動，使用者越會重新整理）就不會。搶購機器人、重試風暴與預料之外的爆紅會超過保守點，這要靠下面選項裡的回退方式處理。三點離預期要拉多寬，看同類活動的歷史：有幾次的歷史時，取歷史上實際值與預估值的偏差範圍；只有一兩次、或沒有歷史時，一次活動只是一個樣本，偏差範圍不可靠，要取得更寬（下面〈每次活動之後的校準，以及沒有歷史資料時的起步〉）。&lt;/p>
&lt;p>示例（數字都是為了說明換算而設的）：預計 20 萬人來（已含平常同一時段的訪客），峰值時段是開賣的頭 10 分鐘（600 秒），不放 CDN、請求全部到源站。集中比例取 20%、30%、40%，每人請求數取 20、30、40。壓測量到一台應用伺服器在延遲開始上升時（膝點）約 420 請求/秒，目標利用率取 60%，約 252，取整成 250。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>估計&lt;/th>
 &lt;th>峰值請求量（每秒）&lt;/th>
 &lt;th>應用伺服器（每台 250 請求/秒）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>樂觀&lt;/td>
 &lt;td>200,000 × 0.2 × 20 ÷ 600 ≈ 1,333&lt;/td>
 &lt;td>6 台&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>預期&lt;/td>
 &lt;td>200,000 × 0.3 × 30 ÷ 600 ＝ 3,000&lt;/td>
 &lt;td>12 台&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>保守&lt;/td>
 &lt;td>200,000 × 0.4 × 40 ÷ 600 ≈ 5,333&lt;/td>
 &lt;td>22 台&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>資料庫假設每個請求平均觸發 1 次查詢，壓測時 4,000 次/秒延遲開始上升，目標利用率取比應用層低的 50%，可以處理 2,000 次/秒，預期的 3,000 已經超過。實際要先從流量模型拆出讀與寫各佔多少：讀的部分用&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-write-split/" data-link-title="Read-Write Split" data-link-desc="說明讀寫流量如何分流到 primary 與 replica，以及它引入的一致性責任">讀取副本&lt;/a>或快取吸收，寫的部分要靠佇列非同步處理（&lt;a href="https://tarrragon.github.io/blog/devops/operations/07-burst-traffic/queue-buffering/" data-link-title="Queue 緩衝" data-link-desc="在 ingestion 和 processing 之間加 message queue 做 burst 緩衝 — Kafka / NATS / Redis Streams 的選型和引入條件">Queue 緩衝&lt;/a>）或更大的主資料庫。這次的瓶頸在資料庫，所以下面各個選項都要寫出資料庫那一層怎麼處理。&lt;/p></description><content:encoded><![CDATA[<p>這篇整理事先知道會有大流量（行銷活動、檔期、上架、直播）時，容量規劃的規格與預算怎麼從同一份業務預估推出來。各節依序是：決策者問「要加多大」與會計問「要多少預算」各自要的是什麼；從業務預估到峰值請求量、再到各層要開幾台的換算鏈，每一個換算係數從哪裡來、預估的誤差怎麼用三點估計帶著走；從台數到費用區間的算法，包括擴容的計費時數、計費模式、跟著用量計費的項目與付款時點；給決策者的選項要附哪些數字、各自適合什麼條件；以及每次活動之後怎麼校準換算係數、沒有歷史資料時怎麼起步。峰值的形狀與倍率階梯在 <a href="/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算</a>，計費模式與單位請求成本在 <a href="/blog/devops/operations/05-capacity-planning/cost-model/" data-link-title="成本模型" data-link-desc="把容量規劃的資源翻成錢時，釐清 on-demand、reserved、spot 三種計費模式的取捨、怎麼算單位請求成本、以及過度與不足配置各自的經濟代價">成本模型</a>，活動前的準備時程、預先擴容的做法與值班在 <a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="可預期峰值從 T-90 到 T-0 的準備時程：容量計畫、quota 申請、壓測、game day 與 pre-scaling 各排在事件前幾天、排在那裡的理由與項目之間的依賴，pre-scaling 的做法、watch room、event tier 分級與準備時間不夠時被壓縮的部分，以及事後 retro">9.11 高峰事件準備</a>，這裡不重寫。</p>
<h2 id="決策者與會計各自要的數字規格與風險金額期間與付款時點">決策者與會計各自要的數字：規格與風險、金額期間與付款時點</h2>
<p>事先知道的大流量，在活動之前通常會被問兩個問題，兩個問題要的數字不同：</p>
<ul>
<li><strong>決策者問要加多大</strong>：要的是規格與風險，也就是加到多少台、多大的資料庫，以及不加或少加時會發生什麼（回應變慢、排隊、下單失敗的比例），才能在幾個選項之間選。</li>
<li><strong>會計問要多少預算</strong>：要的是金額、期間與付款時點，也就是費用落在哪幾個月、哪一筆是一次性的、哪一筆是每月經常性的、什麼時候從信用卡或帳戶扣走，才能編進預算、確認付得出來。</li>
</ul>
<p>規格與預算各估一次時，決策者核准的規格與會計核准的金額會對不上：活動前一週才發現預算買不到核准的規格，或預算編了、規格卻不是照那個預算規劃的。同一份業務預估推出來的兩份數字，改一個假設（例如預估人數加倍）時會一起變，也一起被看到，所以兩份數字從同一份預估推。預算先定下來的情形，同一條換算鏈倒過來算：從預算推出買得到的台數，再推出能服務的峰值，超出的部分交給下面〈給決策者的選項〉裡的降級或等候室。</p>
<h2 id="從業務預估到台數換算鏈換算係數的來源與三點估計">從業務預估到台數：換算鏈、換算係數的來源與三點估計</h2>
<p>業務預估是活動負責人給得出來的數字：預計多少人來、集中在哪個時段。台數是工程要準備的數字：每一層要開幾台。兩者之間是一條換算鏈，前三步各用一個換算係數乘或除，第四步把台數對到機型與規格，上一步的輸出是下一步的輸入：</p>
<ol>
<li><strong>業務預估 → 峰值時段的使用者數</strong>：預計來的人數，乘上集中在峰值時段的比例（下文稱<strong>集中比例</strong>）。預計人數要包含平常在同一時段就會來的訪客；不包含時，把平常同一時段的請求量加到第二步的結果上。集中比例依活動的形式差很多（限量搶購集中在開賣的頭幾分鐘、全館折扣分散在整天），要從同類活動的歷史量；峰值的形狀在 <a href="/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算</a>。</li>
<li><strong>使用者數 → 到源站的峰值請求量</strong>：乘上每個使用者在峰值時段發出的請求數（下文稱<strong>每人請求數</strong>），再乘上到源站的比例（1 減去 CDN 的命中率），再除以峰值時段的秒數，得到源站每秒要處理的請求數。每人請求數從平常的流量模型量（<a href="/blog/devops/operations/05-capacity-planning/traffic-model/" data-link-title="流量模型建立" data-link-desc="要把「這個服務會收到什麼樣的流量」量化成容量規劃的輸入時，釐清該量哪幾個維度、峰均比為什麼是最關鍵的單一指標、以及怎麼從 production log 抽出可信的模型">流量模型建立</a>）；活動期間的行為可能跟平常不同，例如延遲上升時使用者重新整理、客戶端重試，每人請求數會跟著變大。</li>
<li><strong>峰值請求量 → 各層的台數</strong>：除以一台在目標利用率下的每秒處理量，結果無條件進位成整數台。一台的處理量從壓力測試量（<a href="/blog/devops/operations/05-capacity-planning/load-testing-tools/" data-link-title="壓力測試工具與方法" data-link-desc="要用壓測驗證容量規劃時，釐清壓測到底在找什麼、k6 與 wrk 各自的定位、怎麼讀壓測輸出的延遲分布，以及讓結果失真的幾個反模式">壓力測試工具與方法</a>）；目標利用率指膝點處理量的比例（不是 CPU 使用率），設在膝點以下，留出的那段餘裕吸收秒級的波動與故障（<a href="/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算</a>〈安全係數的數學根據在飽和曲線〉）。故障的單位常是一個可用區：部署在 N 個可用區、要承受掉一個時，剩下的主機會吃到原本 N ÷（N－1）倍的量，目標利用率要再壓低到這個比例以下。目標利用率每一層不同：資料庫這類有狀態、有鎖競爭的元件膝點來得早，它的目標利用率要取得比無狀態的應用伺服器低（各類元件留多少的討論在 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>〈Headroom budget 設計〉，概念見 <a href="/blog/backend/knowledge-cards/headroom-budget/" data-link-title="Headroom Budget" data-link-desc="說明容量規劃中為應付異常 burst &#43; AZ 故障 &#43; forecast 誤差的安全餘量">Headroom Budget</a> 卡）。資料庫、快取、第三方 API 這些不一定能水平加台的層，要再乘上每個請求觸發幾次查詢或呼叫，並對照它們的上限；從延遲目標往下反推各層的方法在〈峰值估算〉的〈從業務指標反推各層峰值〉。</li>
<li><strong>台數 → 規格</strong>：台數、機型、資料庫規格、頻寬。</li>
</ol>
<p>預估本身的誤差，這篇從目標利用率拆出來另外處理。〈峰值估算〉與 9.6 把預估誤差也算進膝點以下的那段餘裕（9.6 的〈Headroom budget 設計〉把預測的誤差列為 headroom 要涵蓋的第一項）；這篇改用下面的三點估計把它帶著走，所以目標利用率留下的餘裕只涵蓋秒級的波動與故障，用 9.6 的比例時要先扣掉預測誤差那一份，不重複算兩次。</p>
<p>換算鏈裡誤差最大的是集中比例與每人請求數：每台處理量是壓測量的，相對可靠；集中比例與每人請求數在沒辦過同類活動時是估的。這兩個係數各取三個值，保守（偏高）、預期、樂觀（偏低），高的配高的、低的配低的，整條鏈跑三次，得到峰值請求量的<strong>三點估計</strong>（<a href="/blog/backend/knowledge-cards/peak-forecast/" data-link-title="Peak Forecast" data-link-desc="說明預期峰值流量的預測方法 — 容量規劃的第一個輸入">Peak Forecast</a> 卡寫的最壞、預期、最好三點）；活動負責人對預計人數也沒把握時，人數一樣取三個值配進去。保守那一點是每個係數同時取偏高值的組合，不是機率上的某個分位數：兩個係數彼此獨立時，同時偏高的機率比各自偏高低，保守點會偏寬；兩者正相關時（越集中的活動，使用者越會重新整理）就不會。搶購機器人、重試風暴與預料之外的爆紅會超過保守點，這要靠下面選項裡的回退方式處理。三點離預期要拉多寬，看同類活動的歷史：有幾次的歷史時，取歷史上實際值與預估值的偏差範圍；只有一兩次、或沒有歷史時，一次活動只是一個樣本，偏差範圍不可靠，要取得更寬（下面〈每次活動之後的校準，以及沒有歷史資料時的起步〉）。</p>
<p>示例（數字都是為了說明換算而設的）：預計 20 萬人來（已含平常同一時段的訪客），峰值時段是開賣的頭 10 分鐘（600 秒），不放 CDN、請求全部到源站。集中比例取 20%、30%、40%，每人請求數取 20、30、40。壓測量到一台應用伺服器在延遲開始上升時（膝點）約 420 請求/秒，目標利用率取 60%，約 252，取整成 250。</p>
<table>
  <thead>
      <tr>
          <th>估計</th>
          <th>峰值請求量（每秒）</th>
          <th>應用伺服器（每台 250 請求/秒）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>樂觀</td>
          <td>200,000 × 0.2 × 20 ÷ 600 ≈ 1,333</td>
          <td>6 台</td>
      </tr>
      <tr>
          <td>預期</td>
          <td>200,000 × 0.3 × 30 ÷ 600 ＝ 3,000</td>
          <td>12 台</td>
      </tr>
      <tr>
          <td>保守</td>
          <td>200,000 × 0.4 × 40 ÷ 600 ≈ 5,333</td>
          <td>22 台</td>
      </tr>
  </tbody>
</table>
<p>資料庫假設每個請求平均觸發 1 次查詢，壓測時 4,000 次/秒延遲開始上升，目標利用率取比應用層低的 50%，可以處理 2,000 次/秒，預期的 3,000 已經超過。實際要先從流量模型拆出讀與寫各佔多少：讀的部分用<a href="/blog/backend/knowledge-cards/read-write-split/" data-link-title="Read-Write Split" data-link-desc="說明讀寫流量如何分流到 primary 與 replica，以及它引入的一致性責任">讀取副本</a>或快取吸收，寫的部分要靠佇列非同步處理（<a href="/blog/devops/operations/07-burst-traffic/queue-buffering/" data-link-title="Queue 緩衝" data-link-desc="在 ingestion 和 processing 之間加 message queue 做 burst 緩衝 — Kafka / NATS / Redis Streams 的選型和引入條件">Queue 緩衝</a>）或更大的主資料庫。這次的瓶頸在資料庫，所以下面各個選項都要寫出資料庫那一層怎麼處理。</p>
<p>這條鏈的價值在每一步都看得到換算係數從哪裡來。決策者問「為什麼是 22 台」時，答案是一串可以逐項檢查的乘除，而且看得到保守那一點是哪幾個係數取了偏高的值。</p>
<h2 id="從台數到預算計費時數計費模式跟著用量計費的項目與付款時點">從台數到預算：計費時數、計費模式、跟著用量計費的項目與付款時點</h2>
<p>台數換成金額要決定幾件事：加開的資源計費多少小時、單價用哪一種計費模式，以及不跟著台數走的費用。給會計的是<strong>增量</strong>，也就是活動期間比平常多出來的費用，平常的費用已經在每月的預算裡：</p>
<ul>
<li><strong>計費時數</strong>：加開的資源從加開那一刻開始計費，到縮回為止。活動開始之前要提前多久加開（下文稱<strong>預先擴容</strong>，pre-scaling），有兩種起點：只依預熱（快取與連線先熱起來，快取的部分見 <a href="/blog/backend/knowledge-cards/cache-warmup/" data-link-title="Cache Warmup" data-link-desc="說明服務啟動或活動前如何預先建立快取資料">Cache Warmup</a>；<a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="可預期峰值從 T-90 到 T-0 的準備時程：容量計畫、quota 申請、壓測、game day 與 pre-scaling 各排在事件前幾天、排在那裡的理由與項目之間的依賴，pre-scaling 的做法、watch room、event tier 分級與準備時間不夠時被壓縮的部分，以及事後 retro">9.11 高峰事件準備</a>〈Pre-scaling 策略〉寫的是 30 分鐘到 2 小時）與加開之後驗證規格的時間決定時，是活動前幾小時；照 9.11〈T-90 → T-0 準備時程〉在活動前兩天開始預先擴容時，計費時數從兩天前起算，換到的是更早發現開不出機器或規格不對、還有時間處理。配額申請、正式的壓測這些前置工作要提早好幾週排，它們不算在加開資源的計費時數裡。活動結束之後也不會當場縮回：要確認每秒請求數回到平常的水準、並維持一段時間；流量有日週期，觀察到隔天同一時段，才看得到是否有回流的使用者，所以示例取一個完整的日週期。增量費用＝加開的台數 × 單價 × 計費時數，加開的台數＝所需台數－平常的台數，所需台數不超過平常的台數時不加開。</li>
<li><strong>預先擴容還是交給自動擴縮</strong>：峰值爬升得比自動擴縮的反應時間（指標取樣、開機、通過健康檢查，通常要幾分鐘，推論，各家沒有逐一查）還快時，例如開賣頭幾分鐘就爆量，容量要預先加開到峰值，自動擴縮只能吸收活動中段的變化。預先加開可以手動調高台數，也可以用排程擴縮（<a href="/blog/backend/knowledge-cards/scheduled-scaling/" data-link-title="Scheduled Scaling" data-link-desc="說明按已知時間表預先擴容的 autoscaler 模式">Scheduled Scaling</a>）在指定的時間把自動擴縮的最小台數調高、活動結束後再調回，計費時數照排程的起訖時間算。預先加開時主機費用是固定的；交給自動擴縮時，主機費用是各時段的加開台數 × 單價 × 時數加總起來，上限是全程開滿保守點的台數，下限取決於擴縮的最小台數與預熱期間開了幾台。</li>
<li><strong>計費模式</strong>：活動只有幾天，加開的部分照隨用隨付（on-demand）計。折扣型的預留（reserved instance、savings plan、承諾使用折扣 committed use discount）是一年起跳的承諾，只適合平常就用得到的基準負載（<a href="/blog/devops/operations/05-capacity-planning/cost-model/" data-link-title="成本模型" data-link-desc="把容量規劃的資源翻成錢時，釐清 on-demand、reserved、spot 三種計費模式的取捨、怎麼算單位請求成本、以及過度與不足配置各自的經濟代價">成本模型</a>〈三種計費模式的最佳組合〉；savings plan 這類承諾模式在 <a href="/blog/devops/operations/08-cost-management/billing-models/" data-link-title="計費模式理解" data-link-desc="看懂雲端帳單怎麼生成時，先認清收費的維度、on-demand/reserved/savings plan/spot 的承諾差異、以及 egress 這類最常被忽略的隱藏成本">計費模式理解</a>〈承諾模式：用彈性換折扣〉）；為了一次活動買它，活動之後用不滿的部分照樣每月付。可被收回的執行個體（spot）單價低，但峰值正是那種機型最搶手、最可能被收回的時候，被收回時正好補不回來，所以不用在活動的服務容量上；它適合用在活動前產生壓測流量。</li>
<li><strong>容量預留</strong>：它買的是「當天開得出機器」的保證，不是折扣，跟折扣型的預留是兩回事。預留期間不論有沒有用都照單價計費，所以它是計費時數的另一個起點：從建立預留那一刻算起，不是從加開那一刻。活動當天在那個可用區可能開不出機器時才需要它。例如 AWS 立即生效的容量預留（On-Demand Capacity Reservations）沒有承諾期、可以隨時取消，沒有折扣，算進隨用隨付執行個體的配額；未來日期生效的那一種有承諾期，取消可能收費（<a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-capacity-reservations.html">Reserve compute capacity with EC2 On-Demand Capacity Reservations</a>，2026-10-27 讀）。各家預留容量的欄位名稱見 <a href="/blog/devops/infra/cloud-providers/vm-options/shape-and-image/" data-link-title="機型與映像檔：CPU 架構、核心與記憶體、計費模式與作業系統" data-link-desc="雲端主機的機型決定的 CPU 架構、核心與記憶體，計費模式（隨用隨付、可被收回、預留容量、專屬主機）各自的條件，映像檔與機型之間的架構相容性，以及哪些項目建立後可以調整">機型與映像檔：CPU 架構、核心與記憶體、計費模式與作業系統</a>〈各家的欄位名稱：機型、可被收回、預留容量與映像檔〉。</li>
<li><strong>資料庫那一層</strong>：加讀取副本照同一條式子算（加開的副本數 × 單價 × 計費時數）。垂直升級主資料庫不一樣：有些業者改規格要停機，升級與降回都要排在可以停機的時段，降回常常排在活動之後很久，計費時數從升級算到降回；儲存空間加上去之後通常縮不回來（例如 Amazon RDS 的文件寫「You can&rsquo;t deallocate space」，<a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_PIOPS.ModifyingExisting.html">Increasing DB instance storage capacity</a>，2026-10-27 讀），要算成之後每月的經常性費用，給會計時跟一次性的費用分開列。</li>
<li><strong>跟著用量計費的項目</strong>：出站流量＝活動期間的總請求數 × 平均回應大小 × 每 GB 單價，放 CDN 時分成 CDN 回應的那一段（照 CDN 的計價）與回源的那一段（依業者可能另外計費，推論，各家沒有逐一查）。它跟主機費用誰大取決於回應大小：平均回應到 MB 級（圖片、影片多的頁面）時，出站流量可能比主機費用還高。按請求計價的託管服務與 serverless（函式、API 閘道、按請求計費的資料庫）沒有台數，費用＝請求數 × 單價，三點估計直接影響的是金額；它們另有自己的爬升限制與帳號配額，要先查。另外還有按次計費的第三方 API（簡訊、金流、地圖）、負載平衡器依流量與連線數的計費、跨可用區的流量，以及私有子網路經由 NAT 閘道呼叫外部服務的處理費（這三項依業者，推論，單價沒有查），還有記錄與監控的資料量（流量變大時 log 也跟著變大）。</li>
<li><strong>活動本身的準備費用</strong>：壓測本身也要開資源與產生流量，這筆費用在活動之前就發生。值班與待命的人力＝人數 × 時數 × 費率，時數含預先擴容與活動之後的觀察期；有夜間或假日值班時，這一項常常比雲端費用高。</li>
</ul>
<p>示例（接換算鏈示例的數字，單價、平均回應大小與預熱驗證的時數都是為了說明而設的）：平常開 4 台應用伺服器，每台每小時 0.2 美元；提前 6 小時加開（預熱 2 小時加上驗證 4 小時）、活動 24 小時、觀察 24 小時，計費時數 54 小時；出站流量每 GB 0.08 美元，平均回應 50 KB。</p>
<table>
  <thead>
      <tr>
          <th>估計</th>
          <th>加開台數</th>
          <th>主機增量（54 小時）</th>
          <th>出站流量（當天總量）</th>
          <th>出站流量費用（當天總量）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>樂觀</td>
          <td>2</td>
          <td>2 × 0.2 × 54 ≈ 22 美元</td>
          <td>20 萬人 × 20 × 50 KB ＝ 200 GB</td>
          <td>16 美元</td>
      </tr>
      <tr>
          <td>預期</td>
          <td>8</td>
          <td>8 × 0.2 × 54 ≈ 86 美元</td>
          <td>20 萬人 × 30 × 50 KB ＝ 300 GB</td>
          <td>24 美元</td>
      </tr>
      <tr>
          <td>保守</td>
          <td>18</td>
          <td>18 × 0.2 × 54 ≈ 194 美元</td>
          <td>20 萬人 × 40 × 50 KB ＝ 400 GB</td>
          <td>32 美元</td>
      </tr>
  </tbody>
</table>
<p>出站流量那兩欄用的是全天每人請求數：示例借用峰值時段的 20、30、40，等於假設全天的 20 萬人每人都發出那麼多請求（不論是不是在峰值時段來的）；全天每人請求數本身是另一個要量的係數。這兩欄是活動當天的總量，給會計的增量要扣掉平常一天的出站流量。平均回應大小從存取紀錄（access log）的回應位元組數或 CDN 的報表量；出站流量的單價與每月的免費量查業者價目表的出站（data transfer out）那一項。改成在活動前兩天開始預先擴容時，計費時數變成 48 ＋ 24 ＋ 24 ＝ 96 小時，比示例多 42 小時，保守那一列的主機增量變成 18 × 0.2 × 96 ≈ 346 美元。</p>
<p>給會計的預算是選定選項（下一節）之後的數字：主機費用依選定的做法取固定值（預先加開）或區間（交給自動擴縮），加上資料庫那一層、跟著用量走的項目的區間，並寫出區間從哪幾個係數來。預算編在哪一點有兩種做法：編在保守那一點，活動中不必再核准，代價是編多了的部分要在活動之後退回或留用；編在預期那一點，並寫明超過時由誰核准追加，代價是超出時要有人在活動中當天核准。追加預算的流程跟不上活動時，編在預期那一點的代價是峰值超出時沒有人能及時核准，服務只能停在預算內的規格，或在沒有核准的狀態下超支。</p>
<p>金額還要對齊三件事。一是付款時點：隨用隨付的帳戶多半是月結後付，下個月初扣上個月的費用，預算的月份與扣款的月份差一個月，跨月的活動分在兩期扣；Google Cloud 的門檻扣款帳戶例外，活動中費用暴增、餘額達到付款門檻時就會扣款。二是幣別與稅：雲端多半以美元計價，預算以本地幣別編，扣款金額還會受匯率、稅金與信用卡海外交易手續費影響，給會計的數字要寫出匯率的假設與是否含稅。三是臨時加開資源對停損機制的影響：加開資源要先調高配額，最大花費速度跟著變大，停止之前的最大超支、觸發處置的門檻與信用卡額度都要依新的值重算，否則活動當天正常的流量就會觸發停損（花費超過門檻時，由程式或人停止資源）。這三件事的推算與各家的出處在 <a href="/blog/devops/operations/08-cost-management/spend-planning-before-renting/" data-link-title="租用雲端主機之前的費用規劃：費用訊號的延遲、停止之前的最大超支、扣款的信用卡額度、配額與預算門檻的計算，以及停損停到哪一層" data-link-desc="租用雲端主機或把試用帳號升級成付費帳戶之前，費用失控時從觸發到停止之間的金額怎麼從配額與訊號延遲推出來，再用它推扣款的信用卡額度、配額、預算警示與觸發處置的門檻；Oracle Cloud、Google Cloud 與 AWS 的扣款時點、預先授權、扣款失敗的後果、擋下建立與停止用量的機制各自涵蓋什麼，以及停損停到哪一層的判斷">租用雲端主機之前的費用規劃：費用訊號的延遲、停止之前的最大超支、扣款的信用卡額度、配額與預算門檻的計算，以及停損停到哪一層</a>。</p>
<h2 id="給決策者的選項規格費用區間不足時的後果回退方式與適合的條件">給決策者的選項：規格、費用區間、不足時的後果、回退方式與適合的條件</h2>
<p>決策者要的是選擇，所以給的是幾個選項，每個選項附同一組數字：規格（含瓶頸那一層）、費用區間、容量不足時會發生什麼、活動中發現不夠時能怎麼補，以及這個選項適合什麼條件。「不足時的後果」分兩個門檻寫成可以觀察的結果：峰值超過目標利用率下的處理量時，秒級的尖峰開始讓延遲超過目標；超過膝點時，延遲與逾時明顯上升，逾時的比例用壓測量。以上面的示例（各選項的資料庫處置費用記為 &lt;資料庫處置的增量&gt;，出站流量 16 到 32 美元的當天總量各選項都一樣）：</p>
<ul>
<li><strong>依保守那一點預先加開（應用伺服器 22 台，主機增量約 194 美元，加上資料庫處理到每秒 5,333 次查詢的費用）</strong>：兩個門檻是每秒 5,500 與約 9,200（22 × 420），峰值落在保守點以內時不必降級。預估準確時多出的容量沒用到，多付的是保守與預期之間的那一段。前提是配額已經核准到 22 台，資料庫那一層也在活動前處理好。適合下單失敗的營收損失與商譽代價遠高於多付的雲端費用時。</li>
<li><strong>依預期那一點加開，超出的流量用降級處理（12 台，約 86 美元，加上資料庫處理到每秒 3,000 次的費用）</strong>：兩個門檻是每秒 3,000 與約 5,040（12 × 420）。峰值超過 3,000 時，關掉推薦、延後寄信，把核心交易維持在延遲目標內；觸發點設在目標利用率對應的處理量、不設在膝點，是讓降級在延遲開始上升之前就生效，目標利用率到膝點之間的那段餘裕除了吸收秒級的波動與故障，也留給降級開關生效的時間。峰值到保守點的 5,333 時，降級至少要卸掉約 6% 的請求才不會超過膝點，要卸掉約 44% 才回到觸發點；降級能卸掉多少，要在活動前量。雲端費用較低，但降級本身也有成本：用到被關掉功能的使用者看不到推薦、晚收到信，流失的轉換與客訴要算進去（<a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a>〈降級的成本邊界〉）。降級要先決定犧牲哪些功能、保住哪些（請求依優先等級卸除的做法在 <a href="/blog/backend/09-performance-capacity/unplanned-traffic-surge/" data-link-title="9.15 無預警瞬時大流量：流量來源辨識、擴展緩衝、請求優先等級、准入控制、體積型攻擊與退路" data-link-desc="沒有準備窗口的瞬時大流量（突發流量、流量暴增）怎麼承接：依流量形狀分辨真人需求、應用層的濫用流量、體積型攻擊（DDoS），自動擴展的反應時間換算成緩衝，入口標記請求優先等級與負載卸除的順序，等候室與事前註冊這類准入控制，把體積型攻擊交給更大的邊緣網路，以及擴展跟不上時的退路">9.15 無預警瞬時大流量：流量來源辨識、擴展緩衝、請求優先等級、准入控制、體積型攻擊與退路</a>〈負載卸除與限流的分工、請求的優先等級〉），並在活動前演練過（9.11 的〈Game day 演練〉）：沒有事先定好順序的系統，超載時所有功能會一起逾時或失敗。適合核心交易與周邊功能能拆開、降級演練過的時候。</li>
<li><strong>不加，超過容量的使用者進等候室（維持 4 台，主機增量 0，加上等候室的費用）</strong>：兩個門檻是每秒 1,000 與約 1,680（4 × 420），等候室依每秒 1,000 的容量分批放行（<a href="/blog/backend/09-performance-capacity/unplanned-traffic-surge/" data-link-title="9.15 無預警瞬時大流量：流量來源辨識、擴展緩衝、請求優先等級、准入控制、體積型攻擊與退路" data-link-desc="沒有準備窗口的瞬時大流量（突發流量、流量暴增）怎麼承接：依流量形狀分辨真人需求、應用層的濫用流量、體積型攻擊（DDoS），自動擴展的反應時間換算成緩衝，入口標記請求優先等級與負載卸除的順序，等候室與事前註冊這類准入控制，把體積型攻擊交給更大的邊緣網路，以及擴展跟不上時的退路">9.15 無預警瞬時大流量：流量來源辨識、擴展緩衝、請求優先等級、准入控制、體積型攻擊與退路</a>〈准入控制：虛擬等候室（virtual waiting room）、waitlist 與事前註冊〉）。每秒 1,000 在資料庫的 2,000 以內，資料庫那一層不必處理。等候室本身是一項服務：用現成的要付方案費（例如 Cloudflare 的 Waiting Room 只在 Business 與 Enterprise 方案提供，<a href="https://developers.cloudflare.com/waiting-room/plans/">Plans</a>，2026-10-27 讀），自建要開發與壓測的工時。適合流量的商業價值不高、活動可以分批開放，而且等候室事前已經建好、演練過的情形；代價是排隊的使用者有一部分會離開。</li>
<li><strong>改活動本身的形式，降低換算鏈的係數</strong>：事前登記、抽籤、分時段開賣可以降低集中比例，把落地頁做成 CDN 可快取的靜態頁可以降低到源站的比例。改的是業務端，費用多半比加機器低，代價是活動的形式要跟活動負責人談，有時會降低活動的效果。它可以跟前三個選項並用，先降係數再重跑一次換算鏈。</li>
</ul>
<p>「回退方式」寫活動中發現不夠時要多久補得上：自動擴縮還有多少空間、手動加台要多久、配額還能不能再調；配額申請被拒或來不及核准時，加開的台數停在已核准的配額，依保守那一點加開的選項到不了；已核准的台數低於預期點時，依預期加開也到不了，超出的部分交給降級（能卸掉多少要在活動前量）、等候室與改活動形式。也要寫活動取消或延期時哪些費用已經發生：預先擴容與容量預留從開始那一刻計費，壓測與預付的第三方點數（例如簡訊）多半拿不回來。</p>
<p>決策者比較的是多付的費用，對上不足時的後果發生的機率與代價。不足時的代價可以估成：失敗或放棄的請求數 × 轉換率 × 客單價，再加上客訴與流失的使用者；比法在 <a href="/blog/devops/operations/05-capacity-planning/cost-model/" data-link-title="成本模型" data-link-desc="把容量規劃的資源翻成錢時，釐清 on-demand、reserved、spot 三種計費模式的取捨、怎麼算單位請求成本、以及過度與不足配置各自的經濟代價">成本模型</a>〈過度與不足配置的經濟學〉。比期望值（代價乘上發生的機率）適合活動常辦、單次失敗可以承受的情形；比最壞情況適合一次失敗就會傷到商譽或違反合約、法規的情形。示例裡雲端費用只有數百美元，決策的金額多半落在人力與營收損失這兩項。</p>
<h2 id="每次活動之後的校準以及沒有歷史資料時的起步">每次活動之後的校準，以及沒有歷史資料時的起步</h2>
<p>換算鏈的準度來自校準：每次活動之後，把每一個換算係數的實際值記下來，跟事前的三點估計對照。〈峰值估算〉的〈用歷史倍率階梯推可預期峰值〉寫的是倍率與 headroom 的校準，9.11 的〈事後 retro〉寫的是活動後第七天回顧的時程；這裡加上的是換算鏈自己的係數：</p>
<ul>
<li><strong>集中比例</strong>：實際來了多少人、峰值時段佔了多少。</li>
<li><strong>每人請求數</strong>：峰值時段實際的請求量除以峰值時段實際的使用者數。系統飽和或有等候室限流的時段，伺服器收到的是被服務的量，不是需求量，要用需求端的訊號還原（CDN 邊緣的請求數、等候室的排隊人數、被拒絕或逾時的次數）；因為延遲上升而重試、重新整理的請求要分開記，下次估算時才分得出需求本身與系統變慢造成的放大。</li>
<li><strong>全天每人請求數</strong>：出站流量用的那一個，跟峰值時段的分開量。</li>
<li><strong>每台處理量</strong>：活動中各層的實際利用率與延遲，對照壓測的數字；差很多時，代表壓測的情境跟真實的使用行為不一樣，下次壓測的情境要改。</li>
<li><strong>單位請求成本</strong>：活動期間的實際增量費用除以請求量，拆成主機、資料庫、流量、第三方 API 各一項（單位請求成本的算法在 <a href="/blog/devops/operations/05-capacity-planning/cost-model/" data-link-title="成本模型" data-link-desc="把容量規劃的資源翻成錢時，釐清 on-demand、reserved、spot 三種計費模式的取捨、怎麼算單位請求成本、以及過度與不足配置各自的經濟代價">成本模型</a>〈單位請求成本讓成本可歸因〉，費用怎麼歸因到各項在 <a href="/blog/devops/operations/08-cost-management/cost-monitoring/" data-link-title="成本監控與告警" data-link-desc="要讓雲端帳單不失控時，靠歸因把每筆花費追到擁有者、用異常告警抓突增、以及選 showback 或 chargeback 讓成本有人負責">成本監控與告警</a>〈歸因是監控的地基〉）。帳務明細落後數小時到兩天，這一項要等活動之後幾天、帳務資料補齊再算，跟 9.11 的第七天回顧可以排在一起。</li>
</ul>
<p>記下來的係數就是下一次活動換算鏈的輸入，也是平常評估「如果流量變成兩倍要多少錢」的依據。平常在單價或計費方式有變動時（業者調價、換機型世代、改用承諾方案），或至少在每次編下一期預算之前，用實際帳單重算一次單位請求成本；業務提出下一次活動的規模時，換算鏈直接拿最新的係數給出第一版的規格與預算。</p>
<p>沒有辦過同類活動、沒有歷史資料時，換算鏈裡最不確定的是集中比例與每人請求數。起步的做法有幾種，各自換到的依據與付出的代價不同：</p>
<ul>
<li><strong>拿相近的活動、公開案例或行銷端的數字當起點</strong>：換到一個有依據的初始值，代價是兩個活動的使用者行為不一定相同。拿來當起點的案例要先確認是同一類活動：限量搶購屬於開賣即爆、幾十分鐘結束的瞬間爆量，年度檔期屬於提前數月已知的可預期峰值，兩者的集中比例差很多（<a href="/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算</a> 開頭的五種峰值形狀）。行銷端的漏斗數字也可以當依據，例如推播的發送數乘上開啟率，推播送出之後的幾分鐘是集中最高的時段。</li>
<li><strong>先辦一次小規模的試辦</strong>（限量開賣、只對一部分使用者開放）：量到自己的集中比例與每人請求數，代價是要多辦一次、試辦本身的規模也要估。</li>
<li><strong>把保守點的係數取得更偏高（三點的區間跟著變寬），並確保能在活動中補容量</strong>：自動擴縮與配額留有空間，活動中有人盯著，代價是規格與預算的區間變大。</li>
<li><strong>壓測直接量「每台每秒能完成幾趟使用者旅程」</strong>：把瀏覽、加入購物車、結帳這一串請求當一個單位壓測，省掉每人請求數這個係數，代價是旅程的組成要先定義得夠接近真實。</li>
</ul>
<p>這幾種常常一起用：用相近的案例或行銷端的數字估初始值，用試辦校正，剩下的誤差交給三點估計的保守那一點與活動中的補容量。</p>
]]></content:encoded></item></channel></rss>