用 Rust 写生产级服务要踩多少坑——Cloudflare 把答案做成了一个开源库

写一个在本地跑起来的程序,和把这个程序部署成全球分布式服务,是两件本质上不同的事情。

很多工程师都经历过这个阶段:一个在笔记本上表现良好的原型,一旦进入生产环境,就开始暴露出各种问题------日志不够用、配置改起来麻烦、崩溃了不知道哪里出问题、安全加固一点没做。

Cloudflare 的工程师在开发了大量 Rust 服务之后,把反复解决这些问题的经验提炼成了一个库,叫做 Foundations。2024 年初,这个库正式开源。

原文链接:https://blog.cloudflare.com/introducing-foundations-our-open-source-rust-service-foundation-library/

开源地址:https://github.com/cloudflare/foundations
API 文档:https://docs.rs/foundations/latest/foundations/


从 Oxy 里长出来的基础设施

Foundations 最初是 Cloudflare 的 Oxy 代理框架的一部分。Oxy 是他们用 Rust 重写代理层的核心项目,在构建过程中积累了大量对"每个生产级 Rust 服务都需要什么"这个问题的实践答案。

后来团队意识到,这些能力不只对 Oxy 有价值,对 Cloudflare 内部所有的 Rust 服务都适用,于是把它们抽离出来,做成了独立的库。D1、Constellation 等服务都在用它。


从笔记本到生产环境,差的是什么

Cloudflare 团队总结了从原型到生产的三大核心差距:

可观测性:在本地,开发者有 IDE、调试器、printf。但当服务跑在全球数千台远程服务器上,这些工具都不再适用。你需要结构化的日志、分布式追踪、指标监控,而且这套系统本身必须低摩擦,否则开发者会绕过它。

配置管理:本地原型往往用硬编码或简单的命令行参数。生产环境需要支持复杂的层级配置、动态修改、以及"配置即代码"的范式。

安全:本地运行的程序不需要考虑太多外部威胁。生产环境的服务暴露在各种攻击面前,系统调用层面的沙箱、最小权限原则,都是必要的基础设施。

Foundations 就是针对这三个维度系统性地给出答案。


设计原则:能用比能调节更重要

Foundations 在设计上坚持三个原则:

高度模块化:很多服务比 Foundations 出现得更早,不可能要求所有人同时切换。因此 Foundations 的每个功能都可以独立引入,团队按自己的节奏迁移。

API 人体工学:大量使用 Rust 的过程宏,让开发者少写样板代码,把文档和定义放在同一个地方。

开箱即用优先:Foundations 的目标是让工程师几乎不花时间做基础设施配置,加进来就能用,之后再慢慢调整细节。这是一个有意识的取舍------相比极度灵活的通用配置 API,它针对生产验证过的特定环境进行了优化。


遥测(Telemetry):日志、追踪、指标三合一

Foundations 把可观测性的三个维度统一在一套 API 里,用 TelemetryContext 对象作为隐式的上下文载体贯穿始终。

Tracing(链路追踪)

Foundations 的追踪 API 在接口风格上接近 tokio/tracing,支持隐式上下文传播和 Future 包装。但它在几个地方做了自己的扩展:

采样率动态覆盖:全局采样率保持低值来控制性能开销,但在调试特定账号、特定连接的性能问题时,可以在代码里局部覆盖采样率,强制对这部分流量做完整采样。这在 tokio/tracing 里需要额外的工作。

跨服务追踪拼接:上游服务可以把采样决策传递给下游,整条链路的追踪数据会自动关联成一个完整视图,而不是散落在各个服务的日志里。

Trace Forking(追踪分叉):这是解决一个具体工程问题的设计------对于长连接上的多路复用请求(比如 HTTP/2 或 HTTP/3),如果所有请求共享同一条 Trace,分析起来会非常混乱。Foundations 允许为每个请求创建独立的子 Trace,同时保持与父连接 Trace 的关联,两者都可以独立分析,也可以合并观察。

rust 复制代码
#[tracing::span_fn("respond to request")]
async fn respond(
    endpoint_name: Arc<String>,
    req: Request<Body>,
    routes: Arc<Map<String, ResponseSettings>>,
) -> Result<Response<Body>, Infallible> {
    // 业务逻辑
}

Logging(日志)

Foundations 的日志系统解决了一个在复杂服务里很常见的痛点:层级上下文的传递

典型场景:一个 TCP 连接上有多个 HTTP 请求。日志里需要同时携带连接 ID、协议版本(连接级别的信息),以及请求 URL(请求级别的信息)。

传统做法是为每个请求创建一个新 logger,手动复制连接 logger 的标签,然后显式传递这个 logger 对象。这很繁琐,而且会让业务逻辑代码到处都是与遥测无关的参数传递。

Foundations 的解法是借用 tokio/tracing 对 Future 的 instrumentation 机制,把日志上下文做成隐式传播的:

rust 复制代码
let conn_tele_ctx = TelemetryContext::current();

let on_request = service_fn(move |req| {
    // 每个请求得到独立的日志(继承自连接日志)
    // 和独立的 Trace(链接到连接 Trace)
    conn_tele_ctx
        .with_forked_log()
        .with_forked_trace("request")
        .apply(async move { respond(endpoint_name, req, routes).await })
});

with_forked_log() 创建了一个继承自连接上下文的新日志上下文,并自动传播到整个请求的异步调用链中,不需要显式传参。

Metrics(指标)

Foundations 使用 Prometheus 官方 Rust 客户端,并在上面加了一个过程宏来减少样板代码。定义带标签的指标,只需要这样写:

rust 复制代码
#[metrics]
pub(crate) mod http_server {
    /// 活跃客户端连接数。
    pub fn active_connections(endpoint_name: &Arc<String>) -> Gauge;

    /// 失败连接总数。
    pub fn failed_connections_total(endpoint_name: &Arc<String>) -> Counter;

    /// HTTP 请求总数。
    pub fn requests_total(endpoint_name: &Arc<String>) -> Counter;

    /// 失败请求总数。
    pub fn requests_failed_total(endpoint_name: &Arc<String>, status_code: u16) -> Counter;
}

文档注释直接成为指标的说明文档,标签类型在编译期检查,不需要手动注册或管理生命周期。

内存分析:jemalloc 集成

对于长期运行的服务,jemalloc 在内存分配效率上通常优于系统默认分配器。Foundations 封装了 jemalloc 的集成,并把它的内存分析能力包装成安全的 Rust API,方便在遇到内存增长异常时直接介入分析。


安全:用 seccomp 把系统调用锁进笼子

Foundations 提供了一套对 seccomp(Linux 内核的系统调用过滤机制)的封装。

seccomp 允许为进程定义一张系统调用白名单,任何不在白名单上的系统调用被执行时,内核可以选择杀死进程、返回错误、或者记录日志。这对于防御任意代码执行类攻击是一道有效的纵深防线------即使攻击者利用漏洞在你的进程里执行了代码,大量危险的系统调用也会被直接拦截。

Foundations 的 API 设计力求简洁:

rust 复制代码
use foundations::security::common_syscall_allow_lists::{ASYNC, NET_SOCKET_API, SERVICE_BASICS};
use foundations::security::{allow_list, enable_syscall_sandboxing, ViolationAction};

allow_list! {
    static ALLOWED = [
        ..SERVICE_BASICS,   // 基础服务所需的系统调用
        ..ASYNC,            // 异步运行时所需的系统调用
        ..NET_SOCKET_API    // 网络服务所需的系统调用
    ]
}

enable_syscall_sandboxing(ViolationAction::KillProcess, &ALLOWED)

..SERVICE_BASICS 这样的展开语法允许组合多个预定义白名单,同时支持逐条添加自定义的系统调用。Foundations 内置了几组常用场景的白名单,绝大多数服务直接组合使用即可。


配置与 CLI:Rust 结构体就是配置文档

Foundations 的配置管理方案有一个核心理念:服务应该自带一份可以直接运行的默认配置,而不是让用户在空配置文件面前无从下手。

实现方式是把配置直接定义为 Rust 结构体,默认值写在代码里,文档注释自动变成配置说明:

rust 复制代码
#[settings]
pub(crate) struct HttpServerSettings {
    /// 遥测配置。
    pub(crate) telemetry: TelemetrySettings,
    /// HTTP 端点配置。
    pub(crate) endpoints: Map<String, EndpointSettings>,
}

运行 CLI 的 --print-config 命令,Foundations 会把整个配置结构渲染成一份带注释的 YAML 文件:

yaml 复制代码
---
# 遥测配置。
telemetry:
  # 分布式追踪设置
  tracing:
    # 是否启用追踪。
    enabled: true
    # Jaeger Thrift (UDP) 代理地址。
    jaeger_tracing_server_addr: "127.0.0.1:6831"
    # 采样率(0.0 到 1.0 之间,1.0 表示全量采样)。
    sampling_ratio: 1.0
  # 日志设置。
  logging:
    output: terminal
    format: text
    verbosity: INFO
# HTTP 端点配置。
endpoints:
  Example endpoint:
    addr: "127.0.0.1:0"
    routes:
      /hello:
        status_code: 200
        response: World

这份 YAML 的每一个字段都附有从代码注释生成的说明。用户拿到这份文件,改掉需要修改的部分,不需要翻文档就能理解每个选项的含义。


这套思路的普遍价值

Foundations 解决的问题不是 Cloudflare 独有的,而是每个在生产环境运行 Rust 服务的团队都会遇到的。

它背后有一个值得记住的工程判断:把"基础设施能力"和"业务逻辑"彻底分开。日志应该自动传播,配置应该自文档,系统调用应该默认受限------这些能力应该像空气一样存在,工程师不需要每次写新服务都重新把轮子造一遍。

Foundations 目前在 Cloudflare 内部的多个核心服务上运行,API 文档和示例代码都已随库一起开源,任何 Rust 项目都可以按模块独立引入。

相关推荐
码界奇点1 小时前
基于Python的微信公众号爬虫系统设计与实现
开发语言·爬虫·python·毕业设计·web·源代码管理
码途漫谈1 小时前
UI-UX-Pro-Max开源项目介绍
人工智能·ui·ai·开源·ai编程·ux
落雪寒窗-1 小时前
Python开发个人日常记录
开发语言·python
启山智软2 小时前
【 商城系统源码:Java与PHP的区别】
java·开发语言·php
练习时长两年半的程序员小胡2 小时前
Java程序员转大模型应用开发专题(一):核心基础概念
java·开发语言·transformer·自注意力
源图客2 小时前
PHP开发环境搭建
开发语言·php
数据智能老司机2 小时前
Rust 设计模式与最佳实践——不要和借用检查器对抗
rust
Evand J2 小时前
MATLAB绘图函数介绍:plotmatrix绘图,附MATLAB例子
开发语言·matlab·绘图
比特 GOK2 小时前
Qt项目ui文件中新添加的控件在代码中不识别的问题解决
开发语言·qt·ui