iT邦幫忙

2026 iThome 鐵人賽

DAY 29
0
Software Development

從零打造邊緣運算閘道器:Raspberry Pi 與 Linux 底層軟硬整合實戰系列 第 29

Day 29:實機壓力測試:如何寫一支 Fault Injection (錯誤注入) 腳本摧毀自己的系統?

  • 分享至 

  • xImage
  •  

🎯 問題情境 (The Problem)

許多工程團隊在測試邊緣運算閘道器 (EdgeNode) 時,往往只專注於 「快樂路徑 (Happy Path)」

  • 網路順暢時,檔案能成功上傳。
  • 隨身碟正常拔插時,系統能正確備份。
  • 訊號正常時,封包能順利解析。

然而,根據莫非定律 (Murphy's Law),「會發生的壞事,一定會在最糟糕的時間點發生」。現場實際運行的 EdgeNode 每天都在承受極端考驗:

  1. 傳輸大容量數據時,網路突然高延遲 (High Latency) 甚至隨機斷開 (Packet Loss)。
  2. 隨身碟正在寫入重要數據時,使用者突襲式熱插拔 (Hot Unplug)。
  3. 序列埠 (UART) 通訊遭受電磁干擾,吐出一串亂碼或損壞的封包。
  4. 樹莓派 CPU 與 Memory 被其他背景行程塞爆。

如果沒有經過嚴苛的 「混沌工程 (Chaos Engineering)」 驗證,這些隱藏的 Race Condition (競態條件) 與 Deadlock (死鎖) 將會直接在產線現場爆發。


❌ 錯誤嘗試 (The Pitfalls)

傳統的硬體壓力測試方法往往存在嚴重盲點:

1. 手動拔插電源與線材

工程師坐在桌前手動拔插 USB 線 10 次,沒發現異常就宣告測試通過。這種作法無法觸發「恰好在 fwrite 呼叫當下拔線」這種毫秒級別的 Race Condition Window。

2. 缺乏可重複性與自動化數據統計

隨機手動搞破壞,即便系統崩潰了,也無法重現 (Reproduce) 故障當下的精確時間點與記憶體狀態。

3. 只進行軟體單元測試 (Unit Test)

單元測試模擬的 Mock 物件過於理想化,無法反映 Linux 核心層 (Kernel Module)、USB OTG 驅動、以及實體 File System 在受壓時的真實物理行為。


🧠 底層原理:錯誤注入 (Fault Injection) 與 Chaos 模組設計

為了全面驗證 EdgeNode 的韌性 (Resilience),我們採用 「破壞者模式 (Chaos Engine)」。透過自動化腳本,直接在 OS 核心層與應用層之間注入以下四大故障:

https://ithelp.ithome.com.tw/upload/images/20260914/20183645YUxYy1j3xG.png

1. 網路層:Linux Traffic Control (tc)

利用 Linux 核心的 netem (Network Emulation) 模組,動態注入 500ms 延遲與 30% 丟包率,模擬惡劣的行動網路。

2. USB OTG 層:UDC Dynamic Unbind

透過向 /sys/class/udc/3f980000.usb/soft_connect 寫入 0disconnect,在毫秒級別精確切斷 USB 隨身碟模擬,模擬無預警拔線。

3. 通訊層:UART Noise Injection

在正常的 UART 資料串流中,隨機插入偽造的 Header 或損壞的 Checksum 封包,驗證 Parser 的 Sanitization 防護。


💡 最終解決方案:自動化 Chaos 壓力測試腳本 (chaos_monkey.py)

以下為我們為 EdgeNode 打造的自動化錯誤注入測試工具:

#!/usr/bin/env python3
# /opt/gateway/tools/chaos_monkey.py
# EdgeNode 混沌工程與 Fault Injection 壓力測試腳本

import os
import sys
import time
import random
import subprocess
import threading

class ChaosMonkey:
    def __init__(self, target_ip="10.0.0.5", duration_sec=60):
        self.target_ip = target_ip
        self.duration_sec = duration_sec
        self.is_running = False
        self.stats = {"network_faults": 0, "usb_unplugs": 0, "noise_injections": 0}

    def inject_network_latency(self):
        """1. 模擬網路惡劣環境 (High Latency & Packet Loss)"""
        print("[Chaos] 💥 注入網路延遲與丟包 (tc netem)...")
        cmd_add = "sudo tc qdisc add dev wlan0 root netem delay 300ms 100ms loss 20%"
        cmd_del = "sudo tc qdisc del dev wlan0 root netem 2>/dev/null"
        
        subprocess.run(cmd_add, shell=True)
        self.stats["network_faults"] += 1
        time.sleep(random.uniform(5, 10))
        subprocess.run(cmd_del, shell=True)
        print("[Chaos] 🟢 恢復正常網路環境。")

    def inject_usb_hot_unplug(self):
        """2. 毫秒級微秒切斷 USB OTG 連結 (Software Re-plug Attack)"""
        print("[Chaos] 💥 觸發 USB OTG 軟體突襲式熱插拔...")
        udc_path = "/sys/class/udc/3f980000.usb/soft_connect"
        
        # 強制斷開 UDC
        cmd_disconnect = "echo 'disconnect' | sudo tee /sys/class/udc/*/soft_connect > /dev/null 2>&1 || true"
        cmd_connect = "echo 'connect' | sudo tee /sys/class/udc/*/soft_connect > /dev/null 2>&1 || true"
        
        subprocess.run(cmd_disconnect, shell=True)
        self.stats["usb_unplugs"] += 1
        time.sleep(random.uniform(0.5, 2.0))
        subprocess.run(cmd_connect, shell=True)
        print("[Chaos] 🟢 重新掛載 USB OTG。")

    def inject_uart_garbage_data(self):
        """3. 向序列埠寫入隨機垃圾訊號 (Noise Injection)"""
        print("[Chaos] 💥 注入 UART 嘈雜干擾訊號...")
        garbage = os.urandom(64)
        try:
            # 寫入虛擬或實體串口
            with open("/dev/ttyAMA0", "wb", buffering=0) as f:
                f.write(garbage)
            self.stats["noise_injections"] += 1
        except Exception as e:
            pass

    def run_stress_test(self):
        """主混沌測試迴圈"""
        print(f"==================================================")
        print(f" 🐒 啟動 EdgeNode 混沌工程測試 (預計執行 {self.duration_sec} 秒)")
        print(f"==================================================")
        
        self.is_running = True
        start_time = time.time()
        
        while time.time() - start_time < self.duration_sec:
            # 隨機挑選破壞手段
            action = random.choice([
                self.inject_network_latency,
                self.inject_usb_hot_unplug,
                self.inject_uart_garbage_data
            ])
            
            action()
            time.sleep(random.uniform(3, 8))
            
        self.is_running = False
        print(f"\n==================================================")
        print(f" 📊 測試結束!破壞統計報告:")
        print(f" - 網路故障注入次數: {self.stats['network_faults']}")
        print(f" - USB 突襲熱拔插次數: {self.stats['usb_unplugs']}")
        print(f" - UART 雜訊注入次數: {self.stats['noise_injections']}")
        print(f"==================================================")

if __name__ == "__main__":
    monkey = ChaosMonkey(duration_sec=30)
    monkey.run_stress_test()

🎯 驗證標準:受壓後的「三大零容忍指標」

經過 chaos_monkey.py 連續 24 小時不間斷的破壞注入後,EdgeNode 必須滿足以下指標,方可批准正式發布出貨:

  1. 0 數據損毀 (Zero Data Corruption):所有的 CSV / Raw PPG 檔案格式與筆數必須 100% 符合金標規範。
  2. 0 進程死鎖 (Zero Process Deadlock)systemd 服務必須保持 active (running),若有崩潰必須在 5 秒內自我重啟並自動修復檔案系統。
  3. 0 記憶體洩漏 (Zero Memory Leak):經過上百次斷線與重連,Python 進程記憶體佔用率變動不得超過 5%。

🎯 小結

在 Day 29 中,我們學會了如何「像敵手一樣思考」:

  1. 擺脫 Happy Path 迷思:主動設計 Chaos 工具摧毀自己的軟硬體。
  2. 模擬真實現場故障:涵蓋 tc 網路干擾、UDC 突襲斷線與 UART 雜訊。
  3. 建立嚴格的出貨指標:用數據證明系統具備 100% 的自癒與抗打擊能力。

明天將迎來本系列文章的最終章 Day 30「[完賽回顧] 從原型到穩定出貨:開源這個邊緣運算閘道器的未竟之路」


上一篇
Day 28:系統服務化:systemd 守護神與 journalctl 中文亂碼 (Locale) 的坑
下一篇
Day 30 - 從原型到穩定出貨:開源這個邊緣運算閘道器的未竟之路
系列文
從零打造邊緣運算閘道器:Raspberry Pi 與 Linux 底層軟硬整合實戰30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言