纯情 发布的文章

内容结构概览

  1. 引言 —— 背景与问题缘起

    • Cloudflare Firewall Rules 的需求
    • 为什么选择 Rust
  2. 解析器的设计

    • Wireshark DSL 语法的歧义性挑战
    • 三种解析方案的对比
    • 为何放弃解析器生成器,选择手动解析
    • Rust Trait 驱动的解析器架构
  3. 执行引擎的演进

    • 初版:直接 AST 解释执行
    • 优化一:用固定数组替代 HashMap,实现 2x 加速
    • 关于 JIT 的思考与放弃原因
  4. 核心方案:闭包动态分发

    • Fn trait 与动态分发的原理
    • 将 AST 编译为闭包树
    • 性能、安全、灵活性三者的平衡
  5. 彩蛋:WebAssembly 支持

    • wasm-bindgen 与 wasm-pack
    • 20 行代码暴露解析器给前端


在系统编程领域,有一类工程问题天然具有代表性:如何在安全、性能与可维护性之间找到真正的平衡点。Cloudflare 工程团队在构建防火墙规则引擎时,用 Rust 给出了一份值得拆解的答案。

这篇文章基于 Cloudflare 官方博客 Building fast interpreters in Rust,系统梳理其中的工程决策,适合对编译原理、Rust 或系统设计有兴趣的读者。


背景:为什么要自己造这个轮子

Cloudflare 的防火墙规则系统,需要让用户用类似 Wireshark 的过滤语法来描述规则,例如:

ip.addr == 192.168.0.1
http.request.uri matches "gl=se$"

规则需要在 Go、Lua、C、C++ 以及 Workers(JavaScript)等多种环境中运行,并且要满足以下几个硬性指标:

  • 性能:规则执行路径在关键链路上,延迟敏感
  • 内存安全:生产环境不容许内存漏洞
  • 低内存占用:边缘节点资源受限
  • 可复用性:需要被多个产品集成

在这个约束组合下,Cloudflare 选择了 Rust,并将这个库以 wirefilter 的名字开源。


第一关:解析器怎么写

Wireshark 语法的歧义陷阱

DSL 设计中,解析器是第一道关口。Wireshark 语法看上去简单,但藏着一个不小的歧义问题。

考虑这个值:2f:31:32:33:34:35:36:37

同时是合法的 IPv6 地址,也是合法的字节序列。究竟该如何解析,取决于字段的类型:

ipv6.addr == 2f:31:32:33:34:35:36:37   → 解析为 IPv6 地址
http.request.uri == 2f:31:32:33:34:35:36:37  → 解析为字节序列

类似地,80 既可以是端口号(整数),也可以是 HTTP 响应体中的单字节(0x80)。

这意味着解析器不能独立运行——它必须带着一份预先定义好的 Schema(字段名到类型的映射)才能完成解析。

为什么不用解析器生成器

面对这个需求,常见的三类方案分别是:

  1. 手动字符解析:用状态机、正则或原生字符串 API
  2. 解析器组合子(Parser Combinators):如 nomcombine
  3. 解析器生成器:如 pestLALRPOP 等,通过语法描述自动生成解析器

解析器生成器通常基于独立的词法分析阶段,而这个问题要求词法与类型上下文绑定,大多数生成器做不到。即使用 LALRPOP 这类允许自定义 Lexer 的方案,复杂度也逼近手写,却多了一层黑盒。

最终团队选择了手动解析——在 Rust 中,字符串默认有边界检查,API 丰富,安全性有保证。

Rust Trait 驱动的解析架构

团队将解析器设计为实现统一 Trait 的类型:

pub trait Lex<'i>: Sized {
    fn lex(input: &'i str) -> LexResult<'i, Self>;
}

pub trait LexWith<'i, E>: Sized {
    fn lex_with(input: &'i str, extra: E) -> LexResult<'i, Self>;
}
  • Lex:用于不依赖上下文的解析(如字段名、字面量)
  • LexWith:用于需要 Schema 的上下文感知解析

顺序解析时,直接链式调用:

let input = skip_space(input);
let (op, input) = CombinedExpr::lex_with(input, scheme)?;
let input = skip_space(input);
let input = expect(input, ")")?;

选择分支时,用 Rust 原生的模式匹配:

if let Ok(input) = expect(input, "(") {
    // 括号表达式
} else if let Ok((op, input)) = UnaryOp::lex(input) {
    // 一元操作符
} else {
    // 其他情况
}

对于枚举类型的解析,用宏来减少重复:

lex_enum!(#[repr(u8)] OrderingOp {
    "eq" | "==" => Equal = EQUAL,
    "ne" | "!=" => NotEqual = LESS | GREATER,
    "ge" | ">=" => GreaterThanEqual = GREATER | EQUAL,
    ...
});

这种方式兼具解析器组合子的无状态清晰性,又保留了完整的语言控制权。


第二关:执行引擎怎么快

初版:直接 AST 解释

最初的执行引擎很直接——让每个 AST 节点实现一个 execute 方法:

trait Expr<'s> {
    fn execute(&self, ctx: &ExecutionContext<'s>) -> bool;
}

ExecutionContext 本质上是一个 HashMap,存储字段名到运行时值的映射。执行时递归遍历 AST,在 Map 里查字段值。

功能正确,但 HashMap 查找的开销不可忽视。

优化:用固定数组替代 HashMap

关键洞察在于:Schema 是提前已知的,字段数量是固定的。因此完全可以用一个定长数组来存运行时值,用字段在 Schema 中的下标来索引,彻底省掉哈希计算。

实现上,用 IndexMap(一个保留插入顺序、支持按下标访问的 HashMap 替代品)替换 Schema 内部的 HashMap,在解析阶段就把字段名解析为下标并存入 AST:

pub struct ExecutionContext<'e> {
    scheme: &'e Scheme,
    values: Box<[Option<LhsValue<'e>>]>,
}

执行时直接按下标取值,不再涉及任何哈希。

效果立竿见影:

匹配耗时
优化前2,548 ns/iter
优化后1,227 ns/iter

2 倍的性能提升,且类型错误的检测时机提前到了赋值时,而非执行时,可用性也随之改善。

关于 JIT:想过,但放弃了

"下一步就加 JIT,性能飞起来"——这是 DSL 工程中几乎人人都有过的想法。

但 Cloudflare 团队经过评估后选择放弃,原因很实际:

存储问题:如果静态编译每条规则,需要为 x86-64、ARM、WASM 等多平台分别编译,并维护存储,规则一旦更新逻辑就要全量重编译。

JIT 的代价:JIT 编译发生在运行时,生成原生代码本身就有相当的时间开销,很容易抵消执行加速带来的收益。

安全风险:动态生成代码并标记为可执行内存,本质上是引入了一个运行时的信任边界,这在边缘安全产品中是需要格外谨慎的事。


核心方案:闭包树取代 AST 解释

既然 JIT 不用,有没有一种方式,在不生成原生代码的前提下,尽量逼近 JIT 的运行时性能?

Cloudflare 的答案是:将 AST 编译成一棵闭包树

Fn trait 与动态分发

Rust 的 Fn trait 系列(FnFnMutFnOnce)允许将闭包装箱为 Box<dyn Fn(...)>,在运行时通过动态分发调用。

核心结构如下:

pub(crate) struct CompiledExpr<'s>(Box<dyn 's + Fn(&ExecutionContext<'s>) -> bool>);

impl<'s> CompiledExpr<'s> {
    pub(crate) fn new(closure: impl 's + Fn(&ExecutionContext<'s>) -> bool) -> Self {
        CompiledExpr(Box::new(closure))
    }

    pub fn execute(&self, ctx: &ExecutionContext<'s>) -> bool {
        self.0(ctx)
    }
}

每个 AST 节点通过 compile() 方法转化为一个闭包,闭包可以捕获所需的数据,也可以嵌套调用其他闭包。

闭包套闭包:组合逻辑的自然表达

以 IP 范围检查为例,在编译阶段就可以把 IPv4 和 IPv6 分开处理并缓存:

RhsValues::Ip(ranges) => {
    let mut v4 = Vec::new();
    let mut v6 = Vec::new();
    for range in ranges {
        match range.clone().into() {
            ExplicitIpRange::V4(r) => v4.push(r),
            ExplicitIpRange::V6(r) => v6.push(r),
        }
    }
    let v4 = RangeSet::from(v4);
    let v6 = RangeSet::from(v6);
    CompiledExpr::new(move |ctx| {
        match cast!(ctx.get_field_value_unchecked(field), Ip) {
            IpAddr::V4(addr) => v4.contains(addr),
            IpAddr::V6(addr) => v6.contains(addr),
        }
    })
}

逻辑组合(AND / OR / XOR)同样用闭包嵌套表达:

match op {
    CombiningOp::And => {
        CompiledExpr::new(move |ctx| items.iter().all(|item| item.execute(ctx)))
    }
    CombiningOp::Or => {
        CompiledExpr::new(move |ctx| items.iter().any(|item| item.execute(ctx)))
    }
    ...
}

最终整棵表达式树变成一个单一的顶层闭包,调用它就等于执行整条规则。

这个方案的收益清单

  • 解耦:执行逻辑与 AST 结构完全分离,可以各自演进
  • 零原生代码生成:运行时只调用静态验证过的 Rust 函数,没有安全边界问题
  • 编译开销极低:装箱闭包的代价远低于真正的代码生成
  • 性能意外提升:相较于原始 AST 解释,动态分发带来了约 10~15% 的运行时性能改善

这个结果起初令团队意外——动态分发通常被认为有额外开销,但实测中,闭包树消除了大量递归调用的中间状态,缓存局部性反而更好。

唯一的代价是:相比于真正的 JIT 内联展开,每层闭包仍然有一次虚函数调用开销。但对这个场景而言,这个代价完全可以接受。


彩蛋:20 行代码暴露给前端

Cloudflare 的防火墙规则编辑器有一个 UI,需要在前端实时校验用户输入的规则语法。理想状态是前后端共用同一套解析器。

Rust 对 WebAssembly 的支持恰好提供了这个可能。

借助 wasm-bindgen,只需约 20 行代码就能将解析器暴露为 WASM 模块:

#[wasm_bindgen]
pub struct Scheme(wirefilter::Scheme);

#[wasm_bindgen]
impl Scheme {
    #[wasm_bindgen(constructor)]
    pub fn try_from(fields: &JsValue) -> Result<Scheme, JsValue> {
        fields.into_serde().map(Scheme).map_err(into_js_error)
    }

    pub fn parse(&self, s: &str) -> Result<JsValue, JsValue> {
        let filter = self.0.parse(s).map_err(into_js_error)?;
        JsValue::from_serde(&filter).map_err(into_js_error)
    }
}

配合 wasm-pack,可以自动生成 npm 包并发布。这意味着同一套 Rust 代码可以同时服务于:

  • 后端边缘节点的规则执行
  • 前端编辑器的实时语法校验
  • 甚至 Cloudflare Workers 中的规则运行

总结与思考

Cloudflare 这个案例的核心工程价值在于,它展示了几个在实践中常被忽视的判断:

第一,不是所有问题都适合用现成工具解决。 解析器生成器省事,但一旦 DSL 有上下文相关的歧义,生成器的边界就到了。手写解析器在 Rust 里并不可怕。

第二,性能优化要先找对瓶颈。 HashMap 查找看起来"很快",但在高频执行路径上,换成数组下标索引就能带来 2 倍提升。数据结构的选择往往比算法优化影响更直接。

第三,JIT 不是银弹。 编译代价、存储复杂性、安全边界,这些隐性成本在边缘场景下完全可以让 JIT 得不偿失。闭包树在这里是一个工程上更务实的折中。

第四,语言特性可以成为架构工具。 Rust 的 Fn trait 和动态分发,不只是语言细节,而是可以用来构建可组合执行引擎的设计原语。

如果你正在设计一个嵌入式规则引擎、DSL 解释器,或者类似的执行框架,这套思路有相当强的参考价值。


原文链接:https://blog.cloudflare.com/building-fast-interpreters-in-rust/

开源仓库:https://github.com/cloudflare/wirefilter

视频版:https://www.bilibili.com/video/BV1Kk9kBAEJv

最近 Codex APP 的能力越来越全面,变成了 Codex 四大产品形态里面最强的一个。Codex 比起 Claude Code ,额度更高,功能更全,上手更快,免费账户也能用,而且不会出现限速、封号、降智等问题,用过的小伙伴们直呼真香。本期视频带来一个 Codex APP 的完整教程,主要分为以下 12 个章节。

每个章节里面都会穿插一些重要的知识,对 Codex 的全部功能进行细致讲解。好,废话不多说,我们直接开始。

安装

在安装使用 Codex APP 之前,需要进行 3 个准备工作,也就是需要先把 Git 、Nodejs 还有 VSCode 安装一下。在我上期视频《从 0 开始用国内网络跑通一切 AI Agent 》里面,有详细的操作步骤,不熟悉的朋友们可以参考那一期视频。

接下来我们来到 Codex 的官网,Codex 支持 Windows 跟 Mac 两大操作系统。官网会根据你的操作系统,自动提供对应的安装包。本期视频我主要使用 Windows 来进行演示。Windows 跟 Mac 电脑的功能基本是一致的,唯一欠缺的功能是 computer use ,也就是自动操作电脑的能力。等需要演示这部分的时候,我会切换到 Mac 电脑进行演示。

然后我们一路点击下一步完成安装。接下来我们把 Codex APP 启动起来,选择 ChatGPT 账户完成登录。现在 ChatGPT 的免费账户也能使用 Codex 了,不过额度比较低。第一次进入软件要选一下希望 Codex 为你处理的工作,Codex 会根据你的选择预装一些内置的插件和 skills 。当然进入软件以后,我们还可以按需安装这些插件。然后选择主要的使用场景,是编程还是日常工作,这些都可以后续在设置里面进行修改。接下来点击设置沙盒按钮,完成沙盒的初始化。关于沙盒这部分的内容,我们下一个章节再来介绍。

项目与任务列表

我们点击右上角的按钮显示侧边栏,我们看到 Codex APP 是非常经典的三栏布局:左侧是任务列表,中间是对话窗口,右侧是多功能区域。

我们先创建两个项目文件夹,来展示一下它的基础使用。这里我在桌面新建了两个文件夹,作为两个项目文件夹。然后我们来到 Codex ,点击进入项目工作,使用现有文件夹。我们先选择第一个文件夹,这里我让它做一个 html 单页面的宠物洗护店的网页,开始。在左侧边栏里面增加了一个项目,项目的名称就是文件夹的名称,里面展示了正在运行的任务。

接下来我们点击左上角的新对话按钮。Windows 上的快捷键是 Ctrl+N ,Mac 系统的快捷键是 command+N ,来开启一个新的对话。我们可以选择新对话属于哪个项目。这里我准备开启一个新的项目,把第二个文件夹也添加进来,点击添加新项目,然后选择我们的第二个文件夹。

这里输入我第二个项目的需求:用 react 框架做一个网页版待办事项的提醒工具,回车开始执行。

在两个项目并行工作的同时,我们还可以开启更多的工作对话。比如这里我想询问 Codex 一个技术问题,我们把鼠标指向第二个项目,点击这个小按钮,在项目里面开启新的对话。

这里我询问 Codex:react 框架是什么,回车。这里看到我们开启了 3 个任务并行执行,有两个任务属于项目一,一个任务属于项目 2 。正在执行的任务上面都有一个转圈的小图标,表示 AI 正在工作。我们耐心等待一会。

过了一会状态就不一样了。有一个任务上面显示一个绿色标签,表示等待批准;有一个任务上面出现了一个蓝色小点,表示已经执行完毕了;还有一个任务继续转圈。我们来到这个等待批准的任务里面,发现 Codex 需要联网下载 Vite React 项目的模板,正在申请权限。

我们点击“是”批准,这个任务就继续执行了。又过了一会,三个任务上面都标记了蓝色的小圆点,表示三个任务都执行完毕了。Codex 任务列表非常的简洁美观好用,可以很方便地观察任务状态,可以并行开启多个工作任务,还能高效地从多个任务里面自由地切换。

我们再来看一下左侧边栏任务列表的其他功能。新对话按钮用来开启新的对话,在下面可以选择对应的项目,也可以选择不使用任何的项目,纯粹的闲聊。这种不属于任何项目的对话,都会被收录到任务列表的最下面,也就是对话这一栏里面。

左侧边栏第二个按钮,快捷键是 Ctrl+G ,Mac 电脑的快捷键是 command+G ,可以搜索近期的对话历史。不过我试了一下,这个功能只能搜索到对话的标题,它无法搜索到对话里面的内容。这里补充一下,每个对话标题都是 AI 根据对话内容自动摘要生成的。我们也可以选择某个对话,双击,对它进行一个重命名。如果我们不再需要某个对话了,可以点击这个归档对话的小按钮确认,然后我们的对话就在左侧边栏消失了。在设置已归档对话里面,可以找到我们删除的对话,点击取消归档就可以把它还原回来。左侧边栏还有两个按钮,插件与自动化,这个等我们在后面的章节再来看。

权限控制与沙箱

接下来我们来看一下中间的对话页面。这里最显眼的功能就是权限控制。

Codex 的权限控制全部是围绕沙箱来展开的,这点跟 Claude Code 有本质上的不同。Claude Code 的沙箱功能需要手动开启,Claude Code 的沙箱更像是一层可以额外开启的保护,而 Codex 的沙箱,它是整个权限系统运行的地基。

Codex 会把当前的项目文件夹作为一个沙箱进行管理。在默认权限下面,Codex 具有读取修改沙箱内所有文件的权限。在默认模式下,Codex APP 可以直接修改沙箱内,也就是项目文件夹的所有文件,它并不会一个个地跑来问你。我觉得这点非常的方便,也是符合正常的使用习惯的。当然我们也可以通过设置,改成逐个文件修改都需要审批,这个在视频的后半段高级设置这个我们再来讲。

Codex 的沙箱有两个默认限制。第一点是 Codex 不能修改沙箱外的文件,第二点是 Codex 的沙箱是禁止联网的。这两点硬性限制,它并不是靠模型自觉遵守,而是 Codex 使用操作系统底层功能实现的。不同操作系统的实现机制是不一样的,比如 MacOS 使用的是系统内置的 Seatbelt Sandbox 机制。

Codex 的沙箱功能是前阵爆火的 Harness engineering 概念的一个典型的工程实现,用操作系统级别的机制把 AI 的能力约束在一个可控的范围之内。这也很形象地体现了 Harness 这个词的原始含义,也就是马具。AI 就像一匹能力很强的马,而沙箱权限审批机制这些,就是套在它身上的马具。

如果 Codex 需要修改沙箱外文件,或者需要联网,可以向用户申请权限,这个操作叫做 escalate ,也就是提权操作。在默认情况下,提权操作都是需要人工审核同意的。Codex 为我们提供了第二个档位,也就是自动审查。启动了自动审查以后,Codex 会自动调用一个小模型,对提权操作进行安全性审查。如果发现是低风险的操作,就会直接放行。只有高风险的操作才会触发人工审查,这也是我最推荐的模式。自动审查使得绝大部分操作不需要人工审批,在获取了较高安全性的同时,还极大提升了使用的便利度。

所以一般情况下,在权限管理这里,我都推荐开启第二档,也就是自动审查。Codex 还有第三档,完全访问权限。开启了这个以后,Codex 完全无视沙箱的限制,可以在电脑上执行一切的操作。不过我们尝试开启的时候,这里出现了很醒目的风险提示,提示我们要谨慎使用。

上下文

在权限控制的右边有一个圆圈,展示的是当前上下文使用情况。这里翻译得不太好,准确的翻译应该是上下文使用量信息,显示的是这个对话里的历史对话内容占用了多少模型上下文空间。当上下文超过限制的时候,Codex 会自动对对话历史进行压缩,从而释放出更多的上下文空间。我们也可以输入斜杠,选择压缩选项,手动触发一次上下文压缩。压缩完成以后,Codex 会把之前对话的一些不重要的内容排除掉,可以有效提高 AI 的专注力,并且降低 TOKEN 消耗。

不过在 AI Agent 领域,有一个通用经验是清空好于压缩。因为过多的历史会话,会干扰 AI 的注意力。当我们让 AI 执行完一个任务以后,最好是开一个新的对话,清空上下文。这样有助于 AI 把注意力全部集中到新的任务上面来,从而提高任务的执行效果。

在上下文窗口的右边是模型选择,可以根据任务的复杂程度选择模型的思考强度。下面可以切换模型,这里一般我们就选择最新的模型,比如现在是 GPT-5.5 。

下面还有一个速度选项,可以选择标准还有快速。在快速模式下,会提升 50% 的 AI 推理速度,但是快速模式会消耗两倍的套餐用量。如果你的任务很急,但是套餐的余量还有很多,可以选择开启。

说到套餐余量,我们可以在左下角的设置剩余额度里面找到你现在的套餐余量。这里有两个限额,分别是 5 小时限额,还有周限额。这两个限额任意一个到达上限,Codex 都不能继续使用了。

两个限额都有对应的重置时间,时间到了以后,额度会重置成 100%。右边还有一个语音输入功能,可以让我们跟 AI 的交互从打字变成口喷,非常的好玩。

AI 生图

Codex 内置了 AI 画图功能,而且它使用的是当今最强的 AI 生图模型 GPT-Image-2 。这是刚才我让 Codex 为我们生成的宠物洗护的网站,我们看到它已经为我们配了一些图片。这里我看了一下,这些图片其实都是网络上的免费素材。这里有两个配图非常的不合适。首先这里的店内环境展示的都是一些宠物图片,它并不是一个真正的宠物洗护店的环境。第二点是门店信息,这里画的地图也太粗糙简陋了。

我们就针对这两个问题,让 Codex 帮我们来修改一下。我们来到 Codex ,新开一个对话,项目选择宠物洗护店那个。我让 Codex 调用 AI 绘图功能,绘制三个店内环境的轮播图,三个图应该分别展示店内的不同区域。

我们看到 Codex 为我们生成了三张图片,基本都保持了店内装修风格的一致性。

然后在网页这边也替换成了 3 张图片的轮播图,非常的不错。

接下来我们要改的是门店信息,这里的地图太简陋了。我们回到 Codex ,在宠物店应用这里新开一个对话,输入我的指令:

我们的店在陕西北路 1620 号,就是地图上标记的这个点。你按我发你的位置,用可爱清新的宠物风格的地图把我们的店标记上,然后修改网页里的门店位置信息。

这里我来到地图,我截个图,然后把我们的店用箭头标记上。接下来我们直接 Ctrl+V ,Mac 系统是 Command+V ,把截图粘贴过来,开始。

在 Codex 的执行过程中,我随时跟踪进度。这里我发现了一个问题,就是他画的这个地图是用 SVG 生成的,效果很差。我原本是想让他调用内置的 AI 生图模型来画。借着这个机会,我要介绍 Codex 的一个强大功能。它的英文名叫做 steer ,中文翻译过来是引导。这个词的英文原文的意思是打方向盘。当我们发现 AI 在执行过程中理解错了我们的意思,就不应该让它继续执行了,这时候应该及时接管方向盘,人工进行引导干预。

这里我截个图发送给 Codex 说:你这图不行,应该调用 AI 绘图能力。

在默认模式下,这个新的指令进入了指令队列排队,需要 AI 把上一轮全部执行完,才能执行我们新输入的指令。我们可以点击这里的引导按钮,英文版叫做 steer ,中途接管方向盘,引导干预 AI 的执行。

我们看到 Codex 这里显示已引导对话,然后回复我说味不对,我立即改用 AI 生图,重新生成一张。我们使用 Codex 的 steer 功能,在运行中途成功纠正了模型的运行方向。

在 Codex 的设置常规设置里面,有一个跟进行为。这里可以设置在执行过程中,我们输入的指令是在后面排队,还是直接进入引导。

这里我推荐还是默认选择排队,如果需要引导的话,我们直接点击那个引导按钮,或者按快捷键 Ctrl+回车。我们看到 Codex 为我们重新生成了一张图片,并且把它替换到了网页里面,标注出了我们店的位置,效果非常的棒。

计划模式与内置浏览器

在对话窗口这里有一个加号,里面有三个功能。首先是添加照片和文件,我们可以用照片或者文件给 AI 补充上下文信息,或者可以像刚才一样通过复制粘贴,直接把照片或者文件粘贴进对话窗口。下面是插件,目前预装了 4 个插件,分别对应办公三大件( Word,Excel,PPT ),还有浏览器自动化。

我们主要看这里的计划模式。开启了计划以后,Codex 就不会立即上手干活,而是先为我们输出一份完整的工作计划,跟我们进行了确认以后再干活。对于所有的复杂任务,建议都先开启计划模式,确保你能跟 AI 对齐颗粒度,让 AI 能够精准理解我们的意图。

我们在 Codex 里面打开计划模式,输入我们的需求:

把这个项目改造成 next js 框架

在计划模式里面,Codex 会很倾向于使用这种问题卡片的形式跟用户进行沟通。这里他询问我希望使用哪种项目形态,我选择 APP Router 加 TS 。然后样式迁移希望怎么处理,这里我选择改 Tailwind 。迁移完成要不要同时启动本地开发服务器验证,这里我选择构建加启动。Codex 为我们生成了一份完整的计划,我仔细阅读了一遍,没有发现问题。这里我们点击是,实施此计划。

代码编写完成以后,Codex 可以启动它内置的浏览器进行自动化的测试,这样任务就完成了,开发服务器也启动起来了。在右侧的多功能窗口,Codex 自动打开了浏览器。我们可以点击这里的展开面板按钮,看一下项目的完整状态。我们看到这次架构迁移非常的成功,页面上所有的元素都完整地保留了,非常的不错。

如果对某个部分不满意,我们可以点击这里的批注按钮,然后选中一个元素,在这里可以添加评论。比如这里我说为什么这个星星是空心的,然后在下面点击发送,让 AI 帮我们修改一下。

在下面也可以实时看到它的修改过程。我们刷新一下页面,这个星星就被修改成了实心的。这样我们就通过 Plan 模式配合 Codex 的内置浏览器,成功完成了项目架构的迁移,还顺手修改了一个小 bug 。

代码管理

Codex APP 并不是传统的 IDE ,它并不提供完整的代码编辑功能。我们可以在右上角点击切换文件树,这里虽然可以查看代码,但是没法直接编辑。我们只能点击某行代码来写批注,并不能直接修改代码。我们可以借助第三方的 IDE 来修改代码。

在进行代码管理之前,我们需要先把项目初始化成一个 git 工程。这里我们新开启一个对话,输入提示词:

把项目初始化成一个 git 工程,注意排除掉不需要的文件。

Codex 先为我们创建了 .gitignore 文件,把一些不需要提交的内容排除出去。Codex 帮我们把项目初始化成了 Git 仓库。

初始化成 Git 仓库以后,右上角又多了很多按钮。这里有一个 VSCode 按钮,我们点击一下,我们就可以快捷地使用 VSCode 来查看和修改代码。

除了使用 VSCode ,在这个下拉列表里面,还可以使用其他的 IDE 来打开项目。如果你的电脑上装过这些 IDE ,就可以在这里关联出来。我们也可以在设置常规设置里面选择默认打开的 IDE 。

我还可以要求 AI 把代码帮我提交到 Github 上面。Codex 需要我先在 Github 上面为它创建一个仓库。这里我来到 Github ,点击这个 new 按钮,仓库的名字还叫 pet_care ,点击创建。

创建出来以后把这个地址复制一下,我们回到 Codex 扔给他,成功帮我们把代码推送到了 Github 上面。我们点击这个链接,看到我们的代码已经备份到了 GitHub 的网站上面。我们可以使用对话的方式进行一切 Git 与 Github 操作,这都属于编程的基础知识,本期视频我就不展开讲了。

Git 回滚

本期视频我主要讲两个进阶技巧。第一个是使用 Git 对开发过程进行回滚,第二个技巧是 Git WorkTree 。

我们先看第一个回滚。这里我新开一个对话,我们打开侧边栏,点击加号,浏览器输入我们项目本地开发的地址,这样进入浏览器。我们还是在这里面进行一些批注。我让 Codex 在这里添加一个期望到店时间的功能,直接点击 Ctrl 加回车发送。Codex 为我们添加了期望到店时间这个字段。

当 AI 完成一个功能的开发以后,我们就使用 Git 把它备份保存一下。这里点击提交,

填写一个提交消息,点击继续。这样最新的代码改动就以 Git 的方式保存下来了。接下来我让 AI 把期望到店时间放到联系人的上面,开始。Codex 为我们完成了修改,我们还是从内置的浏览器看一下效果。这个字段跑到了最上面,不过这么一改,我觉得更难看了,我后悔了。我觉得还是把它放到原来的位置比较好。这里我想做的是把这一次的对话,包括这一次的代码改动,全部回滚掉,最好是当做无事发生。

这里我们先借助 Codex 的分叉功能,英文是 fork 。我们先把对话回滚掉。我们找到上一次对话结尾的位置,点击这个分叉按钮,选择派生到本地。

我们看到 Codex 的分叉功能,就是在我点击的这个位置,把对话复制了一份。这样复制出来的新对话,就已经剔除掉了我们刚才想删除的部分了。

不过分叉功能只能回退对话历史,它不能同步回退代码。所以这里我们要做的是把代码一同回退掉。我们可以在 VSCode 里面点击这个 source control 按钮,查看所有的 Git 提交。这里我需要把代码回退到生成期望到店时间的这个状态上。我们点击右键,点击这个 copy commit hash ,

这样我们就把这次提交的 ID 复制下来了。我们回到 Codex ,先让 AI 把代码回退到这个状态,后面就是我们刚才复制的提交 ID 。

我们看到代码回退成功了,在浏览器里可以看到这个期望到店时间又变回了原来的位置。这样我们就使用了 Codex 的对话分岔功能,加上 Git 操作,成功地把这一次不需要的改动,从代码层面和对话历史层面进行了完全的回滚。

Git worktree

接下来我们来看下一个功能,就是 Git worktree 。WorkTree 这个名字听起来比较唬人,其实它本质上就是用 git 创建一个新的分支,然后把这个新分支的代码完整地复制到一个新的文件夹里面。这个新文件夹就是一个 WorkTree 。主文件夹和分支文件夹可以并行工作,我们可以在两个文件夹里面各自修改代码,互相不干扰。我们可以基于主干创建多个分支,它们在底层通过 git 关联在一起。分支文件夹的改动,随时都能轻松合并回主干。

找到我们的项目,右键创建永久工作树。这里起个名字,我想让第一个工作树专注优化客户评价这个部分,我给它加个后缀叫 customer rating 。Codex 把整个项目复制到了一个单独的文件夹里面,它跟主干已经不是同一个文件夹了。我们在分支里面做的操作不会影响到主干。这里我们再建一个工作树,第二个工作树主要用来负责优化下面的门店信息,还有这里的地图。我们给第二个树也起个名字。这样我们就拥有了两个工作树分支,它们都位于不同的文件夹下面,所以它们之间的并行工作不会影响到主干。

这里我们来测试一下。我们先打开第一个分支新对话,输入我们的需求:

优化一下客户评价部分,多写几个评价,做一个动画轮播效果,

开始。然后我们来到第二个分支,创建一个新的对话:

优化一下门店信息部分,让门店信息跟地图上下排列,不要左右排列,把地图展示全。

我们在两个分支上面进行并行开发,因为它们位于两个不同的文件夹,所以互相之间不会产生干扰。

两个分支在各自的文件夹里面都开发完毕了。接下来我们可以把它合并回主干。里直接输入合并回主干,两个分支都一样的操作,合并回主干。

两个分支都成功合并进了主干。我们在浏览器这边可以看到,客户评价已经优化过了,门店信息也从左右排布变成了上下排布,地图展示得更全了。这样我们使用 Git worktree 功能,高效并行开发了两个任务。当我们的分支使用完毕,我们可以直接右键移除,把两个临时的分支移除掉,然后回到主干继续工作。

今天 HR 通知我被裁了,正式离职的时间是五月份月底,我还差 10 天左右工龄满 5 年,公司给的赔偿是 N=5 ,工龄满 5 年时也不再发放年假。我想咨询一下这是否合理合法?

背景:QUIC 是什么,为什么重要

现在我们访问网页走的几乎都是 TCP + TLS 的组合。这套方案运作了几十年,但问题也积累了几十年:TCP 的队头阻塞、握手延迟、网络切换时连接中断……这些问题在移动网络场景下尤为突出。

QUIC 是下一代网络传输协议,由 Google 最初提出,现已进入 IETF 标准化流程。它运行在 UDP 之上,内置 TLS 1.3 加密,默认加密传输,同时解决了 TCP 的多项历史遗留问题:

  • 多路复用且没有队头阻塞(一个流的丢包不会卡住其他流)
  • 连接迁移(换网络不断连)
  • 握手延迟更低(0-RTT 或 1-RTT 建连)

HTTP/3 就是跑在 QUIC 之上的。可以说,QUIC 是当前 Web 传输层最重要的变革之一。


quiche:Cloudflare 的 Rust 实现

Cloudflare 在 2019 年开源了自己的 QUIC 协议实现,叫做 quiche(没错,就是法式咸派的拼法,和 QUIC 谐音)。这个库用 Rust 编写,托管在 GitHub 上。

这篇文章就是介绍 quiche 的设计思路,以及他们在实现过程中做的几个关键决策。


设计原则:只做协议,不碰 I/O

quiche 的核心设计原则是:库本身只处理 QUIC 协议逻辑,不碰网络 I/O

具体来说,quiche 的 API 负责:

  • 接收应用层传入的 UDP 数据包,解析处理
  • 生成需要发出去的 QUIC 数据包,交还给应用层

但它不负责:

  • 打开 socket
  • 读写网络
  • 管理事件循环

这么做的原因很实际:Cloudflare 的边缘网络栈大量代码是 C 写的,不可能把整个栈换成 Rust。quiche 需要能嵌入到 C 的环境里用,所以在 Rust API 之上额外暴露了一层 C 接口头文件,方便 C/C++ 及其他语言集成。

这个设计带来了很好的可复用性。quiche 被集成进了 Cloudflare 内部的 NGINX fork,也被 cURL 用来实验性地支持 QUIC。同时你也可以直接用它写 Rust 的 QUIC 客户端和服务端。

定时器的处理也遵循同样的思路:quiche 告诉应用层"你需要在 X 时刻醒来"(比如用于重传超时),但不自己管理定时器。应用层可以选择任何适合自己环境的方式来实现。


TLS 层:用 BoringSSL,但有讲究

QUIC 协议的握手直接使用 TLS 1.3,而不是像 HTTPS 那样 TLS 跑在 TCP 上。这带来了一个集成上的微妙问题。

普通 TLS 库的工作方式是:TLS 握手消息先经过 TLS 记录层的封装和保护,再交给传输层发出去。但 QUIC 不需要这一层,因为 QUIC 自己负责加密和传输,TLS 握手消息直接承载在加密的 QUIC 数据包里。

所以 quiche 需要一个能把原始 TLS 握手消息"暴露出来"的 TLS 库,不要记录层封装,不要额外保护。

Cloudflare 选择了 BoringSSL(Google 维护的 OpenSSL fork,他们几年前就迁过去了)。BoringSSL 专门为 QUIC 实现提供了对应的 API 接口,quiche 通过 Rust 的 FFI 机制调用它。


密码学层:ring 库,以及一个有意思的贡献

TLS 握手之外,QUIC 还需要处理数据包级别的加密。这部分 quiche 用的是 ring,一个 Rust 生态里很流行的密码学库,提供安全且高效的加密原语。

ring 和 BoringSSL 其实共享了部分底层的高性能密码学算法实现,但 ring 对外暴露的是更符合 Rust 风格的安全 API。

这里有一个细节值得说:QUIC 的加密方式比较特殊。数据包的 payload 加密用的是标准的 AEAD 算法(AES-GCM 或 ChaCha20-Poly1305),这很常见。但数据包 header 的保护用了一套专门为 QUIC 设计的机制,目的是防止网络中间设备(middlebox)读取包头里的元数据(比如包序号)。

Header 保护所需的密码学原语,在 ring 库里原本没有。quiche 团队为此给 ring 提了 PR,把这部分补上了,最终随 ring v0.14.0 正式发布,成为开源贡献的一部分。


现状与后续

quiche 第一次提交距这篇文章发布只有三个月,但已经能和其他更成熟的 QUIC 实现互通,验证了协议的大多数核心特性。

作者也坦诚地说:quiche 和 QUIC 本身一样,都还没有"完成"。协议在标准化过程中还会继续演进,实现里的 bug 会被发现和修复,API 也可能随着经验积累而调整。


几点值得关注的设计取舍

读完这篇博客,有几个设计决策值得单独拎出来说:

不碰 I/O 的边界划分是最核心的一条。这个决定让 quiche 既能在 Rust 项目里用,也能被 C 栈集成,大幅拓宽了使用场景。对于要写协议库的人来说,这是一个很值得参考的思路:把协议状态机和 I/O 彻底分开。

FFI 集成是 Rust 库嵌入 C 生态的标准路径。quiche 在 Rust API 外单独维护了一个 C 头文件,这个做法在需要跨语言集成时很常见,Rust 生态里这类库不少。

给上游库贡献缺失的原语,而不是自己另起炉灶——这在工程上是更好的选择,维护成本更低,也让整个生态受益。


小结

quiche 这个项目本身不复杂,代码量也不大,但它背后的工程判断值得细品:清晰的边界、务实的语言选型、对上游的贡献而非 fork。

对于关注网络协议、Rust 系统编程或者基础设施工程的人来说,quiche 的源码是个不错的阅读材料。QUIC 已经是现实中跑在生产上的协议,HTTP/3 的普及也在持续推进,理解它的实现细节,会越来越有用。

原文地址:https://blog.cloudflare.com/enjoy-a-slice-of-quic-and-rust/

背景

Cloudflare Workers 是 Cloudflare 提供的 Serverless 运行平台,代码运行在全球 150+ 个数据中心的边缘节点上。它原生支持 JavaScript,而随着 WebAssembly(WASM)支持的加入,Rust 开发者也可以把自己的代码编译成 WASM,部署到这套平台上运行。

这篇文章以一个实际项目为例,介绍如何把 Rust 代码编译为 WASM,先在本地浏览器中跑通,再上传到 Workers 作为 Serverless 函数对外提供服务。

原文地址:https://blog.cloudflare.com/cloudflare-workers-as-a-serverles...


什么时候适合用 WASM

在开始之前,有一点需要明确:WASM 不是万能的。

Cloudflare 官方文档中有一段很务实的说法:对于轻量任务,比如做一次请求重定向、校验一个 Token,纯 JavaScript 往往比 WASM 更快、更简单。原因在于 WASM 运行在独立的内存空间里,数据进出都需要拷贝,如果代码本身没有密集的计算,引入 WASM 反而会带来额外开销。

WASM 真正发挥优势的场景是计算密集型任务:图像处理、加解密、复杂的字符串操作等。


环境搭建

Rust 的 WASM 工具链目前已经相当成熟,核心工具是 wasm-pack

# 安装 wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

# 安装 cargo-generate,用于基于模板创建项目
cargo install cargo-generate

# 用官方模板创建一个新项目
cargo generate --git https://github.com/rustwasm/wasm-pack-template

wasm-pack 的作用是把 Rust 代码编译成 WebAssembly,同时生成 JavaScript 和 Rust 之间的类型绑定(binding)。这个绑定层很关键,后面会详细说。


代码结构:#[wasm_bindgen]

模板项目的核心模式是这样的:

#[wasm_bindgen]
extern {
    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet() {
    alert("Hello from Rust!");
}

这里做了两件事:

第一,通过 extern 块声明外部函数——也就是宿主环境(浏览器或 Workers)提供的函数,比如 alert

第二,用 #[wasm_bindgen] 标注的 pub fn,会被暴露出去,让 JavaScript 侧可以直接调用,就像调用普通 JS 函数一样。

编译命令:

wasm-pack build

编译产物在 pkg/ 目录下,包含 .wasm 二进制文件和自动生成的 JS 胶水代码。


本地验证

编译完成后,先不急着上线,在本地浏览器里跑通是个好习惯。

npm 上有一个 create-wasm-app 模板,提供了一个预配置好 webpack 的测试页面,可以直接 import WASM 模块:

npm init wasm-app www
cd www
npm install
npm start

浏览器访问 http://localhost:8080,如果看到页面正常渲染,说明 WASM 模块加载成功。

把自己的 wasm 包用 npm link 关联进来,修改 www/index.js,调用自己的函数:

import * as wasm from "my-wasm-module";
let result = wasm.get_phrase_text(100, 10);
console.log(result);

一个坑:系统调用在 WASM 里不可用

作者在实际开发中踩了一个典型的坑:用 Rust 的随机数库 SmallRng::from_entropy() 时,本地 cargo test 完全正常,但在浏览器里跑 WASM 时直接崩溃。

原因是 from_entropy() 底层依赖系统调用来获取熵值,而 WASM 的编译目标是 wasm32-unknown-unknown——这个 unknown 意味着目标平台不保证提供任何系统调用。编译器不报错,但运行时会直接失败。

解决方案是换掉系统级调用,改用 JavaScript 宿主提供的 Web API。Rust 有一个 js-sys crate,封装了标准 ECMAScript 提供的所有全局对象,其中包括 Date

fn get_rng() -> SmallRng {
    use js_sys::Date;
    use rand::SeedableRng;

    let ticks = Date::now();
    let tick_bytes = transmute(ticks as u128);
    SmallRng::from_seed(tick_bytes)
}

Date.now() 作为种子,绕开了系统调用,问题解决。

这个经验值得记住:在 WASM 环境下,任何涉及 I/O、系统熵、文件系统、时间获取的操作,都需要通过宿主环境(JS)来代理,不能直接走 Rust 标准库的对应实现。


上传到 Workers

本地浏览器跑通之后,接下来把 .wasm 文件上传到 Cloudflare Workers。

上传时把 WASM 绑定到一个全局变量(比如 BOBROSS_WASM),Workers 运行时会在 Worker 脚本启动时自动实例化这个模块。

但这里有一个需要手动处理的地方:wasm-pack build 生成的 JS 胶水代码是为浏览器环境写的(使用了 ES module 的 import/export 语法),Workers 的 WASM 实例化方式和浏览器略有不同,需要做几处改造:

  • 删除顶部的 import 语句
  • 去掉函数的 export 关键字
  • 把所有函数包进一个模块对象
  • 构造 importObject,把需要注入的外部函数传进去
  • 在创建 WebAssembly.Instance 时传入这个 importObject

改造完成后,在 Worker 脚本里调用 Rust 函数的方式和调用普通 JavaScript 函数没有区别:

async function handleRequest(request) {
    let url = new URL(request.url);
    let phraseCount = parseInt(url.searchParams.get("phrases") || 100);
    let newLine = parseInt(url.searchParams.get("newline") || 0);

    // 调用 Rust 编译的 WASM 函数
    let phraseText = mod.get_phrase_text(phraseCount, newLine);
    return new Response(phraseText);
}

请求进来,Rust 函数被调用,结果直接返回——运行在全球 150+ 个边缘节点上。


整体流程回顾

Rust 源码
    ↓ wasm-pack build
.wasm 二进制 + JS 胶水代码
    ↓ 本地 npm link + webpack 测试页
浏览器验证通过
    ↓ 手动改造 JS 胶水代码(适配 Workers)
上传 .wasm + Worker 脚本到 Cloudflare
    ↓
全球边缘节点运行

小结

这篇博客展示的是一条完整的路径:Rust 代码 → WASM → Cloudflare Workers。整个工具链在当时(2018年)还比较初期,需要手动处理 JS 胶水代码的适配。现在 Cloudflare 已经提供了 Wrangler CLI,把这些工作都封装进去了,流程更加顺滑。

几个值得关注的核心点:

WASM 不是银弹,轻量逻辑用 JS 就好,WASM 适合计算密集型场景。

系统调用在 WASM 里不可用,需要通过 js-sys 等工具桥接宿主环境的 Web API。

胶水代码是关键中间层wasm-bindgen 自动处理了 JS 和 Rust 之间的类型转换和内存管理,理解它的工作方式对排查问题很有帮助。

Serverless + Rust + WASM 这条路是通的,而且随着工具链的持续完善,门槛在逐步降低。

  • 背景描述:使用android studio连接真机调试的时候,使用终端指令(如adb logcat -d)无论如何看不到有日志输出

    # 首先我们需要验证是否真连接上真机
    PS C:\android\project> adb devices                 
    List of devices attached
    49a050fc        device
    
    # 先清理之前可能存在的日志,该命令不会输出任何东西
    PS C:\android\project> adb logcat -c
    
    # 然后执行下面指令,该指令尝试验证logcat能否正常获取日志
    # 理论上执行后应该有输出,但实际直接无任何输出并一直卡在那
    PS C:\android\project> adb logcat -d | Select-Object -First 10
    

1.背后逻辑和解决办法

国内太多系统存在基于原版安卓系统的魔改,批量搜索实际上你能发现除了MIUI其他系统大多也需要手动更改某些选项才能开启原生安卓系统的logcat日志记录。接下来说解决流程

1.确认USB调试基础设置
  • 进入手机设置 → 关于手机 → 连续点击“MIUI版本”7次以开启“开发者选项”
  • 返回设置主界面,进入“更多设置” → “开发者选项”
  • 确认“USB调试”开关已开启
  • 连接USB后,下拉通知栏,点击USB连接提示,选择“文件传输”或“MIDI”模式
2.检查设备授权与ADB连接状态

输入adb devices命令查看是否正常连接

# 连接上之后能正常输出设备列表
PS C:\android\project> adb devices                 
List of devices attached
49a050fc        device

如果设备显示为unauthorized,请拔插USB线,并在手机弹出的授权提示中点击“允许”

3.硬件与驱动层面:数据线与电脑驱动问题
  • 无连接反应:说明数据线仅充电不支持数据传输,极有可能是买的第三方线太劣质,需要去更换为支持数据传输的数据线
  • 设备无法识别:缺少USB驱动,需要安装ADB驱动安装工具,一般来说安装好android studio之后是直接就有adb工具了
4.开启系统级日志记录开关
  • 进入 设置 -> 更多设置 -> 开发者选项
  • 找到 “日志记录器缓冲区大小” (Log buffer size),建议设置为 16M 或更大
  • 找到 “日志记录级别” (Log level),确保选择了 Verbose
  • 部分版本可能需要:找到 “开启开发者日志” (Enable developer logs) 开关并打开。部分 MIUI 版本在打开此开关后会提示重启手机,请按提示操作

接下来一般就可以adb logcat获取日志了,你可以执行以下命令,如果执行后有输出说明搞定了

# 使用以下命令查看日志输出级别:
adb shell setprop persist.log.tag

若无输出,可尝试设置全局日志级别:

adb shell setprop persist.log.tag "*:V"

然后再执行以下命令,有输出说明已经成功获取日志,大功告成

# 导出所有日志,并输出前面10行(此命令windows用户有效,其他设备用模型自行查找)
adb logcat -d | Select-Object -First 10

本项目完全免费。请不要相信任何使用本项目包装出来的付费服务。

项目地址: https://github.com/Fokkyp/SoftwareCopyright-Skill

软件著作权申请本身不神秘,真正麻烦的是整理材料:申请表字段要写对,操作手册要像样,代码材料要按规则截取,软件名称、版本号、页数还要保持一致。很多开发者最后会把这件事交给付费代办或资料整理服务,花钱买的往往也只是这些文档整理工作。

这个 skill 的目标很直接:让开发者不用再为整理软著材料额外付费,也不用把项目代码和产品细节交给外部商家来回沟通。把真实项目交给 Codex ,它会按流程引导你确认关键信息,并在本地生成一整套可检查、可修改、可提交前再导出的软著申请资料。

  • 自己生成整套资料:从项目分析、业务理解、申请表信息、操作手册到代码材料,一套流程跑完,不再依赖外部代办整理文档。
  • 从真实源码抽取代码:代码材料只来自开发者已有项目,禁止 AI 编造源码,适合对材料真实性敏感的开发者。
  • 自动处理前 30 页 / 后 30 页规则:源码足够时按常见鉴别材料要求生成前 30 页和后 30 页;不足 60 页时按规则生成全部代码材料。
  • 操作手册不套模板:先理解项目业务、页面和功能,再写面向审核员的操作说明,避免只有空泛功能列表。
  • 申请表字段集中整理:软件名称、版本号、著作权人、开发环境、运行环境、源程序量、功能说明等字段统一生成到 申请表信息.txt,官网填报时可以对照复制。
  • 关键节点都让你确认:业务口径、申请表字段、代码选择、截图方式、最终 Markdown 草稿都会停下来让开发者确认,减少材料写偏的风险。
  • Word/TXT 一键输出:确认后生成操作手册 DOCX 、代码材料 DOCX 和申请表 TXT ,文件统一放在 软件著作权申请资料/正式资料/
  • 本地生成,资料可控:默认在当前项目目录生成材料,代码、文档和草稿都留在本地,方便开发者自行审阅、修改和归档。
  • 提供完整 demo:仓库内提供 生成 demo/软件著作权申请资料/,可以直接点击查看生成后的草稿、正式资料和填报辅助文件。

下载并安装

推荐按下面顺序操作。

第一步:下载代码

会用 Git 的用户执行:


git clone https://github.com/Fokkyp/SoftwareCopyright-Skill.git

cd SoftwareCopyright-Skill

不会用 Git 的用户:

打开 GitHub 仓库页面,点击 Code,再点击 Download ZIP。下载后解压,进入解压出来的目录。

进入目录后,应能看到这个文件夹:


software-copyright-materials/

第二步:安装到 Codex

software-copyright-materials/ 复制到 Codex 的 skills 目录:


mkdir -p ~/.codex/skills

cp -R software-copyright-materials ~/.codex/skills/

安装完成后,应看到:


~/.codex/skills/software-copyright-materials/SKILL.md

第三步:重启 Codex

重新打开 Codex 会话或刷新技能列表,然后在项目中提出“生成软著申请资料”等请求即可使用。

使用截图:

使用截图

生成效果图:

生成效果图

官网填报和提交

官方入口:

官方页面可能会调整,实际填报时以官网当前页面为准。

著作权申请表填写示例

著作权申请表按照以下图片填写。

著作权申请表

生成文件怎么用

申请表信息.txt 是填报辅助文件,用来帮助开发者在官网填写申请表,不是直接上传的申请材料。

docx 文件是本地编辑稿,方便开发者在 Word 、WPS 或 Pages 中继续修改。提交官网前,请将需要上传的 docx 文件导出或另存为 PDF ,再按官网要求上传。

实际文件名会包含软件名称。通常需要转换为 PDF 的文件包括:

  • 操作手册.docx

  • 代码(前 30 页).docx

  • 代码(后 30 页).docx

  • 不足 60 页时生成的全部代码材料

申请人身份证明、权属证明、委托材料等其他文件,请按官网页面要求另行准备并上传。

昨天晚上往 gitlab 提交代码的时候,提示我没有权限。我打开 gitlab 的网页发现账号被封了。翻了翻邮件看到一周之前 gitlab 给我发了个邮件,让我迁移,说中国区的账号 gitlab 不再支持了,但我最近没有查看邮件,现在 gitlab 登不上去了,代码拿不到了。

我是 14 年前就开始使用 gitlab 了,当时 github 没有免费的私有仓库,我就把一些个人项目放在 gitlab 上,后来也就没怎么管了,一些项目一直放在 gitlab 上。

gitlab 最近一周我提交代码的时候没有收到任何异常和消息,现在突然账号就没封禁了,我最近一周打开网页也没有看到任何网页上的提示。就发一封邮件,然后 7 天之后就把账号封了,没见过这么坑的。现在我发邮件给 gitlab 都不知道往哪里发。

大家如果有代码放在 gitlab 上的,尽快把代码迁移到 github 上去吧。

开发者朋友们大家好:

这里是 「RTE 开发者日报」,每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE(Real-Time Engagement) 领域内「有话题的技术」、「有亮点的产品」、「有思考的文章」、「有态度的观点」、「有看点的活动」,但内容仅代表编辑的个人观点,欢迎大家留言、跟帖、讨论。

本期编辑:@koki、@鲍勃

01 有话题的技术

1、Hugging Face 发布 smol-audio:针对本地音频模型微调与多模态检索的开源工具集

Hugging Face 开源了名为 smol-audio 的代码库,其中包含一系列 Notebook 和脚本。该工具包直接为开发者提供了一套开箱即用的脚手架,用于在本地环境中基于前沿音频模型进行二次开发、微调部署与多模态检索。

主流语音大模型微调支持:内置完善的脚本,支持对 Whisper、Parakeet、Voxtral 以及 Granite Speech 等前沿本地语音模型进行直接微调。

Audio Flamingo 3 适配:针对多模态音频语言模型 Audio Flamingo 3,同步提供了全量参数微调(Full fine-tuning)与 LoRA 参效微调的完整代码实现。

对话级 TTS 部署:工具库集成了 Dia-1.6B 模型,开发者可直接调用并运行对话级文本转语音任务。

零样本多模态检索:接入 Meta 的 PE-AV 模型,支持开箱即用的零样本(Zero-shot)视频与音频到文本(video + audio↔text)的双向跨模态检索。

https://github.com/Deep-unlearning/smol-audio

(@Tu7uruu\@X)

2、GPTImage2 成为赛博半仙,给马斯克看面相

在消耗了无数张 GPU 资源、烧掉了够几座城市用一年的电力之后,OpenAI 最新推出的 AI 生图大模型 GPT-Image-2,再次迎来了它人生中的高光时刻——给人类看手相/面相。

只要拍一张自己手掌的高清照片发给 GPT-Image-2,再附上一段简单的 Prompt,它就会化身天桥底下的赛博半仙,为你生成一份排版精美、用词考究的掌纹性格与职业指南。这场由 AI 爱好者 Linus Ekenstam 率先发起的趣味测试,迅速演变成全网算命狂欢。

连 Reddit 联合创始人 Alexis Ohanian 都没忍住,乖乖把自己的手掌特写交给了 AI。然后心满意足地领走了一个「适合创业的务实理想主义者」高帽标签。

除了看手相,甚至还有看面相的版本。世界首富马斯克被测出了「理性,克制,稳健」。

不过,其实手掌、指纹属于敏感生物特征数据,随意上传公开存在泄露与滥用风险。同时,这类分析仅为娱乐参考,并非科学判断。

(@APPSO)

02 有亮点的产品

1、SOLO 上线桌面/网页端语音交互功能:支持结构化转录与功能直调,同步发布 TRAE × 影石 Insta360 联名 Mic Air

字节跳动旗下生产力工具 SOLO(TRAE)正式在桌面端与网页端集成语音输入功能,由火山引擎提供技术支持。该功能通过 AI 实现口语到结构化文本的自动整理,并支持通过自然语言直接调用产品内部命令(Command),旨在将语音转化为可执行的工作流指令

  • 智能结构化转录算法:该功能不仅限于 STT(语音转文本),内置 AI 逻辑可自动剔除语气词、重复表达,修复语法错误,并能识别用户的自我修正(如「不是…是…」),直接输出逻辑清晰的文本内容。
  • 语义指令直调(Action-mapping):支持通过自然语言触发产品功能,目前已打通 /Plan、/Skills 等内部命令。用户无需手动输入特定字符,即可通过日常表达实现复杂功能调用。
  • 高采样率硬件协同:联名款 Mic Air 采用 48KHz 采样率与全指向拾音方案,通过 USB-C 接收器实现低延迟传输,并集成硬件级 AI 降噪模块,优化工位及嘈杂环境下的指令识别率。
  • 流式实时交互(Beta):即将上线实时语音问答功能,支持流式转录字幕同步呈现。对话结束后可自动生成结构化「会议纪要」,并支持将纪要内容直接转化为后续任务节点。
  • 混合识别能力:支持中英双语及中英混合识别。单次录音时长上限为 15 分钟,支持整理后二次编辑。

(@TRAE.ai)

2、「数字孙辈」记忆小舟:面向老年人的生活史数字化存档工具,支持非线性方言对话与结构化档案生成

中国传媒大学「银发记忆工程」团队推出「记忆小舟」系统。该产品以硬件终端为入口,通过「数字孙辈」智能体实现对老年人非线性、多方言口述史的自动化采集、语义理解与结构化整理

  • 非线性对话鲁棒性: 针对老年人交流中常见的逻辑停顿、重复、叙述跳跃(Out-of-order narration)等特征,系统舍弃了传统问答式逻辑,允许 AI 追随用户节奏并实时记录线索,后期通过后端模型进行时序与逻辑重组。
  • 跨 session 长效记忆与上下文关联: 智能体具备识别并关联跨轮次对话信息的能力。系统能提取既往谈话中的关键人物和事件作为后续对话的触发锚点,提升交互的拟人性与连续性
  • 情感阈值与动态反馈机制:系统具备特定情感识别能力,在涉及负面情绪(如亲友离去)时,智能体可触发「静默陪护」模式,主动调整交互策略,避免因过度追问导致的伦理风险。
  • 方言适配与语料结构化:支持特定方言环境下的语音识别与转译。系统最终产出的不仅是转录文本,而是可供文化学术研究使用的结构化语料库与数字记忆档案。

( @APPSO)

3、蚂蚁灵光将世界模型搬上移动端,一图即可生成可交互 3D 场景

昨天,蚂蚁灵光 App 正式上线「体验世界模型」功能,成为业内首个可在移动端体验世界模型的 AGI 产品。用户只需上传一张图片,即可在手机上最长 60 秒探索 AI 即时生成的 3D 世界

在交互设计层面,灵光针对移动端用户习惯引入了手游摇杆操控方式—— 屏幕左侧摇杆控制角色在 3D 场景中的位移,右侧摇杆控制视角旋转,操控逻辑与主流 3D 手游高度一致,无需额外学习即可上手。

针对移动端世界模型算力需求大、延迟控制难、终端性能参差不齐等挑战,灵光团队采用高效低延迟的流式传输技术,将响应延迟压缩至百毫秒级

(@APPSO)

4、Helio 发布 AI 原生协作平台:构建具备独立 Context 与权限体系的「AI 同事」矩阵

AI 劳动力平台 Helio 正式上线,提出 「AI 原生原住民」 概念,将 AI 智能体(Agent)深度嵌入组织架构。通过赋予 AI 独立身份、实时同步全维度 Context 以及建立分级授权护栏,Helio 旨在实现从「被动响应工具」到「主动执行同事」的任务流转化降低人类在多智能体环境下的决策负荷

  • 统一身份与全维度 Context 整合:AI 智能体拥有独立邮箱、头像及通讯录身份。系统打破沙箱限制,允许 AI 实时感知邮件线程、IM 聊天记录、文档及日历数据,实现「团队背景」与「角色定位」的高保真对齐。
  • 事件驱动型(Event-driven)自主执行:摒弃对话框交互逻辑,AI 基于时间戳和事件监听(如新邮件、订单状态变更、日历到点)主动发起任务。支持跨角色协作,例如产品 AI(Wave)与研发 AI(Coda)可自主完成从需求确认到代码测试的闭环
  • 「三重护栏」安全治理架构:工具白名单管控 AI 自主安装/调用新技能的边界。不可逆任务审批,针对支付、发送重要外部邮件、生产环境部署等高风险动作,强制引入 Human-in-the-loop(人工审批)。动态授权机制:提供 Trust(全自动免审)、Always(每次必审)、Onetime(单次授权)三档权限旋钮。
  • Context 连续性优化决策成本:通过自动整理历史记录与任务前因后果,解决「上下文断裂」导致的决策疲劳。人机交互重心从「输入指令」转向「审阅结果」与「关键点拍板」。

参考链接:https://www.helio.im/

(@Z Potentials)

03 有态度的观点

1、声网冯晓东:当供应链走向成熟,「感官体验」将成为硬件产品体验和商业化核心突破口

声网 Physical AI 产品负责人 冯晓东(右)

随着人形机器人在半马赛事中大幅打破人类纪录并超越老牌企业,机器人硬件供应链的成熟度已得到验证。声网 Physical AI 产品负责人冯晓东指出,行业正经历从「技术驱动」向「价值体验驱动」的拐点。硬件本体决定了机器人的能力下限,而以音视频交互为核心的「感官体验」将成为决定产品体验上限和商业化差异的核心突破口

过去,市场普遍认为机器人的运动控制和结构设计是难以逾越的壁垒。然而,跨界入局的产品(如荣耀「闪电」机器人)在不到一年时间内便在半马赛事中超越深耕十年的老牌企业。这一现象标志着机器人底层逻辑被改写:当硬件不再是核心瓶颈,市场对机器人的追求将从「跑得快、动得稳」转向「听得懂、看得懂、融得进」

2025 年机器人产业已走过「认知启蒙」阶段,正式迈入「场景落地」与商业变现期。以自然语言交互为核心的陪伴、服务类机器人率先爆发。例如珞博「芙崽」陪伴机器人不仅销量破 25 万,更成功实现了用户为「流畅 AI 对话体验」买单的订阅制付费。消费端正从「功能尝鲜」转向「体验依赖」,大模型推动人机交互从图形界面(GUI)正式向对话式交互(CUI)跃升

尽管云端大模型赋予了 AI 强大的「大脑」,但终端设备在复杂物理世界中仍面临严重的「感官短板」(如噪音干扰大、响应滞后、无法自然打断等)。

对此,声网提出专注打造「感官智能底座」的解法。自 2024 年 10 月起,声网推出对话式 AI 引擎(Conversational AI Engine),系统解决环境降噪、人声分离、优雅打断及低延迟传输等痛点。同时通过推出R1/R2 系列开发套件,声网帮助硬件以极低的功耗和小体积,实现从「能听会说」到「能看会动」的阶梯式升级,为 AI 装上拟人化的感知中枢。联合多家芯片原厂搭建 AOSL 开放生态,降低开发者接入门槛,不做硬件竞争者,只做行业「最可靠的感官底座」。

中国具身智能产业正站在全球浪潮的前沿。未来的机器人不仅要「跑赢数值」,更要拥有「灵魂」。随着「感官短板」被不断补齐,自然流畅的多模态交互将让 AI 真正走出「黑屋子」,全面融入人类的美好生活

( @凤凰网)

阅读更多 Voice Agent 学习笔记:了解最懂 AI 语音的头脑都在思考什么

写在最后:

我们欢迎更多的小伙伴参与 「RTE 开发者日报」 内容的共创,感兴趣的朋友请通过开发者社区或公众号留言联系,记得报暗号「共创」。

对于任何反馈(包括但不限于内容上、形式上)我们不胜感激、并有小惊喜回馈,例如你希望从日报中看到哪些内容;自己推荐的信源、项目、话题、活动等;或者列举几个你喜欢看、平时常看的内容渠道;内容排版或呈现形式上有哪些可以改进的地方等。

作者提示: 个人观点,仅供参考

我很喜欢在纸上画思维导图,还有圈圈点点。目前使用纸的缺点是不好收纳,我想购入一块平板。我的需求就是记笔记,没有游戏需求(游戏一般在电脑玩),求推荐一款便宜的平板

生产里真正有分量的工作流是能批量处理几千份保险理赔、跑完一周的销售触达节奏、跨系统对账等等的复杂工作,而这些是没办法塞进一次对话轮次里。因为他们的处理时间以天为单位,而不是秒。

一旦动手做这类长时运行的 agent,会遇到一个问题:大多数 agent 架构本质上是无状态的,每次交互都从数据库里把 context 重新拼回来。拼回来的过程中推理链丢了,软信号也丢了,那些让前一步决策看起来合理的置信度梯度也丢了。

Cloud Next 26 上 Google 宣布 Agent Runtime 开始支持长时运行 agent,状态可以维持七天。围绕这次发布,本文整理出五种把生产系统和脆弱 demo 区分开来的设计模式。

Fleet Orchestration

跨天工作流里最常见的崩盘方式就是 context 丢失。一个 agent 花四个小时处理 400 份文档,跑到第 301 份时报错——没有 checkpointing,整个流程要从零再来。

长时运行 agent 必须把执行状态持久化到一个安全的 cloud sandbox 里。agent 拥有 bash 命令权限和一个沙箱化的文件系统,所以可以把中间结果落到磁盘,把处理日志写下来这样故障时也能体面地恢复。

所以这里就需要把 agent 当成一个长期跑着的服务进程,而不是一次请求处理函数。就像写一条要吞掉几百万条记录的数据管道——进度要 checkpoint,部分失败要兜住,幂等性要保证。

每 30 份文档落一次盘,是在持久性和计算开销之间一个比较舒服的折中。出错时agent 直接从最近一次保存的状态接着跑。

# Pattern 1: Checkpoint-and-Resume
def process_documents(docs, checkpoint_file="state.json"):
    state = load_checkpoint(checkpoint_file) or {"processed": 0, "results": []}

    for i in range(state["processed"], len(docs)):
        try:
            result = agent.analyze(docs[i])
            state["results"].append(result)
            state["processed"] = i + 1

            # 每 30 份文档执行一次 checkpoint
            if (i + 1) % 30 == 0:
                save_checkpoint(checkpoint_file, state)

        except Exception as e:
            save_checkpoint(checkpoint_file, state) # 在崩溃前保存
            raise e

    return state["results"]

Delegated Approval(Human-in-the-Loop)

几乎每个框架都在宣传自己支持 human-in-the-loop。但落到实现层面,多数版本都很糙:把状态序列化成 JSON,发一个 webhook 出去,剩下的就指望有人会去看。

问题会越积越多。

JSON 序列化会丢掉那些隐式的推理 context;通知则要和几十条其他告警一起抢眼球。等到几小时后终于有人回过头来响应,agent 又得反序列化、重建 context,并默认期间一切照旧——这是个非常脆弱的假设。

长时运行 agent 的处理方式不一样。走到审批节点时,它就在原地暂停。整套执行状态完整保留下来:推理链、工作记忆、tool call 历史、待执行的动作,全都不动。

关键在于资源效率。agent 等 24 小时让人来点 Approve,这 24 小时对 agent 是死时间,对人是工作时间。暂停期间 agent 不消耗任何计算;亚秒级冷启动让恢复几乎没有延迟代价。

# Pattern 2: Delegated Approval
@agent.tool
def request_human_approval(action_plan: dict, context: str):
    """暂停 agent 执行并请求人类审核。"""
    approval_id = db.create_approval_request(
        plan=action_plan,
        context=context,
        status="pending"
    )

    # 将执行控制权交还给 orchestrator
    # 在 webhook 触发之前,agent 消耗 0 计算资源
    raise SuspendExecution(
        reason="human_approval_required",
        resume_webhook=f"/api/resume/{approval_id}"
    )

Memory-Layered Context

跑七天的 agent 需要的远不止 session 状态。它得记住前几次 session 里的东西、几周前用户留下的偏好,以及那些任何单次对话都装不下的组织信息。

Memory-Layered Context 把长期存储和工作记忆拆开,由严格的策略来管。

分层记忆在这里就变成必需品。一边是 Memory Bank——长期记忆,动态地把对话整理沉淀下来;一边是 Memory Profiles——工作记忆,专门服务那些低延迟、高准确度的细节查询。

但其中藏着一个坑:记忆漂移。agent 从少数几次非典型交互里"学到"某个程序性捷径可以接受,接下来可能就把这个捷径泛化到所有场景。多个 agent 共用同一个记忆池时,数据泄漏也是个真实存在的风险。

不能放任 agent 随便往向量数据库里写东西。治理它们的方式应当和治理 microservices 一样,由三个核心组件支撑:

Agent Identity:相当于 agent 的 IAM,决定它能访问哪些 memory bank 和 tool。

Agent Registry:相当于服务发现,记录哪些 agent 在跑、它们的 prompt 版本是哪一版、当前处于什么执行状态。

Agent Gateway:相当于 API gateway,根据组织策略评估每一次请求——比如阻止 agent 把 PII 写入长期记忆。

# Pattern 3: Memory-Layered Context
class AgentGateway:
    def __init__(self, identity_provider, policy_engine):
        self.iam = identity_provider
        self.policies = policy_engine

    def write_to_memory_bank(self, agent_id, data):
        # 1. 验证身份
        if not self.iam.can_write(agent_id, "long_term_memory"):
            raise UnauthorizedError()

        # 2. 执行策略(例如 PII 脱敏)
        safe_data = self.policies.redact_pii(data)

        # 3. 写入受管理的存储
        vector_db.upsert(
            collection="memory_bank",
            metadata={"source_agent": agent_id},
            content=safe_data
        )

Ambient Processing

不是所有长时运行 agent 都和人类有交互。还有一类是 ambient 的:盯着事件、消费数据流、在后台直接动手,全程不需要任何用户提示。

Ambient Processing 让 agent 在无人值守状态下持续运行,按事件触发动作。

举个例子:一个直接挂在

Pub/Sub

流上的内容审核 agent。它会跑上好几天,处理源源不断进来的用户内容,自己维护一份关于趋势和模式的状态,只在确实需要时才升级给人类。

这里最关键的架构决定又回到治理上。内容策略不应该被硬编码进 agent 本身,而应该在 Agent Gateway 里定义。策略一变,只更新一次 Gateway,整个 fleet 的 ambient agent 立刻拿到新规则。

# Pattern 4: Ambient Processing
async def ambient_moderation_agent(pubsub_stream):
    """持续运行,对事件做出反应而无需提示。"""
    async for event in pubsub_stream.listen("user_content"):

        # agent 自主评估内容
        analysis = await agent.evaluate(event.text)

        if analysis.flagged:
            if analysis.confidence > 0.95:
                # 高置信度时自动处理
                await api.ban_user(event.user_id)
            else:
                # 升级边缘案例
                await request_human_approval(
                    action_plan={"action": "ban", "user": event.user_id},
                    context=analysis.reasoning
                )

Fleet Orchestration

生产里很少出现一个 agent 单打独斗的情况。常见的形态是:一个 coordinator agent 把子任务分派给若干 specialist agent,每个 specialist 独立运行,时长各不相同。

Fleet Orchestration 用一个 coordinator 来统筹一群独立的 specialist agent。

拿销售开发流程举例:Coordinator Agent 把工作分给五个 specialist:Research Agent 负责收集每个 lead 的公开数据;Scoring Agent 按匹配度和意图信号给 lead 打分排序;Draft Agent 写出第一封个性化消息;Outreach Agent 选合适的渠道把消息发出去;Reporting Agent 收尾,把整轮跑完后的结果汇总。

每个 specialist 拥有独立的 Agent Identity,因此只能访问自己需要的资源;通过 Agent Gateway 走自己的策略校验;在 Agent Registry 里有独立条目。Coordinator Agent 维护全局状态,处理 specialist 之间的交接。

把每个 specialist 当成独立单元,一个直接的好处是更新可以分开做。Scoring Agent 的排序逻辑要改进,发新版本上去就行,不会牵连到 fleet 里的其他成员。

# Pattern 5: Fleet Orchestration
async def coordinator_agent(lead_list):
    results = []

    for lead in lead_list:
        # 1. Research Agent — 收集 lead 的公开数据
        research = await fleet.call("research_agent", target=lead)

        # 2. Scoring Agent — 根据匹配度和意图信号对 lead 排名
        score = await fleet.call("scoring_agent", data=research)

        if score > 80:
            # 3. Draft Agent — 撰写个性化的首条消息
            draft = await fleet.call("draft_agent",
                                     context=research,
                                     tone="professional")

            # 4. Outreach Agent — 通过合适的渠道发送消息
            await fleet.call("outreach_agent",
                             lead=lead,
                             message=draft)

            results.append({"lead": lead, "score": score, "draft": draft})

    # 5. Reporting Agent — 总结整个运行过程
    await fleet.call("reporting_agent", summary=results)

总结

这五种模式反映的是 AI agent 在真实世界里运转方式的一次推进。它们也直接对应着 agentic AI 的下一次转向——agent 不再只是被动响应,而是能够持续存在、自我治理、规模化协同。

底层原则其实很简单:结构化、有状态、被治理的执行,比无状态的请求-响应循环是更基础。

把确定性的 checkpointing 和概率性的推理拆开;用原地暂停取代序列化成 JSON 再恢复;让记忆通过身份和策略来约束,而不是盲目信任 agent 的写入行为——做到这几点,工作流里的推理链就能被保留下来,必要时还能复原。

agent 本身就是进程。其余的都是脚手架。

生产级 AI 不是单轮里把 agent 调得多聪明,而是看它能否在很多轮、很多天、很多次交接之间保持可靠。把已知和推断区分清楚的团队,以及一开始就把治理嵌进系统而不是事后再补的团队,会决定公司未来怎么用 AI。

https://avoid.overfit.cn/post/101285ed3b054326a6b07c84a1453fcd

作者:Ana Bildea, PhD

简介

多线程代码里最麻烦的一个点,不是“怎么开线程”,而是“数据到底该不该共享”。

很多并发问题,本质上都不是线程太多,而是:

  • 好几个线程同时改同一份数据
  • 于是开始加锁
  • 锁一多,性能又掉下去

这时候就会发现,有些数据其实根本没必要共享。

比如:

  • 每个线程自己的计数器
  • 每个线程自己的 Random
  • 每个线程临时复用的 StringBuilder
  • 每个线程自己的缓冲区

这种场景下,ThreadLocal<T> 就很合适。

一句话先说透:

ThreadLocal<T> 的作用不是“让共享数据更安全”,而是“让数据干脆别共享”,直接给每个线程一份独立副本。

这篇文章重点会讲清楚几件事:

  • ThreadLocal<T> 到底解决什么问题
  • 它和 lockInterlockedAsyncLocal<T>[ThreadStatic] 的边界是什么
  • 为什么它在 async/await 里经常踩坑
  • 实战里该怎么写,怎么收尾,怎么避坑

ThreadLocal<T> 是什么?

ThreadLocal<T> 位于:

System.Threading

它的核心语义很简单:

  • 同一个 ThreadLocal<T> 实例
  • 不同线程访问 .Value
  • 拿到的是各自线程自己的值

也就是说,看起来像一个变量,实际上一份变量会被拆成很多份,按线程隔离保存。

可以把它想成“每个线程一个独立抽屉”:

  • 线程 A 往抽屉里放了 1
  • 线程 B 往抽屉里放了 100
  • 彼此互相看不见,也互不影响

所以它不是同步工具,不负责协调线程之间的先后顺序。

它做的是另一件事:

既然这份状态本来就不该共享,那就直接按线程拆开存。

为什么需要它?

先看一个最常见的问题。

int counter = 0;

Parallel.For(0, 100000, _ =>
{
    counter++;
});

这段代码有竞态条件,结果通常不对。

因为 counter++ 不是原子操作,它至少包含:

  • 读取旧值
  • 加一
  • 写回新值

多个线程同时做这件事,就会互相覆盖。

传统做法一般有两种:

1. 加锁

int counter = 0;
object sync = new();

Parallel.For(0, 100000, _ =>
{
    lock (sync)
    {
        counter++;
    }
});

优点是简单、正确。

缺点也很直接:

  • 线程会竞争同一把锁
  • 并发越高,等待越明显

2. 用原子操作

int counter = 0;

Parallel.For(0, 100000, _ =>
{
    Interlocked.Increment(ref counter);
});

这个写法通常比 lock 更轻。

但本质上,所有线程还是在争同一个计数器。

如果业务允许“先局部累计,最后再合并”,那还有第三种思路:

3. 每个线程记自己的

using var localCounter = new ThreadLocal<int>(() => 0, trackAllValues: true);

Parallel.For(0, 100000, _ =>
{
    localCounter.Value++;
});

int total = localCounter.Values.Sum();

这就是 ThreadLocal<T> 的典型用法:

  • 运行时不争同一个变量
  • 先在线程内部局部处理
  • 最后统一汇总

很多高性能代码的思路,都是这一套。

它的工作方式是什么?

ThreadLocal<T> 有两个很关键的特点。

1. 按线程隔离

同一个 ThreadLocal<int>,不同线程看到的是不同的 .Value

线程 A -> local.Value = 10
线程 B -> local.Value = 20
线程 C -> local.Value = 30

这三个值不会互相覆盖。

2. 延迟初始化

通常会这样创建:

var local = new ThreadLocal<Random>(() => new Random());

这里的工厂方法不是创建一次,而是:

  • 哪个线程第一次访问 .Value
  • 就在那个线程里执行一次工厂方法
  • 后续该线程再次访问,直接拿缓存值

也就是说,初始化逻辑是“每线程一次”,不是“全局一次”。

最基础的 Demo:每个线程各用各的值

using System;
using System.Threading;

using var localNumber = new ThreadLocal<int>(() =>
{
    Console.WriteLine($"线程 {Thread.CurrentThread.ManagedThreadId} 初始化");
    return 0;
});

Thread t1 = new(() =>
{
    localNumber.Value++;
    localNumber.Value++;
    Console.WriteLine($"线程 {Thread.CurrentThread.ManagedThreadId} 的值:{localNumber.Value}");
});

Thread t2 = new(() =>
{
    localNumber.Value += 10;
    Console.WriteLine($"线程 {Thread.CurrentThread.ManagedThreadId} 的值:{localNumber.Value}");
});

t1.Start();
t2.Start();
t1.Join();
t2.Join();

输出大致会是这样:

线程 8 初始化
线程 9 初始化
线程 8 的值:2
线程 9 的值:10

这里要注意两点:

  • 初始化逻辑分别在两个线程里各跑了一次
  • 两个线程虽然访问的是同一个 localNumber 变量,但值完全独立

常用 API 先过一遍

构造函数

new ThreadLocal<T>()
new ThreadLocal<T>(Func<T> valueFactory)
new ThreadLocal<T>(Func<T> valueFactory, bool trackAllValues)

最常见的是后两种。

Value

local.Value

读取或设置当前线程对应的值。

IsValueCreated

local.IsValueCreated

表示“当前线程”是否已经创建过值。

这个判断不是全局的,而是当前线程视角。

Values

local.Values

获取所有已跟踪线程的值。

但要注意,只有在构造时传了:

trackAllValues: true

这个属性才有意义。

Dispose()

local.Dispose();

释放 ThreadLocal<T> 持有的资源和跟踪结构。

只要生命周期不是跟进程同生共死,通常都应该在不用时及时释放。

Demo 2:高并发计数,先局部累加再汇总

这个例子最能说明 ThreadLocal<T> 的实际价值。

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

using var localCounter = new ThreadLocal<int>(() => 0, trackAllValues: true);

Parallel.For(0, 1_000_000, _ =>
{
    localCounter.Value++;
});

int total = localCounter.Values.Sum();

Console.WriteLine($"总数:{total}");
Console.WriteLine($"线程份数:{localCounter.Values.Count}");
Console.WriteLine($"每个线程的局部计数:{string.Join(", ", localCounter.Values)}");

这种模式的优点是:

  • 运行过程中几乎没有共享写竞争
  • 每个线程只改自己的局部值
  • 最后再做一次集中合并

这类场景通常包括:

  • 批量统计
  • 并行累加
  • 分片处理后的结果归并

但这里也要把边界说清楚:

如果业务要求“每次加一之后,全局值立刻可见”,那就不能用 ThreadLocal<T> 代替 Interlocked

因为它本来就不是共享计数器。

Demo 3:给每个线程一个专属 Random

Random 这个场景很经典。

老代码里经常会看到两种不太理想的写法:

写法 1:多个线程共用一个 Random

Random random = new();

这会有线程安全问题。

写法 2:每次都 new Random()

int value = new Random().Next();

这种写法会反复创建对象,而且在某些旧代码场景里还可能出现种子过近的问题。

ThreadLocal<Random> 会更稳:

using System;
using System.Threading;
using System.Threading.Tasks;

using var localRandom = new ThreadLocal<Random>(() =>
    new Random(HashCode.Combine(Environment.TickCount, Thread.CurrentThread.ManagedThreadId)));

Parallel.For(0, 8, i =>
{
    int value = localRandom.Value.Next(1, 100);
    Console.WriteLine($"线程 {Thread.CurrentThread.ManagedThreadId} -> {value}");
});

这种模式的价值是:

  • 每个线程一个 Random
  • 不需要对同一个实例加锁
  • 也不用每次临时创建

当然,如果只是普通场景下拿随机数,现代 .NET 里也常常可以直接用:

Random.Shared

但如果场景明确需要“线程私有状态”,ThreadLocal<Random> 依然有意义。

Demo 4:线程私有对象复用,减少临时分配

除了计数器,ThreadLocal<T> 另一个很常见的用途,就是对象复用。

比如每个线程都频繁拼接字符串,如果每次都 new StringBuilder(),分配会比较多。

这时候可以给每个线程准备一个自己的 StringBuilder

using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using var localBuilder = new ThreadLocal<StringBuilder>(() => new StringBuilder(256));

Parallel.For(0, 10, i =>
{
    StringBuilder sb = localBuilder.Value;
    sb.Clear();
    sb.Append("任务编号:").Append(i)
      .Append(",线程:").Append(Thread.CurrentThread.ManagedThreadId);

    Console.WriteLine(sb.ToString());
});

这种用法很适合:

  • 文本拼接缓冲区
  • 小对象缓存
  • 线程私有临时上下文
  • 高并发下减少短命对象分配

不过也别走偏:

ThreadLocal<T> 适合放“线程用完即可复用”的对象,不适合随手塞很大的长期对象。

原因后面会讲。

为什么它在 async/await 里容易出事?

这是 ThreadLocal<T> 最常见的误区之一。

先看例子:

using System;
using System.Threading;
using System.Threading.Tasks;

using var localText = new ThreadLocal<string>(() => "未设置");

async Task DemoAsync()
{
    localText.Value = "开始阶段";
    Console.WriteLine($"await 前,线程 {Thread.CurrentThread.ManagedThreadId},值:{localText.Value}");

    await Task.Delay(100);

    Console.WriteLine($"await 后,线程 {Thread.CurrentThread.ManagedThreadId},值:{localText.Value}");
}

await DemoAsync();

很多人第一次看到这里会困惑:

  • await 前明明赋值了
  • 为什么 await 后可能读不到刚才那个值?

原因很简单:

  • ThreadLocal<T> 绑定的是线程
  • async/await 绑定的是异步执行流
  • await 之后,续体不一定还回到原来的线程

线程一旦变了,ThreadLocal<T> 对应的那份值也就变了。

所以一定要记住:

ThreadLocal<T> 适合线程上下文,不适合异步调用链上下文。

如果需求是“跨 await 继续带着这份值走”,应该看的是:

AsyncLocal<T>

ThreadLocal<T>AsyncLocal<T> 到底怎么选?

先把一句话讲明白:

  • ThreadLocal<T>:按线程隔离
  • AsyncLocal<T>:按异步调用链隔离

举个直观点的判断方式:

适合 ThreadLocal<T> 的场景

  • 每个线程自己的缓存对象
  • 每个线程自己的临时缓冲区
  • 并行局部统计,最后汇总
  • 依赖线程隔离,而不是调用链传递

适合 AsyncLocal<T> 的场景

  • 请求上下文
  • 链路追踪 ID
  • 日志作用域
  • 需要跨 await 传递的上下文信息

如果是 ASP.NET Core 请求上下文、日志上下文之类的场景,基本都不该先想到 ThreadLocal<T>

它和 [ThreadStatic] 有什么区别?

[ThreadStatic] 也是线程本地存储,但两者不是一回事。

先看 [ThreadStatic] 的样子:

[ThreadStatic]
private static int _value;

它的特点是:

  • 只能修饰 static 字段
  • 每个线程各有一份字段值
  • 用法很轻,但能力比较原始

ThreadLocal<T> 的优势在于:

  • 支持泛型
  • 支持工厂方法延迟初始化
  • 支持 IsValueCreated
  • 支持在 trackAllValues: true 时拿到所有线程值
  • 支持 Dispose()

可以简单理解成:

  • [ThreadStatic] 更接近“语法级线程字段”
  • ThreadLocal<T> 更接近“功能完整的线程本地容器”

如果只是极简单、极底层的线程静态字段,[ThreadStatic] 可以考虑。

如果需要初始化逻辑、生命周期管理、值跟踪,通常 ThreadLocal<T> 更合适。

它和 lockInterlocked 的边界是什么?

这三个东西经常被拿来一起比较,但职责并不一样。

ThreadLocal<T> 不是锁

它不负责保护共享状态。

如果多线程真的要改同一个 Dictionary、同一个对象、同一个队列,还是得靠:

  • lock
  • Monitor
  • ReaderWriterLockSlim
  • 并发集合

ThreadLocal<T> 只能解决“别共享”这一类问题。

它也不是原子操作替代品

Interlocked 解决的是:

  • 一份共享变量
  • 需要原子读改写

ThreadLocal<T> 解决的是:

  • 多份局部变量
  • 线程先各算各的
  • 最后再归并

所以两者往往不是谁替代谁,而是看业务语义。

一个简单判断标准

如果问题是:

  • “同一个值必须被所有线程共同看到”

优先看共享同步方案。

如果问题是:

  • “每个线程其实只需要自己的那份状态”

这才是 ThreadLocal<T> 的地盘。

trackAllValues: true 有什么代价?

这个参数很有用,但不是白给的。

var local = new ThreadLocal<int>(() => 0, trackAllValues: true);

开启后,ThreadLocal<T> 会额外跟踪所有线程创建过的值,方便最后通过 Values 做汇总。

代价是:

  • 会增加一些管理开销
  • 会让这些线程本地值更容易被整体持有和观察

所以如果根本不需要 Values,就别顺手开着。

能不用就不用,只有在“确实需要全量汇总”时再开启。

生命周期问题一定要重视

ThreadLocal<T> 很容易被误用成“顺手一放就完事”。

这在短命线程里可能没什么感觉,但在线程池环境里,问题就会明显很多。

因为线程池线程不是用完就销毁,它们会长期存在。

如果 ThreadLocal<T> 里放的是:

  • 大对象
  • 缓冲区
  • 数据库连接
  • 大集合

那这些对象可能会在线程上挂很久。

所以实战里要注意几条:

1. 不要把它当万能缓存

线程本地不等于免费缓存。

线程数一多,内存占用会按线程倍增。

2. 不再使用时及时 Dispose()

local.Dispose();

特别是临时创建的 ThreadLocal<T>,别让它一直悬着。

3. 对象越大,越要谨慎

一个线程放 1MB 缓冲区不算什么,32 个线程就是 32MB,64 个线程就是 64MB

这还只是单个 ThreadLocal<T>

4. 线程池线程复用会放大问题

本来以为“任务结束对象就没了”,实际可能只是任务结束了,线程还在,值也还在。

常见误区

误区 1:ThreadLocal<T> 能替代所有锁

不能。

如果状态本身必须共享,它就帮不上忙。

误区 2:ThreadLocal<T> 适合请求上下文

在同步、固定线程模型里也许还凑合,但现代 .NET 大量场景是 async/await,这时通常应该先看 AsyncLocal<T>

误区 3:ThreadLocal<T> 一定更快

不一定。

如果:

  • 数据量很小
  • 线程数不多
  • 初始化对象很贵
  • 最后合并本身也有成本

ThreadLocal<T> 未必真的占优。

性能结论要看场景,不看想象。

误区 4:在线程池任务里放任何对象都没问题

这类代码最容易把小优化写成长期内存占用。

特别是缓存数组、缓冲区、连接对象这类资源,要非常克制。

什么时候适合用 ThreadLocal<T>

比较适合:

  • 并行计算中的线程局部累计
  • 每线程独立的 Random
  • 每线程复用的 StringBuilder
  • 每线程的临时缓冲区
  • 某些依赖线程模型的底层组件状态

不太适合:

  • ASP.NET Core 请求上下文
  • 需要跨 await 传递的数据
  • 真正的全局共享状态
  • 大对象长期缓存
  • 可以直接用参数传递的数据

一段更贴近实战的综合示例

下面这个例子演示一种很常见的模式:

  • 并行处理大量输入
  • 每个线程复用自己的 StringBuilder
  • 每个线程维护自己的成功计数
  • 最后统一汇总
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

var inputs = Enumerable.Range(1, 10000).ToArray();
var outputs = new ConcurrentBag<string>();

using var localBuilder = new ThreadLocal<StringBuilder>(() => new StringBuilder(128));
using var localSuccessCount = new ThreadLocal<int>(() => 0, trackAllValues: true);

Parallel.ForEach(inputs, item =>
{
    StringBuilder sb = localBuilder.Value;
    sb.Clear();

    sb.Append("item=").Append(item)
      .Append(", thread=").Append(Thread.CurrentThread.ManagedThreadId);

    outputs.Add(sb.ToString());

    localSuccessCount.Value++;
});

int totalSuccess = localSuccessCount.Values.Sum();

Console.WriteLine($"输出数量:{outputs.Count}");
Console.WriteLine($"成功数量:{totalSuccess}");
Console.WriteLine($"参与线程数:{localSuccessCount.Values.Count}");

这段代码里:

  • ConcurrentBag<string> 负责承接真正共享的输出结果
  • ThreadLocal<StringBuilder> 负责减少线程内临时分配
  • ThreadLocal<int> 负责线程内局部累计

这就是比较典型的组合拳:

该共享的共享,该局部的局部,不要什么都硬塞进同一种并发手段里。

总结

ThreadLocal<T> 最核心的价值,不是“线程安全”这四个字本身,而是换一种思路:

  • 不是想办法保护共享
  • 而是尽量减少共享

只要场景满足“每个线程维护自己的状态”,它通常会比锁更自然,也往往更高效。

但边界也很明确:

  • 它不保护共享数据
  • 它不适合跨 await
  • 它可能带来线程级内存常驻

codex 官方只有一个剩余额度。当前在 win 或者 linux 中,有没有可以监测具体实时额度消耗速度的工具?
vscode 中找了几个插件,似乎都没有正常工作。

Gatling 是一款基于 Scala + Akka + Netty 构建的高性能压测工具,核心突破了传统JMeter「一用户一线程」的模型瓶颈,通过异步非阻塞事件驱动 + 轻量级Actor并发模型,实现了低资源占用、高并发支撑、毫秒级精准的稳定施压能力,完美适配你做全链路压测时对流量精准控制、长时间稳定运行的核心需求。


一、Gatling 核心实现原理

1. 底层核心架构:两大基石

Gatling 的高性能和稳定性,本质是由两大底层框架的特性决定的,彻底解决了传统压测工具的并发瓶颈:

(1)Akka Actor 模型:轻量级并发管理

传统压测工具(如JMeter)为每个虚拟用户创建一个操作系统线程,高并发下会出现:

  • 线程数量爆炸,内存占用极高
  • 频繁的线程上下文切换,CPU开销巨大
  • 锁竞争导致的线程阻塞、流量抖动

而Gatling 采用 Akka Actor 模型,核心特性:

  • 每个虚拟用户(VU) 是一个轻量级的 Actor 实体,而非操作系统线程,创建/销毁开销几乎为0,单台机器可轻松支撑数万虚拟用户
  • Actor 之间通过异步消息通信,无共享状态,天然无锁设计,彻底规避了锁竞争导致的线程阻塞和抖动
  • 少量固定线程池(通常为CPU核心数*2)即可调度海量Actor,线程利用率接近100%

(2)Netty 异步非阻塞IO:网络通信基石

Gatling 的网络层完全基于 Netty 构建,采用 Reactor 主从多线程模型:

  • 少量 EventLoop 线程(IO线程)即可处理数万TCP连接的读写事件,无需为每个连接创建单独线程
  • 所有网络操作都是异步非阻塞的:发送请求后,线程不会阻塞等待响应,而是注册一个回调函数,响应返回后由EventLoop线程触发回调处理
  • 内置内存池(PooledByteBufAllocator)、零拷贝、连接复用、TCP参数优化,最大化网络通信效率,减少GC和内存抖动

2. 核心组件与执行全流程

Gatling 的执行链路完全围绕「异步、隔离、无锁」设计,核心组件与流程如下:

组件核心职责设计价值
DSL解析层解析Scala/Java DSL编写的压测脚本,构建场景执行计划代码即配置,可版本化管理,直观定义用户行为链
Controller 总控Actor压测全生命周期的大脑,负责用户注入调度、场景启停、全局状态管理单例全局管控,精准控制流量节奏,隔离调度与执行逻辑
User Actor每个虚拟用户对应一个User Actor,负责执行场景步骤、维护用户Session状态、发起请求状态自治,生命周期独立,无共享资源,并发无锁
Protocol Actor/引擎协议层处理(HTTP/gRPC/WebSocket等),基于Netty实现请求发送、响应解析、回调处理网络IO与业务逻辑隔离,异步化处理,不阻塞用户行为执行
Data Writer Actor异步收集、聚合、写入压测指标(响应时间、RPS、错误率等)统计链路与施压链路完全隔离,不会因为指标计算影响施压稳定性

完整执行流程

  1. 脚本编译初始化:Gatling 编译DSL脚本,解析场景定义、协议配置、用户注入策略,初始化Netty客户端、Akka Actor系统
  2. 用户注入调度:Controller 根据注入策略,通过Akka高精度调度器,按固定节奏创建User Actor(虚拟用户)
  3. 场景执行:User Actor 按场景定义的步骤,依次发起请求,通过Protocol引擎发送给被测系统
  4. 异步回调处理:请求发送后,User Actor 不会阻塞等待,而是释放线程处理其他任务;响应返回后,Netty 触发回调,User Actor 继续执行后续步骤(断言、参数提取、下一个请求)
  5. 异步统计收集:请求的响应结果、耗时等指标,通过异步消息发送给Data Writer,批量写入结果文件,全程不阻塞施压线程
  6. 压测终止:所有用户场景执行完成,或达到预设时长后,Controller 停止所有Actor,聚合生成HTML测试报告

二、Gatling 实现稳定施压的核心机制

稳定施压的核心,是无论被测系统响应快慢、压测时长多久,都能精准控制发送的流量节奏,无抖动、无溢出、无中断。Gatling 从流量控制、架构设计、资源优化、容错保护四个维度,实现了生产级的稳定施压能力。

1. 精准的流量注入模型:流量节奏的底层控制

Gatling 提供了两套成熟的负载模型,覆盖绝大多数压测场景,通过Akka时间轮调度器实现微秒级的流量精准控制,这是稳定施压的核心基础。

(1)开放模型(Open Model):最贴近真实流量,稳定RPS控制

核心逻辑:控制用户的到达速率(每秒新增用户数/每秒请求数),无论被测系统响应快慢,都严格按照预设的节奏注入流量,完全模拟真实线上用户的访问行为(线上用户不会因为系统变慢就停止访问)。

核心实现与稳定特性:

  • 恒定用户到达率constantUsersPerSec(200) during(10min),会严格按照每秒200个用户的节奏,均匀拆分到毫秒级创建User Actor,比如每5ms创建1个用户,不会出现集中式的流量突增
  • 恒定RPS控制:通过throttle节流组件,实现全局RPS的精准控制,底层基于令牌桶算法实现:固定速率生成令牌,每个请求需要拿到令牌才能发送,超过速率的请求会被排队延迟,保证全局RPS严格符合预设值

    // 示例:10秒线性提升到1000 RPS,保持1小时稳定
    setUp(scn.inject(constantUsersPerSec(500) during(60min)))
      .throttle(
        reachRps(1000) in (10),
        holdFor(60min)
      )
  • 平滑流量过渡rampUsersPerSec 实现流量的线性增长/下降,避免流量尖峰对被测系统的冲击,同时保证施压过程的平滑稳定

(2)闭环模型(Closed Model):固定并发数稳定施压

核心逻辑:控制同时在线的并发用户数,只有当一个用户完成整个场景执行后,才会注入新的用户,保证系统内的活跃并发数始终稳定,适合压测固定并发容量的系统。

核心实现与稳定特性:

  • 严格控制并发上限,不会因为被测系统响应变慢,导致并发用户数无限累积,避免压测机本身OOM
  • 适合长时间稳定性测试(如72小时压测),保证压测过程中资源占用始终稳定,无内存泄漏风险

2. 无锁、非阻塞的架构设计:从根源杜绝流量抖动

流量抖动的核心根源,是线程阻塞、锁竞争、上下文切换导致的调度延迟,Gatling 的架构设计从根源上规避了这些问题:

  1. Actor 无锁化设计:所有User Actor 状态自治,无共享内存,无需加锁,彻底避免了锁竞争导致的线程阻塞、唤醒延迟,保证调度的精准性
  2. 全链路异步非阻塞:从用户调度、请求发送、响应处理、统计收集,全链路异步化,没有任何阻塞操作。线程不会因为等待响应、等待锁、等待IO而空闲,始终在执行可运行的任务,CPU利用率最大化,施压节奏不会被阻塞操作打断
  3. 调度与执行隔离:Controller 的流量调度、User Actor 的场景执行、Netty 的IO处理、Data Writer 的统计计算,分别在独立的线程池/ Actor 中执行,不会因为某一个环节的慢操作,影响流量调度的精准性

3. 极致的资源优化:长时间压测无性能衰减

稳定施压的前提,是压测机本身的资源占用稳定,不会出现内存溢出、GC频繁、CPU飙升等问题。Gatling 做了大量资源优化:

  1. 极低的内存占用:每个虚拟用户是轻量级Actor,内存占用仅几百字节,单台8核16G机器可轻松支撑5万+并发用户,而传统线程模型单线程栈就需要1M内存,高并发下内存极易耗尽
  2. 内存池化与对象复用:基于Netty的ByteBuf内存池,复用网络缓冲区,减少频繁的内存申请/释放;Scala不可变对象设计,减少临时对象的创建,大幅降低GC频率和停顿时间
  3. 连接复用与TCP优化:内置HTTP连接池,复用TCP连接,减少三次握手/四次挥手的开销;自动开启Keep-Alive,TCP_NODELAY等优化,保证网络通信的稳定低延迟
  4. JVM层面优化:Gatling 官方提供了适配的JVM参数(G1GC、新生代优化、元空间配置等),针对长时间压测场景做了调优,避免Full GC导致的压测停顿,保证施压过程的连续性

4. 过载保护与容错机制:避免压测失控

即使被测系统出现异常,Gatling 也能保证自身施压的稳定,不会出现压测机崩溃、流量失控的问题:

  1. 请求超时控制:每个请求都可设置超时时间,超时的请求会直接中断,不会无限等待,避免连接和线程资源被耗尽
  2. 流量上限保护throttle 组件会严格限制全局最大RPS,即使脚本中配置的用户数过多,也不会超过预设的RPS上限,避免压测机过载
  3. 异常隔离:单个User Actor 的请求异常、断言失败,只会影响当前虚拟用户,不会扩散到其他用户,更不会导致整个压测任务崩溃
  4. 无界队列防护:对于超出速率的请求,会做限流排队处理,避免无限队列堆积导致的OOM,保证压测机在长时间运行中内存始终稳定

三、Gatling 稳定施压的最佳实践(适配你的全链路压测场景)

  1. 优先使用开放模型+throttle控制RPS:全链路压测最贴近真实线上流量的模式,严格控制每秒请求数,避免因为被测系统响应变慢导致并发数累积,保证施压的稳定性
  2. 合理设置超时时间:针对订单接口设置合理的连接超时、读取超时,避免慢请求占用连接资源,影响施压节奏
  3. 开启连接复用:HTTP协议配置中开启shareConnections,全局复用连接池,减少TCP连接开销,提升压测机的并发支撑能力
  4. 优化JVM参数:长时间压测时,使用G1GC,设置合理的堆内存(如-Xms8G -Xmx8G),避免Full GC导致的压测停顿
  5. 避免在场景中做阻塞操作:不要在DSL中写同步数据库查询、文件读写等阻塞操作,所有额外逻辑都要异步化,避免阻塞User Actor的执行,导致流量抖动
  6. 分布式压测拆分流量:当需要超高并发时,使用Gatling的分布式模式,将流量拆分到多台压测机,避免单台压测机的资源瓶颈,保证整体施压的稳定

核心总结

Gatling 能实现稳定施压的本质,是彻底摒弃了传统阻塞式线程模型,用异步事件驱动+Actor轻量级并发,实现了流量的精准控制、资源的高效利用、架构的无锁隔离

对比JMeter等传统工具,Gatling 在高并发场景下,流量控制精度更高、资源占用更低、长时间压测的稳定性更强,尤其适合你做订单系统的全链路压测、长时间稳定性测试等核心场景。

很多人不清楚,我也不是小米内部的。我只是从人性出发,结合整个流程给大家分析分析:

  1. 这个 100T 赠送计划,就是 coding plan 的推广活动。
  2. 基本上申请表单里面要你填写的就是最核心的内容,也就是确认是不是一个开发者
  3. 没有人工审核,走的都是自动化审核(因为人工审核成本都扛不住)。之所以慢或者需要时间,主要问题就是预热和热度慢慢扩散的过程,属于运营策略
  4. 你的申请邮箱会在申请后的 6 小时(夜晚会有延迟,因为队列凌晨不怎么跑,以便回应人工审核)左右收到一个邮件:“你已入选 Xiaomi MiMo Orbit-百万亿 Token 创造者激励计划”
  5. 开放平台地址:platform.xiaomimimo.com 这个是重点,晚点会考
  6. 进入到开放平台,注意:不是 dev.mi.com 但是用小米的账号可以登录。
  7. 再过 2 小时(工作时间内,约到 23 点 59 分前) 你的额度会到账,一般是开了一个月的 coding plan. 如 pro 版会有 7 个亿 token. 其实就是 0.7B 好好用的话,应该可以支撑几小时。
  8. 这个时候,你可以选择自动续费。也可以先用给的 token 先玩。请记住:自动续费前,或者在 platform.xiaomimimo.com 进行支付前,是需要实名认证的。很多人都会奇怪:小米账号不是已经实名了吗?厉害的地方就是这里。这个跟小米汽车是一样的,后面 coding plan 的投诉只能投诉北京小米移动软件有限公司,不能投诉小米科技有限责任公司
  9. 至此基本情况就是如此了
  10. 好了,可能有米粉又会喷我了。“爱用用,不爱用,滚!”

哎~

模型大家可以测试跑跑
上述仅仅描述事实,请不要对号入座,仅为个人体验记录。相关法务不要找我。上述仅仅描述事实,请不要对号入座,仅为个人体验记录。相关法务不要找我。

因为宝宝出生,辞职在家有一年 gap , 今年过了正月十五找工作,正好有一个心意的,让我降薪我没同意,后来通知面试过了。当时薪资没有谈好,就让背调,问了人事,说是他们公司流程就是这样的。
背调材料交完了,和上家领导同事都打好招呼了,等了一周都没有开始。 问人事说:有其他候选人要对比一样。再过了一周再问人事,说其他候选人更合适,我被 pass 了,现在公司招人是这样的流程吗
家中一年景,职场已千年

各位佬好,分享一个自己在做的开源项目 DBX —— 一款基于 Tauri + Vue 3 的跨平台数据库管理工具。

先说重点:安装包只有 15MB,因为用的是 Tauri (系统原生 WebView ),不像 Electron 动不动就 200MB+。

GitHub: https://github.com/t8y2/dbx

screenshot


支持的数据库

一个客户端管所有:

  • MySQL
  • PostgreSQL
  • SQLite
  • Redis
  • MongoDB
  • DuckDB
  • ClickHouse
  • SQL Server

不用再在 Navicat 、RedisInsight 、MongoDB Compass 、DBeaver 之间来回切了。


有什么功能

查询编辑器

  • CodeMirror 6 编辑器,语法高亮
  • Cmd+Enter 执行
  • AI 辅助生成 SQL (支持 Claude / OpenAI )

数据浏览

  • 虚拟滚动,万行数据不卡
  • 行内编辑、排序、搜索、分页
  • 列宽可拖拽调整
  • 导出 CSV / JSON / Markdown

Redis 浏览器

  • 模式匹配搜索 key
  • 支持 String / Hash / List / Set / ZSet 全部数据类型

MongoDB 浏览器

  • 文档增删改查
  • 分页浏览

其他

  • 查询历史(持久化存储,搜索 + 一键恢复)
  • 危险操作确认( DROP / DELETE / TRUNCATE 弹确认框)
  • 连接断开自动重连
  • SSH 隧道连接
  • 深色模式
  • 中英文双语


和同类工具对比

DBX DataGrip DBeaver Navicat
价格 免费开源 ¥688/年 免费/付费 ¥1398
安装包 15MB 800MB+ 400MB+ 200MB+
数据库数量 8 种 很多 很多 主流
启动速度 秒开 一般
内存占用 ~80MB 1GB+ 500MB+ 300MB+

当然功能上和 DataGrip 这种成熟产品还有差距,但日常查数据、改数据、看表结构完全够用了。


安装

GitHub Releases 下载对应平台安装包。

支持 macOS ( Apple Silicon + Intel )、Windows 、Linux 。

macOS 用户首次打开需要执行:

xattr -cr /Applications/dbx.app


后续计划

  • 执行计划可视化( EXPLAIN )
  • ERD 关系图
  • 数据导入( CSV / JSON )
  • 更多快捷键
  • 插件系统

欢迎 Star 、PR 、提 Issue 。有什么想法也可以在下面聊。


GitHubhttps://github.com/t8y2/dbx