前兩天我們聊了系統設計的思維,以及高併發情境下(如演唱會搶票)伺服器連線池被榨乾的慘況。今天,我們終於要切入正題,來面對那個讓無數後端工程師頭痛的終極殺手——高併發下的伺服器卡死問題。
要解決連線池乾涸的問題,我們必須先看懂底層的連線模型。很多新手常把「同步/非同步」與「阻塞/非阻塞」混為一談,今天我們就用一間速食店的日常,把這些底層 I/O 邏輯一次講清楚。
大部分新手工程師寫出來的第一支 API,底層通常都是同步模型(例如傳統的 Apache HTTP Client 或 Tomcat 預設機制)。這適合用在簡單的順序操作、或對效能要求不高的內部系統。
read() 或 write() 操作會阻塞執行緒。這就像店員把訂單交給廚房後,只能站在櫃台發呆,等待 S3 的回應。這段期間,後面排隊的客人只能乾等。
為了突破硬體極限,現代的高併發框架(如 Netty、Node.js 或 Go)幾乎都採用了非同步模型來打造高吞吐量應用。


(圖片來源:touch1973@Github)
透過這張時間軸,可以清晰地看出三種處理模型的差異:
既然非同步很好,多執行緒更好,為什麼我們不用多執行緒就好?
因為「請員工是要付薪水的」。在伺服器中,每開啟一個 Thread,作業系統就需要分配獨立的記憶體空間給它。當執行緒數量一多,CPU 必須頻繁地在不同執行緒之間切換(Context Switch 上下文切換),這就像三個店員在狹窄的廚房裡不斷互撞、交接工作,反而會拖慢整體速度。
此外當我們要面對上萬個瞬間湧入的併發請求時,同步模型雖然會遇到連線池被榨乾、Thread 佔用太多記憶體的瓶頸,但是非同步或是多緒也會遇到太多threads 讓伺服器記憶體瞬間爆炸 (OOM)。這也是為什麼現代高併發架構都會走向「極少量的執行緒 + 非同步非阻塞機制」的原因。
明天,將帶大家實作傳統的同步連線方式,以及解釋執行緒飢餓(Thread Starvation)!