你的 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() 立刻返回——如果資料還沒準備好,返回一個錯誤(EAGAINEWOULDBLOCK),讓你知道「現在沒資料,等一下再試」。

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」兩個優點。

下一篇:為什麼有 GIL、為什麼沒 GIL