<rss version="2.0"><channel><title>Rust.cc</title><link>https://rust.cc</link><description>This Is Rust Crustacean Community RSS feed.</description><item><title>基于 OntoDB 内核构建的语义原生数据湖仓——OntoLake，面世了</title><link>https://rustcc.cn/article?id=33e834ee-3095-4e6a-b883-9a30ef74557c</link><description><![CDATA[<p>今天，我们正式宣布：基于 OntoDB 内核构建的语义原生数据湖仓——OntoLake，面世了。</p>
<p>它不是又一个“套了湖仓壳”的存储系统，而是把本体语义、多模存储、推理能力真正沉到内核里的下一代数据底座：数据落库即完成实体对齐、关系推理与语义标注，从“原始堆积”直接变成“AI-Ready”。</p>
<p>为什么是 OntoLake</p>
<p>传统湖仓解决的是“文件像表一样可靠”，OntoLake 多了一层别人没有的东西：</p>
<p>本地文件当湖、实体表当仓、本体当语义内核、向量当 AI 接口；</p>
<p>结构化、图、向量、时序、文档、空间统一存储，不再多库搬运；</p>
<p>写入即推理，subClassOf、等价类、传递关系自动展开，查询不再靠应用层硬编码。</p>
<p>关键进展：已打通 DuckDB 与 Trino 读取</p>
<p>这一次，OntoLake 完成了与两条主流分析引擎的读取打通：</p>
<p>DuckDB：进程内嵌入式分析直接读 OntoLake 语义表，适合笔记本、边缘节点、Python 工作流里的秒级探查；</p>
<p>Trino：分布式联邦查询接入，多机集群、跨源 JOIN、BI 工具直连，把 OntoLake 当成一等公民目录。</p>
<p>这意味着：分析师用 DuckDB 做本地深挖，平台侧用 Trino 做全局编排，而底层语义、口径、血缘都由 OntoLake 统一托管——查出来的不是“字段”，是“含义”。</p>
<p>对业务意味着什么</p>
<p>生物医药：查“EGFR 抑制剂”自动展开全部子类，不用维护枚举列表；</p>
<p>工业/城市：端侧语义摘要进湖，跨域仲裁结果可溯源、可重放；</p>
<p>AI 应用：GraphRAG、Agent 记忆、语义检索直接消费“已治理好的知识”，而不是先洗数据再推理。</p>
<p>......</p>
<p>OntoLake 的发布，不是多一个组件，而是把“数据湖仓”重新定义为语义数据基座。</p>
<p>DuckDB 与 Trino 的打通，只是开放生态的第一步。</p>
<p>数据先语义化，再谈智能化。OntoLake 和更多朋友一起出发。</p>
]]></description><pubDate>2026-11-10 02:57:26</pubDate></item><item><title>【Rust日报】2026-11-10 改一个标签要 8.4 毫秒</title><link>https://rustcc.cn/article?id=dbf44252-5db7-4ed8-a3a3-9856a203d0da</link><description><![CDATA[<h2>改一个标签要 8.4 毫秒</h2>
<p>这是一篇评 GPUI 状态模型的文章，作者是 Zortax_。文章比较 immediate、Elm、view 和 signal 这几类做法，看一个值变化以后哪些代码会重跑。Reddit 采集时 301 分、81 条评论。帖子时间是 2026-11-09 02:29 UTC。</p>
<p>作者在文首写了两件事。Discord 宣布用 GPUI 重写客户端。不少 LLM 客户端选了 egui。作者认为这两套状态模型撑不住复杂桌面应用。Zed 用起来快，作者自己也在 Bevy 游戏里用 egui 做调试界面。文章同时写明，作者把 GPUI 当前的 Linux 渲染器移植到了 wgpu，下面的基准测试套件是作者搭的，作者还在做信号式框架 zgui。zgui 仓库在 https://github.com/zortax/zgui 。采集时 214 星、6 个 fork，许可证 Apache-2.0，最近推送是 2026-11-08。Zed 仓库采集时 94598 星、11595 个 fork。</p>
<p>GPUI 自述是 immediate 和 retained 的混合。状态留在 entity 里，界面写在 render 函数里。改状态要显式调用 <code>cx.notify()</code>。<code>notify</code> 带的是 entity id。Rust 的 <code>&amp;mut</code> 没有写屏障，GPUI 看不到改了哪个字段，也看不到有没有真的改动。没有有效 <code>.cached()</code> 边界的 view 会重跑。文章写，应用作者很少用 <code>.cached()</code>，这个缓存对布局语义还有限制，实际经常是每个 view 都重跑。</p>
<p>文章里的数字是稀疏更新的成本。3000 个存活行，每行一个带缓存的 view，改一个标签要 8.4 ms。每多 1000 个存活行，大约再加 3 ms。悬停、按键、写成原值，成本接近，因为成本跟着整棵树走。行如果写成普通元素，同一个标签要 92 ms。手写相等判断以后，写成原值的耗时从 7.9 ms 降到 0.9 µs。</p>
<p>文章接着写，把更新做得更细，会把 GPUI 现在这套 API 拆掉。Longbridge 的 gpui-fast 会在帧之间保留每个 view 的子树。3000 行那个标签从 8.4 ms 降到 3.4 ms。能复用的最小单位仍是 view。要让一个标签单独更新，这个标签自己得是一个 view，带 entity、<code>Render</code>、订阅、一份存下来的值和手写判断。状态归每个 panel 自己管时，一次改动能省 83%。状态改成共享模型以后，同样一帧的 retention 慢 11%。窗口和编辑内容没有变。Zed 团队测到，retention 在普通负载上比关掉慢 1% 到 3%，微基准的收益对不上真实 Zed。Zed 自己的说法是，一个窗口有几十个节点和几千个元素。文本编辑器是自定义元素，绕开了 GPUI 的布局和 view 语义。</p>
<p>nicoburns 的评论有 45 分，问能不能把 Dioxus Native 加进这组基准，并希望看到可运行的基准代码。作者回复有 22 分，写 harness 里已经有，文中数字是 taffy 那次更新之前的一次运行，这周会整理对比仓库。zenware 的评论有 81 分，写不喜欢 React 式模型，但拿掉浏览器引擎以后，就算状态模型还像 React，内存和延迟也会下来。作者回复有 58 分，写浏览器的主要成本在 JS、DOM API、线程模型和进程隔离。GPUI 应用的内存、二进制体积和延迟下限会马上下来。作者同时写，随便写的 GPUI 应用会比随便写的 SolidJS 网页应用更顿。simspelaaja 的评论有 46 分，写文章里有多处 LLM 用语，主题有内容，但分不清哪些句子是作者写的。imbev 的评论有 66 分，写 Elm 回答的是发生了什么，<code>View = f(State)</code> 里的函数可以做成纯函数。作者回复有 25 分，写没有显式的 read 绑定，就只能靠 diff 把新的 view 对上保留下来的渲染状态，这个 diff 是 O(n)。</p>
<p>原文链接：https://blog.zortax.de/posts/a-critique-of-gpui</p>
<h2>Cot 0.8 补上事务和迁移回滚</h2>
<p>Cot 是一个 Django 风格的 Rust Web 框架，自带 ORM、管理后台、表单、认证和 session。0.8 的发布说明写于 2026 年 11 月 9 日。仓库在 https://github.com/cot-rs/cot 。采集时 1095 星、60 个 fork，许可证 Apache-2.0，最近推送是 2026-11-09。crates.io 上的包名是 <code>cot</code>，当前版本 0.8.0，更新时间 2026-11-09，累计下载 26243，近期下载 2984，16 个版本。Reddit 作者是 m4tx，采集时 6 分、0 条评论。帖子时间是 2026-11-09 23:40 UTC。</p>
<p>ORM 这一版加上事务。<code>Database::begin</code> 开一个事务，后面的 <code>insert</code>、<code>save</code> 和 <code>query!</code> 可以把这个事务当成原来的数据库来传。事务对象在没有 <code>commit</code> 的情况下被 drop，会自动回滚。在事务上再调用 <code>begin</code>，得到的是 savepoint，可以单独提交或回滚。</p>
<p>查询补了排序和文本匹配。<code>order_by</code> 接受一组字段或表达式，各自带 <code>asc</code> 或 <code>desc</code>，还可以用 <code>nulls_first</code>、<code>nulls_last</code> 和 <code>by_values</code>。<code>query!</code> 增加 <code>contains</code>、<code>starts_with</code>、<code>ends_with</code>，以及忽略大小写的 <code>icontains</code>、<code>istarts_with</code>、<code>iends_with</code>。用户输入里的 <code>%</code> 和 <code>_</code> 会按字面量转义。<code>raw_like</code> 接受 <code>"PKG*US"</code> 这种 glob，再按后端翻译。复杂查询可以走原始 SQL：<code>raw</code> 和 <code>raw_with</code> 执行不返回行的语句，<code>raw_as</code> 和 <code>raw_as_with</code> 把行映射成模型。带 <code>_with</code> 的版本会绑定参数。</p>
<p>迁移可以回滚。<code>cot migration rollback 0002 --dry-run</code> 只打印计划。指定的迁移本身留着，它后面的迁移会被撤掉。跨 app 的依赖会一起撤，比如另一张表的外键指过来。也可以传完整迁移名，或者传 <code>0</code> 撤掉这个 app 的全部迁移。<code>cot migration graph --format mermaid -o migrations.mmd</code> 导出依赖图。</p>
<p>路由器用 <a href="https://crates.io/crates/matchit" rel="noopener noreferrer">matchit</a> 的 radix trie 重写。构建时会拒绝 <code>/foo/{bar}</code> 和 <code>/foo/{baz}</code> 这种有歧义的路由。<code>{*path}</code> 可以匹配剩余路径。<code>reverse!</code> 可以追加 query，值会做百分号编码。新项目默认启用 <code>TrailingSlashMiddleware</code>，GET 和 HEAD 在原路径不匹配、补上斜杠能匹配时会重定向，query string 保留。<code>Redirect::permanent</code> 用 308，<code>Redirect::temporary</code> 用 307。<code>ClientIpAddr</code> 默认取直连对端地址。反代场景在配置里写可信网段和 header：</p>
<ul>
<li><code>proxies = ["10.0.0.0/8"]</code></li>
<li><code>headers = ["x-forwarded-for"]</code></li>
</ul>
<p>Reddit 发布帖写，这一版去掉 axum，改成直接用 hyper。帖子写，项目没有用到 axum 的高层功能，这次是为了减少依赖和编译时间。配置 TOML 的参考页从源码生成。随这次发布一起放出来的示例是 Hack the Space，地址是 https://hackthespace.mackow.ski/ 。1.0 之前还想要什么，写在 https://github.com/cot-rs/cot/issues/434 。</p>
<p>原文链接：https://mackow.ski/blog/cot-v08-lazily-transactional/</p>
<h2>rust-analyzer changelog #353</h2>
<p>这是 rust-analyzer 2026 年 11 月 9 日的 changelog #353。rust-analyzer 是给 IDE 用的 Rust 编译器前端。仓库采集时 16976 星、2284 个 fork，许可证 Apache-2.0，最近推送是 2026-11-10。Reddit 转发作者是 WellMakeItSomehow，采集时 29 分、3 条评论。帖子时间是 2026-11-09 15:40 UTC。评论里没有对上具体改动的内容。</p>
<p>新功能里，#21392 给 Linux glibc 发行版打开 jemalloc。#23600 是第一次贡献，加了一个选项，只对带 drop glue 的类型显示隐式 drop 提示。#23558 从欢迎页去掉 Create Rust Project 按钮。诊断新增三项：<code>invalid-pat-path-res</code>、<code>match-inherited-imm-ref-with-mut-ref-pat</code>、<code>unresolved-associated-type</code>，对应 #23583、#23589、#23619。性能项 #23591 对 lang item 做了微优化。</p>
<p>修复里，#23598 让 problem matcher 对上 rustc-unicode 的位置。#23478 和 #23631 补上类型位置的 <code>dyn</code> trait 补全。#23610 让强制转换逻辑对上 rustc。#23608 修了 MIR 求值 panic。#23579 不再给结构体字面量提示到不了的路径。#23615 修了 <code>#[repr(i28)]</code> 补全。#23620 让 <code>destructure_struct_binding</code> 可以作用在通配符模式上。#23592 让 <code>generate_doc_example</code> 可以作用在重导出项上。#23554 修了 <code>move_bounds_to_where_clause</code> 之后 GAT 约束的位置。#23616 修了 <code>apply_demorgan</code> 在类型转换上的 panic。#23602 修了 <code>generate_function</code> 在带泛型时的 panic。内部改动里，#23514 把 expect-test 合进 rust-analyzer 仓库。</p>
<p>原文链接：https://rust-analyzer.github.io/thisweek/2026/11/09/changelog-353.html</p>
<h2>tellus 验证器 0 次违规，8 个请求失败</h2>
<p>这是一篇讲 tellus 集群故障注入的文章，作者是 Heiko Seeberger。tellus 是跑在 Tokio 上的 actor 框架，带监督、有序 death watch、事件溯源和集群。仓库在 https://github.com/tellus-rs/tellus 。采集时 71 星、3 个 fork，许可证 Apache-2.0，最近推送是 2026-11-09。crates.io 当前版本 0.5.3，更新时间 2026-11-01，累计下载 635。Reddit 作者是 quest_for_7c，采集时 0 分、1 条评论。帖子时间是 2026-11-09 14:01 UTC。作者评论有 1 分，写两个 bug 都出在第一次带集群的发布之前。</p>
<p>演示用 Docker Compose 跑 5 个节点，节点通过同一个 DNS 名互相发现。前面放 Caddy，把 HTTP 请求分到这些节点。每一轮，脚本做一件事：杀掉一个节点，用 SIGTERM 停一个节点，或者把两个节点和另外三个隔开。退出或被杀掉的节点会重启并重新加入。集成测试已经会拉起真实进程，覆盖不到 DNS、容器重启和负载均衡。文章写，两个 bug 都出在这三处。</p>
<p>验证器不会在故障进行时检查成员关系。分区两侧看到的成员不同，这是对的。它等混沌停 30 秒，再检查一次是否恢复。恢复的定义是：每个节点都是成员，都把 5 个节点看成 Up，没有脑裂，每个节点都能找到别人的 actor 并向每个节点发消息。每个节点用 HTTP 提供自己的集群视图，验证器用普通 Rust 把 5 份视图拿来比较。</p>
<p>负载均衡要在故障期间继续应答，所以验证器一直查它。一次请求结束后等 0.5 秒再发下一次。连续失败超过 5 秒才记一次违规。每次失败的请求仍然单独计数。2026 年 10 月一次 45 分钟运行里，违规次数是 0，发出的 5352 个请求里有 8 个失败。</p>
<p>第一个 bug 是违规。分区之后，失败一侧的两个节点几乎同时把自己移出并重启。其中一个查共享 DNS 名时，另一个容器还没起来，所以只看到 4 个节点。<code>tellus-bootstrap-dns</code> 把这次应答缓存了，后面的查询还是旧列表。修复是一行：<code>builder.options_mut().cache_size = 0</code>。文章写，两个 bug 都在 tellus 0.4.0 之前修好，已发布版本不受影响。0.4.0 是第一个带集群的版本。</p>
<p>8 个失败请求对得上混沌日志。5 个紧跟在 kill 后面，3 个紧跟在优雅停机后面，分区后面没有。kill 丢一个请求不意外。SIGTERM 这条路径上，节点已经离开集群，健康检查却继续回 200，直到进程退出。Caddy 还在往它发请求，退出瞬间到达的请求失败。修复是进入 Leaving 阶段后健康检查回 503，并且至少再跑 5 秒，让 Caddy 看到变化。Caddy 也会把失败请求重试到另一个节点。带上这个重试以后，另一次 45 分钟运行里 5400 个验证请求都成功。</p>
<p>文章写，0 次违规不等于集群正确。这套故障没有模拟抖动网络、单向链路、时钟偏移和过载。录演示时终端 UI 在轮询每个节点，1552 个请求里失败了 1 个。去掉终端 UI 的 45 分钟运行没有再出现，原因还没查到。演示代码在 tellus v0.5.3 标签的 <code>tellus-cluster-demo</code> 里。</p>
<p>原文链接：https://heikoseeberger.de/2026-11-09-chaos-verifier/</p>
<hr>
<p>From Rust中文社区 Mike</p>
<p>社区学习交流平台订阅：</p>
<ul>
<li><a href="https://rustcc.cn/" rel="noopener noreferrer">Rustcc论坛: 支持rss</a></li>
<li><a href="https://rustcc.cn/article?id=ed7c9379-d681-47cb-9532-0db97d883f62" rel="noopener noreferrer">微信公众号：Rust语言中文社区</a></li>
</ul>
]]></description><pubDate>2026-11-10 02:08:35</pubDate></item><item><title>基于 LMDB 实现零拷贝的 SQL 计算</title><link>https://rustcc.cn/article?id=957a7cec-eb02-4924-90c7-19ef6467e1f2</link><description><![CDATA[<h1>基于 LMDB 实现零拷贝的 SQL 计算</h1>
<p><a href="zero-copy-sql-on-lmdb.md" rel="noopener noreferrer">English</a> | 中文</p>
<blockquote>
<p>数据库把一行数据交到应用程序手里之前，字符串通常已经被拷贝过好几次：从文件到缓存，从缓存到执行器，再从执行器到你的结构体。这篇文章讨论的是另一种可能：如果存储层是 LMDB 这样基于 mmap 的 KV 引擎，只读事务读到的页在事务结束前地址不变，那么 SQL 引擎能不能让查询结果一路借用这些页，让完整的 SQL 计算做到零拷贝？</p>
</blockquote>
<h2>Summary</h2>
<p>SQLite 是应用进程内最常见的 SQL 数据库，在常见语言中都有稳定的绑定 API，这里以 Rust 的 rusqlite 为例。一个很常见的场景是：取出一个帖子下的全部评论，映射成结构体，按 <code>parent_id</code> 组装成回复树，再序列化成嵌套的 JSON 写给调用方：</p>
<pre><code>use rusqlite::{params, Connection};
use serde::Serialize;
use std::collections::HashMap;
use std::error::Error;
use std::io::Write;

#[derive(Serialize)]
struct Post {
    id: i64,
    thread_id: i64,
    parent_id: i64, // 0 表示顶层评论
    user: String,
    body: String,
}

#[derive(Serialize)]
struct Comment&lt;'a&gt; {
    #[serde(flatten)]
    post: &amp;'a Post,
    replies: Vec&lt;Comment&lt;'a&gt;&gt;,
}

fn build&lt;'a&gt;(children: &amp;'a HashMap&lt;i64, Vec&lt;Post&gt;&gt;, parent: i64) -&gt; Vec&lt;Comment&lt;'a&gt;&gt; {
    let Some(posts) = children.get(&amp;parent) else { return Vec::new() };
    posts.iter()
        .map(|post| Comment { post, replies: build(children, post.id) })
        .collect()
}

fn write_thread(conn: &amp;Connection, thread_id: i64, out: impl Write) -&gt; Result&lt;(), Box&lt;dyn Error&gt;&gt; {
    let mut stmt = conn.prepare_cached("select id, thread_id, parent_id, user, body from posts where thread_id = ?1")?;
    let mut rows = stmt.query(params![thread_id])?;
    // 建树要等所有行到齐，而 row 只活到下一次 next，所以 Post 只能拥有 String
    let mut children: HashMap&lt;i64, Vec&lt;Post&gt;&gt; = HashMap::new();
    while let Some(row) = rows.next()? {
        let post = Post {
            id: row.get(0)?,
            thread_id: row.get(1)?,
            parent_id: row.get(2)?,
            user: row.get(3)?,
            body: row.get(4)?,
        };
        children.entry(post.parent_id).or_default().push(post);
    }
    serde_json::to_writer(out, &amp;build(&amp;children, 0))?;
    Ok(())
}
</code></pre>
<p>这样一段再普通不过的"取行、映射成结构体"的代码，数据要经过下面这些步骤，才能变成你手里的 <code>Post</code>：</p>
<pre><code> 数据库文件
     │
     │  默认：read() 把整页拷进 SQLite 的 page cache    [拷贝]
     │  - 开启 mmap_size：直接读映射进来的文件页
     ▼
 B 树页（page cache 或文件映射）
     │
     │  sqlite3_step → OP_Column 从记录中解出 user / body
     │    - 在页内：拷进寄存器缓冲区                    [拷贝]
     │    - 跨溢出页：malloc 缓冲区，逐页拼接            [分配][拷贝]
     │  有 ORDER BY / GROUP BY 时：写进 sorter 再读出    [拷贝]
     ▼
 寄存器缓冲区
     │
     │  sqlite3_column_text → rusqlite row.get()
     │  指针只活到下一次 next，Post 只能拥有一份拷贝
     │  user / body 各自变成 String                     [分配][拷贝] × 2
     ▼
 Post { user: String, body: String, .. }
     │
     │  按 parent_id 建树，递归写出
     │  serde_json::to_writer 写出 JSON
     ▼
 JSON
</code></pre>
<p>在这样的过程里，SQLite 和它的 API 约定都要求字符串换一个地方存放。字段越多、字符串越长、查询越复杂，这条路上的拷贝和分配就越多。这其实也是常见数据库的标准做法：自己管理 buffer pool，在用户态掌控内存，不让操作系统决定哪些页留在内存里。代价是 pool 里的页随时可能被替换，数据要交出去，就必须先拷贝一份。如果像 DBOS [1] 设想的那样，把数据库放进操作系统的内核，由它来管理操作系统的状态，用户态的 buffer pool 和内核的页缓存或许就不必再分成两层。</p>
<p>可是在常见的 Web 应用里，很多查询的主要工作其实只是数据的加工和传递：从 SQL 里取出数据，经过一些简单的处理转换成对应的结构体（Java 里的 POJO），再序列化成 JSON，通过 HTTP 返回。GraphQL [2] 把这件事做成了接口规范：调用方描述想要的字段和嵌套结构，服务端照着这个形状把数据库里的列取出来、组装好、序列化出去，几乎不做别的。这类几乎纯读取和重排的工作，在 Web 后端里占的比例越来越大。</p>
<p>在这样的情况下，<code>Post</code> 的每个 <code>String</code> 字段都要分配一次，字段越多，分配越多。再回头看刚才的流程图，你会发现字符串一共被拷贝了两次：第一次是 SQLite 在 <code>step</code> 取列时，把它从页里拷进寄存器缓冲区（即使启用了 mmap，这一次拷贝也省不掉）；第二次是映射成 <code>Post</code> 时，又从寄存器缓冲区拷进各个字段。但 <code>row.get_ref</code> 明明能直接借出 <code>&amp;str</code>，让 <code>Post</code> 的字段借用它，不就省掉第二次拷贝了吗？遗憾的是，<code>row</code> 只活到下一次 <code>next</code>，借用它的 <code>Post</code> 带不出这一行，编译器会直接拒绝。而建树要等所有行到齐，SQL 的排序和 JSON 函数都给不出深度不定的嵌套结构，这些行必须同时拿在手里。这次拷贝来自 SQLite API 的约定，靠上层的写法很难绕开。</p>
<h2>实现从存储到返回的零拷贝计算</h2>
<h3>MMAP Storage</h3>
<p>Andy Pavlo 说过，MMAP 会毁掉你的数据库和你的生活 [3]。这是他和 Crotty、Leis 那篇 CIDR 2022 论文被否掉的候选标题之一，最后定名为 <em>Are You Sure You Want to Use MMAP in Your Database Management System?</em>。论文列举了在数据库里用 mmap 替代 buffer pool 的四类问题：事务安全、IO 停顿、错误处理、性能。正如标题所说，这里并没有直接调用 mmap 自己实现存储，而是用 LMDB 作为存储后端。LMDB 是一个成熟的嵌入式 KV 存储引擎，它的设计规避了论文里最致命的事务安全问题 [4]：</p>
<ul>
<li><strong>默认只读映射。</strong> 文件以只读方式映射，进程不能通过指针修改它。宿主程序的野指针写不坏数据库，映射里也不存在会被内核擅自写回磁盘的脏页；试图修改只读事务返回的值，会直接触发 SIGSEGV。</li>
<li><strong>写入不经过映射。</strong> 写事务要修改的页先在堆内存里改好，提交时用 <code>pwrite</code> 写回文件，再按配置 <code>fsync</code>。什么时候落盘、按什么顺序落盘，由 LMDB 自己决定。</li>
<li><strong>写时复制的 B+ 树（影子分页）。</strong> 已提交的页从不原地修改：改一个叶子，就写一份新页，并沿路径一直复制到根。</li>
<li><strong>两个元数据页交替使用。</strong> 提交的最后一步是写元数据页，记下新的根。两份交替写，崩溃后总有一份是完整的，数据库不会停在写了一半的状态。</li>
<li><strong>读者表保护快照。</strong> 每个只读事务都登记自己看到的版本，它可能读到的旧页不会被回收复用。所以只读事务读到的值，在事务结束前一直有效、地址不变，这正是 <strong>零拷贝 SQL 计算</strong> 所依赖的保证。</li>
<li><strong>单写者。</strong> 同一时刻只有一个写事务，论文也提到 LMDB 正是靠这一点避免了事务之间的冲突。</li>
</ul>
<p>当然，论文里的另一些问题 LMDB 并没有解决：缺页时线程会同步阻塞，IO 错误会变成 SIGBUS，哪些页留在内存由内核决定。LMDB 的前提是热数据放得进内存，而在 SQLite 所面向的嵌入式场景里，这个前提通常是成立的。</p>
<h3>Data Encode</h3>
<p>在 KV 存储之上实现 SQL，第一步是决定一行数据怎么变成 key 和 value。TiDB 的做法很有代表性 [5]：</p>
<pre><code>行：  t{TableID}_r{RowID}                       →  [col1, col2, col3, ...]
索引：t{TableID}_i{IndexID}_{索引列的值}_{RowID}  →  null
</code></pre>
<p>同一张表的行共享同一个前缀，按主键排好序，全表扫描和索引查询都变成了一段连续 key 的范围扫描。这里的实现也是同样的结构：key 是"表 ID + 类型标记 + 主键"，value 是这一行的各列。</p>
<p>对零拷贝来说，关键在 value 里字符串怎么存。以后面测试里用到的 <code>orders</code> 表为例，插入 <code>(1, 7, 'paid', 42, 'hello')</code> 这一行，它的 value 是这样的：</p>
<pre><code>  00                               空值位图
  01 00 00 00                      id = 1
  07 00 00 00                      customer_id = 7
  04 00 00 00 70 61 69 64          status：长度 4 + "paid"
  2a 00 00 00                      amount = 42
  05 00 00 00 68 65 6c 6c 6f       note：长度 5 + "hello"
  00 00 00 00 00 00 00 00          语句戳：写入这一行的语句编号，更新时用来跳过本语句刚写入的行
</code></pre>
<p>字符串以"长度 + 原始字节"的形式连续存放。解码 <code>note</code> 时只要读出 4 字节的长度，就得到一段指向 LMDB 页的（指针，长度），正好就是一个 <code>&amp;str</code>，不需要任何转换。LMDB 又保证大值所在的溢出页是连续的，所以无论字符串多长，这段字节在映射里都是连续的一整块，可以直接借出去。注意一行的所有列是一个 LMDB value：LMDB 把放不进叶子的 value 分配在一段连续的溢出页上，所以不只是某个字段连续，而是整行连续，<code>status</code> 和 <code>note</code> 只是同一段字节里的两个偏移。</p>
<p>把大值放到单独的页上，和 PostgreSQL 的 TOAST、InnoDB 的外部页是同一个思路，解决的是行膨胀，让只读小列的查询不用碰大值。但这些机制不解决拷贝：值取回应用时照样要拷。真正不拷的做法是像 FILESTREAM 那样把值变成文件句柄交出去，可那样它就不再是 SQL 值了，引擎里不能再对它过滤、排序、求长度，和行的事务快照也对不齐。这里的区别在于，溢出页仍然在同一个映射、同一个只读事务里，算子和调用方拿到的是同一段字节。</p>
<h3>Execution</h3>
<p>字符串能从存储里借出来，只是第一步。一条 SQL 要经过扫描、过滤、排序、分组、join 等一连串算子，才会把结果交给调用方，其中任何一层把字符串拷贝一份，零拷贝就在那里断掉。这是一种风格：存储给出一个生命周期，算子不打断它，Rust 的借用检查让这件事能在编译期得到保证。</p>
<p><strong>值的表示：要么拥有，要么借用。</strong> 字符串和二进制值都用同一个类型表示（简化后）：</p>
<pre><code>pub struct MaybeMapped&lt;B: ?Sized&gt;(Repr&lt;B&gt;);

enum Repr&lt;B: ?Sized&gt; {
    Owned(B::Owned),     // 自己拥有的内存：SQL 里的字面量、表达式算出来的新值
    Mapped { ptr, len }, // 一对（指针，长度），指向只读事务读到的页
}
</code></pre>
<p>两种状态会出现在同一条查询里。比如 <code>where note = 'hello'</code>，常量 <code>'hello'</code> 是 <code>Owned</code>，列值 <code>note</code> 是 <code>Mapped</code>。</p>
<p>至于列值什么时候是 <code>Mapped</code>，取决于事务。只读事务开始时声明"读到的字节在事务结束前一直有效、地址不变"，行解码就直接产出 <code>Mapped</code>；写事务无法保证这一点，解码时只能拷贝，产出 <code>Owned</code>。</p>
<p><strong>物化算子只拷贝指针。</strong> 物化算子必然要收集上游算子的行：排序要攒齐所有行，分组要保存分组键，hash join 要建哈希表，窗口函数要留住整个分区。它们保存值时，<code>Mapped</code> 字符串只拷贝（指针，长度），不拷贝内容。整条查询都在同一个只读事务里，这些页不会失效，所以只要没有触发 spill，物化算子就能在内存容器里以借用的方式保存值。只有少数情况会产生新的字符串：表达式算出来的值（比如 <code>upper(note)</code>），以及数据量大到需要 spill 时，序列化写出再读回来的值。理论上 spill 出去的文件也可以用 mmap 读回并继续借用，但这是一项额外的做法。</p>
<p><strong>借用的期限：对外靠编译器，对内靠约定。</strong> 借用的风险在于指针活得比页久，这里分两层处理：</p>
<ul>
<li>交给调用方的结果，与事务的生命周期绑定。结论里的 <code>Post</code> 就是这样：它可以活过某一行，一直用到事务结束，但想把它带出事务，编译器会直接拒绝。</li>
<li>执行器内部的借用靠约定。同一条预编译的语句可以在不同事务里反复执行，所以每次执行开始前，算子都要清空上一次留下的借用值。这一层编译器检查不到，由 debug 构建下的存活检查兜底：读到已结束事务的借用值会直接 panic。</li>
</ul>
<h2>Benchmarking SQL Execution: SQLite with mmap vs. LMDB SQL</h2>
<p>下面的测试里，两边装载完全相同的数据、建同样的索引、执行同样的 SQL：语句只准备一次，所有执行都在同一个只读事务里完成。SQLite 开启了 <code>PRAGMA mmap_size</code>（1 GiB，大于数据库文件，整个文件都被映射），所以两边都通过内存映射读数据库文件，IO 这一层的条件是相同的。</p>
<ul>
<li><strong>LMDB SQL</strong>：跑在 LMDB 只读事务上的 SQL 引擎，字符串从存储一路借用到调用方。</li>
<li><strong>LMDB SQL（强制拷贝）</strong>：同一条查询放进写事务执行，字符串会被拷贝，用作对照组。</li>
<li><strong>SQLite</strong>：SQLite 3.45.1，开启 mmap。</li>
</ul>
<p>分配量在 libc 的 <code>malloc</code> 处统计。测试机为 i9-13900HX，程序绑定在同一个 P 核（<code>cpu8</code>）上运行。</p>
<p>测试的目的不是证明哪边更快，而是找出拷贝的成本从什么长度开始显现。所以字符串长度从 64 B 一直拉到 256 KiB：64 B 是 SQLite 领先的一档，256 KiB 则远超一般业务表里一列的大小，中间几档才是常见的文档、日志和 JSON 字段。</p>
<p>表 1 到表 3 使用下面的 <code>orders</code> 表。<code>note</code> 的长度从 64 B 到 256 KiB 分档装载，每档单独建库；每次查询按 <code>(customer_id, status)</code> 命中约 10 行。</p>
<pre><code>create table orders (
    id          int primary key,
    customer_id int,
    status      varchar,  -- 'new' / 'paid' / 'shipped' / 'done' / 'void'
    amount      int,
    note        varchar
);
create index orders_customer_status on orders (customer_id, status);
</code></pre>
<p><strong>表 1：普通索引查询</strong></p>
<p>按索引取出约 10 行，调用方读取每个字符串。复现：<code>examples/alloc_trace.rs</code> [6]。</p>
<pre><code>select id, status, amount, note from orders
where customer_id = $1 and status = $2 and amount &gt; $3;
</code></pre>
<table>
<thead>
<tr>
<th align="right">note 长度</th>
<th align="right">LMDB SQL</th>
<th align="right">LMDB SQL（强制拷贝）</th>
<th align="right">SQLite</th>
</tr>
</thead>
<tbody>
<tr>
<td align="right">64 B</td>
<td align="right">3.74 / 0.4</td>
<td align="right">5.22 / 1.1</td>
<td align="right">2.99 / 0</td>
</tr>
<tr>
<td align="right">1 KiB</td>
<td align="right">3.70 / 0.4</td>
<td align="right">5.33 / 10</td>
<td align="right">3.44 / 0</td>
</tr>
<tr>
<td align="right">4 KiB</td>
<td align="right">4.37 / 0.4</td>
<td align="right">8.00 / 40</td>
<td align="right">6.44 / 40</td>
</tr>
<tr>
<td align="right">16 KiB</td>
<td align="right">3.65 / 0.4</td>
<td align="right">13.07 / 160</td>
<td align="right">11.60 / 158</td>
</tr>
<tr>
<td align="right">64 KiB</td>
<td align="right">2.56 / 0.4</td>
<td align="right">18.57 / 640</td>
<td align="right">18.05 / 628</td>
</tr>
<tr>
<td align="right">256 KiB</td>
<td align="right">2.38 / 0.4</td>
<td align="right">61.58 / 2560</td>
<td align="right">66.14 / 2458</td>
</tr>
</tbody>
</table>
<p>单位：每格为"p50 耗时 µs / 每次执行分配 KiB"。</p>
<p><strong>表 2：长度函数</strong></p>
<p>查询条件和数据与表 1 相同，只把投影换成长度函数。第一列是取出 <code>note</code> 后由调用方对字符串取 <code>len()</code>；<code>char_length</code> 在 SQLite 里对应 <code>length()</code>。</p>
<table>
<thead>
<tr>
<th align="right">note 长度</th>
<th align="right"><code>select note</code>，调用方取 <code>len()</code></th>
<th align="right"><code>octet_length(note)</code></th>
<th align="right"><code>char_length(note)</code></th>
</tr>
</thead>
<tbody>
<tr>
<td align="right">64 B</td>
<td align="right">3.56 / 2.52</td>
<td align="right">3.81 / 2.51</td>
<td align="right">3.93 / 2.77</td>
</tr>
<tr>
<td align="right">4 KiB</td>
<td align="right">4.33 / 6.87</td>
<td align="right">4.77 / 2.93</td>
<td align="right">7.34 / 16.07</td>
</tr>
<tr>
<td align="right">64 KiB</td>
<td align="right">2.35 / 20.35</td>
<td align="right">2.75 / 1.95</td>
<td align="right">26.07 / 164.01</td>
</tr>
<tr>
<td align="right">256 KiB</td>
<td align="right">2.14 / 69.03</td>
<td align="right">2.35 / 1.82</td>
<td align="right">89.25 / 627.00</td>
</tr>
</tbody>
</table>
<p>单位：每格为"LMDB SQL / SQLite 的 p50 耗时 µs"。</p>
<p><strong>表 3：多重物化算子下的数据分配与计算性能</strong></p>
<p>读取与表 1 相同的行，但让字符串先后经过分组、窗口函数和排序三个物化算子。复现：<code>examples/alloc_trace.rs</code> 中的 <code>multi-op</code> 变体 [6]。</p>
<pre><code>select status, note, peak, max(peak) over (partition by status)
from (select status, note, max(amount) as peak from orders
      where customer_id = $1 and status = $2 and amount &gt; $3
      group by status, note) t
order by note desc;
</code></pre>
<table>
<thead>
<tr>
<th align="right">note 长度</th>
<th align="right">LMDB SQL</th>
<th align="right">SQLite</th>
</tr>
</thead>
<tbody>
<tr>
<td align="right">64 B</td>
<td align="right">7.91 / 4.2</td>
<td align="right">12.74 / 109</td>
</tr>
<tr>
<td align="right">4 KiB</td>
<td align="right">13.82 / 4.2</td>
<td align="right">33.19 / 757</td>
</tr>
<tr>
<td align="right">64 KiB</td>
<td align="right">84.74 / 4.1</td>
<td align="right">328.34 / 10846</td>
</tr>
<tr>
<td align="right">256 KiB</td>
<td align="right">320.24 / 4.0</td>
<td align="right">7479.64 / 32945</td>
</tr>
</tbody>
</table>
<p>单位：每格为"p50 耗时 µs / 每次执行分配 KiB"。</p>
<p><strong>表 4：100 篇 JSON / CSV 文档的解析</strong></p>
<p>每次按 <code>grp</code> 取出 100 篇文档，把结果保留到查询结束之后，再解析成结构体。LMDB SQL 的结构体字段直接借用事务里的文本；SQLite 按常见写法，每个字段一个 <code>String</code>。CSV 场景用结构相同的 <code>csv_docs</code> 表，<code>body</code> 存一行 CSV。复现：<code>examples/borrowed_parse.rs</code> [7]。</p>
<pre><code>create table json_docs (
    id   int primary key,
    grp  int,      -- 每组 100 篇
    body varchar
);
create index json_docs_grp on json_docs (grp);

select id, body from json_docs where grp = $1;
</code></pre>
<table>
<thead>
<tr>
<th align="right">文档大小</th>
<th>场景</th>
<th align="right">LMDB SQL</th>
<th align="right">SQLite</th>
</tr>
</thead>
<tbody>
<tr>
<td align="right">256 B</td>
<td>只保留原始行</td>
<td align="right">28.34 / 3.5</td>
<td align="right">24.64 / 28</td>
</tr>
<tr>
<td align="right">256 B</td>
<td>解析 JSON</td>
<td align="right">43.27 / 12</td>
<td align="right">60.17 / 35</td>
</tr>
<tr>
<td align="right">2 KiB</td>
<td>解析 JSON</td>
<td align="right">68.80 / 12</td>
<td align="right">97.20 / 214</td>
</tr>
<tr>
<td align="right">16 KiB</td>
<td>只保留原始行</td>
<td align="right">28.60 / 3.5</td>
<td align="right">137.69 / 3205</td>
</tr>
<tr>
<td align="right">16 KiB</td>
<td>解析 JSON</td>
<td align="right">187.33 / 12</td>
<td align="right">292.65 / 3212</td>
</tr>
<tr>
<td align="right">16 KiB</td>
<td>解析 CSV</td>
<td align="right">35.15 / 12</td>
<td align="right">172.15 / 3206</td>
</tr>
</tbody>
</table>
<p>单位：每格为"p50 耗时 µs / 每次执行分配 KiB"，每次执行取 100 行。</p>
<h3>表格分析</h3>
<p><strong>LMDB SQL 的分配量和数据大小无关，SQLite 的分配量跟着返回的数据走。</strong> 表 1 里 LMDB SQL 始终是 0.4 KiB；SQLite 从 4 KiB 起每行分配一次，总量约等于返回的字节数。SQLite 在 64 B 和 1 KiB 时显示 0，不表示没有拷贝：短字符串被拷进了一块复用的缓冲区，所以统计不到分配。检查返回的指针可以看到，LMDB SQL 的指针全部落在数据库文件的映射里，SQLite 的一个都没有。</p>
<p><strong>SQLite 在短数据上很快，但耗时随数据长度增长；LMDB SQL 的耗时和长度无关。</strong> 64 B 时 SQLite 只要 2.99 µs，比 LMDB SQL 还快，拷贝几十个字节的代价可以忽略。随着 <code>note</code> 变长，SQLite 的耗时和分配量一起增长，到 256 KiB 时是 66.14 µs，拷贝成了主要开销。LMDB SQL 的耗时始终在 2.4 到 4.4 µs 之间，它不读字符串的内容，自然也不受长度影响。</p>
<p><strong>差距来自拷贝，不来自 IO。</strong> 让 LMDB SQL 强制拷贝字符串后，它的耗时和分配都和 SQLite 落在同一个量级（256 KiB 时 61.6 µs 对 66.1 µs）。</p>
<p><strong><code>octet_length</code> 两边都是常数时间，不能作为零拷贝的论据。</strong> LMDB SQL 对借用的 <code>&amp;str</code> 取 <code>len()</code>；SQLite 对只作为长度函数参数的列，直接从记录头读长度，不加载内容、不读溢出页（<code>OP_Column</code> 的 <code>OPFLAG_BYTELENARG</code>），所以反而更快。</p>
<p><strong><code>char_length</code> 两边都要扫完全部字节，64 KiB 以上 LMDB SQL 快 6 到 7 倍。</strong> LMDB SQL 直接在映射的页上计数（<code>chars().count()</code>，256 KiB 档约 28 GB/s）；SQLite 先把溢出页的内容拼进 <code>malloc</code> 的缓冲区，再逐字符计数。这是"读完全部内容、处理很轻"的典型情形：计算本身便宜，拷贝占了大头，零拷贝的收益最明显。如果写入时记下字符数或"是否纯 ASCII"，<code>char_length</code> 也能做到常数时间，目前没有实现。</p>
<p><strong>每经过一个物化算子，差距就放大一次。</strong> 表 3 里 64 KiB 和 256 KiB 两档，SQLite 的分配量是返回数据的 13 到 17 倍，因为每个算子都要把记录写进去再读出来；LMDB SQL 只分配哈希表和排序数组用的约 4 KiB。LMDB SQL 的耗时仍然随 <code>note</code> 增长，原因是分组要对整条 <code>note</code> 求哈希。</p>
<p><strong>解析本身两边一样快。</strong> 16 KiB 解析 JSON 时，扣掉取数据的耗时，纯解析两边差不多（159 µs 对 155 µs）。LMDB SQL 快出的部分，几乎全来自取数据时没有拷贝。</p>
<h2>结论</h2>
<p>回到一开始的代码案例中，在同样功能的代码下可以写成这样：</p>
<pre><code>use kite_sql::db::{Transaction, TxnResultIter};
use kite_sql::storage::lmdb::LmdbStorage;
use kite_sql::Model;
use serde::Serialize;
use std::borrow::Cow;
use std::collections::HashMap;
use std::error::Error;
use std::io::Write;

#[derive(Default, Model, Serialize)]
#[model(table = "posts")]
struct Post&lt;'a&gt; {
    #[model(primary_key)]
    id: i32,
    thread_id: i32,
    parent_id: i32, // 0 表示顶层评论
    user: Cow&lt;'a, str&gt;,
    body: Cow&lt;'a, str&gt;,
}

#[derive(Serialize)]
struct Comment&lt;'a&gt; {
    #[serde(flatten)]
    post: &amp;'a Post&lt;'a&gt;,
    replies: Vec&lt;Comment&lt;'a&gt;&gt;,
}

fn build&lt;'a&gt;(children: &amp;'a HashMap&lt;i32, Vec&lt;Post&lt;'a&gt;&gt;&gt;, parent: i32) -&gt; Vec&lt;Comment&lt;'a&gt;&gt; {
    let Some(posts) = children.get(&amp;parent) else { return Vec::new() };
    posts.iter()
        .map(|post| Comment { post, replies: build(children, post.id) })
        .collect()
}

fn write_thread(tx: &amp;mut Transaction&lt;'_, LmdbStorage&gt;, thread_id: i32, out: impl Write) -&gt; Result&lt;(), Box&lt;dyn Error&gt;&gt; {
    // Post 借用 LMDB 的页，活到事务结束，所以可以先全部收齐再建树
    let mut children: HashMap&lt;i32, Vec&lt;Post&gt;&gt; = HashMap::new();
    let posts = tx
        .bind(|ctx| {
            ctx.from::&lt;Post&gt;()?
                .filter(|e| e.column(Post::thread_id())?.eq(thread_id))?
                .finish()
        })?
        // 每个字符串字段都是 Cow::Borrowed，直接指向 LMDB 的页
        .txn_orm::&lt;Post&gt;();
    for post in posts {
        let post = post?;
        children.entry(post.parent_id).or_default().push(post);
    }
    serde_json::to_writer(out, &amp;build(&amp;children, 0))?;
    Ok(())
}
</code></pre>
<p>同样的过程，换到这里是这样的：</p>
<pre><code> 数据库文件（LMDB 映射）
     │
     │  只读事务：页在事务结束前一直有效、地址不变
     ▼
 B+ 树页 / 溢出页（大值在映射里是连续的一整块）
     │
     │  行解码：读出 4 字节长度，得到（指针，长度）
     ▼
 Cow::Borrowed(&amp;str)，直接指向页
     │
     │  txn_orm：放进 Post 的字段
     ▼
 Post { user: Cow::Borrowed, body: Cow::Borrowed, .. }（活到事务结束）
     │
     │  按 parent_id 建树：只移动指针
     │  serde_json::to_writer 写出 JSON
     ▼
 JSON
</code></pre>
<p>两段代码除了类型，几乎一模一样：逐行读出 <code>Post</code>，按 <code>parent_id</code> 放进 <code>HashMap</code>，最后递归建树并序列化。不同的是，这里的 map 里只有指针：<code>Post</code> 的字符串字段是 <code>Cow&lt;'_, str&gt;</code>，在只读事务里都是 <code>Cow::Borrowed</code>，直接指向 LMDB 的页。它们的生命周期绑在事务上而不是某一行上，所以第一行的 <code>Post</code> 可以和最后一行的一起留在 map 里等着建树，直到写出 JSON 为止，字符串一次都没有被拷贝。字段用 <code>Cow</code> 而不是 <code>&amp;str</code>，是因为在写事务里页可能被改动，那时字段会退化成 <code>Cow::Owned</code> 的拷贝。rusqlite 那边不是不能借用，<code>row.get_ref</code> 就能借出 <code>&amp;str</code>，但它只活到下一次 <code>next</code>；建树要等所有行到齐，借用 <code>row</code> 的 <code>Post</code> 活不过下一次 <code>next</code>，所以放进 map 的只能是拷贝。</p>
<p>这个优势有自己的适用范围。短行、逐行处理的负载两边差不多，SQLite 还略快；写事务里字符串照样要拷贝；TPC-C 这类负载看不出变化。聚合算子把借用的行留在手里，对应的页就要留在内存里，但这些页是 SQLite 读同样数据时也要进页缓存的干净文件页，内核可以随时回收；回收不掉的堆内存反而是零拷贝这边更少（表 3）。借用的结果活多久，只读事务就要开多久，而 LMDB 的旧页在有读者引用时不能回收，长时间持有结果又伴随大量写入，数据库文件会膨胀。</p>
<p>感谢阅读。文中的实现和测试代码都在 KiteSQL 仓库里：<a href="https://github.com/KipData/KiteSQL" rel="noopener noreferrer">https://github.com/KipData/KiteSQL</a>。</p>
<h2>参考</h2>
<p>[1] Athinagoras Skiadopoulos, Qian Li, Peter Kraft, Kostis Kaffes, Daniel Hong, Shana Mathew, David Bestor, Michael Cafarella, Vijay Gadepally, Goetz Graefe, Jeremy Kepner, Christos Kozyrakis, Tim Kraska, Michael Stonebraker, Lalith Suresh, Matei Zaharia. <em>DBOS: A DBMS-oriented Operating System</em>. PVLDB, 15(1): 21–30, 2022. doi:<a href="https://doi.org/10.14778/3485450.3485454" rel="noopener noreferrer">10.14778/3485450.3485454</a>. PDF: <a href="https://www.vldb.org/pvldb/vol15/p21-skiadopoulos.pdf" rel="noopener noreferrer">https://www.vldb.org/pvldb/vol15/p21-skiadopoulos.pdf</a></p>
<p>[2] GraphQL Specification, September 2025 Edition. GraphQL Foundation. <a href="https://spec.graphql.org/September2025/" rel="noopener noreferrer">https://spec.graphql.org/September2025/</a>. 项目主页：<a href="https://graphql.org/" rel="noopener noreferrer">https://graphql.org/</a></p>
<p>[3] Andrew Crotty, Viktor Leis, Andrew Pavlo. <em>Are You Sure You Want to Use MMAP in Your Database Management System?</em> CIDR 2022. 项目页：<a href="https://db.cs.cmu.edu/mmap-cidr2022/" rel="noopener noreferrer">https://db.cs.cmu.edu/mmap-cidr2022/</a>. PDF: <a href="https://db.cs.cmu.edu/papers/2022/cidr2022-p13-crotty.pdf" rel="noopener noreferrer">https://db.cs.cmu.edu/papers/2022/cidr2022-p13-crotty.pdf</a>。候选标题见 Andy Pavlo 的讲座录像 <a href="https://www.youtube.com/watch?v=1BRGU_AS25c" rel="noopener noreferrer">https://www.youtube.com/watch?v=1BRGU_AS25c</a> 00:52 处。</p>
<p>[4] LMDB API 文档，Symas Corporation：<a href="http://www.lmdb.tech/doc/group__mdb.html" rel="noopener noreferrer">http://www.lmdb.tech/doc/group__mdb.html</a>。<code>MDB_val</code> 一节说明返回的值"valid only until a subsequent update operation, or the end of the transaction"，并且"commonly point into the database itself"；<code>MDB_WRITEMAP</code> 一节说明默认的只读映射能防止野指针写坏数据库。</p>
<p>[5] PingCAP. <em>TiDB Computing</em>，"Mapping table data to Key-Value" 一节：<a href="https://docs.pingcap.com/tidb/stable/tidb-computing/" rel="noopener noreferrer">https://docs.pingcap.com/tidb/stable/tidb-computing/</a></p>
<p>[6] <code>examples/alloc_trace.rs</code>：<a href="https://github.com/KipData/KiteSQL/blob/main/examples/alloc_trace.rs" rel="noopener noreferrer">https://github.com/KipData/KiteSQL/blob/main/examples/alloc_trace.rs</a>。运行：<code>cargo run --release --example alloc_trace --features lmdb,orm</code></p>
<p>[7] <code>examples/borrowed_parse.rs</code>：<a href="https://github.com/KipData/KiteSQL/blob/main/examples/borrowed_parse.rs" rel="noopener noreferrer">https://github.com/KipData/KiteSQL/blob/main/examples/borrowed_parse.rs</a>。运行：<code>cargo run --release --example borrowed_parse --features lmdb,orm</code></p>
]]></description><pubDate>2026-11-09 06:32:55</pubDate></item><item><title>【Rust日报】2026-11-09 ping 只发出去一包</title><link>https://rustcc.cn/article?id=a8eb1a43-fdc0-46e2-8313-921ff04f172a</link><description><![CDATA[<h2>ping 只发出去一包</h2>
<p>这是 Redox OS 2026 年 11 月 8 日的一篇开发记录，作者是 MJ Pooladkhay。记录写 Virtio-FS 驱动，以及 Redox 作为客户机跑在 Cloud Hypervisor 上。Reddit 转发帖作者是 ribbon_45，采集时 23 分、2 条评论。帖子时间是 2026-11-08 15:54 UTC。</p>
<p>Virtio-FS 驱动已经合进 base 仓库的 <code>drivers/storage/virtio-fsd</code>。让 Redox 在 Cloud Hypervisor 上启动到 shell 的内核、bootloader 和驱动改动也已经合入。文章写了两个方向。客户机方向是这篇的主体：Redox 跑在 Cloud Hypervisor 里，去掉以前在 QEMU 上用的老 PC 硬件依赖。宿主方向还是提案。客户机这部分由 NGI Zero Commons 和 NLnet 的 Virtualized Redox 资助。</p>
<p>Virtio-FS 让虚拟机使用宿主机上的目录。Redox 的 virtio-fsd 用 FUSE 协议，经 virtqueue 和宿主机上的 virtiofsd 通信。每个共享目录是一个 scheme，路径是 <code>/scheme/virtio-fs.&lt;tag&gt;</code>，tag 来自宿主机。驱动在 QEMU 和 Cloud Hypervisor 上都能用，带自测。目录项缓存用 LRU，不用的项通过 FUSE_FORGET 告诉宿主忘掉。</p>
<p>启动到 shell 时记下的问题和对应合并请求：</p>
<ul>
<li>bootloader!61：传给 ExitBootServices 的 memory-map key 过期，bootloader 出不了 UEFI。</li>
<li>base!422：virtio-blk 被识别成 transitional VirtIO 设备。Cloud Hypervisor 只提供 modern 设备，根磁盘没有出现。</li>
<li>kernel!762：登录提示出现了，键盘输入进不去。串口输出是轮询的，输入走中断，中断指向不存在的 8259。legacy 中断改走 I/O APIC。</li>
<li>没有 PIT，也没有 HPET，定时等待不会醒来。</li>
</ul>
<p>文章里的 ping 记录是 <code>ping -c 4 1.1.1.1</code>。第一包大约 15.304 ms 有回复，后面三包没有发出去。第一包是马上发的。后面的包要 time scheme 在 1 秒后唤醒。内核里 <code>timeout::trigger</code> 原来只由 PIT 中断处理函数调用。QEMU 上 HPET 走同一条中断线。Cloud Hypervisor 没有 PIT，也没有 HPET，这个处理函数不跑。读时钟仍然有效，kvmclock 会把 TSC 换成纳秒，所以第一包的往返时间是正常的。时钟中断没有在跑。调度器的抢占 tick 也停了，系统变成协作式调度。</p>
<p>kernel!762 的修复是：没有 HPET 时，每个 x86 CPU 用自己的 local APIC timer，模式是 TSC-deadline。tick 周期用 kvmclock 已有的比例，从纳秒换成 TSC tick。处理函数补上原来 PIT 处理函数的工作，投递到期的 timeout，并驱动调度器 tick。修完以后四包都发出去，间隔 1 秒，抢占调度恢复。</p>
<p>文章写，现在还不能当可部署实例。Cloud Hypervisor 上 Redox 还不能自己关机。重启已经可以，走键盘控制器的 reset 线。后面的工作包括兼容 cloud-init 的首次启动配置、PVH 直接启动、virtio-vsock、virtio-balloon、virtio-rng，以及快照恢复。现有 VirtIO 设备是 virtio-blk、virtio-net、virtio-fs、virtio-gpu。Redox 还不做 PVH，仍然走 UEFI。Firecracker 没有 UEFI，PVH 做完才开这条路。</p>
<p>SweetEbb9819 的评论有 2 分，写用 stub Linux VM 做硬件驱动值得注意。phip1611 的评论有 1 分，写看到更多人用 Cloud Hypervisor。</p>
<p>原文链接：https://www.redox-os.org/news/virtualized-redox/</p>
<h2>Actix Web 这个季度发了 100 多个 crate</h2>
<p>这是 Actix Web 维护者 robjtede 发在 r/rust 的一份季度报告。采集时 21 分、1 条评论。帖子时间是 2026-11-08 23:51 UTC。actix-web 仓库采集时 24934 星、1994 个 fork，许可证 Apache-2.0。</p>
<p>报告写，这个周期 Actix 生态有 100 多次 crate 发布。HTTP QUERY 方法进了路由宏、手工路由和 awc 客户端。宏路由用 <code>#[query]</code>，手工路由用 <code>web::query()</code>。h2c 和 TCP no-delay 改成默认开启。报告写，某些负载的内存占用最多大约少 99%。中间件可以用 <code>Error::add_response_mapper()</code> 改错误响应。CORS 中间件已经用它修一个旧 bug。静态文件 <code>Files::new()</code> 可以有多个根目录。<code>Files::try_compressed()</code> 可以发预先压缩好的资源。</p>
<p>安全修复写在报告里，也能对上 GitHub release。解压后的请求体受已配置的大小限制。大窗口 Brotli 解压关掉。畸形的 chunked 结束符会被拒绝。静态文件服务修了用户可控文件名带来的 stored XSS，以及 Windows 隐藏文件保护绕过。报告写，自动解压这套隐式行为会在 v5 去掉。报告建议执行 <code>cargo update -p=actix-web</code>。</p>
<p>GitHub 上能对上的发布包括这些。actix-web 4.18.2 发布于 2026-11-06，默认 form payload 上限提到 32 KiB。form、JSON、bytes、string extractor 的解压后大小受同一套限制，公告编号 GHSA-qph8-7859-gq2f。actix-http 3.20.3 关掉大窗口 Brotli，公告编号 GHSA-7p9w-p5qm-5gc5。actix-http 3.20.2 拒绝把单独的 LF 当作 chunked 最后结束符，公告编号 GHSA-2q7f-mf9f-5qhr。actix-http 3.21.0 发布于 2026-11-08，默认打开 <code>TCP_NODELAY</code>。这条 release 写，HTTP/1.x 内存使用有明显改进。actix-http 3.20.5 在优雅关闭时向 HTTP/2 连接发 GOAWAY，在途 stream 结束后关掉连接。actix-multipart 0.8.7 给 <code>MultipartFormConfig::max_parts()</code> 加了默认上限 100。actix-router 0.5.5 的 MSRV 是 1.89，路径参数可以抽成 <code>Vec</code> 或 tuple。</p>
<p>DavidXkL 的评论有 2 分，写自己主要用 Actix Web，会去看这些改动。</p>
<p>原文链接：https://www.reddit.com/r/rust/comments/1xrct2i/actix_web_q4_report/</p>
<h2>cel-rust 上线浏览器 playground</h2>
<p>cel-rust 是用 Rust 写的 Common Expression Language 解释器。CEL 是一门非图灵完备的表达式语言，语法接近 C。仓库在 https://github.com/cel-rust/cel-rust 。采集时 713 星、84 个 fork，许可证 MIT，最近推送是 2026-11-08。crates.io 上的包名是 <code>cel</code>，当前版本 0.15.0，发布于 2026-10-06，累计下载 5719704，近期下载 4915558，18 个版本。这篇帖子写的是 playground。0.15.0 没有随这篇帖子更新。</p>
<p>作者 alexsnaps 2026 年 11 月 8 日 12:25 UTC 在 r/rust 发帖。采集时 18 分、5 条评论。帖子写，cel-rust 几年前加了 Wasm 支持，这次把 playground 放到浏览器里，地址是 https://cel-rust.github.io/playground 。页面上可以开关自动运行。关掉时，Ctrl+Enter 或 Run 才会求值。表达式、输入和输出可以开语法高亮。高级模式在表达式旁边放 AST 浏览器。主题有 System、Light、Dark。代码主题列出 GitHub、Catppuccin、Solarized、Dracula、Tokyo Night Moon。设置和上次写的内容留在这个浏览器里。</p>
<p>README 的安装方式是 <code>cargo add cel</code>。示例编译 <code>add(2, 3) == 5</code>，用 <code>Env::stdlib()</code> 和 <code>Context::with_env</code>，再 <code>context.add_function</code> 注册 <code>add</code>。</p>
<p>Shnatsel 的评论有 15 分，问 CEL 是什么。IvanIsCoding 的评论有 8 分，写 CEL 非图灵完备，语法像 C。评论里的例子以 <code>request.method in ['GET', 'HEAD'] &amp;&amp; request.path.startsWith('/api</code> 开头，采集时后面被截断。syncerr 的评论写，自己用 CEL 做 protobuf 消息校验，并提到 protovalidate 和 protobuf-es。</p>
<p>原文链接：https://cel-rust.github.io/playground</p>
<h2>原生 Mac GUI 工具包问要不要只做 AppKit</h2>
<p>这是 layla4you2 发在 r/rust 的一篇提问。作者在做一套面向 Mac 的 Rust GUI 工具包。帖子里没有放仓库链接。采集时 0 分、16 条评论。帖子时间是 2026-11-09 01:36 UTC。</p>
<p>作者列出现有选择。webview 用 Tauri。自绘 UI 用 egui、Iced、Slint。另一种是 SwiftUI 前端加 Rust FFI。作者写，文本框行为、VoiceOver 和大表格是这些方案里经常对不上 Mac 应用习惯的地方。</p>
<p>作者写，这套工具包里每个 view 是真正的 AppKit 控件。表格用 <code>NSTableView</code>。作者写，一百万行滚动没有问题，因为 AppKit 只取可见行。状态放在 signal 里。改一个 signal 只更新受影响的控件，不重建 view tree。菜单、工具栏、文档、undo、拖放和 Dark Mode 走系统。还有一个 self-check 模式，会驱动应用并检查屏幕上的内容。</p>
<p>podule-games 的评论有 4 分，写只做 Mac 会是阻碍，因为这样就在和 Swift 竞争。rjzak 的评论有 2 分，写自己从 Slint 换到 egui，只做 Mac 的工具包可以接受，需要菜单栏支持。作者回复有 1 分，写菜单栏已经覆盖，包括应用自己的菜单和菜单栏右侧图标。Shoddy-Childhood-511 的评论有 1 分，写对只做 Mac 的 GUI 工具包没有兴趣。</p>
<p>原文链接：https://www.reddit.com/r/rust/comments/1xrezk6/what_would_you_want_from_a_native_mac_gui_toolkit/</p>
<hr>
<p>From Rust中文社区 Mike</p>
<p>社区学习交流平台订阅：</p>
<ul>
<li><a href="https://rustcc.cn/" rel="noopener noreferrer">Rustcc论坛: 支持rss</a></li>
<li><a href="https://rustcc.cn/article?id=ed7c9379-d681-47cb-9532-0db97d883f62" rel="noopener noreferrer">微信公众号：Rust语言中文社区</a></li>
</ul>
]]></description><pubDate>2026-11-09 02:11:56</pubDate></item><item><title>【rpi 更新】11 毫秒启动、14 MiB 安装包：rpi 凭什么比同类编码 Agent 小一号</title><link>https://rustcc.cn/article?id=2f281dfb-18d7-4430-9662-1839a3cdba8d</link><description><![CDATA[<p>这台机器上装了 5 个编码 Agent。同一个动作，打印版本号：rpi 11.1 ms、claude 17.2 ms、codex 33.8 ms、Pi 151.2 ms、opencode 867.0 ms。</p>
<p>装到磁盘上的东西差得更多：rpi 的 Windows 发布包 14.35 MiB，claude 一个可执行文件 231 MB，codex 284 MiB，opencode 172 MiB，Pi 是 384.9 MiB 的包外加 91.2 MiB 的 Node。</p>
<p>最慢和最快差 78 倍，最大和最小差 27 倍。这不是谁调优得更好一点，是构造不同：rpi 是一个不带运行时的 Rust 二进制，另外几个要么自带 Node/Bun 运行时，要么拖着上百兆的依赖树。</p>
<h2>先看表，再说条件</h2>
<table>
<thead>
<tr>
<th>工具</th>
<th>启动（<code>--version</code> 中位，15 次）</th>
<th>装到磁盘上的体积</th>
<th>实现</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>rpi</strong></td>
<td><strong>11.1 ms</strong></td>
<td><strong>14.35 MiB</strong>（发布包）</td>
<td>Rust 单二进制</td>
</tr>
<tr>
<td>claude</td>
<td>17.2 ms</td>
<td>231.0 MB</td>
<td>单可执行文件</td>
</tr>
<tr>
<td>codex</td>
<td>33.8 ms</td>
<td>284.4 MiB</td>
<td>单可执行文件</td>
</tr>
<tr>
<td>Pi</td>
<td>151.2 ms</td>
<td>384.9 MiB 包 + Node 91.2 MiB</td>
<td>TypeScript / Node</td>
</tr>
<tr>
<td>opencode</td>
<td>867.0 ms</td>
<td>172.2 MiB</td>
<td>单可执行文件</td>
</tr>
<tr>
<td>仅 <code>node --version</code></td>
<td>19.4 ms</td>
<td>Node 91.2 MiB</td>
<td>运行时的地板</td>
</tr>
</tbody>
</table>
<p>条件：同一台机器（Windows 11，Ryzen 9 7950X，32 逻辑处理器），每个工具跑 15 次取中位数。rpi 和 Pi 都用离线开关并指向独立的空配置目录，避免加载本机插件。claude、codex、opencode 按安装后的默认行为跑，它们没有对应的离线开关。体积是发布包或已安装可执行文件的实际大小；rpi 那一栏是 zip 发布包，解压出的二进制会比它大一些。</p>
<p><img src="https://blog.bigfisharc.cn/images/rpi-small-agent/startup.png" alt="启动耗时：rpi 11.1 ms 最短，opencode 867.0 ms 最长"></p>
<p><img src="https://blog.bigfisharc.cn/images/rpi-small-agent/size.png" alt="磁盘体积：rpi 14.35 MiB，其余四个在 172–385 MiB，Pi 还要再加 91.2 MiB 的 Node"></p>
<p>图 1 和图 2 把同一张表画成条形。看启动那张时注意 <code>node</code> 那一根：单是启动运行时就要 19.4 ms，任何 Node 系工具都不可能比它更快。看体积那张时注意 Pi 的两段颜色，工具本体之外还要单算一份 Node 运行时。</p>
<p><code>--version</code> 只测进程创建到打印版本，不含任何模型请求。它衡量的正是你每次敲命令、每次被编辑器拉起都要付的那笔启动成本。我这 15 次落在 10.4 ms 到 17.3 ms 之间，你重跑很可能得到 12–13 ms，属同一量级。同一台机器上单独跑 <code>node --version</code> 就要 19.4 ms，这是任何 Node 系工具起跑线的地板。</p>
<p>需要说清一处：启动这一项 rpi 是五个里最快的，但比 claude 只快 6 ms，差距远没有体积那么大。真正拉开数量级的是体积（14 MiB 对 231–385 MB）和内存，下一节说内存。</p>
<h2>内存：地板 12 MiB，空闲 1 MB 出头</h2>
<p>官方离线基准里，rpi 空配置刚就绪的常驻内存是 12.0 MiB，同机对照的 Pi 是 91.5 MiB，冷启动快 10.7 倍（Windows 11、release、离线、每次用新的空配置目录，基准版本 rpi 0.3.6 / pi 0.87.1）。</p>
<p>我自己这台机器上的快照是另一个尺度。31 个 <code>rpi.exe</code> 里，11 个闲置会话的驻留内存只有 1.12 MB 到 1.67 MB，加起来 16.2 MB。10 个正在跑会话的进程在 33.28 MB 到 111.91 MB 之间，中位 48.3 MB。4 个给编辑器用的宿主进程中位 87.1 MB。</p>
<p><img src="https://blog.bigfisharc.cn/images/rpi-process-promo-low/idle-sessions.png" alt="同一父进程下 14 个会话进程：11 个闲置的驻留只有 1 MB 出头，3 个在工作的在 45–143 MB"></p>
<p>图 3 是同一批进程的对照：14 个会话进程挂在同一个父进程下，闲下来的 11 个驻留贴到 1 MB 线，还在干活的 3 个是 45.67 MB、118.99 MB 和 143.07 MB。同一份程序，差别只在有没有活在跑。会话状态逐帧写进 JSONL，进程安静时没有请求在跑，驻留页就能被系统换出去。</p>
<p>12 MiB 和 48 MB 并不矛盾：前者是空配置刚能应答的地板，后者是带着扩展、跑过多轮工具调用、留着会话历史的真实工作进程。地板决定你能不能把它塞进小机器，后者决定你干活时要付多少。</p>
<h2>这三个数字能换来什么</h2>
<h3>容器和 CI 里不用再铺运行时</h3>
<p>一个 14 MiB 的二进制丢进镜像就能跑，不用为了一个 Agent 在镜像里装一层 Node 和一份 node_modules。空配置就绪 12 MiB，512 MB 的容器留得下。</p>
<h3>多开会话不心疼</h3>
<p>一个会话一个进程，闲下来驻留掉到 1 MB 级，11 个这样的实例加起来 16.2 MB；关掉一个立刻还回去。按我这台机器的中位数估，10 个活跃会话加 4 个宿主约 830 MB，剩下的就是闲置的那几个，它们几乎不占物理内存。</p>
<h3>每次调用 11 毫秒</h3>
<p>编辑器每拉起一次、脚本里每循环一次、CI 每个 job 都要启动一次。按一天 300 次算，rpi 是 3.3 秒，Pi 是 45.4 秒。次数越多，这个差距越明显，而且它不需要你改任何配置。</p>
<h2>为什么能这么小</h2>
<p>一是没有运行时。Rust 直接编成机器码，不需要 Node 或 Bun 陪着一起启动，那 19.4 ms 的地板和 91.2 MiB 的运行时都不存在。</p>
<p>二是不带依赖树。扩展是独立的 cdylib，用到才加载；Pi 那种 384.9 MiB 的目录，是把它所有可能用到的依赖都先装到磁盘上。</p>
<p>三是会话不常驻内存。会话状态逐帧写进 JSONL 文件，进程闲下来时可以被系统换出，这就是那 11 个 1 MB 会话的来历。它的代价写在后面。</p>
<h2>自己验一遍</h2>
<p>rpi 发布的是单个二进制，v0.21.1 的 Windows 包 14.35 MiB、macOS arm64 包 13.40 MiB、Linux gnu 包 14.70 MiB（GitHub release 元数据，2026-11-07），不需要 Node 运行时：</p>
<pre><code>cargo install rpi-cli
rpi --version
</code></pre>
<p>不想装 Rust 工具链就用预编译安装脚本：</p>
<pre><code>curl -fsSL https://raw.githubusercontent.com/bigfish1913/pi-rust/main/scripts/install.sh | sh
</code></pre>
<p>装好后你可以照着上面的表自己量一遍：</p>
<pre><code># 启动：连跑 15 次取中位
$t = 1..15 | ForEach-Object {
  $sw = [Diagnostics.Stopwatch]::StartNew()
  &amp; rpi --version | Out-Null
  $sw.Stop(); $sw.Elapsed.TotalMilliseconds
}
($t | Sort-Object)[7]

# 内存：开几个会话后看驻留与提交
Get-Process | Where-Object { $_.ProcessName -eq 'rpi' } |
  Select-Object Id,
    @{n='WS_MB'; e={[math]::Round($_.WorkingSet64/1MB, 1)}},
    @{n='Priv_MB'; e={[math]::Round($_.PrivateMemorySize64/1MB, 1)}} |
  Sort-Object WS_MB -Descending
</code></pre>
<p>11 毫秒、14 MiB、12 MiB 就绪、1 MB 空闲：四个数字都在上面，也都能在你自己的机器上重跑一遍。</p>
<ul>
<li>官网：<a href="https://rpi.bigfisharc.cn/" rel="noopener noreferrer">https://rpi.bigfisharc.cn/</a></li>
<li>文档：<a href="https://rpi.bigfisharc.cn/docs.html" rel="noopener noreferrer">https://rpi.bigfisharc.cn/docs.html</a></li>
<li>仓库：<a href="https://github.com/bigfish1913/pi-rust" rel="noopener noreferrer">https://github.com/bigfish1913/pi-rust</a></li>
</ul>
<hr>
]]></description><pubDate>2026-11-08 15:42:25</pubDate></item><item><title>MoteDB v0.18 发布</title><link>https://rustcc.cn/article?id=f0ede4a8-f439-4454-9aca-d76bd91acba4</link><description><![CDATA[<h2>MoteDB 是什么</h2>
<p>纯 Rust 写的<strong>嵌入式多模态数据库</strong>，面向具身智能（机器人、AR、边缘设备）：向量（DiskANN）、全文（BM25+jieba）、时序、空间四类索引 + 列存 + ACID + SQL，单进程嵌入式零依赖，MIT 协议。<code>cargo add motedb</code> / <code>pip install motedb-python</code> / <code>npm install motedb</code> 即用。</p>
<ul>
<li>GitHub: https://github.com/motedb/motedb （0.18 全家福: https://github.com/motedb/motedb/releases/tag/v0.18.0 ）</li>
<li>文档: https://motedb.github.io · 10 分钟可跑的 demo notebook 在仓库 <code>examples/motedb-demo.ipynb</code></li>
</ul>
<pre><code>-- 0.18 新语法一览
CREATE CONTINUOUS VIEW sensor_1m AS
  SELECT TIME_BUCKET('1m', ts) AS minute, device,
         AVG(v) AS v_avg, FIRST(v, ts) AS v_open, LAST(v, ts) AS v_close
  FROM sensor GROUP BY minute, device;   -- 查询时惰性刷新，当普通表用

ALTER TABLE frames DROP COLUMN legacy;   -- 逻辑删除，schema 演进
ALTER TABLE frames RENAME COLUMN v TO voltage;
</code></pre>
<p>Rust 侧新增 <code>Database::open_shared_readonly</code>：一个写者 + N 个读者进程同时挂活动目录（flock SH/EX），只读会话<strong>零字节落盘</strong>（目录字节指纹验收）。</p>
<h2>v0.18 的主线：把一笔技术债连本带利还掉</h2>
<p>v0.17 五轮猎虫收尾时留下一个实证结论：四轮"同一谓词不同路径不同结果"类缺陷全部同根——<strong>SQL 比较语义存在于 11 个互不相识的实现里</strong>。0.18 工作流 A 就是还这笔债，做法值得写三段。</p>
<h3>1. 先写特征测试，再动实现（PR-1）</h3>
<p>新建 <code>semantics.rs</code> 作为唯一语义层：<code>Cmp3</code> 三值 + <code>OrdOp</code> + <code>coerce_pair</code>（Cow 零分配归一：Bool↔数值、方括号向量文本）。动代码之前先写 38 行类型对 × 8 算子的矩阵测试 + 把引擎各路径的 NULL/Kleene 现状用 12 个探针<strong>锁定</strong>——先证明"现状是什么"，再谈"改成什么"。</p>
<h3>2. 差分测试是照妖镜（PR-4）</h3>
<p>同一份随机谓词，九条执行路径对拍（SELECT / DELETE / UPDATE / JOIN 两种写法 / 子查询 IN / 聚合 / GROUP BY+HAVING）。<strong>上线第一天就揪出 7 个预先存在的跨路径分歧</strong>，全是 0.17 就在的：</p>
<ul>
<li><code>NOT (flag IN (0,1))</code> 在某条路径放行<strong>全部行</strong>——In 分支用了裸 <code>==</code>，NULL 成员匹配了 NULL 值；</li>
<li>UPDATE 探针第一版用 <code>a = a + 1000</code> 探测，忘了 <code>NULL + 1000 = NULL</code>——行原样留下，差分器把它当分歧报出来，<strong>测试工具自己的 bug 也被差分抓了</strong>；</li>
<li>最有意思的裁决：<code>a NOT IN (0, '7')</code> 里哈希归一把纯数字文本 <code>'7'</code> 当微秒时间戳解析（时间戳解析器 first-try 连带），与相等路线分歧。裁决是<strong>纯数字文本不再按时间戳归一</strong>——推翻了自己 PR-1 里锁定的怪癖。特征测试不是圣旨，差分对拍才是。</li>
</ul>
<h3>3. CI 棘轮：只降不升（PR-5）</h3>
<p><code>src/sql</code> 里裸值比较点从 49 处收编到清单化，门禁脚本数违规、上限写在单独文件里（当前 50），<strong>只许降不许升</strong>，行内注释可豁免。从此"再往执行器里手写一个 <code>==</code>"过不了 CI。</p>
<h2>产品侧补的四个硬伤</h2>
<ul>
<li><strong>B1 多进程共享只读</strong>: 写者持 EX 时读者明确报错; 脏目录（WAL 非空/未干净关闭）拒绝打开——只读者永不修复，修复是写者的职责。零落盘用<strong>目录字节级指纹</strong>验收。</li>
<li><strong>B2 Schema 演进</strong>: <code>DROP COLUMN</code> 是逻辑删除——列段按位置存列，物理宽度绝不能收窄（dropped 槽位写 NULL 占位），bincode 位置敏感所以目录镜像链 modern→V017→V013 保跨版本兼容。</li>
<li><strong>B3 向量召回硬化</strong>: 先造测量工具（对抗数据 p50/worst 如实基线），发现真病灶是"列段写缓冲新行对索引路径不可见"——补合并层 + 新鲜度护栏（未入图占比超阈值降级暴力扫并后台重建）。<strong>诚实声明</strong>：对抗簇数据 p50 0.85-1.0 是真实水平，不藏。</li>
<li><strong>B5 连续视图</strong>: 惰性全量重算 v1，语义如实声明。开发日志里最连续的踩坑是：把视图物化结果注册进 CTE map 后，连环揪出<strong>三个预存在的 CTE bug</strong>——<code>WITH x AS (...) SELECT COUNT(*) FROM x</code> 之类形态此前会硬报 Table not found。新特性的测试强度反过来给老特性抓了虫。</li>
</ul>
<h2>发布工程</h2>
<p>全量门禁 4300+ 测试从 ~2.5h 提到 ~30min（独立 release-test profile 关 LTO——300+ 测试二进制的链接时间才是大头）; EXPLAIN 改为决策点实录（thread-local trace 重放执行，计划永不与实际漂移）; 文档版本单源 + CI 校验（README 曾三处三个版本号）; Release 页由 tag 自动生成; 发版前发现 <code>cargo package</code> 会把未跟踪的本地文件收进包——exclude 补 <code>.zcode/</code> 后 <code>package --list</code> 核验归零。</p>
<h2>给同行的三个小教训</h2>
<ol>
<li><strong>差分对拍 &gt; 特征测试</strong>。特征测试锁现状，差分测试找真相——两者冲突时（纯数字文本时间戳）信差分。</li>
<li><strong>新功能是老代码的压力测试</strong>。连续视图把 CTE map 用满，三个陈年 bug 当场现形。给新功能写测试时，顺手把相邻老路径也拍一遍。</li>
<li><strong>位置敏感的序列化（bincode）加字段 = 全部旧目录损坏</strong>。用镜像链在解码边界转换，别在结构体上加带默认值的尾部字段赌运气。</li>
</ol>
<h2>链接</h2>
<ul>
<li>GitHub: https://github.com/motedb/motedb （good first issue 已挂 10 条，欢迎首贡献）</li>
<li>Release Notes: https://github.com/motedb/motedb/releases/tag/v0.18.2</li>
<li>文档: https://motedb.github.io · 能力清单（每行对应实现与测试）: 仓库 <code>CAPABILITIES.md</code></li>
</ul>
<hr>
]]></description><pubDate>2026-11-08 03:41:05</pubDate></item><item><title>【Rust日报】2026-11-08 写入成功，文件却是空的</title><link>https://rustcc.cn/article?id=f107360a-b205-4e25-bdd2-b3ea6aaf8d30</link><description><![CDATA[<h2>导出命令返回 Ok，CSV 却是空的</h2>
<p>这是 Thessi4Coil 发在 r/rust 的一篇故障记录。记录里的导出命令报告成功，留下的 CSV 文件是空的。采集时 0 分、10 条评论。帖子时间是 2026-11-07 17:20 UTC。</p>
<p>导出路径上的写入都用了 <code>write_all(...)?</code>。作者写，没有忽略 <code>Result</code>，也没有 <code>unwrap</code>。导出内容小，能放进 <code>BufWriter&lt;File&gt;</code> 的缓冲区。每次 <code>write_all</code> 返回 <code>Ok(())</code>，字节还在内存里。函数末尾返回 <code>Ok(())</code>。之后 writer 被 drop。</p>
<p><code>std::io::BufWriter</code> 的文档写，drop 之前必须调用 <code>flush</code>。drop 会尝试把缓冲区写出去，这个过程里的错误会被忽略。调用 <code>flush</code> 可以先把缓冲区清空，drop 时就不再做文件操作。文档写默认缓冲区容量目前是 8 KiB，以后可能变。作者写，当时文件系统已满，真正的写入失败发生在函数已经返回成功之后。</p>
<p>作者给出的复现用 Linux 的 <code>/dev/full</code>。打开它并包进 <code>BufWriter</code>，写入短字符串，<code>write_all</code> 返回 <code>Ok(())</code>。显式调用 <code>flush()</code> 得到 <code>ENOSPC</code>。</p>
<p>作者写，当场的修改是在返回成功前加上 <code>flush()?</code>。导出路径改成先写临时文件，写完再 rename，避免失败的导出替换掉原来有效的文件。作者写，<code>flush()</code> 和 <code>sync_all()</code> 是两件事，这次失败停在字节还没有写到文件上。</p>
<p>thegentlecat 的评论有 31 分，把帖子的写法标成 “It wasn’t X, it was Y” speedrun。complexicat 的评论有 24 分，内容是 “slop”。EpochVanquisher 的评论有 5 分，写文件的最后一次使用应该是 flush、sync 或 close，按需要选一个。Kenzzer 的评论有 3 分，写这是常见的 I/O 问题，记得 flush 缓冲区。</p>
<p>原文链接：https://www.reddit.com/r/rust/comments/1xqboal/our_rust_cli_returned_ok_after_failing_to_write/</p>
<h2>RAVET 0.1.0 一次导出五种插件格式</h2>
<p>RAVET 是用 Rust 写音频插件和独立音频程序的框架，全称 Rust Audio Visual Engineering Toolkit。c0wcud 2026 年 11 月 7 日在 r/rust 发了 0.1.0。采集时 0 分、2 条评论。仓库在 https://github.com/mo0kid/ravet 。采集时仓库 1 星、0 个 fork、121 个提交，许可证 GPL-3.0。GitHub release 写，标签 v0.1.0 发布于 2026-11-05。仓库写还没有 crates.io 包，使用方式是依赖这个 tag。最低 Rust 版本是 1.88。</p>
<p>0.1.0 的发布说明写，产品描述写一次，里面有 processor、参数、保存的状态和编辑器，RAVET 再构建各个格式。README 写，0.1.0 把一份产品描述导出成 CLAP、VST3、AUv2、AAX 和 LV2，平台是 macOS、Windows 和 Linux。作者帖子里的格式名单是 VST3、CLAP、LV2、Audio Unit 和 AAX。</p>
<p>发布说明写，Web 编辑器在 macOS、Windows、Linux 上可用，覆盖该平台上支持编辑器的格式。MIDI 输入和输出在每个格式里可用。插件可以告诉宿主延迟变化，并在格式支持时报告 tail。自动化可以按采样应用，也可以每个 block 一次。Bypass 可以由 processor 处理，也可以由 RAVET 处理，方式包括硬切换、交叉淡化和冻结直通。每个格式都处理 denormal。DSP 部分列出立体声 FIR 滤波器和立体声 oversampler。</p>
<p>作者帖子写，支持路径上的音频线程不分配、不加锁、不阻塞。核心音频、事件和参数 crate 用 <code>no_std</code>，没有分配器。参数有永久 key，调整顺序后宿主 ID 保持稳定。保存的状态带版本和迁移钩子。编辑器是嵌在宿主窗口里的 web view，使用 WebKit、WebView2、WebKitGTK，通过一个小的 JS API 和插件通信。</p>
<p>最小例子是 tempo-synced delay。作者写 DSP 大约 350 行普通 Rust，插件描述不到 80 行，目录是 <code>examples/delay-plugin</code>。更大的例子是 DW-303，一个开源的 TB-303 风格合成器和音序器，仓库在 https://github.com/mo0kid/DW-303 。</p>
<p>发布说明记下的测试机是这些。macOS Apple silicon 上，delay 例子跑过 REAPER 的 CLAP、LV2、AU，Ableton Live 12 的 VST3，Logic Pro 的 AU，Pro Tools 的 AAX。Linux 和 Windows 11 上跑过 REAPER 的 CLAP、VST3、LV2，带编辑器。macOS Intel、Rosetta 下跑过 REAPER 的 CLAP、VST3、LV2、AU。Windows 上的 AAX 能构建并通过检查，还没有在 Windows 版 Pro Tools 里跑过。独立音频和 MIDI 通过 DW-303 在三个平台上跑过。</p>
<p>发布说明写了限制。导出层构建的插件处理 32-bit float，一条主总线。Pro Tools 里，编辑器中输入的值在点到别处时才应用。Windows 编辑器还没有跟随显示缩放。作者写，闭源产品另有商业许可，年收入 5 万美元以下免费。README 写，最终商业协议还没定，README 本身不授予商业许可。</p>
<p>complexicat 的评论有 4 分，写这条帖子违反 rule 7。作者回复有 -1 分，写发帖前读过 rule 7，插件已经在 REAPER、Live、Logic 和 Pro Tools 里跑过。</p>
<p>原文链接：https://github.com/mo0kid/ravet/releases/tag/v0.1.0</p>
<h2>编译到 Rust 的语言问能不能用 crates.io</h2>
<p>这是 ElephantElectrical31 发在 r/rust 的一篇提问。作者在做一门编译到 Rust 的新语言，想把包发到 crates.io，名字用 <code>{语言名}_*</code>。包里同时放原始源码和生成出来的 Rust 文件。作者问这样是否可以，还是应该自建 registry。作者写，Rust 用户也可以直接用这些库。采集时 2 分、29 条评论。帖子时间是 2026-11-07 14:38 UTC。</p>
<p>FantaSeahorse 的评论有 71 分。评论写这些包里的文件不算 Rust 源文件，建议给它们单独建公开仓库。</p>
<p>Clean_Assistance9398 的评论有 9 分，问既然编译目标是 Rust，为什么不直接写 Rust，以及这门语言相对 Rust 要解决什么问题。</p>
<p>lenscas 的评论有 3 分，写一门语言可以把 Rust 当作多个输出目标之一。评论提到有人做过 Rust 到 C，C 作为编译目标比较乱。评论把 Rust 写成更适合 native 生态的目标，Web 再用 JS。</p>
<p>stinkytoe42 的评论有 13 分，建议把包系统做成能识别 Git，引用指向公开 Git 服务器。短名字如 <code>语言名/包名</code> 可以自动指向 GitHub 或一个可配置前缀，完整的 <code>https://</code> 和 <code>ssh://</code> 链接也可以用。同一作者另一条 8 分的评论写，crates.io 有这套做法没有的问题，其中一条是命名空间。评论写，语言成熟后再做完整的包系统，起步阶段不依赖正式包管理。</p>
<p>lahwran_ 的评论有 -1 分，写 Git 引用有一批问题，crates.io 的设计就是用来处理这些问题的。</p>
<p>ryo33h 的评论有 5 分，写自己的语言主后端也是 Rust，正在做自己的包管理器。列出的考虑点有中心化还是分布式、semver 还是 rev hash、是否支持最小发布时间。同一作者另一条 1 分的评论介绍 Totebag。Totebag 是一个实验性包管理器，用这门语言自己写。它用 Git，不用 registry，依赖按内容哈希锁定。信任是分布式的，用户可以选择信任谁，以及一个 commit 被观察到之后等多久。</p>
<p>AnArmoredPony 的评论有 2 分，内容是 “yeah we do that”。</p>
<p>原文链接：https://www.reddit.com/r/rust/comments/1xq7vcv/using_cratesio_for_a_language_that_compiles_to/</p>
<h2>Dave Abrahams 在 IWACO 讲可变值语义</h2>
<p>这是 Dave Abrahams 发到 r/rust 的一场演讲视频。演讲标题是 Safe Systems Programming with Mutable Value Semantics，场合是 IWACO 2026 的 keynote。YouTube 频道是 Hylo，时长 1:03:43。dabrahams 2026 年 11 月 8 日 00:08 UTC 发帖，采集时 13 分、3 条评论。帖子正文写，这场演讲讲的是 Hylo 语言，上个月在 IWACO 做的，里面有关于 Rust 的内容。</p>
<p>YouTube 简介写，Hylo 是一门建立在严格可变值语义上的语言，英文是 strict mutable value semantics，缩写 MVS。简介写，MVS 给安全、易用和效率留出空间，并限制这几项之间的冲突。Hylo 的设计尽量用上这个重合点。演讲解释 value semantics 指什么、为什么重要，以及 Hylo 怎样在不引入引用类型的情况下支持 MVS。</p>
<p>teerre 的评论有 2 分，写搜索作者名字时看到退休信息，这次看到作者还在做演讲。dabrahams 的回复有 2 分，写自己大体上已经离开，并附了 <code>#humanityftw</code>。BoringAccount-_- 的评论有 1 分，写看完了这场大约一小时的视频。</p>
<p>原文链接：https://www.youtube.com/watch?v=op51SyyPJmw</p>
<hr>
<p>From Rust中文社区 Mike</p>
<p>社区学习交流平台订阅：</p>
<ul>
<li><a href="https://rustcc.cn/" rel="noopener noreferrer">Rustcc论坛: 支持rss</a></li>
<li><a href="https://rustcc.cn/article?id=ed7c9379-d681-47cb-9532-0db97d883f62" rel="noopener noreferrer">微信公众号：Rust语言中文社区</a></li>
</ul>
]]></description><pubDate>2026-11-08 02:09:22</pubDate></item><item><title>target 目录越来越大，Rust 项目该怎么清理</title><link>https://rustcc.cn/article?id=3cf46754-8c69-4f7a-a7c6-66e861bf0a6b</link><description><![CDATA[<p>Rust 项目一多，磁盘空间常常先被 <code>target</code> 目录吃掉。每个工作区的 <code>target</code> 里，既有最后要运行的程序，也有依赖编译结果、增量编译数据和构建脚本产生的中间文件。它们都在增长，但生命周期并不一样。</p>
<p>整目录删除当然最快，代价是最终产物也会一起消失。只想清掉缓存，又得挨个项目判断哪些目录能删。这个两难，是我写 <a href="https://github.com/bigfish1913/cargoc" rel="noopener noreferrer">cargoc</a> 的原因。</p>
<p>cargoc 把 Cargo 的中间构建文件放到专用目录，按工作区记录和清理空闲缓存；最终产物仍留在 Cargo 原来的 <code>target-dir</code>。它解决的是“哪些中间文件可以清、什么时候清”。文件是否变小，取决于后续有没有删除空闲缓存；编译仍由 Cargo 完成。</p>
<p><img src="https://assets.bigfisharc.cn/images/cargoc-social/cover-zh-a777cfd069.png" alt="cargoc：中间缓存集中管理，最终产物留在原处"></p>
<h2>target 里到底该删什么</h2>
<p>Cargo 现在可以把两类产物分开：<code>target-dir</code> 保存面向使用者的最终产物，<code>build-dir</code> 保存构建过程中的中间文件。两者默认仍可能落在工作区的 <code>target</code> 目录里，所以日常使用时看起来像一整块缓存。</p>
<p>这一区分很重要。最终程序、库或安装结果通常需要继续使用；中间文件则可以在合适的时候重新生成。直接运行 <code>cargo clean</code> 会交给 Cargo 自己处理清理范围，不能把它当成“只删除中间文件”的命令。</p>
<p>Cargo 1.91 的构建缓存文档说明了这两个目录的关系：<a href="https://doc.rust-lang.org/1.91.0/cargo/reference/build-cache.html" rel="noopener noreferrer">https://doc.rust-lang.org/1.91.0/cargo/reference/build-cache.html</a>。</p>
<p>cargoc 的做法是：为每个受管理的工作区分配一个专用的 <code>build-dir</code>，让真实 Cargo 继续完成编译。<code>target-dir</code> 的既有配置不变，清理器也不连接最终产物目录。</p>
<p><img src="https://assets.bigfisharc.cn/images/cargoc-social/layout-zh-96959635ef.png" alt="cargoc 启动真实 Cargo，中间文件进入受管理目录，最终产物保留原 target-dir"></p>
<p><em>目录关系示意，不是磁盘占用实测。图中省略了命令接管、异常和进程管理分支。交互版及源码定位：<a href="https://assets.bigfisharc.cn/images/cargoc-social/layout-zh-ff0d1a14e5.html" rel="noopener noreferrer">https://assets.bigfisharc.cn/images/cargoc-social/layout-zh-ff0d1a14e5.html</a>。</em></p>
<p>缓存根目录下，每个工作区使用自己的路径哈希目录。统一管理不等于让多个项目共用一份可写缓存，也不等于跨项目依赖去重。安装 cargoc 后，旧 <code>target</code> 里的文件也不会自动搬走；旧目录没有立刻变小是正常的。</p>
<h2>正在编译的缓存，不能只按时间删除</h2>
<p>“很久没修改”只能帮助排序，不能证明目录现在没人使用。一个构建可能已经启动，只是还没有产生新的文件。</p>
<p>一次受管理构建里有三层保护：</p>
<ul>
<li>当前工作区持有租约，自动清理会显式保护它；</li>
<li>清理候选时，还要取得候选工作区的租约和扫描到的 Cargo 原生锁；</li>
<li>包装器停止构建时，只关闭本次调用拥有的进程组或 Windows Job，不按进程名杀掉机器上的其他 Cargo。</li>
</ul>
<p>因此，清理器遇到锁忙的缓存会跳过。它宁愿暂时达不到空间目标，也不会为了凑数字强删正在使用的目录。</p>
<p>构建退出时，顺序也不能反过来：包装器先关闭自己的进程树，再更新最后使用时间并释放租约，最后才执行结束后的清理。这样清理器不会在子进程仍可能访问缓存时，把它当成空闲目录。</p>
<p>代码路径也对应这三个职责。<code>runner::run</code> 定位工作区、注入 <code>build-dir</code> 并管理一次 Cargo 调用。<code>Store::lease</code> 和 <code>Store::collect</code> 负责缓存身份、租约与候选清理。<code>OwnedChild</code> 负责本次进程组或 Windows Job 的生命周期。</p>
<p>源码中的 <a href="https://github.com/bigfish1913/cargoc/blob/4a6097a05e83537b33e0a534a554406629fd67b4/src/runner.rs#L260-L352" rel="noopener noreferrer">构建生命周期</a> 和 <a href="https://github.com/bigfish1913/cargoc/blob/4a6097a05e83537b33e0a534a554406629fd67b4/src/store.rs#L328-L417" rel="noopener noreferrer">候选缓存清理</a> 可以对照阅读。链接固定到当前项目提交，避免以后文件变化后读者找不到对应位置。</p>
<h2>预算是软限制，不是硬配额</h2>
<p>cargoc 的默认配置给受管理的中间缓存设置了 80 GiB 软预算。超过预算，或者缓存所在卷的剩余空间低于期望值时，自动清理会从较久没有通过 cargoc 使用的工作区开始处理。默认超过 7 天未使用的缓存，也可能因过期被清理。</p>
<p>压力清理的低水位比例默认是 <code>0.8</code>，所以容量目标可以按配置推算为：</p>
<pre><code>80 GiB × 0.8 = 64 GiB
</code></pre>
<p>64 GiB 是清理策略的目标，不是“已经释放了 16 GiB”。清理按整个工作区的候选缓存进行，也不会精准停在这个数字。活跃缓存受保护、候选缓存不足，或缓存卷与最终产物不在同一块盘时，结果都可能不同。</p>
<p>默认还要求缓存卷保留 20 GiB 空间；运行期间低于 1 GiB 紧急阈值时，cargoc 会停止本次调用拥有的 Cargo 进程树。这两项检查只保护缓存卷和当前调用，不是整台机器的硬盘配额。最终产物、Cargo 下载的依赖数据、旧 <code>target</code> 和其他软件的占用，都不计入这 80 GiB。</p>
<h2>先用显式命令试一遍</h2>
<p>当前仓库源码版本是 0.1.4。预编译包和安装方式以项目的 Releases 页面及 README 为准：<a href="https://github.com/bigfish1913/cargoc/releases" rel="noopener noreferrer">https://github.com/bigfish1913/cargoc/releases</a>。</p>
<p>受管理构建需要 <strong>Cargo 1.91+</strong>。项目声明的 Rust 1.88 是编译 cargoc 自身的最低版本，不能拿来判断被包装的 Cargo 是否支持 <code>build-dir</code>。</p>
<p>安装后可以先检查版本并初始化 cargoc 自己的配置：</p>
<pre><code>cargo --version
cargoc --version
cargoc init
</code></pre>
<p><code>init</code> 默认创建 <code>$CARGO_HOME/cargoc.toml</code>；没有设置 <code>CARGO_HOME</code> 时，通常是 <code>~/.cargo/cargoc.toml</code>。它与 Cargo 的 <code>config.toml</code> 分开，不会把构建目录配置写进 Cargo 的全局配置。</p>
<p>如果指定缓存位置，要使用新的专用目录：</p>
<pre><code>cargoc init --cache-dir /path/to/cargoc-cache
</code></pre>
<p>不要把已有的非空目录、源码、笔记或最终产物目录直接指定为缓存根。程序会拒绝接管没有所有权标记的非空目录。</p>
<p>进入一个 Rust 工作区后，先从显式调用开始：</p>
<pre><code>cargoc build
cargoc status
cargoc clean --dry-run
</code></pre>
<p><code>status</code> 用来确认受管理目录、逻辑占用和缓存卷剩余空间。<code>clean --dry-run</code> 只报告空闲受管理缓存的拟删除结果。<code>removed_bytes</code> 是预演数字，不是已经回收的磁盘空间。确认范围后，再决定是否执行 <code>cargoc clean</code>。</p>
<h3>空间不足时，先看阈值和缓存卷</h3>
<p>一次实际演示中，Windows 上的缓存卷空闲空间低于默认的 20 GiB，cargoc 没有启动 Cargo，而是返回：</p>
<pre><code>cargoc: Insufficient free disk after safe cleanup; refusing to start Cargo
</code></pre>
<p>这类提示先用 <code>cargoc status</code> 和 <code>cargoc config</code> 检查缓存位置、空闲空间和阈值。可以释放空间，或把专用缓存放到更充足的卷；不要为了让命令继续就直接降低日常保护值。</p>
<p>现有演示使用 Cargo 1.99.1、本地 cargoc 0.1.2 和独立配置。演示配置把 <code>min_free_gib</code> 临时调为 2，并保留紧急阈值 1，离线构建才成功。下面的图是实际输出的排版摘录，不是终端窗口截图，也不用于证明空间收益。</p>
<p><img src="https://assets.bigfisharc.cn/images/cargoc-social/demo-zh-fdd974b56b.png" alt="真实命令输出摘录：默认空间检查拒绝构建，独立演示配置调整后运行成功，清理预演未删除文件"></p>
<p><em>图中条件是 Windows / Cargo 1.99.1 / cargoc 0.1.2；<code>[demo config]</code> 表示独立配置。<code>removed_bytes</code> 是预演的拟删除量，不代表本次已经释放的空间。</em></p>
<h2>想继续输入 cargo，可以另外启用接管</h2>
<p>只安装 cargoc 不会改变普通 <code>cargo</code> 命令。希望继续输入 <code>cargo build</code>、<code>cargo test</code> 时，可以启用它提供的 PATH 入口接管：</p>
<pre><code>cargoc setup --enable
export PATH="$HOME/.cargoc/bin:$PATH"
command -v cargo
cargo --version
cargoc setup --status
</code></pre>
<p>Unix 下 <code>setup</code> 不会替你修改 shell 启动文件。需要把 <code>export</code> 放进对应的 shell 配置。Windows 会写入用户级 PATH；启用后要重新打开终端，再用 <code>Get-Command cargo</code> 检查入口。</p>
<p>这个入口是用户目录中的 shim/代理，不替换 Rustup 的真实 Cargo。代理会转发参数和退出码。IDE 或脚本如果写死真实 Cargo 的绝对路径，或者使用 <code>rustup run &lt;toolchain&gt; cargo ...</code>，仍可能绕过接管。</p>
<p>不想继续接管时，执行：</p>
<pre><code>cargoc setup --disable
</code></pre>
<p>它只移除 cargoc 自己的入口，不删除 Rustup 的 Cargo，也不会自动删除已经生成的受管理缓存。</p>
<h2>cargoc clean 和 cargo clean 不是一回事</h2>
<p><code>cargoc clean</code> 清理的是空闲的受管理中间缓存，而且即使没有设置自动清理，也会处理这批缓存。最终产物在它的删除范围之外。</p>
<p>如果执行：</p>
<pre><code>cargoc exec clean
</code></pre>
<p>这条命令会转发给 Cargo 原生的 <code>clean</code>，可能删除 Cargo 目标目录中的最终产物。<code>exec</code> 只是解决命令名冲突，不是关闭缓存管理的开关。</p>
<p>同样，非托管的旧 <code>target</code> 或旧版 <code>build-dir</code> 不会被 cargoc 自动接管。需要清理它们时，先停止相关构建和 IDE，再确认原来的 <code>target-dir</code>、最终产物和恢复副本。之后由原所有者单独处理。cargoc 提供的 <code>audit</code> 只读盘点旧目录，不会替你删除。</p>
<h2>它适合解决哪一种问题</h2>
<p>如果你只是偶尔清理一个 Rust 项目，手动处理可能已经够用。cargoc 适合的是多个工作区长期积累中间构建文件，希望把这批可重建数据集中到一个有归属、可观察、可预演的缓存根里。</p>
<p>它不会带来跨项目依赖复用，也不承诺编译速度提升。空间回收来自删除空闲中间缓存，代价是之后可能重编。它也不负责最终产物、Cargo 下载缓存或整台机器的磁盘配额。</p>
<p>项目地址：<a href="https://github.com/bigfish1913/cargoc" rel="noopener noreferrer">https://github.com/bigfish1913/cargoc</a>。反馈时请带上操作系统、Cargo/cargoc 版本、调用方式，以及脱敏后的配置和错误输出；这些信息有助于区分目录、版本和入口接管问题。</p>
<hr>
<p>原文链接：<a href="https://blog.bigfisharc.cn/blog/rust-disk-cargoc/" rel="noopener noreferrer">https://blog.bigfisharc.cn/blog/rust-disk-cargoc/</a></p>
]]></description><pubDate>2026-11-07 07:54:15</pubDate></item><item><title>【Rust日报】2026-11-07 Servo 0.7 收进 527 个提交</title><link>https://rustcc.cn/article?id=d165ed05-8dc7-44f6-a34c-3d00392a7276</link><description><![CDATA[<h2>Servo 0.7.0 收进 9 月的 527 个提交</h2>
<p>Servo 0.7.0 是 Servo 浏览器引擎的一个版本，官方博客写它包含 2026 年 9 月合入的全部改动，一共 527 个提交。博客发布日期是 2026-11-05。HimikoTogaFromUSSR 2026 年 11 月 6 日把 Phoronix 的报道发到 r/rust，采集时 187 分、18 条评论。GitHub release 写，v0.7.0 已经就绪，内容是 9 月的改动。crates.io 上的 <code>servo</code> crate 版本是 0.7.0，最低 Rust 版本 1.88.0，许可证 MPL-2.0。</p>
<p>新合入的网页平台功能包括 <code>:checked</code> 伪类、<code>user-select</code>、<code>&lt;progress&gt;</code>、CSS typed arithmetic、<code>alpha()</code> 和 <code>font-width</code>。DOM 侧有 <code>Selection</code> 的 <code>getComposedRanges()</code> 和 <code>direction</code>，CSS modules 和 text modules，Canvas 的 <code>roundRect()</code>，<code>VTTCue</code> 的 <code>getCueAsHTML()</code>，以及 WebCrypto 的 <code>MLKEM768-X25519</code>。开发者工具控制台会提前求值 JS 输入。安全更新把 SpiderMonkey 升到 v153.3，mozangle 升到 v0.7，两边都带了上游报过的安全修复。</p>
<p>兼容性例子写了两条。<code>egui.rs</code> 的 demo 在 <code>--pref dom._webegpu_enabled</code> 下按预期工作。一个 Candy Crush 游戏现在可以玩。<code>Control+A</code>，macOS 上是 <code>Command+A</code>，会选中文档里的全部内容。密码输入框里的文本不能再被复制。</p>
<p>嵌入 API 有几处调整。无障碍工具产生的事件走 <code>Servo::forward_accessibility_action</code>。<code>WebView::clear_session_history</code> 可以清掉会话历史。<code>WebView::set_throttled</code> 被去掉，WebView 隐藏时自动节流，显示时停止。<code>webgl</code> 进入 <code>default_web_features</code>，不想带 WebGL 的嵌入方需要关掉默认 feature。多进程模式在构建时打开，博客写去掉这项支持大约少 2MB。Linux 上打开了线程亲和性提升。Android 的 JVM 兼容目标升到 11。Windows 主机可以交叉编译 OpenHarmony 构建。OpenHarmony SDK 更新到 API 23。</p>
<p>release 附件里的 <code>servo-aarch64-linux-ohos.hap</code> 签给 OpenHarmony，不是 HarmonyOS。HarmonyOS 目前需要自行签名，说明指向 ArkTS Servo Demo 仓库。</p>
<p>捐款数字写在文末。经常性捐款是每月 8144 美元，比 8 月高 1%。thanks.dev 上有 37 个依赖 Servo 的 GitHub 用户在赞助，比 8 月多 1 个。博客列出 10 位这个月第一次给 Servo 合入补丁的贡献者。</p>
<p>Bassfaceapollo 的评论有 58 分，希望 Servo 至少成为 Gecko 的替代。noobjaish 的评论有 19 分，提到 Ladybird 计划在接下来几个月放出二进制。r2vcap 的评论有 0 分，写 Servo 由 Linux Foundation Europe 支持，并提到华为通过 OpenHarmony 参与。barsoap 的评论有 2 分，问这怎么构成对中国的依赖，并写 545400 欧元的资助相对很小。</p>
<p>原文链接：https://servo.org/blog/2026/11/05/september-in-servo/</p>
<h2>tokio-tun 0.23.0 补上 Linux offload</h2>
<p>tokio-tun 是用 Tokio 异步创建 TUN/TAP 设备的 Rust 库。仓库在 https://github.com/yaa110/tokio-tun 。n47id 2026 年 11 月 7 日发到 r/rust，采集时 0 分、0 条评论。采集时仓库 95 星、35 个 fork、58 个 tag。最新版本是 v0.23.0。</p>
<p>README 写，支持的平台上，包 I/O 都通过 <code>AsyncFd</code> 注册到 Tokio。没有单独的轮询循环，没有阻塞回退，也不自己抽出文件描述符。<code>Tun::builder()</code> 可以设名字、地址、前缀、对端和 up。名字留空时由内核分配。一次 <code>recv</code> 返回一个 IP 包。写回 TUN 的数据必须是合法的 IPv4 或 IPv6 包。</p>
<p>v0.23.0 的 Linux 部分加了 virtio-net offload。<code>TunBuilder::offload()</code> 打开 TSO/GSO/GRO。新增 <code>VirtioNetHeader</code>、<code>VIRTIO_NET_HDR_LEN</code> 和 GSO 常量。每个收到的包前面有 10 字节的 <code>virtio_net_hdr</code>，发出去的包也要带这个头。<code>offload()</code> 配置 checksum、TSO4、TSO6 和基于 NAPI 的 GRO，并且不能和 <code>packet_info()</code> 一起用。NAPI GRO 作用在从用户态写进内核的包上，不会合并交给用户态的包。打开 offload 后，接收缓冲区至少要 <code>VIRTIO_NET_HDR_LEN + 65_536</code> 字节，因为 GSO 包可以超过接口 MTU。多队列附加时，每个队列都要调用 <code>.offload()</code>，状态不一致会被拒绝。</p>
<p>FreeBSD 这一版补了 TAP 的 MAC 地址，<code>TunBuilder::persist()</code>，以及 TAP 的 broadcast。persistence 保留接口身份。FreeBSD 在最后一个描述符关闭时会清掉地址和 up 状态。分配改用 <code>SIOCIFCREATE2</code>，避免误接管或拆掉已经存在的 persistent 或改过名的设备。</p>
<p>平台表写的是：Linux 有 TUN、TAP、多队列和 offload。macOS 只有 TUN，走 utun。Android 和 iOS 只有 <code>TunQueue</code>，分别接 VpnService 和 NetworkExtension 的 fd。FreeBSD 有 TUN 和 TAP，没有多队列，也没有 offload。macOS 和 FreeBSD 上，TUN 的 IPv6 destination 需要 <code>/128</code> 前缀。</p>
<p>原文链接：https://github.com/yaa110/tokio-tun</p>
<h2>业余开发者问能不能给 rustc 提补丁</h2>
<p>这是 Motor_Alternative944 发在 r/rust 的一篇提问。作者写自己没有计算机专业背景，最近在自学计算机科学和编译器，想知道学习期间能不能给 rustc 或类似的编译器项目做贡献，以及维护者会不会因为水平不够而不收。采集时 22 分、30 条评论。帖子时间是 2026-11-06 15:32 UTC。</p>
<p>Ok-Zookeepergame4391 的评论有 38 分，写除了能力以外没有门槛。Crouton4035 的评论有 67 分，写理论上谁都可以提议代码，实际要有专业水平的知识和经验，补丁才比较有机会通过。评论还写，维护者普遍报酬低、已经超负荷，不确定自己在做什么的时候不要增加他们的工作量。评论建议先把 Rust 代码库下载下来读，先不要指望马上贡献。</p>
<p>Anthony356 的评论有 1 分。作者写自己是业余开发者，大约 4 年前开始学，大约一年半前给 rustc 提了自己最早的一批 PR 之一，最近加入了编译器团队。评论写，业余作者如果愿意听审阅意见，维护者一般不介意一份出于好意的 PR。评论把自己被邀请进编译器团队的原因写成：有时间去处理大家都认为状态不好、但因为复杂而没人愿意碰的部分，并把它记下来。评论写，审阅者到目前为止比较友好，不熟悉的 git 命令也可以问。建议是不要为了改而改。</p>
<p>Kivooeo1 的评论有 12 分，写自己有一篇讲这个话题的文章，以及两篇从 issue 到合并的后续，里面有审阅过程。评论写可以帮忙看，前提是自己做，不要让 LLM 把整个 PR 做完再一直点继续。评论写，维护者现在要花很多时间和精力处理这类自动提交。Equivalent_Head_4803 的评论有 6 分，写 issue 经常在自己动手之前就被机器人占掉，维护者的时间被指向 GitHub 的自动提交占掉了。</p>
<p>QuasiRandomName 的评论有 11 分，写自己最早的开源贡献是给 rust-lang 的两个小 PR，和 <code>fmt::UpperHex</code> / <code>LowerHex</code> 里的检查有关。Eh2406 的评论有 5 分，写时间够的话，靠持续做会变成熟悉这块的人。在那之前，碰到不懂的缩写或术语就去查。查得到就学会了。解释难找，可以建议把链接补上，并记下自己找过但没有答案的地方。找不到解释，就告诉维护者。MilkEnvironmental106 的评论有 7 分，建议从 GitHub 上相对简单的 issue 开始提 PR。</p>
<p>原文链接：https://www.reddit.com/r/rust/comments/1xpfpc5/is_it_possible_for_me_to_contribute_to_the_rust/</p>
<h2>目录扫描先看 ignore，再谈 Rayon</h2>
<p>这是 weaztoven 发在 r/rust 的一篇提问。作者在写一个 Rust CLI，扫描目录并对文件做清理。现在是单线程，准备用 Rayon 把遍历和处理并行起来。问题是普通 SSD 和网络挂载上，线程池打满时有什么坑，Rayon 是不是还合适，有没有限制并发的写法。采集时 10 分、8 条评论。帖子时间是 2026-11-06 16:49 UTC。</p>
<p>tylerlarson 的评论有 23 分。评论写，Rayon 更偏 CPU 并行。只做目录遍历时，评论把 BurntSushi 为 ripgrep 写的 <code>ignore</code> crate 称为现成做法。它自带多线程池，并行遍历，自动遵守 <code>.gitignore</code>，并处理线程边界。作者回复会去看，因为手写 gitignore 规则比较麻烦。</p>
<p>layla4you2 的评论有 3 分。评论写，Rayon 的池子是每个核一个线程，适合 CPU 工作。文件 I/O 大部分时间在等。本地 SSD 用默认值可以。网络挂载上每次 stat 或 unlink 都是一次往返，在途请求要比核数多，但不能把服务器打满。并发数做成 <code>-j</code> 参数，不要猜。遍历可以用 <code>ignore</code> 的 <code>WalkParallel</code>，也就是 ripgrep 用的那条，或者 <code>jwalk</code>。限制并发的写法是：walker 进一个有界的 crossbeam channel，再由 N 个工作线程消费，背压由 channel 给出。清理时先删子项再删目录，不要跟随符号链接，把 <code>NotFound</code> 当成正常情况。</p>
<p>The_8472 的评论有 1 分，建议先用 strace 看实际系统调用和时间花在哪里。如果要 stat 几百万个文件，可以考虑用 io_uring 把这些调用收成一次系统调用。fiedzia 的评论有 1 分，写瓶颈多半在操作系统或硬件，除非先证明不是这样。</p>
<p>原文链接：https://www.reddit.com/r/rust/comments/1xphogg/best_practices_for_parallelizing_filesystem/</p>
<hr>
<p>From Rust中文社区 Mike</p>
<p>社区学习交流平台订阅：</p>
<ul>
<li><a href="https://rustcc.cn/" rel="noopener noreferrer">Rustcc论坛: 支持rss</a></li>
<li><a href="https://rustcc.cn/article?id=ed7c9379-d681-47cb-9532-0db97d883f62" rel="noopener noreferrer">微信公众号：Rust语言中文社区</a></li>
</ul>
]]></description><pubDate>2026-11-07 02:08:38</pubDate></item><item><title>【Rust日报】2026-11-06 用 Lean 证明没有未定义行为</title><link>https://rustcc.cn/article?id=b6eac85d-29d9-42b2-b115-68c3cc7b6920</link><description><![CDATA[<h2>lean-miri 用 Lean 证明没有未定义行为</h2>
<p>lean-miri 是把 Rust 的 Miri 解释器移植到 Lean 4 的项目，用来在 Miri 的执行模型下证明一段 Rust 程序没有未定义行为。项目页在 https://proofs-rs.github.io/lean-miri/ ，仓库在 https://github.com/proofs-rs/lean-miri 。作者 ___yuni 2026 年 11 月 5 日发到 r/rust，采集时 150 分、26 条评论。</p>
<p>Miri 检查的是具体执行。lean-miri 把同一套操作语义放进 Lean 4，于是可以写成定理：对满足前提的输入，解释器的结果不是未定义行为。作者写，Miri 已经覆盖高级 trait 和裸指针，也实现了现有已知的各类未定义行为检查。移植这条现成的解释器，就不另做一套只覆盖语言子集的语义。</p>
<p>目前移植了解释器的大部分。已经证明没有未定义行为的例子有两个。一个是空函数 <code>empty_function</code>。空函数仍会初始化调用帧再返回。定理覆盖调用初始化和这次调用里每个有限执行前缀，前提是声明过的调用方和执行配置。安全的资源耗尽被允许，不保证每种资源供给都能正常返回。另一个是 <code>duplicate_prefix</code>：把缓冲区前 <code>n</code> 个字节复制到后面 <code>n</code> 个字节。<code>n == 0</code> 直接返回 true。<code>n &gt; buf.len() / 2</code> 返回 false。通过检查之后，用 <code>as_mut_ptr</code> 取出指针，在 <code>unsafe</code> 里调用 <code>std::ptr::copy_nonoverlapping</code>。引用按 Tree Borrows 处理。站点写，对声明域里的每个 count 和每份输入，定理给出正常返回、没有解释器错误，以及任何被允许的有限前缀上都没有未定义行为。它不保证每一种 Rust 调用历史都能形成这次调用，也不额外保证返回的布尔值。</p>
<p>程序进入 Lean 的路径是：Rust 写的 exporter 把 MIR 和元数据编成 JSON，Lean 写的解释器读这份 JSON。元数据包括相关标准库类型的布局和大小。作者在评论里写，exporter 处于信任边界，另外有一批差分测试，这不等于移植 100% 忠实。编译器、exporter，以及解释器和 Rust 的对应关系，站点都列为信任边界。</p>
<p>验证 Tree Borrows 的状态转移时，作者碰到 Miri 内部断言失败，报给了 rust-lang/miri#5543。issue 标题是 <code>[tree borrows] Internal assertion failure during wildcard access traversal</code>。</p>
<p>限制也写在说明里。Lean 里的解释器还没有做性能优化。<code>duplicate_prefix</code> 的证明检查要超过 2 小时。导出的程序数据大约 500MiB。现在是向前执行的步进解释器，没有最弱前置条件这类程序逻辑，中间引理要手写。为了先把验证跑通，当前实验关掉了垃圾回收和数据竞争检测。第二个例子的证明超过 70 万行 Lean。作者写，移植大部分由 agent 完成，中间有不少人工介入；例子程序的验证需要更多人工指导。作者写自己的背景是计算机科学和逻辑，技术决定是自己做的。</p>
<p>JoJoModding 的评论有 36 分，问程序怎么进 Lean、怎么知道移植忠实、Tree Borrows 的 bug 是什么。annodomini 的评论有 29 分，问有多少是 LLM 写的，以及作者本人有没有形式化验证经验。</p>
<p>原文链接：https://proofs-rs.github.io/lean-miri/</p>
<h2>不用 String::new 造出字符串</h2>
<p>这是 Matthias Endler 写的一篇说明，列了不用 <code>String::new</code> 得到 <code>String</code> 的若干写法。页面写最后更新于 2026 年 11 月 5 日。作者从 RustLab 2026 回来后把一场没有录像的闪电演讲写成了文章。mre__ 同日发到 r/rust，采集时 94 分、14 条评论。</p>
<p>字符数组可以 <code>into_iter().collect()</code>。UTF-8 字节可以 <code>String::from_utf8(vec![102, 101, 114, 114, 105, 115])</code>，这六个字节是 <code>ferris</code>。给单元变体 <code>ferris</code> 派生 <code>Debug</code>，再 <code>format!("{:?}", Mascot::ferris)</code>，得到的也是这个名字。作者写，Debug 输出不是稳定的序列化格式，这里只是这个名字碰巧能用。</p>
<p><code>CString::new("ferris").unwrap().into_string().unwrap()</code> 会先加上 NUL，转回 <code>String</code> 时去掉终止符并检查剩余字节是合法 UTF-8。另一条路是故意把带 NUL 的字节交给 <code>CString::new</code>，用 <code>unwrap_err().into_vec()</code> 拿回被拒绝的字节，再 <code>pop()</code> 掉注入的 NUL。<code>char::from(u8)</code> 把每个字节当成码点，不按 UTF-8 解码。收成 <code>String</code> 时再编码成合法 UTF-8。原字节如果是多字节 UTF-8，结果文本可以是错的，但转换本身不会失败。</p>
<p><code>include_str!("file.rs").to_owned()</code> 在编译期嵌入文件文本。文件内容是 <code>ferris</code>，没有尾随换行。扩展名不影响，宏不把文件当 Rust 解析。</p>
<p>线程名那条：<code>thread::Builder::new().name("ferris".to_owned())</code>，再在子线程里读 <code>current().name()</code>。子进程那条：同一个可执行文件带 <code>--child</code> 时 <code>print!("ferris")</code>，父进程用 <code>Command</code> 取 stdout，再 <code>String::from_utf8</code>。</p>
<p>浮点那条用 <code>f64::from_bits(0x0000_7369_7272_6566)</code>。这组位按小端字节读是 <code>f e r r i s</code> 加两个 0。<code>to_le_bytes()</code> 后再丢掉两个填充字节。<code>String::from_raw_parts</code> 那条先 <code>Box::new(*b"ferris")</code>，<code>Box::into_raw</code> 交出分配，再以指针、长度 6、容量 6 组装。<code>type_name::&lt;ferris&gt;()</code> 取类型名的最后一段，再 <code>parse::&lt;String&gt;()</code>。作者写，<code>type_name</code> 没有稳定的命名约定。</p>
<p>panic 那条用 <code>panic_any(format!("ferris"))</code>，<code>catch_unwind</code> 之后 <code>downcast::&lt;String&gt;()</code>。这依赖 unwind。<code>panic = "abort"</code> 时进程会直接退出。</p>
<p>格式化填充那条用空字符串和填充说明符：<code>format!("{0:f&gt;1}{0:e&gt;1}{0:r&gt;2}{0:i&gt;1}{0:s&gt;1}", "")</code>。<code>&gt;</code> 右对齐，前面的字符是填充，数字是最小宽度。<code>r&gt;2</code> 用同一个空参数填出两个 <code>r</code>。</p>
<p>somebodddy 的评论有 23 分，指出线程那条里 <code>"ferris".to_owned()</code> 已经造出了 <code>String</code>。作者回复同意。SuplenC 的评论有 7 分，写自己在 RustLab 现场看了这场演讲。</p>
<p>原文链接：https://endler.dev/2026/wrong-ways-to-create-strings/</p>
<h2>compile_fail 抓住了多出来的 Display</h2>
<p>这是 TroutWindlass 发在 r/rust 的一篇工作记录。作者维护一个内部 crate，给几个服务处理凭据。采集时 47 分、9 条评论。帖子时间是 2026-11-05 21:18 UTC。</p>
<p>crate 里有 <code>Secret&lt;T&gt;</code>。它故意不实现 <code>Display</code>。值可以传递，需要时显式取出。<code>Debug</code> 给出打码后的表示。<code>format!("{}", secret)</code> 不能把原始秘密写进日志。</p>
<p>作者先前加了一个 <code>compile_fail</code> doctest，要求用 <code>Display</code> 格式化 <code>Secret</code> 时编译失败。上周有人整理格式化代码，加了一个泛型 <code>Display</code> 实现。普通单元测试通过了。这个 doctest 失败了，因为那段代码现在能编译。编译器按新的实现接受了 API 原本要拒绝的写法。</p>
<p>作者写，<code>compile_fail</code> 不完美。片段可以因为无关原因编译失败。他们把否定测试和正例配在一起，也在看更精确的 UI 测试。作者写，加一个 trait 实现通常是把 API 变宽。这次它去掉了设计里的一条限制。</p>
<p>Tomato7550 的评论有 40 分，建议直接实现一个写出 <code>[redacted]</code> 的 <code>Display</code>，再断言这个行为。WormRabbit 的评论有 6 分，提到 <code>static_assertions::assert_not_impl_any!</code>。</p>
<p>原文链接：https://www.reddit.com/r/rust/comments/1xoulve/the_best_regression_test_we_added_this_year/</p>
<h2>指针来源记录所属分配</h2>
<p>这是一篇对照 Rust 和 C++ 指针来源（provenance）的说明。作者 Sofiabelen15 2026 年 11 月 5 日发到 r/rust，采集时 41 分、4 条评论。</p>
<p>来源记录的是指针属于哪一次分配、因此可以碰到哪块内存。它是编译器里的抽象，不作为字段跟地址存在一起。CHERI 是例外。文章用一段 C++ 说明这件事：<code>opaque</code> 只有前向声明，<code>f</code> 里 <code>int x = 1</code>，调用 <code>opaque(p)</code> 后 <code>return x</code>。Clang <code>-O2</code> 把它收成直接返回 1。编译器认为 <code>p</code> 不能等于 <code>&amp;x</code>，因为 <code>p</code> 不是从 <code>x</code> 派生出来的。LLVM IR 里，<code>%2</code> 是 <code>x</code> 的分配根。根不同，经 <code>%0</code> 写入就不会改 <code>%2</code>。</p>
<p>把 <code>&amp;x</code> 转成 <code>uintptr_t</code> 再传出去，地址就被暴露。编译器必须假设别人能从整数把指针造回来，于是放弃这个优化。</p>
<p>Rust 的 <code>ptr::addr</code> 读出地址数字，并声明不会把它铸回有效指针，因此不暴露来源。作者用对应实验看生成的代码，<code>opaque(p)</code> 之后仍有 <code>mov eax, dword ptr [rsp + 12]</code>，也就是重新加载 <code>x</code>。这一步优化目前同样被放弃。</p>
<p>带标志位的指针用 <code>with_addr</code> 保留来源、只换地址：<code>let tagged: *const u32 = p.with_addr(p.addr() | 1)</code>，清掉最低位再用 <code>with_addr</code> 恢复。<code>p.addr() | 1</code> 对 <code>u32</code> 未对齐，<code>tagged</code> 不能解引用，解引用是未定义行为。旧写法 <code>p as usize</code> 再铸回 <code>*const u32</code> 能跑，但 Miri 会警告 integer-to-pointer cast，并指向 <code>ptr::with_exposed_provenance</code>。警告写，这种语义下 Miri 可能漏掉指针错误。严格来源 API 对应 <code>MIRIFLAGS=-Zmiri-strict-provenance</code>。<code>p as usize</code> 会暴露 <code>p</code> 的来源；重建的指针如果还落在同一次分配上，来源可以恢复。落到另一次分配上，来源就丢了。</p>
<p>Shnatsel 的评论有 34 分，附上了 Ralf Jung 关于指针和来源的几篇文章。</p>
<p>原文链接：https://sofiabelen.github.io/projects/pointer-provenance-in-rust-vs-cpp/</p>
<hr>
<p>From Rust中文社区 Mike</p>
<p>社区学习交流平台订阅：</p>
<ul>
<li><a href="https://rustcc.cn/" rel="noopener noreferrer">Rustcc论坛: 支持rss</a></li>
<li><a href="https://rustcc.cn/article?id=ed7c9379-d681-47cb-9532-0db97d883f62" rel="noopener noreferrer">微信公众号：Rust语言中文社区</a></li>
</ul>
]]></description><pubDate>2026-11-06 12:51:39</pubDate></item><item><title>从 44 万个&quot;幽灵&quot;说起：基于Rust开发的OntoDB 把关系、图、向量、本体等多模态数据融进一个内核，与一次 HNSW 召回坍塌的完整排查</title><link>https://rustcc.cn/article?id=ef3cc6b3-a566-4528-829d-c034aae0c481</link><description><![CDATA[<h2>一、先把结论放前面</h2>
<p>一个 227 万条化合物指纹的 HNSW 索引，召回率不对。不是低，是<strong>低得反常</strong>：</p>
<p><code>Recall@10  ≈ 0.88</code><br>
<code>Recall@50  ≈ 0.85     ← k 越大，找回来的反而越少</code></p>
<p>在健康的 HNSW 上，固定 <code>efSearch</code> 时 <code>Recall@k</code> 应随 k 增大走平甚至逼近 1。倒挂意味着"排在前面的找得到，第 10 到第 50 名够不着"——候选集本身不完整，不是排序问题。</p>
<p>最终查出来的是一串叠加的缺陷：一行严格小于号、两个没进快照的内存状态、一条四段式的数据失败链。修复后 <code>id-recall@10</code> 从 0.844 回到 <strong>0.9784</strong>，<code>dist-recall@10</code> <strong>0.9964</strong>，倒挂从 0.044 收敛到 0.004，图节点数与 SQL 侧精确对齐到 <strong>1,638,397</strong>。</p>
<p>这篇文章的主体就是这次排查。开始之前，先用一小节交代清楚"我们在什么东西上踩到了这个坑"。</p>
<hr>
<h2>二、OntoDB 是什么：一个内核装进 12 种数据模型</h2>
<h3>2.1 拼出来的多模，问题不在性能，在一致性</h3>
<p>主流的多模方案本质是"多套存储 + 应用层编排"：关系在 Postgres、图在 Neo4j、向量在 Milvus、时序在 InfluxDB、本体推理在 Jena，靠 ETL 和中间件对齐。</p>
<p>它的痛点很少出现在单点性能上，而出现在<strong>缝上</strong>：</p>
<ul>
<li>跨系统的写入无法组成一个事务，向量索引与关系数据之间必然存在一个"不一致窗口"，而窗口里的查询结果<strong>是错的、且看不出来</strong>；</li>
<li>同一个业务实体被拆到四五个系统，各存一份，靠 <code>id</code> 映射表勉强对齐；</li>
<li>一次查询要串联多家系统，延迟是各家之和，可解释性在拼接处断掉；</li>
<li>数据冷热度、安全策略、审计规则要在每一套系统上各配一遍。</li>
</ul>
<p>我们的选择是<strong>不拼</strong>：把这 12 种数据模型做进一个 LSM 存储引擎、一套事务体系、和一个查询语言里。下面就是它实际的样子。</p>
<h3>2.2 一个 LSM 引擎，12 种数据类型</h3>
<p>核心的存储是一套 LSM-Tree，通过<strong>键前缀路由</strong>区分数据形态：不同前缀走不同的编解码、索引结构和查询算子，但共用同一份 WAL、同一套 compaction、同一块 block cache。前缀划分覆盖 12 种数据类型：</p>
<table>
<thead>
<tr>
<th>类别</th>
<th>数据类型</th>
<th>关键结构</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>基础</strong></td>
<td>键值、文档（JSON/BSON）、全文（倒排+BM25+中英文分词）、多媒体元数据（SHA256 内容哈希）</td>
<td>LSM 键值、倒排索引、内容哈希索引</td>
</tr>
<tr>
<td><strong>高级</strong></td>
<td><strong>关系</strong>（结构化记录+事务）、<strong>图</strong>（属性图+遍历）、<strong>向量</strong>（HNSW 语义相似）、<strong>三元组</strong>（SPO 知识图谱）</td>
<td>见下文详解</td>
</tr>
<tr>
<td><strong>时序空间</strong></td>
<td>时序（TSM 列式 Delta/Gorilla 编码）、空间（R-Tree+GeoHash）、时空联合索引</td>
<td>TSM 列式块、四叉树</td>
</tr>
<tr>
<td><strong>实时</strong></td>
<td>流数据（CDC 变更捕获+连续查询）、事件（复杂事件处理 CEP）</td>
<td>WAL 直出 CDC、事件窗口匹配</td>
</tr>
</tbody>
</table>
<p>其中四种模型是这次踩坑的主角，也是 AI 场景里最常同时出现的四件套——它们的前缀命名空间和核心数据结构如下：</p>
<table>
<thead>
<tr>
<th>模型</th>
<th>存储形态</th>
<th>核心数据结构</th>
<th>键前缀</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>关系</strong></td>
<td>结构化记录</td>
<td>LSM-Tree；<strong>二进制行零拷贝谓词过滤</strong>（在二进制数据上直接做过滤，无需反序列化，解析时间减少约 60%）</td>
<td>普通表键</td>
</tr>
<tr>
<td><strong>图</strong></td>
<td>属性图（顶点 + 边 + 属性）</td>
<td>index-free adjacency，<code>Vec&lt;Vec&lt;(u32, String)&gt;&gt;</code> 内存邻接</td>
<td><code>__graph_v__</code> / <code>__graph_e__</code></td>
</tr>
<tr>
<td><strong>向量</strong></td>
<td>HNSW 索引 + 向量载荷</td>
<td>内存 HNSW 图 + 快照持久化 + 增量补录</td>
<td>向量索引命名空间</td>
</tr>
<tr>
<td><strong>本体</strong></td>
<td>SPO 三元组</td>
<td>内核内 OWL 2 RL 推理引擎，含 <code>inferred_from</code> 溯源</td>
<td><code>__triple__</code></td>
</tr>
</tbody>
</table>
<p>这四者共享同一套 WAL、同一套 compaction、同一套事务边界——<strong>不是"一个进程里装了四个库"，而是"一份数据上开了四种视图"</strong>。</p>
<p>图引擎值得一提：字符串 ID 只映射一次，之后全链路走 u32 整数索引，取邻居就是数组下标访问，不做哈希、不比字符串：</p>
<p><code>id_to_idx: RwLock&lt;HashMap&lt;String, u32&gt;&gt;,   // 只在这里做一次字符串映射</code><br>
<code>idx_to_id: RwLock&lt;Vec&lt;String&gt;&gt;,</code><br>
<code>adj_out:   RwLock&lt;Vec&lt;Vec&lt;(u32, String)&gt;&gt;&gt;,  // 出邻居</code><br>
<code>adj_in:    RwLock&lt;Vec&lt;Vec&lt;(u32, String)&gt;&gt;&gt;,  // 入邻居</code></p>
<p>k-hop 遍历的复杂度只与访问到的节点数相关，与图规模无关。遍历本身是 FIFO 队列 + 位图 <code>visited</code>，循环体内全是 u32 运算。企业版进一步换成 CSR 压缩存储（内存约省 50%），并用 Rayon 做并行邻居扩展。</p>
<p>本体层则是把 <strong>OWL 2 RL 推理引擎嵌进了存储内核</strong>：子类传播、等价类、传递性、对称性、逆属性、子属性等规则在写入时即触发（增量不动点约 50µs/事实），推导结果与原始事实在同一个事务边界内持久化，并记录来源规则与输入事实——<code>inferred_from</code> 让"这条结论从哪几条事实推出来的"可以被追问到具体的写入和规则触发点。对一个要给 Agent 用的数据库来说，<strong>可溯源不是加分项，是及格线</strong>。</p>
<h3>2.3 把十二种模型缝在一起的两根线</h3>
<p><strong>第一根：统一实体锚点。</strong> 一个字符串同时充当全部模型的键：</p>
<p><code>EntityId = {namespace}::{class}::{pk}</code></p>
<p>于是"化合物 A 的关系行""化合物 A 的图顶点""化合物 A 的向量文档""化合物 A 的时序记录"指向的是同一个实体——零映射表、零 ETL、没有"对不齐"的可能。前缀里的 <code>{namespace}</code> 不是装饰，是<strong>内核级多租户隔离</strong>：本体定义、数据、索引、推理都在命名空间范围内生效，键前缀路由零开销，不会出现"应用层加 tenant_id 容易漏过滤"的问题。</p>
<p><strong>第二根：同一套事务。</strong> 一次写入同时触碰多类结构，走的是同一套提交协议（三阶段锁：准备 → 预提交 → 提交，用来避免写锁与索引锁嵌套死锁）。这也是为什么后来那个"幽灵"问题只能靠<strong>一致性对账</strong>来解决——因为在我们的架构里，图、向量、关系本来就是同一份数据的不同视图，视图之间不对齐，是一种"内部矛盾"，而不是"外部同步延迟"。</p>
<p>数据完整性的最后一道防线在块级：SSTable 每页带 CRC，读路径做哨兵校验，损坏数据被<strong>确定性拒绝</strong>而不是悄悄吐错值；配合 Raft 副本（企业版）可以自动从健康副本修复。这不是"防御性编程"的口号——后面的排查里会看到，我们自己就被一个"读旧版本"的数据层 bug 咬过。</p>
<h3>2.4 一次写入，十二种模型同时就绪；活数据会自己冷</h3>
<p>写入侧是原子联动：文档写入 → 图顶点/边 → 三元组 → 推理 → 向量 → 全文/时序/空间索引全部在同一事务内落盘。数据落地的那一刻，它同时是"可被图遍历的""可被向量检索的""可被本体推理的""可被全文检索的"。没有异步索引队列，也就没有"刚写完查不到"的中间态。</p>
<p>除了"写完即就绪"，每条数据从落地开始就在按<strong>活数据（Live Data）机制</strong>运行：</p>
<ul>
<li><strong>三级半衰期</strong>：高频热点 7 小时 → 中频温数据 70 天 → 低频冷数据 2 年</li>
<li><strong>三种衰减模型</strong>：指数（默认）、线性、对数</li>
<li><strong>访问自动激活</strong>：每次查询命中自动重置衰减时钟，累加价值得分</li>
<li><strong>向量索引联动</strong>：高价值数据的向量优先加载到内存，冷数据向磁盘下沉</li>
</ul>
<p>传统 TTL 是"到点就死"，不理解数据对用户的真实价值；活数据机制让数据"越用越活、不用自动降权"。对 Agent 的长期记忆来说，这比单纯的持久化更重要——记忆不是永恒的，但不该被一刀切清理。</p>
<h3>2.5 查询语言也统一了：OntoQL</h3>
<p>不是"一个进程里装四套查询解析器"，而是<strong>单一查询语言 OntoQL</strong>融合关系投影/过滤/聚合、属性图遍历、向量近邻、时序窗口、空间谓词与本体推理；同时标准 SPARQL 1.1 查询（FILTER/OPTIONAL/UNION/EXISTS）也可直接使用。查询优化器会根据模态组合自动选择执行计划，比如"先用关系过滤缩小范围，再走向量近邻，最后在命中集上做图遍历"这类混合查询由优化器下推到存储层完成，不需要应用层拼装。</p>
<p>对"变化"的处理也是一等公民：<code>SUBSCRIBE</code> 原语支持订阅"某靶点家族新增相似分子""某子图边变化""某设备时序异常"这类条件，数据库主动推送，不需要外挂 Kafka 或应用层轮询；内核级 CDC 直接从 WAL 提取变更事件，Flink/Spark 可消费。</p>
<p>协议接入同样是多模的：PostgreSQL（5432）、MySQL（3306）、HTTP REST（7912）三套协议由内核直接解析——不是代理转发，而是协议级实现。现有应用不加中间层就能接入，原有 ORM、BI 工具可以继续用。</p>
<h3>2.6 为什么是 Rust</h3>
<ol>
<li><strong>无 GC → 确定性延迟。</strong> 向量检索是长尾敏感的，p50 看不出问题而 p99 会跳一个数量级；没有运行时 GC，"驻留内存"这件事不至于在某次搜索中间突然变成一次停顿。</li>
<li><strong>所有权模型 → 锁外计算、锁内交换。</strong> 491MB 的 HNSW 快照加载，我们早期把反序列化干在了写锁里，结果是几秒钟内所有并发搜索全部阻塞。修复后的结构是"锁外完成全部反序列化与校验，锁内只做一次指针交换"——而让这个结构成为<strong>纪律</strong>而不是<strong>自觉</strong>的，是借用检查器：你没法在持有 <code>&amp;mut</code> 的同时把数据再借出去算别的东西，临界区的边界在类型系统里就是可见的。</li>
<li><strong>单二进制、零运行时 → 部署形态覆盖边缘。</strong> 客户拿到的是一个可执行文件，<code>--data-dir</code> 指过去就能起；从边缘服务器到云，跑的是同一份内核代码，不依赖 JVM/Python 运行时。</li>
</ol>
<p>好，背景交代完了。下面进入正题。</p>
<hr>
<h2>三、一次 HNSW 召回坍塌的完整排查</h2>
<h3>3.1 现象：Recall@50 低于 Recall@10</h3>
<p>数据规模：227 万条 Morgan 指纹（1024 位二进制，Tanimoto 距离）。现象就是开头那两行——<code>Recall@10 ≈ 0.88</code>，<code>Recall@50 ≈ 0.85</code>。</p>
<h3>3.2 三次排除，排除掉三个错误假设</h3>
<p>我们没急着调参，先做排除。而且用的是<strong>分层大样本</strong>（500 节点，按 fp popcount 五分位抽样），不是拍 25 个点就下结论：</p>
<ol>
<li><strong>自检索 sanity check</strong>：用图中已存在的节点当查询，看它能否找回自己。结果 <strong>self-recall = 1.00</strong>（5000 样本下 0.9972）——图是连通且可导航的，<strong>排除"图结构崩坏"</strong>。</li>
<li><strong>距离口径对照</strong>：Tanimoto 是离散距离，可能有大量并列。id 口径与 dist 口径各测一遍，两者只差约 0.018（几乎全是并列距离造成的）——<strong>排除"并列造成假象"</strong>。</li>
<li><code>efSearch</code> <strong>扫描</strong>：从 64 提到 800，召回只从 0.72 涨到 0.89 就见顶——<strong>排除"搜索深度不够"</strong>。</li>
</ol>
<p>三次排除之后，剩下的可能性只有一个：<strong>真邻居压根不在可达子图上</strong>。边被系统性剪坏了，搜索再深也进不去。</p>
<h3>3.3 根因一：一行严格小于号</h3>
<p>问题出在 HNSW 的多样性剪枝启发式里：</p>
<p><code>// 对候选邻居 c，检查已被选中的邻居 r 是否"覆盖"了 c</code><br>
<code>// 若 r 到 c 比 q（新插入节点）到 c 更近，说明 c 由 r 代表即可 → c 冗余、可剪</code><br>
<code>for r in &amp;selected {</code><br>
<code>    if dist(r, c) &lt; dist(q, c) {   // ← 问题在这一行的边界语义</code><br>
<code>        drop(c);</code><br>
<code>    }</code><br>
<code>}</code></p>
<p>在连续欧氏空间里，两个距离完全相等的概率是 0，这行怎么写都不出问题。但 <strong>Morgan 指纹是 1024 位二进制，Tanimoto 是离散值——并列是常态，不是例外</strong>。于是大量数值上并不冗余的新节点，被系统性判为"冗余"剔除。</p>
<p>更隐蔽的是<strong>触发条件</strong>：</p>
<ul>
<li>建图初期邻居列表远没到上限，<strong>剪枝根本不会被触发</strong>——所以早期写入的 146 万向量质量是好的；</li>
<li>图饱和之后，<strong>每次增量插入都会触发剪枝</strong>——后续写入的向量入边被反复切断，逐渐沦为孤点。</li>
</ul>
<p>这解释了全部观测：早期批次正常、后期批次下滑、@50 倒挂、部分区域"几乎什么都搜不到"。</p>
<p><strong>修复两处：并列时放行（不剪），以及保护刚接上的新节点（保证至少一条入边）。</strong></p>
<h3>3.4 根因二：44 万个"幽灵"</h3>
<p>修完剪枝，召回只回到 0.85 左右。继续查，发现一个更严重的不一致：</p>
<p><code>HNSW 图内节点数        2,083,259</code><br>
<code>SQL 中有合法指纹的行数 1,638,397</code><br>
<code>差                      444,862</code></p>
<p>大量节点存在于 HNSW 里，SQL 侧却查不到对应的指纹。抽查发现这些"幽灵"的 SMILES 都是<strong>正常药物分子</strong>（阿司匹林类似物那一类），本该有指纹，不是脏数据。</p>
<p>根因是<strong>两个纯内存状态没有进快照</strong>：</p>
<ul>
<li><code>deleted_keys</code>：哪些向量已被逻辑删除（本应随索引落盘）；</li>
<li><code>doc_vectors</code>：哪些 id 当前持有有效向量。</li>
</ul>
<p>重启后这两个集合清零，于是删除过滤失效，历史被删节点重新漏进搜索结果；增量对账逻辑又因为"图里已经有这个节点"直接跳过，没去核对"它在文档侧其实已经没有指纹了"。搜索时的候选放大系数还写成了 <code>k + 已删数量</code>——44 万的删除量下，等于<strong>每次搜索都要扫全图</strong>。</p>
<p>泄漏路径的修复是 <strong>restore 后全量对账</strong>，把图内节点与文档侧做完整比对：</p>
<p><code>图有节点 + 文档有 fp        → noted        = 1,638,397  （重建 doc_vectors）</code><br>
<code>图有节点 + 文档无 fp / 行已删 → ghosts_marked = 444,862  （mark_deleted）</code><br>
<code>图无节点 + 文档有 fp        → 补录</code><br>
<code>合计                       = 2,083,259  = 图内节点数  ✓</code></p>
<p>随后执行<strong>原地 prune（非全量重建）</strong>：保留拓扑、全层邻居重写、双向边补全、entry/max_layer 重算，摘除 444,862 个节点耗时 <strong>23.1 秒</strong>（重建路径是小时级），快照从 959.8MB 瘦到 <strong>694.8MB（−27.6%）</strong>，<code>over_fetch</code> 从 8× 回落到 2×。</p>
<h3>3.5 根因三：四段失败链，不是一个 bug</h3>
<p>排查没有停在"对账修好了"。我们继续追"这些正常分子的 fp 为什么会变成 NULL"——这才是真正的源头。结论是<strong>四段失败叠加，任一段单独都不会出事故</strong>：</p>
<ol>
<li><strong>A · 导入用了不完整的 id 索引做 keyset 分页</strong>，导致分区断流，漏写约 65~78 万行的指纹；但导入日志显示的完成数约等于全表行数，当时以为已 100%。</li>
<li><strong>B · 覆盖率验证抽样偏差</strong>：<code>check_fp_coverage.py</code> 只抽查 7 个 id，<code>refill_gaps.py</code> 只查 CHEMBL1-9 前缀——全落在未漏写区域，<strong>假收口</strong>。</li>
<li><strong>C · chem 表批量导入的 INSERT 列清单不含 fp 列</strong>，新写入的版本覆盖了有 fp 的旧行，产生无 fp 的最新版本。</li>
<li><strong>D · pass-2 版本选择 bug</strong>（<code>w_seq == seq</code> 匹配 + <code>snapshot_sst_handles</code> 层序 L6→L0 反转）：向量 backfill 恰好跑在这个 bug 期间，层序从旧到新扫描、<code>seq</code> 被 <code>clean_index</code> 清零时读到了导入前的旧版本（有 fp），把旧版本指纹索引进图，而活版本无 fp——<strong>这就是 44.4 万差额的精确来源</strong>。</li>
</ol>
<p>这里有一个值得单独说的设计教训。我们这套索引的<strong>版本权威来自 LSM-Tree 的物理层序（newest-first + first-seen-wins），而不是</strong> <code>seq_no</code>。层序是存储引擎自己维护、不可篡改的事实；seq 是元数据，可以被重整、清零、碰撞。这个取舍在这次事故里救了我们——D 段的病灶正是"用 seq 代替层序"。</p>
<p><strong>生产端三条修复</strong>：pass-2 改走层序优先；<code>prune_ids</code> 按 id 去重（保留与 <code>doc_vectors</code> 匹配的那个，防止同 id 双节点一起存活）；chem 导入列清单补全 fp。覆盖率验证从 7 点抽查升级为"全量 COUNT + 分区分层抽样"。</p>
<p>排查过程中还顺带修掉 planner 一处结构性问题：<code>remaining_filter</code> 被包在 Limit 节点<strong>之上</strong>，计划形状是 <code>Filter(Limit(SeqScan))</code>，而 SQL 语义要求 <code>Limit(Filter(SeqScan))</code>——导致 <code>WHERE ... LIMIT N</code> 只返回 N−1 行，<code>ORDER BY ... LIMIT</code> 场景更是返回"错的行"而非"少的行"。</p>
<h3>3.6 结果</h3>
<p>最终验收采用 500 节点分层抽样（fp popcount 五分位 × 100）、GT 装载 1,638,397 条（与节点数逐位一致）：</p>
<table>
<thead>
<tr>
<th>指标</th>
<th>修复前</th>
<th>修复后（500 分层）</th>
</tr>
</thead>
<tbody>
<tr>
<td>id-recall@10</td>
<td>0.844</td>
<td><strong>0.9784</strong></td>
</tr>
<tr>
<td>dist-recall@10</td>
<td>0.848</td>
<td><strong>0.9964</strong></td>
</tr>
<tr>
<td>dist-recall@50</td>
<td>—</td>
<td><strong>0.9916</strong></td>
</tr>
<tr>
<td>id-recall@50</td>
<td>—</td>
<td><strong>0.9832</strong></td>
</tr>
<tr>
<td>倒挂幅度（@10 − @50）</td>
<td>0.044</td>
<td><strong>0.004</strong></td>
</tr>
<tr>
<td>self-recall（5000 样本）</td>
<td>1.00（小样本）</td>
<td><strong>0.9972</strong></td>
</tr>
<tr>
<td>幽灵命中（多查询点 TOP50）</td>
<td>18/50</td>
<td><strong>0 / 88</strong></td>
</tr>
<tr>
<td>快照体积</td>
<td>960MB</td>
<td><strong>695MB（−27.6%）</strong></td>
</tr>
<tr>
<td>over_fetch 放大</td>
<td>8×</td>
<td><strong>2×</strong></td>
</tr>
<tr>
<td>图节点数</td>
<td>2,083,259</td>
<td><strong>1,638,397（精确对齐 SQL）</strong></td>
</tr>
</tbody>
</table>
<p>所有闸门（id≥0.968 / dist@10≥0.990 / dist@50≥0.985 / self≥0.995 / ghost=0 / 节点数精确）全部通过。</p>
<h3>3.7 五个 bug，其实是同一类病</h3>
<p>这一路共修掉 5 个查询正确性 bug：equality 等值点查返回双行、ghost 泄漏、pass-2 版本选反、<code>NOT</code> 谓词被当成列名、以及 planner 的 Filter/Limit 形状错误。</p>
<p>它们底层是同一个病：<strong>"版本合流必须走层序、谓词归属必须覆盖所有算子形态"这条纪律，之前只在部分代码路径上落地了</strong>。修复方式不是逐个打补丁，而是<strong>收敛到一个公共迭代器</strong>——整族免疫，而不是这一次修好。</p>
<p>顺带记一条通用规则：凡输出顺序承载语义的路径（range 扫描、keyset 分页、层序扫描），去重一律用<strong>保序</strong>实现。我们最初给 equality 加去重时用了 <code>sort() + dedup()</code>，打乱了 range 的索引序，导致分页重复扫描、装载数虚高到 1.97M、召回掉到 0.76——换成保序去重后立刻恢复。</p>
<hr>
<h2>四、排查之后：从"人工深挖"到"自动发现"</h2>
<p>这次事故最值得反思的一点是：<strong>上述 5 个 bug，没有一个是监控发现的，全靠人工深挖。</strong> 修完 bug 只是第一步，第二步是让同类问题以后能被自动看见：</p>
<ul>
<li><strong>schema API 暴露向量索引状态</strong>：<code>nodes</code> / <code>deleted_keys</code> / <code>ready</code> / <code>m</code> / <code>ef_construction</code>，探针不再依赖日志格式。</li>
<li><strong>reconcile 日志分级告警</strong>：冷启动首次对账允许个位数 ghost（快照窗口内写入属正常）；冷启动后出现任何非零 → WARN；占比 &gt; 1% 建议 COMPACT；&gt; 10 万 → ERROR。</li>
<li><strong>独立探针拆分</strong>：<code>probe_vec_nodes.py</code> / <code>probe_vec_ghost.py</code> / <code>probe_vec_self.py</code>（秒级健康检查）/ <code>probe_ready_gate.py</code>（启动窗口负向探针），各自可跑。</li>
<li><strong>启动门闩</strong>：对账完成前向量查询返回明确错误，而不是静默返回脏结果——用约 28 秒的"等待可用"换"不返回错数据"。</li>
<li><strong>k8s 探针拆分</strong>：liveness 立即返回 200，readiness 等待 <code>ready=true</code>，避免对账未完成时探针失败触发无限重启循环。</li>
</ul>
<hr>
<h2>五、共建与邀约</h2>
<p>OntoDB 核心内核以<strong>开源生态层 + 闭源企业能力</strong>双轨推进。我们想找的是：</p>
<ul>
<li>在真实场景里用得上"向量 + 图 + 本体 + 流"的团队——<strong>来仓库提 Issue 聊场景，哪怕是"你这个设计在我的场景下不成立"这种</strong>；</li>
<li>愿意补 client SDK、文档、查询算子的同学——<code>crates/</code> 下每个 crate 边界写得比较清楚，PR 门槛不高；</li>
<li>想写点东西的人——欢迎写"我用 OntoDB 接了哪个 Agent、哪张知识图谱"，踩坑文比赞美文受欢迎得多。</li>
</ul>
<p><strong>给评论区留一个我们确实没想透的问题：</strong></p>
<blockquote>
<p>在离散距离空间（二进制指纹、量化嵌入）里，HNSW 的多样性剪枝该怎么处理"并列距离"？并列放行，还是引入第二判据（比如按层序或 id 稳定排序）？你们的实现是怎么处理的，有没有踩过类似的坑？</p>
</blockquote>
<p>仓库在这里，欢迎 clone、跑 Demo、开 Issue：</p>
<p><strong>https://github.com/jianzhuguo/ontodb</strong></p>
<p><code># 前置：Rust 1.77+</code><br>
<code>git clone https://github.com/jianzhuguo/ontodb.git</code><br>
<code>cd ontodb</code><br>
<code>cargo build --release</code><br>
<code>./target/release/ontodb-server --data-dir ./data --http 0.0.0.0:7912</code></p>
<hr>
<h2>六、In Rust We Trust</h2>
<p>谢谢 Rust 中文社区，谢谢各位Ruster，我们产品已经开源发布，现在轮到社区来用、来骂、来改、来共建。</p>
<hr>
<p>*（本文性能数字与故障复盘均来自仓库基准报告与真实排查记录；最终验收采用 500 节点分层抽样与 5000 节点 self-recall 验证。未覆盖项——并发压测、kill-9 崩溃注入、千万级规模边界、*<code>deleted_keys/doc_vectors</code> <em>持久化——不在本文宣称范围内，不做隐藏。）</em></p>
]]></description><pubDate>2026-10-21 03:17:29</pubDate></item><item><title>【RPI 更新】把 rpi Agent 装进你的zed 编辑器</title><link>https://rustcc.cn/article?id=4dd995ce-b5ee-4e94-a883-d4b2a532c25f</link><description><![CDATA[<h2>架构</h2>
<pre><code>Zed  ──ACP (JSON-RPC over stdio)──▶  rpi --acp  ──spawn──▶  rpi --mode rpc  (每会话一个)
</code></pre>
<p><code>rpi-acp</code> 是个 cdylib 扩展，做三件事：<strong>抢 stdio</strong>、<strong>说 ACP v1</strong>、<strong>给每个会话挂一个 <code>rpi --mode rpc</code> 子进程</strong>。桥接层自己不跑模型——tools、models、auth、skills 全在子进程里，所以行为跟终端里跑 rpi 完全一致。</p>
<h2>关键机制：抢夺 stdio</h2>
<p>宿主默认会 drain stdin，扩展就拿不到 stdio，ACP server 无从谈起。解法：带 <code>--acp</code> 时，扩展在注册阶段声明 <code>{"ownsStdio": true}</code>，宿主随即停止 drain，改走 <code>StdioLifecycle</code>，把 stdin/stdout 交给扩展。</p>
<p>从终端直接跑 <code>--acp</code> 会<strong>快速失败</strong>（一条消息 + exit 2），而不是空转。要注册用 <code>rpi --install-zed</code>。</p>
<h2>事件映射</h2>
<table>
<thead>
<tr>
<th>rpi RPC 事件</th>
<th>ACP update</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>text_delta</code> / <code>thinking_delta</code></td>
<td><code>agent_message_chunk</code> / <code>agent_thought_chunk</code></td>
</tr>
<tr>
<td><code>tool_execution_start</code></td>
<td><code>tool_call</code>（edit/write 带 diff）</td>
</tr>
<tr>
<td><code>tool_execution_update</code> / <code>_end</code></td>
<td><code>tool_call_update</code></td>
</tr>
<tr>
<td><code>plan_mode_complete</code></td>
<td><code>plan</code></td>
</tr>
<tr>
<td>outcome aborted / completed</td>
<td><code>stopReason: cancelled</code> / <code>end_turn</code></td>
</tr>
</tbody>
</table>
<p>流式文本用<strong>累积快照 + finalText</strong> 去重，不重发已发过的 chunk。</p>
<h2>会话持久化</h2>
<p>稳定 UUID，原生 JSONL 历史落在 <code>~/.rpi/agent/acp/sessions</code>（可用 <code>RPI_ACP_SESSION_DIR</code> 覆盖），进程退出后仍在。<code>session/load</code> 恢复并<strong>重放</strong>，<code>session/resume</code> 恢复但<strong>不重放</strong>。未知会话<strong>返回错误</strong>，不静默开空对话。</p>
<h2>权限与问题扩展</h2>
<ul>
<li><strong>权限</strong>：插件弹窗（如 bash 确认）映射成标准 <code>session/request_permission</code>，用户选择回传插件。默认带 <code>--permission-gate</code>。</li>
<li><strong>问题</strong>：ACP v1 没有通用多选方法，于是双方在 <code>initialize</code> 里用 <code>_meta: {"pi-rust/questions": 1}</code> 协商。只有广告了 v1 的客户端才收到 <code>_rpi/request_question</code>；超时默认 300s，配套 <code>_rpi/dialog_closed</code> 通知回收 UI。</li>
</ul>
<h2>安装配置</h2>
<pre><code>rpi install rpi-acp    # 或 task install
</code></pre>
<p>Zed <code>settings.json</code>：</p>
<pre><code>{ "agent_servers": { "rpi": { "type": "custom", "command": "rpi", "args": ["--acp"] } } }
</code></pre>
<p><code>args</code> 可透传普通 flag，如 <code>["--acp", "--model", "gateway/model"]</code>。<code>--acp</code> 会自动注册到 Zed（幂等、原子写、保留 JSONC 注释）；设 <code>RPI_ACP_NO_AUTO_REGISTER_ZED=1</code> 可关掉。</p>
<h2>独立二进制 <code>rpi-acp-bridge</code></h2>
<p>同样的 server 也能编成独立二进制，<strong>原生拥有 stdin/stdout</strong>，不需要 <code>ownsStdio</code>，可直接对着 stock rpi 跑。找 agent 的顺序：<code>RPI_BIN</code> → 自身 → 兄弟文件 → PATH。</p>
<pre><code>cargo build --release -p rpi-acp --bin rpi-acp-bridge
task pack-acp   # 打包进 ACP Registry
</code></pre>
<p>相关链接</p>
<ul>
<li>🏠 官网与文档：<a href="https://rpi.laofu.online/" rel="noopener noreferrer">https://rpi.laofu.online/</a></li>
<li>📦 核心仓库：<a href="https://github.com/bigfish1913/pi-rust" rel="noopener noreferrer">bigfish1913/pi-rust</a></li>
<li>🔌 扩展仓库：<a href="https://github.com/pi-rust/rpi-package" rel="noopener noreferrer">pi-rust/rpi-package</a></li>
<li>📚 软件包目录：<a href="https://rpi.laofu.online/packages.html" rel="noopener noreferrer">https://rpi.laofu.online/packages.html</a></li>
<li>✍️ 个人博客：<a href="https://blog.laofu.online/" rel="noopener noreferrer">https://blog.laofu.online/</a></li>
</ul>
]]></description><pubDate>2026-10-14 15:51:34</pubDate></item><item><title>Ramag v0.0.2 发布：新增 SSH、SFTP 与 JumpServer 导入</title><link>https://rustcc.cn/article?id=691353be-7377-4388-86ac-edd20b706443</link><description><![CDATA[<p>Ramag 是一个使用 Rust + GPUI 构建的本地优先开发者桌面工作台，将数据库、Git、SSH 和剪贴板放进同一个原生应用。</p>
<pre><code>数据库 ↔ Git ↔ SSH / SFTP ↔ 剪贴板
</code></pre>
<ul>
<li>GitHub：https://github.com/tools-rs/ramag</li>
<li>下载：https://github.com/tools-rs/ramag/releases/tag/v0.0.2</li>
<li>更新记录：https://github.com/tools-rs/ramag/blob/main/CHANGELOG.md</li>
</ul>
<p><img src="https://cdn.jsdelivr.net/gh/tools-rs/ramag@main/docs/screenshots/v0.0.2/home-light.png" alt="Ramag v0.0.2 首页"></p>
<h2>v0.0.2 主要更新</h2>
<h3>SSH 连接管理</h3>
<p>新增完整的 SSH 管理入口，支持：</p>
<ul>
<li>密码、系统 SSH 配置和密钥认证</li>
<li>解析 <code>ssh user@host -p 2222 -i /path/to/key</code> 命令</li>
<li>连接测试、默认目录和生产连接标记</li>
<li>多连接标签和连接搜索</li>
<li>本机加密保存密码与敏感连接参数</li>
</ul>
<p>终端连接复用系统 OpenSSH，因此可以继续使用现有的 <code>~/.ssh/config</code>、SSH Agent、密钥和 <code>known_hosts</code>。</p>
<p><img src="https://cdn.jsdelivr.net/gh/tools-rs/ramag@main/docs/screenshots/v0.0.2/ssh-connections-empty-light.png" alt="SSH 连接管理"></p>
<h3>内嵌终端与 SFTP 文件工作区</h3>
<p>连接成功后，可以在同一个工作区中使用内嵌终端和远程文件浏览：</p>
<ul>
<li>ANSI 终端显示和常用键盘输入</li>
<li>一个连接下打开多个终端标签</li>
<li>始终保留至少一个终端，避免误关后留下空页面</li>
<li>远程目录浏览、路径导航和名称搜索</li>
<li>文本预览与编辑、日志跟随</li>
<li>文件和目录上传下载</li>
<li>覆盖确认、取消和传输进度</li>
</ul>
<p>还可以从当前路径、目录或文件所在位置创建新终端。新终端会进入对应远程目录，不影响已有终端的运行状态。</p>
<p>生产连接会禁止 SFTP 上传、编辑、重命名和删除。终端命令仍由远端账号权限与服务器策略约束。</p>
<p><img src="https://cdn.jsdelivr.net/gh/tools-rs/ramag@main/docs/screenshots/v0.0.2/ssh-workspace-light.png" alt="SSH 内嵌终端与远程文件工作区"></p>
<h3>JumpServer 导入</h3>
<p>支持保存多个 JumpServer 登录，并直接读取：</p>
<ul>
<li>组织与资产树</li>
<li>已授权资产</li>
<li>资产平台和地址</li>
<li>可用的授权账号</li>
</ul>
<p>选择资产与授权账号后，可以导入为普通 SSH 连接，继续编辑、测试和打开。未开放 SSH 协议的资产会明确提示，不会错误导入。</p>
<h3>数据库改进</h3>
<ul>
<li>结果搜索支持字符串 ID 与整数 ID 双向转换。</li>
<li>内置 Base10、Base16、Base36、Base58 Bitcoin、Base58 Flickr 和自定义字符表。</li>
<li>支持带路径、超时和输出限制的外部转换器。</li>
<li>修复 MySQL <code>SHOW WARNINGS</code> 被识别为普通分页查询的问题。</li>
<li>数据库连接导入导出迁移到全局设置，可统一处理 MySQL、PostgreSQL、Redis 和 MongoDB。</li>
<li>编辑连接时完整回填现有参数，生产连接继续受只读保护。</li>
</ul>
<h3>Git 与剪贴板改进</h3>
<p>Git 工作台新增明确的克隆入口，Markdown 文件默认渲染预览，并重新整理了分支、远端、Tag 和 Stash 操作。同时修复新建分支导致的界面崩溃、分栏宽度串联和部分标签无法通过 <code>⌘W</code> / <code>Ctrl+W</code> 关闭的问题。</p>
<p>剪贴板仍默认关闭。启用状态、采集行为、全局热键和“清空全部历史”统一迁移到全局设置；关闭后会隐藏入口并释放全局快捷键。</p>
<pre><code>macOS：⌘⇧V
Windows：Ctrl+Shift+V
</code></pre>
<h2>本地优先与安全边界</h2>
<p>Ramag 不要求登录账号，也不会把数据库连接、SSH 凭据、Git 仓库或剪贴板内容上传到 Ramag 服务。</p>
<ul>
<li>敏感配置使用 AES-256-GCM 加密。</li>
<li>主密钥保存在 macOS Keychain 或 Windows Credential Manager。</li>
<li>SSH 认证、主机校验和 Git 网络操作复用系统已有配置。</li>
<li>数据库和 SSH 生产连接会限制界面中的写操作。</li>
</ul>
<p>Git 功能仍处于试验阶段。生产数据库操作、Git 写操作和远程终端命令仍需要使用者确认目标环境和影响范围。</p>
<h2>下载</h2>
<p>v0.0.2 Release：</p>
<p>https://github.com/tools-rs/ramag/releases/tag/v0.0.2</p>
<p>当前提供：</p>
<pre><code>Ramag-0.0.2-macos-arm64.dmg
Ramag-0.0.2-macos-x86_64.dmg
Ramag-0.0.2-windows-x64-setup.exe
SHA256SUMS.txt
</code></pre>
<p>支持 macOS 12+ Apple Silicon、macOS 12+ Intel 和 Windows 10/11 x64，暂不支持 Linux。</p>
<p>当前 Windows 安装包尚未做 Authenticode 签名，macOS 安装包尚未完成 Developer ID 签名与 Apple 公证。请只从项目 Releases 下载，并使用同一页面的 <code>SHA256SUMS.txt</code> 校验文件。</p>
<p>从源码运行：</p>
<pre><code>git clone https://github.com/tools-rs/ramag.git
cd ramag
make develop
</code></pre>
<p>如果你正在使用 Rust、GPUI、数据库工具或 SSH/SFTP 工作流，欢迎下载体验。遇到问题时，可以在 GitHub Issues 中附上操作系统、Ramag 版本和复现步骤；提交前请删除服务器地址、用户名、密码和业务数据。</p>
<ul>
<li>Issues：https://github.com/tools-rs/ramag/issues</li>
<li>源码：https://github.com/tools-rs/ramag</li>
</ul>
]]></description><pubDate>2026-08-04 09:46:17</pubDate></item><item><title>GitBundle v3.5</title><link>https://rustcc.cn/article?id=4f9a98e9-2ea8-4029-b455-1da1e040702a</link><description><![CDATA[<p>大家好, 我是一名独立开发者, 同时也是 <a href="https://github.com/gitbundle/gitbundle" rel="noopener noreferrer">GitBundle</a> 的项目作者, 在这个项目上持续投入了巨量的时间和精力, 经过持续的迭代和打磨，GitBundle 终于迎来了 v3.5 版本。这次更新在安全性、CI 交互和用户体验上都做了重点提升，希望给大家带来更好的自托管 Git 体验。</p>
<p>🔐 安全性大幅增强</p>
<ul>
<li>移除 SHA-1，新增 SSH layer，支持后量子密钥交换算法 mlkem768x25519，彻底修复 SSH 安全警告</li>
<li>修复了 git clone 无法安全断开 TCP 连接的问题</li>
</ul>
<p>⚙️ 后台管理优化</p>
<ul>
<li>支持用户软删除，数据管理更灵活</li>
<li>支持用户安全更新邮箱</li>
</ul>
<p>🚀 CI 与体验提升</p>
<ul>
<li>支持 cursor-based CI 日志拉取，交互更顺畅</li>
<li>UI 全面打磨，修复了多项历史遗留问题，视觉和操作更一致流畅</li>
</ul>
<p>为什么要做这个项目:
第一点: 肯定是因为兴趣爱好, 因为我喜欢写代码, 喜欢做这个事情
第二点: 我见识过类似的各种平台, 但都是差强人意, 体验很糟糕
第三点: 的的确确我找不到工作, 失业了, 职场远远不是你想写代码那么简单, 这是一个很痛的现实, 但我必须要接受, 因为我还想继续写代码直到写不动的那一天, 可现实不允许我这样</p>
<p>欢迎大家下载试用，也期待大家的反馈和建议！ 🙏</p>
<p>关于大家关心的源代码开源问题, 目前有计划在将来进行开源, 但具体开源时间还不确定.</p>
<p>详细发布日志:</p>
<p>https://github.com/gitbundle/gitbundle/releases/tag/server-v3.5.0</p>
]]></description><pubDate>2026-06-11 11:48:16</pubDate></item><item><title>A high-performance async Rust implementation of KCP - A Fast and Reliable ARQ Protocol built on top of Tokio.</title><link>https://rustcc.cn/article?id=29969d7b-6ba8-4f9e-908c-4004a893fee0</link><description><![CDATA[<p>https://github.com/leihuxi/rust-kcp
A high-performance async Rust implementation of KCP - A Fast and Reliable ARQ Protocol built on top of Tokio.</p>
<p>Features
Async-First Design: Built from ground up for async/await with Tokio integration
Zero-Copy: Efficient buffer management using the bytes crate
Lock-Free Buffer Pool: High-performance memory management with crossbeam
Connection-Oriented: High-level connection abstractions (KcpStream, KcpListener)
Protocol Compatible: Compatible with original C KCP implementation
Observability: Integrated tracing and metrics support
Memory Efficient: Object pooling and buffer reuse
Multiple Performance Modes: Normal, Fast, Turbo, Gaming presets
Installation
Add this to your Cargo.toml:</p>
<p>[dependencies]
kcp-tokio = "0.4"
tokio = { version = "1.0", features = ["full"] }
Quick Start
Client
use kcp_tokio::{KcpConfig, KcpStream};
use tokio::io::{AsyncReadExt, AsyncWriteExt};</p>
<p>#[tokio::main]
async fn main() -&gt; Result&lt;(), Box&gt; {
let config = KcpConfig::new().fast_mode();
let mut stream = KcpStream::connect("127.0.0.1:12345".parse()?, config).await?;</p>
<pre><code>// Send data
stream.write_all(b"Hello, KCP!").await?;

// Receive response
let mut buffer = [0u8; 1024];
let n = stream.read(&amp;mut buffer).await?;
println!("Received: {}", String::from_utf8_lossy(&amp;buffer[..n]));

Ok(())
</code></pre>
<p>}
Server
use kcp_tokio::{KcpConfig, KcpListener};
use tokio::io::{AsyncReadExt, AsyncWriteExt};</p>
<p>#[tokio::main]
async fn main() -&gt; Result&lt;(), Box&gt; {
let config = KcpConfig::realtime();
let mut listener = KcpListener::bind("127.0.0.1:12345".parse()?, config).await?;</p>
<pre><code>println!("Server listening on 127.0.0.1:12345");

while let Ok((mut stream, addr)) = listener.accept().await {
    println!("New connection from {}", addr);
    tokio::spawn(async move {
        let mut buf = [0u8; 1024];
        while let Ok(n) = stream.read(&amp;mut buf).await {
            if n == 0 { break; }
            let _ = stream.write_all(&amp;buf[..n]).await;
        }
    });
}

Ok(())
</code></pre>
<p>}
Architecture
┌─────────────────────────────────────────────────────────────┐
│                    Application Layer                         │
│              (User code using KcpStream/KcpListener)         │
├─────────────────────────────────────────────────────────────┤
│                    High-Level API Layer                      │
│                  KcpStream    KcpListener                    │
│           (AsyncRead/AsyncWrite, TCP-like interface)         │
├─────────────────────────────────────────────────────────────┤
│                    Protocol Core Layer                       │
│                       KcpEngine                              │
│        (ARQ logic, congestion control, retransmission)       │
├─────────────────────────────────────────────────────────────┤
│                    Common Layer                              │
│         KcpSegment, KcpHeader, BufferPool, Constants         │
├─────────────────────────────────────────────────────────────┤
│                    Transport Layer                           │
│          Generic Transport trait (UDP default)               │
└─────────────────────────────────────────────────────────────┘
Configuration
Performance Presets
// Gaming - ultra-low latency (3ms update interval)
let config = KcpConfig::gaming();</p>
<p>// Real-time communication (8ms update interval)
let config = KcpConfig::realtime();</p>
<p>// File transfer - high throughput
let config = KcpConfig::file_transfer();</p>
<p>// Testing with packet loss simulation
let config = KcpConfig::testing(0.1); // 10% packet loss
Performance Modes
Mode	Update Interval	Resend	Congestion Control	Use Case
Normal	40ms	0	Yes	General purpose
Fast	8ms	2	Yes	Low latency
Turbo	4ms	1	No	Maximum speed
Gaming	3ms	1	No	Real-time games
Custom Configuration
use std::time::Duration;</p>
<p>let config = KcpConfig::new()
.fast_mode()
.window_size(128, 128)
.mtu(1400)
.connect_timeout(Duration::from_secs(10))
.keep_alive(Some(Duration::from_secs(30)))
.stream_mode(true);
Examples</p>
<h1>Run performance test server</h1>
<p>cargo run --example perf_test_server -- 127.0.0.1:12345 gaming</p>
<h1>Run performance test client</h1>
<p>cargo run --example perf_test_client -- 127.0.0.1:12345</p>
<h1>Run simple echo example</h1>
<p>cargo run --example simple_echo
Testing</p>
<h1>Run all tests</h1>
<p>cargo test</p>
<h1>Run resilience tests (packet loss, reorder, concurrent connections)</h1>
<p>cargo test --test resilience_test</p>
<h1>Run benchmarks</h1>
<p>cargo bench</p>
<h1>Run with logging</h1>
<p>RUST_LOG=debug cargo test -- --nocapture</p>
<h1>Run clippy</h1>
<p>cargo clippy --all-targets -- --deny clippy::all
Documentation
Detailed documentation is available in the doc/ directory:</p>
<p>Document	Description
ARCHITECTURE.md	System architecture and design
MODULES.md	Module reference and APIs
USAGE.md	Usage guide and examples
TESTING.md	Testing guide
Performance
KCP provides significant latency improvements over TCP:</p>
<p>30-40% lower latency in typical network conditions
Better performance on lossy networks
Configurable trade-offs between latency and bandwidth
Optimizations in this Implementation
Actor-based lock-free architecture: KcpEngine runs in a single dedicated tokio task, eliminating Arc&lt;Mutex&lt;&gt;&gt; contention
Generic Transport trait: Associated Addr type with RPITIT — zero heap allocation on hot path (no Pin&lt;Box&gt;)
DashMap for packet routing: Listener uses lock-free concurrent hashmap on the hot path
Lock-free buffer pools: crossbeam::queue::ArrayQueue for zero-allocation fast path
BTreeMap receive buffer: O(log n) insertion for out-of-order packets (vs O(n) linear scan)
Zero-copy segment encoding: Flush avoids cloning segments, encodes by reference
Cached timestamps: Single syscall per input() call instead of 3+
Pre-allocated buffers: VecDeque::with_capacity based on window sizes, avoiding grow overhead on send burst
Zero-copy packet handling with bytes crate
Grouped state structs for better cache locality
Configurable update intervals (3-40ms)
Batch ACK processing
Use Cases
Gaming: Ultra-low latency for real-time multiplayer
VoIP/Video: Real-time communication
Live Streaming: Low-latency data delivery
File Transfer: Reliable bulk data transfer
IoT: Efficient communication for constrained devices
Compatibility
Protocol: Compatible with original C KCP
Rust: Edition 2021, stable toolchain
Tokio: 1.0+
License
MIT License - see LICENSE file.</p>
<p>Contributing
Contributions are welcome! Please feel free to submit a Pull Request.</p>
<p>Resources
Original KCP Protocol
KCP Protocol Documentation
Tokio Documentation
Benchmarks
Criterion benchmarks measure engine-level throughput and latency:</p>
<p>cargo bench
Benchmark	Description
engine_throughput	10/100/500 x 1KB messages
engine_small_messages	1000 x 64B messages
engine_large_message	Single 16KB/64KB message fragmentation + reassembly
Version History
v0.4.0: Extract kcp-core as standalone protocol crate, restructure source layout (src/ → kcp/, flatten async_kcp/)
v0.3.7: Fix ACK window/UNA fields, generic Transport trait with RPITIT, resilience tests, criterion benchmarks
v0.3.4: Engine refactoring, lock-free buffer pools, documentation
v0.3.3: Performance optimizations, sub-millisecond latency
v0.3.1: Full async support, comprehensive configuration
v0.2.x: Performance improvements and bug fixes
v0.1.x: Initial implementation</p>
]]></description><pubDate>2026-05-11 07:11:35</pubDate></item><item><title>mace：又一个嵌入式 key-value 存储</title><link>https://rustcc.cn/article?id=e2ec9976-8f93-4c2e-b63e-5d4419f55631</link><description><![CDATA[<p>mace 是一个 Rust 实现的嵌入式 KV 引擎，结合了 B+ 树的读性能和 LSM 树的写吞吐，在读多写少和扫描场景下有明显的性能优势。</p>
<hr>
<h2>核心能力</h2>
<ul>
<li><strong>混合架构</strong>：兼顾 B+ 树读速与 LSM 树写吞吐</li>
<li><strong>MVCC 并发</strong>：非阻塞的并发读写</li>
<li><strong>闪存优化</strong>：面向 SSD/NVMe 的 log-structured 设计</li>
<li><strong>大值分离</strong>：独立 Blob 存储，减少写放大</li>
<li><strong>ACID 事务</strong>：完整的事务支持</li>
</ul>
<hr>
<h2>性能数据</h2>
<table>
<thead>
<tr>
<th>场景</th>
<th>吞吐量提升</th>
</tr>
</thead>
<tbody>
<tr>
<td>随机读</td>
<td>2.4x</td>
</tr>
<tr>
<td>范围扫描</td>
<td>3.5x</td>
</tr>
<tr>
<td>读 heavy 混合负载</td>
<td>2.3x</td>
</tr>
<tr>
<td>写 heavy 混合负载</td>
<td>0.76x</td>
</tr>
</tbody>
</table>
<blockquote>
<p>注：以上为与 RocksDB 对比的中位数倍数。</p>
</blockquote>
<hr>
<h2>适用场景</h2>
<ul>
<li>需要高并发读写的嵌入式服务（尤其是 mixed/read-heavy 负载）</li>
<li>写入吞吐敏感的本地存储层（中小 value 场景优势更明显）</li>
<li>混合读写 + 扫描的业务</li>
<li>需要本地事务和 MVCC 的 Rust 应用</li>
</ul>
<hr>
<h2>地址</h2>
<ul>
<li>源码：<a href="https://github.com/abbycin/mace" rel="noopener noreferrer">https://github.com/abbycin/mace</a></li>
<li>Benchmark 脚本：<a href="https://github.com/abbycin/kv_bench" rel="noopener noreferrer">https://github.com/abbycin/kv_bench（scale 分支）</a></li>
</ul>
<blockquote>
<p>mace 还在非常早期的阶段，目前还在努力提升稳定性以及对特定workload进行优化...</p>
</blockquote>
<p><strong>0.0.29 版更新</strong></p>
<table>
<thead>
<tr>
<th>Workload</th>
<th align="right">Mace胜OPS</th>
<th align="right">OPS中位数比 (Mace/RocksDB)</th>
<th align="right">Mace胜p99</th>
<th align="right">p99中位数比 (Mace/RocksDB)</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>W1</code> (95R/5U, uniform)</td>
<td align="right">16 / 16</td>
<td align="right"><strong>2.3x</strong></td>
<td align="right">5 / 16</td>
<td align="right"><strong>1.0x</strong></td>
</tr>
<tr>
<td><code>W2</code> (95R/5U, zipf)</td>
<td align="right">16 / 16</td>
<td align="right"><strong>1.5x</strong></td>
<td align="right">11 / 16</td>
<td align="right"><strong>0.5x</strong></td>
</tr>
<tr>
<td><code>W3</code> (50R/50U)</td>
<td align="right">15 / 16</td>
<td align="right"><strong>1.4x</strong></td>
<td align="right">9 / 16</td>
<td align="right"><strong>0.5x</strong></td>
</tr>
<tr>
<td><code>W4</code> (5R/95U)</td>
<td align="right">12 / 16</td>
<td align="right"><strong>1.3x</strong></td>
<td align="right">7 / 16</td>
<td align="right"><strong>1.0x</strong></td>
</tr>
<tr>
<td><code>W5</code> (70R/25U/5S)</td>
<td align="right">15 / 16</td>
<td align="right"><strong>2.1x</strong></td>
<td align="right">16 / 16</td>
<td align="right"><strong>0.2x</strong></td>
</tr>
<tr>
<td><code>W6</code> (100% scan)</td>
<td align="right">16 / 16</td>
<td align="right"><strong>4.6x</strong></td>
<td align="right">15 / 16</td>
<td align="right"><strong>0.2x</strong></td>
</tr>
</tbody>
</table>
]]></description><pubDate>2026-03-09 11:56:39</pubDate></item><item><title>🌱 Rudis 0.4.0 发布，一个高性能内存数据库</title><link>https://rustcc.cn/article?id=682d0f5e-15ec-4138-aff6-d045fb529a7e</link><description><![CDATA[<p>项目介绍：</p>
<p>Rudis 是一个采用 Rust 语言编写得高性能键值存储系统，旨在利用 Rust 语言的优势来重新复现 Rudis 的核心功能，以满足用户对高性能、可靠性和安全性的需求，同时保证与 Rudis API 的兼容。</p>
<p>跨平台，兼容 windows、linux 系统架构。 兼容 字符串、集合、哈希、列表、有序集合数据结构。 提供 rdb 与 aof 机制以支持数据备份和恢复。 拥有卓越的处理速度和即时响应能力。 兼容 Rudis 的命令和协议规范。</p>
<p>欢迎在 GitHub 上关注我们的项目发展轨迹：</p>
<p>👉 https://github.com/lunar-landing/rudis</p>
<p>更新日志：</p>
<ul>
<li>新增 List 数据结构 Blpop、Brpop 命名。</li>
<li>新增 Hash 数据结构 HSCAN 命令，支持 MATCH 和 COUNT 参数。</li>
<li>新增 String 数据结构 SETEX、PSETEX、SETNX、SETBIT、GETBIT、BITCOUNT、BITOP 命令。</li>
<li>新增 Set 数据结构 SRANDMEMBER、SDIFFSTORE、SINTERSTORE、SMOVE 命令。</li>
<li>新增 HyperLogLog 数据结构及 PFADD、PFCOUNT、PFMERGE 命令。</li>
<li>重构 SortedSet 底层实现，采用 HashMap + SkipList 架构提升性能，并支持 bincode 序列化。</li>
<li>修复 SETEX/PSETEX 过期记录清理逻辑以及系统时间倒退导致的 RDB 调度 Panic 问题。</li>
</ul>
]]></description><pubDate>2026-02-03 03:12:19</pubDate></item><item><title>我做了一个独立开发者行情板，想试着对抗一次内卷</title><link>https://rustcc.cn/article?id=7a4bcdcd-4650-425b-92a4-6ef65838534b</link><description><![CDATA[<h1>接私活这几年，我发现我们根本不知道「合理报价」是多少</h1>
<p>这几年接私活、做外包、做独立项目，有一个问题一直困扰我：</p>
<blockquote>
<p><strong>我们其实不知道一个项目「合理的价格」是多少。</strong></p>
</blockquote>
<p>不是技术难度不知道，而是——<br>
你不知道别人真实成交是多少，只能靠猜、靠平台最低价、靠「听说」。</p>
<p>需求方会说：</p>
<blockquote>
<p>「别人比你便宜一半。」</p>
</blockquote>
<p>开发者只能纠结：</p>
<blockquote>
<p>「我是报高了，还是别人报低了？」</p>
</blockquote>
<p>时间久了，就变成大家都在往下试探，<br>
<strong>内卷不是某个人的选择，而是信息不透明的结果。</strong></p>
<hr>
<h2>我已经做了什么</h2>
<p>我先从自己开始。</p>
<p>我把自己这几年做过的一些真实项目整理出来，包括：</p>
<ul>
<li>项目类型</li>
<li>实际成交价格</li>
<li>大概工期</li>
<li>是否反复改需求</li>
<li>是否包含售后</li>
</ul>
<p>做成了一个 <strong>独立开发者行情板</strong>。</p>
<p>目前一共 <strong>23 个案例</strong>：</p>
<ul>
<li>大部分是我自己的真实成交</li>
<li>少部分是朋友的</li>
<li>也有几个是匿名提交的</li>
</ul>
<p>我不回避这个事实：<br>
<strong>数据现在还很少，而且并不「漂亮」。</strong></p>
<p>但它至少是真实的。</p>
<hr>
<h2>为什么我需要更多人，而不是「更多数据」</h2>
<p>我一个人的案例，其实没什么意义。</p>
<p>但如果有：</p>
<ul>
<li>50 个</li>
<li>100 个</li>
<li>200 个</li>
</ul>
<p>来自不同背景、不同技术栈、不同城市的真实案例，<br>
至少可以做到一件事：</p>
<blockquote>
<p><strong>让后来的人，在报价时有一个不被平台最低价绑架的参考。</strong></p>
</blockquote>
<p>你不需要证明你多厉害，<br>
也不需要报一个「体面」的价格，<br>
<strong>真实比好看重要。</strong></p>
<hr>
<h2>关于匿名和安全</h2>
<p>我知道大家最担心什么，所以我一开始就做了两件事：</p>
<ul>
<li>提供 <strong>匿名提交</strong></li>
<li>不要求任何可追溯身份信息</li>
</ul>
<p>目前有两个入口：</p>
<p><a href="https://test-cigsro9bfq3z.feishu.cn/share/base/form/shrcnoJFwnYGX1E8NKW6qjpNJ6X?from=navigation" rel="noopener noreferrer">飞书表单</a>
<a href="https://market.fxlogo.site" rel="noopener noreferrer">行情板网站</a></p>
<p>不署名、不展示来源、不做商业售卖。<br>
你可以只写你愿意写的字段。</p>
<hr>
<h2>说一句更远一点的想法（不画饼）</h2>
<p>行情板不是终点。</p>
<p>我真正想做的，是一个 <strong>不竞价、不抽佣、不负责售后</strong> 的撮合平台，<br>
只做一件事：</p>
<blockquote>
<p><strong>把预算真实的需求方，和愿意按合理价格做事的开发者，匹配到一起。</strong></p>
</blockquote>
<p>行情板只是前战，是定价共识的基础。<br>
如果连「合理价格区间」都没有，<br>
任何撮合都会退化成比谁便宜。</p>
<p>我不确定这条路能走多远，<br>
但至少想先试一次。</p>
<hr>
<h2>最后</h2>
<p>如果你愿意贡献一个案例：</p>
<ul>
<li>成功的</li>
<li>失败的</li>
<li>觉得自己报低了的</li>
<li>或者被压价压得很难受的</li>
</ul>
<p>都可以。</p>
<p>如果你不想提交，也没关系，<br>
<strong>至少希望这个东西能让你下次报价时，心里多一点底气。</strong></p>
]]></description><pubDate>2026-02-02 10:25:00</pubDate></item><item><title>低成本 AI 赋能首选！算纽 GPUNexus 聚合全球算力，MaaS 服务直达业务核心</title><link>https://rustcc.cn/article?id=d599b7f7-9c0b-4fe6-8396-133b85abbe30</link><description><![CDATA[<p>算纽GPUNexus定位全球 GPU 资源智能调度枢纽，致力于构建低成本、高弹性的下一代分布式 AI 计算生态。我们的核心服务模式：</p>
<ul>
<li>
<p>算力层聚合：广泛接入全球闲散 GPU 算力资源，通过标准化调度技术实现算力的统一管理与高效利用；</p>
</li>
<li>
<p>服务层赋能：在聚合算力之上深度部署 MaaS 模型服务，客户无需投入高昂成本搭建算力与模型架构，只需通过简洁的大模型接口，即可按需调用 AI 能力，快速赋能业务创新。</p>
</li>
</ul>
<p>算纽（GPUNexus）打通算力资源与模型应用的壁垒，让 AI 服务更便捷、更普惠。</p>
<h1>2. 产品形态</h1>
<h2>2.1. 算力资产分享</h2>
<p>算纽算力资产分享产品，核心打破算力孤岛，依托智能调度技术，实现各类计算资源一键接入、整合与统一调度，激活分散算力价值。</p>
<p>产品支持全场景接入，覆盖算力中心、企业服务器等专业设备及个人电脑、手机等终端，实现“云-边-端”全域覆盖。无论闲置算力拥有方（企业/机构/个人）还是算力需求方，均可通过平台精准匹配、高效流转。</p>
<p>无需复杂配置即可快速上线，智能调度实现供需实时匹配，既提升算力利用率，又帮助需求方降本、分享方变现，构建互利共赢的算力生态。</p>
<h2>2.2. MAAS服务</h2>
<p>算纽 MaaS服务，一站式整合30 余款主流开源大模型矩阵，囊括 DeepSeek、Qwen、GLM、Kimi、MiniMax 等明星模型，深度覆盖编程开发、学术研究与论文创作、数学推理、视觉处理与多模态交互、对话与长文本处理五大核心场景。</p>
<h2>2.3. 开发者套餐</h2>
<p>算纽开发者套餐，专为学生、独立开发者及中小团队量身定制，以超高性价比解锁顶级大模型编程能力，让每一份开发需求都能高效落地。</p>
<p>套餐核心优势直击开发痛点：成本颠覆性降低，计费低至传统tokens计费的一折，大幅压缩开发成本；模型自由切换，无需冗余购买多平台会员，一键直达GLM-4.7、MiniMax-M2.1、Kimi-K2三大顶级编程模型，最新最强的模型能力随心选；高效创作不等待，生成速度媲美同类高级套餐，助力快速完成代码编写、调试、优化等核心工作。</p>
<p>更有7天免费体验限时开启！零成本即可抢先体验顶级模型的强悍编程能力，轻松开启高效开发新体验。</p>
<p>​</p>
<ul>
<li>官方网址：<a href="https://gpunexus.com/signup?aff=c1xh" rel="noopener noreferrer">https://gpunexus.com/</a></li>
<li>咨询电话：010-53650986</li>
<li>联系邮箱：data@chengfangtech.com</li>
</ul>
]]></description><pubDate>2026-01-14 02:18:35</pubDate></item><item><title>helix-kanban 终端内的多窗口看板</title><link>https://rustcc.cn/article?id=56234088-880c-4fc8-8281-726abca68b8a</link><description><![CDATA[<h1>Kanban</h1>
<p>一个终端看板应用，灵感来自 <a href="https://helix-editor.com/" rel="noopener noreferrer">Helix 编辑器</a>的键位设计。</p>
<h2>预览</h2>
<p><img src="https://raw.githubusercontent.com/menzil/helix-kanban/master/screenshoot.png" alt="Kanban TUI 截图"></p>
<h2>特性</h2>
<ul>
<li>📁 <strong>基于文件存储</strong> - 使用 Markdown 文件和 TOML 配置，易于版本控制</li>
<li>🎯 <strong>多项目支持</strong> - 支持全局项目和本地项目（<code>.kanban/</code>）</li>
<li>⌨️  <strong>Helix 风格键位</strong> - 符合直觉的键盘快捷键</li>
<li>🪟 <strong>窗口管理</strong> - 支持垂直/水平分屏，同时查看多个项目，自动保存和恢复工作区布局</li>
<li>🎨 <strong>现代 TUI</strong> - 基于 ratatui 的美观终端界面</li>
<li>📝 <strong>Markdown 支持</strong> - 任务使用 Markdown 格式，支持外部编辑器</li>
<li>🔍 <strong>任务预览</strong> - 内置预览和外部预览工具支持</li>
<li>⚙️  <strong>自动配置</strong> - 首次运行自动检测编辑器和预览器</li>
</ul>
<h2>安装</h2>
<h3>从 crates.io 安装</h3>
<pre><code>cargo install helix-kanban
</code></pre>
<h3>从源码构建</h3>
<pre><code>git clone https://github.com/menzil/helix-kanban.git
cd helix-kanban
cargo build --release
</code></pre>
<h2>快速开始</h2>
<p>首次运行会显示欢迎对话框，自动检测系统编辑器和 Markdown 预览器：</p>
<pre><code>hxk
</code></pre>
<h3>输入法切换（macOS）</h3>
<p>为了更好的输入体验，在正常模式下自动切换到英文输入法，在对话框模式（如创建/编辑任务）时保持用户的输入法。</p>
<p><strong>推荐安装 im-select 工具：</strong></p>
<pre><code># 使用 Homebrew 安装
brew install im-select

# 或者使用 curl 安装
curl -Ls https://raw.githubusercontent.com/daipeihust/im-select/master/install_mac.sh | sh
</code></pre>
<blockquote>
<p>注意：如果不安装 im-select，程序仍可正常运行，只是不会自动切换输入法。</p>
</blockquote>
<h3>配置管理</h3>
<p>查看当前配置：</p>
<pre><code>hxk config show
</code></pre>
<p>设置编辑器：</p>
<pre><code>hxk config editor nvim
hxk config editor "code --wait"
</code></pre>
<p>设置 Markdown 预览器：</p>
<pre><code>hxk config viewer glow
hxk config viewer "open -a Marked 2"
</code></pre>
<h2>键位绑定</h2>
<h3>基础导航</h3>
<table>
<thead>
<tr>
<th>键位</th>
<th>功能</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>j</code> / <code>↓</code></td>
<td>下一个任务</td>
</tr>
<tr>
<td><code>k</code> / <code>↑</code></td>
<td>上一个任务</td>
</tr>
<tr>
<td><code>h</code> / <code>←</code></td>
<td>左边的列</td>
</tr>
<tr>
<td><code>l</code> / <code>→</code></td>
<td>右边的列</td>
</tr>
<tr>
<td><code>q</code></td>
<td>退出程序</td>
</tr>
<tr>
<td><code>ESC</code></td>
<td>取消/返回</td>
</tr>
<tr>
<td><code>:</code></td>
<td>命令模式</td>
</tr>
<tr>
<td><code>?</code></td>
<td>显示帮助</td>
</tr>
<tr>
<td><code>Space</code></td>
<td>打开命令菜单</td>
</tr>
</tbody>
</table>
<h3>任务操作</h3>
<table>
<thead>
<tr>
<th>键位</th>
<th>功能</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>a</code></td>
<td>创建新任务</td>
</tr>
<tr>
<td><code>e</code></td>
<td>编辑任务标题</td>
</tr>
<tr>
<td><code>E</code></td>
<td>用外部编辑器编辑任务</td>
</tr>
<tr>
<td><code>v</code></td>
<td>预览任务（TUI 内）</td>
</tr>
<tr>
<td><code>V</code></td>
<td>用外部工具预览任务</td>
</tr>
<tr>
<td><code>d</code></td>
<td>删除任务</td>
</tr>
<tr>
<td><code>H</code></td>
<td>任务移到左列</td>
</tr>
<tr>
<td><code>L</code></td>
<td>任务移到右列</td>
</tr>
<tr>
<td><code>J</code></td>
<td>任务在列内下移</td>
</tr>
<tr>
<td><code>K</code></td>
<td>任务在列内上移</td>
</tr>
</tbody>
</table>
<h3>项目管理</h3>
<table>
<thead>
<tr>
<th>键位</th>
<th>功能</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>n</code></td>
<td>新建本地项目 [L]</td>
</tr>
<tr>
<td><code>N</code></td>
<td>新建全局项目 [G]</td>
</tr>
<tr>
<td><code>Space f</code></td>
<td>快速切换项目</td>
</tr>
<tr>
<td><code>Space p o</code></td>
<td>打开项目</td>
</tr>
<tr>
<td><code>Space p n</code></td>
<td>创建新项目</td>
</tr>
<tr>
<td><code>Space p d</code></td>
<td>删除项目</td>
</tr>
<tr>
<td><code>Space p r</code></td>
<td>重命名项目</td>
</tr>
<tr>
<td><code>Space r</code></td>
<td>重新加载当前项目</td>
</tr>
<tr>
<td><code>Space R</code></td>
<td>重新加载所有项目</td>
</tr>
</tbody>
</table>
<h3>窗口管理</h3>
<table>
<thead>
<tr>
<th>键位</th>
<th>功能</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Space w w</code></td>
<td>下一个窗口</td>
</tr>
<tr>
<td><code>Space w v</code></td>
<td>垂直分屏</td>
</tr>
<tr>
<td><code>Space w s</code></td>
<td>水平分屏</td>
</tr>
<tr>
<td><code>Space w q</code></td>
<td>关闭窗口</td>
</tr>
<tr>
<td><code>Space w h</code></td>
<td>聚焦左面板</td>
</tr>
<tr>
<td><code>Space w l</code></td>
<td>聚焦右面板</td>
</tr>
<tr>
<td><code>Space w j</code></td>
<td>聚焦下面板</td>
</tr>
<tr>
<td><code>Space w k</code></td>
<td>聚焦上面板</td>
</tr>
</tbody>
</table>
<h3>命令模式</h3>
<p>按 <code>:</code> 进入命令模式，支持的命令：</p>
<ul>
<li><code>:q</code> / <code>:quit</code> - 退出应用</li>
<li><code>:open</code> / <code>:po</code> - 打开项目</li>
<li><code>:new</code> / <code>:pn</code> - 创建新项目（全局）</li>
<li><code>:new-local</code> / <code>:pnl</code> - 创建新项目（本地）</li>
<li><code>:add</code> / <code>:tn</code> - 创建新任务</li>
<li><code>:edit</code> / <code>:te</code> - 编辑任务</li>
<li><code>:view</code> / <code>:tv</code> - 预览任务</li>
<li><code>:reload</code> / <code>:r</code> / <code>:refresh</code> - 重新加载当前项目</li>
<li><code>:reload-all</code> / <code>:ra</code> / <code>:refresh-all</code> - 重新加载所有项目</li>
<li><code>:vsplit</code> / <code>:sv</code> - 垂直分屏</li>
<li><code>:hsplit</code> / <code>:sh</code> - 水平分屏</li>
<li><code>:help</code> / <code>:h</code> - 显示帮助</li>
</ul>
<h2>数据存储</h2>
<h3>全局项目</h3>
<p>全局项目存储在 <code>~/.kanban/projects/</code> 目录下。</p>
<h3>本地项目</h3>
<p>在任何目录下按 <code>n</code> 创建本地项目，会在当前目录的 <code>.kanban/</code> 下存储：</p>
<pre><code>your-project/
├── .kanban/
│   └── kanban-project/
│       ├── .kanban.toml
│       ├── todo/
│       ├── doing/
│       └── done/
└── ... (你的其他文件)
</code></pre>
<h3>项目结构</h3>
<pre><code>project-name/
├── .kanban.toml          # 项目配置
├── todo/                 # Todo 任务
│   ├── 001.md
│   └── 002.md
├── doing/                # 进行中任务
│   └── 003.md
└── done/                 # 完成的任务
    └── 004.md
</code></pre>
<h3>任务文件格式</h3>
<p>任务以 Markdown 格式存储：</p>
<pre><code># 任务标题

created: 2025-12-10T10:30:00+08:00
priority: high

任务的详细描述内容...

## 子任务

- [ ] 子任务 1
- [x] 子任务 2
</code></pre>
<h3>配置文件</h3>
<p>应用配置存储在 <code>~/.kanban/config.toml</code>：</p>
<pre><code>editor = "nvim"
markdown_viewer = "glow"

# 隐藏的全局项目列表（软删除）
hidden_projects = ["old-project", "archived-project"]
</code></pre>
<h3>工作区状态保存</h3>
<p>应用会自动保存窗口布局和工作状态，下次启动时恢复：</p>
<p><strong>保存内容</strong>：</p>
<ul>
<li>分屏结构（垂直/水平分割）</li>
<li>每个窗格打开的项目</li>
<li>当前选中的列和任务</li>
<li>聚焦的窗格</li>
</ul>
<p><strong>保存位置</strong>：</p>
<ul>
<li>全局工作区：<code>~/.kanban/workspace.toml</code> - 在任何目录启动时使用</li>
<li>本地工作区：<code>.kanban/workspace.toml</code> - 在项目目录下启动时优先使用</li>
</ul>
<p><strong>使用场景</strong>：</p>
<ul>
<li>经常需要同时查看多个项目？设置好分屏布局后，下次启动自动恢复</li>
<li>在不同项目目录工作？每个目录都有自己独立的工作区布局</li>
<li>想要重置布局？使用命令 <code>:reset-layout</code> 恢复默认单窗格</li>
</ul>
<p><strong>示例工作区配置</strong> (<code>workspace.toml</code>)：</p>
<pre><code># 自动生成，通常无需手动编辑
focused_pane = 2
next_pane_id = 4

[[panes]]
id = 0
type = "horizontal_split"
left = 1
right = 2

[[panes]]
id = 1
type = "leaf"
project = "work-project"
selected_column = 1
selected_task_index = 0

[[panes]]
id = 2
type = "leaf"
project = "personal-project"
selected_column = 0
selected_task_index = 2
</code></pre>
<h2>开发</h2>
<pre><code># 运行开发版本
cargo run

# 运行测试
cargo test

# 构建 release 版本
cargo build --release
</code></pre>
<h2>致谢</h2>
<ul>
<li>键位设计灵感来自 <a href="https://helix-editor.com/" rel="noopener noreferrer">Helix Editor</a></li>
<li>UI 框架使用 <a href="https://github.com/ratatui-org/ratatui" rel="noopener noreferrer">ratatui</a></li>
</ul>
<h2>许可证</h2>
<p>MIT OR Apache-2.0</p>
]]></description><pubDate>2025-12-11 10:37:54</pubDate></item></channel></rss>