如何對 Rust web 應用程序做性能分析 - 2 火焰圖

在這篇文章中, 我們使用 cargo-flamegraph 工具對上一篇文章的 Rust web 應用程序進行分析,並繪製火焰圖。

火焰圖

基本思想是,使用 perf 或 dtrace 等工具收集性能數據,特別是在採樣期間,哪些函數佔用了多少 CPU 時間,然後以一種可以很好理解的方式將結果可視化。

火焰圖還可以用於分析 Off-CPU,例如,它可以幫助查找線程頻繁等待 I/O 的問題。

在本例中,我們將只做 CPU 時間分析,這是由 cargo-flamegraph 支持的。

安裝 cargo-flamegraph:

cargo install flamegraph

首先,讓我們在 handler.rs 中增加如下代碼:

 1pub async fn cpu_handler_alloc(clients: FasterClients) -> WebResult<impl Reply> {
 2    let clients_lock = clients.read().await;
 3    let user_ids: Vec<String> = clients_lock
 4        .iter()
 5        .map(|(_, client)| client.user_id.to_string())
 6        .collect();
 7    drop(clients_lock);
 8
 9    let mut result = 0;
10    for i in 0..1000000 {
11        result += user_ids
12            .iter()
13            .cloned()
14            .rev()
15            .map(|user_id| user_id.parse::<usize>().expect("can be parsed to usize"))
16            .fold(i, |acc, x| acc + x);
17    }
18    Ok(reply::html(result.to_string()))
19}

這也是個精心設計的示例,我們重用了 / fast 處理程序,但我們將計算擴展爲在一個長循環中運行。還要注意我們在迭代器上使用. clone(),每次迭代都克隆了整個列表。這是一個相當明顯的性能問題,在運行時,這會導致性能問題。

接下來在 main.rs 中增加如下代碼:

1let cpu_route_alloc = warp::path!("cpualloc")
2    .and(with_faster_clients(faster_clients.clone()))
3    .and_then(handler::cpu_handler_alloc);    
4
5println!("Started server at localhost:8080");
6warp::serve(read_route.or(fast_route).or(cpu_route_alloc))
7    .run(([0, 0, 0, 0], 8080))
8    .await;

此外,我們在 / locust 目錄中添加了另一個名爲 cpu.py 的 Locust 文件:

from locust import HttpUser, task, between
class Basic(HttpUser):
    wait_time = between(0.5, 0.5)
    @task
    def cpu(self):
        self.client.get("/cpualloc")

這在本質上與之前相同,只是調用 / cpualloc 接口。

讓我們使用以下命令運行 cargo flamegraph 來收集分析數據:

sudo cargo flamegraph --bin rust-web-profiling-example

在項目根目錄下生成 flamegraph.svg 文件。

在我們的應用程序上運行上篇文章介紹的負載測試並使用 CTRL+C 停止運行 Rust web 服務器後,我們得到這樣一個火焰圖:

可以看到這種可視化的好處,我們可以從 Tokio 運行時一直跟蹤到 cpu_handler 和計算過程。需要注意的一點是,我們花了很多時間做分配。

這並不奇怪,因爲我們在迭代器中添加了. clone(),對於每次循環迭代,它都會在處理數據之前克隆列表的內容。我們可以看到,在分配塊之間,我們還花了一些時間將字符串解析爲數字。

修復這個問題非常簡單,我們只需刪除. clone(),因爲我們在這裏根本不需要它,但你可能已經注意到,不必要的克隆會導致很大的性能影響,特別是在熱代碼中。

在 src/handler.rs 中增加如下代碼:

 1pub async fn cpu_handler(clients: FasterClients) -> WebResult<impl Reply> {
 2    let clients_lock = clients.read().await;
 3    let user_ids: Vec<String> = clients_lock
 4        .iter()
 5        .map(|(_, client)| client.user_id.to_string())
 6        .collect();
 7    drop(clients_lock);
 8
 9    let mut result = 0;
10    for i in 0..1000000 {
11        result += user_ids
12            .iter()
13            .rev()
14            .map(|user_id| user_id.parse::<usize>().expect("can be parsed to usize"))
15            .fold(i, |acc, x| acc + x);
16    }
17    Ok(reply::html(result.to_string()))
18}

在 src/main.rs 中增加如下代碼:

1let cpu_route = warp::path!("cpu")
2    .and(with_faster_clients(faster_clients.clone()))
3    .and_then(handler::cpu_handler);   
4
5println!("Started server at localhost:8080");
6warp::serve(read_route.or(fast_route).or(cpu_route_alloc).or(cpu_route))
7    .run(([0, 0, 0, 0], 8081))
8    .await;

我們再進行一次抽樣。我們會得到這樣的火焰圖:

差別很大啊!我們花在分配內存上的時間少了很多,而把大部分時間花在將字符串解析爲數字和計算結果上。

你還可以使用 Hotspot 等工具來創建和分析火焰圖。

本文翻譯自:

https://blog.logrocket.com/an-introduction-to-profiling-a-rust-web-application/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/nphBaY9Bms9Tw2gEGGgqNg