一個名字,成千上萬扇門
在上一篇裡,你認識了 內容傳遞網路(CDN):一連串的在地倉庫,把邊緣伺服器散佈在數百座城市裡,好讓讀者想要的內容早已坐在附近。但這幅圖留下了一個頑固的問題沒回答。讀者的瀏覽器從來不會打出某座城市或某個伺服器編號——它只打出一個名字,像是 `cdn.example.com`。那麼在網頁載入前那短短幾毫秒裡,這單一一個名字,是怎麼從成千上萬台之中,變成「最近那座倉庫」的位址的?這個悄悄把每位訪客指向一扇好門的動作,正是本篇要談的。工程師稱它為「請求引導」。
為什麼要在「哪扇門」上斤斤計較?因為本級第一篇的教訓:距離與來回主宰了延遲,而再多的頻寬也打不過光速。把一位台北的讀者送到台北的邊緣伺服器、而不是法蘭克福的那台,能把來回時間從約 250 毫秒砍到幾毫秒。所以好的引導不是錦上添花,而是讓一個 CDN「感覺很快」的大部分原因。難處在於:這個引導決定必須在瀏覽器真正連線之前就做好——它得被摺進一個更早發生的步驟,而早到足以承擔此事的地方只有兩個:名字查詢,以及那第一個封包的路由。
用名字引導:以 DNS 為基礎的重導
第一個可以引導的地方,是 DNS 查詢——你先前從根伺服器追到權威伺服器的那本網際網路電話簿。把戲很簡單,也帶點狡猾:CDN 自己經營 `cdn.example.com` 的權威名稱伺服器,所以是 CDN 在決定要交回哪個位址——而它可以對不同的提問者交回不同的答案。當一台台北的解析器查這個名字時,CDN 的名稱伺服器看一眼這問題從哪來、挑一台在台北或靠近台北的邊緣伺服器,回覆那台邊緣伺服器的 IP 位址。一台倫敦的解析器查同一個名字,拿到的是一台倫敦邊緣伺服器的位址。同樣的名字、不同的門,在查詢時被選定。
- 瀏覽器向它的解析器查詢 cdn.example.com,就跟任何一次名字查詢一樣。
- 解析器一路查下這本電話簿,抵達該名字由 CDN 自己經營的權威名稱伺服器。
- 那台名稱伺服器看一眼是誰在問(大致上是解析器的位置),挑出一台附近、健康的邊緣伺服器。
- 它回覆那台邊緣伺服器的 IP 位址——而且帶著一個刻意設短的存活時間(TTL),好讓這個選擇能很快被修改。
- 瀏覽器連到那個位址,從一座附近的倉庫取得內容。
注意那個短短的 TTL。你先前學過,DNS 答案帶著一個存活時間,好讓它可以被快取,而長 TTL 意味著較少的查詢。CDN 刻意反其道而行:它把 TTL 設得很小,常常一分鐘或更短,好讓引導答案很快過期,下一次查詢就能被重新導向。如果一台邊緣伺服器塞滿了、出了故障,或整個區域陷入黑暗,CDN 只要開始用一個不同的位址回答新的查詢,流量就會在一兩個 TTL 之內從麻煩處排空。電話簿因此變成了一個活的方向盤。
用路由引導:任播
第二個可以引導的地方更早、也更激進。用 任播,CDN 把同一個 IP 位址同時賦予許多城市裡的許多台邊緣伺服器,然後讓網際網路自己的路由把每個封包載往「最近的那一份副本」。這聽起來不可能——一個位址不就該對應一個地方嗎?回想路由那幾級裡封包是怎麼找到路的:路由器並不知道最終目的地,它只是沿著「被通告出來、成本最低的路徑」把封包交給下一跳,而位址區塊則由 BGP 通告到整個網際網路。任播只不過是讓 CDN 從數十個地點通告同一個位址區塊。於是每台路由器自然會朝最近的那一份轉送,因為那本來就是它認為最便宜的路徑。
Anycast: ONE address, advertised from MANY sites
Site A (Taipei) --advertises--> 203.0.113.0/24
Site B (Tokyo) --advertises--> 203.0.113.0/24
Site C (Frankfurt)--advertises--> 203.0.113.0/24
Packet from a reader in Taipei, dest 203.0.113.10:
router picks the cheapest route it knows for 203.0.113.0/24
-> that route leads to Site A (Taipei) <- nearest copy wins
A reader in Germany sending to the SAME 203.0.113.10
-> their routers' cheapest route leads to Site C (Frankfurt)任播有兩個可愛的性質。第一,引導是即時的,且不需要對每個請求做決定:那些反正都由 BGP 建立與維護的路由表,把活全幹完了。第二,它會自我癒合。如果一個站點下線,它只要停止通告那個位址;幾秒內 BGP 就會撤回那條路由,每台路由器悄悄地在「次近的副本」上重新收斂。不需要重新解析任何名字,也不需要通知任何用戶端——故障被路由結構本身吸收了。這正是為什麼任播是那些「絕不能倒」之物的主力,像是 DNS 根伺服器,以及一個 CDN 的最前線。
誠實的陷阱:路由是依「網路拓樸」挑最便宜的路徑,而不是依地理或毫秒。對路由器最便宜的那份副本通常是最近的,但並非永遠——有時一條在 BGP 眼中看起來很短的路徑,在現實中卻很慢。更糟的是,一條 TCP 連線假設自己在跟「一台固定的機器」說話,可是如果路由在連線途中改變,一個封包可能突然抵達另一份任播副本——那份副本根本不知道這條連線,連線就斷了。這正是為什麼任播對「短而自足的交換」(單一一次 DNS 查詢、連到 CDN 的第一次連線)非常美妙,卻要對長壽的 TCP 工作階段小心搭配別的把戲。每種引導方法都有它最擅長的差事。
在單一站點內部:負載平衡
用名字或用路由引導,能把一個請求送到對的城市。但一個繁忙的邊緣站點不是一台機器——它是一個機架,或一排排機架,裡頭是一群相同的伺服器,藏在單一一個公開位址後面。必須有某個東西把進來的連線分散到它們之間,免得一台伺服器淹死、其他台卻閒著。那個東西就是 負載平衡器,它是資料中心的接待員:每個請求走到同一個櫃台,由櫃台決定眾多等候中的專員裡,實際由哪一位來處理。沒有它,那些聰明的引導只會把整個區域的流量灌進一台可憐的機器。
負載平衡器有兩種口味,依它們所讀的層級命名。一種是 第四層負載平衡器,它在傳輸層工作:它看得到 TCP 連線——來源與目的位址、以及通訊埠號——但不會打開裡面的訊息。它只是挑一台後端伺服器(譬如,把連線那組四件式身分做雜湊,好讓同一條連線永遠落在同一台伺服器上),然後在用戶端與那台伺服器之間鏟封包,又快又便宜,自始至終都不曉得這其實是 HTTP。它是個只憑你票券顏色把你引去某處、卻從不讀票上寫什麼的接待員。
另一種是 第七層負載平衡器,它在應用層工作:它真的會終結那條連線、讀取 HTTP 請求,並能依其內容做決定。因為它看得到被請求的路徑與標頭,它可以把 `/api/...` 送往 API 伺服器、把 `/images/...` 送往圖片伺服器,把某位使用者的工作階段釘在同一台後端,或在一台健康的伺服器上重試一個失敗的請求。這正是前一篇裡那個 反向代理,如今兼任分派者——它讀你票上寫的內容,把你引向處理你這類請求的專員。代價是:打開並理解每一個請求,比起只會鏟封包的第四層平衡器要費更多工。
健康檢查,與把它兜起來
還有一塊讓這一切值得信賴的拼圖:負載平衡器會不斷用一個小小的健康檢查去輪詢它的後端——一個快速的探測,問每台伺服器「你還活著、還好嗎?」。一台停止回應、或開始回傳錯誤的伺服器,會在幾秒內被踢出輪替,新的請求只流向健康的那些。這跟 CDN 那個短短的 DNS TTL、以及任播的路由撤回,是同一個反射動作,只是往內一層而已:在每一個尺度上,好的引導都意味著持續地感知「什麼壞了」,並悄悄地繞過它。
現在把一個請求穿過整台機器走一遍。一位台北的讀者打開一個影片頁面。他的瀏覽器解析 CDN 的名字,然後要嘛以 DNS 為基礎的重導交回一個台北邊緣位址,要嘛這名字解析到一個任播位址、而它的路由本來就通往台北站點——兩條路都以同樣方式收尾:在一扇附近的前門。在那扇門,一個第四層負載平衡器把新連線分散到一排第七層平衡器上;其中一台讀取 HTTP 請求、看出它要的是一段影片片段,把它派給一台持有該內容的伺服器。三個引導決定,發生在三個不同的層級,全都瞄準你在本級開頭就立下的同一個目標:砍掉距離、砍掉來回。
把一條誠實的界線放在眼裡。這一切引導縮短了距離、平衡了負載,但它本身並不保護任何東西的安全:一個終結 TCP 連線的第七層平衡器,會明文讀到你的請求,除非有 TLS 把它包起來;而把你指向一台附近的伺服器,也完全沒說那台伺服器是不是誠實的。引導談的是速度與韌性,不是信任——下一級的安全篇會接起那條另外的線。