Skip to main content

Command Palette

Search for a command to run...

Day 7 閒聊I/O密集型任務與 Context Switch

Updated
4 min read
Day 7 閒聊I/O密集型任務與 Context Switch

突然今天想寫這篇是因為 Line 社群有網友問到 I/O密集型任務 如果開大量 Thread 或是將這個任務以容器啟動了數十個容器在消費從 Message Queue 接收到的事件,然後做大量的 I/O 密集任務。會不會導致 Context Switch ?以及如果真要提高吞吐量與處理效能,開大量 Worker 是常見的方式,但要怎能知道開多少數量的 Worker 在這台機器上適合呢?

今天先從資源使用情況來看,明天在從任務的執行時間來分析。

今天會講比較多基本知識先了解打底用,也能當我在混天數吧 :)


CPU 使用率

CPU 的使用率可以透過測量一段時間內 CPU 忙於執行任務的時間比例獲得,通常以百分比 % 表示。也可以透過測量 CPU 未執行 kernel idel 的時間得出,這段時間內 CPU 可能會執行一些 user level 的應用程式,或其他的 kernel 程式,或者在處理 interrupt。

CPU 使用率高不代表一定有問題,只能說系統有在工作。也有人認為這是投資回報率(ROI)的指標,畢竟機器買了租了就用好用滿。CPU 資源高度被利用的系統認為有著較好的 ROI,而空閒太多的系統則是浪費。這點與硬碟(I/O)有著很大的不同。

ROI 很重要,一開始就用的好,則花費都用在刀口上。

後期才投入分析的話,那就是省運行成本,看能否跟老闆凹,省下來的10%當bonus吧!

在OpenTeletetry 入門指南,第 2 章也有提到可觀測性工程對於數位轉型的 ROI 是否幫助的。

CPU 使用率高,不等於應用程式的性能跟著出現顯著的下降!因為 kernel 支援優先級別的處理、搶佔處理和分時共享處理。這些概念組合起來讓 kernel 決定了什麼應用程式或執行緒的優先級更高,並保證它優先執行。

CPU 的時間花費在處理 user space 的時間稱為 User-CPU-Time,而執行 kernel 類型的時間稱為 System-CPU-Time。 System-CPU-Time 包含系統底層調用、kernel 執行緒和處理 interrupt 的時間。在整個系統範圍內進行量測時,User-CPU-Time 與 System-CPU-Time的比例揭示了該系統執行的負載類型。

如果User-CPU-Time 比例很高,那麼可能就在處理像是影像處理、機器學習、數學運算或數據分析等。

反之,如果 System-CPU-Time 很高,則可能是 I/O Intensive Workload,通過 kernel 在進行 I/O 操作。

I/O Intensive Workload

又稱 I/O-bound 或 I/O 密集型工作負載。這裡的 bound 或 intensive,指「受限於」或「受制於」。當我們說一個任務是「I/O-bound」時,意思是這個任務的性能或速度主要受限於 I/O 操作的速度,而非 CPU 的處理能力。

當一個任務是 I/O-intensive 時,系統的其他資源(例如 CPU、記憶體等)可能無法被充分利用。這是因為系統必須等待 I/O 操作完成,而在這段等待時間內,其他資源可能處於閒置或低效狀態。也就是說,儘管 CPU 可能有足夠的能力處理更多的計算任務,但由於 I/O 操作成為瓶頸,整個系統的資源利用率會受到限制。

例如,在一個 I/O-bound 系統中,即使 CPU 的利用率很低,系統整體的性能也可能達不到預期,因為它主要受限於磁碟或網路 I/O 操作的速度和容量。這種情況下,即便增加更多的 CPU 或記憶體資源,也無法顯著提高系統性能,因為真正的瓶頸是 I/O 操作。

相呼應的是速度,系統在處理這類任務時,CPU 的計算能力可能有富餘,但由於需要等待 I/O 操作(例如讀取磁碟、網路請求、文件讀寫等)完成,整體系統的速度和性能會受到這些 I/O 操作的制約。因此,任務的執行效率主要取決於 I/O 操作的效率,而不是計算的速度。

舉例來說,假設一個應用程序需要頻繁地從磁碟讀取數據並進行處理,如果磁碟讀取速度較慢,即使 CPU 再快,也要等待數據讀取完成後才能繼續處理。這時候,我們就可以說這個任務是「I/O-bound」,因為它的性能主要受限於磁碟的 I/O 速度。

[YouTube I/O Bound Process](https://youtu.be/6CegqIE-yxk?t=204)

該影片用圖簡單闡述,I/O Bound 的處理,其實真正用到 CPU 的時間很少很少。但對於一個I/O操作具體什麼時候能回應,其實是未知的。由於 I/O 操作的延遲不可預期,這就導致了系統在等待 I/O 回應的過程中,CPU 的資源可能無法被充分利用。在這些等待期間,CPU 可能切換到另 一個可以利用 CPU 資源任務的執行,這就引出了 CPU Context Switching 的概念。

CPU Context Switching

Context

這裡指的 Context,指的是該應用程式/Thread/Coroutine 等等在執行時的環境,包含了所有的 Register 的內容、該應用程式正在使用的文件(或你說 FileDescriptor)、記憶體中的等變數內容(MMU)等等。

[圖片來源 CPU 上下文切換](https://jaminzhang.github.io/os/CPU-Context-Switch/)

上圖示意,同一個 CPU 從 Process A 切換至 Process B 來執行的流程跟需要用到的 context 內容,過程中會進行現有 context 的儲存,已經載入新的 context。

其實 CPU context swithing 細說有三種,Process context switching、Thread context switching 與 Interrupt Context Switching。有興趣能根據這關鍵字去搜尋學習。

這三種類型的 context switching 切換的成本依序是 Process context switching > Thread context switch > Interrupt Context Switching。

在 Linux 系統中,這些 context switching 共同協作,以確保系統能夠多任務併發運行,並及時回應各種事件和操作。但這些 switching 其實都有成本與開銷,如果設計軟體時沒設計好,就會產生巨量的 context switching 其實反而沒達到當初想要的吞吐量與效能,反而表現的會更差。

用 Go 產生大量的 context switching

通過增加大量的 Worker 或 Thread 來提高吞吐量是一種常見的優化方法,但我們需要謹慎地評估系統的 context switching 頻率,因為過多的 context switching 可能會使問題更加嚴重。

Go 有個函式 Gosched() 可以強制讓該 goroutine 釋放所佔有的 CPU,來讓其他 gorouine 能使用該 CPU 處理事情。搭配 GOMAXPROCS() 限定該程式最多使用 1 個 CPU 核心。透過模擬單核心環境中的行為,這樣所有的goroutine 都必須在同一個 CPU 上執行,這也意味著它們需要更多的 context switching 來共享這個 CPU 處理時間。

package main

import (
    "flag"
    "fmt"
    "log"
    "os"
    "os/signal"
    "runtime"
    "sync"
    "syscall"
)

func excessiveWorker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    for {
        // 模擬大量的磁碟 I/O 操作
        data := make([]byte, 1024*1024*1) // 1MB 大小的資料
        err := os.WriteFile(fmt.Sprintf("/tmp/testfile_%d", id), data, 0644)
        if err != nil {
            continue
        }

        _, err = os.ReadFile(fmt.Sprintf("/tmp/testfile_%d", id))
        if err != nil {
            continue
        }

        runtime.Gosched()

        // 模擬取得資料後的計算
        sum := 0
        for i := 0; i < 1000; i++ {
            sum += i
        }
    }
}

func main() {
    numWorkers := flag.Int("workers", 1000, "number of workers to start")
    procs := flag.Int("procs", 1, "number of go max procs")

    flag.Parse()

    runtime.GOMAXPROCS(*procs)

    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

    var wg sync.WaitGroup

    for i := 0; i < *numWorkers; i++ {
        wg.Add(1)
        go excessiveWorker(i, &wg)
    }
    fmt.Printf("Running workers: %d", *numWorkers)
    go func() {
        <-sigChan
        log.Println("Received signal, shutting down...")
        os.Exit(0)
    }()

    wg.Wait()
}
go build -o cs main.go
./cs

在我執行該程式後,首先我先使用 TOP 這工具做簡單的觀察,

執行程式之前

執行程式之後

能看見load average 瞬間來到五百多。load average: 518.11, 197.55, 75.01分別表示系統在過去 1 分鐘、5 分鐘和 15 分鐘內的平均負載。這些數值非常高,表示系統的運行隊列中有大量的程式在等待 CPU 資源。

然後 CPU 情況,%Cpu(s): 3.4 us, 15.8 sy, 0.0 ni, 0.0 id, 80.8 wa, 0.0 hi, 0.0 si, 0.0 st,其中80.8 wa: 等待 I/O 操作完成的時間百分比。這是主要的負載來源,表示 CPU 大部分時間在等待磁碟或其他 I/O 操作完成。

然後能看到這程式使用了 VIRT 3.1 GB, RES 佔用了 160MB。%CPU 169.9: 該程式大約 1.7 個 CPU 核心。

RES 是我們這程式佔用的實際物理記憶體大小,佔用 160MB,因為我們有把文件內的資料讀取進去程式中。VIRT 來到了 3.1 GB,這是由於大量的 goroutine 分配或HTTP 請求的高併發處理,以及 Go 運行時記憶體管理所致。這些因素在大量併發的情況下累積,會導致 VIRT 看起來非常大,但實際上未必消耗了同樣多的物理記憶體。

這次 top 的輸出顯示了系統處於極高負載的狀態,主要表現在高 load average、高 wa 值和幾乎耗盡的記憶體。這些數據表明系統可能正在執行大量 I/O 密集型任務,導致 CPU 大部分時間在等待 I/O 完成,記憶體資源緊張,並且進程競爭 CPU 資源非常激烈。如果不加以優化,系統性能可能會進一步惡化,影響到正常運行。

VMSTAT

VMSTAT 是個工具能夠動態監看 OS 的 CPU、記憶體、I/O 等活動。我們能透過 TOP 看到資源的使用情況,但還是看不到有在發生 context switching。其中在 VMSTAT 提供了 system 中提供了 cs (context switches per second)。還有 CPU 的資訊像是 us(user time) 和 sy(system time)、id(idle)和 wa(waiting for IO)。

vmstat 5 表示我每5秒統計顯示一次數據,這裡面的 cs 是這5秒內,每秒平均發生 context switching的次數。可以看見我平時大概每秒3000次上下的context switching。大多數情況下 us 和 sy 僅有 1% 到 2% 的 CPU 使用,這表示系統在執行較少的計算任務。然後 CPU 大部分時間處於 Idle,顯示 95% 到 99% 的 idle 時間。這表示系統在當前負載下有足夠的資源。

接著來啟動 go cs程式。可以看見下圖,第 2 行開始有很顯著的不同了。Context switching 次數從數百次到數千次迅速增加到每秒幾千次甚至超過一萬次。,這正是由於我建立了大量 goroutine 並在單個 CPU 核心上運行所導致的結果。這樣的情況會顯著增加系統的調度負擔,導致更多的 context switching。我們能通過 VMSTAT 該工具觀察。

此外這裡還透漏很多資訊。b: 正在等待 I/O 操作完成的thread數量。這裡的數字非常高,大約在 1000,這與我們程式設定的 numWorkers 一致,這表示有大量的進程因為等待 I/O 操作(例如磁碟讀寫)而被阻塞。I/O 的 bo 寫入量很高,這是能理解的,畢竟我們真的有寫資料的動作。

重點在於 System 與 CPU 的部份。 in: 每秒進行的 interrupt 次數。in 值大幅增加,有時高達 11,827 次,表示系統正在處理大量的中斷。cs: 每秒上下文切換的次數。cs 值大幅增加,有時高達 17,219 次,表示大量的 context switching,系統負載非常高。us: User space 程式佔用的 CPU 百分比。通常在 0% 到 4% 之間,這表示 CPU 的計算資源大部分用於處理系統操作和 I/O,而不是應用程式邏輯。id: 空閒時間百分比。大部分時間 id 非常低,甚至達到 0%,這表明 CPU 幾乎沒有空閒時間,完全被利用。wa: I/O 等待時間百分比。wa 值非常高,達到 94% 甚至更高,這表明 CPU 大部分時間在等待 I/O 操作完成。這是 I/O 密集型負載的特徵。

這些統計資料表明系統處於非常高的負載狀態,特別是在 I/O 操作上,CPU 大部分時間都在等待磁碟 I/O 完成。相比於平常,系統的記憶體和 I/O 資源消耗非常嚴重,Context switching 次數大幅增加,CPU 幾乎無空閒時間。這樣的情況可能會導致系統性能下降或回應變慢,特別是在 I/O 密集型應用場景下。

PIDSTAT

剛剛 VMSTAT 能看到整體情況,但要是我們想往更細緻的方向去排查分析,通常就要依賴 pidstat 這工具了。 pidstat 主要用來監控和報告個別處理程式的各種性能指標,包括 CPU 使用率、記憶體使用率、I/O 操作、Context switching 等。

其中 -w 是用來顯示處理程式的Context switching次數,包括 cswch 自願 Context switching(由處理程自身引起)和 nvcswch 非自願Context switching(由操作系統引起)。

能看見下圖所示,PID 128422 (cs):此處的 cs 處理程式在報告時間內每秒發生了 4701.70 次 cswch,並且有 11.89 次 nvcswch/s。這表明該進程頻繁地進行了 I/O 操作或其他等待操作,導致了大量的 cswch

那我們就能抓到是哪個程式沒設計好,導致 CPU 的資源忙於 Context switching上。

明天再來分享 go tool 怎發現這樣的情況。

L

好文!愛看,請多發點~

More from this blog

Claude Code 監控秘錄:OpenTelemetry(OTel/OTLP)實戰指南

稟告主公:此乃司馬懿進呈之兵書,詳解如何以 OpenTelemetry 陣法,令臥龍神算之一舉一動盡在掌握,知糧草消耗、察兵器效能、辨戰報異常,使主公運籌帷幄於大帳之中。 為何需要斥候情報? 司馬懿稟告主公: 臥龍神算(Claude Code)乃當世利器,然若無斥候回報,主公便如蒙眼行軍——兵器耗損幾何、糧草消費幾許、哪路斥候出了差錯,一概不知。臣以為,此乃兵家大忌。 無情報之弊,有四: 軍

Feb 19, 202610 min read184
Claude Code 監控秘錄:OpenTelemetry(OTel/OTLP)實戰指南

工程師的 Claude Code 實戰指南:從零開始到高效開發

工程師的 Claude Code 實戰指南:從零開始到高效開發 本文整合 Anthropic 官方 Best Practices 與社群實戰 Tips,帶你由淺入深掌握 Claude Code。 什麼是 Claude Code?為什麼值得學? 如果你還在用「複製程式碼貼到 ChatGPT,再複製答案貼回去」的工作流程,Claude Code 會讓你大開眼界。 Claude Code 是 Anthropic 推出的命令列工具,它直接活在你的 terminal 裡,能夠讀懂你的整個 codeb...

Feb 18, 20265 min read80
工程師的 Claude Code 實戰指南:從零開始到高效開發

System Design Interview Ch 12 Digital Wallet

確立問題與設計範疇 角色對話內容 面試者我們應該只關注兩個數位錢包之間的餘額轉帳操作嗎?我們是否需要擔心其他功能? 面試官讓我們只關注餘額轉帳操作。 面試者該系統需要支援多少 TPS(每秒交易次數)? 面試官讓我們假設是 1,000,000 TPS (每秒 100 萬次交易)。 面試者數位錢包對正確性有嚴格的要求。我們可以假設事務保證 就足夠了嗎? 面試官聽起來不錯。 面試者我們需要證明正確性嗎? 面試官這是一個很好的問題。正確性(Correctness)通常只有在交...

Feb 2, 202610 min read229
System Design Interview Ch 12 Digital Wallet

Claude Code 利用 Event-Driven Hooks 打造自動化開發大腦

在現代 AI 輔助開發中,我們不僅需要 AI 寫程式,更需要它懂規則、記性好,並且能自動處理那些繁瑣的雜事。透過 Claude Code Hooks 機制,我們可以介入 AI 的思考與執行迴圈,實現真正的「人機協作自動化」。 一、 動機與痛點:為什麼你需要介入 AI 的生命週期? 在預設狀態下,Claude Code 雖然強大,但它是「被動」且「無狀態」的,這導致了開發者常遇到以下痛點: 記憶重置 (Session Amnesia): 痛點:每次重啟終端機,AI 就像失憶一樣。 解法:你...

Jan 24, 20266 min read549
Claude Code 利用 Event-Driven Hooks 打造自動化開發大腦
M

MicroFIRE

71 posts