得物技术 01月20日
RUST练习生如何在生产环境构建万亿流量|得物技术
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

文章探讨了计算层从Java迁移至Rust的全流程,介绍了Rust的特性及优势,包括内存安全、高性能等,还阐述了Rust在应用部署中的情况,以及迁移后带来的好处和面临的挑战。

Rust的所有权机制确保内存安全,避免内存泄漏和数据竞争,其规则包括单一所有权、作用域结束时自动清理、所有权转移和借用。

Rust的生命周期与借用机制保证内存安全和线程安全,分为不可变借用和可变借用。

Rust的并发模型通过语言特性在编译时强制执行线程安全,包括线程、消息传递、异步编程、锁和同步原语等,避免数据竞争。

Rust应用的部署过程简单高效,可通过Cargo编译,监控包括性能监控、日志管理和系统健康检查。

原创 南风 2025-01-20 18:30 上海

作为一种新兴的底层系统语言,Rust 以其独特的内存安全机制、与 C/C++ 相媲美的高性能、活跃的开发者社区以及出色的文档、工具链和 IDE 等优点而闻名。

 

 

目录

一、引言:Rust在可观测性体系中的崛起与机遇

二、Rust: 系统级编程的破冰之旅

    1. 所有权

    2. 生命周期与借用

    3. 并发模型

三、应用部署

    1. Rust 应用的部署

    2. 监控 Rust 应用

    3. 小

四、总结

引言

《得物新一代可观测性架构:海量数据下的存算分离设计与实践》一文中,我们探讨了存算分离架构如何通过解耦计算与存储资源,显著降低存储成本并提升系统扩展性。然而,仅优化存储成本不足以支撑高效可观测性系统的全局目标。在生产环境中,计算层作为可观测性体系的核心模块,需在处理日益复杂和动态的大流量数据时,保持高性能、强稳定性与优异的资源利用效率。


在得物的可观测性计算层中,Java凭借其成熟的生态系统和强大的工具链,在系统建设初期帮助团队快速迭代和稳定交付。然而,随着业务流量规模的指数级增长,计算层对低延迟处理高效资源利用的要求不断提高,Java开始显现局限性:


为了缓解Java在高吞吐场景下的GC延迟问题,我们尝试引入了ZGC(Z Garbage Collector),这是Java的一种低延迟垃圾回收器,它通过并发执行大部分垃圾回收任务来减少应用程序的停顿时间。然而,我们观察到ZGC在处理大量数据时可能会占用较多的CPU资源,并且在需要大堆内存时可能导致内存使用率上升,这在资源受限的环境中可能成为性能瓶颈


在此背景下,Rust成为计算层技术升级的关键选项。Rust以其内存安全、零成本抽象和高性能异步编程模型(如 Tokio)闻名,不仅可以规避GC相关性能波动,还能在资源利用效率上带来显著优势。Rust 的优势促使其在许多高性能场景中逐渐取代传统的编程语言。作为一种新兴的底层系统语言,Rust 以其独特的内存安全机制、与 C/C++ 相媲美的高性能、活跃的开发者社区以及出色的文档、工具链和 IDE 等优点而闻名。


因此,可观测性计算层迁移到Rust后获得了以下显著好处:


本篇文章将结合得物可观测性的技术实践,深入探讨计算层从Java迁移至Rust的全流程。我们将重点分析迁移过程中的技术挑战及其解决方案,展示Rust如何在万亿流量场景下实现性能与资源优化,并为其他面临类似挑战的团队提供实践参考与技术启发。


遇到的问题


如图所示,得物可观测性架构采用经典的数据处理流程(Pipeline)。在该架构中,OTel-Exporter订阅上游Kafka的数据,经过清洗后批量写入ClickHouse。然而,随着数据量的增加,OTel-Exporter和ClickHouse之间的写入性能逐渐成为瓶颈,特别是在海量数据的处理过程中。


Rust的引入:在尝试了多种优化策略后,我们依然未能找到有效的解决方案,直到我们发现Rust的极低资源消耗特性。Rust在内存管理和性能方面的优势使得它成为解决这一瓶颈问题的关键。通过引入Rust,我们显著降低了内存占用,减少了CPU负担,进而提升了OTel-Exporter的吞吐量,同时避免了Java在内存管理上的开销。


Rust: 系统级编程的破冰之旅

Rust作为一种系统级编程语言,在现代编程中发挥着越来越重要的作用。其独特的内存安全机制和高效并发能力使其在处理复杂应用时克服了传统编程语言的性能瓶颈。通过全面控制内存使用,Rust能有效减少运行时错误,并确保代码的安全性。


在面临高流量数据处理和系统性能优化的挑战时,Rust提供了一种可靠的解决方案。接下来,我们将聚焦于Rust的并发模型、所有权和生命周期管理,探讨这些特性如何在实际应用中提升代码的安全性和性能。


所有权

在Rust中,所有权是一个核心概念,它决定了内存如何管理以及数据如何在程序中传递。Rust的所有权机制确保了内存安全,避免了内存泄漏和数据竞争,这是通过编译时的规则来实现的,而不是依赖运行时的垃圾回收机制。

所有权规则

Rust 中的所有权有三个主要规则:


为了更直观地理解所有权的运行机制,我们可以比较 Rust、C++ 和 Java 中对象赋值的不同:

Rust 的所有权概念内置于语言本身,在编译期间对所有权和借用规则进行检查。这样,程序员可以在运行之前解决错误,提高代码的可靠性。

共享所有权

尽管Rust规定大多数值会有唯一的拥有者,但在某些情况下,我们希望某个值在每个拥有者使用完后就自动释放。简单来说,就是可以在代码的不同地方拥有某个值的所有权,所有地方都使用完这个值后,会自动释放内存。对于这种情况,Rust提供了引用计数智能指针:Rc和Arc。


Rust的开发者确保了即使在多个地方共享所有权,也不会引入数据竞争的问题。引用计数智能指针是内部不可变的,即无法对共享的值进行修改。如果要对共享的值进行修改,可以使用Mutex等同步原语来避免数据竞争和未定义行为。


生命的周期与借用

生命周期(Lifetimes)和借用(Borrowing)是Rust保证内存安全和线程安全的两个重要机制。生命周期帮助Rust跟踪引用的有效性,而借用允许你在不拥有数据所有权的情况下,访问数据。


不可变借用:允许多个地方同时读取数据,但不允许修改数据。Rust保证,所有不可变引用在数据被销毁之前都有效,避免了悬垂引用。


可变借用:允许我们修改数据,但在同一时刻只能有一个可变借用。Rust会确保没有其他引用可以同时访问该数据,从而避免并发修改引发的问题。


不可变借用示例

#[derive(Debug)]struct Student {    name: String,    grade: u32,}
fn print_student_info(student: &Student) { println!("Student: {:?}, Grade: {}", student.name, student.grade);}
fn main() { let student = Student { name: "Alice".to_string(), grade: 85, };
// 不可变借用 print_student_info(&student); // student 仍然有效,因为没有可变借用,可以再次访问 println!("{:?}", student);}


在这个例子中,print_student_info 函数通过不可变借用 &Student 获取学生的引用。由于 Rust 的生命周期和借用机制,student 在整个程序中都有效,直到它超出作用域。这允许多个函数同时读取 student 的数据而不会导致数据被修改。


可变借用示例

#[derive(Debug)]struct Student {    name: String,    grade: u32,}
fn print_student_info(student: &Student) { println!("Student: {:?}, Grade: {}", student.name, student.grade);}
fn main() { let student = Student { name: "Alice".to_string(), grade: 85, };
// 不可变借用 print_student_info(&student); // student 仍然有效,因为没有可变借用,可以再次访问 println!("{:?}", student);}


在这个例子中,increase_student_grade 函数通过可变借用 &mut Student 接收一个学生对象的引用,并更新学生的成绩。Rust 保证在同一时间只有一个可变借用,确保了数据的一致性。这意味着在 increase_student_grade 函数执行期间,其他任何代码都不能访问 student 的可变引用,从而避免了数据竞争。


并发模型

Rust的并发模型是其系统级编程能力的核心,它通过所有权、借用和生命周期等语言特性,在编译时强制执行线程安全,从而避免了运行时的数据竞争和同步问题。这种模型允许开发者安全地利用多核处理器的能力,而无需牺牲代码的安全性。Rust的并发原语,如互斥锁(Mutex)、读写锁(RwLock)和原子操作,都是基于这种模型构建的,它们帮助开发者在避免数据竞争的同时,实现高效的并发控制。

线程

在Rust中,线程(thread)是并发执行的基本单位。Rust标准库提供了强大的线程支持,允许开发者轻松地创建和管理线程。Rust的线程模型基于操作系统的原生线程,这意味着每个Rust线程都直接映射到一个操作系统线程。

use std::thread;
fn main() { let handle = thread::spawn(move || { // 线程执行的代码 println!("Hello from a new thread!"); });
// 等待线程结束 handle.join().unwrap();}

消息传递

Rust中的消息传递不仅包括基本的通道(channels),还有更高级的并发原语,如crossbeam-channel库提供的无锁通道,这些通道在高并发场景下可以显著提高性能。通道分为单生产者单消费者(如mpsc)和多生产者单消费者(如mpsc)两种类型,它们都是通过所有权和借用规则来保证线程安全的。

use std::sync::mpsc;
fn main() { let (tx, rx) = mpsc::channel();
// 发送端 thread::spawn(move || { let val = 10; tx.send(val).unwrap(); });
// 接收端 let received = rx.recv().unwrap(); println!("Received: {}", received);}

异步编程

Rust的异步编程模型通过async/await语法糖和Future trait,允许开发者在单个线程内以非阻塞的方式执行多个I/O密集型任务。这种模型利用事件循环和非阻塞I/O,使得可以在单个线程内高效地处理大量并发操作,从而提高了程序的吞吐量和响应性。

async fn async_task() {    // 异步代码    println!("Async task");}
#[tokio::main]async fn main() { async_task().await;}

锁和同步原语

Rust提供了多种锁和同步原语,如互斥锁(Mutex)和读写锁(RwLock),它们都可以通过std::sync模块访问。这些锁内部使用原子操作来保证在多线程环境下的内存一致性。此外,Rust还提供了条件变量(Condvar)来实现线程间的协调,以及Once类型来确保全局初始化的安全执行。

use std::sync::{Arc, Mutex};use std::thread;
fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![];
for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); }
for handle in handles { handle.join().unwrap(); }
println!("Result: {}", *counter.lock().unwrap());}

无数据竞争保证

Rust的所有权系统保证了在任何给定时间,对同一数据的访问不会同时存在多个可变引用,这从根本上避免了数据竞争。此外,Rust的借用检查器在编译时期就会检查代码,确保所有引用的使用都符合所有权规则,从而在编译时就排除了数据竞争的可能性。


通过这些工具和规则,Rust的并发模型为开发者提供了一个强大而灵活的平台,以安全和高效的方式构建并发系统。这使得Rust成为构建高性能、高可靠性并发系统的不二之选。


应用部署

Rust 提供了强大的性能和内存安全保证,但在实际生产环境中,部署和监控同样至关重要。确保应用程序在部署后的高可用性和健康性,需要关注性能瓶颈、错误日志、系统指标以及应用的稳定性。以下内容将帮助您了解如何在生产环境中部署 Rust 应用,并确保其在运行时能被有效监控。

Rust 应用的部署

Rust 的编译结果是一个二进制文件,这意味着部署过程非常简单且高效。Rust 应用不依赖于虚拟机或解释器,这使得它能够直接运行在目标操作系统上。这个过程可以通过持续集成/持续部署(CI/CD)管道自动化,以确保部署的一致性和可靠性。

1.使用 Cargo 编译 Rust 应用

cargo 是 Rust 的包管理器和构建工具,它可以帮助您编译项目并生成二进制文件。在本地机器上构建 Rust 项目,可以使用以下命令:

cargo build --release

这会生成优化过的二进制文件,位于 target/release 目录中。


2.在服务器上运行应用

上传完成后,在服务器上执行二进制文件以启动应用:

./my_rust_app


监控Rust应用

一旦Rust 应用成功部署到生产环境中,接下来就需要考虑如何监控其运行状况,确保其健康,并及时发现问题。监控通常包括以下几个方面:


1.性能监控

Rust的高性能特点是它的一大优势,但在生产环境中,监控性能瓶颈仍然至关重要。为了监控Rust应用的性能,您可以使用Prometheus和Grafana等工具。


创建和注册指标:


在Rust中,您可以创建多种类型的指标,如Counter、Gauge、Histogram和Summary,来记录不同的性能数据。以下是一些基本的示例:


创建 Counter:

use prometheus::{register_counter_vec,  CounterVec};
static ref counter: CounterVec = register_counter_vec!( "my_counter", "My counter help.", &["type"]).unwrap();
counter .with_label_values(&[&"app"]) .inc();

为了实现Rust应用的性能监控,我们通常会集成Prometheus客户端库,该库允许应用暴露一个HTTP端点,用于Prometheus抓取预定义的性能指标。这些指标包括但不限于请求处理时间、吞吐量、错误率等关键性能指标。通过这种方式,我们可以实时监控应用的性能,并根据需要调整资源配置或优化代码。


如图所示,可以实时观察Rust服务的运行情况,并且根据自己的埋点分析系统的瓶颈。可以看到,Rust应用运行非常平稳。相比于有GC的Java应用,Rust明显毛刺很少,非常平滑,而且内存占用相比Java减少了70%。


2.日志管理

及时发现和处理错误对于保证Rust应用的稳定性至关重要。您可以使用flexi_logger作为日志模块,它提供了灵活的日志配置和处理能力。以下是如何在Rust中集成flexi_logger的示例代码:

[dependencies]flexi_logger = "0.29.6"log = "0.4.22"
use log::{info, warn};
fn main() { flexi_logger::Logger::try_with_str("info") // 设置日志级别 .unwrap() .log_to_file( flexi_logger::FileSpec::default() .directory(log_path) .basename("server") // 设置日志文件基本名称 .suppress_timestamp() .suffix("log"), // 设置日志文件后缀 ) .rotate( Criterion::Size(64_000_000), // 单个日志文件大小64MB Naming::Timestamps, Cleanup::KeepLogFiles(7), // 保留的滚动日志文件数量 ) .format(flexi_logger::colored_with_thread) .start() .unwrap_or_else(|e| panic!("Logger initialization failed with {}", e)); info!("App started"); warn!("This is a warning message");}


通过 flexi_logger,您可以将日志输出到文件和控制台,并设置日志轮转和压缩,以便于日志的管理。


3.系统健康检查

监控系统健康是确保应用稳定运行的必要步骤。您可以为Rust应用添加健康检查端点,供外部系统定期检查应用状态。这些端点可以返回应用是否正常工作,以及其他相关的诊断信息。


使用Rust的warp或actix-web等框架,您可以轻松实现一个健康检查API:

use warp::Filter;
#[tokio::main]async fn main() { let health = warp::path("health") .map(|| warp::reply::with_status("OK", warp::http::StatusCode::OK)); warp::serve(health).run(([127, 0, 0, 1], 3030)).await;}

这样,您就可以通过访问 /health 路径来检查应用是否处于正常状态。


小结

Rust的部署和监控非常直观且高效。使用cargo进行编译和部署,结合flexi_logger进行日志管理,以及使用健康检查端点,可以确保您的Rust应用在高流量和复杂系统集成中表现出色。


总结

通过迁移到Rust,我们的计算层能够在处理高并发请求时显著提高系统的吞吐量和响应能力,同时减少服务器资源的浪费。这不仅能降低运营成本,还能为我们的用户提供更流畅、更快速的体验。


但是,如果要持续地拥抱Rust生态,目前仍然面临如下挑战:


1.生态不完善:尽管Rust已经有一些非常优秀的库和工具,但某些特定领域仍然缺乏成熟且广泛使用的库。这意味着开发者可能需要花费更多的时间来构建自己的解决方案或者整合不同语言的库。


2.学习曲线陡峭:Rust语言引入了许多独特的概念和特性,对于初学者和来自其他语言的开发者来说,这些特性可能需要一段时间来彻底掌握。

3.开发进度:相比于自动内存管理类型语言的开发任务,Rust严格的编译检查会让开发进度一度阻塞。


尽管开发Rust生产级应用有那么多阻碍,我们目前已经发布的Rust应用已经证明了,相比于付出,迁移Rust带来的收益更大。希望大家都可以探索Rust的可行性,为节能减排和世界和平出一份力,也欢迎各位对Rust有兴趣的同学一起交流。


往期回顾


1. 软件开发领域变革前夕-AI智能编码的发展|得物技术

2. RAG应用在得物开放平台的智能答疑的探索

3. 如何以MLOps保障时效表达稳定性|得物技术

4. 盘点这些年搭建器在用户体验优化的实践|得物技术

5. 站外商详的重构与优化|得物技术

文 / 南风


关注得物技术,每周一、三更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。


扫码添加小助手微信

如有任何疑问,或想要了解更多技术资讯,请添加小助手微信:


阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Rust 内存安全 并发模型 应用部署
相关文章