整個這一級要回答的問題
你已經從零打造出整座網頁堆疊。你知道一條 TCP 連線是以三向交握的來回之舞開場、知道 HTTP 請求一個頁面而伺服器回覆、知道名字如何透過網際網路的電話簿變成位址。那麼問一個合理的問題:經過這一切精巧的機械之後,為什麼點一個連結有時感覺像在等一壺水燒開?這一級就是答案。在五篇導讀裡,我們會看清當網頁感覺很慢時,時間究竟花到哪裡去了,以及工程師用來把它討回來的那些巧計——快取、代理伺服器、內容遞送網路、負載平衡、自適應串流。
第一步,也是整級最重要的觀念,就是別再怪錯對象。當頁面載入緩慢時,幾乎所有人的第一直覺都一樣:我的連線太慢了,我需要更多頻寬。有時候這是真的。但出乎意料地,常常並非如此——而當真正的罪魁禍首是距離時還拚命追求更多頻寬,就像為了縮短一趟長途旅程而去買一條更寬的車道。要看出為什麼,我們得拆開三個每天都被攪混在一起的詞:頻寬、吞吐量與延遲。
頻寬、吞吐量、延遲:三件不同的事
想像一根輸水管。頻寬 是這根管子有多寬——若全力運轉,這條鏈路每秒最多能搬多少位元,比如 100 Mbps 或 1 Gbps。吞吐量 則是此刻實際流動著多少水,它總是小於管子的寬度,因為有額外開銷、共享與壅塞。一條由數段鏈路組成的路徑,只會跟它最窄的那一段一樣快,所以端到端的吞吐量是 min(R1, R2, ...)——瓶頸獲勝,正如你在階梯較早處看到的。關鍵在於:這兩者量的都是「每秒的量」——你能搬「多少」。
延遲 則是完全不同的量:它是「拖延」——一個位元從一端走到另一端要花「多久」。把管子加寬,絲毫不會讓管子變短。你可以更快地把水倒進去,但第一滴水仍得走完整段長度才會抵達。這就是為什麼此主題那句頭號誠實話值得明講:更多頻寬「並不」削減延遲。從 100 Mbps 升級到 1 Gbps,能讓你用十分之一的時間下載一個巨大的檔案,但若伺服器在另一塊大陸上,它不會讓一個小小的請求早一點回來。你買不到比光速更快——靠一根更肥的管子辦不到。
延遲從何而來——以及那段你修不了的部分
回想階梯早期那份延遲的拆解。一個封包抵達某處所花的總時間,是四個部分的總和:傳輸延遲(把位元推上線路所需的時間,取決於頻寬)、處理延遲(路由器瞄一眼標頭)、排隊延遲(排在其他封包後面等候),以及傳播延遲(位元以接近光速的速度,實際走過那段距離)。前三者你可以用更快的硬體、更聰明的佇列、更肥的鏈路去攻克。第四者你辦不到。它由物理與距離決定,而它通常就是那段沒有人能用工程手段消去的部分。
讓我們把它具體化。光在真空中一秒約走 300,000 公里;在玻璃光纖中較慢,每秒大約 200,000 公里。倫敦到雪梨直線約 17,000 公里——但光纖不走直線,它沿著海岸線與纜線路徑繞行,所以就算它 20,000 公里的玻璃好了。那是一秒的 20,000 / 200,000,約 100 毫秒,而且只是走「單程」,還是在一個零排隊、路由器瞬間完成的完美世界裡。一趟來回——去了又回——就已經是 200 毫秒,而這是任何升級、任何金錢、任何巧妙程式碼都無法低於的絕對底線。這是地球又圓又大所要付的代價。
one-way propagation, London <-> Sydney (fibre, ~20,000 km)
-----------------------------------------------------------
distance / speed-in-fibre = 20,000 km / 200,000 km/s
= 0.1 s = 100 ms one way
= 200 ms round trip (the hard floor)
...and a fresh HTTPS page often needs SEVERAL round trips:
DNS lookup ~1 RTT (find the address)
TCP handshake ~1 RTT (SYN, SYN-ACK, ACK)
TLS handshake 1-2 RTT (agree on encryption)
HTTP request ~1 RTT (finally ask for the page)
----------------------------------------------------------
~4 RTT x 200 ms ~= 0.8 s before a single useful byte arrives
來回稅:為什麼一次點擊是多趟往返
這裡是最令人意外的部分。一次頁面載入幾乎從來不是一趟橫越海洋的往返——而是一疊往返,且許多趟必須依序發生,一趟等著上一趟做完。如同上面的草圖所示,一個全新的加密頁面在能開始抵達之前,瀏覽器可能要付一趟來回去 網域名稱系統查名字、再一趟去完成 TCP 交握、再一兩趟去用 TLS 談妥加密,然後「才」用一趟送出真正的 HTTP 請求。這每一趟都被課了整整一個來回時間的稅。當這個時間是 200 毫秒時,四趟接續的往返會在單獨一個有用位元落地之前燒掉大半秒。
情況更糟,因為一個現代網頁不是一個檔案。它是一份 HTML 文件,一旦被解析,就會再去要求數十甚至數百樣東西——樣式表、指令稿、字型、圖片——每一樣都可能是自己的一個請求。如果瀏覽器嚴格地一個接一個地抓取它們,每次抓取都會再付一次來回稅,頁面就會爬行。這正是為什麼如此多的網頁效能工程其實是延遲工程:用一條連線重複服務許多檔案(持久連線)、讓許多請求平行飛出而不必等每個回覆、並在任何可能之處削掉整趟整趟的來回。像 HTTP/2 與 QUIC 這樣的通訊協定,存在的目的很大程度上就是對抗這道來回稅——QUIC 甚至把連線與加密的建立摺疊在一起,好讓一位回訪者能以零額外來回就開始送資料。
唯一真正有效的槓桿:縮短距離
如果來回底線由距離決定,而一個頁面需要好幾趟來回,那麼你能做的最有力的一件事,就是那個顯而易見的:把距離變短。你沒辦法把雪梨搬到離倫敦近一點——但你可以把內容的一份「副本」放在雪梨,這樣那裡的使用者就改跟一台附近的機器對話,而不是隔著海洋的那一台。一趟去到同城伺服器的 5 毫秒來回,乘以四次交握,只是那 800 毫秒的零頭的零頭。物理沒有改變;改變的是距離。
- 想像在倫敦有一台來源伺服器,握著真正的網站,直接服務整個星球。
- 每一位訪客——雪梨、東京、聖保羅——都為每一個頁面付出整整的長途來回,還要付好幾遍。
- 現在把數十份邊緣副本散佈到世界各地,每一份都靠近一群使用者,握著相同的內容。
- 把每一位訪客導向最近的副本,讓他們的來回是數十公里、而非數萬公里。
- 同樣的位元只走極短的距離,每一趟疊起來的來回都縮小了,於是網頁忽然感覺變快了。
那張由散佈副本織成的網,就是一個 內容遞送網路,簡稱 CDN——把它想成網路的一連串在地倉庫。與其大家都向一座遙遠的工廠下單,貨品被預先囤放在你附近的倉庫裡,於是配送是一小步、而非一趟跨洲的長運。這一級接下來會把這個想法好好建立起來:下一篇解釋一份副本如何得知自己是否仍然新鮮(HTTP 快取、ETag、那個 304),接著是 CDN 與其邊緣伺服器如何組織、一個請求如何被導向正確的鄰近副本(DNS 把戲與任播)並在多台機器間平衡,最後是連直播與隨選的影片,如何用自適應串流倚靠同一個縮短距離的想法。前方的一切,骨子裡都是一場對來回的戰爭。