<?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>System-Design on Tarragon</title><link>https://tarrragon.github.io/blog/tags/system-design/</link><description>Recent content in System-Design on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Thu, 15 Oct 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/system-design/index.xml" rel="self" type="application/rss+xml"/><item><title>9.0 請求路徑上的負載分析：一次一筆的處理、在它之前能回應的請求、多出來的請求怎麼處置、保護把負載轉到哪裡、重送與去重，以及保護元件與依賴故障時的處置</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/load-path-analysis/</link><pubDate>Thu, 15 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/load-path-analysis/</guid><description>&lt;p>這篇整理沿著一個會改變狀態的請求（購票、下單、委託、建立短網址）從用戶端走到資料庫的路徑，分析負載時共用的概念。讀取請求（短網址的轉址、商品頁、座位圖上的剩餘數量）本身的快取設計不在本篇的範圍內，只有保護為了寫入路徑而把讀取移到邊緣的那一段（座位圖的剩餘數量由 CDN 合併回源）在範圍內。概念以 &lt;a href="https://tarrragon.github.io/blog/backend/case-systems/" data-link-title="Backend 範例系統：短網址、售票搶購、電商 checkout 與交易撮合的功能、需求與規模假設" data-link-desc="Backend 教材幾篇以具體系統走完整條請求路徑的文章共用的四個範例系統：每個系統提供的功能與請求種類、不能讓步與可以讓步的需求、規模假設與起點架構，以及四個系統在成功請求的總數是否有上限、延遲是否改變請求的價值、同一個請求被處理兩次的後果上的差別">Backend 範例系統&lt;/a> 的短網址、售票搶購、電商 checkout 與交易撮合舉例；完整走過一條路徑的案例是 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/ticketing-load-path/" data-link-title="9.0.1 售票搶購的負載路徑：熱點列與連線池、CDN 合併回源、排隊放行的速率、資料庫認領與加入 Redis 閘門的條件、付款期限的條件更新" data-link-desc="以售票搶購為例，沿著一張票從座位圖到付款記帳的路徑，寫出全部請求更新同一個庫存計數時熱點列與連線池的耗盡順序，邊緣快取、排隊放行、資料庫逐張認領與加入 Redis 扣庫存各自把負載轉給了哪一段、自己出錯時由哪一段補救，以及成功請求的總數有上限、延遲不改變請求的價值、先到先得這幾個前提讓哪些做法可以簡化、哪些需求無法簡化">9.0.1 售票搶購的負載路徑&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/matching-engine-load-path/" data-link-title="9.0.2 交易撮合的負載路徑：閘道的帳戶限流與明確拒絕、依序日誌的複寫延遲、單一寫入者的撮合引擎、行情的合併送出與重放驗證容量" data-link-desc="以交易撮合為例，沿著一筆委託從入口閘道到成交回報的路徑，寫出撮合之前結果無法確定、處理順序決定成交、延遲改變請求的價值這些性質讓哪些負載保護不能用，閘道、依序日誌、單一寫入者的撮合引擎與行情發布各自先耗盡的資源與出錯時的補救，過載時明確拒絕超量委託與以重放驗證容量的做法，並與售票搶購對照">9.0.2 交易撮合的負載路徑&lt;/a>。負載超過容量之後服務呈現哪一種結果（變慢、請求失敗、無回應、程序終止、停在失效），在 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/overload-outcomes/" data-link-title="9.16 負載超過容量時服務的表現：變慢、請求失敗、無回應、程序終止，尖峰過去或應對切換之後仍持續的停在失效、擺盪與第二波湧入，以及應對選出的降級回應，各自的判斷標準、成因、應對與看得到它的監控層" data-link-desc="負載超過容量（過載）時，服務呈現的結果分成變慢、請求失敗（抵達後被拒與根本沒有抵達）、無回應、程序終止，以及尖峰過去、依賴恢復或應對切換之後仍持續的停在失效、擺盪與第二波湧入；每一種的判斷標準、通常的成因與應對，哪一種先出現由先耗盡的資源與等待是否設有上限決定，影響範圍怎麼隨時間擴大，服務端監控的各層各自看得到哪一種，降級回應怎麼計數，過載在資料上造成的重複寫入、接受後遺失與讀到舊資料，以及描述一次過載要寫出的項目">9.16 負載超過容量時服務的表現&lt;/a>。&lt;/p>
&lt;h2 id="一次一筆的處理與它的互斥範圍">一次一筆的處理與它的互斥範圍&lt;/h2>
&lt;p>一個服務的需求裡，通常有一件事必須一次只處理一筆（業界常稱為 serialization point）：同一張票不能同時賣給兩個人，同一筆訂單不能同時被付款與取消，同一個交易對的委託要依單一順序撮合，同一個短代碼不能同時分配給兩個長網址。這件事要靠鎖、單一執行緒或條件更新依序處理，所以不論前面有多少台伺服器，抵達這件處理的請求都要逐一通過，負載最後集中在這件處理上。&lt;/p>
&lt;p>決定有多少請求要依序等候的，是&lt;strong>互斥範圍&lt;/strong>：哪些請求彼此必須輪流處理。互斥範圍有三個來源：需求、資料的表示方式，以及把多個範圍放進同一個依序處理的執行者。&lt;/p>
&lt;p>售票的需求要互斥的只是同一張票；全場庫存若寫成一個計數欄，所有購票請求都更新資料表的同一列，整場的請求依序等候同一把鎖，這種情形叫熱點列（hot row）。改成每張票一列，而且認領的寫法讓每個請求各自挑到不同的列（例如跳過別的交易已經鎖住的列，寫法在 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/ticketing-load-path/" data-link-title="9.0.1 售票搶購的負載路徑：熱點列與連線池、CDN 合併回源、排隊放行的速率、資料庫認領與加入 Redis 閘門的條件、付款期限的條件更新" data-link-desc="以售票搶購為例，沿著一張票從座位圖到付款記帳的路徑，寫出全部請求更新同一個庫存計數時熱點列與連線池的耗盡順序，邊緣快取、排隊放行、資料庫逐張認領與加入 Redis 扣庫存各自把負載轉給了哪一段、自己出錯時由哪一段補救，以及成功請求的總數有上限、延遲不改變請求的價值、先到先得這幾個前提讓哪些做法可以簡化、哪些需求無法簡化">9.0.1 售票搶購的負載路徑&lt;/a> 的〈扣庫存：資料庫逐張認領與加入 Redis 閘門的條件〉），只有搶同一張票（同一個座位）的請求才需要輪流；認領時若每個請求都挑「第一張還沒賣出的票」，所有請求仍會撞在同一列上。表示方式造成的互斥範圍，在實作上表現為鎖的粒度（lock granularity），可以靠改寫法縮小。熱點列的排隊機制與分散寫入的做法在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/high-frequency-counting/" data-link-title="1.18 高頻計數的寫入與彙總：熱點列的鎖、只新增的事件、可以重跑的彙總與重送的事件" data-link-desc="熱點列更新的吞吐量上限與它的成因、拆成多列／只新增事件／先在記憶體裡合併的差別、只新增事件搬去哪裡的成本、累加與重算在什麼條件下漏算或重複計算、讓彙總排程互斥的建議鎖與連線池的交互，以及重送事件的冪等寫入、推導事實的存放位置，以及 Redis 承擔即時計數、跨小時不重複數、去重與互斥時各自的邊界">1.18 高頻計數的寫入與彙總&lt;/a>。&lt;/p>
&lt;p>撮合的需求要互斥的是整本委託簿（同一個交易對上還沒成交的買賣委託），因為一筆委託是否成交取決於簿上所有其他委託的價格與時間；這是需求要求的下限，換表示方式也縮不小。撮合引擎以單一執行緒依序處理、沒有用鎖，而同一條執行緒或同一份依序日誌裡放的交易對越多，互斥範圍就越大：LMAX 把所有市場放在一條執行緒上，這時範圍是整個交易平台；依交易對分開撮合引擎，是把範圍縮回一本簿（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/single-writer-model/" data-link-title="Single Writer Model" data-link-desc="說明單寫者模型如何序列化寫入，並成為系統的容量邊界">Single Writer Model&lt;/a>；兩種做法在 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/matching-engine-load-path/" data-link-title="9.0.2 交易撮合的負載路徑：閘道的帳戶限流與明確拒絕、依序日誌的複寫延遲、單一寫入者的撮合引擎、行情的合併送出與重放驗證容量" data-link-desc="以交易撮合為例，沿著一筆委託從入口閘道到成交回報的路徑，寫出撮合之前結果無法確定、處理順序決定成交、延遲改變請求的價值這些性質讓哪些負載保護不能用，閘道、依序日誌、單一寫入者的撮合引擎與行情發布各自先耗盡的資源與出錯時的補救，過載時明確拒絕超量委託與以重放驗證容量的做法，並與售票搶購對照">9.0.2 交易撮合的負載路徑&lt;/a> 的〈撮合引擎：單一寫入者與記憶體內的委託簿〉）。&lt;/p>
&lt;p>需求本身可以協商時，互斥範圍也能縮小。廣告平台允許預算小幅超支，就可以把一個廣告活動的預算預先切成多份額度、分給各台伺服器各自扣，定期同步，互斥範圍從整個活動縮到一台伺服器上的那一份，代價是超支有上限而不是零。撮合沒有這種空間：不論撮合規則是價格與時間優先，還是依比例分配，一筆委託的成交都取決於整本簿，所以在一本簿之內，撮合能做的只有讓依序處理的那一條路徑變快。&lt;/p>
&lt;h2 id="能在一次一筆的處理之前回應的請求">能在一次一筆的處理之前回應的請求&lt;/h2>
&lt;p>需要經過一次一筆處理（必須依序、不能並行處理的那件事）的請求（購票、下單、委託、建立短網址）裡，有三類在進入那件處理之前就能回應：格式、權限或帳戶狀態不符的請求（例如餘額不足）在入口就能拒絕，結果已經確定的請求可以直接告訴它結果（票賣完之後的購票請求），以及重送的請求可以回傳上一次的結果。同一段時間內，這三類請求的筆數在所有需要經過一次一筆處理的請求筆數裡所佔的比例（分母包含這三類本身），下文稱&lt;strong>可提早回應的比例&lt;/strong>；這個比例越高，前面的元件擋下的越多，一次一筆的處理承受的越少。&lt;/p>
&lt;p>提早回應的元件讀的是一次一筆的處理所持有狀態的副本（售票的剩餘數量、帳戶餘額），副本會落後。一旦確定就不再改變的結果可以安全地提早回應；會回頭的狀態（帳戶餘額、付款期限到期時被放回的票），提早回應的只是暫時的結果，而副本落後造成的錯有兩個方向，補救各不相同。副本把請求判成不行（已售完、餘額不足），可能是錯拒，所以要讓使用者能再試；副本把請求判成可以而放行，副本可能已經過時，所以通過提早檢查的請求一律要在一次一筆的處理裡再檢查一次，否則餘額會透支、票會超賣。&lt;/p>
&lt;p>可提早回應的比例由需求的性質決定。售票的成功請求總數有上限（票數），票數之外的購票請求結果在售完那一刻就確定（直到付款期限到期、有票被放回為止），所以比例高；交易撮合沒有這種上限，一筆委託的結果要撮合當下才知道，只有無效的委託能提早回應，比例低。成功請求的總數有上限，不保證比例高：拍賣的成功請求總數是一（只有一個得標者），而結標之前每一次出價都要處理、誰得標事先不知道。&lt;/p>
&lt;p>比例高的系統，負載保護的重心在一次一筆的處理之前：排隊、提早回應，把請求擋在資料庫前面。比例低時，比對同一個互斥範圍每秒到達的請求數與依序處理一個範圍的處理量。到達的請求數低於處理量時（checkout 的每一筆訂單、一般帳戶的轉帳），一次一筆的處理不是瓶頸，負載先耗盡的是別的資源（連線數、資料庫的總寫入量），見〈每一段保護把負載轉到哪裡〉。到達的請求數高於處理量、而範圍能靠改表示方式縮小時（熱門商家的收款帳戶改成只新增入帳紀錄），回到〈一次一筆的處理與它的互斥範圍〉的熱點列。範圍縮不小時（一本委託簿、結標前的一件拍品），同一個範圍的全部請求都要通過同一個依序處理的執行者，重心才落在那件處理本身：記憶體內的狀態、單一寫入者、處理路徑上沒有資料庫，以及事先配置的容量。&lt;/p>
&lt;h2 id="多出來的請求排隊拒絕或請使用者重新確認">多出來的請求：排隊、拒絕或請使用者重新確認&lt;/h2>
&lt;p>扣掉能提早回應的請求，抵達一次一筆的處理（例如扣庫存或撮合）的請求仍可能超過它的處理量，多出來的那些要決定怎麼處置，而決定的依據是&lt;strong>延遲是否改變請求的價值&lt;/strong>。售票在不劃位、固定票價時，排隊幾分鐘之後買到的仍是同一種票、同一個價格，多出來的請求可以排隊（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/admission-control/" data-link-title="Admission Control（准入控制）" data-link-desc="要決定湧入的使用者誰能進入、什麼時候進入時，例如開賣的虛擬等候室、waitlist、事前註冊與抽籤；和負載卸除、限流的分工，以及和 Kubernetes admission controller 的區分">Admission Control&lt;/a>），放行的速率設在後端實測的處理量。連續撮合的交易所，一筆委託晚一秒送進委託簿，市場已經變了，市價單以偏離預期的價格成交，排隊讓請求失去下單時的意義。所以委託到達時以佇列長度除以處理速率預估要等多久，預估超過委託延遲目標就在接受之前明確拒絕（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-shedding/" data-link-title="Load Shedding" data-link-desc="說明服務過載時如何主動拒絕低優先工作以保護核心能力">Load Shedding&lt;/a>；9.0.2 的閘道以佇列長度的上限做這件事）。&lt;/p>
&lt;p>延遲改變價值、所以要拒絕，還有一個前提：請求在處理時照送出當下的條件直接執行，不再經使用者確認。處理之前會讓使用者重新確認的請求，延遲的代價只是多一次確認：checkout 的價格在使用者排隊期間變了，下單時以新價格重算並請使用者確認，訂單照樣成立。市場的規則也會改變這個判斷：集合競價是把一段時間內的委託集中起來、以單一價格一次撮合的規則，以固定間隔做集合競價的市場，等到下一次撮合本來就是規則的一部分。&lt;/p>
&lt;p>被拒絕的用戶端會重送時，負載以重送的形式回來：用戶端是程式時往往立刻重送，重送的請求疊在原本的需求上，讓進來的請求率高於真實的需求，這是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retry-storm/" data-link-title="Retry Storm" data-link-desc="說明大量重試如何把局部故障放大成系統壓力">Retry Storm&lt;/a>。所以拒絕的回應要帶重試的時機（&lt;code>Retry-After&lt;/code> 這類欄位，或約定加上隨機延遲的退避間隔，見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/jitter/" data-link-title="Jitter" data-link-desc="說明重試或排程加入隨機偏移如何降低同步尖峰">Jitter&lt;/a>），排隊的回應要讓使用者看得到自己的位置，拒絕與排隊的回應都在讓用戶端不要用重送去確認結果。逾時就作廢的用戶端（廣告的即時競價）不會重送，拒絕本身就是結果。&lt;/p>
&lt;h2 id="每一段保護把負載轉到哪裡">每一段保護把負載轉到哪裡&lt;/h2>
&lt;p>把請求擋下或延後的機制（邊緣快取合併回源、速率限制、排隊放行、Redis 閘門、機器人偵測），本篇統稱&lt;strong>保護&lt;/strong>，實作它的程式或服務是&lt;strong>保護元件&lt;/strong>。每一段保護擋下或延後某一類請求（可以提早回應的、還沒輪到的），被擋下的請求並沒有消失，改由保護元件承受，或轉給保護後面的下一個元件，承受的那一方會先耗盡某種資源；保護也會讓下游出現新的上限，或讓同一個請求被處理不只一次（去重的做法在〈佇列、使用者重按與回呼帶來的重送與去重〉）。邊緣快取合併回源與機器人偵測把負載留在邊緣，速率限制、排隊放行與 Redis 閘門把它轉到保護元件自己的狀態與連到它的連線上，排隊放行另外帶來位置查詢；保護把請求順利送到下游之後，負載轉到最慢的接收端與第三方業者的配額。&lt;/p>
&lt;p>&lt;strong>保護元件自己的狀態&lt;/strong>。排隊的順序、已發出的進入資格與速率限制的計數，要存在保護元件的程序以外，重啟之後才接得下去（〈保護元件故障時的輸出：停止放行、全部放行、保守的固定上限與預設值〉）。每一次入列、位置查詢與計數都是對那個儲存的一次寫入或讀取，而它承受的是全部到達的請求，不只放行之後的請求，所以它的容量要以到達的請求率規劃（&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> 的計數儲存是同一件事）。&lt;/p>
&lt;p>&lt;strong>連線數&lt;/strong>。連到一個元件的連線數，等於應用程式這一側同時在執行的單位數（程序、執行緒或協程）乘以每個單位各開的連線數，或在用連線池的執行環境裡，等於每個程序的連線池大小乘以程序數。把扣庫存或計數從資料庫移到 Redis 這類記憶體儲存，資料庫的鎖等待少了，連到新儲存的連線數卻成為新的上限。PHP-FPM 這類每個請求佔一個 worker 的執行環境，連線數的上限是全部主機的 worker 總數（每台的 &lt;code>pm.max_children&lt;/code> 乘以主機數），為了尖峰把主機擴到幾十台時，總數可能超過 Redis 預設的 &lt;code>maxclients&lt;/code>（一萬；Redis 行程的檔案描述元上限不夠時，實際的上限是那個上限減 32，可能低很多）。持久連線（phpredis 的 &lt;code>pconnect&lt;/code>）讓每個 worker 長期保留一條連線，閒置的 worker 也佔著一條，省下的是每次請求的握手，連線數不會變少；要讓連線數少於執行單位數，要在中間放一層連線代理（例如 twemproxy，它對後端維持少數的持久連線）。沒有持久連線時，每次新開連線要付 TCP 握手的成本，用完的連線在主動關閉的那一端（通常是應用程式這一端）停留在 TIME_WAIT 狀態，佔住應用程式主機的本機連接埠。資料庫連線的同一件事在 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/connection-pool-amplification/" data-link-title="9.14 連線池放大解法（PgBouncer / RDS Proxy / ProxySQL）" data-link-desc="水平擴展應用層時 DB 連線池放大問題的具體解法、connection pooler 三大選項對比、解 9.13 提出但未深入的隱性成本">9.14 連線池放大解法&lt;/a>。&lt;/p>
&lt;p>&lt;strong>輪詢&lt;/strong>。排隊把請求從資料庫移開，排隊中的使用者卻會反覆查自己的位置，每秒的查詢數是等候人數除以查詢的間隔（9.0.1 的假設：十萬人每五秒一次，每秒兩萬個），落在排隊服務上。讓每一次查詢便宜（位置資訊放在記憶體或邊緣），讓查詢變少（由伺服器在回應裡指定下一次查詢的間隔），或改成伺服器推送；改成推送時，等候中的每一條連線都是要規劃的容量。&lt;/p>
&lt;p>&lt;strong>下游的接收端&lt;/strong>。一個元件若把輸出直接送給每一個接收端、等接收端收下才處理下一筆，最慢的那一個接收端決定了整個元件的處理速度，負載以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">Backpressure&lt;/a> 的形式傳回來。把輸出先寫進一份日誌，由另外的發布服務送給各個接收端，接收端跟不上時，延遲的只是送到它那裡的資料。&lt;/p>
&lt;p>&lt;strong>外部的配額&lt;/strong>。第三方業者（金流、簡訊、雲端 API）對每個商家的速率限制會成為下一個上限。這個上限由業者設定，所以只能事先查到數字、在尖峰之前向業者申請提高；超過時業者回錯誤（例如 HTTP 429）或變慢，所以要在自己這一側排隊，以不超過配額的速率送出；呼叫業者的請求若和其他流量共用同一個連線池，業者變慢時等待中的請求會佔住其他請求也要用的連線，所以呼叫業者用獨立的連線池與逾時（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/bulkhead/" data-link-title="Bulkhead" data-link-desc="說明 bulkhead 如何用資源分艙限制故障擴散">Bulkhead&lt;/a>）。&lt;/p>
&lt;h2 id="佇列使用者重按與回呼帶來的重送與去重">佇列、使用者重按與回呼帶來的重送與去重&lt;/h2>
&lt;p>把請求放進佇列延後處理，會讓同一個請求被處理不只一次。佇列在消費端以確認（ack）模式使用時保證的是至少送達一次，消費端處理到一半當機、佇列重送，同一個請求會被處理兩次。請求在進入佇列之前要有一個識別碼（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency-key/" data-link-title="Idempotency Key（冪等鍵）" data-link-desc="同一操作重送時該由誰生成識別碼、存多久、衝突怎麼回——冪等性質的對外契約落地機制">Idempotency Key&lt;/a>），處理那一段以它辨認重送：辨認重送與狀態改變寫在同一個交易裡（以識別碼當唯一鍵寫入結果），識別碼至少保存到用戶端不會再重送為止；先查一個快取、再寫資料庫的寫法，在兩步之間當機或兩個重送同時到達時仍會重複。識別碼若在使用者那一次操作時就由用戶端產生（表單載入時產生、重按時沿用），使用者重按也辨認得出；識別碼在入列時才由伺服器產生時，使用者重按產生的是新的請求，佇列分不出來，要以業務規則限制（例如一個帳號同時只能有一筆在排隊，在進入佇列時檢查）。排程服務與第三方的回呼同樣可能把同一件事送兩次，條件更新（&lt;code>UPDATE ... WHERE status = 'pending'&lt;/code>）讓第二次執行更新零列；對外的動作（寄信、出貨、呼叫退款 API）要在更新了一列之後才做，寫在更新之前、或不看更新了幾列就執行的動作，仍會做兩次。只靠「之後才做」還會漏做：更新提交之後、對外動作完成之前當機，重送時更新零列，動作就被跳過。所以待執行的對外動作和狀態改變寫在同一個交易裡，由另一個程序讀出、至少送一次（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">Outbox Pattern&lt;/a>），呼叫業者時帶業者的冪等鍵（例如 Stripe 的 &lt;code>Idempotency-Key&lt;/code>），讓至少一次的送出在業者那一側只生效一次；業者不支援冪等鍵時，以對帳找出重複或漏做的動作。&lt;/p>
&lt;h2 id="保護元件故障時的輸出停止放行全部放行保守的固定上限與預設值">保護元件故障時的輸出：停止放行、全部放行、保守的固定上限與預設值&lt;/h2>
&lt;p>保護元件故障時的處置有三種：停止放行（fail closed）、全部放行（fail open），以及介於兩者之間、退回一個保守的固定上限放行。選擇時先分出保護擋的是什麼。權限檢查、餘額檢查、詐欺評分與法規要求的檢查擋的是不該進來的請求，放過去就違反不能讓步的需求，而停止放行的代價是整個服務停擺，所以這一類再依它在檢查順序裡的位置分開處理。之後沒有其他檢查的權限與法規檢查，故障時停止放行，全部放行的結果是越權或跳過法規要求的檢查。後面還有權威檢查的前置檢查（閘道上的餘額檢查，最終檢查在一次一筆的處理裡），故障時可以放行給權威檢查。權限檢查可以先用快取的最近決定繼續放行，代價是權限被撤銷之後，要等快取到期才生效。詐欺評分漏掉的損失有上限、事後可以經由拒付追回部分金額，故障時放行多少是業務決定的損失上限，例如低金額放行、高金額暫停、事後審查。&lt;/p>
&lt;p>只用來擋流量的保護（排隊、速率限制、閘門），依下游在故障那一刻要承受的流量選。保護的資源一定處理不了全部請求時（售票的排隊，保護的是扣庫存那一段），全部放行等於拿掉保護，全部請求直接打到必須依序處理的扣庫存，選停止放行，使用者看到的是暫停。保護的資源處理得了全部合法請求、保護只用來擋濫用時（邊緣依來源位址的速率限制），停止放行等於整個服務停擺，選全部放行。保護的資源處理得了全部合法請求、原本會選全部放行的那一類保護，常常正是因為它要擋的那股負載而故障（速率限制的計數儲存在攻擊流量下過載），這時全部放行是在負載最大的時候拿掉保護，所以退回保守的固定上限，它取代的是全部放行，不取代排隊這類該停止放行的保護：共用的限流服務故障時，呼叫它的每一個 API 實例改以本機的計數、每秒放行固定的請求數，這個數字是下游實測的總處理量除以實例數，全部實例加起來不超過下游處理得了的量；實例數會因自動擴充而變時，以最大實例數計算，或在故障期間停止擴充。代價有三個：每個實例只有一個總計數，一個重度呼叫者就能吃光一個實例的額度，所以本機計數也依呼叫者分開；負載平衡不均時有的實例拒絕、有的閒著，實際放行量低於下游處理得了的量；實際實例數遠少於最大實例數時，放行量只剩一小部分，接近停止放行。這個折衷是從機制推出的，只用在擋流量的保護上；擋不該進來的請求的保護照上一段的分法處理。停止放行之後要能在恢復後照原本的順序繼續，保護元件的狀態（排隊的順序、已發出的資格）要存在它的程序以外，只存在記憶體的狀態在重啟時就丟了。&lt;/p>
&lt;p>選了全部放行之後，還要確認下游讀到的值代表放行：保護元件故障時，保護元件自己或呼叫它的那一方的備援邏輯會給下游一個值，這個值必須是下游判成放行的那個值。&lt;a href="https://blog.cloudflare.com/18-november-2025-outage/">Cloudflare 2025 年 11 月 18 日的事故報告&lt;/a> 寫到，機器人偵測讀到一份因為重複的列而變大的設定檔，舊版代理引擎上的所有流量都拿到零分的機器人分數。那套評分的正常範圍是 1 到 99，分數越低越像機器人，零分代表沒有評估（&lt;a href="https://developers.cloudflare.com/bots/concepts/bot-score/">Cloudflare 的 bot score 文件&lt;/a>）；客戶的規則寫成低於某個分數就擋，代表沒有評估的零分落在擋的那一側，設了擋機器人規則的客戶因此擋掉了大量的真實流量。保護元件的故障也不只停掉一種，讀到壞掉的規則或設定檔、輸出錯的判斷，是同樣要事先決定處置的情形；那時保護元件還在運作，監控上每一層都顯示正常，這一類異常要靠 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/monitoring-outside-the-chain/" data-link-title="4.0.1 監控鏈事前偵測不到的異常：業務結果、資料正確性、到期事件、排程與背景工作、備份、安全、第三方依賴與成本的監看" data-link-desc="監控鏈各層都正常時仍會發生的幾類異常：業務停了而程式回應正常、資料寫錯、TLS 憑證與網域到期、排程工作與佇列消費端沒跑、備份還原不回來、以帳號正常操作進行的攻擊、第三方業者停掉與帳單失控，各自要先定義的預期、檢查它的規則與承接的專章">4.0.1 監控鏈事前偵測不到的異常&lt;/a> 裡的業務結果發現。保護元件是購買的服務（業者提供的等候室、WAF）時，故障時放行還是不放行由業者的行為決定，要事先查清楚業者的預設，以及它能改成什麼。&lt;/p></description><content:encoded><![CDATA[<p>這篇整理沿著一個會改變狀態的請求（購票、下單、委託、建立短網址）從用戶端走到資料庫的路徑，分析負載時共用的概念。讀取請求（短網址的轉址、商品頁、座位圖上的剩餘數量）本身的快取設計不在本篇的範圍內，只有保護為了寫入路徑而把讀取移到邊緣的那一段（座位圖的剩餘數量由 CDN 合併回源）在範圍內。概念以 <a href="/blog/backend/case-systems/" data-link-title="Backend 範例系統：短網址、售票搶購、電商 checkout 與交易撮合的功能、需求與規模假設" data-link-desc="Backend 教材幾篇以具體系統走完整條請求路徑的文章共用的四個範例系統：每個系統提供的功能與請求種類、不能讓步與可以讓步的需求、規模假設與起點架構，以及四個系統在成功請求的總數是否有上限、延遲是否改變請求的價值、同一個請求被處理兩次的後果上的差別">Backend 範例系統</a> 的短網址、售票搶購、電商 checkout 與交易撮合舉例；完整走過一條路徑的案例是 <a href="/blog/backend/09-performance-capacity/ticketing-load-path/" data-link-title="9.0.1 售票搶購的負載路徑：熱點列與連線池、CDN 合併回源、排隊放行的速率、資料庫認領與加入 Redis 閘門的條件、付款期限的條件更新" data-link-desc="以售票搶購為例，沿著一張票從座位圖到付款記帳的路徑，寫出全部請求更新同一個庫存計數時熱點列與連線池的耗盡順序，邊緣快取、排隊放行、資料庫逐張認領與加入 Redis 扣庫存各自把負載轉給了哪一段、自己出錯時由哪一段補救，以及成功請求的總數有上限、延遲不改變請求的價值、先到先得這幾個前提讓哪些做法可以簡化、哪些需求無法簡化">9.0.1 售票搶購的負載路徑</a> 與 <a href="/blog/backend/09-performance-capacity/matching-engine-load-path/" data-link-title="9.0.2 交易撮合的負載路徑：閘道的帳戶限流與明確拒絕、依序日誌的複寫延遲、單一寫入者的撮合引擎、行情的合併送出與重放驗證容量" data-link-desc="以交易撮合為例，沿著一筆委託從入口閘道到成交回報的路徑，寫出撮合之前結果無法確定、處理順序決定成交、延遲改變請求的價值這些性質讓哪些負載保護不能用，閘道、依序日誌、單一寫入者的撮合引擎與行情發布各自先耗盡的資源與出錯時的補救，過載時明確拒絕超量委託與以重放驗證容量的做法，並與售票搶購對照">9.0.2 交易撮合的負載路徑</a>。負載超過容量之後服務呈現哪一種結果（變慢、請求失敗、無回應、程序終止、停在失效），在 <a href="/blog/backend/09-performance-capacity/overload-outcomes/" data-link-title="9.16 負載超過容量時服務的表現：變慢、請求失敗、無回應、程序終止，尖峰過去或應對切換之後仍持續的停在失效、擺盪與第二波湧入，以及應對選出的降級回應，各自的判斷標準、成因、應對與看得到它的監控層" data-link-desc="負載超過容量（過載）時，服務呈現的結果分成變慢、請求失敗（抵達後被拒與根本沒有抵達）、無回應、程序終止，以及尖峰過去、依賴恢復或應對切換之後仍持續的停在失效、擺盪與第二波湧入；每一種的判斷標準、通常的成因與應對，哪一種先出現由先耗盡的資源與等待是否設有上限決定，影響範圍怎麼隨時間擴大，服務端監控的各層各自看得到哪一種，降級回應怎麼計數，過載在資料上造成的重複寫入、接受後遺失與讀到舊資料，以及描述一次過載要寫出的項目">9.16 負載超過容量時服務的表現</a>。</p>
<h2 id="一次一筆的處理與它的互斥範圍">一次一筆的處理與它的互斥範圍</h2>
<p>一個服務的需求裡，通常有一件事必須一次只處理一筆（業界常稱為 serialization point）：同一張票不能同時賣給兩個人，同一筆訂單不能同時被付款與取消，同一個交易對的委託要依單一順序撮合，同一個短代碼不能同時分配給兩個長網址。這件事要靠鎖、單一執行緒或條件更新依序處理，所以不論前面有多少台伺服器，抵達這件處理的請求都要逐一通過，負載最後集中在這件處理上。</p>
<p>決定有多少請求要依序等候的，是<strong>互斥範圍</strong>：哪些請求彼此必須輪流處理。互斥範圍有三個來源：需求、資料的表示方式，以及把多個範圍放進同一個依序處理的執行者。</p>
<p>售票的需求要互斥的只是同一張票；全場庫存若寫成一個計數欄，所有購票請求都更新資料表的同一列，整場的請求依序等候同一把鎖，這種情形叫熱點列（hot row）。改成每張票一列，而且認領的寫法讓每個請求各自挑到不同的列（例如跳過別的交易已經鎖住的列，寫法在 <a href="/blog/backend/09-performance-capacity/ticketing-load-path/" data-link-title="9.0.1 售票搶購的負載路徑：熱點列與連線池、CDN 合併回源、排隊放行的速率、資料庫認領與加入 Redis 閘門的條件、付款期限的條件更新" data-link-desc="以售票搶購為例，沿著一張票從座位圖到付款記帳的路徑，寫出全部請求更新同一個庫存計數時熱點列與連線池的耗盡順序，邊緣快取、排隊放行、資料庫逐張認領與加入 Redis 扣庫存各自把負載轉給了哪一段、自己出錯時由哪一段補救，以及成功請求的總數有上限、延遲不改變請求的價值、先到先得這幾個前提讓哪些做法可以簡化、哪些需求無法簡化">9.0.1 售票搶購的負載路徑</a> 的〈扣庫存：資料庫逐張認領與加入 Redis 閘門的條件〉），只有搶同一張票（同一個座位）的請求才需要輪流；認領時若每個請求都挑「第一張還沒賣出的票」，所有請求仍會撞在同一列上。表示方式造成的互斥範圍，在實作上表現為鎖的粒度（lock granularity），可以靠改寫法縮小。熱點列的排隊機制與分散寫入的做法在 <a href="/blog/backend/01-database/high-frequency-counting/" data-link-title="1.18 高頻計數的寫入與彙總：熱點列的鎖、只新增的事件、可以重跑的彙總與重送的事件" data-link-desc="熱點列更新的吞吐量上限與它的成因、拆成多列／只新增事件／先在記憶體裡合併的差別、只新增事件搬去哪裡的成本、累加與重算在什麼條件下漏算或重複計算、讓彙總排程互斥的建議鎖與連線池的交互，以及重送事件的冪等寫入、推導事實的存放位置，以及 Redis 承擔即時計數、跨小時不重複數、去重與互斥時各自的邊界">1.18 高頻計數的寫入與彙總</a>。</p>
<p>撮合的需求要互斥的是整本委託簿（同一個交易對上還沒成交的買賣委託），因為一筆委託是否成交取決於簿上所有其他委託的價格與時間；這是需求要求的下限，換表示方式也縮不小。撮合引擎以單一執行緒依序處理、沒有用鎖，而同一條執行緒或同一份依序日誌裡放的交易對越多，互斥範圍就越大：LMAX 把所有市場放在一條執行緒上，這時範圍是整個交易平台；依交易對分開撮合引擎，是把範圍縮回一本簿（<a href="/blog/backend/knowledge-cards/single-writer-model/" data-link-title="Single Writer Model" data-link-desc="說明單寫者模型如何序列化寫入，並成為系統的容量邊界">Single Writer Model</a>；兩種做法在 <a href="/blog/backend/09-performance-capacity/matching-engine-load-path/" data-link-title="9.0.2 交易撮合的負載路徑：閘道的帳戶限流與明確拒絕、依序日誌的複寫延遲、單一寫入者的撮合引擎、行情的合併送出與重放驗證容量" data-link-desc="以交易撮合為例，沿著一筆委託從入口閘道到成交回報的路徑，寫出撮合之前結果無法確定、處理順序決定成交、延遲改變請求的價值這些性質讓哪些負載保護不能用，閘道、依序日誌、單一寫入者的撮合引擎與行情發布各自先耗盡的資源與出錯時的補救，過載時明確拒絕超量委託與以重放驗證容量的做法，並與售票搶購對照">9.0.2 交易撮合的負載路徑</a> 的〈撮合引擎：單一寫入者與記憶體內的委託簿〉）。</p>
<p>需求本身可以協商時，互斥範圍也能縮小。廣告平台允許預算小幅超支，就可以把一個廣告活動的預算預先切成多份額度、分給各台伺服器各自扣，定期同步，互斥範圍從整個活動縮到一台伺服器上的那一份，代價是超支有上限而不是零。撮合沒有這種空間：不論撮合規則是價格與時間優先，還是依比例分配，一筆委託的成交都取決於整本簿，所以在一本簿之內，撮合能做的只有讓依序處理的那一條路徑變快。</p>
<h2 id="能在一次一筆的處理之前回應的請求">能在一次一筆的處理之前回應的請求</h2>
<p>需要經過一次一筆處理（必須依序、不能並行處理的那件事）的請求（購票、下單、委託、建立短網址）裡，有三類在進入那件處理之前就能回應：格式、權限或帳戶狀態不符的請求（例如餘額不足）在入口就能拒絕，結果已經確定的請求可以直接告訴它結果（票賣完之後的購票請求），以及重送的請求可以回傳上一次的結果。同一段時間內，這三類請求的筆數在所有需要經過一次一筆處理的請求筆數裡所佔的比例（分母包含這三類本身），下文稱<strong>可提早回應的比例</strong>；這個比例越高，前面的元件擋下的越多，一次一筆的處理承受的越少。</p>
<p>提早回應的元件讀的是一次一筆的處理所持有狀態的副本（售票的剩餘數量、帳戶餘額），副本會落後。一旦確定就不再改變的結果可以安全地提早回應；會回頭的狀態（帳戶餘額、付款期限到期時被放回的票），提早回應的只是暫時的結果，而副本落後造成的錯有兩個方向，補救各不相同。副本把請求判成不行（已售完、餘額不足），可能是錯拒，所以要讓使用者能再試；副本把請求判成可以而放行，副本可能已經過時，所以通過提早檢查的請求一律要在一次一筆的處理裡再檢查一次，否則餘額會透支、票會超賣。</p>
<p>可提早回應的比例由需求的性質決定。售票的成功請求總數有上限（票數），票數之外的購票請求結果在售完那一刻就確定（直到付款期限到期、有票被放回為止），所以比例高；交易撮合沒有這種上限，一筆委託的結果要撮合當下才知道，只有無效的委託能提早回應，比例低。成功請求的總數有上限，不保證比例高：拍賣的成功請求總數是一（只有一個得標者），而結標之前每一次出價都要處理、誰得標事先不知道。</p>
<p>比例高的系統，負載保護的重心在一次一筆的處理之前：排隊、提早回應，把請求擋在資料庫前面。比例低時，比對同一個互斥範圍每秒到達的請求數與依序處理一個範圍的處理量。到達的請求數低於處理量時（checkout 的每一筆訂單、一般帳戶的轉帳），一次一筆的處理不是瓶頸，負載先耗盡的是別的資源（連線數、資料庫的總寫入量），見〈每一段保護把負載轉到哪裡〉。到達的請求數高於處理量、而範圍能靠改表示方式縮小時（熱門商家的收款帳戶改成只新增入帳紀錄），回到〈一次一筆的處理與它的互斥範圍〉的熱點列。範圍縮不小時（一本委託簿、結標前的一件拍品），同一個範圍的全部請求都要通過同一個依序處理的執行者，重心才落在那件處理本身：記憶體內的狀態、單一寫入者、處理路徑上沒有資料庫，以及事先配置的容量。</p>
<h2 id="多出來的請求排隊拒絕或請使用者重新確認">多出來的請求：排隊、拒絕或請使用者重新確認</h2>
<p>扣掉能提早回應的請求，抵達一次一筆的處理（例如扣庫存或撮合）的請求仍可能超過它的處理量，多出來的那些要決定怎麼處置，而決定的依據是<strong>延遲是否改變請求的價值</strong>。售票在不劃位、固定票價時，排隊幾分鐘之後買到的仍是同一種票、同一個價格，多出來的請求可以排隊（<a href="/blog/backend/knowledge-cards/admission-control/" data-link-title="Admission Control（准入控制）" data-link-desc="要決定湧入的使用者誰能進入、什麼時候進入時，例如開賣的虛擬等候室、waitlist、事前註冊與抽籤；和負載卸除、限流的分工，以及和 Kubernetes admission controller 的區分">Admission Control</a>），放行的速率設在後端實測的處理量。連續撮合的交易所，一筆委託晚一秒送進委託簿，市場已經變了，市價單以偏離預期的價格成交，排隊讓請求失去下單時的意義。所以委託到達時以佇列長度除以處理速率預估要等多久，預估超過委託延遲目標就在接受之前明確拒絕（<a href="/blog/backend/knowledge-cards/load-shedding/" data-link-title="Load Shedding" data-link-desc="說明服務過載時如何主動拒絕低優先工作以保護核心能力">Load Shedding</a>；9.0.2 的閘道以佇列長度的上限做這件事）。</p>
<p>延遲改變價值、所以要拒絕，還有一個前提：請求在處理時照送出當下的條件直接執行，不再經使用者確認。處理之前會讓使用者重新確認的請求，延遲的代價只是多一次確認：checkout 的價格在使用者排隊期間變了，下單時以新價格重算並請使用者確認，訂單照樣成立。市場的規則也會改變這個判斷：集合競價是把一段時間內的委託集中起來、以單一價格一次撮合的規則，以固定間隔做集合競價的市場，等到下一次撮合本來就是規則的一部分。</p>
<p>被拒絕的用戶端會重送時，負載以重送的形式回來：用戶端是程式時往往立刻重送，重送的請求疊在原本的需求上，讓進來的請求率高於真實的需求，這是 <a href="/blog/backend/knowledge-cards/retry-storm/" data-link-title="Retry Storm" data-link-desc="說明大量重試如何把局部故障放大成系統壓力">Retry Storm</a>。所以拒絕的回應要帶重試的時機（<code>Retry-After</code> 這類欄位，或約定加上隨機延遲的退避間隔，見 <a href="/blog/backend/knowledge-cards/jitter/" data-link-title="Jitter" data-link-desc="說明重試或排程加入隨機偏移如何降低同步尖峰">Jitter</a>），排隊的回應要讓使用者看得到自己的位置，拒絕與排隊的回應都在讓用戶端不要用重送去確認結果。逾時就作廢的用戶端（廣告的即時競價）不會重送，拒絕本身就是結果。</p>
<h2 id="每一段保護把負載轉到哪裡">每一段保護把負載轉到哪裡</h2>
<p>把請求擋下或延後的機制（邊緣快取合併回源、速率限制、排隊放行、Redis 閘門、機器人偵測），本篇統稱<strong>保護</strong>，實作它的程式或服務是<strong>保護元件</strong>。每一段保護擋下或延後某一類請求（可以提早回應的、還沒輪到的），被擋下的請求並沒有消失，改由保護元件承受，或轉給保護後面的下一個元件，承受的那一方會先耗盡某種資源；保護也會讓下游出現新的上限，或讓同一個請求被處理不只一次（去重的做法在〈佇列、使用者重按與回呼帶來的重送與去重〉）。邊緣快取合併回源與機器人偵測把負載留在邊緣，速率限制、排隊放行與 Redis 閘門把它轉到保護元件自己的狀態與連到它的連線上，排隊放行另外帶來位置查詢；保護把請求順利送到下游之後，負載轉到最慢的接收端與第三方業者的配額。</p>
<p><strong>保護元件自己的狀態</strong>。排隊的順序、已發出的進入資格與速率限制的計數，要存在保護元件的程序以外，重啟之後才接得下去（〈保護元件故障時的輸出：停止放行、全部放行、保守的固定上限與預設值〉）。每一次入列、位置查詢與計數都是對那個儲存的一次寫入或讀取，而它承受的是全部到達的請求，不只放行之後的請求，所以它的容量要以到達的請求率規劃（<a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">Rate Limit</a> 的計數儲存是同一件事）。</p>
<p><strong>連線數</strong>。連到一個元件的連線數，等於應用程式這一側同時在執行的單位數（程序、執行緒或協程）乘以每個單位各開的連線數，或在用連線池的執行環境裡，等於每個程序的連線池大小乘以程序數。把扣庫存或計數從資料庫移到 Redis 這類記憶體儲存，資料庫的鎖等待少了，連到新儲存的連線數卻成為新的上限。PHP-FPM 這類每個請求佔一個 worker 的執行環境，連線數的上限是全部主機的 worker 總數（每台的 <code>pm.max_children</code> 乘以主機數），為了尖峰把主機擴到幾十台時，總數可能超過 Redis 預設的 <code>maxclients</code>（一萬；Redis 行程的檔案描述元上限不夠時，實際的上限是那個上限減 32，可能低很多）。持久連線（phpredis 的 <code>pconnect</code>）讓每個 worker 長期保留一條連線，閒置的 worker 也佔著一條，省下的是每次請求的握手，連線數不會變少；要讓連線數少於執行單位數，要在中間放一層連線代理（例如 twemproxy，它對後端維持少數的持久連線）。沒有持久連線時，每次新開連線要付 TCP 握手的成本，用完的連線在主動關閉的那一端（通常是應用程式這一端）停留在 TIME_WAIT 狀態，佔住應用程式主機的本機連接埠。資料庫連線的同一件事在 <a href="/blog/backend/09-performance-capacity/connection-pool-amplification/" data-link-title="9.14 連線池放大解法（PgBouncer / RDS Proxy / ProxySQL）" data-link-desc="水平擴展應用層時 DB 連線池放大問題的具體解法、connection pooler 三大選項對比、解 9.13 提出但未深入的隱性成本">9.14 連線池放大解法</a>。</p>
<p><strong>輪詢</strong>。排隊把請求從資料庫移開，排隊中的使用者卻會反覆查自己的位置，每秒的查詢數是等候人數除以查詢的間隔（9.0.1 的假設：十萬人每五秒一次，每秒兩萬個），落在排隊服務上。讓每一次查詢便宜（位置資訊放在記憶體或邊緣），讓查詢變少（由伺服器在回應裡指定下一次查詢的間隔），或改成伺服器推送；改成推送時，等候中的每一條連線都是要規劃的容量。</p>
<p><strong>下游的接收端</strong>。一個元件若把輸出直接送給每一個接收端、等接收端收下才處理下一筆，最慢的那一個接收端決定了整個元件的處理速度，負載以 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">Backpressure</a> 的形式傳回來。把輸出先寫進一份日誌，由另外的發布服務送給各個接收端，接收端跟不上時，延遲的只是送到它那裡的資料。</p>
<p><strong>外部的配額</strong>。第三方業者（金流、簡訊、雲端 API）對每個商家的速率限制會成為下一個上限。這個上限由業者設定，所以只能事先查到數字、在尖峰之前向業者申請提高；超過時業者回錯誤（例如 HTTP 429）或變慢，所以要在自己這一側排隊，以不超過配額的速率送出；呼叫業者的請求若和其他流量共用同一個連線池，業者變慢時等待中的請求會佔住其他請求也要用的連線，所以呼叫業者用獨立的連線池與逾時（<a href="/blog/backend/knowledge-cards/bulkhead/" data-link-title="Bulkhead" data-link-desc="說明 bulkhead 如何用資源分艙限制故障擴散">Bulkhead</a>）。</p>
<h2 id="佇列使用者重按與回呼帶來的重送與去重">佇列、使用者重按與回呼帶來的重送與去重</h2>
<p>把請求放進佇列延後處理，會讓同一個請求被處理不只一次。佇列在消費端以確認（ack）模式使用時保證的是至少送達一次，消費端處理到一半當機、佇列重送，同一個請求會被處理兩次。請求在進入佇列之前要有一個識別碼（<a href="/blog/backend/knowledge-cards/idempotency-key/" data-link-title="Idempotency Key（冪等鍵）" data-link-desc="同一操作重送時該由誰生成識別碼、存多久、衝突怎麼回——冪等性質的對外契約落地機制">Idempotency Key</a>），處理那一段以它辨認重送：辨認重送與狀態改變寫在同一個交易裡（以識別碼當唯一鍵寫入結果），識別碼至少保存到用戶端不會再重送為止；先查一個快取、再寫資料庫的寫法，在兩步之間當機或兩個重送同時到達時仍會重複。識別碼若在使用者那一次操作時就由用戶端產生（表單載入時產生、重按時沿用），使用者重按也辨認得出；識別碼在入列時才由伺服器產生時，使用者重按產生的是新的請求，佇列分不出來，要以業務規則限制（例如一個帳號同時只能有一筆在排隊，在進入佇列時檢查）。排程服務與第三方的回呼同樣可能把同一件事送兩次，條件更新（<code>UPDATE ... WHERE status = 'pending'</code>）讓第二次執行更新零列；對外的動作（寄信、出貨、呼叫退款 API）要在更新了一列之後才做，寫在更新之前、或不看更新了幾列就執行的動作，仍會做兩次。只靠「之後才做」還會漏做：更新提交之後、對外動作完成之前當機，重送時更新零列，動作就被跳過。所以待執行的對外動作和狀態改變寫在同一個交易裡，由另一個程序讀出、至少送一次（<a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">Outbox Pattern</a>），呼叫業者時帶業者的冪等鍵（例如 Stripe 的 <code>Idempotency-Key</code>），讓至少一次的送出在業者那一側只生效一次；業者不支援冪等鍵時，以對帳找出重複或漏做的動作。</p>
<h2 id="保護元件故障時的輸出停止放行全部放行保守的固定上限與預設值">保護元件故障時的輸出：停止放行、全部放行、保守的固定上限與預設值</h2>
<p>保護元件故障時的處置有三種：停止放行（fail closed）、全部放行（fail open），以及介於兩者之間、退回一個保守的固定上限放行。選擇時先分出保護擋的是什麼。權限檢查、餘額檢查、詐欺評分與法規要求的檢查擋的是不該進來的請求，放過去就違反不能讓步的需求，而停止放行的代價是整個服務停擺，所以這一類再依它在檢查順序裡的位置分開處理。之後沒有其他檢查的權限與法規檢查，故障時停止放行，全部放行的結果是越權或跳過法規要求的檢查。後面還有權威檢查的前置檢查（閘道上的餘額檢查，最終檢查在一次一筆的處理裡），故障時可以放行給權威檢查。權限檢查可以先用快取的最近決定繼續放行，代價是權限被撤銷之後，要等快取到期才生效。詐欺評分漏掉的損失有上限、事後可以經由拒付追回部分金額，故障時放行多少是業務決定的損失上限，例如低金額放行、高金額暫停、事後審查。</p>
<p>只用來擋流量的保護（排隊、速率限制、閘門），依下游在故障那一刻要承受的流量選。保護的資源一定處理不了全部請求時（售票的排隊，保護的是扣庫存那一段），全部放行等於拿掉保護，全部請求直接打到必須依序處理的扣庫存，選停止放行，使用者看到的是暫停。保護的資源處理得了全部合法請求、保護只用來擋濫用時（邊緣依來源位址的速率限制），停止放行等於整個服務停擺，選全部放行。保護的資源處理得了全部合法請求、原本會選全部放行的那一類保護，常常正是因為它要擋的那股負載而故障（速率限制的計數儲存在攻擊流量下過載），這時全部放行是在負載最大的時候拿掉保護，所以退回保守的固定上限，它取代的是全部放行，不取代排隊這類該停止放行的保護：共用的限流服務故障時，呼叫它的每一個 API 實例改以本機的計數、每秒放行固定的請求數，這個數字是下游實測的總處理量除以實例數，全部實例加起來不超過下游處理得了的量；實例數會因自動擴充而變時，以最大實例數計算，或在故障期間停止擴充。代價有三個：每個實例只有一個總計數，一個重度呼叫者就能吃光一個實例的額度，所以本機計數也依呼叫者分開；負載平衡不均時有的實例拒絕、有的閒著，實際放行量低於下游處理得了的量；實際實例數遠少於最大實例數時，放行量只剩一小部分，接近停止放行。這個折衷是從機制推出的，只用在擋流量的保護上；擋不該進來的請求的保護照上一段的分法處理。停止放行之後要能在恢復後照原本的順序繼續，保護元件的狀態（排隊的順序、已發出的資格）要存在它的程序以外，只存在記憶體的狀態在重啟時就丟了。</p>
<p>選了全部放行之後，還要確認下游讀到的值代表放行：保護元件故障時，保護元件自己或呼叫它的那一方的備援邏輯會給下游一個值，這個值必須是下游判成放行的那個值。<a href="https://blog.cloudflare.com/18-november-2025-outage/">Cloudflare 2025 年 11 月 18 日的事故報告</a> 寫到，機器人偵測讀到一份因為重複的列而變大的設定檔，舊版代理引擎上的所有流量都拿到零分的機器人分數。那套評分的正常範圍是 1 到 99，分數越低越像機器人，零分代表沒有評估（<a href="https://developers.cloudflare.com/bots/concepts/bot-score/">Cloudflare 的 bot score 文件</a>）；客戶的規則寫成低於某個分數就擋，代表沒有評估的零分落在擋的那一側，設了擋機器人規則的客戶因此擋掉了大量的真實流量。保護元件的故障也不只停掉一種，讀到壞掉的規則或設定檔、輸出錯的判斷，是同樣要事先決定處置的情形；那時保護元件還在運作，監控上每一層都顯示正常，這一類異常要靠 <a href="/blog/backend/04-observability/monitoring-outside-the-chain/" data-link-title="4.0.1 監控鏈事前偵測不到的異常：業務結果、資料正確性、到期事件、排程與背景工作、備份、安全、第三方依賴與成本的監看" data-link-desc="監控鏈各層都正常時仍會發生的幾類異常：業務停了而程式回應正常、資料寫錯、TLS 憑證與網域到期、排程工作與佇列消費端沒跑、備份還原不回來、以帳號正常操作進行的攻擊、第三方業者停掉與帳單失控，各自要先定義的預期、檢查它的規則與承接的專章">4.0.1 監控鏈事前偵測不到的異常</a> 裡的業務結果發現。保護元件是購買的服務（業者提供的等候室、WAF）時，故障時放行還是不放行由業者的行為決定，要事先查清楚業者的預設，以及它能改成什麼。</p>
<h2 id="依賴變慢部分失效與恢復時的第二波湧入">依賴變慢、部分失效與恢復時的第二波湧入</h2>
<p>保護元件與它保護的依賴，失效時也不只停掉或輸出錯的值。<strong>依賴變慢</strong>時，每一個等待中的請求在逾時之前都佔著呼叫方的 worker 與連線，所以變慢通常比停掉更早耗盡呼叫方，見 <a href="/blog/backend/09-performance-capacity/overload-outcomes/" data-link-title="9.16 負載超過容量時服務的表現：變慢、請求失敗、無回應、程序終止，尖峰過去或應對切換之後仍持續的停在失效、擺盪與第二波湧入，以及應對選出的降級回應，各自的判斷標準、成因、應對與看得到它的監控層" data-link-desc="負載超過容量（過載）時，服務呈現的結果分成變慢、請求失敗（抵達後被拒與根本沒有抵達）、無回應、程序終止，以及尖峰過去、依賴恢復或應對切換之後仍持續的停在失效、擺盪與第二波湧入；每一種的判斷標準、通常的成因與應對，哪一種先出現由先耗盡的資源與等待是否設有上限決定，影響範圍怎麼隨時間擴大，服務端監控的各層各自看得到哪一種，降級回應怎麼計數，過載在資料上造成的重複寫入、接受後遺失與讀到舊資料，以及描述一次過載要寫出的項目">9.16 負載超過容量時服務的表現</a> 的〈無回應：程序還在，請求等到逾時也沒有結果〉。<strong>部分失效</strong>時只有一部分副本連不到依賴（某一個可用區的網路出口壞了），讓那幾個副本下線是對的，而剩下的副本要承接全部流量，容量不夠時接著變慢（<a href="/blog/backend/knowledge-cards/partial-failure/" data-link-title="Partial Failure" data-link-desc="說明分散式系統中部分依賴失效時如何保留整體可用性">Partial Failure</a>；Kubernetes 上的例子在 <a href="/blog/backend/04-observability/monitoring-inventory-k8s-checkout/" data-link-title="4.0.3 以監控鏈盤點 Kubernetes 上的電商 checkout：叢集內的監控、託管資料庫與第三方金流各自的失效域，以及佇列消費端與外部依賴的缺口" data-link-desc="把一個跑在託管 Kubernetes 上、有訊息佇列消費端、託管資料庫與第三方金流的電商 checkout 現有的監控放進監控鏈的各層，逐層判讀缺口與補法；另寫付款服務的 readiness 納入金流業者時對付款回呼的影響、失效域從主機換成叢集與雲端帳號之後觀察者要放在哪裡，以及業務結果、對帳、到期事件、消費端、備份、安全、第三方依賴與成本的逐類盤點">4.0.3 以監控鏈盤點 Kubernetes 上的電商 checkout</a> 的〈付款服務的 readiness 納入金流業者時的後果〉）。<strong>恢復時的第二波湧入</strong>：依賴恢復的那一刻，故障期間累積的請求與重試一起到達，讓剛恢復的依賴再過載一次，這是 9.16〈尖峰過去或應對切換之後仍持續的情形：停在失效、擺盪與第二波湧入〉的第二波湧入。第二波湧入本身是一次尖峰，累積的請求送完就結束；重試若把過載維持下去，它就轉成另一種情形：負載退去之後服務仍停在失效（<a href="/blog/backend/knowledge-cards/metastable-failure/" data-link-title="Metastable Failure（負載退去後仍停在失效狀態）" data-link-desc="流量已經回到平常的量而服務仍然大量逾時或出錯、或要判斷一次過載為什麼沒有自己恢復時，查 metastable failure 的定義、讓它停在失效狀態的持續效應、偵測它要比對的訊號與脫離它的做法">Metastable Failure</a>）。委託簿這種狀態會隨時間失去價值的系統，恢復時還要決定處理的順序，交易所停機後先只收取消、再以集合競價重開的流程在 9.0.2 的〈撮合引擎：單一寫入者與記憶體內的委託簿〉。</p>
<h2 id="延伸閱讀售票搶購與交易撮合的負載路徑">延伸閱讀：售票搶購與交易撮合的負載路徑</h2>
<p><a href="/blog/backend/09-performance-capacity/ticketing-load-path/" data-link-title="9.0.1 售票搶購的負載路徑：熱點列與連線池、CDN 合併回源、排隊放行的速率、資料庫認領與加入 Redis 閘門的條件、付款期限的條件更新" data-link-desc="以售票搶購為例，沿著一張票從座位圖到付款記帳的路徑，寫出全部請求更新同一個庫存計數時熱點列與連線池的耗盡順序，邊緣快取、排隊放行、資料庫逐張認領與加入 Redis 扣庫存各自把負載轉給了哪一段、自己出錯時由哪一段補救，以及成功請求的總數有上限、延遲不改變請求的價值、先到先得這幾個前提讓哪些做法可以簡化、哪些需求無法簡化">9.0.1 售票搶購的負載路徑</a> 從全場庫存寫成一個計數欄時熱點列與連線池的耗盡順序起算，走過 CDN 合併回源、排隊放行、資料庫逐張認領與付款期限，示範互斥範圍怎麼靠改表示方式從整場縮到一張票、可提早回應的比例高時排隊服務怎麼提早告訴後面的人買不到，以及什麼條件下才值得在扣庫存前面加一道 Redis 閘門。<a href="/blog/backend/09-performance-capacity/matching-engine-load-path/" data-link-title="9.0.2 交易撮合的負載路徑：閘道的帳戶限流與明確拒絕、依序日誌的複寫延遲、單一寫入者的撮合引擎、行情的合併送出與重放驗證容量" data-link-desc="以交易撮合為例，沿著一筆委託從入口閘道到成交回報的路徑，寫出撮合之前結果無法確定、處理順序決定成交、延遲改變請求的價值這些性質讓哪些負載保護不能用，閘道、依序日誌、單一寫入者的撮合引擎與行情發布各自先耗盡的資源與出錯時的補救，過載時明確拒絕超量委託與以重放驗證容量的做法，並與售票搶購對照">9.0.2 交易撮合的負載路徑</a> 走過閘道、依序日誌、撮合引擎與行情發布，示範互斥範圍沒辦法縮小、可提早回應的比例低時，負載保護的重心怎麼移到處理路徑本身與事先配置的容量，以及延遲改變請求的價值時，過載與停機後的處置為什麼與售票相反。</p>
]]></content:encoded></item><item><title>Backend 範例系統：短網址、售票搶購、電商 checkout 與交易撮合的功能、需求與規模假設</title><link>https://tarrragon.github.io/blog/backend/case-systems/</link><pubDate>Thu, 15 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/case-systems/</guid><description>&lt;p>這篇定義四個範例系統：短網址、售票搶購、電商 checkout 與交易撮合，供幾篇沿著具體系統的請求路徑逐段分析的文章共用，目前有 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/load-path-analysis/" data-link-title="9.0 請求路徑上的負載分析：一次一筆的處理、在它之前能回應的請求、多出來的請求怎麼處置、保護把負載轉到哪裡、重送與去重，以及保護元件與依賴故障時的處置" data-link-desc="沿著一個會改變狀態的請求從用戶端到資料庫的路徑分析負載時共用的幾個概念：必須一次只處理一筆的那件事與它的互斥範圍、需要經過它的請求裡結果事先已確定的比例、多出來的請求要排隊、拒絕還是請使用者重新確認、每一段保護把負載轉給了哪個元件（保護元件自己的狀態、連線數、輪詢、下游接收端、外部配額）、佇列與重按帶來的重送怎麼去重、保護元件故障或輸出錯的值時下游讀到什麼，以及依賴變慢、部分失效與恢復時的第二波湧入">9.0 請求路徑上的負載分析&lt;/a> 的舉例、&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/ticketing-load-path/" data-link-title="9.0.1 售票搶購的負載路徑：熱點列與連線池、CDN 合併回源、排隊放行的速率、資料庫認領與加入 Redis 閘門的條件、付款期限的條件更新" data-link-desc="以售票搶購為例，沿著一張票從座位圖到付款記帳的路徑，寫出全部請求更新同一個庫存計數時熱點列與連線池的耗盡順序，邊緣快取、排隊放行、資料庫逐張認領與加入 Redis 扣庫存各自把負載轉給了哪一段、自己出錯時由哪一段補救，以及成功請求的總數有上限、延遲不改變請求的價值、先到先得這幾個前提讓哪些做法可以簡化、哪些需求無法簡化">9.0.1 售票搶購的負載路徑&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/matching-engine-load-path/" data-link-title="9.0.2 交易撮合的負載路徑：閘道的帳戶限流與明確拒絕、依序日誌的複寫延遲、單一寫入者的撮合引擎、行情的合併送出與重放驗證容量" data-link-desc="以交易撮合為例，沿著一筆委託從入口閘道到成交回報的路徑，寫出撮合之前結果無法確定、處理順序決定成交、延遲改變請求的價值這些性質讓哪些負載保護不能用，閘道、依序日誌、單一寫入者的撮合引擎與行情發布各自先耗盡的資源與出錯時的補救，過載時明確拒絕超量委託與以重放驗證容量的做法，並與售票搶購對照">9.0.2 交易撮合的負載路徑&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/monitoring-inventory-single-host/" data-link-title="4.0.2 以監控鏈盤點單機短網址服務：各層現有的監控、缺口的性質與補法，以及監控鏈事前偵測不到的異常" data-link-desc="把一台主機上以 Docker Compose 跑的短網址服務現有的每一種監控放進監控鏈的各層，逐層判讀還開著的缺口屬於檢查範圍盲區、位置盲區、讀取缺口還是訊號失真，各自的補法，以及補上失效域以外的觀察者之前，主機整台停掉時沒有任何一層會通知的情形；另逐類查業務結果、資料正確性、安全、成本、到期事件與備份">4.0.2 以監控鏈盤點單機短網址服務&lt;/a> 、&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/monitoring-inventory-k8s-checkout/" data-link-title="4.0.3 以監控鏈盤點 Kubernetes 上的電商 checkout：叢集內的監控、託管資料庫與第三方金流各自的失效域，以及佇列消費端與外部依賴的缺口" data-link-desc="把一個跑在託管 Kubernetes 上、有訊息佇列消費端、託管資料庫與第三方金流的電商 checkout 現有的監控放進監控鏈的各層，逐層判讀缺口與補法；另寫付款服務的 readiness 納入金流業者時對付款回呼的影響、失效域從主機換成叢集與雲端帳號之後觀察者要放在哪裡，以及業務結果、對帳、到期事件、消費端、備份、安全、第三方依賴與成本的逐類盤點">4.0.3 以監控鏈盤點 Kubernetes 上的電商 checkout&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/monitoring-inventory-matching-engine/" data-link-title="4.0.4 以監控鏈盤點交易撮合系統：觀察者讀數字的位置、沒有新委託時分辨市場安靜與路徑停住、分段量測延遲，以及委託序號、帳戶部位的對帳與其他監控鏈事前偵測不到的異常" data-link-desc="把一個交易撮合系統的監控放進監控鏈的各層判讀缺口：撮合引擎單一執行緒時觀察者為什麼要從輸出日誌讀數字、沒有新委託時怎麼分辨市場安靜還是處理路徑停住、委託延遲怎麼依段量測與時鐘同步的前提、依序日誌多數節點與同一可用區的失效域，以及輸出序號的斷號、帳戶部位與行情資料的一致性這類監控鏈事前偵測不到的異常">4.0.4 以監控鏈盤點交易撮合系統&lt;/a>。那幾篇引用的請求量、票數這類規模數字是本篇的假設，用來讓例子有具體的量，不是任何真實服務的數字；&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/host-traffic-resource-monitoring/" data-link-title="4.26 單機容器服務的流量與資源監控：各層的訊號、Prometheus 與 exporter、監控所在層的盲區" data-link-desc="健康檢查之外的流量與資源監控：入口、應用程式、容器、主機與資料庫等服務內部各層的訊號從哪裡收、用哪一個 exporter，Prometheus 加 exporter、單一 agent、開箱即用工具與臨時查看指令的取捨，單次取樣與時間窗的差別，監控裝在虛擬機裡時看不到主機的 CPU 負載、看到的磁碟要分清是哪一顆，以及每接上一個來源的驗收與偵測容器停止的寫法；cAdvisor 與 node_exporter 在 OrbStack 上實測過">4.26 單機容器服務的流量與資源監控&lt;/a> 到 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/single-host-monitoring-outside-view/" data-link-title="4.28 單台主機的監控盲區與失效域以外的觀察者：主機停機、使用者路徑中斷與告警鏈中斷，心跳監看與外部探測各自回答的問題與位置，以及與內部探測並排的判讀" data-link-desc="服務與監控放在同一台主機時監控結構上看不到的故障：主機整台停掉、使用者到主機的路徑中斷、告警鏈停掉與通知送不出去；從失效域以外送出訊號的心跳監看與外部探測各自回答的問題與成立條件，觀察者由第三方服務、另一個失效域的主機（含兩台主機互相監看）或另一個業者的排程平台提供時的差別，以及心跳監看、外部探測與主機內部探測並排時的判讀">4.28 單台主機的監控盲區與失效域以外的觀察者&lt;/a> 也用短網址服務當例子，那幾篇的量測數字來自實際的開發環境，不在本篇的假設之內。&lt;/p>
&lt;p>同一個元件在不同需求下的設計不同。售票系統可以讓大多數請求在進資料庫之前就收到「售完」，交易撮合每一筆被接受的委託都要處理；剛建立的短網址轉址讀到幾秒前的舊資料沒有影響，checkout 的付款狀態讀到舊資料會讓使用者重複付款。每個系統都有一件必須一次只處理一筆的事，它的互斥範圍（哪些請求彼此必須輪流處理，定義在 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/load-path-analysis/" data-link-title="9.0 請求路徑上的負載分析：一次一筆的處理、在它之前能回應的請求、多出來的請求怎麼處置、保護把負載轉到哪裡、重送與去重，以及保護元件與依賴故障時的處置" data-link-desc="沿著一個會改變狀態的請求從用戶端到資料庫的路徑分析負載時共用的幾個概念：必須一次只處理一筆的那件事與它的互斥範圍、需要經過它的請求裡結果事先已確定的比例、多出來的請求要排隊、拒絕還是請使用者重新確認、每一段保護把負載轉給了哪個元件（保護元件自己的狀態、連線數、輪詢、下游接收端、外部配額）、佇列與重按帶來的重送怎麼去重、保護元件故障或輸出錯的值時下游讀到什麼，以及依賴變慢、部分失效與恢復時的第二波湧入">9.0 請求路徑上的負載分析&lt;/a> 的〈一次一筆的處理與它的互斥範圍〉）各自不同：售票是同一張票（座位）的認領與訂單狀態，撮合是同一個交易對的整本委託簿，checkout 是同一筆訂單的付款狀態，短網址是同一個代碼的分配。&lt;/p>
&lt;p>四個系統共用幾個部署上的前提：都在一個雲端帳號的一個區域裡（短網址是一台主機），有人值班接告警；售票的等候室自建、一次只有一個場次開賣；撮合的用戶端以程式為主，市場是連續撮合。前提不同時，各篇裡依賴它的那幾段會標出來。&lt;/p>
&lt;h2 id="短網址服務">短網址服務&lt;/h2>
&lt;p>短網址服務把一個長網址換成一段短代碼，使用者打開短網址時，服務回 &lt;code>301&lt;/code> 或 &lt;code>302&lt;/code> 轉址到原本的長網址。請求有三種：轉址（以代碼查出長網址，不需要登入）、建立與管理短網址（登入之後建立、修改、停用自己的短網址），以及管理者的後台操作（停用被檢舉的短網址、查看帳號）。轉址的量遠大於其他兩種，示意的比例是轉址與建立一百比一。&lt;/p>
&lt;p>&lt;strong>不能讓步的需求&lt;/strong>：轉址的目標必須是短網址目前登記的那一個長網址；寫錯的轉址會把使用者送到錯的網站，而主機、容器與請求的監控數字都正常。代碼不能重複分配給兩個長網址。帳號只能修改自己的短網址。管理者停用一個被檢舉的短網址之後，要在幾分鐘內（示意）停止轉址，所以轉址回應的快取期限有上限。這個範例允許建立者修改目的地，修改之後要重新做惡意網址檢查並保留修改紀錄；目的地是否允許修改、撤回要多快生效，是短網址服務要先定案的需求決定，見 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/url-shortener-requirements/" data-link-title="0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格" data-link-desc="短網址服務交給工程之前要定案的需求決定：自建或購買、點擊資料的地位、連結的保存承諾、目的地能否修改、建立者的範圍、短碼的可猜測性、網域策略與可用性分級；多數答案由商業模型推出，目的地修改、短碼可猜測性與可用性分級由使用情境決定">0.23 短網址服務的需求定義&lt;/a> 的〈目的地修改的功能需求與風險〉與 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/url-shortener-implementation/" data-link-title="0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄" data-link-desc="以需求已定案為前提的實作取捨：建立與轉址兩條路徑的流量形狀、各種短碼生成方式的可猜測性與協調成本、轉址狀態碼與各層快取怎麼影響停用或修改連結多久生效、點擊事件的非同步紀錄、濫用防護、服務結束時的靜態轉址與網域去向，以及換掉任一項需求決定時要重選的實作位置">0.24 短網址服務的實作&lt;/a> 的〈各層快取與撤回所需的時間〉。&lt;/p>
&lt;p>&lt;strong>可以讓步的需求&lt;/strong>：剛建立的短網址在幾秒內轉址失敗，影響的是一個剛建好、還沒散出去的連結；轉址的延遲多個幾十毫秒，使用者感覺不到。點擊次數的統計可以延遲、可以少算幾筆。&lt;/p>
&lt;p>&lt;strong>規模假設（示意）&lt;/strong>：每秒數百次轉址，尖峰是平常的數倍（一則被大量轉貼的連結）；短網址總數在百萬筆的量級。&lt;/p>
&lt;p>&lt;strong>起點架構&lt;/strong>：一台主機上以 Docker Compose 跑入口 Nginx、後端程式、PostgreSQL 與 Valkey，各在一個容器裡；Valkey 快取代碼到長網址的對應。監控的分層以它為例，見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/monitoring-layers/" data-link-title="4.0 服務端監控的分層：容器與程序的自檢、服務網路內的探測、元件指標、應用層訊號、告警規則與分送、告警鏈的監看與使用者路徑的合成監控" data-link-desc="以每一層監控檢查的對象、看不到的範圍與接手的那一層，把服務端監控串成一條監控鏈，以及拿它盤點一個服務時怎麼判讀缺口的性質（檢查範圍、位置、讀取與訊號失真），涵蓋自架主機、Kubernetes 與雲端託管的情形；使用者路徑那一層另寫合成監控與真實使用者監測的差別，以及盤點時各種缺口的補法">4.0 服務端監控的分層&lt;/a>，實際的盤點見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/monitoring-inventory-single-host/" data-link-title="4.0.2 以監控鏈盤點單機短網址服務：各層現有的監控、缺口的性質與補法，以及監控鏈事前偵測不到的異常" data-link-desc="把一台主機上以 Docker Compose 跑的短網址服務現有的每一種監控放進監控鏈的各層，逐層判讀還開著的缺口屬於檢查範圍盲區、位置盲區、讀取缺口還是訊號失真，各自的補法，以及補上失效域以外的觀察者之前，主機整台停掉時沒有任何一層會通知的情形；另逐類查業務結果、資料正確性、安全、成本、到期事件與備份">4.0.2 以監控鏈盤點單機短網址服務&lt;/a>。&lt;/p>
&lt;h2 id="售票搶購">售票搶購&lt;/h2>
&lt;p>售票系統在指定的開賣時間讓使用者選場次、選座位或票種、下單並在期限內付款。請求有四種：讀取場次與座位圖、排隊或取得購票資格、扣庫存下單、付款與付款結果的回呼。開賣前幾秒到開賣後幾分鐘的請求量是平常的數百倍，票賣完之後請求量迅速回落。&lt;/p>
&lt;p>&lt;strong>不能讓步的需求&lt;/strong>：不超賣，賣出的票數不能超過場次的票數，同一個座位不能賣給兩個人。付款成功的訂單一定要有票。付款期限（示意：十分鐘）到期而未付款的票要放回可售，放回與付款不能同時成立。&lt;/p>
&lt;p>&lt;strong>可以讓步的需求&lt;/strong>：使用者可以接受排隊幾分鐘，等候期間看到自己的位置即可；座位圖上的剩餘數量可以落後幾秒，因為最後的判斷在扣庫存那一步；票在開賣後的前幾秒沒有全部賣出（少賣），只要之後回收得回來，是可以接受的短暫狀態。誰先買到票的順序依業務決定（先到先得或抽籤），而不論哪一種，規則要事先公布並照做。&lt;/p>
&lt;p>&lt;strong>成功請求的總數有上限&lt;/strong>：成功的購票總數不會超過票數。十萬個帳號搶五千張票、每個帳號限購一張時（示意），票賣完那一刻，其餘九萬五千人的購票請求結果已經確定是售完（直到付款期限到期、有票被放回為止）。&lt;/p>
&lt;p>&lt;strong>規模假設（示意）&lt;/strong>：一個場次五千張票，開賣時十萬人同時在線；開賣後第一分鐘各種請求加起來每秒數萬個，其中扣庫存的請求受排隊放行的速率限制。&lt;/p>
&lt;p>&lt;strong>起點架構&lt;/strong>：CDN 與入口、無狀態的 API 服務（可水平擴充）、排隊服務、關聯式資料庫存放每張票一列與訂單、第三方金流處理付款、排程工作處理付款期限。站內的對照案例有 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元把售票系統搬到 AWS 後，30 分鐘內把資源擴到 130 倍（一場熱門活動擴到 800 台 EC2），處理 10 萬以上同時在線與每秒 2,500 筆以上訂單；資料庫改用 DynamoDB">9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &amp;#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &amp;#43; Lambda Bouncer 實作 flash-sale 的准入控制（等候室）、取代第三方 waiting room 服務">9.C16 SeatGeek：DynamoDB + Lambda 打造的虛擬等候室&lt;/a>；負載路徑的分析，包括什麼條件下才在扣庫存前面加一道 Redis 閘門，見 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/ticketing-load-path/" data-link-title="9.0.1 售票搶購的負載路徑：熱點列與連線池、CDN 合併回源、排隊放行的速率、資料庫認領與加入 Redis 閘門的條件、付款期限的條件更新" data-link-desc="以售票搶購為例，沿著一張票從座位圖到付款記帳的路徑，寫出全部請求更新同一個庫存計數時熱點列與連線池的耗盡順序，邊緣快取、排隊放行、資料庫逐張認領與加入 Redis 扣庫存各自把負載轉給了哪一段、自己出錯時由哪一段補救，以及成功請求的總數有上限、延遲不改變請求的價值、先到先得這幾個前提讓哪些做法可以簡化、哪些需求無法簡化">9.0.1 售票搶購的負載路徑&lt;/a>。&lt;/p>
&lt;h2 id="電商-checkout">電商 checkout&lt;/h2>
&lt;p>電商的 checkout 讓使用者把購物車裡的商品下單、付款，之後由下游寄送確認信、更新庫存與報表。請求有：讀取商品與價格、建立訂單、送出付款、接收金流業者的付款結果回呼；訂單建立之後的通知、出貨與報表由訊息佇列的消費端非同步處理。這是 &lt;a href="https://tarrragon.github.io/blog/backend/reading-paths/" data-link-title="Backend 教材的閱讀路線：教材的分層、依讀者目的排的模組順序與貫穿各模組的 checkout 範例流程" data-link-desc="Backend 教材各模組之間的閱讀順序：分類語言、服務路徑與具體工具的分層，依讀者目的（把服務上線、設計 API、學 production 操作、看懂資安控制、比較工具、讓服務撐住成長）排的路線，以及各模組在一條 checkout 流程裡處理的問題">Backend 教材的閱讀路線&lt;/a> 裡貫穿各模組的 checkout 流程。&lt;/p></description><content:encoded><![CDATA[<p>這篇定義四個範例系統：短網址、售票搶購、電商 checkout 與交易撮合，供幾篇沿著具體系統的請求路徑逐段分析的文章共用，目前有 <a href="/blog/backend/09-performance-capacity/load-path-analysis/" data-link-title="9.0 請求路徑上的負載分析：一次一筆的處理、在它之前能回應的請求、多出來的請求怎麼處置、保護把負載轉到哪裡、重送與去重，以及保護元件與依賴故障時的處置" data-link-desc="沿著一個會改變狀態的請求從用戶端到資料庫的路徑分析負載時共用的幾個概念：必須一次只處理一筆的那件事與它的互斥範圍、需要經過它的請求裡結果事先已確定的比例、多出來的請求要排隊、拒絕還是請使用者重新確認、每一段保護把負載轉給了哪個元件（保護元件自己的狀態、連線數、輪詢、下游接收端、外部配額）、佇列與重按帶來的重送怎麼去重、保護元件故障或輸出錯的值時下游讀到什麼，以及依賴變慢、部分失效與恢復時的第二波湧入">9.0 請求路徑上的負載分析</a> 的舉例、<a href="/blog/backend/09-performance-capacity/ticketing-load-path/" data-link-title="9.0.1 售票搶購的負載路徑：熱點列與連線池、CDN 合併回源、排隊放行的速率、資料庫認領與加入 Redis 閘門的條件、付款期限的條件更新" data-link-desc="以售票搶購為例，沿著一張票從座位圖到付款記帳的路徑，寫出全部請求更新同一個庫存計數時熱點列與連線池的耗盡順序，邊緣快取、排隊放行、資料庫逐張認領與加入 Redis 扣庫存各自把負載轉給了哪一段、自己出錯時由哪一段補救，以及成功請求的總數有上限、延遲不改變請求的價值、先到先得這幾個前提讓哪些做法可以簡化、哪些需求無法簡化">9.0.1 售票搶購的負載路徑</a>、<a href="/blog/backend/09-performance-capacity/matching-engine-load-path/" data-link-title="9.0.2 交易撮合的負載路徑：閘道的帳戶限流與明確拒絕、依序日誌的複寫延遲、單一寫入者的撮合引擎、行情的合併送出與重放驗證容量" data-link-desc="以交易撮合為例，沿著一筆委託從入口閘道到成交回報的路徑，寫出撮合之前結果無法確定、處理順序決定成交、延遲改變請求的價值這些性質讓哪些負載保護不能用，閘道、依序日誌、單一寫入者的撮合引擎與行情發布各自先耗盡的資源與出錯時的補救，過載時明確拒絕超量委託與以重放驗證容量的做法，並與售票搶購對照">9.0.2 交易撮合的負載路徑</a>、<a href="/blog/backend/04-observability/monitoring-inventory-single-host/" data-link-title="4.0.2 以監控鏈盤點單機短網址服務：各層現有的監控、缺口的性質與補法，以及監控鏈事前偵測不到的異常" data-link-desc="把一台主機上以 Docker Compose 跑的短網址服務現有的每一種監控放進監控鏈的各層，逐層判讀還開著的缺口屬於檢查範圍盲區、位置盲區、讀取缺口還是訊號失真，各自的補法，以及補上失效域以外的觀察者之前，主機整台停掉時沒有任何一層會通知的情形；另逐類查業務結果、資料正確性、安全、成本、到期事件與備份">4.0.2 以監控鏈盤點單機短網址服務</a> 、<a href="/blog/backend/04-observability/monitoring-inventory-k8s-checkout/" data-link-title="4.0.3 以監控鏈盤點 Kubernetes 上的電商 checkout：叢集內的監控、託管資料庫與第三方金流各自的失效域，以及佇列消費端與外部依賴的缺口" data-link-desc="把一個跑在託管 Kubernetes 上、有訊息佇列消費端、託管資料庫與第三方金流的電商 checkout 現有的監控放進監控鏈的各層，逐層判讀缺口與補法；另寫付款服務的 readiness 納入金流業者時對付款回呼的影響、失效域從主機換成叢集與雲端帳號之後觀察者要放在哪裡，以及業務結果、對帳、到期事件、消費端、備份、安全、第三方依賴與成本的逐類盤點">4.0.3 以監控鏈盤點 Kubernetes 上的電商 checkout</a> 與 <a href="/blog/backend/04-observability/monitoring-inventory-matching-engine/" data-link-title="4.0.4 以監控鏈盤點交易撮合系統：觀察者讀數字的位置、沒有新委託時分辨市場安靜與路徑停住、分段量測延遲，以及委託序號、帳戶部位的對帳與其他監控鏈事前偵測不到的異常" data-link-desc="把一個交易撮合系統的監控放進監控鏈的各層判讀缺口：撮合引擎單一執行緒時觀察者為什麼要從輸出日誌讀數字、沒有新委託時怎麼分辨市場安靜還是處理路徑停住、委託延遲怎麼依段量測與時鐘同步的前提、依序日誌多數節點與同一可用區的失效域，以及輸出序號的斷號、帳戶部位與行情資料的一致性這類監控鏈事前偵測不到的異常">4.0.4 以監控鏈盤點交易撮合系統</a>。那幾篇引用的請求量、票數這類規模數字是本篇的假設，用來讓例子有具體的量，不是任何真實服務的數字；<a href="/blog/backend/04-observability/host-traffic-resource-monitoring/" data-link-title="4.26 單機容器服務的流量與資源監控：各層的訊號、Prometheus 與 exporter、監控所在層的盲區" data-link-desc="健康檢查之外的流量與資源監控：入口、應用程式、容器、主機與資料庫等服務內部各層的訊號從哪裡收、用哪一個 exporter，Prometheus 加 exporter、單一 agent、開箱即用工具與臨時查看指令的取捨，單次取樣與時間窗的差別，監控裝在虛擬機裡時看不到主機的 CPU 負載、看到的磁碟要分清是哪一顆，以及每接上一個來源的驗收與偵測容器停止的寫法；cAdvisor 與 node_exporter 在 OrbStack 上實測過">4.26 單機容器服務的流量與資源監控</a> 到 <a href="/blog/backend/04-observability/single-host-monitoring-outside-view/" data-link-title="4.28 單台主機的監控盲區與失效域以外的觀察者：主機停機、使用者路徑中斷與告警鏈中斷，心跳監看與外部探測各自回答的問題與位置，以及與內部探測並排的判讀" data-link-desc="服務與監控放在同一台主機時監控結構上看不到的故障：主機整台停掉、使用者到主機的路徑中斷、告警鏈停掉與通知送不出去；從失效域以外送出訊號的心跳監看與外部探測各自回答的問題與成立條件，觀察者由第三方服務、另一個失效域的主機（含兩台主機互相監看）或另一個業者的排程平台提供時的差別，以及心跳監看、外部探測與主機內部探測並排時的判讀">4.28 單台主機的監控盲區與失效域以外的觀察者</a> 也用短網址服務當例子，那幾篇的量測數字來自實際的開發環境，不在本篇的假設之內。</p>
<p>同一個元件在不同需求下的設計不同。售票系統可以讓大多數請求在進資料庫之前就收到「售完」，交易撮合每一筆被接受的委託都要處理；剛建立的短網址轉址讀到幾秒前的舊資料沒有影響，checkout 的付款狀態讀到舊資料會讓使用者重複付款。每個系統都有一件必須一次只處理一筆的事，它的互斥範圍（哪些請求彼此必須輪流處理，定義在 <a href="/blog/backend/09-performance-capacity/load-path-analysis/" data-link-title="9.0 請求路徑上的負載分析：一次一筆的處理、在它之前能回應的請求、多出來的請求怎麼處置、保護把負載轉到哪裡、重送與去重，以及保護元件與依賴故障時的處置" data-link-desc="沿著一個會改變狀態的請求從用戶端到資料庫的路徑分析負載時共用的幾個概念：必須一次只處理一筆的那件事與它的互斥範圍、需要經過它的請求裡結果事先已確定的比例、多出來的請求要排隊、拒絕還是請使用者重新確認、每一段保護把負載轉給了哪個元件（保護元件自己的狀態、連線數、輪詢、下游接收端、外部配額）、佇列與重按帶來的重送怎麼去重、保護元件故障或輸出錯的值時下游讀到什麼，以及依賴變慢、部分失效與恢復時的第二波湧入">9.0 請求路徑上的負載分析</a> 的〈一次一筆的處理與它的互斥範圍〉）各自不同：售票是同一張票（座位）的認領與訂單狀態，撮合是同一個交易對的整本委託簿，checkout 是同一筆訂單的付款狀態，短網址是同一個代碼的分配。</p>
<p>四個系統共用幾個部署上的前提：都在一個雲端帳號的一個區域裡（短網址是一台主機），有人值班接告警；售票的等候室自建、一次只有一個場次開賣；撮合的用戶端以程式為主，市場是連續撮合。前提不同時，各篇裡依賴它的那幾段會標出來。</p>
<h2 id="短網址服務">短網址服務</h2>
<p>短網址服務把一個長網址換成一段短代碼，使用者打開短網址時，服務回 <code>301</code> 或 <code>302</code> 轉址到原本的長網址。請求有三種：轉址（以代碼查出長網址，不需要登入）、建立與管理短網址（登入之後建立、修改、停用自己的短網址），以及管理者的後台操作（停用被檢舉的短網址、查看帳號）。轉址的量遠大於其他兩種，示意的比例是轉址與建立一百比一。</p>
<p><strong>不能讓步的需求</strong>：轉址的目標必須是短網址目前登記的那一個長網址；寫錯的轉址會把使用者送到錯的網站，而主機、容器與請求的監控數字都正常。代碼不能重複分配給兩個長網址。帳號只能修改自己的短網址。管理者停用一個被檢舉的短網址之後，要在幾分鐘內（示意）停止轉址，所以轉址回應的快取期限有上限。這個範例允許建立者修改目的地，修改之後要重新做惡意網址檢查並保留修改紀錄；目的地是否允許修改、撤回要多快生效，是短網址服務要先定案的需求決定，見 <a href="/blog/backend/00-service-selection/url-shortener-requirements/" data-link-title="0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格" data-link-desc="短網址服務交給工程之前要定案的需求決定：自建或購買、點擊資料的地位、連結的保存承諾、目的地能否修改、建立者的範圍、短碼的可猜測性、網域策略與可用性分級；多數答案由商業模型推出，目的地修改、短碼可猜測性與可用性分級由使用情境決定">0.23 短網址服務的需求定義</a> 的〈目的地修改的功能需求與風險〉與 <a href="/blog/backend/00-service-selection/url-shortener-implementation/" data-link-title="0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄" data-link-desc="以需求已定案為前提的實作取捨：建立與轉址兩條路徑的流量形狀、各種短碼生成方式的可猜測性與協調成本、轉址狀態碼與各層快取怎麼影響停用或修改連結多久生效、點擊事件的非同步紀錄、濫用防護、服務結束時的靜態轉址與網域去向，以及換掉任一項需求決定時要重選的實作位置">0.24 短網址服務的實作</a> 的〈各層快取與撤回所需的時間〉。</p>
<p><strong>可以讓步的需求</strong>：剛建立的短網址在幾秒內轉址失敗，影響的是一個剛建好、還沒散出去的連結；轉址的延遲多個幾十毫秒，使用者感覺不到。點擊次數的統計可以延遲、可以少算幾筆。</p>
<p><strong>規模假設（示意）</strong>：每秒數百次轉址，尖峰是平常的數倍（一則被大量轉貼的連結）；短網址總數在百萬筆的量級。</p>
<p><strong>起點架構</strong>：一台主機上以 Docker Compose 跑入口 Nginx、後端程式、PostgreSQL 與 Valkey，各在一個容器裡；Valkey 快取代碼到長網址的對應。監控的分層以它為例，見 <a href="/blog/backend/04-observability/monitoring-layers/" data-link-title="4.0 服務端監控的分層：容器與程序的自檢、服務網路內的探測、元件指標、應用層訊號、告警規則與分送、告警鏈的監看與使用者路徑的合成監控" data-link-desc="以每一層監控檢查的對象、看不到的範圍與接手的那一層，把服務端監控串成一條監控鏈，以及拿它盤點一個服務時怎麼判讀缺口的性質（檢查範圍、位置、讀取與訊號失真），涵蓋自架主機、Kubernetes 與雲端託管的情形；使用者路徑那一層另寫合成監控與真實使用者監測的差別，以及盤點時各種缺口的補法">4.0 服務端監控的分層</a>，實際的盤點見 <a href="/blog/backend/04-observability/monitoring-inventory-single-host/" data-link-title="4.0.2 以監控鏈盤點單機短網址服務：各層現有的監控、缺口的性質與補法，以及監控鏈事前偵測不到的異常" data-link-desc="把一台主機上以 Docker Compose 跑的短網址服務現有的每一種監控放進監控鏈的各層，逐層判讀還開著的缺口屬於檢查範圍盲區、位置盲區、讀取缺口還是訊號失真，各自的補法，以及補上失效域以外的觀察者之前，主機整台停掉時沒有任何一層會通知的情形；另逐類查業務結果、資料正確性、安全、成本、到期事件與備份">4.0.2 以監控鏈盤點單機短網址服務</a>。</p>
<h2 id="售票搶購">售票搶購</h2>
<p>售票系統在指定的開賣時間讓使用者選場次、選座位或票種、下單並在期限內付款。請求有四種：讀取場次與座位圖、排隊或取得購票資格、扣庫存下單、付款與付款結果的回呼。開賣前幾秒到開賣後幾分鐘的請求量是平常的數百倍，票賣完之後請求量迅速回落。</p>
<p><strong>不能讓步的需求</strong>：不超賣，賣出的票數不能超過場次的票數，同一個座位不能賣給兩個人。付款成功的訂單一定要有票。付款期限（示意：十分鐘）到期而未付款的票要放回可售，放回與付款不能同時成立。</p>
<p><strong>可以讓步的需求</strong>：使用者可以接受排隊幾分鐘，等候期間看到自己的位置即可；座位圖上的剩餘數量可以落後幾秒，因為最後的判斷在扣庫存那一步；票在開賣後的前幾秒沒有全部賣出（少賣），只要之後回收得回來，是可以接受的短暫狀態。誰先買到票的順序依業務決定（先到先得或抽籤），而不論哪一種，規則要事先公布並照做。</p>
<p><strong>成功請求的總數有上限</strong>：成功的購票總數不會超過票數。十萬個帳號搶五千張票、每個帳號限購一張時（示意），票賣完那一刻，其餘九萬五千人的購票請求結果已經確定是售完（直到付款期限到期、有票被放回為止）。</p>
<p><strong>規模假設（示意）</strong>：一個場次五千張票，開賣時十萬人同時在線；開賣後第一分鐘各種請求加起來每秒數萬個，其中扣庫存的請求受排隊放行的速率限制。</p>
<p><strong>起點架構</strong>：CDN 與入口、無狀態的 API 服務（可水平擴充）、排隊服務、關聯式資料庫存放每張票一列與訂單、第三方金流處理付款、排程工作處理付款期限。站內的對照案例有 <a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元把售票系統搬到 AWS 後，30 分鐘內把資源擴到 130 倍（一場熱門活動擴到 800 台 EC2），處理 10 萬以上同時在線與每秒 2,500 筆以上訂單；資料庫改用 DynamoDB">9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構</a> 與 <a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 的准入控制（等候室）、取代第三方 waiting room 服務">9.C16 SeatGeek：DynamoDB + Lambda 打造的虛擬等候室</a>；負載路徑的分析，包括什麼條件下才在扣庫存前面加一道 Redis 閘門，見 <a href="/blog/backend/09-performance-capacity/ticketing-load-path/" data-link-title="9.0.1 售票搶購的負載路徑：熱點列與連線池、CDN 合併回源、排隊放行的速率、資料庫認領與加入 Redis 閘門的條件、付款期限的條件更新" data-link-desc="以售票搶購為例，沿著一張票從座位圖到付款記帳的路徑，寫出全部請求更新同一個庫存計數時熱點列與連線池的耗盡順序，邊緣快取、排隊放行、資料庫逐張認領與加入 Redis 扣庫存各自把負載轉給了哪一段、自己出錯時由哪一段補救，以及成功請求的總數有上限、延遲不改變請求的價值、先到先得這幾個前提讓哪些做法可以簡化、哪些需求無法簡化">9.0.1 售票搶購的負載路徑</a>。</p>
<h2 id="電商-checkout">電商 checkout</h2>
<p>電商的 checkout 讓使用者把購物車裡的商品下單、付款，之後由下游寄送確認信、更新庫存與報表。請求有：讀取商品與價格、建立訂單、送出付款、接收金流業者的付款結果回呼；訂單建立之後的通知、出貨與報表由訊息佇列的消費端非同步處理。這是 <a href="/blog/backend/reading-paths/" data-link-title="Backend 教材的閱讀路線：教材的分層、依讀者目的排的模組順序與貫穿各模組的 checkout 範例流程" data-link-desc="Backend 教材各模組之間的閱讀順序：分類語言、服務路徑與具體工具的分層，依讀者目的（把服務上線、設計 API、學 production 操作、看懂資安控制、比較工具、讓服務撐住成長）排的路線，以及各模組在一條 checkout 流程裡處理的問題">Backend 教材的閱讀路線</a> 裡貫穿各模組的 checkout 流程。</p>
<p><strong>不能讓步的需求</strong>：同一筆訂單只能扣款一次，使用者重按付款或用戶端重試都不能多扣；付款成功的訂單不能遺失；訂單金額必須等於使用者確認時看到的金額。</p>
<p><strong>可以讓步的需求</strong>：確認信晚幾分鐘寄出、報表晚一小時更新，使用者與營運都能接受；商品頁的價格快取可以落後，只要下單時以資料庫的價格重新計算並在金額不同時請使用者確認。</p>
<p><strong>成功請求的總數沒有固定上限</strong>：庫存充足時，每一個下單請求都可能成交，擋掉一個就是少一筆營收。</p>
<p><strong>規模假設（示意）</strong>：平日每秒數十筆訂單，促銷活動時是平常的十倍；商品頁的讀取是下單的數百倍。</p>
<p><strong>起點架構</strong>：Kubernetes 上的商品、訂單與付款服務，PostgreSQL 存放訂單，Redis 快取商品與價格，訊息佇列連接訂單服務與通知、出貨、報表的消費端，付款服務呼叫第三方金流並接收回呼。監控的盤點見 <a href="/blog/backend/04-observability/monitoring-inventory-k8s-checkout/" data-link-title="4.0.3 以監控鏈盤點 Kubernetes 上的電商 checkout：叢集內的監控、託管資料庫與第三方金流各自的失效域，以及佇列消費端與外部依賴的缺口" data-link-desc="把一個跑在託管 Kubernetes 上、有訊息佇列消費端、託管資料庫與第三方金流的電商 checkout 現有的監控放進監控鏈的各層，逐層判讀缺口與補法；另寫付款服務的 readiness 納入金流業者時對付款回呼的影響、失效域從主機換成叢集與雲端帳號之後觀察者要放在哪裡，以及業務結果、對帳、到期事件、消費端、備份、安全、第三方依賴與成本的逐類盤點">4.0.3 以監控鏈盤點 Kubernetes 上的電商 checkout</a>。</p>
<h2 id="交易撮合">交易撮合</h2>
<p>交易撮合系統接收買方與賣方的委託，在委託簿裡配對成交。<strong>交易對</strong>（trading pair）是一組可以互相交易的資產，例如 BTC 對 USD；<strong>委託簿</strong>（order book）是一個交易對上還沒成交的買賣委託，依價格與時間排成的清單；撮合依價格與時間的優先順序（price-time priority：價格較好的先成交，同價格時先到的先成交；CME 的文件稱這條規則為 FIFO）把新進的委託與簿上的對手配對。成交之後，系統送出成交回報給雙方，更新雙方的<strong>部位</strong>（position，帳戶持有的各項資產數量），並把行情（最新成交價、各價位的委託量）送給訂閱者。請求有：送出委託、取消委託、查詢帳戶與委託狀態、訂閱行情。</p>
<p><strong>不能讓步的需求</strong>：每一筆被接受的委託都要處理，處理不了的要在接受之前明確拒絕；處理的順序必須與到達撮合引擎的順序一致，因為順序決定誰成交；同一筆委託不能執行兩次；已回應「已接受」的委託不能遺失。</p>
<p><strong>可以讓步的需求</strong>：行情送給處理不及的訂閱者時，可以合併中間的報價更新只送最新的狀態；帳戶的歷史查詢可以讀稍舊的副本。</p>
<p><strong>延遲改變請求的價值</strong>：在連續撮合的市場裡（委託一到就與簿上的對手配對），一筆委託在佇列裡多等一秒，送進委託簿時市場已經不是使用者下單時的市場。市價單會以偏離預期的價格成交；限價單不會比限價差，代價是掛著的價格已經過時、在市場變動後被以不利的時機成交。</p>
<p><strong>規模假設（示意）</strong>：一個熱門交易對每秒數萬筆委託與取消，市場劇烈波動時是平常的十倍以上；撮合的延遲目標在毫秒以下。</p>
<p><strong>起點架構</strong>：入口閘道（驗證身分、檢查餘額與帳戶的速率限制）、依交易對分開的撮合引擎、委託先寫入依序編號的日誌再撮合、行情發布服務、帳戶與歷史資料存放在資料庫。站內的對照案例有 <a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計</a>，負載路徑的分析見 <a href="/blog/backend/09-performance-capacity/matching-engine-load-path/" data-link-title="9.0.2 交易撮合的負載路徑：閘道的帳戶限流與明確拒絕、依序日誌的複寫延遲、單一寫入者的撮合引擎、行情的合併送出與重放驗證容量" data-link-desc="以交易撮合為例，沿著一筆委託從入口閘道到成交回報的路徑，寫出撮合之前結果無法確定、處理順序決定成交、延遲改變請求的價值這些性質讓哪些負載保護不能用，閘道、依序日誌、單一寫入者的撮合引擎與行情發布各自先耗盡的資源與出錯時的補救，過載時明確拒絕超量委託與以重放驗證容量的做法，並與售票搶購對照">9.0.2 交易撮合的負載路徑</a>，監控的盤點見 <a href="/blog/backend/04-observability/monitoring-inventory-matching-engine/" data-link-title="4.0.4 以監控鏈盤點交易撮合系統：觀察者讀數字的位置、沒有新委託時分辨市場安靜與路徑停住、分段量測延遲，以及委託序號、帳戶部位的對帳與其他監控鏈事前偵測不到的異常" data-link-desc="把一個交易撮合系統的監控放進監控鏈的各層判讀缺口：撮合引擎單一執行緒時觀察者為什麼要從輸出日誌讀數字、沒有新委託時怎麼分辨市場安靜還是處理路徑停住、委託延遲怎麼依段量測與時鐘同步的前提、依序日誌多數節點與同一可用區的失效域，以及輸出序號的斷號、帳戶部位與行情資料的一致性這類監控鏈事前偵測不到的異常">4.0.4 以監控鏈盤點交易撮合系統</a>。</p>
<h2 id="四個系統在請求性質上的差別">四個系統在請求性質上的差別</h2>
<p>四個系統在下面三個性質上的位置。第一欄只對要經過那件一次一筆處理的請求有意義（購票、下單、委託、建立短網址），轉址、查商品這類讀取不在其中：</p>
<table>
  <thead>
      <tr>
          <th>系統</th>
          <th>成功請求的總數</th>
          <th>延遲是否改變請求的價值</th>
          <th>同一個請求被處理兩次的後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>短網址</td>
          <td>無上限，每一次建立都要成功</td>
          <td>幾十毫秒內不改變</td>
          <td>轉址重複無害；建立重複會多一個代碼</td>
      </tr>
      <tr>
          <td>售票搶購</td>
          <td>有上限：票數；售完之後的請求結果已確定</td>
          <td>不劃位、固定票價時不改變：排隊幾分鐘之後買到的仍是同一種票、同一個價格；排隊決定的是誰買到；對號入座時改變的是剩下哪些座位，動態定價時連價格也變</td>
          <td>同一人買到兩張或扣兩次款</td>
      </tr>
      <tr>
          <td>電商 checkout</td>
          <td>庫存充足時無上限，每一筆都可能成交</td>
          <td>幾秒內不改變，太久使用者放棄</td>
          <td>重複扣款</td>
      </tr>
      <tr>
          <td>交易撮合</td>
          <td>無上限，每一筆被接受的委託都要處理</td>
          <td>改變：毫秒等級就影響成交價格或成交時機</td>
          <td>重複成交，帳戶部位錯誤</td>
      </tr>
  </tbody>
</table>
<p>三個性質怎麼用來判斷負載保護的方向（第一欄只是可提早回應的比例的一個來源，拍賣就是有上限而比例低的反例；第二欄要以「改變、所以要拒絕」判讀，還要請求處理時不再經使用者確認），在 <a href="/blog/backend/09-performance-capacity/load-path-analysis/" data-link-title="9.0 請求路徑上的負載分析：一次一筆的處理、在它之前能回應的請求、多出來的請求怎麼處置、保護把負載轉到哪裡、重送與去重，以及保護元件與依賴故障時的處置" data-link-desc="沿著一個會改變狀態的請求從用戶端到資料庫的路徑分析負載時共用的幾個概念：必須一次只處理一筆的那件事與它的互斥範圍、需要經過它的請求裡結果事先已確定的比例、多出來的請求要排隊、拒絕還是請使用者重新確認、每一段保護把負載轉給了哪個元件（保護元件自己的狀態、連線數、輪詢、下游接收端、外部配額）、佇列與重按帶來的重送怎麼去重、保護元件故障或輸出錯的值時下游讀到什麼，以及依賴變慢、部分失效與恢復時的第二波湧入">9.0 請求路徑上的負載分析</a>。延遲改變請求價值的系統不只連續撮合的交易所。廣告的即時競價要在交易平台設定的期限內回出價（期限以毫秒計，各平台不同），晚到的出價直接作廢；叫車的媒合晚了幾秒，司機與乘客的位置都變了。同一個請求被處理兩次有後果的系統，從用戶端到資料庫的每一段都要能辨認重送的請求（<a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">Idempotency</a>）。可提早回應的比例與延遲這兩個性質，加上互斥範圍能縮小到什麼程度，如何決定負載保護的方向，是 <a href="/blog/backend/09-performance-capacity/ticketing-load-path/" data-link-title="9.0.1 售票搶購的負載路徑：熱點列與連線池、CDN 合併回源、排隊放行的速率、資料庫認領與加入 Redis 閘門的條件、付款期限的條件更新" data-link-desc="以售票搶購為例，沿著一張票從座位圖到付款記帳的路徑，寫出全部請求更新同一個庫存計數時熱點列與連線池的耗盡順序，邊緣快取、排隊放行、資料庫逐張認領與加入 Redis 扣庫存各自把負載轉給了哪一段、自己出錯時由哪一段補救，以及成功請求的總數有上限、延遲不改變請求的價值、先到先得這幾個前提讓哪些做法可以簡化、哪些需求無法簡化">9.0.1 售票搶購的負載路徑</a> 與 <a href="/blog/backend/09-performance-capacity/matching-engine-load-path/" data-link-title="9.0.2 交易撮合的負載路徑：閘道的帳戶限流與明確拒絕、依序日誌的複寫延遲、單一寫入者的撮合引擎、行情的合併送出與重放驗證容量" data-link-desc="以交易撮合為例，沿著一筆委託從入口閘道到成交回報的路徑，寫出撮合之前結果無法確定、處理順序決定成交、延遲改變請求的價值這些性質讓哪些負載保護不能用，閘道、依序日誌、單一寫入者的撮合引擎與行情發布各自先耗盡的資源與出錯時的補救，過載時明確拒絕超量委託與以重放驗證容量的做法，並與售票搶購對照">9.0.2 交易撮合的負載路徑</a> 對照的主題：售票的比例高（成功請求的總數有上限，售完之後的請求結果已確定）、延遲不改變價值，撮合兩者都相反。</p>
]]></content:encoded></item><item><title>短網址的商業模型：訂閱、自架、平台內建與廣告插頁各由誰出資維持轉址</title><link>https://tarrragon.github.io/blog/business/case-analyses/url-shortener-business-models/</link><pubDate>Fri, 02 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/business/case-analyses/url-shortener-business-models/</guid><description>&lt;p>這篇拆解短網址這個產品類別裡，各家業者靠誰出的錢維持轉址，以及每一種出資方式對「連結會活多久、點擊會不會被記錄」做了什麼承諾。資料取自各家的方案頁與說明頁、官方公告、新聞報導與一篇學術研究；方案頁與說明頁的內容是 2026-10-02 查閱時的狀態，會隨業者調整而變動，所以文中寫方案之間的結構差異與查閱當日的分級內容、不寫價格，採用前以各家當下的方案頁為準。&lt;/p>
&lt;p>要不要買現成服務、以及這些承諾怎麼轉成自建時的需求規格，見 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/url-shortener-requirements/" data-link-title="0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格" data-link-desc="短網址服務交給工程之前要定案的需求決定：自建或購買、點擊資料的地位、連結的保存承諾、目的地能否修改、建立者的範圍、短碼的可猜測性、網域策略與可用性分級；多數答案由商業模型推出，目的地修改、短碼可猜測性與可用性分級由使用情境決定">0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格&lt;/a>；需求規格怎麼落到實作，見 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/url-shortener-implementation/" data-link-title="0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄" data-link-desc="以需求已定案為前提的實作取捨：建立與轉址兩條路徑的流量形狀、各種短碼生成方式的可猜測性與協調成本、轉址狀態碼與各層快取怎麼影響停用或修改連結多久生效、點擊事件的非同步紀錄、濫用防護、服務結束時的靜態轉址與網域去向，以及換掉任一項需求決定時要重選的實作位置">0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄&lt;/a>。&lt;/p>
&lt;h2 id="轉址功能的邊際成本差異化空間與出資方式的界定">轉址功能的邊際成本、差異化空間與出資方式的界定&lt;/h2>
&lt;p>短網址的核心功能是收到一個短碼、回一個轉址到長網址。這個功能的&lt;a href="https://tarrragon.github.io/blog/business/knowledge-cards/marginal-cost/" data-link-title="Marginal Cost" data-link-desc="說明邊際成本及其對商業模式擴張性的決定作用">邊際成本&lt;/a>接近零：多轉址一次只是多一次資料庫查詢與一個 HTTP 回應。功能本身也沒有差異化的空間——對點擊的人來說，經過哪一家的轉址都是抵達同一個目的地，而 MIT 授權的開源專案 YOURLS 讓任何人都能在自己的主機上架一套。&lt;/p>
&lt;p>功能沒有差異、成本接近零、任何人都能自己架，這是 &lt;a href="https://tarrragon.github.io/blog/business/knowledge-cards/commodity-business/" data-link-title="Commodity Business" data-link-desc="判斷一間公司是否受困於大宗商品結構（產品同質、定價被市場決定、利潤被上下游夾擠）時，辨識 commodity 特徵和突圍路徑的概念">commodity business&lt;/a> 的形狀，而客戶很難只為這類功能本身付錢。早期業者的經歷印證了這一點：TinyURL 在 2002 年推出，2004 年 Wired 的報導記載它的營運成本由 Google AdSense 廣告支付，創辦人表示服務會維持免費，同時計畫對客製化與統計功能收費（&lt;a href="https://www.wired.com/2004/03/honey-i-shrunk-the-url/">Wired, 2004&lt;/a>）；tr.im 在 2009 年宣布關站時，營運方的說法是「沒有辦法靠縮網址賺錢，使用者不會為它付費」，而 Twitter 幾乎已經欽定 bit.ly 為市場的勝出者（&lt;a href="https://www.theregister.com/software/2009/08/10/trim-cuts-air-supply-says-no-money-in-tiny-web-20-pot/660990">The Register, 2009&lt;/a>）。&lt;/p>
&lt;p>業者仍然按每月可建立的連結數計價（Bitly 的方案頁在每一級方案列出每月可建立的 QR code 數與連結數），但連結數只是計量單位，讓客戶升級的理由落在轉址以外的功能上。所以要問的是維持轉址的錢從哪裡來，本篇把答案統稱為&lt;strong>出資方式&lt;/strong>，它由兩件事界定：誰出錢，以及出錢的人買到什麼。出錢的有四方：建立連結的人（付訂閱費給業者）、自架者自己（付主機與網域費）、母公司（以其他業務的預算支付平台內建的短網址）、廣告主（付廣告插頁的廣告費）。向建立連結的人收費的業者，各自用不同的功能當付費理由：點擊分析、品牌網域，或代管開源專案的託管版本；同一家業者可以同時用好幾個理由，Dub 就同時賣分析與託管。和短網址一起出現的 QR code 與 link-in-bio 是點擊分析這個付費理由往外的成長方向；連結能活多久，則取決於每一種出資方式能維持多久。&lt;/p>
&lt;h2 id="點擊資料分析功能的訂閱分級與保留期">點擊資料：分析功能的訂閱分級與保留期&lt;/h2>
&lt;p>每一次轉址都經過短網址服務，所以服務能記下這次點擊的時間、來源、地理位置與裝置類型。這份點擊資料對建立連結的人有價值：行銷團隊靠它判斷哪一個活動、哪一個管道帶來了流量。以點擊分析為核心的業者把這份資料做成訂閱制的 &lt;a href="https://tarrragon.github.io/blog/business/knowledge-cards/saas/" data-link-title="SaaS" data-link-desc="說明雲端訂閱軟體的商業模式與經濟特徵">SaaS&lt;/a>，用免費方案吸引使用者，再依分析能力分級收費，這種「免費方案加付費分級」的定價叫 freemium。&lt;/p>
&lt;p>Bitly 是這一型的代表。它在 2008 年成立，2017 年 Spectrum Equity 對它做了 6,300 萬美元的多數股權投資，投資方的合夥人形容經營團隊在投資之前完成的轉變是「把公司從只有免費服務，轉成快速成長、已經獲利的 freemium SaaS」（&lt;a href="https://www.spectrumequity.com/news/bitly-receives-investment-from-spectrum-equity/">Spectrum Equity, 2017&lt;/a>）。Bitly 的方案頁上，各級方案以每月可建立的連結數計量，同時以點擊資料區隔：免費方案看不到歷史資料，付費方案的資料保留期從 30 天逐級延長到企業方案的 2 年，城市層級與裝置類型的細分要到較高的方案才開放（&lt;a href="https://bitly.com/pages/pricing">Bitly 方案頁&lt;/a>）。以開源起家的 Dub 用同一條軸分級，分析資料的保留期從免費方案的 30 天逐級延長到 Advanced 方案的 5 年，企業方案不設上限（&lt;a href="https://dub.co/pricing/links">Dub 方案頁&lt;/a>）。&lt;/p>
&lt;p>保留期適合當分級的主軸，因為它對客戶的價值隨時間增加：保存越久，能做的跨季、跨年比較越多；而點擊資料按小時彙總之後體積很小，延長保留期對業者的邊際成本低，所以這條分級軸對應的主要是客戶的支付意願，不是業者的成本；業者沒有公開資料保存的成本結構，這是由彙總後的資料量推出的判斷。&lt;/p>
&lt;p>點擊分析型收入對產品的要求是：客戶付費買的是點擊數字，而瀏覽器可能把轉址結果存起來，之後的重複點擊直接在本機轉往目的地、不經過服務；所以轉址回應要限制瀏覽器保存的時間，讓點擊在很短的時間窗內就回到服務被記錄，記下的點擊事件也不能遺失。這個要求轉成需求規格的過程，見 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/url-shortener-requirements/" data-link-title="0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格" data-link-desc="短網址服務交給工程之前要定案的需求決定：自建或購買、點擊資料的地位、連結的保存承諾、目的地能否修改、建立者的範圍、短碼的可猜測性、網域策略與可用性分級；多數答案由商業模型推出，目的地修改、短碼可猜測性與可用性分級由使用情境決定">0.23 短網址服務的需求定義&lt;/a> 的〈點擊資料的地位：付費功能或內部參考，以及對應的規格〉一節。&lt;/p>
&lt;h2 id="網域品牌短網址的定價與切換成本">網域：品牌短網址的定價與切換成本&lt;/h2>
&lt;p>短網址預設使用業者自己的網域（例如 &lt;code>bit.ly/xxxx&lt;/code>），企業客戶通常想換成自己的&lt;strong>品牌網域&lt;/strong>（例如 &lt;code>go.example.com/xxxx&lt;/code>；各家方案頁多稱為「自訂網域」，指的是同一件事）。品牌網域的價值有兩層：點擊的人看得到建立連結的是誰，信任度較高；連結的名聲也和業者共用網域上的其他使用者分開，別人用共用網域散布釣魚連結，不會連累到品牌網域。&lt;/p>
&lt;p>各家對這項功能的定價差異很大。Bitly 把品牌網域放在中階以上的付費方案，多個網域另外加購（&lt;a href="https://bitly.com/pages/pricing">Bitly 方案頁&lt;/a>）；Rebrandly 的免費方案就附一個自訂網域，付費分級改依詳細點擊資料的數量、團隊協作與合規認證（&lt;a href="https://www.rebrandly.com/pricing">Rebrandly 方案頁&lt;/a>）；Short.io 的方案頁直接標明每一個方案都包含自訂網域與點擊分析（&lt;a href="https://short.io/pricing">Short.io 方案頁&lt;/a>）。品牌網域在 Bitly 仍是付費牆，在 Rebrandly、Short.io 這類以品牌網域為主打的業者已經成了吸引使用者的入門功能，付費牆移到了分析與合規。&lt;/p>
&lt;p>品牌網域還改變了客戶的&lt;a href="https://tarrragon.github.io/blog/business/knowledge-cards/switching-cost/" data-link-title="Switching Cost" data-link-desc="說明切換成本如何鞏固客戶留存">切換成本&lt;/a>。連結使用業者的網域時，客戶換業者就得放棄所有已發出的連結，這是一種 &lt;a href="https://tarrragon.github.io/blog/business/knowledge-cards/lock-in/" data-link-title="Lock-in" data-link-desc="說明鎖定效應如何形成護城河">lock-in&lt;/a>；連結使用客戶自己的網域時，客戶把網域的 DNS 指到新業者、匯入&lt;strong>短碼對照表&lt;/strong>（每個短碼對應到哪一個長網址的那份資料；前提是舊業者允許匯出、新業者接受匯入既有短碼），舊連結就能繼續運作。所以品牌網域對客戶是保險，對業者則降低了綁定客戶的力量；免費附自訂網域的業者，等於讓新客戶從第一條連結起就不被綁定。&lt;/p>
&lt;h2 id="相鄰產品qr-code-與-link-in-bio">相鄰產品：QR code 與 link-in-bio&lt;/h2>
&lt;p>轉址本身沒有差異化空間，業者成長的方向是和短網址一起出現的相鄰產品。業者產生的動態 QR code（碼裡編的是短網址，不是目的地）運作方式和短網址相同：印在實體物品上，掃描後同樣經過一次轉址，同樣能記錄掃描資料。link-in-bio 頁面（社群帳號自介欄位裡放的那一條連結，點進去是一頁列出多條連結的頁面）則是短網址往「一條連結承載多個目的地」的延伸。QR code 印上實體物品之後就無法更換，它背後短網址的存續條件和一般連結相同，而且更難補救；停止付費之後印好的 QR code 還轉不轉址，要在簽約前查清楚，見〈訂閱型業者的收入重心與連結承諾的預警訊號〉。&lt;/p>
&lt;p>Bitly 在 2021 年收購 QR code 生成服務 Egoditor，並把產品線合稱為 Customer Connections Platform，涵蓋連結管理、QR code 與 link-in-bio。收購公告附上的規模數字是 Bitly 與 Egoditor 合併後有 32.5 萬名付費客戶、年度經常性收入超過 7,500 萬美元，並且已經獲利（&lt;a href="https://www.prnewswire.com/news-releases/bitly-emerges-as-global-saas-leader-with-acquisition-of-egoditor-301434779.html">收購新聞稿, 2021&lt;/a>）。2023 年 1 月 Bitly 另外公布 2022 年的年度經常性收入超過 1 億美元、客戶超過 50 萬（這份公告沒有區分付費客戶，也沒有提到獲利）（&lt;a href="https://www.prnewswire.com/news-releases/bitly-wraps-2022-surpassing-100m-in-arr-and-500-000-global-customers-301724225.html">Bitly 新聞稿, 2023&lt;/a>）。&lt;/p>
&lt;h2 id="開源核心與自架收入安排與營運責任">開源核心與自架：收入安排與營運責任&lt;/h2>
&lt;p>轉址功能容易實作，也就容易開源。開源方案有兩種收入安排。Dub 採 open core：主要程式碼以 AGPLv3 授權公開，企業版功能放在另一個商業授權的目錄裡，同時提供託管服務收費（&lt;a href="https://github.com/dubinc/dub">Dub GitHub&lt;/a>）。YOURLS 以 MIT 授權發布，沒有付費方案，營運靠贊助（&lt;a href="https://yourls.org/">YOURLS&lt;/a>）。&lt;/p>
&lt;p>選擇自架的組織省下訂閱費，同時接下業者原本承擔的幾件事：網域要自己續約、主機要自己維持運作、濫用要自己處理、點擊資料裡的個人資料要自己負責保管與刪除。網域與主機停了，連結就跟著失效；濫用沒處理，網域就可能被封鎖清單擋下（見〈連結永久性：各出資方式下連結的存續條件〉與〈廣告插頁：廣告抽成的差價與濫用風險〉兩節）。所以自架是把成本從訂閱費換成營運責任，而不是消除成本。&lt;/p>
&lt;h2 id="平台內建平台自己經營短網址的用途與預算來源">平台內建：平台自己經營短網址的用途與預算來源&lt;/h2>
&lt;p>社群平台與電商平台自己經營短網址，不向任何人收費。這類短網址對平台的用處由平台自己的需求決定。&lt;/p>
&lt;p>X（前 Twitter）的說明頁列出它把貼文中的連結一律改寫成 t.co 短網址的理由：讓長網址放得進字數上限；量測點擊次數，當作判斷貼文相關性的品質訊號；比對潛在危險網站的清單，點到時顯示警告頁。使用者不能選擇不被縮短，t.co 也不對外提供一般的縮網址服務（&lt;a href="https://help.x.com/en/using-x/url-shortener">X 說明頁&lt;/a>）。電商的推薦計畫是另一種用途：Amazon Associates 產生的短連結會自動帶入推薦者的追蹤 ID，讓推薦帶來的購買能被歸因、分潤給推薦者（&lt;a href="https://affiliate-program.amazon.com/help/node/topic/GJMMT7G4C8K4Y3AY">Amazon Associates 說明頁&lt;/a>）。各平台縮短連結的理由不同：LinkedIn 的說明頁對自動縮短連結只寫了「方便閱讀」，沒有提到安全掃描（&lt;a href="https://www.linkedin.com/help/linkedin/answer/a521889/short-urls-in-shared-posts">LinkedIn 說明頁&lt;/a>）。&lt;/p></description><content:encoded><![CDATA[<p>這篇拆解短網址這個產品類別裡，各家業者靠誰出的錢維持轉址，以及每一種出資方式對「連結會活多久、點擊會不會被記錄」做了什麼承諾。資料取自各家的方案頁與說明頁、官方公告、新聞報導與一篇學術研究；方案頁與說明頁的內容是 2026-10-02 查閱時的狀態，會隨業者調整而變動，所以文中寫方案之間的結構差異與查閱當日的分級內容、不寫價格，採用前以各家當下的方案頁為準。</p>
<p>要不要買現成服務、以及這些承諾怎麼轉成自建時的需求規格，見 <a href="/blog/backend/00-service-selection/url-shortener-requirements/" data-link-title="0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格" data-link-desc="短網址服務交給工程之前要定案的需求決定：自建或購買、點擊資料的地位、連結的保存承諾、目的地能否修改、建立者的範圍、短碼的可猜測性、網域策略與可用性分級；多數答案由商業模型推出，目的地修改、短碼可猜測性與可用性分級由使用情境決定">0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格</a>；需求規格怎麼落到實作，見 <a href="/blog/backend/00-service-selection/url-shortener-implementation/" data-link-title="0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄" data-link-desc="以需求已定案為前提的實作取捨：建立與轉址兩條路徑的流量形狀、各種短碼生成方式的可猜測性與協調成本、轉址狀態碼與各層快取怎麼影響停用或修改連結多久生效、點擊事件的非同步紀錄、濫用防護、服務結束時的靜態轉址與網域去向，以及換掉任一項需求決定時要重選的實作位置">0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄</a>。</p>
<h2 id="轉址功能的邊際成本差異化空間與出資方式的界定">轉址功能的邊際成本、差異化空間與出資方式的界定</h2>
<p>短網址的核心功能是收到一個短碼、回一個轉址到長網址。這個功能的<a href="/blog/business/knowledge-cards/marginal-cost/" data-link-title="Marginal Cost" data-link-desc="說明邊際成本及其對商業模式擴張性的決定作用">邊際成本</a>接近零：多轉址一次只是多一次資料庫查詢與一個 HTTP 回應。功能本身也沒有差異化的空間——對點擊的人來說，經過哪一家的轉址都是抵達同一個目的地，而 MIT 授權的開源專案 YOURLS 讓任何人都能在自己的主機上架一套。</p>
<p>功能沒有差異、成本接近零、任何人都能自己架，這是 <a href="/blog/business/knowledge-cards/commodity-business/" data-link-title="Commodity Business" data-link-desc="判斷一間公司是否受困於大宗商品結構（產品同質、定價被市場決定、利潤被上下游夾擠）時，辨識 commodity 特徵和突圍路徑的概念">commodity business</a> 的形狀，而客戶很難只為這類功能本身付錢。早期業者的經歷印證了這一點：TinyURL 在 2002 年推出，2004 年 Wired 的報導記載它的營運成本由 Google AdSense 廣告支付，創辦人表示服務會維持免費，同時計畫對客製化與統計功能收費（<a href="https://www.wired.com/2004/03/honey-i-shrunk-the-url/">Wired, 2004</a>）；tr.im 在 2009 年宣布關站時，營運方的說法是「沒有辦法靠縮網址賺錢，使用者不會為它付費」，而 Twitter 幾乎已經欽定 bit.ly 為市場的勝出者（<a href="https://www.theregister.com/software/2009/08/10/trim-cuts-air-supply-says-no-money-in-tiny-web-20-pot/660990">The Register, 2009</a>）。</p>
<p>業者仍然按每月可建立的連結數計價（Bitly 的方案頁在每一級方案列出每月可建立的 QR code 數與連結數），但連結數只是計量單位，讓客戶升級的理由落在轉址以外的功能上。所以要問的是維持轉址的錢從哪裡來，本篇把答案統稱為<strong>出資方式</strong>，它由兩件事界定：誰出錢，以及出錢的人買到什麼。出錢的有四方：建立連結的人（付訂閱費給業者）、自架者自己（付主機與網域費）、母公司（以其他業務的預算支付平台內建的短網址）、廣告主（付廣告插頁的廣告費）。向建立連結的人收費的業者，各自用不同的功能當付費理由：點擊分析、品牌網域，或代管開源專案的託管版本；同一家業者可以同時用好幾個理由，Dub 就同時賣分析與託管。和短網址一起出現的 QR code 與 link-in-bio 是點擊分析這個付費理由往外的成長方向；連結能活多久，則取決於每一種出資方式能維持多久。</p>
<h2 id="點擊資料分析功能的訂閱分級與保留期">點擊資料：分析功能的訂閱分級與保留期</h2>
<p>每一次轉址都經過短網址服務，所以服務能記下這次點擊的時間、來源、地理位置與裝置類型。這份點擊資料對建立連結的人有價值：行銷團隊靠它判斷哪一個活動、哪一個管道帶來了流量。以點擊分析為核心的業者把這份資料做成訂閱制的 <a href="/blog/business/knowledge-cards/saas/" data-link-title="SaaS" data-link-desc="說明雲端訂閱軟體的商業模式與經濟特徵">SaaS</a>，用免費方案吸引使用者，再依分析能力分級收費，這種「免費方案加付費分級」的定價叫 freemium。</p>
<p>Bitly 是這一型的代表。它在 2008 年成立，2017 年 Spectrum Equity 對它做了 6,300 萬美元的多數股權投資，投資方的合夥人形容經營團隊在投資之前完成的轉變是「把公司從只有免費服務，轉成快速成長、已經獲利的 freemium SaaS」（<a href="https://www.spectrumequity.com/news/bitly-receives-investment-from-spectrum-equity/">Spectrum Equity, 2017</a>）。Bitly 的方案頁上，各級方案以每月可建立的連結數計量，同時以點擊資料區隔：免費方案看不到歷史資料，付費方案的資料保留期從 30 天逐級延長到企業方案的 2 年，城市層級與裝置類型的細分要到較高的方案才開放（<a href="https://bitly.com/pages/pricing">Bitly 方案頁</a>）。以開源起家的 Dub 用同一條軸分級，分析資料的保留期從免費方案的 30 天逐級延長到 Advanced 方案的 5 年，企業方案不設上限（<a href="https://dub.co/pricing/links">Dub 方案頁</a>）。</p>
<p>保留期適合當分級的主軸，因為它對客戶的價值隨時間增加：保存越久，能做的跨季、跨年比較越多；而點擊資料按小時彙總之後體積很小，延長保留期對業者的邊際成本低，所以這條分級軸對應的主要是客戶的支付意願，不是業者的成本；業者沒有公開資料保存的成本結構，這是由彙總後的資料量推出的判斷。</p>
<p>點擊分析型收入對產品的要求是：客戶付費買的是點擊數字，而瀏覽器可能把轉址結果存起來，之後的重複點擊直接在本機轉往目的地、不經過服務；所以轉址回應要限制瀏覽器保存的時間，讓點擊在很短的時間窗內就回到服務被記錄，記下的點擊事件也不能遺失。這個要求轉成需求規格的過程，見 <a href="/blog/backend/00-service-selection/url-shortener-requirements/" data-link-title="0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格" data-link-desc="短網址服務交給工程之前要定案的需求決定：自建或購買、點擊資料的地位、連結的保存承諾、目的地能否修改、建立者的範圍、短碼的可猜測性、網域策略與可用性分級；多數答案由商業模型推出，目的地修改、短碼可猜測性與可用性分級由使用情境決定">0.23 短網址服務的需求定義</a> 的〈點擊資料的地位：付費功能或內部參考，以及對應的規格〉一節。</p>
<h2 id="網域品牌短網址的定價與切換成本">網域：品牌短網址的定價與切換成本</h2>
<p>短網址預設使用業者自己的網域（例如 <code>bit.ly/xxxx</code>），企業客戶通常想換成自己的<strong>品牌網域</strong>（例如 <code>go.example.com/xxxx</code>；各家方案頁多稱為「自訂網域」，指的是同一件事）。品牌網域的價值有兩層：點擊的人看得到建立連結的是誰，信任度較高；連結的名聲也和業者共用網域上的其他使用者分開，別人用共用網域散布釣魚連結，不會連累到品牌網域。</p>
<p>各家對這項功能的定價差異很大。Bitly 把品牌網域放在中階以上的付費方案，多個網域另外加購（<a href="https://bitly.com/pages/pricing">Bitly 方案頁</a>）；Rebrandly 的免費方案就附一個自訂網域，付費分級改依詳細點擊資料的數量、團隊協作與合規認證（<a href="https://www.rebrandly.com/pricing">Rebrandly 方案頁</a>）；Short.io 的方案頁直接標明每一個方案都包含自訂網域與點擊分析（<a href="https://short.io/pricing">Short.io 方案頁</a>）。品牌網域在 Bitly 仍是付費牆，在 Rebrandly、Short.io 這類以品牌網域為主打的業者已經成了吸引使用者的入門功能，付費牆移到了分析與合規。</p>
<p>品牌網域還改變了客戶的<a href="/blog/business/knowledge-cards/switching-cost/" data-link-title="Switching Cost" data-link-desc="說明切換成本如何鞏固客戶留存">切換成本</a>。連結使用業者的網域時，客戶換業者就得放棄所有已發出的連結，這是一種 <a href="/blog/business/knowledge-cards/lock-in/" data-link-title="Lock-in" data-link-desc="說明鎖定效應如何形成護城河">lock-in</a>；連結使用客戶自己的網域時，客戶把網域的 DNS 指到新業者、匯入<strong>短碼對照表</strong>（每個短碼對應到哪一個長網址的那份資料；前提是舊業者允許匯出、新業者接受匯入既有短碼），舊連結就能繼續運作。所以品牌網域對客戶是保險，對業者則降低了綁定客戶的力量；免費附自訂網域的業者，等於讓新客戶從第一條連結起就不被綁定。</p>
<h2 id="相鄰產品qr-code-與-link-in-bio">相鄰產品：QR code 與 link-in-bio</h2>
<p>轉址本身沒有差異化空間，業者成長的方向是和短網址一起出現的相鄰產品。業者產生的動態 QR code（碼裡編的是短網址，不是目的地）運作方式和短網址相同：印在實體物品上，掃描後同樣經過一次轉址，同樣能記錄掃描資料。link-in-bio 頁面（社群帳號自介欄位裡放的那一條連結，點進去是一頁列出多條連結的頁面）則是短網址往「一條連結承載多個目的地」的延伸。QR code 印上實體物品之後就無法更換，它背後短網址的存續條件和一般連結相同，而且更難補救；停止付費之後印好的 QR code 還轉不轉址，要在簽約前查清楚，見〈訂閱型業者的收入重心與連結承諾的預警訊號〉。</p>
<p>Bitly 在 2021 年收購 QR code 生成服務 Egoditor，並把產品線合稱為 Customer Connections Platform，涵蓋連結管理、QR code 與 link-in-bio。收購公告附上的規模數字是 Bitly 與 Egoditor 合併後有 32.5 萬名付費客戶、年度經常性收入超過 7,500 萬美元，並且已經獲利（<a href="https://www.prnewswire.com/news-releases/bitly-emerges-as-global-saas-leader-with-acquisition-of-egoditor-301434779.html">收購新聞稿, 2021</a>）。2023 年 1 月 Bitly 另外公布 2022 年的年度經常性收入超過 1 億美元、客戶超過 50 萬（這份公告沒有區分付費客戶，也沒有提到獲利）（<a href="https://www.prnewswire.com/news-releases/bitly-wraps-2022-surpassing-100m-in-arr-and-500-000-global-customers-301724225.html">Bitly 新聞稿, 2023</a>）。</p>
<h2 id="開源核心與自架收入安排與營運責任">開源核心與自架：收入安排與營運責任</h2>
<p>轉址功能容易實作，也就容易開源。開源方案有兩種收入安排。Dub 採 open core：主要程式碼以 AGPLv3 授權公開，企業版功能放在另一個商業授權的目錄裡，同時提供託管服務收費（<a href="https://github.com/dubinc/dub">Dub GitHub</a>）。YOURLS 以 MIT 授權發布，沒有付費方案，營運靠贊助（<a href="https://yourls.org/">YOURLS</a>）。</p>
<p>選擇自架的組織省下訂閱費，同時接下業者原本承擔的幾件事：網域要自己續約、主機要自己維持運作、濫用要自己處理、點擊資料裡的個人資料要自己負責保管與刪除。網域與主機停了，連結就跟著失效；濫用沒處理，網域就可能被封鎖清單擋下（見〈連結永久性：各出資方式下連結的存續條件〉與〈廣告插頁：廣告抽成的差價與濫用風險〉兩節）。所以自架是把成本從訂閱費換成營運責任，而不是消除成本。</p>
<h2 id="平台內建平台自己經營短網址的用途與預算來源">平台內建：平台自己經營短網址的用途與預算來源</h2>
<p>社群平台與電商平台自己經營短網址，不向任何人收費。這類短網址對平台的用處由平台自己的需求決定。</p>
<p>X（前 Twitter）的說明頁列出它把貼文中的連結一律改寫成 t.co 短網址的理由：讓長網址放得進字數上限；量測點擊次數，當作判斷貼文相關性的品質訊號；比對潛在危險網站的清單，點到時顯示警告頁。使用者不能選擇不被縮短，t.co 也不對外提供一般的縮網址服務（<a href="https://help.x.com/en/using-x/url-shortener">X 說明頁</a>）。電商的推薦計畫是另一種用途：Amazon Associates 產生的短連結會自動帶入推薦者的追蹤 ID，讓推薦帶來的購買能被歸因、分潤給推薦者（<a href="https://affiliate-program.amazon.com/help/node/topic/GJMMT7G4C8K4Y3AY">Amazon Associates 說明頁</a>）。各平台縮短連結的理由不同：LinkedIn 的說明頁對自動縮短連結只寫了「方便閱讀」，沒有提到安全掃描（<a href="https://www.linkedin.com/help/linkedin/answer/a521889/short-urls-in-shared-posts">LinkedIn 說明頁</a>）。</p>
<p>平台內建的短網址是平台的<strong>成本中心</strong>：它只產生營運成本、本身不帶進收入，預算由母公司的其他業務支付，換到的點擊訊號、安全掃描與歸因資料都留在平台內部使用。成本中心的存續取決於母公司還需不需要它，goo.gl 的關站過程就是這個存續條件的實例（見〈連結永久性：各出資方式下連結的存續條件〉）。</p>
<h2 id="廣告插頁廣告抽成的差價與濫用風險">廣告插頁：廣告抽成的差價與濫用風險</h2>
<p>廣告插頁型的短網址把付錢的方向反過來：業者付錢給建立連結的人。點擊者點下短網址之後，先看到一頁廣告（插頁），再繼續前往目的地；業者向廣告主收費，再把其中一部分分給建立連結的人。一篇 2014 年發表的研究記錄了這個差價：2013 年 10 月 adf.ly 對美國流量向廣告主收取每千次廣告曝光 5 美元，付給建立連結的人每千位美國訪客 3.94 美元（<a href="https://sites.cs.ucsb.edu/~chris/research/doc/www14_shortads.pdf">Nikiforakis 等, WWW 2014</a>）。現行的同類服務仍公開這種依國家分級的每千次瀏覽付款表（<a href="https://ouo.io/rates">ouo.io 付款頁</a>）。</p>
<p>金錢誘因讓這一型的濫用風險高於其他出資方式。同一篇研究發現，受檢的主要廣告型服務都沒有把插頁上的廣告內容適當隔離，惡意廣告可以控制頁面、把點擊者轉往其他網站；2013 年 1 至 7 月間，自動分析惡意網頁的服務 Wepawet 記錄到 892 條廣告型短網址的插頁因為上面的廣告而被判定為惡意（與最終目的地無關），其中 80.7% 來自 adf.ly。安全廠商 Malwarebytes 在 2017 年也記錄到 adf.ly 經由廣告網路的腳本把使用者轉往惡意廣告網域（<a href="https://www.malwarebytes.com/blog/news/2017/05/roughted-the-anti-ad-blocker-malvertiser">Malwarebytes, 2017</a>）。</p>
<p>短網址被拿來藏匿目的地這件事，讓維基百科這類平台選擇整類封鎖。英文維基百科在垃圾連結白名單的常見請求說明裡，把 adf.ly、bit.ly、TinyURL、youtu.be 等短網址列為幾乎一律拒絕放行，並說明這些網域在全域層級被封鎖，理由是短網址可以被用來繞過維基百科的垃圾連結黑名單（<a href="https://en.wikipedia.org/wiki/MediaWiki_talk:Spam-whitelist/Common_requests">維基百科白名單常見請求</a>）。這個封鎖不分出資方式，正規的分析型業者也一起被擋：濫用者的行為讓整個產品類別的連結都失去了在這類平台上流通的資格。</p>
<h2 id="連結永久性各出資方式下連結的存續條件">連結永久性：各出資方式下連結的存續條件</h2>
<p>連結的持有人不是業者的付費客戶，手上只有短網址，服務停止轉址的那一天，連結就失效，而他們自己沒有任何修復的途徑。所以連結使用業者的網域時，連結能活多久由業者決定（使用客戶自己的品牌網域時，客戶可以把連結搬走，見〈網域：品牌短網址的定價與切換成本〉），而業者能維持多久，取決於有沒有一個出資者在支付轉址的成本。</p>
<p>Google 的 goo.gl 是成本中心型短網址的實例。2018 年 4 月 Google 停止讓新使用者建立 goo.gl 短網址，既有使用者可以繼續使用到 2019 年 3 月，同時承諾所有既有連結會繼續轉址到原本的目的地（<a href="https://developers.googleblog.com/en/transitioning-google-url-shortener-to-firebase-dynamic-links/">Google 公告, 2018</a>）。2024 年 7 月 Google 推翻這個承諾，宣布 goo.gl 連結在 2025 年 8 月 25 日之後停止運作，理由是超過 99% 的連結在公告前一個月內沒有任何活動；2025 年 8 月 1 日公告再度更新，改為只停用 2024 年底沒有活動、且已顯示即將停用提示的連結，其餘連結保留（<a href="https://developers.googleblog.com/en/google-url-shortener-links-will-no-longer-be-available/">Google 公告, 2024 年發布、2025 年更新</a>）。Google 當年建議 goo.gl 使用者遷往的 Firebase Dynamic Links 也在 2025 年 8 月 25 日關閉，所有連結停止運作（<a href="https://firebase.google.com/support/dynamic-links-faq">Firebase FAQ</a>）。2018 年的承諾在 2024 年與 2025 年各被改了一次，最後留下的標準是「這條連結還有沒有人在用」。</p>
<p>找不到收入來源的獨立業者，連結的存續同樣取決於有沒有人支付轉址的成本。tr.im 在 2009 年關站時保證連結至少轉址到當年年底；Bitly 提議代管 tr.im 的對照表，或讓 tr.im 加入封存計畫，tr.im 選擇嘗試出售服務（<a href="https://techcrunch.com/2009/08/10/trim-cuts-off-bitlys-301works-idea-wants-to-sell">TechCrunch, 2009</a>）。同年 Internet Archive 推出封存計畫 301Works：成員定期把短碼對照表交給 Internet Archive 備份，成員關站時，短網址網域的技術控制（不是所有權）移交給 301Works 繼續轉址（<a href="https://archive.org/post/273961/url-shorteners-working-with-internet-archive-for-long-term-preservation">Internet Archive 公告, 2009</a>）。這個安排是讓永久性承諾不依賴業者存續的一種做法：把對照表與網域交到業者以外的人手上。工程面的退場安排（網域的去向、靜態轉址、交給封存機構）見 <a href="/blog/backend/10-system-evolution/namespace-and-domain-disposition/" data-link-title="10.6 命名空間與網域的處置：續約、名稱保留與回收、懸空 DNS" data-link-desc="服務結束時網域與名稱的去向，以及它們決定外部引用是失效還是易手：網域的續約與出售、交給中立方的技術控制、套件與雲端資源名稱釋出後被重新申請與佔位、雲端資源刪除後的懸空 DNS 與子網域接管，以及綁在可過期網域上的帳號身分">10.6 命名空間與網域的處置：續約、名稱保留與回收、懸空 DNS</a> 與 <a href="/blog/backend/10-system-evolution/handover-and-archival/" data-link-title="10.8 移交：資料匯出、開源與交給社群或封存機構" data-link-desc="服務結束時把讓外部引用繼續運作所需的資料、程式碼與網域交出去：交給直接客戶的資料匯出、開源程式碼能不能獨立運作、社群接手需要提供者事先留下什麼、封存機構的兩種取得方式（業者合作交出與外部爬取），以及交不出去的部分">10.8 移交：資料匯出、開源與交給社群或封存機構</a>。</p>
<p>訂閱型業者把永久性寫進方案條款，而且不同方案承諾的內容不同。TinyURL 的方案比較表上，一般方案的連結預設永不過期，大量建立連結的方案預設 90 天到期（<a href="https://tinyurl.com/app/pricing">TinyURL 方案頁</a>）。永久性在這裡是一項可以定價的規格。</p>
<h2 id="訂閱型業者的收入重心與連結承諾的預警訊號">訂閱型業者的收入重心與連結承諾的預警訊號</h2>
<p>本篇對短網址出資方式的判讀依賴兩個假設，各自有可監控的訊號：</p>
<ul>
<li><strong>分析資料是訂閱型業者的主要付費理由</strong>：業者若把分析資料的保留期或細分維度開放給免費方案，或者付費分級的主軸換成別的功能（例如合規認證；查閱當日 QR code 數量已經和連結數並列為分級額度），代表點擊資料的付費價值在下降，收入重心正在移動。</li>
<li><strong>連結永久性的強度跟著出資者走</strong>：成本中心型短網址的母公司公告產品整併或停止新建連結時，是既有連結進入風險期的訊號；訂閱型業者被收購、或方案條款新增預設到期時間時，對已發出連結的承諾也要重新確認。</li>
</ul>
<p>依賴第三方短網址的組織，最低成本的保險是使用自己的品牌網域，並定期匯出短碼對照表：網域在自己手上、對照表也在自己手上時，業者關站、漲價或改條款都只影響轉址由誰執行，連結本身可以搬到另一家業者或自架的服務上。另一件要在簽約前從方案條款查到答案的事，是降級或停止付費之後，業者網域上的既有連結還會不會繼續轉址。</p>
<h2 id="可遷移框架核心功能商品化之後的出資方式與產品承諾">可遷移框架：核心功能商品化之後的出資方式與產品承諾</h2>
<p>短網址的案例可以推廣到其他核心功能已經商品化的產品類別。客戶願意只為核心功能付錢時，周圍的出資方式就不是分析的主線；邊際成本、差異化空間、開源替代品三者都指向「客戶不會只為核心功能付錢」時，要找維持核心功能的錢由誰、為了買到什麼而出，而那個出資方式決定產品要對誰做什麼承諾。</p>
<table>
  <thead>
      <tr>
          <th>出資者與付費理由</th>
          <th>出錢的人買到的內容</th>
          <th>對持有連結的人的承諾</th>
          <th>交給工程的規格</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>建立連結的人，為點擊分析付費</td>
          <td>分析與保留期</td>
          <td>由付費客戶的存續出資</td>
          <td>重複點擊不被瀏覽器快取吞掉、點擊事件不能遺失</td>
      </tr>
      <tr>
          <td>建立連結的人，為品牌網域付費</td>
          <td>信任與名聲隔離</td>
          <td>網域在客戶手上，可以搬到別家</td>
          <td>同一短碼在不同網域是不同連結、客戶網域的 TLS 憑證自動簽發、對照表可匯出</td>
      </tr>
      <tr>
          <td>建立連結的人，為託管服務付費</td>
          <td>免自行維運</td>
          <td>由託管業者的存續出資</td>
          <td>對照表可匯出、可自架的部署形態</td>
      </tr>
      <tr>
          <td>自架者自己</td>
          <td>資料與網域留在自己手上</td>
          <td>由自架者自己續約網域與維持主機決定</td>
          <td>對照表可匯出、可自架的部署形態</td>
      </tr>
      <tr>
          <td>母公司（平台內建）</td>
          <td>訊號、安全掃描與歸因</td>
          <td>由母公司的需求決定，需求消失時可能收回</td>
          <td>建立時與轉址當下的惡意網址檢查、點擊記錄</td>
      </tr>
      <tr>
          <td>廣告主（廣告插頁）</td>
          <td>廣告曝光，分潤給建立者</td>
          <td>無明確承諾，且金錢誘因吸引濫用</td>
          <td>插頁隔離與濫用處置，否則整類被封鎖</td>
      </tr>
  </tbody>
</table>
<p>表中各列交給工程的規格，在 <a href="/blog/backend/00-service-selection/url-shortener-requirements/" data-link-title="0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格" data-link-desc="短網址服務交給工程之前要定案的需求決定：自建或購買、點擊資料的地位、連結的保存承諾、目的地能否修改、建立者的範圍、短碼的可猜測性、網域策略與可用性分級；多數答案由商業模型推出，目的地修改、短碼可猜測性與可用性分級由使用情境決定">0.23 短網址服務的需求定義</a> 轉成需求決定：</p>
<ul>
<li>為點擊分析付費那一列 → 〈點擊資料的地位：付費功能或內部參考，以及對應的規格〉</li>
<li>為品牌網域付費那一列 → 〈自訂網域與網域名聲的隔離〉</li>
<li>母公司那一列 → 〈自建或購買：自建成立的理由與自建之後的長期成本〉</li>
<li>廣告主那一列的濫用處置 → 〈建立者的範圍與濫用處理〉</li>
<li>「對持有連結的人的承諾」這一欄 → 〈連結的保存期限與保存承諾〉</li>
</ul>
<p>需求定義與實作兩篇以自建短網址服務的營運者為視角，涵蓋自用的組織與把短網址當產品賣的業者；為託管服務付費那一列的「可自架的部署形態」是開源專案發布給別人自架時的規格，廣告主那一列的「插頁隔離」只在經營廣告插頁時需要，兩篇都只涵蓋其中的對照表匯出與濫用處置；自架者自己那一列對應需求定義篇的〈自建或購買：自建成立的理由與自建之後的長期成本〉。</p>
]]></content:encoded></item><item><title>0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/url-shortener-requirements/</link><pubDate>Fri, 02 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/url-shortener-requirements/</guid><description>&lt;p>「做一個短網址服務」這項需求在交給工程實作之前，有幾個決定要先定案。多數決定的答案從商業模型推出，少數由使用情境決定，每一項又落到實作的某一個位置：商業模型的分類在 &lt;a href="https://tarrragon.github.io/blog/business/case-analyses/url-shortener-business-models/" data-link-title="短網址的商業模型：訂閱、自架、平台內建與廣告插頁各由誰出資維持轉址" data-link-desc="拆解維持短網址轉址的錢從哪裡來：建立連結的人為點擊分析、品牌網域或託管服務付訂閱費，自架者自己付主機與網域，平台由母公司出資，廣告插頁由廣告主出資；連結能活多久跟著出資者走，各出資方式交給工程的規格也不同">短網址的商業模型：訂閱、自架、平台內建與廣告插頁各由誰出資維持轉址&lt;/a>，實作的取捨在 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/url-shortener-implementation/" data-link-title="0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄" data-link-desc="以需求已定案為前提的實作取捨：建立與轉址兩條路徑的流量形狀、各種短碼生成方式的可猜測性與協調成本、轉址狀態碼與各層快取怎麼影響停用或修改連結多久生效、點擊事件的非同步紀錄、濫用防護、服務結束時的靜態轉址與網域去向，以及換掉任一項需求決定時要重選的實作位置">0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄&lt;/a>（下文稱實作篇），本篇處理兩者之間的轉換。&lt;/p>
&lt;p>短網址的核心功能——收到一個短碼、轉往它對應的長網址——用幾十行程式就寫得出來，所以這個需求的難度集中在功能周圍的承諾：連結會活多久、點擊有沒有被記下來、被人拿去散布釣魚網址時誰處理。這些承諾在需求文件裡常常沒有被寫出來，而工程實作的每一個關鍵選擇都依賴它們。&lt;/p>
&lt;h2 id="自建或購買自建成立的理由與自建之後的長期成本">自建或購買：自建成立的理由與自建之後的長期成本&lt;/h2>
&lt;p>要不要自建要最先定案，因為買現成服務時，轉址、快取與濫用處理的多數決定由供應商做；買方仍然要決定三件事：用不用自己的品牌網域、能不能定期匯出短碼對照表、降級或停止付費之後既有連結還轉不轉址。這三項決定換業者或停止付費時，已發出的連結還能不能用，判讀見&lt;a href="https://tarrragon.github.io/blog/business/case-analyses/url-shortener-business-models/" data-link-title="短網址的商業模型：訂閱、自架、平台內建與廣告插頁各由誰出資維持轉址" data-link-desc="拆解維持短網址轉址的錢從哪裡來：建立連結的人為點擊分析、品牌網域或託管服務付訂閱費，自架者自己付主機與網域，平台由母公司出資，廣告插頁由廣告主出資；連結能活多久跟著出資者走，各出資方式交給工程的規格也不同">短網址的商業模型&lt;/a>的〈訂閱型業者的收入重心與連結承諾的預警訊號〉一節與 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型&lt;/a> 的可遷出保險。現成的短網址 SaaS 提供建立、轉址、點擊分析、自訂網域與 API，而且承擔了濫用處理與長期營運，判斷方式和其他能力相同（見 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/capability-buy-vs-build/" data-link-title="0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型" data-link-desc="在交付形態決定整個系統要不要自建之後、逐能力判斷該外包還是自建：辨識 managed 基礎設施、feature SaaS 與 BaaS bundle 三種外包深度、no-code 到 dev-tool 的服務光譜、買 vs 建判斷標準與權重浮動、整合接縫與遷出代價">0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型&lt;/a>）：差異化不在這項能力上，就先買。這一項的依據來自商業模型：短網址在產品裡承擔什麼角色，決定它值不值得自己營運。&lt;/p>
&lt;p>行銷團隊要為活動產生可追蹤的連結、客服要在簡訊裡放短網址，這類需求是 SaaS 的標準用途，自建換來的只有營運負擔。自建成立的理由通常落在下列幾種之一：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>短網址是產品本身的一部分&lt;/strong>：社群平台把使用者貼文裡的每一個外部連結改寫成自己網域的短網址，目的是在轉址當下掃描惡意網址、記錄哪一則貼文帶出了多少點擊。這些資料是平台自己的產品資料，交給第三方就失去了。&lt;/li>
&lt;li>&lt;strong>點擊資料不能離開組織&lt;/strong>：連結的目的地或點擊者的資訊受法規或合約限制，不能存放在外部服務。例如醫療院所在就診提醒簡訊裡放的連結，目的地網址帶著病患的預約編號。&lt;/li>
&lt;li>&lt;strong>需要 SaaS 不提供的轉址邏輯&lt;/strong>：例如依裝置或地區導向不同目的地、深層連結到 App。&lt;/li>
&lt;li>&lt;strong>用量大到按量計價不划算&lt;/strong>：SaaS 多半依連結數或點擊數分級收費，用量到某個規模之後，自建的營運成本低於訂閱費。這個交叉點要拿實際的報價與預估用量算，不同供應商的級距差異很大。&lt;/li>
&lt;/ul>
&lt;p>自建也有不寫程式的做法：在自己的主機上部署開源的短網址專案（MIT 授權的 YOURLS、AGPLv3 的 Dub）。「點擊資料不能離開組織」與「用量大到按量計價不划算」這兩個理由，多數用自架就能滿足：點擊資料留在組織內，費用從訂閱費換成營運責任。只有連自架版本的資料模型或擴充點都不夠時，才走實作篇的整套實作路線；交付形態的完整光譜見 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型&lt;/a>。&lt;/p>
&lt;p>不論自架或自寫，自建之後的成本都集中在三項長期承諾：連結的永久營運、濫用處理的人力與流程、點擊資料的隱私責任，轉址程式本身佔的比例很小。估算自建成本時把這幾項算進去，交叉點會往後移。&lt;/p>
&lt;p>決定自建的有兩種營運者：自用的組織（為自己的簡訊、行銷或內部系統產生連結），以及把短網址當產品賣給別人的業者。下文的點擊分析收費與自訂網域兩節以業者為主角；自用的組織讀到「客戶」時，對應的是組織內各個建立連結的部門。&lt;/p>
&lt;h2 id="點擊資料的地位付費功能或內部參考以及對應的規格">點擊資料的地位：付費功能或內部參考，以及對應的規格&lt;/h2>
&lt;p>短網址服務每一次轉址都能記下一筆點擊：時間、來源、大致的地理位置、裝置類型。這筆資料在商業模型裡的位置決定了一連串規格。&lt;/p>
&lt;p>點擊分析是付費功能時，點擊資料就是產品。客戶付費買的是準確的數字，少算等於少交付，所以點擊要在很短的時間窗內回到服務被記錄，事件不能遺失，報表要能回答客戶會問的維度（哪一個活動、哪一個國家、哪一天）。點擊統計只是內部參考時，它是副產品，可以接受取樣、可以接受伺服器故障期間遺失還沒送出的點擊事件。&lt;/p>
&lt;p>這一項決定在實作上落到兩個位置。瀏覽器預設會把「永久移動」的轉址長期保存，之後的重複點擊直接在本機轉走、不再送到服務，也就記不到；所以轉址結果能被瀏覽器保存多久要有一個很短的上限，狀態碼與快取標頭怎麼選見實作篇的〈轉址回應的快取標頭與瀏覽器保存回應的時間〉。點擊事件則要寫進不會遺失的佇列，而不是記憶體裡的緩衝。兩者的代價都是服務要承擔幾乎每一次點擊的負載。&lt;/p>
&lt;p>點擊資料裡的 IP 位址與裝置資訊屬於個人資料，所以報表的粒度（國家、城市還是精確位置）、保存期限、客戶能不能下載原始資料，同時也是隱私責任的大小。報表只需要國家時，原始 IP 在彙總後就可以刪除，隱私責任隨之縮小。&lt;/p>
&lt;h2 id="連結的保存期限與保存承諾">連結的保存期限與保存承諾&lt;/h2>
&lt;p>短網址一旦發出去，就被印在海報上、寫進書裡、存進別人的書籤。持有連結的人只有短網址，沒有原本的長網址，服務停止回應的那一天，這些連結就全部失效，而持有連結的人自己沒有任何修復的途徑。所以「連結保存多久」是對所有持有連結的人做的承諾，這些人大多不是服務的客戶，也從來沒有同意過任何條款。&lt;/p>
&lt;p>保存承諾有三個問題沒有預設答案，每一個答案都直接改變持有連結的人點下去之後看到什麼：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>連結有沒有到期時間&lt;/strong>：永久保存，或者在一段時間沒有點擊之後回收。回收可以刪掉長網址與點擊原始資料、控制儲存成本，代價是舊海報上的連結會失效；回收的短碼不能再發給新的連結，否則舊連結的持有人會被帶到別人的網頁，所以短碼本身仍要留著一筆紀錄佔住。承載私密連結的服務，到期時間另外是一項安全控制，見〈互相限制的需求決定〉。&lt;/li>
&lt;li>&lt;strong>建立者刪除連結之後，點擊的人看到什麼&lt;/strong>：一頁說明「這條連結已被移除」，或直接回 404。印在印刷品或書裡的連結失效時，說明頁讓點擊的人知道連結是被刻意移除，而不是自己打錯網址；只在線上流通的連結可以直接回 404。&lt;/li>
&lt;li>&lt;strong>服務本身結束營運時怎麼辦&lt;/strong>：預先公告的期限、能不能讓客戶匯出短碼對照表（每個短碼對應到哪一個長網址的那份資料）、網域在停止營運後由誰續約。短網址活在網域上，網域到期被別人註冊，舊連結就全部落入新註冊者的控制。這三項在服務結束時的做法：公告期見 &lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/staged-shutdown/" data-link-title="10.7 分階段退場：停止新建、唯讀、靜態封存、關閉與公告期" data-link-desc="服務結束時把一次全部關閉拆成幾個階段：停止新建、唯讀、靜態封存與關閉各保留什麼、對哪一類持有者有效、維運成本降到哪裡；依使用紀錄決定保留範圍；推動客戶遷移的手段；以及公告期長度、通知管道與補償的安排">10.7 分階段退場：停止新建、唯讀、靜態封存、關閉與公告期&lt;/a> 的〈公告期的長度與通知管道〉，匯出見 &lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/handover-and-archival/" data-link-title="10.8 移交：資料匯出、開源與交給社群或封存機構" data-link-desc="服務結束時把讓外部引用繼續運作所需的資料、程式碼與網域交出去：交給直接客戶的資料匯出、開源程式碼能不能獨立運作、社群接手需要提供者事先留下什麼、封存機構的兩種取得方式（業者合作交出與外部爬取），以及交不出去的部分">10.8 移交：資料匯出、開源與交給社群或封存機構&lt;/a> 的〈交給直接客戶的資料匯出〉，網域續約見 &lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/namespace-and-domain-disposition/" data-link-title="10.6 命名空間與網域的處置：續約、名稱保留與回收、懸空 DNS" data-link-desc="服務結束時網域與名稱的去向，以及它們決定外部引用是失效還是易手：網域的續約與出售、交給中立方的技術控制、套件與雲端資源名稱釋出後被重新申請與佔位、雲端資源刪除後的懸空 DNS 與子網域接管，以及綁在可過期網域上的帳號身分">10.6 命名空間與網域的處置：續約、名稱保留與回收、懸空 DNS&lt;/a> 的〈網域在服務結束後的去向：續約並回應、續約不回應、到期、出售〉；外部引用的整體盤點見 &lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/service-termination-scope/" data-link-title="10.4 服務終止的範圍：外部引用的種類與持有者" data-link-desc="服務本身要結束時，盤點它發出去、存在別人手上的外部引用：網址、寫在別人程式或韌體裡的端點、被嵌入的程式碼網址、名稱與綁在網域上的身分；持有者依聯絡得到的程度分成直接客戶、間接使用者與第三方持有者；引用在服務結束後的結局：失效或易手">10.4 服務終止的範圍：外部引用的種類與持有者&lt;/a>；短網址的實作見 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/url-shortener-implementation/" data-link-title="0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄" data-link-desc="以需求已定案為前提的實作取捨：建立與轉址兩條路徑的流量形狀、各種短碼生成方式的可猜測性與協調成本、轉址狀態碼與各層快取怎麼影響停用或修改連結多久生效、點擊事件的非同步紀錄、濫用防護、服務結束時的靜態轉址與網域去向，以及換掉任一項需求決定時要重選的實作位置">0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄&lt;/a> 的〈服務結束時的轉址延續〉。&lt;/li>
&lt;/ul>
&lt;p>這項承諾能維持多久，取決於由哪一種商業模型出資。由付費客戶出資的服務，連結的存續與營收綁在一起；作為大公司附屬工具、沒有獨立營收的短網址服務，母公司不再需要它時就可能關閉，實際的關站案例見&lt;a href="https://tarrragon.github.io/blog/business/case-analyses/url-shortener-business-models/" data-link-title="短網址的商業模型：訂閱、自架、平台內建與廣告插頁各由誰出資維持轉址" data-link-desc="拆解維持短網址轉址的錢從哪裡來：建立連結的人為點擊分析、品牌網域或託管服務付訂閱費，自架者自己付主機與網域，平台由母公司出資，廣告插頁由廣告主出資；連結能活多久跟著出資者走，各出資方式交給工程的規格也不同">短網址的商業模型&lt;/a>的〈連結永久性：各出資方式下連結的存續條件〉一節。&lt;/p>
&lt;h2 id="目的地修改的功能需求與風險">目的地修改的功能需求與風險&lt;/h2>
&lt;p>允許建立者在連結發出之後修改目的地，是一項有明確需求的功能。QR code 印在包裝、名片或店面上之後就無法更換，背後的網址能改，同一張印刷品就能在活動結束後改導到新頁面；連結的目的地網站搬家時，修改短網址的目的地就能讓舊連結繼續有效。這一項由使用情境決定：印刷品印出之後無法更換，目的地網站也會搬家。&lt;/p>
&lt;p>同一項功能也是風險。收到連結的人信任它，是基於分享當下它指向的內容；目的地可以修改時，建立者可以先放一個正常頁面讓連結被廣泛分享，再改成釣魚頁面。所以允許修改的同時，要一起定案三件事：修改後要不要重新做惡意網址檢查、修改紀錄要不要保留、要不要讓點擊者能預覽目前的目的地；少了任何一件，修改目的地就成了先騙取信任再換成釣魚頁的工具。&lt;/p>
&lt;p>這一項決定在實作上影響快取。目的地不會變時，轉址結果可以被瀏覽器、CDN 與應用程式快取很久；目的地能改時，每一層快取都要能在修改後失效，否則有人會被送到舊的目的地。教材裡幾篇以短網址為例的盤點與分析，用的是允許修改目的地、撤回在幾分鐘內生效的設定，見 &lt;a href="https://tarrragon.github.io/blog/backend/case-systems/" data-link-title="Backend 範例系統：短網址、售票搶購、電商 checkout 與交易撮合的功能、需求與規模假設" data-link-desc="Backend 教材幾篇以具體系統走完整條請求路徑的文章共用的四個範例系統：每個系統提供的功能與請求種類、不能讓步與可以讓步的需求、規模假設與起點架構，以及四個系統在成功請求的總數是否有上限、延遲是否改變請求的價值、同一個請求被處理兩次的後果上的差別">Backend 範例系統&lt;/a> 的〈短網址服務〉。&lt;/p>
&lt;h2 id="建立者的範圍與濫用處理">建立者的範圍與濫用處理&lt;/h2>
&lt;p>短網址把目的地藏在一個看不出內容的網址後面，這個性質被釣魚與惡意軟體散布者大量利用，以廣告插頁付錢給建立者的服務被濫用得最嚴重（見&lt;a href="https://tarrragon.github.io/blog/business/case-analyses/url-shortener-business-models/" data-link-title="短網址的商業模型：訂閱、自架、平台內建與廣告插頁各由誰出資維持轉址" data-link-desc="拆解維持短網址轉址的錢從哪裡來：建立連結的人為點擊分析、品牌網域或託管服務付訂閱費，自架者自己付主機與網域，平台由母公司出資，廣告插頁由廣告主出資；連結能活多久跟著出資者走，各出資方式交給工程的規格也不同">短網址的商業模型&lt;/a>的〈廣告插頁：廣告抽成的差價與濫用風險〉一節）。開放越多人建立連結，濫用越難追溯到人；而服務的網域被大量用來散布惡意連結之後，郵件過濾器與瀏覽器可能把整個網域列為可疑，所有正常連結跟著被擋下。&lt;/p>
&lt;p>建立者的範圍大致有下列幾種，濫用處理的投入差距很大：&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;li>&lt;strong>經由 API 代客戶的使用者建立&lt;/strong>：客戶把建立功能開放給自己的使用者，濫用只能追溯到客戶的 API 金鑰，處理方式是要求客戶自己做使用者驗證、對每把金鑰設速率上限，並能整批停用某把金鑰建立的連結。&lt;/li>
&lt;/ul>
&lt;p>開放對外建立時，濫用處理有幾個問題沒有預設答案：檢舉從哪裡進來、誰負責判定、確認惡意之後多久之內連結要失效、被停用的連結點下去顯示什麼、執法或法律請求由誰回應。其中失效時限會直接限制工程實作的快取時間，因為已被瀏覽器或 CDN 快取的轉址，在快取到期之前不會回來查詢連結是否已被停用；所以這個數字屬於需求，不屬於技術選擇。檢舉與詐騙回報多半先進到客服，客服怎麼判斷一則回報要當場答覆、開單還是立即呼叫值班，見 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/support-escalation-and-engineering-reply/" data-link-title="8.24 客服回報的處理：客服當場答覆時用的資訊、轉給值班的條件、工程查證後給客服的回覆、查證結果寫回知識文章、告警與查詢工具，以及流程失效時的徵兆" data-link-desc="客服接到使用者回報之後，哪些資訊讓客服當場答覆（進行中的事故、已知錯誤清單、維護與變更公告、錯誤碼對照表、帳號查詢工具），哪些條件要立即呼叫值班、哪些開單在上班時間處理，工程查證之後回覆給客服的內容，同一類回報重複出現時怎麼把查證結果寫回知識文章、告警規則或查詢工具，以及從客服單與事故紀錄看出流程在哪一段失效">8.24 客服回報的處理&lt;/a>。&lt;/p>
&lt;h2 id="短碼的可猜測性與私密連結">短碼的可猜測性與私密連結&lt;/h2>
&lt;p>建立者會把什麼樣的網址拿來縮短，影響短碼本身的規格。這一項由使用情境決定，與收入模型無關。雲端文件的分享連結、私人行事曆的訂閱網址、密碼重設連結，都屬於「知道網址就能存取」的連結。這類連結被縮短之後，短碼能不能被猜到，就成了連結背後的文件、行事曆或帳號能不能被外人取得的問題。&lt;/p>
&lt;p>服務預期承載這類連結時，短碼必須不可預測：工程實作就不能把遞增序號直接編碼成短碼（序號加一就得到別人的連結），要保留序號，編碼前得先用帶金鑰的加密把它打散，否則改用隨機產生。服務只用於公開的行銷頁面時，這項限制可以放寬，換取更短的短碼。&lt;/p>
&lt;h2 id="自訂網域與網域名聲的隔離">自訂網域與網域名聲的隔離&lt;/h2>
&lt;p>所有連結共用服務自己的短網域，是最簡單的做法，代價是名聲共用：一個客戶的濫用行為會讓所有客戶的連結一起被封鎖清單擋下。讓客戶使用自己的網域（例如把 &lt;code>go.example.com&lt;/code> 指到服務），點擊的人看得到品牌，信任度較高，該客戶的連結名聲也和其他客戶分開。多數業者的方案頁稱這項功能為「自訂網域」，商業模型那篇從客戶的角度稱它「品牌網域」，兩者指同一件事。&lt;/p>
&lt;p>自訂網域在商業上常是付費方案的區分點，在工程上帶來網域所有權驗證、每個網域的 TLS 憑證簽發與更新，以及「同一個短碼在不同網域是不同連結」的資料模型變化。要不要支援、由哪個方案提供，以及客戶停止付費之後網域上的連結怎麼處理，三個答案合起來決定自訂網域對客戶是保險還是另一種綁定。&lt;/p>
&lt;h2 id="建立路徑與轉址路徑的可用性分級">建立路徑與轉址路徑的可用性分級&lt;/h2>
&lt;p>短網址服務對外有兩條路徑：&lt;strong>建立路徑&lt;/strong>接收長網址、產生短碼，&lt;strong>轉址路徑&lt;/strong>收到短網址的請求後轉往長網址。兩條路徑對可用性的要求不同，這一項同樣由使用情境決定。建立路徑暫停時，受影響的是正在建立連結的少數人，他們可以稍後再試；轉址路徑暫停時，所有已經發出去的連結同時失效，影響的是持有連結的所有人，而他們沒有替代方案。&lt;/p>
&lt;p>這個排序的前提是建立是一個獨立的操作，由使用者主動送出、失敗可以重試。建立由系統在另一條寫入路徑裡自動觸發時（社群平台在發文當下改寫連結、通知系統在送出簡訊之前產生連結），建立路徑暫停就擋住整批發文或訊息，它的可用性與延遲就是那條路徑的一部分，目標要跟著那條路徑訂；這類服務也常讓發送端在建立失敗時退回使用長網址，或在內容發出後仍可改寫的情境（例如貼文），把建立改成非同步、先用原網址發出再補上短網址。&lt;/p>
&lt;p>所以可用性目標要分開訂（見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO&lt;/a>）：轉址路徑的目標要高於建立路徑，點擊統計與報表的目標可以更低，而且統計的故障不能拖垮轉址（見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/degradation/" data-link-title="Degradation" data-link-desc="說明服務部分能力失效時如何保留核心功能與控制風險">degradation&lt;/a>）。尖峰的估計也以轉址為主：共用連結的尖峰來自單一連結被大量轉發，和每天建立多少連結關係不大；替每位收件人各產生一條的連結，尖峰則跟著每一批發送的收件人數走。客戶若有大型活動（例如電視廣告上出現的短網址），事先通知的管道讓服務能在尖峰之前擴容；沒有預告的爆紅見 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/url-shortener-traffic-surge/" data-link-title="0.25 短網址服務的瞬時大流量：爆紅連結、建立路徑的機器人濫用與轉址路徑的攻擊" data-link-desc="短網址服務遇到瞬時大流量時的處置：單一連結被大量轉發時負載落在哪一層、點擊事件管道的積壓與目的地網站承受不住匯集的點擊，爆紅的連結是惡意連結時快取與撤回的衝突，建立路徑被機器人大量建立連結時的成本、檢查管道與准入手段，以及轉址網域遭受掃描或體積型攻擊">0.25 短網址服務的瞬時大流量：爆紅連結、建立路徑的機器人濫用與轉址路徑的攻擊&lt;/a> 的〈爆紅連結的負載落點：CDN、程序內快取與對照表〉。&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;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;a href="https://tarrragon.github.io/blog/backend/00-service-selection/url-shortener-implementation/" data-link-title="0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄" data-link-desc="以需求已定案為前提的實作取捨：建立與轉址兩條路徑的流量形狀、各種短碼生成方式的可猜測性與協調成本、轉址狀態碼與各層快取怎麼影響停用或修改連結多久生效、點擊事件的非同步紀錄、濫用防護、服務結束時的靜態轉址與網域去向，以及換掉任一項需求決定時要重選的實作位置">實作篇&lt;/a>的〈各項需求決定對應的實作位置〉一節。&lt;/p></description><content:encoded><![CDATA[<p>「做一個短網址服務」這項需求在交給工程實作之前，有幾個決定要先定案。多數決定的答案從商業模型推出，少數由使用情境決定，每一項又落到實作的某一個位置：商業模型的分類在 <a href="/blog/business/case-analyses/url-shortener-business-models/" data-link-title="短網址的商業模型：訂閱、自架、平台內建與廣告插頁各由誰出資維持轉址" data-link-desc="拆解維持短網址轉址的錢從哪裡來：建立連結的人為點擊分析、品牌網域或託管服務付訂閱費，自架者自己付主機與網域，平台由母公司出資，廣告插頁由廣告主出資；連結能活多久跟著出資者走，各出資方式交給工程的規格也不同">短網址的商業模型：訂閱、自架、平台內建與廣告插頁各由誰出資維持轉址</a>，實作的取捨在 <a href="/blog/backend/00-service-selection/url-shortener-implementation/" data-link-title="0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄" data-link-desc="以需求已定案為前提的實作取捨：建立與轉址兩條路徑的流量形狀、各種短碼生成方式的可猜測性與協調成本、轉址狀態碼與各層快取怎麼影響停用或修改連結多久生效、點擊事件的非同步紀錄、濫用防護、服務結束時的靜態轉址與網域去向，以及換掉任一項需求決定時要重選的實作位置">0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄</a>（下文稱實作篇），本篇處理兩者之間的轉換。</p>
<p>短網址的核心功能——收到一個短碼、轉往它對應的長網址——用幾十行程式就寫得出來，所以這個需求的難度集中在功能周圍的承諾：連結會活多久、點擊有沒有被記下來、被人拿去散布釣魚網址時誰處理。這些承諾在需求文件裡常常沒有被寫出來，而工程實作的每一個關鍵選擇都依賴它們。</p>
<h2 id="自建或購買自建成立的理由與自建之後的長期成本">自建或購買：自建成立的理由與自建之後的長期成本</h2>
<p>要不要自建要最先定案，因為買現成服務時，轉址、快取與濫用處理的多數決定由供應商做；買方仍然要決定三件事：用不用自己的品牌網域、能不能定期匯出短碼對照表、降級或停止付費之後既有連結還轉不轉址。這三項決定換業者或停止付費時，已發出的連結還能不能用，判讀見<a href="/blog/business/case-analyses/url-shortener-business-models/" data-link-title="短網址的商業模型：訂閱、自架、平台內建與廣告插頁各由誰出資維持轉址" data-link-desc="拆解維持短網址轉址的錢從哪裡來：建立連結的人為點擊分析、品牌網域或託管服務付訂閱費，自架者自己付主機與網域，平台由母公司出資，廣告插頁由廣告主出資；連結能活多久跟著出資者走，各出資方式交給工程的規格也不同">短網址的商業模型</a>的〈訂閱型業者的收入重心與連結承諾的預警訊號〉一節與 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a> 的可遷出保險。現成的短網址 SaaS 提供建立、轉址、點擊分析、自訂網域與 API，而且承擔了濫用處理與長期營運，判斷方式和其他能力相同（見 <a href="/blog/backend/00-service-selection/capability-buy-vs-build/" data-link-title="0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型" data-link-desc="在交付形態決定整個系統要不要自建之後、逐能力判斷該外包還是自建：辨識 managed 基礎設施、feature SaaS 與 BaaS bundle 三種外包深度、no-code 到 dev-tool 的服務光譜、買 vs 建判斷標準與權重浮動、整合接縫與遷出代價">0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型</a>）：差異化不在這項能力上，就先買。這一項的依據來自商業模型：短網址在產品裡承擔什麼角色，決定它值不值得自己營運。</p>
<p>行銷團隊要為活動產生可追蹤的連結、客服要在簡訊裡放短網址，這類需求是 SaaS 的標準用途，自建換來的只有營運負擔。自建成立的理由通常落在下列幾種之一：</p>
<ul>
<li><strong>短網址是產品本身的一部分</strong>：社群平台把使用者貼文裡的每一個外部連結改寫成自己網域的短網址，目的是在轉址當下掃描惡意網址、記錄哪一則貼文帶出了多少點擊。這些資料是平台自己的產品資料，交給第三方就失去了。</li>
<li><strong>點擊資料不能離開組織</strong>：連結的目的地或點擊者的資訊受法規或合約限制，不能存放在外部服務。例如醫療院所在就診提醒簡訊裡放的連結，目的地網址帶著病患的預約編號。</li>
<li><strong>需要 SaaS 不提供的轉址邏輯</strong>：例如依裝置或地區導向不同目的地、深層連結到 App。</li>
<li><strong>用量大到按量計價不划算</strong>：SaaS 多半依連結數或點擊數分級收費，用量到某個規模之後，自建的營運成本低於訂閱費。這個交叉點要拿實際的報價與預估用量算，不同供應商的級距差異很大。</li>
</ul>
<p>自建也有不寫程式的做法：在自己的主機上部署開源的短網址專案（MIT 授權的 YOURLS、AGPLv3 的 Dub）。「點擊資料不能離開組織」與「用量大到按量計價不划算」這兩個理由，多數用自架就能滿足：點擊資料留在組織內，費用從訂閱費換成營運責任。只有連自架版本的資料模型或擴充點都不夠時，才走實作篇的整套實作路線；交付形態的完整光譜見 <a href="/blog/backend/00-service-selection/delivery-mode-selection/" data-link-title="0.21 交付形態選型：從全託管到自建的光譜與邊界" data-link-desc="在進入資料庫、快取與部署選型之前、先判斷服務該用託管平台（Wix / Shopify / Google Sites）、辦公生態自動化（Apps Script）、BaaS（Firebase）、半託管 CMS（WordPress）還是自建、並為日後遷往自建保留可遷出路徑">0.21 交付形態選型</a>。</p>
<p>不論自架或自寫，自建之後的成本都集中在三項長期承諾：連結的永久營運、濫用處理的人力與流程、點擊資料的隱私責任，轉址程式本身佔的比例很小。估算自建成本時把這幾項算進去，交叉點會往後移。</p>
<p>決定自建的有兩種營運者：自用的組織（為自己的簡訊、行銷或內部系統產生連結），以及把短網址當產品賣給別人的業者。下文的點擊分析收費與自訂網域兩節以業者為主角；自用的組織讀到「客戶」時，對應的是組織內各個建立連結的部門。</p>
<h2 id="點擊資料的地位付費功能或內部參考以及對應的規格">點擊資料的地位：付費功能或內部參考，以及對應的規格</h2>
<p>短網址服務每一次轉址都能記下一筆點擊：時間、來源、大致的地理位置、裝置類型。這筆資料在商業模型裡的位置決定了一連串規格。</p>
<p>點擊分析是付費功能時，點擊資料就是產品。客戶付費買的是準確的數字，少算等於少交付，所以點擊要在很短的時間窗內回到服務被記錄，事件不能遺失，報表要能回答客戶會問的維度（哪一個活動、哪一個國家、哪一天）。點擊統計只是內部參考時，它是副產品，可以接受取樣、可以接受伺服器故障期間遺失還沒送出的點擊事件。</p>
<p>這一項決定在實作上落到兩個位置。瀏覽器預設會把「永久移動」的轉址長期保存，之後的重複點擊直接在本機轉走、不再送到服務，也就記不到；所以轉址結果能被瀏覽器保存多久要有一個很短的上限，狀態碼與快取標頭怎麼選見實作篇的〈轉址回應的快取標頭與瀏覽器保存回應的時間〉。點擊事件則要寫進不會遺失的佇列，而不是記憶體裡的緩衝。兩者的代價都是服務要承擔幾乎每一次點擊的負載。</p>
<p>點擊資料裡的 IP 位址與裝置資訊屬於個人資料，所以報表的粒度（國家、城市還是精確位置）、保存期限、客戶能不能下載原始資料，同時也是隱私責任的大小。報表只需要國家時，原始 IP 在彙總後就可以刪除，隱私責任隨之縮小。</p>
<h2 id="連結的保存期限與保存承諾">連結的保存期限與保存承諾</h2>
<p>短網址一旦發出去，就被印在海報上、寫進書裡、存進別人的書籤。持有連結的人只有短網址，沒有原本的長網址，服務停止回應的那一天，這些連結就全部失效，而持有連結的人自己沒有任何修復的途徑。所以「連結保存多久」是對所有持有連結的人做的承諾，這些人大多不是服務的客戶，也從來沒有同意過任何條款。</p>
<p>保存承諾有三個問題沒有預設答案，每一個答案都直接改變持有連結的人點下去之後看到什麼：</p>
<ul>
<li><strong>連結有沒有到期時間</strong>：永久保存，或者在一段時間沒有點擊之後回收。回收可以刪掉長網址與點擊原始資料、控制儲存成本，代價是舊海報上的連結會失效；回收的短碼不能再發給新的連結，否則舊連結的持有人會被帶到別人的網頁，所以短碼本身仍要留著一筆紀錄佔住。承載私密連結的服務，到期時間另外是一項安全控制，見〈互相限制的需求決定〉。</li>
<li><strong>建立者刪除連結之後，點擊的人看到什麼</strong>：一頁說明「這條連結已被移除」，或直接回 404。印在印刷品或書裡的連結失效時，說明頁讓點擊的人知道連結是被刻意移除，而不是自己打錯網址；只在線上流通的連結可以直接回 404。</li>
<li><strong>服務本身結束營運時怎麼辦</strong>：預先公告的期限、能不能讓客戶匯出短碼對照表（每個短碼對應到哪一個長網址的那份資料）、網域在停止營運後由誰續約。短網址活在網域上，網域到期被別人註冊，舊連結就全部落入新註冊者的控制。這三項在服務結束時的做法：公告期見 <a href="/blog/backend/10-system-evolution/staged-shutdown/" data-link-title="10.7 分階段退場：停止新建、唯讀、靜態封存、關閉與公告期" data-link-desc="服務結束時把一次全部關閉拆成幾個階段：停止新建、唯讀、靜態封存與關閉各保留什麼、對哪一類持有者有效、維運成本降到哪裡；依使用紀錄決定保留範圍；推動客戶遷移的手段；以及公告期長度、通知管道與補償的安排">10.7 分階段退場：停止新建、唯讀、靜態封存、關閉與公告期</a> 的〈公告期的長度與通知管道〉，匯出見 <a href="/blog/backend/10-system-evolution/handover-and-archival/" data-link-title="10.8 移交：資料匯出、開源與交給社群或封存機構" data-link-desc="服務結束時把讓外部引用繼續運作所需的資料、程式碼與網域交出去：交給直接客戶的資料匯出、開源程式碼能不能獨立運作、社群接手需要提供者事先留下什麼、封存機構的兩種取得方式（業者合作交出與外部爬取），以及交不出去的部分">10.8 移交：資料匯出、開源與交給社群或封存機構</a> 的〈交給直接客戶的資料匯出〉，網域續約見 <a href="/blog/backend/10-system-evolution/namespace-and-domain-disposition/" data-link-title="10.6 命名空間與網域的處置：續約、名稱保留與回收、懸空 DNS" data-link-desc="服務結束時網域與名稱的去向，以及它們決定外部引用是失效還是易手：網域的續約與出售、交給中立方的技術控制、套件與雲端資源名稱釋出後被重新申請與佔位、雲端資源刪除後的懸空 DNS 與子網域接管，以及綁在可過期網域上的帳號身分">10.6 命名空間與網域的處置：續約、名稱保留與回收、懸空 DNS</a> 的〈網域在服務結束後的去向：續約並回應、續約不回應、到期、出售〉；外部引用的整體盤點見 <a href="/blog/backend/10-system-evolution/service-termination-scope/" data-link-title="10.4 服務終止的範圍：外部引用的種類與持有者" data-link-desc="服務本身要結束時，盤點它發出去、存在別人手上的外部引用：網址、寫在別人程式或韌體裡的端點、被嵌入的程式碼網址、名稱與綁在網域上的身分；持有者依聯絡得到的程度分成直接客戶、間接使用者與第三方持有者；引用在服務結束後的結局：失效或易手">10.4 服務終止的範圍：外部引用的種類與持有者</a>；短網址的實作見 <a href="/blog/backend/00-service-selection/url-shortener-implementation/" data-link-title="0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄" data-link-desc="以需求已定案為前提的實作取捨：建立與轉址兩條路徑的流量形狀、各種短碼生成方式的可猜測性與協調成本、轉址狀態碼與各層快取怎麼影響停用或修改連結多久生效、點擊事件的非同步紀錄、濫用防護、服務結束時的靜態轉址與網域去向，以及換掉任一項需求決定時要重選的實作位置">0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄</a> 的〈服務結束時的轉址延續〉。</li>
</ul>
<p>這項承諾能維持多久，取決於由哪一種商業模型出資。由付費客戶出資的服務，連結的存續與營收綁在一起；作為大公司附屬工具、沒有獨立營收的短網址服務，母公司不再需要它時就可能關閉，實際的關站案例見<a href="/blog/business/case-analyses/url-shortener-business-models/" data-link-title="短網址的商業模型：訂閱、自架、平台內建與廣告插頁各由誰出資維持轉址" data-link-desc="拆解維持短網址轉址的錢從哪裡來：建立連結的人為點擊分析、品牌網域或託管服務付訂閱費，自架者自己付主機與網域，平台由母公司出資，廣告插頁由廣告主出資；連結能活多久跟著出資者走，各出資方式交給工程的規格也不同">短網址的商業模型</a>的〈連結永久性：各出資方式下連結的存續條件〉一節。</p>
<h2 id="目的地修改的功能需求與風險">目的地修改的功能需求與風險</h2>
<p>允許建立者在連結發出之後修改目的地，是一項有明確需求的功能。QR code 印在包裝、名片或店面上之後就無法更換，背後的網址能改，同一張印刷品就能在活動結束後改導到新頁面；連結的目的地網站搬家時，修改短網址的目的地就能讓舊連結繼續有效。這一項由使用情境決定：印刷品印出之後無法更換，目的地網站也會搬家。</p>
<p>同一項功能也是風險。收到連結的人信任它，是基於分享當下它指向的內容；目的地可以修改時，建立者可以先放一個正常頁面讓連結被廣泛分享，再改成釣魚頁面。所以允許修改的同時，要一起定案三件事：修改後要不要重新做惡意網址檢查、修改紀錄要不要保留、要不要讓點擊者能預覽目前的目的地；少了任何一件，修改目的地就成了先騙取信任再換成釣魚頁的工具。</p>
<p>這一項決定在實作上影響快取。目的地不會變時，轉址結果可以被瀏覽器、CDN 與應用程式快取很久；目的地能改時，每一層快取都要能在修改後失效，否則有人會被送到舊的目的地。教材裡幾篇以短網址為例的盤點與分析，用的是允許修改目的地、撤回在幾分鐘內生效的設定，見 <a href="/blog/backend/case-systems/" data-link-title="Backend 範例系統：短網址、售票搶購、電商 checkout 與交易撮合的功能、需求與規模假設" data-link-desc="Backend 教材幾篇以具體系統走完整條請求路徑的文章共用的四個範例系統：每個系統提供的功能與請求種類、不能讓步與可以讓步的需求、規模假設與起點架構，以及四個系統在成功請求的總數是否有上限、延遲是否改變請求的價值、同一個請求被處理兩次的後果上的差別">Backend 範例系統</a> 的〈短網址服務〉。</p>
<h2 id="建立者的範圍與濫用處理">建立者的範圍與濫用處理</h2>
<p>短網址把目的地藏在一個看不出內容的網址後面，這個性質被釣魚與惡意軟體散布者大量利用，以廣告插頁付錢給建立者的服務被濫用得最嚴重（見<a href="/blog/business/case-analyses/url-shortener-business-models/" data-link-title="短網址的商業模型：訂閱、自架、平台內建與廣告插頁各由誰出資維持轉址" data-link-desc="拆解維持短網址轉址的錢從哪裡來：建立連結的人為點擊分析、品牌網域或託管服務付訂閱費，自架者自己付主機與網域，平台由母公司出資，廣告插頁由廣告主出資；連結能活多久跟著出資者走，各出資方式交給工程的規格也不同">短網址的商業模型</a>的〈廣告插頁：廣告抽成的差價與濫用風險〉一節）。開放越多人建立連結，濫用越難追溯到人；而服務的網域被大量用來散布惡意連結之後，郵件過濾器與瀏覽器可能把整個網域列為可疑，所有正常連結跟著被擋下。</p>
<p>建立者的範圍大致有下列幾種，濫用處理的投入差距很大：</p>
<ul>
<li><strong>只限組織內部使用者</strong>：建立者可追溯，濫用風險接近組織內部的資安問題。</li>
<li><strong>開放註冊，需要驗證帳號</strong>：濫用可以追溯到帳號，處理方式是停權與停用連結，需要一個檢舉管道與處理流程。</li>
<li><strong>匿名建立</strong>：濫用無法追溯到人，只能靠速率限制、建立時的惡意網址檢查與事後的停用。</li>
<li><strong>經由 API 代客戶的使用者建立</strong>：客戶把建立功能開放給自己的使用者，濫用只能追溯到客戶的 API 金鑰，處理方式是要求客戶自己做使用者驗證、對每把金鑰設速率上限，並能整批停用某把金鑰建立的連結。</li>
</ul>
<p>開放對外建立時，濫用處理有幾個問題沒有預設答案：檢舉從哪裡進來、誰負責判定、確認惡意之後多久之內連結要失效、被停用的連結點下去顯示什麼、執法或法律請求由誰回應。其中失效時限會直接限制工程實作的快取時間，因為已被瀏覽器或 CDN 快取的轉址，在快取到期之前不會回來查詢連結是否已被停用；所以這個數字屬於需求，不屬於技術選擇。檢舉與詐騙回報多半先進到客服，客服怎麼判斷一則回報要當場答覆、開單還是立即呼叫值班，見 <a href="/blog/backend/08-incident-response/support-escalation-and-engineering-reply/" data-link-title="8.24 客服回報的處理：客服當場答覆時用的資訊、轉給值班的條件、工程查證後給客服的回覆、查證結果寫回知識文章、告警與查詢工具，以及流程失效時的徵兆" data-link-desc="客服接到使用者回報之後，哪些資訊讓客服當場答覆（進行中的事故、已知錯誤清單、維護與變更公告、錯誤碼對照表、帳號查詢工具），哪些條件要立即呼叫值班、哪些開單在上班時間處理，工程查證之後回覆給客服的內容，同一類回報重複出現時怎麼把查證結果寫回知識文章、告警規則或查詢工具，以及從客服單與事故紀錄看出流程在哪一段失效">8.24 客服回報的處理</a>。</p>
<h2 id="短碼的可猜測性與私密連結">短碼的可猜測性與私密連結</h2>
<p>建立者會把什麼樣的網址拿來縮短，影響短碼本身的規格。這一項由使用情境決定，與收入模型無關。雲端文件的分享連結、私人行事曆的訂閱網址、密碼重設連結，都屬於「知道網址就能存取」的連結。這類連結被縮短之後，短碼能不能被猜到，就成了連結背後的文件、行事曆或帳號能不能被外人取得的問題。</p>
<p>服務預期承載這類連結時，短碼必須不可預測：工程實作就不能把遞增序號直接編碼成短碼（序號加一就得到別人的連結），要保留序號，編碼前得先用帶金鑰的加密把它打散，否則改用隨機產生。服務只用於公開的行銷頁面時，這項限制可以放寬，換取更短的短碼。</p>
<h2 id="自訂網域與網域名聲的隔離">自訂網域與網域名聲的隔離</h2>
<p>所有連結共用服務自己的短網域，是最簡單的做法，代價是名聲共用：一個客戶的濫用行為會讓所有客戶的連結一起被封鎖清單擋下。讓客戶使用自己的網域（例如把 <code>go.example.com</code> 指到服務），點擊的人看得到品牌，信任度較高，該客戶的連結名聲也和其他客戶分開。多數業者的方案頁稱這項功能為「自訂網域」，商業模型那篇從客戶的角度稱它「品牌網域」，兩者指同一件事。</p>
<p>自訂網域在商業上常是付費方案的區分點，在工程上帶來網域所有權驗證、每個網域的 TLS 憑證簽發與更新，以及「同一個短碼在不同網域是不同連結」的資料模型變化。要不要支援、由哪個方案提供，以及客戶停止付費之後網域上的連結怎麼處理，三個答案合起來決定自訂網域對客戶是保險還是另一種綁定。</p>
<h2 id="建立路徑與轉址路徑的可用性分級">建立路徑與轉址路徑的可用性分級</h2>
<p>短網址服務對外有兩條路徑：<strong>建立路徑</strong>接收長網址、產生短碼，<strong>轉址路徑</strong>收到短網址的請求後轉往長網址。兩條路徑對可用性的要求不同，這一項同樣由使用情境決定。建立路徑暫停時，受影響的是正在建立連結的少數人，他們可以稍後再試；轉址路徑暫停時，所有已經發出去的連結同時失效，影響的是持有連結的所有人，而他們沒有替代方案。</p>
<p>這個排序的前提是建立是一個獨立的操作，由使用者主動送出、失敗可以重試。建立由系統在另一條寫入路徑裡自動觸發時（社群平台在發文當下改寫連結、通知系統在送出簡訊之前產生連結），建立路徑暫停就擋住整批發文或訊息，它的可用性與延遲就是那條路徑的一部分，目標要跟著那條路徑訂；這類服務也常讓發送端在建立失敗時退回使用長網址，或在內容發出後仍可改寫的情境（例如貼文），把建立改成非同步、先用原網址發出再補上短網址。</p>
<p>所以可用性目標要分開訂（見 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO</a>）：轉址路徑的目標要高於建立路徑，點擊統計與報表的目標可以更低，而且統計的故障不能拖垮轉址（見 <a href="/blog/backend/knowledge-cards/degradation/" data-link-title="Degradation" data-link-desc="說明服務部分能力失效時如何保留核心功能與控制風險">degradation</a>）。尖峰的估計也以轉址為主：共用連結的尖峰來自單一連結被大量轉發，和每天建立多少連結關係不大；替每位收件人各產生一條的連結，尖峰則跟著每一批發送的收件人數走。客戶若有大型活動（例如電視廣告上出現的短網址），事先通知的管道讓服務能在尖峰之前擴容；沒有預告的爆紅見 <a href="/blog/backend/00-service-selection/url-shortener-traffic-surge/" data-link-title="0.25 短網址服務的瞬時大流量：爆紅連結、建立路徑的機器人濫用與轉址路徑的攻擊" data-link-desc="短網址服務遇到瞬時大流量時的處置：單一連結被大量轉發時負載落在哪一層、點擊事件管道的積壓與目的地網站承受不住匯集的點擊，爆紅的連結是惡意連結時快取與撤回的衝突，建立路徑被機器人大量建立連結時的成本、檢查管道與准入手段，以及轉址網域遭受掃描或體積型攻擊">0.25 短網址服務的瞬時大流量：爆紅連結、建立路徑的機器人濫用與轉址路徑的攻擊</a> 的〈爆紅連結的負載落點：CDN、程序內快取與對照表〉。</p>
<h2 id="互相限制的需求決定">互相限制的需求決定</h2>
<p>本篇各節的決定各自有答案，組合起來有幾組會互相限制，需求定案前要一起檢查：</p>
<ul>
<li><strong>開放建立與可被長期快取的轉址不相容</strong>：開放建立就必須能停用惡意連結（連結會到期或可被刪除時也是同一個限制），而沒有快取上限的轉址會被瀏覽器保存，已經點過這條連結的瀏覽器存著舊的轉址，停用對它們無效。所以只要開放對外建立，轉址結果就不能被瀏覽器無限期保存；這個快取上限同時決定停用最慢多久生效，以及有多少重複點擊不被計入。</li>
<li><strong>承載私密連結與永久保存互相衝突</strong>：短網址就是存取憑證，永久保存等於存取憑證不過期。預期承載這類連結的服務，到期時間要依連結背後的資料來訂，而不是依儲存成本；到期時要把對照表裡的長網址一起清空，因為長網址才是那份資料的存取憑證；點擊紀錄或修改歷史若存了長網址，保存期限要跟著縮短。</li>
<li><strong>目的地可修改與永久保存疊加時，風險隨時間累積</strong>：連結活得越久，被改成惡意目的地的機會越多，修改後的重新檢查與修改紀錄要隨保存期限一起設計。</li>
<li><strong>點擊分析的粒度與隱私責任同向增加</strong>：報表要的維度越細，保存的個人資料越多，保存期限與刪除流程要隨報表規格一起定。</li>
<li><strong>自訂網域讓客戶在服務終止時接手自己的連結</strong>：客戶的網域在客戶手上，服務終止時客戶可以把網域指到別處、在新服務上重建連結，前提是短碼對照表能匯出。這一點也是自訂網域在保存承諾上的優勢。</li>
</ul>
<p>這些決定定案之後，每一項落到實作的哪一個位置、換掉其中一項時實作要改哪裡，列在<a href="/blog/backend/00-service-selection/url-shortener-implementation/" data-link-title="0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄" data-link-desc="以需求已定案為前提的實作取捨：建立與轉址兩條路徑的流量形狀、各種短碼生成方式的可猜測性與協調成本、轉址狀態碼與各層快取怎麼影響停用或修改連結多久生效、點擊事件的非同步紀錄、濫用防護、服務結束時的靜態轉址與網域去向，以及換掉任一項需求決定時要重選的實作位置">實作篇</a>的〈各項需求決定對應的實作位置〉一節。</p>
]]></content:encoded></item><item><title>0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/url-shortener-implementation/</link><pubDate>Fri, 02 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/url-shortener-implementation/</guid><description>&lt;p>自建短網址服務的實作取捨，以需求已經定案為前提：連結能不能改目的地、要不要統計點擊、誰可以建立連結、短碼會不會承載私密連結、連結保存多久、支不支援自訂網域、連結是一對多分享還是替每位收件人各產生一條。這幾個需求決定由 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/url-shortener-requirements/" data-link-title="0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格" data-link-desc="短網址服務交給工程之前要定案的需求決定：自建或購買、點擊資料的地位、連結的保存承諾、目的地能否修改、建立者的範圍、短碼的可猜測性、網域策略與可用性分級；多數答案由商業模型推出，目的地修改、短碼可猜測性與可用性分級由使用情境決定">0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格&lt;/a>（下文稱需求定義篇）推導出來，本篇把它們當成輸入，逐項說明它們落到實作的哪一個位置。&lt;/p>
&lt;h2 id="建立路徑與轉址路徑的流量形狀與失敗代價">建立路徑與轉址路徑的流量形狀與失敗代價&lt;/h2>
&lt;p>短網址服務對外只做兩件事。&lt;strong>建立路徑&lt;/strong>接收一個長網址，產生一個短碼，把「短碼 → 長網址」寫進對照表，回傳短網址。&lt;strong>轉址路徑&lt;/strong>接收瀏覽器對短網址的請求，用短碼查對照表，回一個 3xx 狀態碼並在 &lt;code>Location&lt;/code> 標頭放長網址，瀏覽器收到後自己前往長網址。&lt;strong>對照表&lt;/strong>就是這張記錄每個短碼對應到哪一個長網址的資料表，兩條路徑都以它為中心。&lt;/p>
&lt;p>兩條路徑的流量形狀差很多。一條連結只建立一次，被點擊的次數由它被分享到哪裡決定：放在個人筆記裡的連結可能一輩子被點幾次，放進一則被大量轉發的貼文裡的連結可能在幾小時內被點擊數百萬次。所以這個服務讀遠多於寫（見 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5 流量與資料量評估&lt;/a> 的讀寫比例一節），而且點擊在各條連結之間呈長尾分佈：多數連結幾乎沒有流量，大部分的點擊集中在少數連結上。&lt;/p>
&lt;p>這個形狀屬於一對多公開分享的連結。另一種流量形狀來自替每位收件人各產生一條的連結：簡訊或郵件平台在發送時替每位收件人各建一條短網址（目的地帶著收件人自己的識別碼），一條連結只會被一個人點一兩次。這種用法下建立量與點擊量是同一個量級，點擊在發送後的幾小時內集中出現、分散在剛建立的大量連結上，應用層快取幾乎不會命中，尖峰的大小由那一批發送的收件人數決定。服務要承接這類用途時，轉址的容量要按單次發送的批量估，對照表要能直接承受尖峰時的點查詢。&lt;/p>
&lt;p>兩條路徑失敗的代價也不對稱。建立路徑失敗時，建立連結的人看到錯誤、稍後重試，損失是一次操作。轉址路徑失敗時，所有已經發出去的短網址同時失效——印在海報上的、寄出去的簡訊裡的、別人轉貼的——而持有這些連結的人手上只有短網址，沒有長網址可以改用。這個不對稱決定了快取、點擊紀錄與濫用防護的取捨方向：&lt;strong>轉址路徑的可用性與延遲優先；建立是一個由使用者主動送出、失敗可以重試的獨立操作時，建立路徑可以為了唯一性檢查與惡意網址檢查付出延遲&lt;/strong>（建立嵌在另一條寫入路徑裡的情形，例如發文當下改寫連結、送出簡訊前才產生連結，見需求定義篇〈建立路徑與轉址路徑的可用性分級〉），點擊統計與濫用掃描這類附加功能要設計成失敗時不拖垮轉址。短碼生成與對照表的取捨另有驅動因素：短碼會不會承載私密連結，以及轉址路徑的存取形狀。&lt;/p>
&lt;p>容量估算也照兩條路徑分開算。對照表的儲存量是「每日建立數 × 每筆大小 × 保存天數」，每筆紀錄包含短碼、長網址與幾個中繼欄位（擁有者、建立時間、到期時間、狀態），長網址本身的長度通常佔大宗；轉址路徑要估的是尖峰而不是平均：一對多分享的連結，尖峰由單一連結被大量轉發的那幾個小時決定，和整體建立量的關聯很弱；替每位收件人各產生一條的連結，尖峰由單次發送的批量決定。&lt;/p>
&lt;h2 id="短碼生成可猜測性與協調成本的取捨">短碼生成：可猜測性與協調成本的取捨&lt;/h2>
&lt;p>短碼通常用 base62 編碼，字元集是 &lt;code>0-9&lt;/code>、&lt;code>a-z&lt;/code>、&lt;code>A-Z&lt;/code> 共 62 個字元，每個字元都能直接出現在網址路徑裡、不需要跳脫。&lt;strong>碼空間&lt;/strong>指一種碼長能產生的短碼總數：6 碼是 62⁶，約 568 億個；7 碼是 62⁷，約 3.5 兆個。碼空間大小本身很少是瓶頸，生成方式要權衡的是另外兩件事：短碼能不能被猜到，以及多台伺服器同時生成時要不要彼此協調。&lt;/p>
&lt;h3 id="遞增計數器碰撞可列舉性與發號協調">遞增計數器：碰撞、可列舉性與發號協調&lt;/h3>
&lt;p>遞增計數器把一個全域遞增的整數轉成 base62。同一個整數只會被發出一次，所以短碼不會重複，碼長也是各種生成方式裡最短的：前 568 億條連結都只需要 6 碼。&lt;/p>
&lt;p>這個做法的代價是短碼可以被列舉。拿到任何一個短碼的人，把它解碼成整數、加一或減一再編碼回去，就得到相鄰時間建立的別人的連結；寫一支腳本把整數從 0 起逐一編碼並請求，就能取得服務裡的每一條連結。如果有使用者把雲端文件的分享連結、私人行事曆的訂閱網址拿來縮短，這些「知道網址就能存取」的連結會全部外洩。短碼可被列舉這個性質還洩漏營運數據：在兩個時間點各建立一條連結，兩個整數的差就是這段期間全站的建立量。&lt;/p>
&lt;p>多台伺服器同時發號時，計數器需要協調。常見的做法是號段分配：一個中央協調者（一張資料庫表或一個協調服務）每次發給伺服器一整段號碼，例如一次一千個，伺服器在記憶體裡從這一段依序發號，用完再去領下一段。協調的頻率因此降到每一千次建立一次；代價是伺服器重啟時沒發完的號碼作廢，短碼序列會出現跳號，這對短網址沒有影響，因為沒有人依賴短碼連續。&lt;/p>
&lt;p>想保留計數器、又不想讓相鄰的整數產生相鄰的短碼時，會在編碼前先做一次可逆的置換。乘上一個與碼空間互質的常數再取模是最簡單的置換，而它是線性的：攻擊者自己連續建立兩條連結，就拿到一對由相鄰整數產生的短碼，兩者解碼後相減再對碼空間取模，就得到那個常數，整個置換因此被還原。要讓短碼在知道演算法的人面前也無法預測，置換必須帶金鑰，常見的選擇是保留格式的加密（format-preserving encryption）：密文和明文落在同一個數值範圍，加密後的結果仍是碼空間裡的一個碼，碼長不變；一般的區塊加密輸出固定長度的位元串，會讓短碼變長。做不到帶金鑰的置換時，就放棄計數器、改用隨機產生。一般資料表主鍵面對的是同一組取捨（序號、UUID v4、UUID v7 各自可不可預測），見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 Schema Design 與資料建模&lt;/a> 的〈ID 類型選型矩陣〉。&lt;/p>
&lt;p>不靠中央協調者的時間序 ID（例如 Snowflake 的格式）把時間、機器編號與序號組成一個 64 位元整數，base62 編碼後約 11 碼，而且短碼本身洩漏建立時間；對短網址來說，碼長與資訊洩漏兩項代價都偏高。&lt;/p>
&lt;h3 id="隨機產生不可預測性與碰撞處理">隨機產生：不可預測性與碰撞處理&lt;/h3>
&lt;p>隨機產生直接從碼空間裡均勻抽一個碼。它不需要伺服器之間協調，短碼之間也沒有任何順序關係，列舉只剩暴力猜測一條路，而猜中的機率是「已用的碼數 ÷ 碼空間」。&lt;/p>
&lt;p>代價是碰撞。每一次新抽的碼撞到既有碼的機率同樣是「已用的碼數 ÷ 碼空間」：7 碼的空間裡已經有十億條連結時，這個機率約是萬分之三，單次建立幾乎不會撞到。但服務的總建立次數很大，依生日悖論（在 N 個可能值裡隨機抽取，抽到約 √N 個時就很可能有兩個相同），碼空間 N 裡抽到 √N 這個量級的碼數時（7 碼約一兩百萬個），碰撞就很可能已經發生。所以服務規模一大，碰撞就很可能已經發生；規模小時機率雖低，碰撞沒處理的後果是新連結蓋掉別人的連結，設計仍要決定由哪一層偵測並處理碰撞。&lt;/p>
&lt;p>處理碰撞的做法是讓資料庫的唯一約束當最後的判定者：直接嘗試寫入，違反唯一約束就重新抽碼再寫。「先查詢這個碼存不存在、不存在再寫入」這種兩步做法在併發下會讓兩個請求同時查到不存在、同時寫入，正確性要靠唯一約束而不是應用程式的判斷。已用比例升高、重試次數開始明顯增加時，把碼長加一碼，碼空間就乘上 62。&lt;/p>
&lt;h3 id="預先產生的短碼池離線處理碰撞與補貨">預先產生的短碼池：離線處理碰撞與補貨&lt;/h3>
&lt;p>預先產生的短碼池在離線時就隨機產生一批短碼、去掉重複後存起來，建立路徑只要從池子取一個並標記為已用，不需要重試，也不需要伺服器之間協調計數器。碰撞仍然會發生，只是改由產生池子的批次作業處理。代價是多一個要維運的元件：池子要在用完之前補貨，每台伺服器一次取走一批時，重啟會讓沒用完的那一批作廢。短碼的不可預測性和隨機產生相同。開放自訂短碼時，使用者自訂的碼可能與池中還沒發出的碼相同，所以寫入仍要經過唯一約束，撞到就從池子再取一個；或在接受自訂短碼時，同時把池中同值的碼標記為已用。&lt;/p>
&lt;h3 id="長網址雜湊截斷去重性質與適用情境">長網址雜湊截斷：去重性質與適用情境&lt;/h3>
&lt;p>雜湊截斷對長網址做雜湊，取雜湊值的前幾碼當短碼（取幾碼由碼空間要多大決定）。同一個長網址永遠得到同一個短碼，所以兩個人縮短同一個網址會拿到同一條短網址。&lt;/p>
&lt;p>這個去重性質在多數商業模型下和需求衝突。兩個使用者縮短同一個網址，各自要看自己那一條的點擊統計、各自要能刪除自己那一條；共用一條短網址時，誰的點擊是誰的、誰有權刪除，都沒有答案。把使用者 ID 加進雜湊的輸入可以讓每個人拿到不同的短碼，剩下的性質就接近隨機產生，只保留了同一個使用者對同一個網址的去重。截斷後的雜湊同樣會碰撞，碰撞處理和隨機產生一樣要靠唯一約束與重試。&lt;/p>
&lt;p>這個做法適合的情境是沒有擁有者概念、也不統計點擊的服務，例如內部文件系統把過長的網址改寫成固定的短碼，目的只是讓網址變短，同一個長網址共用一條紀錄反而省儲存。需要逐則貼文點擊數的平台不屬於這一類，同一個外部網址共用一條紀錄時，點擊就分不出來自哪一則貼文。&lt;/p>
&lt;h3 id="自訂短碼共用的命名空間與保留字">自訂短碼：共用的命名空間與保留字&lt;/h3>
&lt;p>開放使用者自訂短碼時（例如把活動連結設成 &lt;code>/summer-sale&lt;/code>），自訂短碼與系統生成的短碼查的是同一張對照表，所以兩者走同一個唯一約束。另外要維護一份保留字清單：服務自己的路由（&lt;code>api&lt;/code>、&lt;code>login&lt;/code>、&lt;code>admin&lt;/code>、&lt;code>static&lt;/code>）不能被註冊成短碼，否則轉址路徑會攔下本來要送到那些路由的請求。品牌詞與他人商標的搶註屬於使用政策，本篇與需求定義篇都不處理。&lt;/p>
&lt;h2 id="對照表儲存選型欄位與分片">對照表：儲存選型、欄位與分片&lt;/h2>
&lt;p>轉址路徑對對照表只做一種查詢：給一個短碼，取回一列。這種存取形狀適合任何以主鍵點查詢為主的儲存——關聯式資料庫的主鍵索引、key-value store、document store 都能承擔，選型的考量落在團隊已經熟悉哪一種，以及資料量大到要分片時怎麼切。分片時用短碼當分片鍵；只有系統隨機生成的短碼時，短碼本身就均勻分佈。遞增計數器直接編碼的短碼依序遞增，按範圍切的分片會讓最新的分片同時承擔所有寫入與最熱的讀取，同樣要先對短碼做雜湊。開放自訂短碼之後，人取的短碼集中在常見字首與常見單字，按範圍切的分片（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/range-sharding/" data-link-title="Range Sharding" data-link-desc="分散式 SQL 把 key space 切成可自動 split / merge 的 range、每個 range 自己的 consensus group、application 透明">range sharding&lt;/a>）會讓少數分片承擔大部分的鍵，所以分片前要先對短碼做雜湊。&lt;/p>
&lt;p>base62 的碼空間依賴大小寫被當成不同字元，所以短碼欄位要用區分大小寫的比對規則。部分關聯式資料庫的預設 collation 不分大小寫（例如 MySQL 的 &lt;code>utf8mb4_0900_ai_ci&lt;/code>），沿用預設會讓 &lt;code>aB3x&lt;/code> 與 &lt;code>Ab3X&lt;/code> 撞上同一個唯一約束，可用的碼空間退回每位 36 個字元，碼空間與碰撞機率的估算全部失準。開放自訂短碼時，自訂短碼通常希望不分大小寫（使用者會自己打字），兩者的正規化規則要在寫入時就決定。&lt;/p>
&lt;p>對照表的一列至少包含：短碼、長網址、擁有者、建立時間、到期時間、狀態，刪除時另記刪除時間。狀態欄位的值包含「正常」「已停用」「已刪除」，轉址路徑每次查詢都要讀它，並依狀態決定回應：正常時轉址；已停用時顯示的頁面（常見的是一頁警告）由需求定義篇〈建立者的範圍與濫用處理〉一節的決定給出；已刪除或已過到期時間時回一頁說明或 404／410，回哪一種由需求定義篇〈連結的保存期限與保存承諾〉一節的決定給出。&lt;/p>
&lt;p>幾個需求會改變這張表的鍵與欄位。支援自訂網域時，同一個短碼在不同網域底下是兩條不同的連結，主鍵變成「網域 + 短碼」；服務還要為每一個客戶網域驗證所有權、自動簽發並更新 TLS 憑證（見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/acme-automation/" data-link-title="ACME Automation" data-link-desc="說明網站憑證如何透過 ACME 自動簽發與續期">ACME Automation&lt;/a>）。允許修改目的地時，長網址欄位會被更新，同一個短碼在修改前後指向不同的網址，快取的設計因此跟著改變（見〈各層快取與撤回所需的時間〉）；需求定義篇要求保留修改紀錄時，另外存一張以「短碼 + 修改時間」為鍵的修改歷史表。承諾客戶能匯出對照表時，匯出內容至少要有網域、短碼、長網址與狀態，客戶才能在另一個服務上重建同一批連結。&lt;/p>
&lt;p>「列出我建立的所有連結」這類查詢以擁有者為條件，和轉址路徑的主鍵查詢是不同的存取形狀。資料量小時加一個擁有者的次要索引就夠；分片之後，次要索引要查所有分片，這時改成另外維護一份以擁有者為鍵的&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">讀取模型&lt;/a>。&lt;/p>
&lt;p>被刪除的短碼不重新發給別人。舊的短網址可能還留在某封信、某張海報上，把同一個短碼發給新的連結，持有舊連結的人點下去會被送到一個與原本無關的網址，而新連結的擁有者可以因此取得原本屬於別人的流量。刪除因此做成軟刪除（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/soft-delete/" data-link-title="Soft Delete（軟刪除）" data-link-desc="資料表有 deleted_at 欄位、ORM 的刪除改成 UPDATE，或刪掉的資料再建一次卻撞唯一約束、報表數字和頁面對不上時，查軟刪除的過濾條件由誰加、哪些路徑沒有加">Soft Delete&lt;/a>）：保留這一列、只把狀態改成「已刪除」，讓這個短碼永遠被佔住、不被重用。佔位需要的只有主鍵（短碼，支援自訂網域時加上網域）、狀態與刪除時間；長網址可能帶著個人資料（例如帶預約編號的就診提醒連結），刪除或到期時把長網址欄位清空，修改歷史裡的舊長網址一併清空。&lt;/p>
&lt;h2 id="轉址回應的快取標頭與瀏覽器保存回應的時間">轉址回應的快取標頭與瀏覽器保存回應的時間&lt;/h2>
&lt;p>&lt;strong>撤回&lt;/strong>指讓一條已經發出去的短網址不再轉往原本的目的地，包含停用、刪除、到期失效，以及修改目的地。這幾種操作要生效，都需要點擊的人的請求回到短網址服務，所以撤回能不能做到、多快做到，取決於瀏覽器把轉址結果快取多久。&lt;/p>
&lt;p>控制這件事的是回應上的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-control/" data-link-title="Cache-Control（快取控制標頭）" data-link-desc="看到回應帶 Cache-Control、或要決定某個回應能被誰保存、保存多久、使用前要不要先問 origin 時，查這個標頭各指令的分工">&lt;code>Cache-Control&lt;/code>&lt;/a> 標頭；轉址狀態碼只決定沒有帶這個標頭時的預設行為。HTTP 規範（RFC 9110）定義的四個轉址狀態碼在預設行為上不同，其中「允許啟發式快取」指的是：回應沒帶快取標頭時，規範允許快取自行估一個保存時間（見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/heuristic-freshness/" data-link-title="Heuristic Freshness（啟發式新鮮度期限）" data-link-desc="回應沒寫任何快取期限卻仍被瀏覽器或 CDN 保存、改了內容使用者卻還拿到舊版時，查快取自己估期限的規則">Heuristic Freshness&lt;/a> 卡）。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>狀態碼&lt;/th>
 &lt;th>語意&lt;/th>
 &lt;th>未帶快取標頭時規範是否允許快取&lt;/th>
 &lt;th>重送請求時方法是否保留&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>301&lt;/td>
 &lt;td>永久移動&lt;/td>
 &lt;td>允許（啟發式快取，Chromium 會保存且永不過期）&lt;/td>
 &lt;td>歷史上可能把 POST 改成 GET&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>308&lt;/td>
 &lt;td>永久移動&lt;/td>
 &lt;td>允許（啟發式快取，Chromium 會保存且永不過期）&lt;/td>
 &lt;td>保留&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>302&lt;/td>
 &lt;td>暫時移動&lt;/td>
 &lt;td>不允許&lt;/td>
 &lt;td>歷史上可能把 POST 改成 GET&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>307&lt;/td>
 &lt;td>暫時移動&lt;/td>
 &lt;td>不允許&lt;/td>
 &lt;td>保留&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>點擊短網址送出的都是 GET，所以方法保留與否在這裡沒有作用，差別只在「永久」與「暫時」兩種語意，以及沒帶快取標頭時的預設行為。&lt;/p></description><content:encoded><![CDATA[<p>自建短網址服務的實作取捨，以需求已經定案為前提：連結能不能改目的地、要不要統計點擊、誰可以建立連結、短碼會不會承載私密連結、連結保存多久、支不支援自訂網域、連結是一對多分享還是替每位收件人各產生一條。這幾個需求決定由 <a href="/blog/backend/00-service-selection/url-shortener-requirements/" data-link-title="0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格" data-link-desc="短網址服務交給工程之前要定案的需求決定：自建或購買、點擊資料的地位、連結的保存承諾、目的地能否修改、建立者的範圍、短碼的可猜測性、網域策略與可用性分級；多數答案由商業模型推出，目的地修改、短碼可猜測性與可用性分級由使用情境決定">0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格</a>（下文稱需求定義篇）推導出來，本篇把它們當成輸入，逐項說明它們落到實作的哪一個位置。</p>
<h2 id="建立路徑與轉址路徑的流量形狀與失敗代價">建立路徑與轉址路徑的流量形狀與失敗代價</h2>
<p>短網址服務對外只做兩件事。<strong>建立路徑</strong>接收一個長網址，產生一個短碼，把「短碼 → 長網址」寫進對照表，回傳短網址。<strong>轉址路徑</strong>接收瀏覽器對短網址的請求，用短碼查對照表，回一個 3xx 狀態碼並在 <code>Location</code> 標頭放長網址，瀏覽器收到後自己前往長網址。<strong>對照表</strong>就是這張記錄每個短碼對應到哪一個長網址的資料表，兩條路徑都以它為中心。</p>
<p>兩條路徑的流量形狀差很多。一條連結只建立一次，被點擊的次數由它被分享到哪裡決定：放在個人筆記裡的連結可能一輩子被點幾次，放進一則被大量轉發的貼文裡的連結可能在幾小時內被點擊數百萬次。所以這個服務讀遠多於寫（見 <a href="/blog/backend/00-service-selection/traffic-data-scale/" data-link-title="0.5 流量與資料量評估" data-link-desc="用流量形狀、資料成長、hot key、保留期限與尖峰模式評估後端需求規模">0.5 流量與資料量評估</a> 的讀寫比例一節），而且點擊在各條連結之間呈長尾分佈：多數連結幾乎沒有流量，大部分的點擊集中在少數連結上。</p>
<p>這個形狀屬於一對多公開分享的連結。另一種流量形狀來自替每位收件人各產生一條的連結：簡訊或郵件平台在發送時替每位收件人各建一條短網址（目的地帶著收件人自己的識別碼），一條連結只會被一個人點一兩次。這種用法下建立量與點擊量是同一個量級，點擊在發送後的幾小時內集中出現、分散在剛建立的大量連結上，應用層快取幾乎不會命中，尖峰的大小由那一批發送的收件人數決定。服務要承接這類用途時，轉址的容量要按單次發送的批量估，對照表要能直接承受尖峰時的點查詢。</p>
<p>兩條路徑失敗的代價也不對稱。建立路徑失敗時，建立連結的人看到錯誤、稍後重試，損失是一次操作。轉址路徑失敗時，所有已經發出去的短網址同時失效——印在海報上的、寄出去的簡訊裡的、別人轉貼的——而持有這些連結的人手上只有短網址，沒有長網址可以改用。這個不對稱決定了快取、點擊紀錄與濫用防護的取捨方向：<strong>轉址路徑的可用性與延遲優先；建立是一個由使用者主動送出、失敗可以重試的獨立操作時，建立路徑可以為了唯一性檢查與惡意網址檢查付出延遲</strong>（建立嵌在另一條寫入路徑裡的情形，例如發文當下改寫連結、送出簡訊前才產生連結，見需求定義篇〈建立路徑與轉址路徑的可用性分級〉），點擊統計與濫用掃描這類附加功能要設計成失敗時不拖垮轉址。短碼生成與對照表的取捨另有驅動因素：短碼會不會承載私密連結，以及轉址路徑的存取形狀。</p>
<p>容量估算也照兩條路徑分開算。對照表的儲存量是「每日建立數 × 每筆大小 × 保存天數」，每筆紀錄包含短碼、長網址與幾個中繼欄位（擁有者、建立時間、到期時間、狀態），長網址本身的長度通常佔大宗；轉址路徑要估的是尖峰而不是平均：一對多分享的連結，尖峰由單一連結被大量轉發的那幾個小時決定，和整體建立量的關聯很弱；替每位收件人各產生一條的連結，尖峰由單次發送的批量決定。</p>
<h2 id="短碼生成可猜測性與協調成本的取捨">短碼生成：可猜測性與協調成本的取捨</h2>
<p>短碼通常用 base62 編碼，字元集是 <code>0-9</code>、<code>a-z</code>、<code>A-Z</code> 共 62 個字元，每個字元都能直接出現在網址路徑裡、不需要跳脫。<strong>碼空間</strong>指一種碼長能產生的短碼總數：6 碼是 62⁶，約 568 億個；7 碼是 62⁷，約 3.5 兆個。碼空間大小本身很少是瓶頸，生成方式要權衡的是另外兩件事：短碼能不能被猜到，以及多台伺服器同時生成時要不要彼此協調。</p>
<h3 id="遞增計數器碰撞可列舉性與發號協調">遞增計數器：碰撞、可列舉性與發號協調</h3>
<p>遞增計數器把一個全域遞增的整數轉成 base62。同一個整數只會被發出一次，所以短碼不會重複，碼長也是各種生成方式裡最短的：前 568 億條連結都只需要 6 碼。</p>
<p>這個做法的代價是短碼可以被列舉。拿到任何一個短碼的人，把它解碼成整數、加一或減一再編碼回去，就得到相鄰時間建立的別人的連結；寫一支腳本把整數從 0 起逐一編碼並請求，就能取得服務裡的每一條連結。如果有使用者把雲端文件的分享連結、私人行事曆的訂閱網址拿來縮短，這些「知道網址就能存取」的連結會全部外洩。短碼可被列舉這個性質還洩漏營運數據：在兩個時間點各建立一條連結，兩個整數的差就是這段期間全站的建立量。</p>
<p>多台伺服器同時發號時，計數器需要協調。常見的做法是號段分配：一個中央協調者（一張資料庫表或一個協調服務）每次發給伺服器一整段號碼，例如一次一千個，伺服器在記憶體裡從這一段依序發號，用完再去領下一段。協調的頻率因此降到每一千次建立一次；代價是伺服器重啟時沒發完的號碼作廢，短碼序列會出現跳號，這對短網址沒有影響，因為沒有人依賴短碼連續。</p>
<p>想保留計數器、又不想讓相鄰的整數產生相鄰的短碼時，會在編碼前先做一次可逆的置換。乘上一個與碼空間互質的常數再取模是最簡單的置換，而它是線性的：攻擊者自己連續建立兩條連結，就拿到一對由相鄰整數產生的短碼，兩者解碼後相減再對碼空間取模，就得到那個常數，整個置換因此被還原。要讓短碼在知道演算法的人面前也無法預測，置換必須帶金鑰，常見的選擇是保留格式的加密（format-preserving encryption）：密文和明文落在同一個數值範圍，加密後的結果仍是碼空間裡的一個碼，碼長不變；一般的區塊加密輸出固定長度的位元串，會讓短碼變長。做不到帶金鑰的置換時，就放棄計數器、改用隨機產生。一般資料表主鍵面對的是同一組取捨（序號、UUID v4、UUID v7 各自可不可預測），見 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 Schema Design 與資料建模</a> 的〈ID 類型選型矩陣〉。</p>
<p>不靠中央協調者的時間序 ID（例如 Snowflake 的格式）把時間、機器編號與序號組成一個 64 位元整數，base62 編碼後約 11 碼，而且短碼本身洩漏建立時間；對短網址來說，碼長與資訊洩漏兩項代價都偏高。</p>
<h3 id="隨機產生不可預測性與碰撞處理">隨機產生：不可預測性與碰撞處理</h3>
<p>隨機產生直接從碼空間裡均勻抽一個碼。它不需要伺服器之間協調，短碼之間也沒有任何順序關係，列舉只剩暴力猜測一條路，而猜中的機率是「已用的碼數 ÷ 碼空間」。</p>
<p>代價是碰撞。每一次新抽的碼撞到既有碼的機率同樣是「已用的碼數 ÷ 碼空間」：7 碼的空間裡已經有十億條連結時，這個機率約是萬分之三，單次建立幾乎不會撞到。但服務的總建立次數很大，依生日悖論（在 N 個可能值裡隨機抽取，抽到約 √N 個時就很可能有兩個相同），碼空間 N 裡抽到 √N 這個量級的碼數時（7 碼約一兩百萬個），碰撞就很可能已經發生。所以服務規模一大，碰撞就很可能已經發生；規模小時機率雖低，碰撞沒處理的後果是新連結蓋掉別人的連結，設計仍要決定由哪一層偵測並處理碰撞。</p>
<p>處理碰撞的做法是讓資料庫的唯一約束當最後的判定者：直接嘗試寫入，違反唯一約束就重新抽碼再寫。「先查詢這個碼存不存在、不存在再寫入」這種兩步做法在併發下會讓兩個請求同時查到不存在、同時寫入，正確性要靠唯一約束而不是應用程式的判斷。已用比例升高、重試次數開始明顯增加時，把碼長加一碼，碼空間就乘上 62。</p>
<h3 id="預先產生的短碼池離線處理碰撞與補貨">預先產生的短碼池：離線處理碰撞與補貨</h3>
<p>預先產生的短碼池在離線時就隨機產生一批短碼、去掉重複後存起來，建立路徑只要從池子取一個並標記為已用，不需要重試，也不需要伺服器之間協調計數器。碰撞仍然會發生，只是改由產生池子的批次作業處理。代價是多一個要維運的元件：池子要在用完之前補貨，每台伺服器一次取走一批時，重啟會讓沒用完的那一批作廢。短碼的不可預測性和隨機產生相同。開放自訂短碼時，使用者自訂的碼可能與池中還沒發出的碼相同，所以寫入仍要經過唯一約束，撞到就從池子再取一個；或在接受自訂短碼時，同時把池中同值的碼標記為已用。</p>
<h3 id="長網址雜湊截斷去重性質與適用情境">長網址雜湊截斷：去重性質與適用情境</h3>
<p>雜湊截斷對長網址做雜湊，取雜湊值的前幾碼當短碼（取幾碼由碼空間要多大決定）。同一個長網址永遠得到同一個短碼，所以兩個人縮短同一個網址會拿到同一條短網址。</p>
<p>這個去重性質在多數商業模型下和需求衝突。兩個使用者縮短同一個網址，各自要看自己那一條的點擊統計、各自要能刪除自己那一條；共用一條短網址時，誰的點擊是誰的、誰有權刪除，都沒有答案。把使用者 ID 加進雜湊的輸入可以讓每個人拿到不同的短碼，剩下的性質就接近隨機產生，只保留了同一個使用者對同一個網址的去重。截斷後的雜湊同樣會碰撞，碰撞處理和隨機產生一樣要靠唯一約束與重試。</p>
<p>這個做法適合的情境是沒有擁有者概念、也不統計點擊的服務，例如內部文件系統把過長的網址改寫成固定的短碼，目的只是讓網址變短，同一個長網址共用一條紀錄反而省儲存。需要逐則貼文點擊數的平台不屬於這一類，同一個外部網址共用一條紀錄時，點擊就分不出來自哪一則貼文。</p>
<h3 id="自訂短碼共用的命名空間與保留字">自訂短碼：共用的命名空間與保留字</h3>
<p>開放使用者自訂短碼時（例如把活動連結設成 <code>/summer-sale</code>），自訂短碼與系統生成的短碼查的是同一張對照表，所以兩者走同一個唯一約束。另外要維護一份保留字清單：服務自己的路由（<code>api</code>、<code>login</code>、<code>admin</code>、<code>static</code>）不能被註冊成短碼，否則轉址路徑會攔下本來要送到那些路由的請求。品牌詞與他人商標的搶註屬於使用政策，本篇與需求定義篇都不處理。</p>
<h2 id="對照表儲存選型欄位與分片">對照表：儲存選型、欄位與分片</h2>
<p>轉址路徑對對照表只做一種查詢：給一個短碼，取回一列。這種存取形狀適合任何以主鍵點查詢為主的儲存——關聯式資料庫的主鍵索引、key-value store、document store 都能承擔，選型的考量落在團隊已經熟悉哪一種，以及資料量大到要分片時怎麼切。分片時用短碼當分片鍵；只有系統隨機生成的短碼時，短碼本身就均勻分佈。遞增計數器直接編碼的短碼依序遞增，按範圍切的分片會讓最新的分片同時承擔所有寫入與最熱的讀取，同樣要先對短碼做雜湊。開放自訂短碼之後，人取的短碼集中在常見字首與常見單字，按範圍切的分片（<a href="/blog/backend/knowledge-cards/range-sharding/" data-link-title="Range Sharding" data-link-desc="分散式 SQL 把 key space 切成可自動 split / merge 的 range、每個 range 自己的 consensus group、application 透明">range sharding</a>）會讓少數分片承擔大部分的鍵，所以分片前要先對短碼做雜湊。</p>
<p>base62 的碼空間依賴大小寫被當成不同字元，所以短碼欄位要用區分大小寫的比對規則。部分關聯式資料庫的預設 collation 不分大小寫（例如 MySQL 的 <code>utf8mb4_0900_ai_ci</code>），沿用預設會讓 <code>aB3x</code> 與 <code>Ab3X</code> 撞上同一個唯一約束，可用的碼空間退回每位 36 個字元，碼空間與碰撞機率的估算全部失準。開放自訂短碼時，自訂短碼通常希望不分大小寫（使用者會自己打字），兩者的正規化規則要在寫入時就決定。</p>
<p>對照表的一列至少包含：短碼、長網址、擁有者、建立時間、到期時間、狀態，刪除時另記刪除時間。狀態欄位的值包含「正常」「已停用」「已刪除」，轉址路徑每次查詢都要讀它，並依狀態決定回應：正常時轉址；已停用時顯示的頁面（常見的是一頁警告）由需求定義篇〈建立者的範圍與濫用處理〉一節的決定給出；已刪除或已過到期時間時回一頁說明或 404／410，回哪一種由需求定義篇〈連結的保存期限與保存承諾〉一節的決定給出。</p>
<p>幾個需求會改變這張表的鍵與欄位。支援自訂網域時，同一個短碼在不同網域底下是兩條不同的連結，主鍵變成「網域 + 短碼」；服務還要為每一個客戶網域驗證所有權、自動簽發並更新 TLS 憑證（見 <a href="/blog/backend/knowledge-cards/acme-automation/" data-link-title="ACME Automation" data-link-desc="說明網站憑證如何透過 ACME 自動簽發與續期">ACME Automation</a>）。允許修改目的地時，長網址欄位會被更新，同一個短碼在修改前後指向不同的網址，快取的設計因此跟著改變（見〈各層快取與撤回所需的時間〉）；需求定義篇要求保留修改紀錄時，另外存一張以「短碼 + 修改時間」為鍵的修改歷史表。承諾客戶能匯出對照表時，匯出內容至少要有網域、短碼、長網址與狀態，客戶才能在另一個服務上重建同一批連結。</p>
<p>「列出我建立的所有連結」這類查詢以擁有者為條件，和轉址路徑的主鍵查詢是不同的存取形狀。資料量小時加一個擁有者的次要索引就夠；分片之後，次要索引要查所有分片，這時改成另外維護一份以擁有者為鍵的<a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">讀取模型</a>。</p>
<p>被刪除的短碼不重新發給別人。舊的短網址可能還留在某封信、某張海報上，把同一個短碼發給新的連結，持有舊連結的人點下去會被送到一個與原本無關的網址，而新連結的擁有者可以因此取得原本屬於別人的流量。刪除因此做成軟刪除（<a href="/blog/backend/knowledge-cards/soft-delete/" data-link-title="Soft Delete（軟刪除）" data-link-desc="資料表有 deleted_at 欄位、ORM 的刪除改成 UPDATE，或刪掉的資料再建一次卻撞唯一約束、報表數字和頁面對不上時，查軟刪除的過濾條件由誰加、哪些路徑沒有加">Soft Delete</a>）：保留這一列、只把狀態改成「已刪除」，讓這個短碼永遠被佔住、不被重用。佔位需要的只有主鍵（短碼，支援自訂網域時加上網域）、狀態與刪除時間；長網址可能帶著個人資料（例如帶預約編號的就診提醒連結），刪除或到期時把長網址欄位清空，修改歷史裡的舊長網址一併清空。</p>
<h2 id="轉址回應的快取標頭與瀏覽器保存回應的時間">轉址回應的快取標頭與瀏覽器保存回應的時間</h2>
<p><strong>撤回</strong>指讓一條已經發出去的短網址不再轉往原本的目的地，包含停用、刪除、到期失效，以及修改目的地。這幾種操作要生效，都需要點擊的人的請求回到短網址服務，所以撤回能不能做到、多快做到，取決於瀏覽器把轉址結果快取多久。</p>
<p>控制這件事的是回應上的 <a href="/blog/backend/knowledge-cards/cache-control/" data-link-title="Cache-Control（快取控制標頭）" data-link-desc="看到回應帶 Cache-Control、或要決定某個回應能被誰保存、保存多久、使用前要不要先問 origin 時，查這個標頭各指令的分工"><code>Cache-Control</code></a> 標頭；轉址狀態碼只決定沒有帶這個標頭時的預設行為。HTTP 規範（RFC 9110）定義的四個轉址狀態碼在預設行為上不同，其中「允許啟發式快取」指的是：回應沒帶快取標頭時，規範允許快取自行估一個保存時間（見 <a href="/blog/backend/knowledge-cards/heuristic-freshness/" data-link-title="Heuristic Freshness（啟發式新鮮度期限）" data-link-desc="回應沒寫任何快取期限卻仍被瀏覽器或 CDN 保存、改了內容使用者卻還拿到舊版時，查快取自己估期限的規則">Heuristic Freshness</a> 卡）。</p>
<table>
  <thead>
      <tr>
          <th>狀態碼</th>
          <th>語意</th>
          <th>未帶快取標頭時規範是否允許快取</th>
          <th>重送請求時方法是否保留</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>301</td>
          <td>永久移動</td>
          <td>允許（啟發式快取，Chromium 會保存且永不過期）</td>
          <td>歷史上可能把 POST 改成 GET</td>
      </tr>
      <tr>
          <td>308</td>
          <td>永久移動</td>
          <td>允許（啟發式快取，Chromium 會保存且永不過期）</td>
          <td>保留</td>
      </tr>
      <tr>
          <td>302</td>
          <td>暫時移動</td>
          <td>不允許</td>
          <td>歷史上可能把 POST 改成 GET</td>
      </tr>
      <tr>
          <td>307</td>
          <td>暫時移動</td>
          <td>不允許</td>
          <td>保留</td>
      </tr>
  </tbody>
</table>
<p>點擊短網址送出的都是 GET，所以方法保留與否在這裡沒有作用，差別只在「永久」與「暫時」兩種語意，以及沒帶快取標頭時的預設行為。</p>
<p>先看沒帶快取標頭時的預設。各瀏覽器對沒帶快取標頭的 301 估算的期限不同（各實作的規則見 <a href="/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限</a> 的〈各實作的啟發式期限〉），以 Chromium 為例：回 301 時，瀏覽器會把這筆轉址存進快取，之後同一個瀏覽器再點同一條短網址，直接在本機轉往長網址，請求不會送到短網址服務。這有兩個後果：短網址服務看不到這些重複點擊，點擊統計只會記到每個瀏覽器的第一次；之後撤回這條連結，已經快取過的瀏覽器不會知道，會繼續被送往舊的目的地。回 302 時，瀏覽器每一次點擊都會回來問短網址服務，所以每一次點擊都被記到，撤回也對下一次點擊立即生效；代價是重複點擊帶來的負載全部由服務承擔。</p>
<p>明確的快取標頭會蓋過這個預設，轉址回應選標頭的一般規則見 <a href="/blog/backend/12-http-caching/applying-cache-headers/" data-link-title="12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面" data-link-desc="把 HTTP 快取的保存位置、新鮮度、驗證、快取鍵與更新規則用在具體回應上：永久與暫時轉址的狀態碼與快取標頭、需登入的 API 回應、個人化頁面、靜態資源與引用它的頁面，以及檢查實際送出的快取標頭的方法">12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面</a> 的〈轉址回應的狀態碼與快取標頭〉；這裡只寫短網址用到的幾個。回應帶上 <code>Cache-Control: private</code> 時，CDN 與代理伺服器這類共用快取不保存它；帶上 <code>max-age=&lt;秒數&gt;</code> 時，瀏覽器只在這段秒數內直接使用快取的轉址；帶上 <code>no-store</code> 時，瀏覽器完全不保存。所以 max-age 的值就是瀏覽器快取這一層撤回延遲的上限，也是同一個瀏覽器的重複點擊不會被計入的時間窗。</p>
<p>2026-10-02 對 bit.ly 與 t.co 的短網址各送一次 GET 請求，回應都是 301 並帶 <code>private</code> 與很短的 max-age，而不是 302。保留 301 的常見解釋落在狀態碼的語意上：搜尋引擎把永久轉址當成目的地應取代原網址的較強訊號，暫時轉址則是較弱的訊號；而從建立者的角度看，點擊的人該抵達的網頁正是目的地那一頁。業者自己沒有公開說明這個選擇，這是由回應推出的解釋。</p>
<p>這個選擇的依據來自需求。允許修改目的地、連結會到期或可被刪除、要以點擊分析收費、開放任何人建立連結，這幾個需求任何一個成立，轉址回應就不能讓瀏覽器長期快取：用 302，或在 301 上附 <code>private</code> 與很短的 max-age。max-age 要多短，由兩個要求各自算出一個上限，取比較短的那一個：撤回最慢多久要生效，以及可以接受同一個瀏覽器多久之內的重複點擊不被計入。</p>
<p>不帶快取上限的 301 能把重複點擊的負載整個交給瀏覽器，而它只在下列條件全部成立時沒有代價：</p>
<ul>
<li>連結建立後就不再變動，不會到期也不會被刪除</li>
<li>不統計點擊</li>
<li>建立者全是可信的內部使用者</li>
<li>能接受目的地網站日後被入侵時，已經快取的瀏覽器撤不回</li>
<li>預期以上條件日後也不會改變：瀏覽器已經存下的永久轉址不會因為服務改了回應而失效，日後開放建立或改成可修改時，舊回應在那些瀏覽器上撤不回</li>
</ul>
<p>任何一條不成立，就附上 max-age 為撤回設上限。</p>
<h2 id="各層快取與撤回所需的時間">各層快取與撤回所需的時間</h2>
<p>轉址路徑的一次查詢可能在幾個位置得到答案：瀏覽器的快取、CDN 或邊緣節點、每台應用伺服器記憶體裡的程序內快取、共用的應用層快取（例如 Redis），最後才是對照表本身。每多一層快取，轉址延遲與對照表的負載就少一些，撤回時要清的位置也多一處。</p>
<p>CDN 這一層一旦保存轉址回應，被 CDN 直接回應的點擊不會送到應用伺服器，〈點擊紀錄：非同步事件、非人為請求與個人資料〉描述的點擊事件就不會發出。要統計點擊又要讓 CDN 保存轉址，點擊事件要改由 CDN 的存取紀錄或邊緣函式產生；點擊分析是付費功能的服務多半讓 CDN 不保存轉址回應（這就是 <code>Cache-Control: private</code> 的作用），CDN 只負責 TLS 終結與擋掉異常流量。部分 CDN 在回應沒帶快取標頭時會自行替 301 與 302 套一個預設保存時間，所以轉址回應一律明確帶上快取標頭；CDN 端若設定了依狀態碼覆寫保存時間的規則，會蓋過 origin 送出的標頭，CDN 設定也要確認轉址路徑沿用 origin 的快取標頭。</p>
<p>應用層快取用 <a href="/blog/backend/knowledge-cards/cache-aside/" data-link-title="Cache Aside" data-link-desc="說明 application 如何在讀取時自行管理快取與正式資料來源">cache-aside</a>：查快取，沒有就查對照表並寫回快取。一對多分享的連結呈長尾分佈，命中率偏高；替每位收件人各產生一條的連結每條只被點一兩次，命中率接近零，這類流量的轉址容量要直接落在對照表上（見〈建立路徑與轉址路徑的流量形狀與失敗代價〉）。一條爆紅的連結會成為 <a href="/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key</a>，所有請求打在同一個快取鍵、同一個快取節點上；這時在每台應用伺服器的記憶體裡再放一層短 TTL 的程序內快取，把同一個鍵的讀取分散到所有伺服器。這一層分散在每台伺服器上，通常無法從中央清除，所以它的 TTL 也計入撤回延遲。爆紅時負載在各層怎麼分布、爆紅的是惡意連結時保存時間與撤回怎麼折衷，以及建立路徑被機器人大量建立連結時的處置，見 <a href="/blog/backend/00-service-selection/url-shortener-traffic-surge/" data-link-title="0.25 短網址服務的瞬時大流量：爆紅連結、建立路徑的機器人濫用與轉址路徑的攻擊" data-link-desc="短網址服務遇到瞬時大流量時的處置：單一連結被大量轉發時負載落在哪一層、點擊事件管道的積壓與目的地網站承受不住匯集的點擊，爆紅的連結是惡意連結時快取與撤回的衝突，建立路徑被機器人大量建立連結時的成本、檢查管道與准入手段，以及轉址網域遭受掃描或體積型攻擊">0.25 短網址服務的瞬時大流量：爆紅連結、建立路徑的機器人濫用與轉址路徑的攻擊</a>。</p>
<p>不存在的短碼是另一種流量。掃描程式會大量請求隨機產生的短碼，嘗試撞到別人的連結，這些請求在快取裡永遠查不到，每一次都穿過快取打到對照表，形成 <a href="/blog/backend/knowledge-cards/cache-penetration/" data-link-title="Cache Penetration" data-link-desc="說明查詢必定不存在的 key 繞過快取直接打向 origin 的弱點與防護">cache penetration</a>。對策是把「這個短碼不存在」也寫進快取（<a href="/blog/backend/knowledge-cards/negative-cache/" data-link-title="Negative Cache" data-link-desc="說明把「查無此 key」的結果也快取一小段時間，擋掉重複穿透的防護與代價">negative cache</a>），TTL 設短，避免新建立的短碼在 TTL 期間被誤判為不存在（對照表有跨區域讀副本時，剛建立的短碼可能在副本上還查不到；副本查無時先回主節點確認，主節點也查無才寫入 negative cache，建立連結時同時刪除這個短碼的 negative cache 鍵）；同時，單一來源在一段時間內收到的 404 超過門檻時，<a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">限制</a>這個來源的請求速率，單一來源大量的 404 本身就是掃描行為的可觀察訊號。</p>
<p>撤回一條連結時，共用的應用層快取與 CDN 可以主動清除（見 <a href="/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">cache invalidation</a>），瀏覽器快取與程序內快取只能等各自的到期時間。所以從送出撤回到所有點擊都看到結果，最長要經過的時間由各層的快取時間決定，而且不只是其中最長的那一個：過期之後允許延用舊回應的窗口（HTTP 的 <code>stale-while-revalidate</code>、<code>stale-if-error</code> 指令，以及本節後面應用層的 stale-if-error 做法：對照表查不到時延用過期的快取）會再延長一段，上游不送 <code>Age</code> 又把 <code>Date</code> 換成送出當下時間的層，會讓各層的期限沿路相加，算法見 <a href="/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期</a> 的〈等待到期與改版全面生效的時間〉。這個數字是工程實作對需求「被檢舉的連結多久之內要失效」的回答：需求要求的時限比某一層的快取時間短，那一層的快取時間就要調短，或者那一層不能放這條連結。</p>
<p>對照表的連結內容不再變動時（不允許修改目的地），快取裡的資料只有「存在且正確」與「已被撤回（停用、刪除或到期）」兩種狀態，快取不一致造成的後果只剩撤回晚一點生效。這讓轉址路徑可以採用 <a href="/blog/backend/knowledge-cards/stale-if-error/" data-link-title="Stale-If-Error" data-link-desc="HTTP cache-control directive、origin 出錯時用舊版頂著、確保使用者拿到有效回應">stale-if-error</a>：對照表暫時查不到時，繼續用快取裡過期的那一筆轉址，對照表的故障因此不會讓轉址跟著失敗。允許修改目的地之後，過期的快取可能把人送到舊的目的地，這個做法的代價跟著變高。</p>
<h2 id="點擊紀錄非同步事件非人為請求與個人資料">點擊紀錄：非同步事件、非人為請求與個人資料</h2>
<p>每次轉址都要記錄一筆點擊時，最直接的做法是在回應之前把這筆點擊寫進資料庫。這樣做會讓轉址延遲包含一次寫入延遲，資料庫的寫入量等於全站點擊量，而且資料庫寫入變慢或失敗時，轉址也跟著變慢或失敗——附加功能的故障傳到了最需要可用性的路徑上。</p>
<p>比較常見的做法是轉址時只發出一筆點擊事件：寫進本機的緩衝或送到訊息佇列，然後立即回應轉址。背景的消費者讀取這些事件，按小時彙總（<a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a>），同時按連結、國家、裝置這幾個維度分組計數，報表查的是彙總後的結果，不是原始事件。重送的事件怎麼不被算兩次、彙總怎麼寫才能重跑，見 <a href="/blog/backend/01-database/high-frequency-counting/" data-link-title="1.18 高頻計數的寫入與彙總：熱點列的鎖、只新增的事件、可以重跑的彙總與重送的事件" data-link-desc="熱點列更新的吞吐量上限與它的成因、拆成多列／只新增事件／先在記憶體裡合併的差別、只新增事件搬去哪裡的成本、累加與重算在什麼條件下漏算或重複計算、讓彙總排程互斥的建議鎖與連線池的交互，以及重送事件的冪等寫入、推導事實的存放位置，以及 Redis 承擔即時計數、跨小時不重複數、去重與互斥時各自的邊界">1.18 高頻計數的寫入與彙總：熱點列的鎖、只新增的事件、可以重跑的彙總與重送的事件</a>。</p>
<p>事件遺失可以容忍到什麼程度，取決於點擊資料在商業模型裡的位置。點擊分析就是付費功能時，少算的點擊等於少交付了客戶付費購買的功能，事件要寫進 <a href="/blog/backend/knowledge-cards/durable-queue/" data-link-title="Durable Queue" data-link-desc="說明可持久化的 queue 如何在重啟與失敗後保留待處理工作">durable queue</a>；點擊統計只是平台自己參考用時，伺服器當機時遺失緩衝區裡還沒送出的點擊事件不影響任何承諾，甚至只取樣記錄也可以。</p>
<h3 id="預覽抓取與安全掃描等非人為請求的標記">預覽抓取與安全掃描等非人為請求的標記</h3>
<p>把連結貼進聊天軟體時，聊天軟體會先抓取那個網址來產生預覽，抓取的可能是聊天軟體的伺服器，也可能是傳送者自己的裝置；企業郵件的安全閘道會預先開啟信件裡的連結來掃描內容；搜尋引擎與各種爬蟲也會跟著連結走。這些非人為的請求和真人點擊一樣經過轉址路徑。處理方式是依 User-Agent（預覽抓取多半自報身分，例如聊天軟體的連結展開爬蟲）、請求方法（HEAD 請求不算點擊）與來源網段把這類請求標記出來、分開計數，而不是在轉址當下丟棄，因為辨識規則會改，原始事件留著才能重算。</p>
<h3 id="點擊事件裡的個人資料與保存期限">點擊事件裡的個人資料與保存期限</h3>
<p>點擊事件裡的 IP 位址與裝置資訊屬於 <a href="/blog/backend/knowledge-cards/pii/" data-link-title="PII" data-link-desc="說明可識別個人的資料如何影響權限、遮罩、保留與稽核">PII</a>。報表需要的通常只是由 IP 推出的國家或城市，所以彙總時只保留推導出來的欄位，原始事件設定明確的保存期限（見 <a href="/blog/backend/knowledge-cards/data-lifecycle/" data-link-title="Data Lifecycle" data-link-desc="說明資料從建立、使用、保留到刪除的責任邊界">資料生命週期</a>），到期刪除，隱私責任隨保存的範圍縮小。</p>
<h2 id="濫用防護建立時的檢查建立後的重查與轉址時的比對">濫用防護：建立時的檢查、建立後的重查與轉址時的比對</h2>
<p>短網址把真正的目的地藏在一個看不出內容的網址後面，這正是釣魚與惡意軟體散布者需要的性質。濫用對短網址服務的傷害有兩層：直接的那一層是使用者被騙；間接的那一層是服務網域的名聲受損。郵件過濾器、瀏覽器的安全警告與社群平台判斷一個網址可不可信時，常以網域為單位，短網址服務的網域被大量用來散布釣魚連結之後，整個網域可能被列入封鎖清單，所有客戶的正常連結同時失效。這種封鎖發生在郵件過濾器與瀏覽器那一端，服務自己的監控看到的轉址仍然全部成功，最先察覺的往往是客戶回報信件被擋；匿名建立的服務沒有客戶可以回報，所以要靠外部監測主動發現：定期用公開的網址信譽查詢服務查自己的轉址網域，並監看服務自己寄出的通知信與客戶轉交的退信裡的封鎖原因。濫用防護因此同時是可用性問題。這類「合法功能被轉用成攻擊工具」的情境，在設計階段可以用 <a href="/blog/backend/knowledge-cards/abuse-case/" data-link-title="Abuse Case" data-link-desc="說明合法功能如何被惡意轉用成突破或濫用路徑">abuse case</a> 的方式逐條列出來。</p>
<p>建立時的檢查包括：把長網址送去查惡意網址黑名單、限制單一帳號與單一來源的建立速率、要求登入或驗證後才能建立。這些檢查只能判斷建立當下的狀態，而長網址指向的頁面內容由別人控制，可以在檢查通過之後才換成釣魚頁。所以檢查要在建立之後持續進行：定期對既有連結重新查詢黑名單，並提供檢舉管道。</p>
<p>檢查也可以放進轉址路徑本身：轉址時拿長網址比對一份危險網址清單，命中就改為顯示警告頁。這份清單要放在轉址路徑的本機記憶體或快取裡，由背景程序定期更新，不在每次轉址時呼叫外部服務，否則外部服務的延遲與故障會直接傳到轉址路徑上。清單更新失敗時，轉址繼續用記憶體裡最後一份成功取得的清單，並把更新失敗設成告警；連舊清單都沒有時（例如程序剛啟動），才要在放行（fail-open）與阻擋（fail-closed）之間選一個。放行保住轉址可用性，代價是這段時間的惡意連結照常轉址；阻擋保住安全，代價是所有連結暫時失效。只限內部使用者建立時，依轉址路徑可用性優先的取捨方向選放行；開放匿名建立時惡意連結的比例高，可以只對最近建立、還沒通過背景重查的連結阻擋，其餘放行；開放註冊或經由 API 代建時介於兩者之間，可以只對最近建立、還沒通過背景重查、且建立者帳號或金鑰尚無紀錄的連結阻擋。</p>
<p>確認是惡意連結之後，把對照表的狀態改成「已停用」，轉址路徑讀到這個狀態時改為顯示一頁警告，不再轉往原目的地。這個停用要多快對所有點擊生效，就是〈各層快取與撤回所需的時間〉那一節處理的問題，濫用防護與快取標頭、各層快取時間的選擇在這裡是同一個決定。</p>
<p>登入、帳號設定這類頁面放在另一個可註冊網域（不是同一網域底下的兩個子網域），短網址的網域只用來做轉址，理由有兩個。短網址網域被封鎖時，使用者仍然登得進管理介面。轉址網域上仍會有服務自己產生的頁面（警告頁、說明頁、預覽頁），其中一頁若被注入腳本，而兩者分屬不同的 <a href="/blog/backend/knowledge-cards/registrable-domain/" data-link-title="Registrable Domain" data-link-desc="判斷兩個主機名算不算同一方、或某個跨站防護的信任邊界實際涵蓋誰時，用來定位「站」的切點在哪一層">registrable domain</a>，那段腳本讀不到、也改不了管理介面的登入 cookie。機器人大量建立連結、把檢查配額用完時這幾層檢查怎麼運作，見 <a href="/blog/backend/00-service-selection/url-shortener-traffic-surge/" data-link-title="0.25 短網址服務的瞬時大流量：爆紅連結、建立路徑的機器人濫用與轉址路徑的攻擊" data-link-desc="短網址服務遇到瞬時大流量時的處置：單一連結被大量轉發時負載落在哪一層、點擊事件管道的積壓與目的地網站承受不住匯集的點擊，爆紅的連結是惡意連結時快取與撤回的衝突，建立路徑被機器人大量建立連結時的成本、檢查管道與准入手段，以及轉址網域遭受掃描或體積型攻擊">0.25 短網址服務的瞬時大流量：爆紅連結、建立路徑的機器人濫用與轉址路徑的攻擊</a> 的〈建立路徑被機器人大量建立連結〉。</p>
<h2 id="服務結束時的轉址延續">服務結束時的轉址延續</h2>
<p>短網址服務的全部價值在外部引用上：短網址寫在別人的文件、印刷品與貼文裡，持有者幾乎都是聯絡不到的第三方（外部引用與持有者的分類見 <a href="/blog/backend/10-system-evolution/service-termination-scope/" data-link-title="10.4 服務終止的範圍：外部引用的種類與持有者" data-link-desc="服務本身要結束時，盤點它發出去、存在別人手上的外部引用：網址、寫在別人程式或韌體裡的端點、被嵌入的程式碼網址、名稱與綁在網域上的身分；持有者依聯絡得到的程度分成直接客戶、間接使用者與第三方持有者；引用在服務結束後的結局：失效或易手">10.4 服務終止的範圍：外部引用的種類與持有者</a>）。服務結束或失去出資時，讓這些連結繼續運作所需的東西是對照表與短網址所在的網域；支援自訂網域時，還有每個客戶網域指向服務的那筆 DNS 紀錄。實作上要預留的，是讓這些在沒有原本的應用程式與團隊時仍能運作。</p>
<p><strong>對照表匯出成靜態轉址規則</strong>。轉址路徑只做一件事：用短碼查長網址、回一個 3xx。這件事不需要資料庫與應用伺服器，可以把對照表輸出成物件儲存或 CDN 能直接回應的轉址規則（例如每個短碼一個帶轉址設定的物件，或 CDN 的轉址規則清單），讓服務退到只剩靜態檔的狀態。已停用與已刪除的連結在匯出時改成指向說明頁或回 404 的紀錄（依需求定義篇對刪除後顯示什麼的決定），日後有人接手、恢復發號時，這些短碼也不會被發給新的連結（名稱佔位的做法見 <a href="/blog/backend/10-system-evolution/namespace-and-domain-disposition/" data-link-title="10.6 命名空間與網域的處置：續約、名稱保留與回收、懸空 DNS" data-link-desc="服務結束時網域與名稱的去向，以及它們決定外部引用是失效還是易手：網域的續約與出售、交給中立方的技術控制、套件與雲端資源名稱釋出後被重新申請與佔位、雲端資源刪除後的懸空 DNS 與子網域接管，以及綁在可過期網域上的帳號身分">10.6 命名空間與網域的處置：續約、名稱保留與回收、懸空 DNS</a> 的〈名稱的保留與回收：套件名稱、雲端資源名稱與短碼〉）。匯出的靜態規則要沿用〈轉址回應的快取標頭與瀏覽器保存回應的時間〉決定的狀態碼與 Cache-Control；物件儲存或 CDN 的轉址功能預設回什麼狀態碼、帶不帶快取標頭，要查所用服務的文件，預設回 301 且不帶快取標頭時，已經點過的瀏覽器收不到之後的單筆改寫。退到這個狀態之後，成本是網域續約、儲存與流量，加上用單筆改寫處理濫用回報，以及〈自訂網域的連結〉一段的 TLS 憑證更新與綁定保留，各階段怎麼安排見 <a href="/blog/backend/10-system-evolution/staged-shutdown/" data-link-title="10.7 分階段退場：停止新建、唯讀、靜態封存、關閉與公告期" data-link-desc="服務結束時把一次全部關閉拆成幾個階段：停止新建、唯讀、靜態封存與關閉各保留什麼、對哪一類持有者有效、維運成本降到哪裡；依使用紀錄決定保留範圍；推動客戶遷移的手段；以及公告期長度、通知管道與補償的安排">10.7 分階段退場：停止新建、唯讀、靜態封存、關閉與公告期</a>。要讓這一步在結束時只是執行一次匯出，對照表的格式與匯出程式在服務運作期間就要存在。靜態規則也要能單筆改寫：〈濫用防護：建立時的檢查、建立後的重查與轉址時的比對〉一節在轉址時比對危險網址清單的那一層，退到靜態檔之後就不存在了，所以封存之後收到的濫用回報、以及目的地網域到期被別人註冊的連結，要靠逐筆把規則改成說明頁或 404 處理。</p>
<p><strong>網域的去向</strong>。靜態轉址規則只在網域還指向它時有用；網域到期被別人註冊，舊連結全部落入新註冊者的控制。退場計畫要寫明網域由誰繼續續約，或在服務運作期間就和封存機構約定，關站時把網域的技術控制交給對方，見 <a href="/blog/backend/10-system-evolution/namespace-and-domain-disposition/" data-link-title="10.6 命名空間與網域的處置：續約、名稱保留與回收、懸空 DNS" data-link-desc="服務結束時網域與名稱的去向，以及它們決定外部引用是失效還是易手：網域的續約與出售、交給中立方的技術控制、套件與雲端資源名稱釋出後被重新申請與佔位、雲端資源刪除後的懸空 DNS 與子網域接管，以及綁在可過期網域上的帳號身分">10.6 命名空間與網域的處置：續約、名稱保留與回收、懸空 DNS</a> 與 <a href="/blog/backend/10-system-evolution/handover-and-archival/" data-link-title="10.8 移交：資料匯出、開源與交給社群或封存機構" data-link-desc="服務結束時把讓外部引用繼續運作所需的資料、程式碼與網域交出去：交給直接客戶的資料匯出、開源程式碼能不能獨立運作、社群接手需要提供者事先留下什麼、封存機構的兩種取得方式（業者合作交出與外部爬取），以及交不出去的部分">10.8 移交：資料匯出、開源與交給社群或封存機構</a>。</p>
<p><strong>自訂網域的連結</strong>。客戶網域底下的連結用的是客戶自己的網域，由客戶設定的 CNAME 指向服務。服務退場時，對照表要交給網域所屬的客戶，並通知客戶在指定日期前改指或刪除那筆 CNAME；客戶確認之前，提供者保留那個主機名或 CDN 上的網域綁定，否則那筆紀錄會懸空、可能被別人接管，見 <a href="/blog/backend/10-system-evolution/namespace-and-domain-disposition/" data-link-title="10.6 命名空間與網域的處置：續約、名稱保留與回收、懸空 DNS" data-link-desc="服務結束時網域與名稱的去向，以及它們決定外部引用是失效還是易手：網域的續約與出售、交給中立方的技術控制、套件與雲端資源名稱釋出後被重新申請與佔位、雲端資源刪除後的懸空 DNS 與子網域接管，以及綁在可過期網域上的帳號身分">10.6 命名空間與網域的處置：續約、名稱保留與回收、懸空 DNS</a> 的〈懸空 DNS 與子網域接管〉。客戶一直沒有回應時，提供者可以定期查詢客戶主機名的公開 DNS 紀錄，確認它不再指向服務才釋出綁定；客戶始終沒改，綁定就跟著服務網域一起依 10.6 續約不回應的做法延續，不在網域放手時一起釋出。靜態封存期間，這些網域的 TLS 憑證也要繼續更新。</p>
<p><strong>依點擊紀錄決定保留範圍</strong>。〈點擊紀錄：非同步事件、非人為請求與個人資料〉一節的資料，在退場時用來回答哪些連結還有人在點：依最後一次點擊的時間把連結分成要保留轉址的與可以停用的，停用的連結先顯示一段時間的中間頁，讓點擊的人知道連結即將失效。goo.gl 從 2024 年 8 月起先對部分連結顯示中間頁；Google 2025 年 8 月修改停用計畫時說明，2024 年底沒有活動的連結在九個月前已經改導到告知即將停用的訊息頁，修改後只停用這一批，案例見 <a href="/blog/backend/10-system-evolution/staged-shutdown/" data-link-title="10.7 分階段退場：停止新建、唯讀、靜態封存、關閉與公告期" data-link-desc="服務結束時把一次全部關閉拆成幾個階段：停止新建、唯讀、靜態封存與關閉各保留什麼、對哪一類持有者有效、維運成本降到哪裡；依使用紀錄決定保留範圍；推動客戶遷移的手段；以及公告期長度、通知管道與補償的安排">10.7 分階段退場：停止新建、唯讀、靜態封存、關閉與公告期</a> 的〈依使用紀錄決定保留範圍〉。只為這個用途保存點擊資料時，保存每條連結的最後點擊時間就夠了，不必保存個別點擊的原始紀錄。判定要用一個固定的時點：goo.gl 以 2024 年底有沒有活動為準，被導到停用訊息的連結就是要停用的那一批，所以中間頁上「即將失效」的告知是確定的；若讓點擊中間頁的請求繼續更新最後點擊時間，每一條被告知的連結都會因此重新算成有人在用。點擊紀錄也要先排除爬蟲、連結預覽與掃描短碼空間的請求（〈點擊紀錄：非同步事件、非人為請求與個人資料〉一節），否則每條連結看起來都還有人在用；退到靜態檔之後若要繼續累積紀錄，來源改成 CDN 或物件儲存的存取紀錄，同樣先排除這些請求。</p>
<h2 id="各項需求決定對應的實作位置">各項需求決定對應的實作位置</h2>
<p>本篇的實作建立在需求定義篇推導出的需求決定上，其中任何一個換掉，對應的實作位置就要重新選：</p>
<ul>
<li><strong>要不要統計點擊</strong>：從「不統計」改成「統計且是付費功能」，轉址回應要從可被瀏覽器長期快取，改成讓每一次點擊（或只隔很短時間窗的重複點擊）都回到服務：302，或 301 加 <code>private</code> 與短 max-age。之前發出的回應沒帶快取上限時，已快取那批連結的瀏覽器不在新規則的作用範圍內，它們的重複點擊仍然記不到。點擊事件要從可取樣改成寫進 durable queue。事件消費者的去重與重放安全在 <a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計與去重</a> 的〈checkpoint、offset 與 idempotency〉與〈replay safety〉兩節展開。</li>
<li><strong>連結能不能修改目的地</strong>：從「不能」改成「能」，快取不能再把對照表當成不變的資料，stale-if-error 的代價變高，每一層快取都要能被主動清除或縮短快取時間，對照表另存修改歷史。之前發出的回應沒帶快取上限時，已快取的瀏覽器撤不回，這批連結要另外評估；短碼由長網址雜湊截斷產生、而且同一個長網址共用一列時，要先拆成每位擁有者各自一列才能開放修改，否則改一條就改到所有共用這個短碼的人。失效策略在 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside 與失效策略</a> 的〈失效策略〉一節，快取時間怎麼訂在 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL 與 eviction</a>。</li>
<li><strong>誰可以建立連結</strong>：從內部使用者改成開放註冊或匿名，濫用防護從可有可無變成可用性的前提，轉址回應同時被撤回需求鎖定在 302 或短 max-age。濫用情境怎麼在設計階段列出來，見 <a href="/blog/backend/knowledge-cards/abuse-case/" data-link-title="Abuse Case" data-link-desc="說明合法功能如何被惡意轉用成突破或濫用路徑">abuse case</a> 卡。</li>
<li><strong>短碼會不會承載私密連結</strong>：使用者會縮短「知道網址就能存取」的連結時，遞增計數器只有在編碼前經過帶金鑰的置換（例如保留格式的加密）時才可用，否則短碼生成改成隨機產生。</li>
<li><strong>連結保存多久</strong>：決定對照表的儲存量、已到期與已刪除連結的回應，以及點擊原始事件的保存期限；服務本身結束時的匯出與網域去向見〈服務結束時的轉址延續〉；資料量增長到需要分片時，分片鍵怎麼設計在 <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇，以及加 region 之後寫入容量怎麼跟著乘上去">1.10 KV / Document DB 容量規劃</a> 的〈Partition key 設計：容量的命脈〉一節。</li>
<li><strong>連結是一對多分享還是替每位收件人各產生一條</strong>：改成後者時，轉址容量按單次發送批量估，應用層快取不再分擔讀取，建立嵌在發送流程裡時建立路徑的可用性跟著發送路徑訂。</li>
<li><strong>支不支援自訂網域</strong>：從不支援改成支援，對照表主鍵從短碼改成「網域 + 短碼」，分片鍵跟著改，並要加上網域所有權驗證與 TLS 憑證自動化。</li>
</ul>
<p>其中短碼會不會承載私密連結，由使用情境決定、與收入模型無關；點擊統計、保存承諾、建立者範圍與自訂網域的商業依據，在需求定義篇的〈點擊資料的地位：付費功能或內部參考，以及對應的規格〉〈連結的保存期限與保存承諾〉〈建立者的範圍與濫用處理〉〈自訂網域與網域名聲的隔離〉四節。這些依據背後的出資方式，見 <a href="/blog/business/case-analyses/url-shortener-business-models/" data-link-title="短網址的商業模型：訂閱、自架、平台內建與廣告插頁各由誰出資維持轉址" data-link-desc="拆解維持短網址轉址的錢從哪裡來：建立連結的人為點擊分析、品牌網域或託管服務付訂閱費，自架者自己付主機與網域，平台由母公司出資，廣告插頁由廣告主出資；連結能活多久跟著出資者走，各出資方式交給工程的規格也不同">短網址的商業模型：訂閱、自架、平台內建與廣告插頁各由誰出資維持轉址</a>。</p>
]]></content:encoded></item><item><title>0.25 短網址服務的瞬時大流量：爆紅連結、建立路徑的機器人濫用與轉址路徑的攻擊</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/url-shortener-traffic-surge/</link><pubDate>Fri, 02 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/url-shortener-traffic-surge/</guid><description>&lt;p>這篇整理短網址服務遇到瞬時大流量時的處置，前提是 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/url-shortener-implementation/" data-link-title="0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄" data-link-desc="以需求已定案為前提的實作取捨：建立與轉址兩條路徑的流量形狀、各種短碼生成方式的可猜測性與協調成本、轉址狀態碼與各層快取怎麼影響停用或修改連結多久生效、點擊事件的非同步紀錄、濫用防護、服務結束時的靜態轉址與網域去向，以及換掉任一項需求決定時要重選的實作位置">0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄&lt;/a> 的實作已經存在：建立路徑與轉址路徑分開、各層快取、非同步的點擊事件與濫用防護。不限於短網址的通用機制（流量來源的辨識、擴展緩衝、負載卸除、准入控制、體積型攻擊）見 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/unplanned-traffic-surge/" data-link-title="9.15 無預警瞬時大流量：流量來源辨識、擴展緩衝、請求優先等級、准入控制、體積型攻擊與退路" data-link-desc="沒有準備窗口的瞬時大流量（突發流量、流量暴增）怎麼承接：依流量形狀分辨真人需求、應用層的濫用流量、體積型攻擊（DDoS），自動擴展的反應時間換算成緩衝，入口標記請求優先等級與負載卸除的順序，等候室與事前註冊這類准入控制，把體積型攻擊交給更大的邊緣網路，以及擴展跟不上時的退路">9.15 無預警瞬時大流量：流量來源辨識、擴展緩衝、請求優先等級、准入控制、體積型攻擊與退路&lt;/a>，這篇只寫它們落在短網址服務上的形狀。&lt;/p>
&lt;p>短網址服務的瞬時大流量有三種來源，落在不同的路徑上：一條連結被大量轉發，流量落在轉址路徑上而且集中在單一短碼；機器人大量建立連結，流量落在建立路徑上；有人掃描短碼或攻擊轉址網域，流量落在轉址路徑上而且分散在不存在的短碼或整個入口。三者在 9.15 的分類裡分別是真人需求、應用層的濫用流量，以及應用層的濫用流量或體積型攻擊。&lt;/p>
&lt;h2 id="爆紅連結的負載落點cdn程序內快取與對照表">爆紅連結的負載落點：CDN、程序內快取與對照表&lt;/h2>
&lt;p>一條連結被放進大量轉發的貼文之後，幾小時內的點擊數可能超過整個服務平常一天的量，而且全部打在同一個短碼上。這些點擊預設落在哪一層，由需求定義裡點擊資料的地位決定：點擊分析是不是要對客戶交付的功能（&lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/url-shortener-requirements/" data-link-title="0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格" data-link-desc="短網址服務交給工程之前要定案的需求決定：自建或購買、點擊資料的地位、連結的保存承諾、目的地能否修改、建立者的範圍、短碼的可猜測性、網域策略與可用性分級；多數答案由商業模型推出，目的地修改、短碼可猜測性與可用性分級由使用情境決定">0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格&lt;/a> 的〈點擊資料的地位：付費功能或內部參考，以及對應的規格〉）；同一篇〈建立者的範圍與濫用處理〉的撤回時限，則限制了任何一層能保存轉址多久。&lt;/p>
&lt;p>不統計點擊、或點擊只是內部參考時，轉址回應可以讓 CDN 保存，爆紅連結的點擊絕大多數由 CDN 直接回應，應用伺服器只在 CDN 的副本到期時收到請求（每個邊緣節點各一次，有 origin shield（CDN 內部集中回源的中間層，見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源（CDN / Origin Protection）&lt;/a> 的〈Origin Protection 的設計責任〉）這類中間層時才接近整體一次）。點擊只是內部參考時，點擊事件改由 CDN 的存取紀錄產生，應用伺服器不再發出（0.24 的〈各層快取與撤回所需的時間〉）。CDN 保存轉址時，爆紅幾乎不會影響後端，承受流量的是 CDN 的頻寬，CDN 的流量費用隨點擊數增加。&lt;/p>
&lt;p>點擊分析是付費功能時，轉址回應多半帶 &lt;code>Cache-Control: private&lt;/code>，CDN 不保存，每一次點擊都送到應用伺服器。0.24 的〈各層快取與撤回所需的時間〉已經處理這條連結成為 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key&lt;/a> 的情形：每台應用伺服器的記憶體裡放一層短 TTL 的程序內快取，讓同一個短碼的讀取分散到所有伺服器，而不是全部打在同一個共用快取節點上。剩下的瓶頸是應用伺服器的數量本身，伺服器數量要靠自動擴展追上；爆紅的上升速度比自動擴展的反應時間快時，差距由擴展緩衝承接，緩衝的算法見 9.15 的〈自動擴展（autoscaling）的反應時間與緩衝〉。&lt;/p>
&lt;p>這個預設在爆紅時有一個例外做法：平常不讓 CDN 保存轉址，origin 統計單一短碼的請求速率（計數放在各台應用伺服器共用的計數器裡，或每台依伺服器台數換算門檻），超過門檻時，只對那一個短碼讓 CDN 保存幾秒到幾十秒，點擊事件改由 CDN 的存取紀錄補回；只補回存取紀錄裡由 CDN 直接回應（快取命中）的那幾筆，回源到 origin 的請求 origin 已經發過點擊事件，全部補回會重複計數。最直接的做法是 origin 只在這個短碼的回應裡，把 &lt;code>private&lt;/code> 換成 &lt;code>s-maxage=&amp;lt;秒數&amp;gt;&lt;/code>：&lt;code>private&lt;/code> 會禁止 CDN 保存，留著它 &lt;code>s-maxage&lt;/code> 不會生效；&lt;code>max-age&lt;/code> 照舊留給瀏覽器，不必改 CDN 設定（兩個指令見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限&lt;/a>）。開啟之後，origin 看到的這個短碼請求速率會掉到門檻以下，所以關閉的條件不能用 origin 端的速率，否則保存規則會一開一關地擺盪；開啟後給一段固定的保存期，或改看 CDN 存取紀錄算出的總速率再決定關閉。這樣做的前提是 CDN 遵循 origin 的 &lt;code>s-maxage&lt;/code>、存取紀錄能在可接受的延遲內送回，而且這套切換在流量來之前就要建好並演練過，當下只是觸發它。&lt;/p>
&lt;h2 id="點擊事件管道的積壓與非人為請求">點擊事件管道的積壓與非人為請求&lt;/h2>
&lt;p>轉址路徑撐住之後，壓力往後移到點擊事件的管道。0.24 把點擊紀錄設計成轉址時只發出一筆事件、由背景消費者彙總，所以爆紅時轉址不會變慢，佇列裡未處理的事件數（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth&lt;/a>）會上升，消費者處理到的位置落後最新事件越來越多（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a>）。客戶在報表上看到的是點擊數晚了幾分鐘到幾小時才更新。&lt;/p>
&lt;p>這個積壓要不要處理、處理到什麼程度，看的是點擊報表的時效承諾。點擊分析是付費功能、報表承諾近即時時，消費者要能跟著事件量擴展，佇列的保存時間要長過最長的預期積壓，否則事件在被處理之前就過期遺失；點擊只是內部參考時，積壓只是報表晚一點，甚至可以在尖峰時改為取樣記錄：只保存部分點擊事件，報表依取樣比例還原（技術上同 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling&lt;/a>），尖峰過後再恢復全量。佇列滿了的時候，兩種情形都是轉址照常回應，這一點 0.24 已經決定：點擊統計失敗不能拖垮轉址。差別在點擊事件的去處：點擊只是內部參考時，可以丟棄或取樣；點擊分析是付費功能時，0.23 要求事件不能遺失，事件要改寫到另一個持久的緩衝，或事後由 CDN 與負載平衡器的存取紀錄補回。&lt;/p>
&lt;p>爆紅的連結也會帶來一波非人為請求。連結被貼進聊天軟體、社群平台時，平台會抓取網址產生預覽；多數平台在伺服器端依網址快取預覽，同一個網址只抓少數幾次；由傳送者自己的裝置產生預覽的平台，預覽抓取會隨轉發次數增加，而且集中在連結剛開始擴散的那段時間。0.24 的〈預覽抓取與安全掃描等非人為請求的標記〉把這類請求依 User-Agent、請求方法與來源網段標記出來、分開計數。爆紅時這份標記決定了客戶看到的點擊數有多少是真人，所以標記規則要在平常就涵蓋主要的聊天與社群平台。&lt;/p>
&lt;h2 id="目的地網站承接匯集點擊的能力">目的地網站承接匯集點擊的能力&lt;/h2>
&lt;p>短網址服務撐住了，流量仍然要到達目的地網站，而目的地網站往往比短網址服務小得多。爆紅連結把所有點擊匯集到同一個目的地，對方的伺服器可能因此回應變慢或錯誤。這時短網址服務的監控看到的是轉址全部成功，看不到使用者實際到不到得了目的地。&lt;/p>
&lt;p>目的地網站承接不了匯集的點擊時，短網址服務能做的有限，因為目的地不在它的控制範圍內。它能做的是兩件事：在點擊報表上說明點擊數是轉址數，不等於成功到達目的地的人數；以及在建立介面與文件裡提醒客戶，把一條短網址放進大量曝光的位置之前，目的地網站要先依 &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>爆紅最危險的情形是那條連結指向釣魚頁或惡意軟體。0.24 的濫用防護在確認惡意之後把對照表的狀態改成已停用，而停用多快對所有點擊生效，由各層快取的保存時間決定。〈爆紅連結的負載落點：CDN、程序內快取與對照表〉那一節為了扛住爆紅，要讓轉址回應盡量被 CDN 與程序內快取保存；撤回惡意連結時為了盡快生效，要讓轉址回應盡量不被保存，兩個目標在同一條連結上衝突。&lt;/p>
&lt;p>這個衝突最尖銳的是 CDN 有保存轉址的時候（不統計點擊，或依短碼切換保存的做法）；點擊分析是付費功能、CDN 不保存時，同樣的取捨落在程序內快取的 TTL 上，撤回的時效由它與瀏覽器的保存時間決定。CDN 有保存時，兩者的折衷是保存時間短、而且能主動清除。CDN 保存轉址的時間設在幾秒到幾十秒，同時保留依路徑主動清除（purge）單一短碼的能力（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">cache invalidation&lt;/a>）；程序內快取在沒有建立廣播失效機制時無法從中央清除，所以它的 TTL 要短到符合需求定義的撤回時限；瀏覽器那一層只能靠轉址回應的快取標頭事先限制，301 不帶快取標頭時瀏覽器可能永久保存，已經點過的人之後看不到停用後的警告頁（見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/applying-cache-headers/" data-link-title="12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面" data-link-desc="把 HTTP 快取的保存位置、新鮮度、驗證、快取鍵與更新規則用在具體回應上：永久與暫時轉址的狀態碼與快取標頭、需登入的 API 回應、個人化頁面、靜態資源與引用它的頁面，以及檢查實際送出的快取標頭的方法">12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面&lt;/a>）。爆紅期間的請求量大，幾秒的保存時間就足以讓絕大部分點擊由快取回應，所以短的保存時間對扛流量的效果損失不大，換來的是 CDN 這一層的停用能在幾秒內生效；整體撤回時間仍要加上程序內快取的 TTL 與瀏覽器的保存時間。&lt;/p></description><content:encoded><![CDATA[<p>這篇整理短網址服務遇到瞬時大流量時的處置，前提是 <a href="/blog/backend/00-service-selection/url-shortener-implementation/" data-link-title="0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄" data-link-desc="以需求已定案為前提的實作取捨：建立與轉址兩條路徑的流量形狀、各種短碼生成方式的可猜測性與協調成本、轉址狀態碼與各層快取怎麼影響停用或修改連結多久生效、點擊事件的非同步紀錄、濫用防護、服務結束時的靜態轉址與網域去向，以及換掉任一項需求決定時要重選的實作位置">0.24 短網址服務的實作：短碼生成、對照表儲存、轉址快取與點擊紀錄</a> 的實作已經存在：建立路徑與轉址路徑分開、各層快取、非同步的點擊事件與濫用防護。不限於短網址的通用機制（流量來源的辨識、擴展緩衝、負載卸除、准入控制、體積型攻擊）見 <a href="/blog/backend/09-performance-capacity/unplanned-traffic-surge/" data-link-title="9.15 無預警瞬時大流量：流量來源辨識、擴展緩衝、請求優先等級、准入控制、體積型攻擊與退路" data-link-desc="沒有準備窗口的瞬時大流量（突發流量、流量暴增）怎麼承接：依流量形狀分辨真人需求、應用層的濫用流量、體積型攻擊（DDoS），自動擴展的反應時間換算成緩衝，入口標記請求優先等級與負載卸除的順序，等候室與事前註冊這類准入控制，把體積型攻擊交給更大的邊緣網路，以及擴展跟不上時的退路">9.15 無預警瞬時大流量：流量來源辨識、擴展緩衝、請求優先等級、准入控制、體積型攻擊與退路</a>，這篇只寫它們落在短網址服務上的形狀。</p>
<p>短網址服務的瞬時大流量有三種來源，落在不同的路徑上：一條連結被大量轉發，流量落在轉址路徑上而且集中在單一短碼；機器人大量建立連結，流量落在建立路徑上；有人掃描短碼或攻擊轉址網域，流量落在轉址路徑上而且分散在不存在的短碼或整個入口。三者在 9.15 的分類裡分別是真人需求、應用層的濫用流量，以及應用層的濫用流量或體積型攻擊。</p>
<h2 id="爆紅連結的負載落點cdn程序內快取與對照表">爆紅連結的負載落點：CDN、程序內快取與對照表</h2>
<p>一條連結被放進大量轉發的貼文之後，幾小時內的點擊數可能超過整個服務平常一天的量，而且全部打在同一個短碼上。這些點擊預設落在哪一層，由需求定義裡點擊資料的地位決定：點擊分析是不是要對客戶交付的功能（<a href="/blog/backend/00-service-selection/url-shortener-requirements/" data-link-title="0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格" data-link-desc="短網址服務交給工程之前要定案的需求決定：自建或購買、點擊資料的地位、連結的保存承諾、目的地能否修改、建立者的範圍、短碼的可猜測性、網域策略與可用性分級；多數答案由商業模型推出，目的地修改、短碼可猜測性與可用性分級由使用情境決定">0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格</a> 的〈點擊資料的地位：付費功能或內部參考，以及對應的規格〉）；同一篇〈建立者的範圍與濫用處理〉的撤回時限，則限制了任何一層能保存轉址多久。</p>
<p>不統計點擊、或點擊只是內部參考時，轉址回應可以讓 CDN 保存，爆紅連結的點擊絕大多數由 CDN 直接回應，應用伺服器只在 CDN 的副本到期時收到請求（每個邊緣節點各一次，有 origin shield（CDN 內部集中回源的中間層，見 <a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源（CDN / Origin Protection）</a> 的〈Origin Protection 的設計責任〉）這類中間層時才接近整體一次）。點擊只是內部參考時，點擊事件改由 CDN 的存取紀錄產生，應用伺服器不再發出（0.24 的〈各層快取與撤回所需的時間〉）。CDN 保存轉址時，爆紅幾乎不會影響後端，承受流量的是 CDN 的頻寬，CDN 的流量費用隨點擊數增加。</p>
<p>點擊分析是付費功能時，轉址回應多半帶 <code>Cache-Control: private</code>，CDN 不保存，每一次點擊都送到應用伺服器。0.24 的〈各層快取與撤回所需的時間〉已經處理這條連結成為 <a href="/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key</a> 的情形：每台應用伺服器的記憶體裡放一層短 TTL 的程序內快取，讓同一個短碼的讀取分散到所有伺服器，而不是全部打在同一個共用快取節點上。剩下的瓶頸是應用伺服器的數量本身，伺服器數量要靠自動擴展追上；爆紅的上升速度比自動擴展的反應時間快時，差距由擴展緩衝承接，緩衝的算法見 9.15 的〈自動擴展（autoscaling）的反應時間與緩衝〉。</p>
<p>這個預設在爆紅時有一個例外做法：平常不讓 CDN 保存轉址，origin 統計單一短碼的請求速率（計數放在各台應用伺服器共用的計數器裡，或每台依伺服器台數換算門檻），超過門檻時，只對那一個短碼讓 CDN 保存幾秒到幾十秒，點擊事件改由 CDN 的存取紀錄補回；只補回存取紀錄裡由 CDN 直接回應（快取命中）的那幾筆，回源到 origin 的請求 origin 已經發過點擊事件，全部補回會重複計數。最直接的做法是 origin 只在這個短碼的回應裡，把 <code>private</code> 換成 <code>s-maxage=&lt;秒數&gt;</code>：<code>private</code> 會禁止 CDN 保存，留著它 <code>s-maxage</code> 不會生效；<code>max-age</code> 照舊留給瀏覽器，不必改 CDN 設定（兩個指令見 <a href="/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限</a>）。開啟之後，origin 看到的這個短碼請求速率會掉到門檻以下，所以關閉的條件不能用 origin 端的速率，否則保存規則會一開一關地擺盪；開啟後給一段固定的保存期，或改看 CDN 存取紀錄算出的總速率再決定關閉。這樣做的前提是 CDN 遵循 origin 的 <code>s-maxage</code>、存取紀錄能在可接受的延遲內送回，而且這套切換在流量來之前就要建好並演練過，當下只是觸發它。</p>
<h2 id="點擊事件管道的積壓與非人為請求">點擊事件管道的積壓與非人為請求</h2>
<p>轉址路徑撐住之後，壓力往後移到點擊事件的管道。0.24 把點擊紀錄設計成轉址時只發出一筆事件、由背景消費者彙總，所以爆紅時轉址不會變慢，佇列裡未處理的事件數（<a href="/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth</a>）會上升，消費者處理到的位置落後最新事件越來越多（<a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a>）。客戶在報表上看到的是點擊數晚了幾分鐘到幾小時才更新。</p>
<p>這個積壓要不要處理、處理到什麼程度，看的是點擊報表的時效承諾。點擊分析是付費功能、報表承諾近即時時，消費者要能跟著事件量擴展，佇列的保存時間要長過最長的預期積壓，否則事件在被處理之前就過期遺失；點擊只是內部參考時，積壓只是報表晚一點，甚至可以在尖峰時改為取樣記錄：只保存部分點擊事件，報表依取樣比例還原（技術上同 <a href="/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling</a>），尖峰過後再恢復全量。佇列滿了的時候，兩種情形都是轉址照常回應，這一點 0.24 已經決定：點擊統計失敗不能拖垮轉址。差別在點擊事件的去處：點擊只是內部參考時，可以丟棄或取樣；點擊分析是付費功能時，0.23 要求事件不能遺失，事件要改寫到另一個持久的緩衝，或事後由 CDN 與負載平衡器的存取紀錄補回。</p>
<p>爆紅的連結也會帶來一波非人為請求。連結被貼進聊天軟體、社群平台時，平台會抓取網址產生預覽；多數平台在伺服器端依網址快取預覽，同一個網址只抓少數幾次；由傳送者自己的裝置產生預覽的平台，預覽抓取會隨轉發次數增加，而且集中在連結剛開始擴散的那段時間。0.24 的〈預覽抓取與安全掃描等非人為請求的標記〉把這類請求依 User-Agent、請求方法與來源網段標記出來、分開計數。爆紅時這份標記決定了客戶看到的點擊數有多少是真人，所以標記規則要在平常就涵蓋主要的聊天與社群平台。</p>
<h2 id="目的地網站承接匯集點擊的能力">目的地網站承接匯集點擊的能力</h2>
<p>短網址服務撐住了，流量仍然要到達目的地網站，而目的地網站往往比短網址服務小得多。爆紅連結把所有點擊匯集到同一個目的地，對方的伺服器可能因此回應變慢或錯誤。這時短網址服務的監控看到的是轉址全部成功，看不到使用者實際到不到得了目的地。</p>
<p>目的地網站承接不了匯集的點擊時，短網址服務能做的有限，因為目的地不在它的控制範圍內。它能做的是兩件事：在點擊報表上說明點擊數是轉址數，不等於成功到達目的地的人數；以及在建立介面與文件裡提醒客戶，把一條短網址放進大量曝光的位置之前，目的地網站要先依 <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>爆紅最危險的情形是那條連結指向釣魚頁或惡意軟體。0.24 的濫用防護在確認惡意之後把對照表的狀態改成已停用，而停用多快對所有點擊生效，由各層快取的保存時間決定。〈爆紅連結的負載落點：CDN、程序內快取與對照表〉那一節為了扛住爆紅，要讓轉址回應盡量被 CDN 與程序內快取保存；撤回惡意連結時為了盡快生效，要讓轉址回應盡量不被保存，兩個目標在同一條連結上衝突。</p>
<p>這個衝突最尖銳的是 CDN 有保存轉址的時候（不統計點擊，或依短碼切換保存的做法）；點擊分析是付費功能、CDN 不保存時，同樣的取捨落在程序內快取的 TTL 上，撤回的時效由它與瀏覽器的保存時間決定。CDN 有保存時，兩者的折衷是保存時間短、而且能主動清除。CDN 保存轉址的時間設在幾秒到幾十秒，同時保留依路徑主動清除（purge）單一短碼的能力（<a href="/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">cache invalidation</a>）；程序內快取在沒有建立廣播失效機制時無法從中央清除，所以它的 TTL 要短到符合需求定義的撤回時限；瀏覽器那一層只能靠轉址回應的快取標頭事先限制，301 不帶快取標頭時瀏覽器可能永久保存，已經點過的人之後看不到停用後的警告頁（見 <a href="/blog/backend/12-http-caching/applying-cache-headers/" data-link-title="12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面" data-link-desc="把 HTTP 快取的保存位置、新鮮度、驗證、快取鍵與更新規則用在具體回應上：永久與暫時轉址的狀態碼與快取標頭、需登入的 API 回應、個人化頁面、靜態資源與引用它的頁面，以及檢查實際送出的快取標頭的方法">12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面</a>）。爆紅期間的請求量大，幾秒的保存時間就足以讓絕大部分點擊由快取回應，所以短的保存時間對扛流量的效果損失不大，換來的是 CDN 這一層的停用能在幾秒內生效；整體撤回時間仍要加上程序內快取的 TTL 與瀏覽器的保存時間。</p>
<h2 id="建立路徑被機器人大量建立連結">建立路徑被機器人大量建立連結</h2>
<p>建立路徑的瞬時大流量通常來自機器人：用大量帳號或來源位址建立連結，用來散布釣魚頁、垃圾訊息或搜尋引擎垃圾連結。它和爆紅不同的地方在於這些請求本身就是要擋的對象，處置的方向是辨識並拒絕，而不是承接。</p>
<p>機器人大量建立連結的成本不只是一時的流量，下列三項在攻擊結束後仍然留著：</p>
<ul>
<li><strong>儲存</strong>：每一條建立成功的連結都進入對照表，依需求定義的保存承諾可能要永久保存，即使事後刪除，短碼也要留一筆佔位紀錄不能重用（見 0.24 的〈對照表：儲存選型、欄位與分片〉）。</li>
<li><strong>濫用檢查的配額</strong>：建立時與建立後的黑名單查詢通常呼叫外部的網址信譽服務，這些服務本身有查詢配額，機器人在短時間內建立大量連結，會把配額用完，檢查排不進去的連結越積越多。檢查佇列因此要有優先順序：依點擊量排序，正在爆紅的連結先重查，或另外保留一份配額給它們，否則機器人用光配額的期間，爆紅中的釣魚連結也跟著排隊。網址信譽服務也有可以下載到本機比對的清單形式，本機比對沒有每次查詢的配額；外部查詢只用在本機清單判斷不了的情形。0.24 的〈濫用防護：建立時的檢查、建立後的重查與轉址時的比對〉為「連危險網址清單都還沒取得」的情形，給了兩種情形的選項：開放匿名建立的服務，對最近建立、還沒通過背景重查的連結阻擋；開放註冊或經由 API 代建的服務，只對其中建立者帳號或金鑰還沒有紀錄的連結阻擋。檢查積壓時，這些連結同樣還沒被檢查過，所以同一條規則可以延伸過來；本篇延伸時把範圍收在匿名與沒有紀錄的建立者：只擋匿名或沒有紀錄的建立者在積壓期間建立的連結，付費客戶與高優先等級的 API 金鑰（例如簡訊發送前產生的連結）不在範圍內。被擋的連結顯示一頁「連結正在檢查中」的說明，檢查通過後恢復轉址；這需要記錄每條連結是否已通過檢查，例如另設一個最後通過檢查的時間，轉址依它顯示檢查中的頁面；0.24 的阻擋規則本身就需要這個欄位，它和正常、已停用、已刪除這個狀態欄是兩個維度，合在同一欄會遇到「檢查中的連結被刪除時填哪一個」這類衝突。</li>
<li><strong>網域名聲</strong>：只要有一部分惡意連結在被停用之前轉址出去，服務的轉址網域就可能被郵件過濾器與瀏覽器列入封鎖清單，所有客戶的正常連結一起受影響（0.24 的〈濫用防護：建立時的檢查、建立後的重查與轉址時的比對〉）。</li>
</ul>
<p>依單一來源位址或單一帳號限制建立速率（<a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit</a>）擋得住集中的攻擊，擋不住分散在大量位址與帳號上的攻擊：每一個來源都在配額之內，總量仍然暴增。分散的攻擊要靠准入控制（<a href="/blog/backend/knowledge-cards/admission-control/" data-link-title="Admission Control（准入控制）" data-link-desc="要決定湧入的使用者誰能進入、什麼時候進入時，例如開賣的虛擬等候室、waitlist、事前註冊與抽籤；和負載卸除、限流的分工，以及和 Kubernetes admission controller 的區分">Admission Control</a>），以及提高每建立一個帳號或一條連結的成本：</p>
<ul>
<li><strong>限制誰能建立</strong>：攻擊期間關閉匿名建立，只讓已登入、信箱已驗證、或有付費紀錄的帳號建立。0.23 的〈建立者的範圍與濫用處理〉在需求層決定了平常誰能建立，這裡是把那個範圍在攻擊期間暫時收窄。這個切換要做成一個設定開關（<a href="/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">feature flag</a>，維運用的開關見 <a href="/blog/backend/06-reliability/feature-flag-governance/" data-link-title="6.17 Feature Flag Governance" data-link-desc="把 feature flag 從上線開關升級為有角色分類、lifecycle 管理與 debt 治理的 runtime artifact">6.17 Feature Flag Governance</a> 的〈Kill switch 設計〉），平常就存在並演練過，攻擊時只是撥動它。</li>
<li><strong>限制新帳號的額度</strong>：新註冊、還沒有任何紀錄的帳號給較低的建立額度，帳號累積一段時間的正常建立與點擊紀錄之後再放寬，讓大量新註冊的帳號各自只能建立少量連結。</li>
<li><strong>依目的地限速</strong>：攻擊者用大量連結散布同一批頁面，所以對同一個目的地在單位時間內新建的連結數設上限，不論建立者是誰。門檻依各網域平常的建立量設定，熱門網域（影音、購物、文件服務）本來就有大量正常連結，要另設較高門檻；攻擊頁放在共用託管平台的網域下時，依網域計數會連帶擋到平台的正常用戶，要改用完整網址或路徑計數。</li>
<li><strong>建立時加挑戰</strong>：匿名建立時要求通過挑戰（例如驗證碼），直接提高每一條連結的建立成本；這只適用於瀏覽器上的建立，經由 API 建立的請求要靠金鑰與額度。</li>
</ul>
<p>什麼時候撥動准入開關、收緊新帳號額度，依據的是攻擊在建立事件上留下的形狀。新註冊帳號的數量在幾分鐘內跳升，而正常的註冊隨行銷活動漸增；新建立的連結大多指向少數幾個網域，而且遠超過那幾個網域平常的建立量，因為攻擊者用大量連結散布同一批頁面，正常客戶的目的地是分散的；黑名單命中率上升，因為攻擊的目的地正是要被封鎖的那一類。新建立的連結幾乎都沒有被點擊，也是攻擊的形狀，但它是落後訊號，要等一段時間才觀察得到，用來事後確認；即時的判斷靠帳號數、目的地集中度與黑名單命中率。匿名建立沒有帳號，帳號數這個訊號不會觸發，要改看單位時間的匿名建立量與來源網段的分布；黑名單命中率依賴網址信譽服務，配額被用完時這個訊號跟著失效，這時剩下的即時訊號是目的地集中度與匿名建立量。這些訊號在平常就要能從建立事件裡算出來。</p>
<p>建立路徑與轉址路徑也要在容量上互相隔離。0.24 已經把管理介面放在另一個可註冊網域（<a href="/blog/backend/knowledge-cards/registrable-domain/" data-link-title="Registrable Domain" data-link-desc="判斷兩個主機名算不算同一方、或某個跨站防護的信任邊界實際涵蓋誰時，用來定位「站」的切點在哪一層">registrable domain</a>）；容量上，建立路徑的請求不能和轉址路徑共用同一組應用伺服器與連線池而互相排擠，否則對建立路徑的攻擊會拖慢所有既有連結的轉址。把兩條路徑放在各自的伺服器群組與資源配額裡（<a href="/blog/backend/knowledge-cards/bulkhead/" data-link-title="Bulkhead" data-link-desc="說明 bulkhead 如何用資源分艙限制故障擴散">bulkhead</a>），對建立路徑的攻擊就不會佔用轉址的伺服器。兩條路徑仍然共用對照表，所以資料層也要隔離：轉址讀副本或快取，建立的寫入另外限速，避免大量寫入拖慢轉址的讀取。寫入暴增時副本的延遲會上升，0.24 的做法裡副本查無時要回主節點確認，回主節點的請求也會跟著變多，所以這段回主節點的流量也要限速，副本查不到、必須回主節點確認的，只有剛建立、還沒複製到副本的短碼，所以可以只讓這些短碼回主節點；遞增計數器發號時，比對已發出號段的上界就能判斷一個短碼是不是新的，隨機產生的短碼做不到這件事。另一個做法不依賴發號方式：建立連結時同時把新的對照寫進共用快取，轉址先查快取，就不必依賴副本查到新建立的短碼。</p>
<p>優先等級的機制見 9.15 的〈負載卸除與限流的分工、請求的優先等級〉；短網址服務裡各路徑的等級由 <a href="/blog/backend/00-service-selection/url-shortener-requirements/" data-link-title="0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格" data-link-desc="短網址服務交給工程之前要定案的需求決定：自建或購買、點擊資料的地位、連結的保存承諾、目的地能否修改、建立者的範圍、短碼的可猜測性、網域策略與可用性分級；多數答案由商業模型推出，目的地修改、短碼可猜測性與可用性分級由使用情境決定">0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格</a> 的〈建立路徑與轉址路徑的可用性分級〉決定：轉址最高，建立、報表查詢與管理介面較低，整體過載時先卸除較低的那幾類。例外是嵌在另一條寫入路徑裡的建立（發文當下改寫連結、送出簡訊前才產生連結），它的可用性跟著那條路徑，要依 API 金鑰或用途標成較高的等級，否則卸除建立會擋住整批發送。</p>
<h2 id="轉址路徑的掃描與體積型攻擊">轉址路徑的掃描與體積型攻擊</h2>
<p>轉址路徑上的攻擊有掃描與體積型攻擊兩種形狀。<strong>掃描</strong>是程式大量請求隨機或連續的短碼，嘗試撞到別人的連結。0.24 的〈各層快取與撤回所需的時間〉已經處理它的負載面：不存在的短碼寫進短 TTL 的 <a href="/blog/backend/knowledge-cards/negative-cache/" data-link-title="Negative Cache" data-link-desc="說明把「查無此 key」的結果也快取一小段時間，擋掉重複穿透的防護與代價">negative cache</a>，避免每次都打到對照表，單一來源的 404 超過門檻時限制它的速率。掃描能撞到多少連結，則由短碼生成的方式決定，見 0.24 的〈短碼生成：可猜測性與協調成本的取捨〉。</p>
<p><strong>體積型攻擊</strong>對準轉址網域，在網路層塞滿入口，應用層的快取與限流都位在被塞滿的入口之後，攻擊流量在到達它們之前就已經佔滿頻寬。轉址網域平常就放在 CDN 或雲端業者的邊緣網路後面，攻擊流量由邊緣吸收，前提是 origin 只接受邊緣網路來源的連線、位址不外洩，見 9.15 的〈體積型攻擊（DDoS）的處置：流量清洗與邊緣網路〉。轉址網域與管理介面分屬不同網域，再加上兩者解析到不同的入口與後端時，轉址網域遭受攻擊期間，客戶仍然登得進管理介面查看狀態；只是網域不同、入口相同時，攻擊照樣會打到管理介面。</p>
<h2 id="爆紅建立濫用與轉址攻擊事前要建好的機制">爆紅、建立濫用與轉址攻擊事前要建好的機制</h2>
<p>爆紅、建立路徑的濫用與轉址路徑的攻擊，當下能做的決定都很少：對爆紅的短碼開啟 CDN 保存、撥動關閉匿名建立的開關、清除一條惡意連結的快取。每一個決定都依賴一個平常就存在的機制：依短碼切換 CDN 保存規則與從存取紀錄補回點擊、依路徑清除 CDN 的能力、建立路徑的准入開關與新帳號額度、建立事件裡的攻擊訊號、轉址與建立兩條路徑在容量上的隔離、以及放在邊緣網路後面的轉址網域。這些機制和 0.24 的實作一起建好，瞬時大流量來的時候才只剩下決定要做。</p>
<h2 id="延伸閱讀通用機制需求定義限流與快取標頭">延伸閱讀：通用機制、需求定義、限流與快取標頭</h2>
<ul>
<li>流量來源的辨識、擴展緩衝、負載卸除、准入控制與體積型攻擊的通用機制：<a href="/blog/backend/09-performance-capacity/unplanned-traffic-surge/" data-link-title="9.15 無預警瞬時大流量：流量來源辨識、擴展緩衝、請求優先等級、准入控制、體積型攻擊與退路" data-link-desc="沒有準備窗口的瞬時大流量（突發流量、流量暴增）怎麼承接：依流量形狀分辨真人需求、應用層的濫用流量、體積型攻擊（DDoS），自動擴展的反應時間換算成緩衝，入口標記請求優先等級與負載卸除的順序，等候室與事前註冊這類准入控制，把體積型攻擊交給更大的邊緣網路，以及擴展跟不上時的退路">9.15 無預警瞬時大流量：流量來源辨識、擴展緩衝、請求優先等級、准入控制、體積型攻擊與退路</a>。</li>
<li>誰能建立連結、點擊資料的地位、建立與轉址的可用性分級：<a href="/blog/backend/00-service-selection/url-shortener-requirements/" data-link-title="0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格" data-link-desc="短網址服務交給工程之前要定案的需求決定：自建或購買、點擊資料的地位、連結的保存承諾、目的地能否修改、建立者的範圍、短碼的可猜測性、網域策略與可用性分級；多數答案由商業模型推出，目的地修改、短碼可猜測性與可用性分級由使用情境決定">0.23 短網址服務的需求定義：自建或購買、保存期限、目的地修改與濫用處理的規格</a>。</li>
<li>依帳號、來源位址或 API 金鑰分配建立額度的演算法：<a href="/blog/backend/09-performance-capacity/rate-limit-implementation/" data-link-title="Rate Limit 實作" data-link-desc="單機 middleware / Redis 分散式限速 / 配額設計 — 概念見 DevOps 流量管控，本章聚焦後端實作">Rate Limit 實作</a>。</li>
<li>轉址回應的狀態碼與快取標頭怎麼影響撤回：<a href="/blog/backend/12-http-caching/applying-cache-headers/" data-link-title="12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面" data-link-desc="把 HTTP 快取的保存位置、新鮮度、驗證、快取鍵與更新規則用在具體回應上：永久與暫時轉址的狀態碼與快取標頭、需登入的 API 回應、個人化頁面、靜態資源與引用它的頁面，以及檢查實際送出的快取標頭的方法">12.8 快取設定的應用：轉址、需登入的 API 回應與個人化頁面</a>。</li>
</ul>
]]></content:encoded></item></channel></rss>