常见坑与面试重点
最后把新手最常栽的坑和高频面试题集中摆出来。这些都是线上项目真踩过的,提前知道,少熬几个夜。
在 async 函数里干 CPU 密集活(比如大数组排序、JSON 大字符串序列化),会把当前 worker 线程堵死。解法:CPU 密集活丢给 tokio::task::spawn_blocking,或者用 rayon 做并行迭代器。
spawn 出去的 future 必须 'static,意思是它不能借用外部数据。新手常报 cannot refer to outer variable。解法:能 clone 的 clone(Arc 很便宜),不能 clone 的把数据 move 进 async 块。
CI 上 query! 宏报 "error: error occurred while looking up schema"。解法:本地跑 cargo sqlx prepare,把 .sqlx/ 目录提交 git,CI 设 SQLX_OFFLINE=true。
Rust 编译慢是常态,一个中型项目 clean build 五分钟。解法:开发时用增量编译(默认就开),装 sccache 缓存编译产物,CI 用 Swatinem/rust-cache。
Cargo 会解析出多版本依赖,二进制变大、行为不一致。解法:定期 cargo update,用 cargo tree 看依赖树,必要时升级。
高频面试题(折叠答案)
1. Rust 为什么适合写高并发后端?
看答案
无 GC 停顿、编译期内存安全、异步零成本抽象、单文件部署。一台机器挂几十万连接是常态。
2. Tokio 异步运行时怎么工作?
看答案
多线程 work-stealing 调度器。async fn 编译成状态机 Future,.await 时若未就绪则让出执行权,调度器跑别的任务,IO 就绪后唤醒。
3. join! 和 spawn 有什么区别?
看答案
join! 在当前任务里并发跑多个 future,等全部完成;spawn 创建独立任务,返回 JoinHandle,可在别处 await。spawn 出去的任务必须 'static。
4. std::sync::Mutex 和 tokio::sync::Mutex 区别?
看答案
std 的 lock 阻塞线程,async 里用会堵死调度器;tokio 的 lock().await 在等待时让出执行权。短临界区用 std 也行,跨 await 用 tokio。
5. Axum 提取器为什么要有顺序?
看答案
body 只能读一次。Path/Query/State 不碰 body,Json/Form/Bytes 消费 body。所以 body 提取器必须最后,且只能一个。
6. SQLx 的编译期检查怎么做到的?
看答案
query! 宏在编译期连接真实数据库(或读 .sqlx 缓存),把 SQL 发给数据库引擎解析,把结果类型反推成 Rust 类型。
7. SQLx 和 SeaORM 怎么选?
看答案
喜欢手写 SQL、要性能和控制力选 SQLx;团队 ORM 思维、业务模型复杂选 SeaORM。二者也能混用。
8. thiserror 和 anyhow 的分工?
看答案
thiserror 用于库 crate 定义公开错误枚举(结构化、可被 match);anyhow 用于应用层,一个 Error 装所有错误,带 .context()。
9. 事务怎么用?
看答案
pool.begin() 拿 Transaction,把 &mut *tx 传给 query 方法,全部成功后 tx.commit(),任何一步 ? 报错自动 drop 回滚。
10. 怎么处理阻塞操作?
看答案
spawn_blocking 把同步 IO 丢到专门的 blocking 线程池;CPU 密集用 rayon 并行迭代器。不要在 async 里写 std::thread::sleep。
11. tonic 的代码生成在哪一步?
看答案
build.rs 在 cargo build 时调用 tonic_build,把 .proto 编译成 Rust trait + message struct。
12. tracing 和 log 有什么区别?
看答案
log 是单行日志;tracing 引入 span 概念,能追踪一个请求跨多个函数、多个异步任务的完整路径,天然支持分布式追踪。
13. 怎么做优雅停机?
看答案
axum::serve.with_graceful_shutdown(signal::ctrl_c()),收到信号后停止接新请求,等在飞请求处理完。
14. 连接池大小怎么定?
看答案
SQLx/PgPool 默认 max_connections=10。经验值:(CPU 核数 * 2) + 磁盘数。不是越大越好,数据库自己有上限。
15. 为什么要 LTO 和 codegen-units=1?
看答案
Release 下跨 crate 内联、单编译单元优化,二进制更小更快,代价是编译变慢。生产配置就该这么写。
16. Rust 怎么做限流?
看答案
governor 令牌桶算法做客户端限流;tower::limit::ConcurrencyLimit 做服务端并发限制;二者可组合。
17. 怎么生成 OpenAPI 文档?
看答案
utoipa 给 handler 和 DTO 加 derive,自动生成 OpenAPI 3.0 JSON,再挂 swagger-ui 路由。
18. Rust 后端怎么部署?
看答案
多阶段 Docker 构建,运行镜像 debian-slim 或 scratch,前面 Nginx 反代 HTTPS,systemd 管进程,Prometheus 抓 metrics。
19. 为什么 Rust 二进制可以跑在 scratch 上?
看答案
用 musl target 静态链接 libc,不依赖动态库。整个镜像就一个可执行文件,启动快、攻击面小。
20. Rust 后端性能瓶颈怎么查?
看答案
criterion 做微基准,perf/flamegraph/samply 抓火焰图,tracing span 看哪段慢,sqlx 看慢查询。先量再优化,别瞎猜。
1.(概念题)为什么 Rust 后端能比 Java 扛更高并发?
查看答案
没有 GC 停顿,异步协程 KB 级内存,一个 OS 线程调度成千上万个任务。JVM 线程栈 1MB,Rust 协程几 KB。
2.(排错题)在 async 函数里写了 std::thread::sleep,会发生什么?
查看答案
整个 Tokio worker 线程被堵死,其他任务全部卡住。换成 tokio::time::sleep(...).await。
3.(选择题)Axum handler 签名 async fn h(State(s): State<App>, Json(b): Json<Req>) -> ... 顺序对吗?
查看答案
对。State 不碰 body,Json 消费 body 放最后。反过来会编译错误。
4.(选型题)团队喜欢写 SQL,项目要编译期检查,选 SQLx 还是 SeaORM?
查看答案
SQLx。query! 宏编译期校验 SQL,手写 SQL 性能可控。SeaORM 适合不想写 SQL 的场景。
5.(部署题)怎么把 Rust 后端镜像压到 5MB?
查看答案
交叉编译到 x86_64-unknown-linux-musl,静态链接,运行镜像用 scratch。整个镜像就一个二进制。
① 你已经能从 Tokio 异步运行时 一路讲到 Docker 上线:Axum 路由、SQLx 数据库、tracing 日志、tonic gRPC、测试、Nginx 反代。
② 下一步:把第 2 章的 Todo API 跑起来,加上 SQLx 持久化和 JWT 鉴权,就是一个能交付的小项目。
③ Rust 后端的核心心法:类型系统替你把住关,编译器替你把住关,剩下的就是把业务写清楚。