120ms 到 30ms:从 Python 到 Rust


我们喜欢看到性能数据。这是我们的核心目标。我们很高兴看到我们持续努力的另一个里程碑:数据管道的写入延迟减少了 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 = "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
#[derive(Debug)]
struct Event {
    id: u32,
    data: String,
}

// 处理每个事件的函数
async fn handle_event(event: Event) {
    println!(
"Processing event: {:?}", event);
   
// Simulate processing time
    sleep(Duration::from_millis(200)).await;
}

// 处理接收器接收到的数据的函数
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 的通道
    let (tx, rx) = mpsc::channel(100);

   
//生成一个任务来处理接收到的数据
    spawn(process_data(rx));

   
// 使用虚拟数据模拟事件流以进行演示
    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() },
    ];

   
// 通过通道发送事件
    for event in event_stream {
        if tx.send(event).await.is_err() {
            eprintln!(
"Receiver dropped");
        }
    }
}

Rust 示例文件

  1. Cargo.toml:
    • 指定包名称、版本和版次。
    • 包含“完整”功能集所需的 tokio 依赖项。
  • main.rs:
    • 定义事件结构。
    • 实现handle_event函数来处理每个事件。
    • 实现process_data函数来接收和处理来自通道的事件。
    • 为了演示目的,创建一个带有虚拟数据的 event_stream。
    • 使用 Tokio 运行时生成一个处理事件的任务,并通过主函数中的通道发送事件。

    基准
    测试所用的工具
    为了验证我们的性能改进,我们在开发和准备环境中进行了广泛的基准测试。我们使用 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 服务的承诺相结合。我们的经验使我们能够满足并超越现在和未来的实时通信需求。