iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0

昨天我們親眼見證了同步連線的慘況:當大量併發請求湧入時,少量的執行緒會因為等待 I/O 而卡死(執行緒飢餓);但如果無腦加開成千上萬個執行緒,又會讓伺服器記憶體瞬間被撐爆,並陷入 Context Switch 的泥沼中。
既然「同步阻塞」這條路走不通,那些乘載千萬流量的科技巨頭(如 Apple、Twitter、阿里)到底是如何解決連線問題的?答案就是我們這系列的主角,也是 Java 生態系中最霸道的網路通訊框架 —— Netty

何謂 Netty ?

Netty 是一個基於 Java NIO (Non-blocking I/O) 的非同步、事件驅動網路應用框架。包含大家熟知的 Spring WebFlux、Elasticsearch、Cassandra 底層,全都是靠 Netty 在撐起那海量的併發連線。

它到底施了什麼魔法?我們一樣用「餐廳」的比喻來拆解它的兩大核心機制。

1. Selector(精明的經理)

在昨天的傳統同步餐廳裡,一個服務生(Thread)接待一桌客人後,就必須死死盯著廚房,等菜做好了才能端給客人。10,000 個客人就需要 10,000 個服務生,效率極差。

Netty 導入了 Java NIO 中的 Selector(多工選擇器)機制。你可以把 Selector 想像成一位擁有千里眼的超強經理。
現在,服務生(Thread)幫客人點完餐、將訂單交給廚房後,不用再親自死等。經理 (Selector) 會站在櫃台,同時監控著廚房裡成千上萬張訂單的狀態。只有當某一份炸雞「真正炸好」可以出餐時,經理才會廣播通知:「第 34 號桌的餐點好了,來個人把它送過去!」

透過 Selector,伺服器不需要為每一個連線保留一個卡死的執行緒,而是用極少數的經理,就能同時監控海量連線的 I/O 狀態。

2. Event Loop(不知疲倦的事件迴圈)

有了大堂經理監控狀態,那誰來負責送餐呢?這就是 Event Loop(事件迴圈) 的工作。

在 Netty 中,Event Loop 就是一個綁定在單一執行緒上,不斷執行 while(true) 無窮迴圈的員工。它的工作非常單純且極度高效,只做兩件事:

  1. 問經理 (Select): 詢問 Selector 有沒有已經就緒的 I/O 事件?(有沒有炸雞做好了?)
  2. 處理事件 (Process): 如果有,立刻觸發對應的回呼函數 (Callback),把炸雞送給客人;送完立刻回頭繼續問經理。

因為 Event Loop 處理的都是「已經準備好」的資料,所以它絕對不會發生阻塞 (Non-blocking)。一個 Event Loop 執行緒,可以在一秒內在數千個不同的客人(連線)之間來回切換處理,完美榨乾 CPU 的每一滴效能。

非同步連線實驗環境:守護執行緒地雷

為了驗證這個機制,我們用 Java 內建的 CompletableFuture 來模擬 Netty Event Loop 的非同步回呼 (Callback) 機制。請仔細觀察主執行緒 (Main Thread) 這次是不是完全沒有卡住:

import java.util.concurrent.CompletableFuture;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;

public class AsyncEventLoopDemo {
    private static String now() {
        return LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
    }

    public static void main(String[] args) {
        System.out.println("[" + now() + "] 餐廳開門!大堂經理 (Main Thread) 開始接客");
        
        List<CompletableFuture<Void>> futures = new ArrayList<>();

        // 瞬間湧入 10 個使用者的上傳請求
        for (int i = 1; i <= 10; i++) {
            final int taskId = i;
            System.out.println("[" + now() + "] 經理: 幫客人 " + taskId + " 點餐,轉交廚房");
            
            // 模擬非同步處理:把任務交給背景的 Event Loop
            CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
                try {
                    // 模擬 S3 網路延遲
                    Thread.sleep(2000); 
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "客人 " + taskId + " 的炸雞";
            }).thenAccept(result -> {
                String threadName = Thread.currentThread().getName();
                System.out.println("[" + now() + "] 送餐員 (" + threadName + "): 叮咚!" + result + " 好了,送餐 V");
            });
            
            futures.add(future);
        }

        System.out.println("[" + now() + "] 大堂經理 (Main Thread): 10個客人點餐完畢!經理去旁邊喝咖啡了");
        
        // 【關鍵防呆】把所有號碼牌收集起來,等待所有任務完成才結束程式
        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
        
        System.out.println("[" + now() + "] 所有客人皆已送餐完畢,餐廳完美打烊!");
    }
}

結果分析

https://ithelp.ithome.com.tw/upload/images/20260901/20183864znUtHnJ1R4.png

仔細看這張圖的時間軸,經理 (Main Thread) 在短短幾毫秒內,一口氣幫 10 個客人點完餐,然後就去旁邊納涼了。他完全沒有被 Thread.sleep(2000) 卡住!且處理十個請求花費總執行時間也只有4秒,比前一天的同步實驗的結果9秒還要快上一倍,

為什麼花了 4 秒? 你可能會想,既然是非同步,為什麼 10 個請求需要分兩批(7 個與 3 個),總共花了 4 秒才處理完?如果真的一點都不阻塞,不是應該 2 秒就全部出餐嗎?

這就是這個模擬程式碼的破綻,也是大家最常搞混的地方!
Java 預設的 CompletableFuture 會使用 ForkJoinPool 作為背景執行緒池。因為我的電腦是 8 核心,Java 預設分配了 7 個背景送餐員(系統核心數 - 1)。

雖然經理沒有卡住,但我們在背景任務裡使用了 Thread.sleep(2000),這是一個絕對阻塞的方法。這 7 個送餐員接下前 7 份訂單後,依然在廚房裡等了 2 秒。直到他們送完前 7 份,才能回頭處理剩下的 3 個客人。

換句話說,這目前段程式碼只做到了「半套非同步」我們解放了主執行緒,但依然卡死了背景執行緒。而在真實的 Netty 底層與 AWS Async SDK 中,連背景執行緒都不會使用阻塞的 sleep 或 read!在真正的全非同步環境下,只需要 1 個 Event Loop 執行緒,就能在 2 秒內毫無阻礙地處理完這 10 個請求。後續我們會以AWS S3連線的方式來執行真正的非同步實作。

最後特別注意第 43 行的 CompletableFuture.allOf().join()。Java 預設的背景執行緒建立的都是守護執行緒 (Daemon Thread)。如果主執行緒沒等大家做完就提早下班,JVM 會直接拔掉電源,導致還在排隊的背景任務直接腰斬。確保任務的完整性,是實作非同步系統時最常踩到的地雷。

Netty 架構:Reactor 模型

為了解決上述「背景執行緒依然被阻塞」的半套問題,真正的 Netty 底層與 AWS Async SDK 採用了業界著名的 Reactor 模型(這也是 Node.js 和 Redis 高效能的核心秘密)。連背景執行緒都不會使用阻塞的 sleepread

實務上在 Netty 的設定中,會將 Event Loop 分為兩組獨立的群組來完美分工 :
https://ithelp.ithome.com.tw/upload/images/20260901/20183864CfQTbSfI1m.jpg

  • BossGroup(接待員): 專門負責在門口接待新客人(接收新的 Socket 連線)。接到客人後,立刻把他安排給後面的員工。
  • WorkerGroup(送餐員): 負責處理客人後續的所有點餐與送餐服務(處理實際的 I/O 讀寫操作)。

透過這種 Reactor 分工,接待員永遠不會被送餐卡住,送餐員也永遠不用在廚房罰站。在真正的全非同步環境下,只需要 1 個 Worker 執行緒,就能在 2 秒內毫無阻礙地處理完剛剛那 10 個請求。整個伺服器的連線池就可以持續高速運轉。

那麼理論課到此結束,大家準備好迎接真正的挑戰了嗎?明天,我們將打開 IDE,使用 Java 21 實作基於 Netty 與 AWS Async SDK 的 S3 非同步上傳引擎,讓大家親手感受非同步連線的威力!


上一篇
[ Day 4 ] 傳統的極限:實作同步連線與執行緒飢餓(Thread Starvation)
系列文
學校沒教的後端生存指南:30 天打造非同步 S3 微服務,部署 K8s 實現 HA 架構5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言