iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Software Development

學校沒教的後端生存指南:30 天打造非同步 S3 微服務,部署 K8s 實現 HA 架構系列 第 10

[ Day 10 ] 網路世界的殘酷真相:封包遺失、超時處理與最終一致性

  • 分享至 

  • xImage
  •  

昨天我們用 JMeter 與 JConsole 進行了本地端的第一波大壓測,親眼見證了非同步 Netty 在面對高併發流量時,如何以極其穩定的執行緒表現,不過在現實世界中還必須考量到網路異常與分散式資料不一致的災難場景

很多人在剛寫出高吞吐量的非同步 API 後會非常興奮,卻忽略了在分散式系統中,網路是不可靠的!除了網路抖動造成的延遲,封包也有可能真實世界的物理與硬體限制被無情丟棄:

  • 網路壅塞與緩衝區溢位 (Buffer Overflow): 當我們利用 Netty 瞬間併發海量任務時,如果公司對外路由器的頻寬較小,緩衝區瞬間被塞滿,路由器為了自保,就會主動把後續進來的新封包全部丟棄(尾部丟棄)。
  • 物理層訊號干擾與交接 (Handoff): 想像一萬個使用者同時在網路訊號極差的高鐵上,試圖上傳照片到 AWS S3。手機網路在 4G、5G 基地台之間不斷切換,或是經過地下室導致訊號衰減,封包在空中破損後,接收端核對失敗就會直接將其丟棄。

換言之當高併發遇上這些惡劣的網路環境(延遲、超時、遺失封包),我們這套 S3 API 會迎來怎樣的狀態不一致災難?今天我們不寫程式,先回到理論與架構層面,探討分散式事務的痛點,並為接下來的防禦機制打下地基!

網路世界的殘酷真相

在本機開發時(localhost),網路延遲幾乎是 0ms,封包傳輸成功率是 100%。但在雲端生產環境中,微服務與 AWS S3 遠端機房之間跨越了太平洋,網路充滿了不確定性。

當微服務向 S3 發起一個 HTTP 請求,而請求在規定的時間內沒有收到回應,系統就會噴出 TimeoutException(超時)。這時問題來了超時不代表失敗了,其實也有可能成功一半!在網路世界中,一個超時背後可能隱藏著三種截然不同結果:

  1. 去程遺失: 封包根本沒抵達 S3。(上傳失敗)
  2. 回程遺失: S3 儲存了檔案,但在回傳 "200 OK" 的路上,封包遺失了。(上傳成功,但微服務以為失敗)
  3. 處理中超時: S3 收到但處理得太慢(可能來自連不到或是等待太久),超過了微服務等待的極限。(狀態未知)

https://ithelp.ithome.com.tw/upload/images/20260903/201838648Bms8diYuC.jpg

真實世界會發生嗎?

你一定遇過這種情況:在外送平台(如 UberEats 或 Foodpanda)點餐時,按下「確認結帳」,畫面轉了 30 秒後跳出「網路超時,請重試」。你直覺地再按了一次。結果 30 分鐘後,兩個外送員同時抵達或是你的信用卡也被扣了兩次錢!
https://ithelp.ithome.com.tw/upload/images/20260904/20183864NIO6Z2fQIC.png
( 圖片來源 : Yahoo新聞)

這就是最經典的狀態不一致災難。系統遇到超時,沒有做狀態二次確認(Double-Check)也沒有做防重複機制(Idempotency),把超時武斷地視為失敗,最終讓使用者買單。當然我們可以說後續手動補救,客服協助退款,但是造成的是商譽受損,還是要再最初規劃階段就把程式架構設計好,來避免這樣的問題!

災難現場:沒有 DB 的 S3 微服務也會狀態不一致?

我們這次開發的微服務很簡單,只單純把前端的檔案轉傳給S3,沒有串接DB,這樣還會有狀態不一致的問題嗎?

當然會,而且非常嚴重!這裡的不一致,發生在客戶端 (Frontend/App)、微服務與AWS S3三者之間。下方是我們昨天寫為服務的上傳function,已經有寫了try...catch...還會有什麼問題嗎?

public ResponseEntity<String> uploadFileFlow(MultipartFile file) {
    try {
        // 直接調用 S3 引擎上傳檔案
        s3Service.uploadSync(bucket, file.getOriginalFilename(), file.getBytes());
        return ResponseEntity.ok("SUCCESS");
    } catch (Exception e) {
        // 只要發生異常(包含 Timeout),就直接告訴前端失敗
        return ResponseEntity.status(500).body("FAILED");
    }
}

一般正常環境下可能沒問題,但是如果在高併發或弱網路環境下,會引發兩大災難:

  1. 孤兒檔案(Orphan Files)—— 燒錢的無底洞

    • 發生情境:前端呼叫微服務上傳 photo.jpg。在微服務發起 PUT 到 S3 時,發生了前面提到的「回程遺失」導致 S3 其實已經成功儲存了檔案,但微服務因為網路超時而捕獲到 TimeoutException,隨後無奈地向前端回傳了 500 Internal Server Error(失敗)。
    • 不一致:前端此時認為上傳失敗;但 S3 此時狀態卻是上傳成功。
    • 結果:使用者看到網頁顯示失敗,理所當然地按下重新上傳。為了不覆蓋,前端每次重試都生成一個隨機名稱。結果,這個檔案被重複傳了數次,S3 bucket 裡留下了 photo_1.jpg、photo_2.jpg、photo_3.jpg 這些默默躺在雲端、與任何業務關聯斷開的孤兒檔案(Orphan Files),會一直在 S3 累積。
    • 燒錢危機:AWS S3 是按照儲存容量與 API 呼叫次數計費的。這些看不見的吸血鬼孤兒檔案會一直在雲端無情地燃燒錢錢。
  2. 幽靈記錄(Phantom Records / 虛假成功)—— 崩潰的信任危機

    • 發生情境:微服務一收到前端上傳請求,將資料丟進 Netty 的背景傳輸隊列後,不等 S3 的實際回應,就急匆匆地回傳給前端:HTTP 200 SUCCESS, Key=avatar.png。
    • 不一致:前端此時認為上傳成功;但 S3 此時狀態卻是空無一物。
    • 災果:使用者高興地看到網頁顯示上傳成功,隨後興沖沖地把檔案連結分享給朋友。結果在 1 秒後,背景的 Netty 傳輸因為網路突然斷線而徹底傳輸失敗,S3 根本沒存入這個檔案,迎來的卻是冷酷無情的 404 NoSuchKey (Not Found)。使用者在論壇和客服瘋狂投訴:「系統明明跟我說成功了,為什麼點開什麼都沒有?!」,這種讓使用者看到幽靈成功的信任危機,是摧毀品牌商譽的最速路徑。

以上的狀況都還是只有單純client使用者端和S3 api服務的狀況下,如果系統架構還有包含DB要記錄一些metadata或是商業邏輯更複雜一個API行為會包含到三四張不同資料表的變更,如同前面提到的外送平台例子一樣,沒有做好一致性的錯誤處理造成的後果就更嚴重了!

直覺的解法:強一致性分散式事務 (3PC) 是救星嗎?

遇到這種跨節點的不一致問題,教科書上通常會教你使用分散式事務來處理,例如 三階段提交 (3PC-Three-Phase Commit)。

3PC 將整個事務提交過程拆解為三個步驟,並引入了超時自動 Commit 機制,試圖解決死鎖問題:

  1. CanCommit (詢問): 協調者詢問所有節點:「網路與資源都 OK 嗎?」
  2. PreCommit (預備): 大家回應 OK 後,發出預備命令,節點開始鎖定資源。
  3. DoCommit (正式提交): 協調者下達最終寫入指令。

三階段提交 (3PC) 的程式碼實作概念

雖然實務上我們不愛用 3PC,但了解它的實作邏輯,能幫助你徹底明白它為什麼這麼慢。3PC 必須由一個協調者 (Coordinator) 來指揮所有參與者 (Participants)(例如資料庫 A、資料庫 B)。
以下是用 Java 虛擬碼呈現 3PC 協調者的核心邏輯。注意它如何透過三次網路來回(RTT)與超時機制來運作:

public class ThreePhaseCommitCoordinator {

    // 階段 1:CanCommit (詢問大家準備好了沒?)
    public boolean phase1CanCommit(List<Participant> participants, Transaction tx) {
        for (Participant p : participants) {
            // 只要有一個節點回覆 NO 或網路超時,整個事務直接取消
            if (!p.askCanCommit(tx)) { 
                abortAll(participants);
                return false;
            }
        }
        return true;
    }

    // 階段 2:PreCommit (預先提交,鎖定資源)
    public boolean phase2PreCommit(List<Participant> participants, Transaction tx) {
        for (Participant p : participants) {
            // 參與者在此刻會「鎖定 DB 資料行」並寫入 Redo Log,但還不生效
            boolean ack = p.sendPreCommit(tx);
            if (!ack) {
                // 如果有人在這個階段當機,協調者發出 Rollback 廣播釋放鎖定
                rollbackAll(participants);
                return false;
            }
        }
        return true;
    }

    // 階段 3:DoCommit (正式提交)
    public void phase3DoCommit(List<Participant> participants, Transaction tx) {
        for (Participant p : participants) {
            // 發出最終指令。如果協調者在這裡當機沒發出指令,
            // 參與者因為已經過了 PreCommit 階段,等 Timeout 一到,就會「自動提交」。
            p.sendDoCommit(tx); 
        }
    }

    // 完整 3PC 執行流程
    public void executeDistributedTransaction(Transaction tx) {
        List<Participant> participants = getInvolvedNodes();

        if (phase1CanCommit(participants, tx)) {
            if (phase2PreCommit(participants, tx)) {
                phase3DoCommit(participants, tx);
            }
        }
    }
}

為什麼實務上不使用 3PC?

從前面的程式碼就可以看出一個簡單的上傳動作,在 3PC 架構下每一種API請求都必須等待 3 次完整的網路來回,且在階段二和階段三之間,資料庫的資源是被死死鎖住的,所以實務上不使用的原因主要有二:

  1. 效能極差(Latency Multiplier): 一個簡單的上傳動作,要在網路上來回溝通三次。在我們昨天的壓測場景中,如果每個請求都要走 3PC,系統的吞吐量 (QPS) 會瞬間雪崩,毫無效能可言。

  2. AWS S3 根本不支援! S3 是 RESTful API 的物件儲存服務,它底層沒有Transaction的概念。無法對 S3 說:「嘿,我先上傳檔案做 PreCommit,等我確認好你再正式 DoCommit。」S3 只要傳輸完畢,檔案就直接生效了。

輕量級分散式防禦:最終一致性(Eventual Consistency)

既然 3PC 這種強一致性(ACID)在雲端架構中不切實際且無法實作,我們該如何拯救我們的 S3 微服務?

我們不強求前端、微服務與 S3 在同一微秒內保持同步。我們允許系統在網路發生抖動時,處於極短暫的不一致狀態,但我們必須保證:「不管網路怎麼抖動、怎麼超時,系統在幾秒鐘後,會自動對齊狀態,徹底消滅孤兒檔案與幽靈記錄。」所以現在更採用實務權衡:拋棄強一致性,追求最終一致性 (Eventual Consistency)!

為此,我們在未來的章節中,將帶領大家親自實作一套輕量級的防禦機制二部曲:

  1. Double-Check Pattern(狀態二次確認機制): 當微服務遇到超時或未知狀態時,不直接宣告失敗,而是主動發起一次 headObjec 輕量查詢,向 S3 進行二次確認,確定檔案是否真實存在。

  2. Idempotent Client Retries(冪等性重試設計): 客戶端在遇到超時後可以放心重試。我們將在後端設計冪等性校驗,保證重試 100 次,S3 也只會留下一份檔案,絕不產生多餘的孤兒垃圾。

總結

今天我們走出了 localhost 的烏托邦,看清了真實雲端網路中超時的危險,也理解了為什麼教科書上的分散式事務(3PC)在現代 API 型儲存服務前顯得如此無力。

明天,我們將重回 Java 的程式碼戰場,在實作Double-Check Pattern,為微服務打造第一面堅不可摧的自動狀態對齊盾牌!


上一篇
[ Day 9 ] 效能大對決 : 同步Apache vs. 非同步Netty連線AWS S3 API 效能壓力測試
下一篇
[ Day 11 ] 實作 Double-Check Pattern:狀態的二次確認機制
系列文
學校沒教的後端生存指南:30 天打造非同步 S3 微服務,部署 K8s 實現 HA 架構14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言