聊聊技术探索与实践:一个北京小厂后端的深夜自白

山月写前端
2025-12-13 15:40
阅读 419

今天凌晨两点,我刚合上 MacBook,窗外五环外的夜色静得能听见自己心跳。咖啡杯底残留的冷萃已经结了一层膜,脑子里还回荡着 Rust 编译器那句亲切又扎心的 “error[E0382]: use of moved value”。
但奇怪的是,我不觉得累——甚至有点兴奋。因为今天终于把那个拖了两周的异步任务队列用 Rust 重写完了,吞吐量直接翻了三倍。

我是谁?简单说:北京一家不到百人的小厂后端,独立负责一条业务线(说得高大上,其实就是没人帮我背锅)。每天通勤一小时,在国贸和回龙观之间反复横跳,白天应付产品经理“能不能加个按钮但别改逻辑”的需求,晚上回家才能摸点真正有意思的东西。最近迷上了 Rust,虽然学得磕磕绊绊,但越啃越上头。

今天这篇文章,不讲八股文,也不画架构图吹牛逼。就想和大家聊聊:在资源有限、时间紧张的小厂环境下,一个普通后端怎么在“活着”和“成长”之间找平衡,顺便搞点技术探索与实践。


起因:前端甩过来一个“合理”需求

事情得从上周五说起。那天 PM 在群里@我:“后端大佬,我们想做个实时数据看板,前端需要每秒推 5k 条记录,延迟不能超过 200ms,能支持吗?”

我盯着消息看了十秒,默默打开 Jira —— 果然,Deadline 是三天后上线,理由是“配合运营双十二预热”。

我们现在的技术栈?Java Spring Boot + MySQL + Redis,典型的 CRUD 小作坊配置。之前处理实时数据靠 WebSocket + 长轮询,QPS 顶天 800。5k?还 200ms?我当时差点把键盘扔出窗户。

更绝的是,前端同事补了一句:“我们用 Svelte 写的,轻量,应该压力不大吧?”
兄弟,你轻量,但我后端不是永动机啊!

但抱怨归抱怨,活还得干。我拉了个会,和前端、测试、运维一起对齐。结论很现实:现有架构撑不住,要么砍需求,要么换方案。PM 摇头:“用户要的就是‘丝滑’。” 得,没得选。


探索:为什么是 Rust?

其实第一个想到的是 Go。毕竟 Goroutine 天然适合高并发,生态也成熟。但转念一想:我们团队就我一个后端,Go 虽然简单,但 GC 停顿在极端场景下还是不可控。而且……说实话,我早就想试试 Rust 了。

契机其实挺“功利”:上个月面试了一家大厂,被问到“有没有系统级语言经验”,我支支吾吾说了半天 Java 的 JIT,对方礼貌微笑。回来我就下了决心:技术债可以欠,但成长不能停。

Rust 的卖点我很清楚:零成本抽象、内存安全、无 GC、高性能。但风险也很明显:学习曲线陡,团队无人会,线上出问题我一个人兜底。

权衡之后,我决定赌一把——用 Rust 写一个独立的微服务,专门处理实时数据推送,和主 Java 服务通过 Kafka 解耦。 这样即使挂了,也不影响核心交易链路。跟老板汇报时我说:“就当技术预研,万一成了,明年双11就靠它扛。”

老板居然点头了。可能是因为我用了“降本增效”这个词(笑)。


实践:从 Hello World 到线上跑

第一步:搭架子

我用 cargo new realtime-pusher --bin 创建项目,引入几个关键 crate:

[dependencies]
tokio = { version = "1.0", features = ["full"] }
warp = "0.3"          # Web 框架,比 Actix 轻
tokio-tungstenite = "0.20"  # WebSocket 支持
rdkafka = "0.34"      # Kafka 客户端
serde = { version = "1.0", features = ["derive"] }
tracing = "0.1"       # 日志追踪

选择 Warp 而不是 Actix,是因为它基于 Hyper,组合式 API 更符合我的函数式偏好,而且代码量少,调试方便。

第二步:处理 WebSocket 连接

前端要的是“推”,所以服务得维护大量长连接。Rust 的 async/await + tokio 的多线程 runtime 刚好合适。核心逻辑如下:

use warp::ws::{Message, WebSocket};
use futures_util::{SinkExt, StreamExt};

async fn handle_ws(ws: WebSocket) {
    let (mut user_ws_tx, mut user_ws_rx) = ws.split();
    
    // 从全局广播通道订阅消息(后面会讲)
    let mut broadcast_rx = BROADCAST_CHANNEL.subscribe();
    
    // 启动两个任务:收 + 发
    let send_task = tokio::spawn(async move {
        while let Ok(msg) = broadcast_rx.recv().await {
            if user_ws_tx.send(Message::text(msg)).await.is_err() {
                break; // 连接断开
            }
        }
    });

    let recv_task = tokio::spawn(async move {
        while let Some(_) = user_wsrx.next().await {
            // 心跳或关闭处理,略
        }
    });

    tokio::select! {
        _ = send_task => {},
        _ = recv_task => {}
    }
}

这里踩了个坑:一开始用 mpsc channel,结果发现它不支持多消费者。后来换成 tokio::sync::broadcast,完美解决一对多广播。

第三步:对接 Kafka

前端数据来自上游业务系统,通过 Kafka Topic 发送。我用 rdkafka 消费消息,并推送到广播通道:

async fn kafka_consumer(broadcast_tx: broadcast::Sender<String>) {
    let consumer: StreamConsumer = ClientConfig::new()
        .set("bootstrap.servers", "kafka:9092")
        .set("group.id", "realtime-pusher")
        .create()
        .expect("Kafka consumer");

    consumer.subscribe(&["user-events"]).expect("Subscribe");

    loop {
        let message = consumer.recv().await.unwrap();
        let payload = String::from_utf8(message.payload().unwrap().to_vec()).unwrap();
        
        // 广播给所有 WebSocket 客户端
        let _ = broadcast_tx.send(payload);
    }
}

注意:broadcast::Sender::send() 如果没有接收者会返回 Err,但这里我们不在乎,直接丢弃即可。


踩坑实录:那些让我想砸电脑的瞬间

  1. 生命周期地狱
    初期我把 broadcast::Sender 作为全局变量,结果在 Warp handler 里引用时各种 'static 报错。最后用 Arc<Mutex<...>> 包一层才搞定。Rust 的所有权模型真不是盖的,第一次写时感觉像在解数学题。

  2. WebSocket 断连检测
    测试时发现客户端断网后,服务端还在傻乎乎地发消息。后来加了 ping/pong 心跳,并设置超时断开:

    tokio::time::timeout(Duration::from_secs(30), user_ws_rx.next()).await
    
  3. 日志打不出来
    本地跑得好好的,上 K8s 后日志全没了。原来是 tracing 默认输出到 stderr,而我们的日志收集只认 stdout。加了个 tracing-subscriber 配置才解决。

  4. CPU 爆了
    压测时发现 CPU 占用 90%+。用 perf 分析发现是频繁字符串拷贝。改成 Arc<str> 共享数据后,CPU 降到 30%,内存也省了 40%。


效果对比:值不值得折腾?

上线前我做了压测(用 websocat 模拟 5k 并发连接),结果如下:

指标 原 Java 方案 新 Rust 方案
QPS 800 5200
P99 延迟 450ms 120ms
内存占用 1.2GB 320MB
CPU 峰值 75% 35%

最爽的是,Rust 服务跑了三天零 panic。运维老哥都惊了:“你这新服务是不是没流量?” 我:“不,它在默默扛着全场最狠的活。”

前端同事也反馈:“现在滑动看板真的丝滑了!” —— 虽然我知道他根本分不清是前端优化还是后端升级,但至少没再提新需求(笑)。


综合思考:小厂技术人的“野蛮生长”

回头看看这段经历,其实挺典型的:需求倒逼 + 个人兴趣 + 风险隔离 = 可行的技术探索路径。

在小厂,没有专职 SRE,没有架构师帮你兜底,但好处是决策链短,试错成本低。只要不影响核心业务,老板其实挺愿意让你“折腾点新东西”——前提是别把线上搞崩。

我也不是盲目追新。选 Rust 不是因为 hype,而是它确实解决了我们的问题:高性能 + 内存安全 + 无运行时依赖。部署时一个二进制文件扔进 Docker,连 JVM 都不用装,运维直呼内行。

当然,前端同学要是哪天说“我们要上 WebAssembly”,我可能会连夜跑路。不过话说回来,现在前后端界限越来越模糊,“综合能力”才是小厂程序员的生存之道。你得懂点网络协议,会调性能,能看前端报错,还得会写文档哄 PM 开心。


最后一点真心话

技术探索不是为了炫技,而是为了解放生产力。当你能在深夜安静地写代码,不用开会、不用改需求、不用解释“为什么这个 Bug 修了三天”,那种纯粹的快乐,是加班费给不了的。

Rust 我还在学,下周打算试试用它写个 CLI 工具,自动同步数据库表结构到前端 Type 文件——这样就不用每次改字段都手动通知前端了。PM 再说“就改一个字段”,我就甩他一个 .rs 文件。

如果你也在小厂挣扎,别觉得技术探索是大厂专利。哪怕每天只学 30 分钟,一年后回头看,你会感谢那个没躺平的自己。

好了,天快亮了,我得去挤地铁了。希望今天别遇到“需求变更”。

附:项目已开源(内部 GitLab),欢迎 star(如果你们能访问的话)😉

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝