在 Fly.io 上使用 Rust 构建远程开发环境:从 Tokio 到 eBPF

本文是对 Remote development with Rust on fly.io 的整理与翻译

(内容结构概览)

  1. 为什么要构建远程开发环境?
  2. Fly.io 的底层逻辑:Firecracker 微型虚拟机
  3. 部署测试应用:一个极简的 Rust Axum 服务
  4. 深入虚拟机内部:运行 Docker 与 Perf
  5. 核心挑战:如何让机器在闲置时自动休眠?
  6. 解决方案一:编写基于 Tokio 与 io-uring 的 TCP 代理
  7. 解决方案二:使用 eBPF (Aya) 无感监听网络流量
  8. 总结

1. 为什么要构建远程开发环境?

在当今的开发场景下,构建一个远程开发环境有诸多不可忽视的理由。

首先是性能瓶颈。编译大型的 Rust 项目(例如 Rust 编译器本身、rust-analyzer,或是多个庞大的私有代码库)需要消耗大量的 CPU 资源。与其耗费巨资购买顶配的桌面 CPU,不如按需租用高性能的云端机器,仅在执行重度任务时开启。

其次是跨平台编译的痛点。随着 Apple M1/M2 等 arm64 架构芯片的普及,开发者在本地进行开发,却往往需要将应用部署到 Linux x86_64 环境。虽然可以通过虚拟机或 Docker 容器进行指令集模拟,但这不仅会严重拖慢运行速度,还会让笔记本电脑发热严重。

此外,团队协作也是一个重要因素。远程开发环境使得让分布在全球的开发者快速入职成为可能,你无需为他们邮寄昂贵的硬件设备,就能提供一个一致、开箱即用的标准环境。

就作者个人而言,原因非常纯粹:炎炎夏日,运行一台高功耗、散发大量热量的桌面台式机实在不够环保,也会让室内温度急剧上升。通过远程连接到云端的开发环境,可以彻底解决算力与物理散热之间的矛盾。

2. Fly.io 的底层逻辑:Firecracker 微型虚拟机

对于云服务,大家可能熟悉 Heroku 或者 Google Cloud Run,它们主要依赖于常规的容器(Docker)技术。然而,Fly.io 的运作方式略有不同。

当你在 Fly.io 上部署代码时,它并不是单纯地在 Docker 引擎中运行一个隔离容器,而是将你的应用运行在一个 Firecracker 微型虚拟机 (microVM) 中。这意味着你拥有一个真正的虚拟机环境,它具备独立的 Linux 内核,没有传统容器环境在内核权限方面的诸多限制。

3. 部署测试应用:一个极简的 Rust Axum 服务

为了演示,我们先构建一个最基础的 HTTP 服务器。这里使用了 tokioaxum 框架:

rust 复制代码
use axum::{response::IntoResponse, routing::get, Router, Server};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(index));
    let addr = "[::]:8080".parse().unwrap();
    println!("Listening on http://{addr}");
    Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn index() -> impl IntoResponse {
    "hello from axum\n"
}

配合该应用,我们可以编写一个使用多阶段构建(Multi-stage build)的 Dockerfile。为了加速 Rust 的编译,作者在这里巧妙地利用了 Docker BuildKit 的缓存挂载特性:

dockerfile 复制代码
# syntax = docker/dockerfile:1.4
FROM rust:1.61.0-slim-bullseye AS builder
WORKDIR /app
COPY . .
RUN --mount=type=cache,target=/app/target \
    --mount=type=cache,target=/usr/local/cargo/registry \
    --mount=type=cache,target=/usr/local/cargo/git \
    --mount=type=cache,target=/usr/local/rustup \
    set -eux; \
    rustup install stable; \
    cargo build --release; \
    objcopy --compress-debug-sections target/release/hello-axum ./hello-axum

FROM debian:11.3-slim
# 安装基础网络排查工具
RUN apt update && apt install --yes bind9-dnsutils iputils-ping iproute2 curl htop
WORKDIR app
COPY --from=builder /app/hello-axum ./hello-axum
CMD ["./hello-axum"]

使用 flyctl 生成配置文件 fly.toml 并进行部署后,这个 Rust 应用就会被打包为精简镜像,并运行在 Fly.io 离你最近的边缘节点上。

4. 深入虚拟机内部:运行 Docker 与 Perf

部署完成后,通过执行 fly ssh console 命令,我们可以直接通过原生 SSH 连入这个实例。

连入后,通过 uname -a 查看系统信息,你会发现系统运行着一个真实的 Linux 内核(文章示例中为 Linux 5.12),而且我们的身份是真正的 root。因为这是一个由 Firecracker 驱动的虚拟机,不仅有独立的用户态,还有完整的内核态支持。

这意味着我们甚至可以在这个 Fly.io 的虚拟机内部再次安装并运行完整的 Docker 引擎!通过向其挂载持久化存储卷(Volume),你完全可以将其当做一个全功能的 Linux 开发机来使用。

更进一步,得益于真实内核的存在,你可以直接下载当前内核版本的源码,编译并安装底层的性能分析工具 perf

bash 复制代码
# 获取并编译安装 perf 的大致流程
KERNEL_VERSION=$(uname -r | sed -r 's/(^[^-]+).*/\1/' | sed -r 's/\.0//g')
curl --location "[https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-$](https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-$ "https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-$"){KERNEL_VERSION}.tar.xz" | tar -xJ
make -C tools/ perf_install prefix=/usr/

这让你能在云端直接使用 perf top 去深入分析 Rust 程序的 CPU 周期消耗,这在普通的受限容器托管平台上是绝对做不到的。

5. 核心挑战:如何让机器在闲置时自动休眠?

搭建一台高性能云端开发机的关键在于成本控制。Fly.io 的机器默认情况下会一直运行,如果不手动执行关机命令,就会产生持续的计费。

一个理想的远程开发环境应该具备这样的特性:当我连接上它并进行开发时,它保持唤醒;当没有活跃的连接或是网络流量处于长期静默状态后,它能够自动关闭自己。

为了解决这个自动化生命周期管理的问题,作者提出了两种不同层面的 Rust 技术实践方案。

6. 解决方案一:编写基于 Tokio 与 io-uring 的 TCP 代理

第一种思路非常直接:我们不让开发者直接连接到虚拟机的标准端口,而是自己写一个驻留的 TCP 代理应用监听外部请求。这个代理程序负责将流量无缝转发到后端开发服务端口。

它的核心逻辑如下:

  1. 监听对外的公开端口并接收流量。
  2. 在内存中维护一个原子性的"活跃连接数"计数器。每建立一个连接,计数器加一;连接断开,计数器减一。
  3. 后台运行一个异步轮询任务:如果连接数归零,并持续了设定的超时倒计时(如几分钟),程序就直接调用 std::process::exit(0) 退出。由于该代理进程被设为微型虚拟机的初始主进程(Init 进程),一旦主进程退出,底层设施就会触发整个虚拟机的安全关闭动作。

在具体代码层面,为了追求极致的吞吐量并体验前沿技术,作者引入了 tokio-uring 运行时,这是一种建立在 Linux 新型异步 I/O 接口 io-uring 之上的高性能方案。由于 tokio-uring 采用单线程轮询模型,我们可以使用成本极低的 Rc(引用计数)来代替多线程环境下的 Arc 进行 TCP 连接流的所有权管理。代理任务在完成双向的数据拷贝(Ingress 到 Egress 互相直通)结束后,再清理引用计数并触发全局活动连接数的删减。

7. 解决方案二:使用 eBPF (Aya) 无感监听网络流量

虽然业务层的 TCP 代理方案能完美解决问题,但它有一定的侵入性:你需要劫持和转发端口流量,实质上充当了一个中间人。

有没有一种更底层的做法,能够直接在操作系统层面"偷听"网络状态的改变,而不去干扰和阻断原始的网络链路呢?

答案是 eBPF (Extended Berkeley Packet Filter)。通过使用 Rust 社区强大的 Aya 框架,我们可以将编译好的 BPF 字节码直接安全地注入到操作系统的网络栈中。

这个纯净方案分为两部分:

  • eBPF 探针代码 (Kernel space):一个用 #[no_std] 约束编写的极简 Rust 程序。我们将其挂载在 sock_ops 钩子上。它的任务是拦截并洞察系统中所有的套接字行为。当检测到有新的 TCP 连接处于 Established 状态或被 Closed 关闭时,它会将这些网络状态变化通过 BPF 的环形缓冲区(Ring Buffer)安全地传递给用户空间。
  • 用户态守护进程 (User space):一个常规的 Rust 守护进程,负责将 eBPF 程序加载进内核,并持续监听内核发来的事件。它同样维护着网络连接统计,一旦发现所有的网络连接均已关闭并且闲置达到预设的超时时间,直接下达指令让系统关机休眠。

这种 eBPF 方案展现了在 Linux 内核层面实现无缝监控的强大能力。它不需要拦截和转发业务流量,程序仅仅是作为系统网络底层的一个旁观者,根据真实的 TCP 握手和挥手情况,精准、低耗地控制开发机的生命周期。

8. 总结

这篇博客生动地展现了将现代云原生基础设施(以 Fly.io 为代表的微型虚拟机)与系统级编程语言(Rust)结合起来的巨大应用价值。

对于饱受本地硬件性能受限、架构兼容性差以及散热噪音困扰的开发者来说,构建一个完全属于自己的云端个人工作站已经不再是遥远的梦想。凭借 Rust 在异步网络编程和底层系统级交互领域的深厚积累,不管是通过编写一个利用 io-uring 榨干性能的 TCP 代理,还是运用前沿的 eBPF 技术在内核无感监控全局网络连接,我们都能以最高效优雅的方式,解决机器休眠的自动化控制与云端成本管理难题。

相关推荐
留白_2 小时前
pandas文件读取与存储
开发语言·python·pandas
夕除2 小时前
AOP 实现 Redis 缓存切面解析
java·开发语言·python
摇滚侠2 小时前
Spring 零基础入门到进阶 面向切面 AOP 52-60
java·后端·spring
feifeigo1232 小时前
马尔可夫决策过程(MDP)MATLAB 实现
开发语言·matlab
攻城狮Soar2 小时前
STL源码解析之list(1)
开发语言·c++
x***r1512 小时前
Postman-win64-7.3.5-Setup安装配置教程(Windows 详细版)
开发语言·lua
林森lsjs2 小时前
【日耕一题】4. 较为复杂情况下的求和
java·开发语言
2401_869769592 小时前
内容5 日期类实现
开发语言·c++
白露与泡影2 小时前
2026秋招冲刺:1000道Java高频面试题(各大厂考点汇总)
java·开发语言·面试