从 Kotlin 缓存服务迁移到 Rust:性能之外,编译器如何加快交付

2026-07-16 29 预计阅读时间: 1 分钟
来源: infoq.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:8 分钟

把高性能缓存服务从 Kotlin 迁移到 Rust,很容易被理解成一次单纯的性能优化。但 Ruth Linehan 分享的经验打破了另一个更顽固的预设:Rust 未必会拖慢交付,借用检查器和编译期安全反而能缩短反馈循环,让并发错误更早暴露,并让性能优化建立在可重复测量之上。

Rust 的成本发生在提交代码之前

Kotlin/JVM 与 Rust 的运行模型不同,迁移不能只做语法替换。缓存服务通常包含共享状态、并发读写、过期策略和大量对象访问,这些部分正好会触碰 Rust 的所有权、生命周期以及线程安全约束。

借用检查器确实增加了编码阶段的约束,但它也改变了问题出现的时间:许多悬垂引用、跨线程共享和可变别名问题会在编译时失败,而不是进入压力测试或生产环境后才表现为偶发故障。

例如,下面这种跨线程共享缓存使用 Arc<RwLock<_>> 明确表达了三件事:

  • Arc 负责多个线程共同持有缓存。
  • RwLock 允许多个读操作并行,但写操作需要独占访问。
  • 锁保护的数据不能在锁的生命周期之外被不安全地引用。

这并不意味着代码自动具备高性能。RwLock 仍可能发生锁竞争,值的 clone 也会产生开销。Rust 提供的是可验证的并发边界,而具体结构是否高效,仍然需要基准测试和剖析工具回答。

可以这样实践:构建一个可测量的并发缓存

下面是一个最小实验项目。它不是来源中缓存服务的实现,而是根据演讲涉及的借用检查、Criterion 和 flamegraph 工具链设计的可运行示例。

创建目录:

mkdir -p rust-cache-lab/src rust-cache-lab/benches
cd rust-cache-lab

写入 Cargo.toml

[package]
name = "rust-cache-lab"
version = "0.1.0"
edition = "2021"

[dev-dependencies]
criterion = "0.5"

[[bench]]
name = "cache"
harness = false

写入 src/lib.rs

use std::collections::HashMap;
use std::sync::RwLock;

#[derive(Default)]
pub struct Cache {
    entries: RwLock<HashMap<String, String>>,
}

impl Cache {
    pub fn insert(&self, key: String, value: String) {
        self.entries
            .write()
            .expect("cache write lock poisoned")
            .insert(key, value);
    }

    pub fn get(&self, key: &str) -> Option<String> {
        self.entries
            .read()
            .expect("cache read lock poisoned")
            .get(key)
            .cloned()
    }
}

#[cfg(test)]
mod tests {
    use super::Cache;

    #[test]
    fn returns_inserted_value() {
        let cache = Cache::default();
        cache.insert("region".into(), "eu-west".into());
        assert_eq!(cache.get("region").as_deref(), Some("eu-west"));
    }
}

写入 src/main.rs,生成一个可供 flamegraph 分析的并发负载:

use rust_cache_lab::Cache;
use std::sync::Arc;
use std::thread;

fn main() {
    let cache = Arc::new(Cache::default());

    for i in 0..1_000 {
        cache.insert(format!("key-{i}"), format!("value-{i}"));
    }

    let mut workers = Vec::new();
    for worker_id in 0..8 {
        let cache = Arc::clone(&cache);
        workers.push(thread::spawn(move || {
            for i in 0..500_000 {
                let key = format!("key-{}", (i + worker_id) % 1_000);
                std::hint::black_box(cache.get(&key));
            }
        }));
    }

    for worker in workers {
        worker.join().expect("worker thread failed");
    }
}

再写入 benches/cache.rs

use criterion::{black_box, criterion_group, criterion_main, Criterion};
use rust_cache_lab::Cache;

fn benchmark_cache_hit(c: &mut Criterion) {
    let cache = Cache::default();
    cache.insert("session:42".into(), "active".into());

    c.bench_function("cache hit", |b| {
        b.iter(|| black_box(cache.get(black_box("session:42"))))
    });
}

criterion_group!(benches, benchmark_cache_hit);
criterion_main!(benches);

运行测试和基准:

cargo test
cargo bench

Criterion 会执行多轮采样并报告时间分布。不要只比较一次运行的数字;应该在相同机器、相同编译配置和接近真实的数据规模下比较修改前后的结果。

从基准数字追到具体代码路径

Criterion 能告诉你性能是否变化,flamegraph 则帮助定位 CPU 时间消耗在哪里。Linux 环境通常需要 perf,不同发行版的安装命令可能不同。工具准备完成后可以运行:

cargo install flamegraph
cargo build --release
cargo flamegraph --release --bin rust-cache-lab

命令会生成 flamegraph.svg。分析时应优先查看宽度明显的调用栈,并提出具体问题:

  • 时间是否大量消耗在 RwLock 的读锁或写锁上?
  • String::clone 和内存分配是否占据主要路径?
  • 热点来自缓存查询本身,还是示例中的 format! 等负载生成代码?
  • 增加线程后,吞吐量是否增长,还是锁竞争让性能提前见顶?

最后一点尤其重要。并发代码在单线程微基准中可能表现很好,却在多线程压力下退化。可以保留 Criterion 测量稳定的局部操作,再用接近生产读写比例的负载测试整体吞吐量和尾延迟。

迁移时不要只比较语言

从 Kotlin 迁移到 Rust时,应同时比较运行成本和工程反馈周期。一个可执行的评估清单包括:

  • 选择 CPU、内存或尾延迟确实构成瓶颈的服务,而不是全面重写。
  • 记录迁移前的吞吐量、P95/P99 延迟、内存和 GC 行为。
  • 把编译错误、单元测试、Criterion 基准和 flamegraph 纳入日常反馈循环。
  • 审查锁粒度、复制次数和分配热点,不把“通过借用检查器”等同于“性能已经最优”。
  • 评估团队学习成本、构建时间、可观测性和线上排障能力。

这场分享最值得关注的结论并非“Rust 一定比 Kotlin 快”,而是性能、安全性和交付速度并不必然互相排斥。Rust 把一部分调试成本前移到编译阶段,再通过可重复基准和剖析工具,让工程师更快判断一项优化究竟有效,还是只改变了代码外观。


相关推荐