120ms 到 30ms:從 Python 到 Rust

banq發表於2024-06-28


我們喜歡看到效能資料。這是我們的核心目標。我們很高興看到我們持續努力的另一個里程碑:資料管道的寫入延遲減少了 4 倍,從 120 毫秒降至 30 毫秒!
這一改進是從透過 Python 應用程式訪問的 C 庫過渡到完全基於 Rust 的實現的結果。

這是對我們的架構變化、實際結果以及對系統效能和使用者體驗的影響的簡單介紹。

從 Python 切換到 Rust
那麼,我們為什麼要從 Python 切換到 Rust?我們的資料管道被所有服務使用!

我們的資料管道是我們實時通訊平臺的支柱。我們的團隊負責將事件資料從所有 API 複製到所有內部系統和服務。資料處理、事件儲存和索引、連線狀態等等。我們的主要目標是確保實時通訊的準確性和可靠性。

在遷移之前,舊管道使用透過 Python 服務訪問的 C 庫,該庫緩衝和捆綁資料。這確實是導致我們延遲的關鍵因素。我們希望進行最佳化,並且知道這是可以實現的。

我們探索了向 Rust 的過渡,因為我們之前已經看到效能、記憶體安全性和併發能力對我們有益。是時候再次這樣做了!

高度重視 Rust 的效能和非同步 IO 優勢
Rust 在效能密集型環境中表現出色,尤其是與 Tokio 等非同步 IO 庫結合使用時。Tokio 支援使用 Rust 程式語言編寫非同步應用程式的多執行緒、非阻塞執行時。遷移到 Rust 使我們能夠充分利用這些功能,實現高吞吐量和低延遲。所有這些都具有編譯時記憶體和併發安全性。

記憶體和併發安全
Rust 的所有權模型為記憶體和併發安全提供了編譯時保證,從而避免了最常見的問題,例如資料競爭、記憶體洩漏和無效記憶體訪問。這對我們來說很有利。

展望未來,我們可以自信地管理程式碼庫的生命週期。如果以後需要,可以進行無情的重構。而且總會有“以後需要”的情況。

使用 MPSC 和 Tokio 進行架構變更、服務到服務以及訊息傳遞的技術實現
以前的架構依賴於服務到服務的訊息傳遞系統,這會帶來相當大的開銷和延遲。Python 服務使用 C 庫來緩衝和捆綁資料。當在多個服務之間交換訊息時,會發生延遲,從而增加系統的複雜性。C 庫中的緩衝機制是一個很大的瓶頸,導致端到端延遲大約為 120 毫秒。我們認為這是最佳的,因為我們每個事件的平均延遲為 40 微秒。雖然從舊的 Python 服務角度來看這看起來不錯,但下游系統在解綁期間受到了影響。這導致總體延遲更高。

當我們部署時,平均每個事件的延遲從原來的 40 微秒增加到 100 微秒。這似乎不是最佳的。

不過,當我們回過頭來看原因時,我們可以看到這是怎麼回事了。
好訊息是,現在下游服務可以更快地逐個使用事件,而無需解綁。

整體端到端延遲有機會從 120 毫秒顯著改善到 30 毫秒。

  • 新的 Rust 應用程式可以立即併發觸發事件。

這種方法在 Python 中是不可能的,因為使用不同的併發模型也需要重寫。我們可能可以用 Python 重寫。如果要重寫,不妨用 Rust 進行最好的重寫!

資源減少 CPU 和記憶體:
我們的 Python 服務會消耗 60% 以上的核心資源。而新的 Rust 服務在多個核心上消耗的資源不到 5%。記憶體減少也非常顯著,Rust 執行時佔用的記憶體約為 200MB,而 Python 則需要佔用數 GB 的記憶體。

基於 Rust 的新架構:

  • 新架構利用了 Rust 強大的併發機制和非同步 IO 功能。
  • 服務到服務的訊息傳遞被利用多生產者、單消費者 (MPSC) 通道的多個例項所取代。
  • Tokio 專為高效的非同步操作而構建,可減少阻塞並提高吞吐量。
  • 我們的資料流程透過消除對中間緩衝階段的需求而得到簡化,轉而選擇併發和並行。
  •  

這些措施提高了效能和效率。

Rust 應用程式示例
該程式碼並非直接複製,它只是一個替代示例,用於模擬我們的生產程式碼的功能。此外,該程式碼僅顯示一個 MPSC,而我們的生產系統使用多個通道。

  • Cargo.toml:我們需要包含 Tokio 和我們可能使用的任何其他板條箱的依賴項(例如事件的非同步通道)。
  • 事件定義:事件型別在程式碼中使用但未定義,因為我們有許多未在此示例中顯示的型別。
  • 事件流:event_stream 被引用,但建立方式與許多流不同。取決於您的方法,因此示例保持簡單。

以下是帶有程式碼和 Cargo.toml 檔案的 Rust 示例。還有事件定義和事件流初始化。

Cargo.toml

[package]
name = <font>"tokio_mpsc_example"
version =
"0.1.0"
edition =
"2021"

[dependencies]
tokio = { version =
"1", features = ["full"] }
main.rs

use tokio::sync::mpsc;
use tokio::task::spawn;
use tokio::time::{sleep, Duration};

// Define the Event type<i>
#[derive(Debug)]
struct Event {
    id: u32,
    data: String,
}

// 處理每個事件的函式<i>
async fn handle_event(event: Event) {
    println!(
"Processing event: {:?}", event);
   
// Simulate processing time<i>
    sleep(Duration::from_millis(200)).await;
}

// 處理接收器接收到的資料的函式<i>
async fn process_data(mut rx: mpsc::Receiver<Event>) {
    while let Some(event) = rx.recv().await {
        handle_event(event).await;
    }
}

#[tokio::main]
async fn main() {
   
// 建立緩衝區大小為 100 的通道<i>
    let (tx, rx) = mpsc::channel(100);

   
//生成一個任務來處理接收到的資料<i>
    spawn(process_data(rx));

   
// 使用虛擬資料模擬事件流以進行演示<i>
    let event_stream = vec![
        Event { id: 1, data:
"Event 1".to_string() },
        Event { id: 2, data:
"Event 2".to_string() },
        Event { id: 3, data:
"Event 3".to_string() },
    ];

   
// 透過通道傳送事件<i>
    for event in event_stream {
        if tx.send(event).await.is_err() {
            eprintln!(
"Receiver dropped");
        }
    }
}

Rust 示例檔案

  1. Cargo.toml:
    • 指定包名稱、版本和版次。
    • 包含“完整”功能集所需的 tokio 依賴項。
  2. main.rs:
    • 定義事件結構。
    • 實現handle_event函式來處理每個事件。
    • 實現process_data函式來接收和處理來自通道的事件。
    • 為了演示目的,建立一個帶有虛擬資料的 event_stream。
    • 使用 Tokio 執行時生成一個處理事件的任務,並透過主函式中的通道傳送事件。
    <ul>
  3. 基準
    測試所用的工具
    為了驗證我們的效能改進,我們在開發和準備環境中進行了廣泛的基準測試。我們使用 hyperfine https://github.com/sharkdp/hyperfinecriterion.rs   https://crates.io/crates/criterion 等工具  來收集延遲和吞吐量指標。我們模擬了各種場景來模擬類似生產的負載,包括高峰流量期和極端情況。

    生產驗證
    為了評估生產環境的實際效能,我們使用 Grafana 和 Prometheus 實施了持續監控。此設定允許跟蹤關鍵指標,例如寫入延遲、吞吐量和資源利用率。此外,還配置了警報和儀表板,以便及時識別系統效能中的任何偏差或瓶頸,確保可以及時解決潛在問題。當然,我們會在幾周內謹慎地部署到低流量百分比。您看到的圖表是我們驗證階段後的全面部署。

    僅有基準還不夠
    負載測試證明了改進。雖然是的,但測試並不能證明成功,因為它提供了證據。寫入延遲從 120 毫秒持續減少到 30 毫秒。響應時間得到增強,端到端資料可用性得到加速。這些進步顯著提高了整體效能和效率。

    之前和之後
    在舊系統出現之前,服務到服務的訊息傳遞是透過 C 庫緩衝完成的。這涉及訊息傳遞迴圈中的多個服務,並且 C 庫透過事件緩衝增加了延遲。由於 Python 的全域性直譯器鎖 (GIL) 及其固有的運營開銷,Python 服務增加了一層額外的延遲。這些因素導致了較高的端到端延遲、複雜的錯誤處理和除錯過程,以及由於事件緩衝和 Python GIL 引入的瓶頸而導致的有限的可擴充套件性。

    實施 Rust 後,透過直接渠道傳遞訊息消除了中介服務,而 Tokio 啟用了非阻塞非同步 IO,顯著提高了吞吐量。Rust 的嚴格編譯時保證了執行時錯誤的減少,我們獲得了強大的效能。觀察到的改進包括端到端延遲從 120 毫秒減少到 30 毫秒,透過高效的資源管理增強了可擴充套件性,並透過 Rust 的嚴格型別和錯誤處理模型改善了錯誤處理和除錯。除了 Rust 之外,很難爭論使用其他任何東西。

    部署和運營
    最小限度的操作變化
    部署經過了最小程度的修改,以適應從 Python 到 Rust 的遷移。相同的部署和 CI/CD。配置管理繼續利用現有工具(如 Ansible 和 Terraform),促進無縫整合。這使我們能夠順利過渡,而不會中斷現有的部署流程。這是一種常見的方法。您希望在遷移過程中儘可能少地進行更改。這樣,如果出現問題,我們可以隔離足跡並更快地找到問題。

    監控和維護
    我們的應用程式與現有的監控堆疊無縫整合,包括 Prometheus 和 Grafana,可實現實時指標監控。Rust 的記憶體安全功能和減少的執行時錯誤顯著降低了維護開銷,從而實現了更穩定、更高效的應用程式。很高興看到我們的構建系統正常工作,甚至更棒的是,我們可以在膝上型電腦上捕獲開發過程中的錯誤,這使我們能夠在推送可能導致構建失敗的提交之前捕獲錯誤。

    對使用者體驗的實際影響
    提高資料可用性更快的寫入操作可實現近乎即時的資料讀取和索引準備,從而提升使用者體驗。這些增強功能包括減少資料檢索延遲,從而實現更高效、響應更快的應用程式。實時分析和洞察也更好。這為企業提供了最新資訊,以便做出明智的決策。此外,在所有使用者介面上更快地傳播更新可確保使用者始終能夠訪問最新資料,從而增強使用我們提供的 API 的團隊的協作和生產力。從外部角度來看,延遲是顯而易見的。結合 API 可以確保資料現在可用且更快。

    提高系統可擴充套件性和可靠性
    專注於 Rust 的企業將獲得顯著的提升優勢。他們將能夠分析大量資料,而不會降低系統速度。這意味著您可以跟上使用者負載。而且,我們不要忘記更具彈性的系統和更少的停機時間帶來的額外好處。我們經營著一家擁有十億臺聯網裝置的企業,中斷是絕對不允許的,連續執行是必須的。

    過渡到 Rust 不僅顯著降低了延遲,還為未來效能、可擴充套件性和可靠性的增強奠定了堅實的基礎。我們為使用者提供最佳體驗。

    Rust 與我們致力於為數十億使用者提供最佳 API 服務的承諾相結合。我們的經驗使我們能夠滿足並超越現在和未來的實時通訊需求。

    相關文章