站点是 zendot.org ,开源项目推荐。后端 Rust ( Actix Web + SeaORM + Postgres ),前端 Next.js ,后端约 1.4 万行,跑在一台 4G 内存的小机器上。 先说结论,免得看到最后:Rust 在这个项目里给我最大的好处不是性能,是「部署就是一个二进制、没有运行时依赖」和「没有 GC 尖刺」。最大的代价是编译时间。 下面全是具体的表和数字,没有心得。 一、数据模型我是直接抄 WordPress 的 内容站要解决的问题,WordPress 二十年前就解完了。我没想到任何必须重新发明的地方,所以表结构基本照着 WP 抄: posts ≈ wp_posts 文章本体,多两列 title_en / body_en 存英文 terms ≈ wp_terms + wp_term_taxonomy 我把 taxonomy_id 直接放在 terms 上 taxonomies ≈ wp_taxonomies 只有三条:category / post_tag / domain post_terms ≈ wp_term_relationships post_revisions ≈ wp_posts 的 revision 单独拆表 options ≈ wp_options media ≈ attachment menus / menu_items 抄的好处很具体:后期想加任何「内容站都会有」的需求,WP 里都有成熟答案,我只需要判断要不要实现,不用重新推演一遍。 比如 options 表: create table options ( key varchar primary key, value jsonb not null, autoload boolean not null default false, updated_at timestamptz not null default now() ); 站点标题、描述、评论开关全在里头,加一个配置就是加一行数据。autoload 那列也是抄的——每次请求都要读的配置标 true 。 再说一个我踩的坑。terms 的唯一约束我建的是 (taxonomy_id, slug),不是 slug 。于是「 analytics 」可以同时存在于 domain (中文名「数据分析」)和 post_tag 两个分类法里。 我某天排查数据时写了这么一句: select slug, count() from terms group by slug having count() > 1; 返回 analytics | 2 ,我当场判定是脏数据,准备写脚本合并。看了表结构才发现是自己漏了 taxonomy_id——不同分类法下同名 slug 完全合法。 教训:查数据之前先看约束。这个错误值我一个下午。 二、分层和 trait ,收益到底在哪 大概是 domain / app / infra / api 四层,ObjectStorage 、各种 Repository 都是 trait 。 我不想吹「架构清晰」,说两个具体的: 一是一个二进制里塞多个运维命令。cargo build --bins 一次编出主服务和 seed-admin /backfill-avatars / backfill-stats 三个脚本。它们和主服务共用同一份 domain 和 infra ,所以运维脚本操作数据的口径和线上完全一致——不会出现「脚本把文件写到另一个存储后端」这种事。这才是端口抽象给我的实际价值。 二是存储后端能换。ObjectStorage 有 LocalFs 和 S3 两个实现,本地开发落本地盘。 代价也说:Arc 到处飞,每个仓储方法都要 async_trait ,样板量不小。 三、Actix 用下来的真实感受 顺手的: - web::Data 共享状态很直接,不用引 DI 框架 - actix-files 一行挂出上传目录: .service(Files::new("/uploads", upload_dir.clone())) - 中间件能精确包一层。后台鉴权我只包了 /admin 那一层: web::scope("/admin") .wrap(HttpAuthentication::bearer(middleware::validator)) .service(handlers::auth::register) .service(handlers::posts::create) - 重构时编译器会兜住你。改了 domain 层的字段,所有没跟上的地方会被列出来。 不顺手的: - 错误处理要自己搭。我写了 ApiError + ResponseError 把 DomainError 映射成状态码, 否则每个 handler 都得手写 match 。 - HttpAuthentication::bearer 和自己那套 Claims 的接法,文档得翻一会儿。 - 生态比 axum 小。我需要的它都有,但差距是客观存在的。 四、SeaORM 的坑(这段信息量最大) SeaORM 够用,但别以为它生成的 SQL 理所当然是最优的。三个具体的: 1 ) .count() 会生成 SELECT COUNT() FROM (SELECT 所有列 ...) 慢查询日志里抓到的: SELECT COUNT() AS num_items FROM ( SELECT "repo_items"."id", ..., "repo_items"."readme", ... FROM "repo_items" WHERE "repo_items"."status" = $1 ) AS "sub_query" readme 是整篇 README 的正文。为了数一行数,把几百条记录的 README 都 SELECT 出来。Postgres 一般会把这个子查询优化掉,但这条 SQL 本身就不该存在。改成 count(id) 或者裸 SQL 就完了。 (列表查询是同一个毛病,SELECT * 把 body / body_en 一起拖出来,一次 12 篇正文。这是我还欠着的优化项。) 2 ) 预编译语句让查询日志翻倍 开 log_min_duration_statement = 0 排查时,一条查询在日志里是两行:bind 和 execute 。我第一次统计「每页多少条 SQL 」直接算成了两倍。只数 execute 。 3 ) 迁移里表达不了 partial index 我需要这么一个索引: create index ... on repo_items (status, language) where language is not null and language ''; SeaORM 的 Index builder 表达不了这个 WHERE ,最后是在迁移里 execute_unprepared 写裸 SQL 。能用 Builder 就用,不能的地方别硬拗。 五、一个 Rust 特有的坑:feature gate 导致的静默降级 这个我觉得最值得单独拎出来


  • 情报分类:硬件与数码
  • 分类依据:内容涉及硬件、数码产品或通信卡
  • 信息来源:服务器 / V2EX
  • 发布时间:2026/9/17 16:02:13