当项目从 1 万行膨胀到 100 万行,Rust 的类型系统和所有权模型从约束变成了武器。这篇文章讲的是:如何在 Rust 的规则下,设计真正可演进的复杂系统。
在 Go/Java/Python 里,架构师画完架构图,开发者用引用和回调把东西连起来——一切都很灵活。Rust 不一样:所有权关系就是你的架构图。谁能访问什么数据、谁能修改什么状态,不是靠文档约束,而是靠编译器强制。
一个 Rust 程序的所有权图,本质上是一棵有向无环树。每个值有且仅有一个 owner,借用只是临时访问权。这意味着:
🔑 核心原则:在设计 Rust 系统时,先画所有权树,再写代码。如果你的所有权图是网状的(循环引用、多重拥有),说明架构有问题,需要引入间接层(Arc、索引、消息传递)来打破循环。
Rust 的 aliasing + mutation 规则禁止同时有可变引用和不可变引用。但在复杂系统中,共享可变状态是刚需。Rust 给了你四条路:
| 方案 | 适用场景 | 复杂度 | 运行时开销 | 典型模式 |
|---|---|---|---|---|
RefCell<T> | 单线程,需要运行时借用检查 | 低 | 极小(borrow counter) | 树形结构内部可变 |
Mutex<T> / RwLock<T> | 多线程,需要互斥访问 | 中 | 锁竞争 | 共享配置、全局状态 |
Arc<Mutex<T>> | 多线程 + 多 owner | 高 | 原子计数 + 锁 | 跨组件共享资源 |
消息传递 mpsc / crossbeam | 多线程,无需共享内存 | 中 | Channel 开销 | Actor 模型、管道 |
struct App {
config: Config, // App owns
db: Database, // App owns
workers: Vec<Worker>, // App owns
}
impl App {
fn process(&mut self, req: Request) {
// 借用链清晰:&mut self → &mut db
self.db.save(req.data());
self.workers[0].notify();
}
}
struct App {
db: Arc<Mutex<Database>>, // 谁都能 lock
cache: Arc<RwLock<Cache>>, // 随时可能死锁
workers: Arc<Mutex<Vec<Worker>>>, // 为什么要 Arc?
state: Arc<Mutex<Arc<Mutex<State>>>>, // 😱
}
// 没人知道谁在什么时候持有锁
// 没人能证明不会死锁
// 编译器帮不了你
⚠️ Arc<Mutex<T>> 闻到味道:如果你发现代码里到处都是 Arc<Mutex<T>>,这通常意味着架构缺少清晰的 ownership 边界。每多一层 Arc<Mutex>,编译器能帮你检查的东西就少一层。正确做法:先重构所有权结构,让大多数数据只有一个 owner,只在真正的跨 owner 共享点用 Arc。
Rust 最强大的架构工具不是 trait,不是宏,而是类型系统本身。在 Rust 里,一个好的设计是:非法状态不可表达(make illegal states unrepresentable)。
/// 连接状态机:编译器保证你不会在 Disconnected 状态下发送数据
enum ConnectionState {
Disconnected,
Connecting(TcpStream, Instant), // stream + 超时时间
Connected {
stream: TcpStream,
last_heartbeat: Instant,
seq: u64,
},
Reconnecting {
attempts: u8,
backoff: Duration,
},
}
struct Connection {
state: ConnectionState,
}
impl Connection {
fn send(&mut self, data: &[u8]) -> Result<(), ConnError> {
match &mut self.state {
ConnectionState::Connected { stream, seq, .. } => {
*seq += 1;
stream.write_all(data)?;
Ok(())
}
// 编译器强制你处理每个状态
ConnectionState::Disconnected => Err(ConnError::NotConnected),
ConnectionState::Connecting(_, _) => Err(ConnError::InProgress),
ConnectionState::Reconnecting { attempts, .. } => {
Err(ConnError::Reconnecting { attempts: *attempts })
}
}
}
}
对比 Java 的做法:一个 Connection 类,里面有 boolean connected、boolean connecting,运行时靠 if-else 判断状态——你永远无法在编译期阻止别人在 disconnected 时调用 send。
// 不要用裸类型——编译器无法区分 UserId 和 OrderId
// struct User { id: u64 } // ❌ 容易和 Order.id 混淆
// 用 newtype 让编译器帮你守门
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct UserId(u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct OrderId(u64);
fn get_user(id: UserId) -> User { ... }
fn get_order(id: OrderId) -> Order { ... }
// 现在这样写会编译错误:
// get_user(OrderId(42)) // ❌ type mismatch!
// 只有 get_user(UserId(42)) 才能编译通过
类型状态模式(Type State Pattern)是 Rust 独有的设计模式:用泛型参数编码对象的状态,让状态转换在编译期完成检查。
use std::marker::PhantomData;
// 状态编码在类型参数里
struct Unconfigured;
struct Configured;
struct Running;
struct Server<State = Unconfigured> {
config: Option<ServerConfig>,
listener: Option<TcpListener>,
_state: PhantomData<State>,
}
impl Server<Unconfigured> {
fn new() -> Self { ... }
// configure 消费 Unconfigured,返回 Configured
fn configure(self, cfg: ServerConfig) -> Server<Configured> {
Server {
config: Some(cfg),
listener: None,
_state: PhantomData,
}
}
}
impl Server<Configured> {
// start 只在 Configured 状态可用
fn start(self) -> Result<Server<Running>, Error> {
let config = self.config.unwrap(); // 编译器保证 Some
let listener = TcpListener::bind(config.addr)?;
Ok(Server {
config: Some(config),
listener: Some(listener),
_state: PhantomData,
})
}
}
impl Server<Running> {
fn accept(&self) -> Result<TcpStream, Error> { ... }
}
// 使用:
let server = Server::new()
.configure(config) // 必须先 configure
.start()?; // 才能 start
// server.accept() // 才能 accept
// 你无法在 Unconfigured 状态调用 start()——编译错误!
// 你无法跳过 configure 直接 start——编译错误!
✅ 零成本:PhantomData 不占运行时内存,状态转换在编译期完成,运行时零开销。这比任何运行时状态检查都快,也比任何文档都可靠。
Rust 的 mod 不仅仅是代码组织,它定义了可见性边界。pub(crate) 和 pub(super) 让你能精确控制谁能看到什么——这在大型项目中是架构级别的控制。
| 原则 | 描述 | 反例 |
|---|---|---|
| 依赖方向单向 | Crate A 依赖 B,B 不能依赖 A | 循环依赖 → 合并或抽接口 crate |
| 核心 crate 无外部依赖 | 数据类型和 trait 定义不依赖 tokio/serde | domain crate 依赖 async runtime |
| 编译时间隔离 | 频繁改动的代码和稳定代码分开 | 所有代码在一个 crate 里 |
| 可选依赖向下 | 只有叶 crate 可以有 optional deps | 核心 crate 有 20 个 feature flags |
🔑 my-app-core 的价值:它只定义 trait 和数据类型,不依赖 tokio/serde/任何 async runtime。这意味着:
① 单元测试可以纯同步跑,快 10 倍
② 替换 HTTP 框架不影响业务逻辑
③ 可以在 WASM/embedded 环境复用核心逻辑
Actor 模型天然契合 Rust 的所有权理念:每个 Actor 独占自己的状态,外部通过消息通信,没有共享可变状态。
use tokio::sync::{mpsc, oneshot};
// Actor 的消息协议——用 enum 强类型化
enum DbActorMsg {
Get { key: String, reply: oneshot::Sender<Option<String>> },
Set { key: String, value: String },
Delete { key: String },
Flush { reply: oneshot::Sender<()> },
}
struct DbActor {
rx: mpsc::Receiver<DbActorMsg>,
data: HashMap<String, String>, // Actor 独占,无需锁
pending_writes: Vec<String>,
}
impl DbActor {
async fn run(&mut self) {
while let Some(msg) = self.rx.recv().await {
match msg {
DbActorMsg::Get { key, reply } => {
let _ = reply.send(self.data.get(&key).cloned());
}
DbActorMsg::Set { key, value } => {
self.data.insert(key.clone(), value);
self.pending_writes.push(key);
}
DbActorMsg::Delete { key } => {
self.data.remove(&key);
}
DbActorMsg::Flush { reply } => {
self.flush_to_disk().await;
self.pending_writes.clear();
let _ = reply.send(());
}
}
}
}
}
// Actor 的 handle——对外暴露的类型安全接口
struct DbHandle {
tx: mpsc::Sender<DbActorMsg>,
}
impl DbHandle {
pub async fn get(&self, key: String) -> Option<String> {
let (tx, rx) = oneshot::channel();
self.tx.send(DbActorMsg::Get { key, reply: tx }).await.ok()?;
rx.await.ok()?
}
pub async fn set(&self, key: String, value: String) {
self.tx.send(DbActorMsg::Set { key, value }).await.ok();
}
}
✅ 为什么 Actor 在 Rust 里特别香:
① 状态不需要锁——Actor 内部单线程执行
② 消息协议是 enum——编译器保证你处理了所有消息类型
③ oneshot::Sender 做请求-响应——类型安全,不会搞混 reply
④ Actor 崩溃不影响其他 Actor——天然容错边界
Entity-Component-System 是游戏引擎的标准架构,但它的思想——组合优于继承,数据与逻辑分离——同样适用于任何需要高性能、高扩展性的 Rust 项目。
// Entity = ID
// Component = 纯数据
struct Position(f32, f32);
struct Velocity(f32, f32);
struct Health(i32);
// System = 纯逻辑
fn movement_sys(
pos: &mut Position,
vel: &Velocity,
) {
pos.0 += vel.0;
pos.1 += vel.1;
}
// 数据连续存储,CPU cache 友好
// 逻辑无状态,天然并行
// trait GameObject {
// fn update(&mut self);
// fn render(&self);
// }
//
// struct Player { ... }
// impl GameObject for Player { ... }
//
// struct Enemy { ... }
// impl GameObject for Enemy { ... }
//
// 问题1:异构集合 Vec<Box<dyn GameObject>>
// → 虚函数调用 + cache miss
// 问题2:新功能需改所有 struct
// 问题3:无法组合 (会飞+会游泳?)
use std::any::{Any, TypeId};
use std::collections::HashMap;
type EntityId = u64;
struct World {
entities: Vec<EntityId>,
components: HashMap<TypeId, Box<dyn Any>>, // TypeId → Vec<Component>
next_id: EntityId,
}
impl World {
fn spawn(&mut self) -> EntityId {
let id = self.next_id;
self.next_id += 1;
self.entities.push(id);
id
}
fn add_component<T: 'static>(&mut self, entity: EntityId, comp: T) {
let type_id = TypeId::of::<T>();
let storage = self.components
.entry(type_id)
.or_insert_with(|| Box::new(Vec::<(EntityId, T)>::new()));
let vec = storage.downcast_mut::<Vec<(EntityId, T)>>().unwrap();
vec.push((entity, comp));
}
fn query<T: 'static>(&self) -> Vec<(EntityId, &T)> {
let type_id = TypeId::of::<T>();
self.components
.get(&type_id)
.and_then(|s| s.downcast_ref::<Vec<(EntityId, T)>>())
.map(|v| v.iter().map(|(id, c)| (*id, c)).collect())
.unwrap_or_default()
}
fn query2<A: 'static, B: 'static>(&self) -> Vec<(EntityId, &A, &B)> {
// Join two component storages on EntityId
let a_map: HashMap<_, _> = self.query::<A>().into_iter().collect();
let b_map: HashMap<_, _> = self.query::<B>().into_iter().collect();
a_map.keys()
.filter_map(|&id| {
Some((id, a_map.get(&id)?, b_map.get(&id)?))
})
.collect()
}
}
// 使用
let mut world = World::new();
let player = world.spawn();
world.add_component(player, Position(0.0, 0.0));
world.add_component(player, Velocity(1.0, 0.0));
world.add_component(player, Health(100));
// Movement system: 只关心有 Position + Velocity 的 entity
for (id, pos, vel) in world.query2::<Position, Velocity>() {
println!("Entity {} at ({}, {}) moving ({}, {})",
id, pos.0, pos.1, vel.0, vel.1);
}
🔑 生产级 ECS:实际项目用 bevy_ecs(可独立使用,不依赖 bevy 引擎)或 hecs。它们提供了 archetype-based 存储、并行调度、change detection 等生产级功能。
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("Database error: {0}")]
Database(#[from] DbError),
#[error("User {user_id} not found")]
UserNotFound { user_id: UserId },
#[error("Rate limited: {requests} requests in {window:?}")]
RateLimited { requests: u32, window: Duration },
#[error("Invalid configuration: {message}")]
Config { message: String },
#[error(transparent)]
Internal(#[from] anyhow::Error), // 兜底
}
// 每个错误变体携带足够的上下文用于调试
// #[from] 自动实现 From,? 运算符自动转换
| 层 | 错误类型 | 处理策略 |
|---|---|---|
| Crate 内部 | thiserror 定义具体 enum | 用 ? 传播,不丢弃上下文 |
| Crate 边界 | 转换为本 crate 的错误类型 | From + ?,隐藏内部细节 |
| 应用顶层 | anyhow::Error | 收集所有错误,统一日志/上报 |
| API 边界 | HTTP 状态码 + JSON | 错误 → 状态码映射,不泄露内部信息 |
| 机制 | 编译期/运行期 | 类型安全 | 性能 | 典型用例 |
|---|---|---|---|---|
Trait 对象 dyn T | 运行期 | 强 | 虚函数调用 | 策略模式、插件接口 |
泛型 impl T | 编译期 | 强 | 单态化,零开销 | 算法、容器 |
| 过程宏 | 编译期 | 中 | 编译时开销 | 代码生成、DSL |
| WASM 插件 | 运行期 | 中 | 沙箱开销 | 不可信插件 |
| dylib 动态加载 | 运行期 | 弱 | C FFI 开销 | 热加载、第三方扩展 |
// 核心 crate 定义插件接口
pub trait Plugin: Send + Sync {
fn name(&self) -> &str;
fn on_load(&self, ctx: &PluginContext) -> Result<(), PluginError>;
fn on_request(&self, req: &mut Request) -> Result<(), PluginError>;
fn on_response(&self, resp: &mut Response) -> Result<(), PluginError>;
fn on_unload(&self) {}
}
pub struct PluginContext {
pub config: &Config,
pub metrics: &MetricsSink,
}
// 插件管理器
struct PluginManager {
plugins: Vec<Box<dyn Plugin>>, // 运行期多态
}
impl PluginManager {
fn register(&mut self, plugin: Box<dyn Plugin>) {
self.plugins.push(plugin);
}
fn on_request(&self, req: &mut Request) -> Result<(), PluginError> {
for plugin in &self.plugins {
plugin.on_request(req)?; // 责任链模式
}
Ok(())
}
}
// 具体插件——可以独立 crate
struct AuthPlugin { secret: String }
impl Plugin for AuthPlugin {
fn name(&self) -> &str { "auth" }
fn on_request(&self, req: &mut Request) -> Result<(), PluginError> {
let token = req.header("Authorization")
.ok_or(PluginError::Unauthorized)?;
self.verify(token)?;
Ok(())
}
// ...
}
| 策略 | 效果 | 实现 |
|---|---|---|
| Crate 拆分 | ★★★ | 频繁改动的代码独立成 crate |
| 减少泛型单态化 | ★★★ | 热点路径用 dyn,冷路径用泛型 |
| 依赖裁剪 | ★★ | cargo tree -d 去重,feature 精简 |
cfg(test) 隔离 | ★★ | 测试依赖不进入生产编译 |
| sccache / mold | ★ | 缓存和更快的 linker |
把前面所有概念串起来,设计一个分布式任务调度器(类似简化版 Temporal / Nomad)。
// === 领域类型 ===
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct JobId(Uuid);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct WorkerId(Uuid);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct TaskId(Uuid);
// === 状态机 ===
enum JobState {
Pending,
Scheduled { worker: WorkerId, task: TaskId },
Running { worker: WorkerId, started_at: Instant },
Completed { result: JobResult },
Failed { error: String, retries: u8 },
Cancelled,
}
enum TaskState {
Queued,
Dispatched { worker: WorkerId },
Running { heartbeat: Instant },
Finished { exit_code: i32 },
Crashed { signal: i32 },
Lost, // worker 失联
}
// === 事件溯源 ===
enum JobEvent {
Created { spec: JobSpec },
Scheduled { worker: WorkerId, task: TaskId },
Started { worker: WorkerId, at: Timestamp },
Heartbeat { worker: WorkerId, progress: f32 },
Completed { result: JobResult },
Failed { error: String },
RetryScheduled { attempt: u8, after: Duration },
Cancelled { reason: String },
}
// === 插件接口 ===
trait SchedulerPlugin: Send + Sync {
fn on_job_created(&self, job: &Job) -> Option<ScheduleHint>;
fn on_task_finished(&self, result: &TaskResult) -> Vec<Action>;
fn on_worker_lost(&self, worker: WorkerId, tasks: &[TaskId]) -> Vec<Action>;
}
enum Action {
Reschedule { task: TaskId, priority: Priority },
Notify { channel: String, message: String },
ScaleUp { count: u32 },
}
enum ScheduleHint {
Prefer { worker: WorkerId },
Avoid { worker: WorkerId },
Require { resource: String, amount: u64 },
}
enum WorkerMsg {
Dispatch { task: Task, reply: oneshot::Sender<Result<(), DispatchError>> },
Cancel { task_id: TaskId },
HeartbeatCheck,
Shutdown { reply: oneshot::Sender<()> },
}
struct WorkerActor {
id: WorkerId,
capacity: u32,
running: HashMap<TaskId, JoinHandle<()>>,
rx: mpsc::Receiver<WorkerMsg>,
heartbeat_tx: watch::Sender<WorkerStatus>,
}
impl WorkerActor {
async fn run(&mut self) {
let mut heartbeat_interval = tokio::time::interval(Duration::from_secs(5));
loop {
tokio::select! {
Some(msg) = self.rx.recv() => {
match msg {
WorkerMsg::Dispatch { task, reply } => {
let handle = self.spawn_task(task).await;
let _ = reply.send(handle);
}
WorkerMsg::Cancel { task_id } => {
if let Some(h) = self.running.remove(&task_id) {
h.abort();
}
}
WorkerMsg::Shutdown { reply } => {
for (_, h) in self.running.drain() { h.abort(); }
let _ = reply.send(());
return;
}
WorkerMsg::HeartbeatCheck => {
self.cleanup_finished();
}
}
}
_ = heartbeat_interval.tick() => {
let status = WorkerStatus {
id: self.id,
running: self.running.len() as u32,
capacity: self.capacity,
};
let _ = self.heartbeat_tx.send(status);
}
}
}
}
}
所有字段都包 Arc<Mutex> → 编译器无法验证正确性 → 运行时死锁/竞态。
解法:重新设计所有权,让数据有单一 owner,用 channel 替代共享。
一个 struct 持有所有状态,所有方法都 &mut self → 无法并行。
解法:拆成多个独立 struct,各自持有自己的数据。
到处传 String,用字符串匹配做逻辑 → 运行时才知道出错。
解法:newtype + enum,让编译器替你检查。
大量闭包回调 + 生命周期参数爆炸 → 代码不可读。
解法:用 channel/future 替代回调,或用 Actor 模型。
5 层 trait 嵌套,泛型参数满天飞 → 编译慢 + 错误信息不可读。
解法:YAGNI。只在有第二种实现时才抽 trait。
到处 .clone() 绕过借用检查 → 性能崩盘。
解法:先理解为什么借用检查失败,重构数据流,而不是无脑 clone。
🔴 终极测试:如果你的 Rust 项目要加 unsafe 才能完成一个常规业务功能,大概率是架构有问题,不是语言有问题。回去重新设计所有权和类型。Rust 编译器的拒绝是你的朋友——它在告诉你设计有缺陷。
🦀 Rust 架构的终极原则:
让编译器做架构审查,而不是代码审查。
让类型表达业务规则,而不是注释。
让所有权约束数据流,而不是文档。
让非法状态不可编译,而不是运行时报错。
当你的 Rust 代码能编译通过时,它不仅是类型正确的——它的架构也是正确的。