CHAPTER 09 · 全栈

部署三条路线:Vercel、Cloudflare、自托管

选择部署平台,本质上是选择运行时的形状:你的代码是一个一直活着的进程,还是按需被唤醒、随时可能被回收的实例?理解这个差异,你就能预判哪些代码在某个平台上会“悄悄失效”。

⏱ 约 55 分钟原理(运行时形状与状态)会用(至少一个平台的完整部署)
读完本章,你应该能够:
  • 比较常驻进程、按需函数实例、V8 isolate 三种运行时的生命周期和限制。
  • 指出一段代码在 serverless 平台上会失效的原因(内存状态、定时器、本地文件、长连接)。
  • 分别说出在 Vercel、Cloudflare、自托管上部署 LinkNote 的步骤和配置要点。
  • 按项目需求用决策树选出平台。

开场:在服务器上正常,上了云就不对

开场问题

LinkNote 的 API 在你自己的服务器上运行良好:它用一个模块级变量 let requestCount = 0 做简单的限流;用 setInterval 每小时清理过期的草稿;上传的图片先存到本地 /tmp 再处理。部署到 Vercel 或 Cloudflare 后:限流时灵时不灵,清理任务从不执行,图片处理偶尔报文件不存在。代码一行没改,为什么行为变了?

答案方向:在自己的服务器上,你的代码是一个常驻进程:内存里的变量一直在,定时器一直在跑,本地文件一直在。在 serverless 平台上,你的代码是按需启动的许多个实例:可能同时有 50 个实例各自计数;请求处理完后实例会被冻结或回收,定时器不会继续执行;每个实例有自己的临时文件系统,下一个请求可能落到另一个实例上。所以:状态不能放在内存里,定时任务和文件存储要用平台提供的专门能力。

三种运行时形状 原理

常驻进程(自托管)按需函数实例(Vercel Functions)V8 isolate(Cloudflare Workers)
生命周期一直运行,直到你重启按需启动;空闲一段时间后回收按需创建,非常轻量,随时可能被回收
实例数量你决定(通常 1~几个)平台自动扩缩,可能很多全球各机房各自独立运行
冷启动无有(通常几百毫秒级,平台有优化)几乎为零(毫秒级)
内存变量✓ 可靠共享✗ 仅在单个实例内、且不持久✗ 同左
后台定时器✓✗ 用平台 Cron✗ 用 Cron Triggers / Durable Object alarm
本地文件✓仅临时目录,不跨请求保证✗ 用 R2 / KV
单次执行时长无限制有上限(按套餐)CPU 时间有上限;等待 I/O 的时间不计入
WebSocket / 长连接✓受限✓ 通过 Durable Objects
计费按机器(固定)按用量(CPU 时间、调用次数)按用量(CPU 时间、请求数),单价很低
类比

常驻进程像你自己开的一家店:店员(进程)一直在,账本(内存)放在柜台上。Serverless 像劳务派遣:来一单活派一个临时工,干完就走,下一单可能是另一个人。账本不能放在临时工口袋里,必须放在所有人都能访问的地方(数据库、KV)。

失配点:平台会复用“热”实例,所以内存变量有时能在多个请求之间保留——这正是 bug 表现为“时灵时不灵”的原因。不要依赖它,但可以利用它做纯粹的性能缓存(例如复用数据库客户端对象)。

一次请求的三条路径

切换标签,看看同一个请求在三个平台上分别经过什么:

Vercel 路线 会用

Vercel 最大的价值是“git push 即部署”的开发体验:

  1. 在 Vercel 控制台导入 GitHub 仓库,它自动识别框架(Next.js、TanStack Start、Vite 等)和构建命令。
  2. 每次推送到主分支 → 生产部署;每个 Pull Request → 独立的预览部署(preview deployment),有自己的 URL。这对 Agent 工作流极其有用:Agent 提交 PR,你打开预览链接直接验收。
  3. 在控制台为 Production / Preview / Development 分别设置环境变量;本地用 vercel env pull 同步。
  4. 数据库、Redis、Blob 等通过 Marketplace 接入第三方服务(Neon、Supabase、Upstash 等),环境变量自动注入。

需要注意的限制:函数有最长执行时间(按套餐不同);长任务应该交给队列或工作流;团队协作按席位收费;Next.js 的部分高级特性(如 ISR、图片优化)在 Vercel 上体验最完整,迁移到其他平台时需要逐项验证。

Cloudflare 路线 会用

Cloudflare Workers 的核心概念是绑定(binding):你在配置文件中声明需要哪些资源(数据库、KV、存储桶、队列……),运行时通过 env 对象注入给你的代码。没有连接字符串,没有 SDK 密钥。

Cloudflare 的存储与计算原语

产品是什么LinkNote 用途
D1托管 SQLite收藏、用户数据
KV全球分布的键值存储,读多写少,最终一致缓存网页标题、配置
R2对象存储,兼容 S3 API,无出口流量费网页快照、截图
Durable Objects每个 ID 对应一个全球唯一、单线程、自带存储的对象每个用户一个 Agent 实例(第 12 章);实时协作
Queues / Workflows消息队列 / 持久化多步工作流摘要生成任务
Workers AI / AI Gateway / Vectorize边缘推理 / LLM 调用网关(缓存、限流、日志)/ 向量数据库嵌入向量、语义搜索
Hyperdrive到外部 Postgres 的连接池与查询缓存如果选择用 Postgres 而非 D1

Durable Objects 值得多说一句,因为它是 Cloudflare 独有、也最难理解的原语。它解决了 serverless 的根本难题——“状态放在哪”:你可以按一个 ID(比如用户 ID、聊天室 ID)获取一个对象,全世界所有对这个 ID 的请求都路由到同一个实例,而且它一次只处理一个请求(不需要锁),自带持久化存储,可以持有 WebSocket 连接、设置闹钟(alarm)。Cloudflare 的 Agents SDK 就构建在它之上。

部署流程:pnpm wrangler login → wrangler d1 create 等创建资源 → wrangler deploy;也可以在控制台连接 Git 仓库自动部署并生成预览 URL。本地开发时,Cloudflare 的 Vite 插件会在真实的 workerd 运行时里运行你的代码,并在本地模拟 D1、KV 等绑定。密钥用 wrangler secret put ANTHROPIC_API_KEY 设置。

自托管路线 会用

如果你有运维经验,这条路最“熟悉”:构建一个 Docker 镜像,跑在一台 VPS 上,前面放一个反向代理处理 HTTPS。

让自托管变得省心的工具:Coolify、Dokploy(开源的自托管 PaaS,提供类似 Vercel 的 Git 部署、预览环境、数据库一键创建);Kamal(把 Docker 应用零停机部署到任意服务器)。常见做法是在前面再加一层 Cloudflare CDN,同时获得缓存和 DDoS 防护。

你需要自己负责的事:系统和依赖的安全更新、数据库备份(并演练恢复!)、监控与告警、日志收集、磁盘空间、扩容。这些是“成本可预测、完全可控”的代价。

怎么选

需要长时间运行的计算 / GPU / 常驻后台进程? 是 否 自托管 / 容器平台 VPS + Docker,或 Fly、Railway 重度依赖 Next.js 的服务器组件、缓存等特性? 是 否 Vercel 功能最完整、零配置 需要实时 / 有状态 / 成本敏感 / 全球低延迟? 是 否 Cloudflare Workers + DO + D1/R2 任选 选你最快上线的
一个粗略的决策树。实际中还要考虑团队经验、数据合规、已有基础设施。第一次上线时,“最快能上线”比“理论最优”更重要。
保持可迁移性的三个习惯
  • 业务逻辑写在普通 TS 模块里,平台相关代码(绑定、Server Action)只在适配层。
  • 优先使用 Web 标准 API(fetch、Request、Response、crypto)和可移植的存储(Postgres、S3 兼容对象存储)。
  • 把平台特定能力(Durable Objects、ISR)视为“有意识的锁定”:用它时写下为什么。
LinkNote 中的落点
  • Vercel 版:Next.js + Neon Postgres + Vercel 的队列 / 工作流能力处理摘要任务。
  • Cloudflare 版:TanStack Start 或 Hono + D1 + Queues + 每个用户一个 Durable Object 承载“问我的收藏” Agent。
  • 自托管版:TanStack Start(Node)+ Postgres + BullMQ/Redis,Docker Compose 一键启动,Caddy 做 HTTPS。
对 Agent 怎么说
模糊
精确
“帮我部署到 Cloudflare。”
“部署目标 Cloudflare Workers。检查代码中所有依赖常驻进程的地方:模块级可变状态、setInterval/setTimeout 后台任务、本地文件读写、长时间 CPU 计算,列出清单和替代方案(KV/D1、Cron Trigger、R2、Queue)。先给清单,确认后再改。”
“加一个定时清理。”
“每天 UTC 3 点清理 7 天前的草稿。我们部署在 Vercel,用平台的 Cron 调用一个受密钥保护的路由(校验 Authorization 头),不要在代码里用 setInterval。”

本章词汇:常驻进程 / serverless / isolate、冷启动、实例、绑定(binding)、Cron 触发、预览部署、回滚、环境变量 / 密钥(secret)、兼容性日期、Durable Object、供应商锁定。

面试角
  • serverless 有什么优缺点?优点:自动扩缩容、按用量计费、免运维。缺点:冷启动、执行时长限制、无状态导致需要外部存储、调试和可观测性更难、厂商锁定。
  • 边缘计算(edge)适合做什么,不适合做什么?适合低延迟的请求处理、个性化、鉴权、缓存;如果数据库在单一区域,边缘函数频繁往返数据库反而更慢——计算应该靠近数据。

自检

1. 为了缓存一个很少变化的配置,代码在模块顶层写了 let cache: Config | null = null,第一次请求时从数据库加载。在 serverless 平台上这是 bug 吗?

通常不是 bug,而是可以接受的性能优化:每个实例第一次请求时加载一次,之后在该实例内复用。关键在于它是可以丢失、可以不一致的缓存,而不是唯一的数据来源。需要注意的是配置更新后,各个实例会在不同时间刷新;如果需要立即生效,就要设置过期时间或改用 KV。对比开场问题中的限流计数器——那是“必须准确共享”的状态,放在内存里就是 bug。

2. 你的 Worker 部署在全球 300 个城市,但 Postgres 在美国东部。一个请求要做 5 次串行查询。一位东京用户的请求会发生什么?有哪些改进方法?

Worker 在东京运行,但每次查询都要跨太平洋往返(每次约 150ms),5 次串行 ≈ 750ms,反而比把整个服务部署在美国东部更慢。改进:减少往返次数(合并查询、并行);用 Hyperdrive 维护连接池并缓存查询;开启 Smart Placement,让 Worker 在靠近数据库的地方执行;或把数据迁移到更靠近用户的存储(如 D1 的读副本)。

3. 预览部署(preview deployment)连接的是生产数据库。这样做有什么风险?

未经审查的 PR 代码(可能是 Agent 写的)直接操作生产数据:一次错误的迁移或删除逻辑就会破坏真实数据。预览环境应该使用独立的数据库(或者用 Neon 这类支持分支的数据库,为每个 PR 创建一个数据分支),并使用单独的、权限受限的密钥。