从玩具项目到生产落地:一个奶爸程序员的Rust Embedding实战手记
凌晨一点半,老大刚睡,老二还在哼哼唧唧。我蹑手蹑脚摸回书房,打开那台用了三年的MacBook Pro,屏幕幽光照着黑眼圈——没错,就是那个白天在公司写业务逻辑、晚上回家带娃、深夜才偷得两小时学新技术的奶爸程序员。
我在当前这个团队干了快两年,主要负责后端服务和部分AI能力集成。最近半年,团队被产品经理“温柔”地推进了大模型时代:用户希望我们的搜索结果更智能,推荐更精准,还得支持语义理解。这背后绕不开一个词:Embedding。
说实话,一开始我对Embedding的理解还停留在“把文本变成向量”的模糊认知。但真正动手之后才发现,这玩意儿远不止调个API那么简单。尤其是在资源有限、性能敏感、还要扛住线上流量的现实场景下,光有理论远远不够。今天这篇,就聊聊我是怎么从踩坑小白,一步步把Embedding能力稳稳落地生产的全过程。
起因:别再让用户搜不到东西了!
事情要从去年双11前说起。我们有个内部知识库系统,用户经常反馈:“明明文档里写了,怎么搜不到?”传统关键词匹配(比如Elasticsearch的BM25)在面对“退款流程”搜“怎么退钱”这种表达差异时,直接歇菜。
产品老大拍板:“上语义搜索!”技术方案很快聚焦到Embedding + 向量检索。思路很清晰:把所有文档用模型编码成向量,用户查询也转成向量,然后算相似度召回。
听起来简单?上线前一周,测试环境一压测,我就想砸键盘。
- 单次Embedding生成耗时 800ms+
- 内存占用飙升,Pod频繁OOM
- 模型文件 1.2GB,部署成本高得离谱
运维同事看着监控图直摇头:“你这服务跑起来比数据库还吃资源。”测试妹子更是每天追着问:“能不能先降级?用户都投诉搜索变慢了。”
那一刻我意识到:技术探索不能只停留在Jupyter Notebook里跑通demo就算完事,必须考虑工程化、资源消耗和稳定性。
技术选型:为什么是Rust?
起初我们用Python+Sentence Transformers方案,开发快、生态好。但性能瓶颈太明显。于是开始寻找替代方案。
选项有三:
| 方案 | 开发速度 | 性能 | 部署复杂度 | 内存占用 |
|---|---|---|---|---|
| Python + transformers | ⭐⭐⭐⭐ | ⭐ | ⭐⭐ | 高(~1.2GB) |
| ONNX Runtime + C++ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | 中 |
Rust + sentence-transformers-rs |
⭐⭐ | ⭐⭐⭐⭐ | ⭐ | 低(~400MB) |
我选了Rust。原因有几个:
- 内存安全 & 零成本抽象:对我们这种资源受限的服务太友好了。
- 编译成二进制:部署只需一个可执行文件,Docker镜像小到离谱。
- 异步生态成熟:
tokio+axum写HTTP服务丝滑如德芙。 - 个人兴趣驱动:被领导“建议”学点新东西,正好拿项目练手。
当然,代价是学习曲线陡峭。第一次写Arc<Mutex<>>的时候,我差点以为自己在解微分方程。但熬过头两周,真香!
实战:Embedding服务的优化三板斧
第一板斧:模型瘦身 —— 从DistilBERT到ONNX量化
我们最初用的是sentence-transformers/all-MiniLM-L6-v2,虽然比BERT小,但在高频调用下依然吃不消。
解决方案:ONNX导出 + INT8量化。
# 在Python中先导出ONNX模型(仅一次)
from optimum.onnxruntime import ORTModelForFeatureExtraction
from transformers import AutoTokenizer
model_id = "sentence-transformers/all-MiniLM-L6-v2"
onnx_model = ORTModelForFeatureExtraction.from_pretrained(model_id, from_transformers=True)
tokenizer = AutoTokenizer.from_pretrained(model_id)
onnx_model.save_pretrained("./onnx-model")
tokenizer.save_pretrained("./onnx-model")
然后在Rust中加载:
use ort::{Environment, SessionBuilder};
let env = Environment::builder().with_name("embedding").build()?;
let session = SessionBuilder::new(&env)?
.with_optimization_level(ort::GraphOptimizationLevel::Level3)?
.with_intra_threads(2)? // 控制线程数,避免争抢
.commit_from_file("onnx-model/model.onnx")?;
通过onnxruntime的INT8量化(使用optimum-cli工具),模型体积从98MB压到32MB,推理速度提升2.3倍,CPU占用下降40%。
小贴士:别盲目开多线程!我试过
intra_threads=8,结果因为上下文切换开销反而更慢。实测2-4线程最适合我们的负载。
第二板斧:缓存复用 —— 用户查过的别再算
很多查询是重复的,比如“怎么重置密码”。于是加了两级缓存:
- 本地LRU缓存(基于
lrucrate):缓存最近1000个query的embedding - Redis远程缓存:跨实例共享,TTL设为24小时
use lru::LruCache;
use std::sync::Mutex;
lazy_static! {
static ref LOCAL_CACHE: Mutex<LruCache<String, Vec<f32>>> =
Mutex::new(LruCache::new(NonZeroUsize::new(1000).unwrap()));
}
效果立竿见影:缓存命中率约65%,QPS从300飙到900+,P99延迟从320ms降到85ms。
第三板斧:批处理 + 异步流水线
最开始是来一个请求算一个,IO等待浪费严重。后来改造成动态批处理:
- 收到请求先入队
- 每10ms或队列满16条时,批量送入模型
- 返回结果通过channel一一对应返还
核心逻辑用tokio::sync::mpsc实现:
// 简化版示意
let (tx, mut rx) = mpsc::channel::<BatchItem>(100);
tokio::spawn(async move {
let mut batch = Vec::new();
loop {
tokio::select! {
item = rx.recv() => {
if let Some(i) = item { batch.push(i); }
}
_ = sleep(Duration::from_millis(10)) => {
if !batch.is_empty() {
let embeddings = run_inference(batch.clone()).await;
// 发回各自的结果...
batch.clear();
}
}
}
}
});
这一招让GPU/ML加速单元利用率从30%提到80%,单位时间吞吐翻倍。
效果:上线后老板笑了,运维也不骂了
经过三周迭代(中间熬了两个通宵,老婆差点把我赶出卧室),新服务终于上线。
关键指标对比:
| 指标 | 旧方案(Python) | 新方案(Rust+ONNX) |
|---|---|---|
| 平均延迟 | 780ms | 92ms |
| P99延迟 | 1200ms | 180ms |
| 内存占用/实例 | 1.1GB | 380MB |
| CPU使用率 | 75% | 35% |
| Docker镜像大小 | 1.8GB | 120MB |
最爽的是,现在一个4核8G的K8s Pod能扛住5000+ QPS,而之前需要3个实例才能勉强支撑。运维老哥甚至主动问我:“你们这个服务能开源不?”
开发心得:技术探索不能脱离现实约束
作为一个只能在娃睡后学习的奶爸程序员,我深刻体会到:炫技不如稳赢。
- 别迷信“最新”:Rust虽好,但如果团队没人会,维护成本可能更高。我们是在已有Python服务基础上做增量替换,逐步迁移。
- Embedding不是万能药:对短query、专业术语多的场景,混合关键词+向量的hybrid search效果更好。我们现在用BM25召回+Embedding重排。
- 监控必须跟上:给Embedding服务加了专属Metrics(缓存命中率、批处理大小分布、推理耗时分位数),否则线上出问题根本没法定位。
- 文档即代码:我把整个部署流程、压测报告、回滚方案都写进了项目的
DEPLOY.md,方便新人接手——毕竟谁也不想半夜被PagerDuty叫醒。
结语:在尿布与代码之间寻找平衡
写这篇文章时,老二又醒了。赶紧保存草稿,冲去换尿布。但心里挺踏实:技术落地了,用户满意了,团队信任度也涨了。
有人说程序员35岁就该转管理,但我觉得,只要还能写出优雅高效的代码,在一线解决问题,就值得骄傲。哪怕每天只有两小时属于自己的时间,也能做出有价值的东西。
如果你也在带娃、加班、学新技术的路上挣扎,记住:每一个深夜敲下的代码,都是给未来自己的一份礼物。
共勉。
附:项目已脱敏开源,欢迎Star(不是广告,纯分享)
GitHub:github.com/dad-coder/rust-embedding-service
(名字是我临时起的,别笑)

评论 0