你的 web server 在等資料庫回應的那 50ms,它在做什麼?
這個問題的答案,決定了你的 server 能同時服務多少個連線。
Blocking I/O(阻塞式)
最直觀的模型:呼叫 read(),等 OS 把資料準備好,函式才返回。
f = open("large_file.txt")
data = f.read() # 等到讀完才繼續
print(data)等待期間,這個 thread 完全暫停——不能做任何其他事。
Apache 的 prefork MPM 是這個模型:每個連線一個 thread(或行程),thread 在等 I/O 的時候阻塞,什麼都不做但資源佔著。
100 個並行連線 = 100 個 thread = 幾百 MB RAM,大部分時間都在等 I/O 什麼都不做。
Non-blocking I/O(非阻塞式)
設定 socket 為 non-blocking 模式,read() 立刻返回——如果資料還沒準備好,返回一個錯誤(EAGAIN 或 EWOULDBLOCK),讓你知道「現在沒資料,等一下再試」。
import socket, errno
sock.setblocking(False)
try:
data = sock.recv(1024)
except BlockingIOError:
# 資料還沒準備好,繼續做其他事
pass問題:你怎麼知道什麼時候資料準備好了?輪詢(polling)——你必須不停地去問,浪費 CPU。
這個問題有一個更好的解法:讓 OS 在資料準備好時通知你,不要你自己一直問。
Synchronous I/O Multiplexing:select / poll / epoll
讓 OS 幫你監控多個 file descriptor(socket、pipe、file),任何一個準備好了就通知你。
import select
readable, _, _ = select.select([sock1, sock2, sock3], [], [])
for s in readable:
data = s.recv(1024) # 這時候一定有資料,不會阻塞epoll(Linux)是這個思路的現代實作:
select/poll每次要把所有 fd 傳給 OS,fd 多了性能差(O(n))epoll只把「有事件的 fd」通知你(O(1)),適合大量並行連線
Nginx 用 epoll。Redis 的網路層用 epoll。這是高性能 server 的基礎。
Event-driven(事件驅動)
把 epoll 封裝成更高層的抽象:你不直接操作 epoll,而是註冊 callback,事件發生時 event loop 呼叫你的 callback。
server.on('request', (req, res) => {
// 這個 callback 在請求進來時被 event loop 呼叫
db.query('SELECT ...', (err, data) => {
// 這個 callback 在 DB 回應時被呼叫
res.end(JSON.stringify(data));
});
});Node.js 的核心就是這個:一個 thread,一個 event loop,用 epoll 監控所有 I/O。任何 I/O 完成,把對應的 callback 放進 event queue,event loop 依序執行。
一個 thread 服務幾萬個並行連線——所有連線在等 I/O 的時候,thread 去處理其他連線的 callback,不浪費。
Async I/O(真正的非同步)
上面的 epoll 和 event loop,本質上是 同步 I/O 的多工——你呼叫 epoll_wait,OS 說「socket X 有資料了」,你去讀。這個「讀」本身還是同步的。
真正的 async I/O(AIO):你告訴 OS「幫我把這個 socket 的資料讀到這個 buffer,讀完通知我」。OS 自己去讀,讀完才通知你,你完全不參與等待過程。
Linux 的 io_uring(2019)是最現代的實作:
你發出 read request → OS 在背景執行 → 讀完把結果放到 completion queue → 你去 completion queue 拿結果
io_uring 比 epoll 更徹底地解耦「發出請求」和「處理結果」,在高 I/O 密度場景有明顯優勢。PostgreSQL 16 開始實驗性支援 io_uring,Tokio(Rust 的 async runtime)也有 io_uring 後端。
這四個模型的關係
Blocking I/O
↓ 問題:等 I/O 浪費 thread
Non-blocking I/O
↓ 問題:輪詢浪費 CPU
I/O Multiplexing (epoll)
↓ OS 通知有資料再讀,多路複用
Event-driven (event loop)
↓ 把 epoll 封裝成更高層抽象
Async I/O (io_uring)
↓ 連「讀」這個動作本身都非同步
不是說 blocking 是錯的——如果你的服務天生低並行(一次只處理幾個請求),blocking I/O 的程式最簡單,沒有必要引入 event loop 的複雜度。
選型的實際考量
| 模型 | 適合場景 | 代表 |
|---|---|---|
| Blocking + Thread Pool | 中等並行,邏輯複雜,寫法直觀 | Java Servlet, Django |
| Event Loop | 高並行 I/O 密集,連線數超過幾千 | Node.js, Nginx |
| async/await | 高並行 I/O 密集,但想保留同步寫法 | Python asyncio, Go |
| io_uring | 極高 I/O 吞吐,延遲極敏感 | Tokio (Rust), PostgreSQL 16+ |
Go 的 goroutine 底層其實用了 epoll(在 Linux 上)——當 goroutine 等 I/O,scheduler 把它放下,用 epoll 等 OS 通知,通知來了再繼續。這讓 goroutine 兼顧了「寫法像 blocking」和「底層像 event loop」兩個優點。