楼层: 首页/ 软件技术/ Rust 后端技术栈/ 常见坑与面试重点
08

常见坑与面试重点

Pitfalls & Interview

最后把新手最常栽的坑和高频面试题集中摆出来。这些都是线上项目真踩过的,提前知道,少熬几个夜。

坑一:阻塞异步运行时

在 async 函数里干 CPU 密集活(比如大数组排序、JSON 大字符串序列化),会把当前 worker 线程堵死。解法:CPU 密集活丢给 tokio::task::spawn_blocking,或者用 rayon 做并行迭代器。

坑二:async 里的生命周期与 'static

spawn 出去的 future 必须 'static,意思是它不能借用外部数据。新手常报 cannot refer to outer variable。解法:能 clone 的 clone(Arc 很便宜),不能 clone 的把数据 move 进 async 块。

坑三:sqlx 离线编译报错

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 看依赖树,必要时升级。

高频面试题(折叠答案)

面试重点 · 20 题速记

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 后端的核心心法:类型系统替你把住关,编译器替你把住关,剩下的就是把业务写清楚。