CHAPTER 09 · 全栈
部署三条路线:Vercel、Cloudflare、自托管
选择部署平台,本质上是选择运行时的形状:你的代码是一个一直活着的进程,还是按需被唤醒、随时可能被回收的实例?理解这个差异,你就能预判哪些代码在某个平台上会“悄悄失效”。
- 比较常驻进程、按需函数实例、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 即部署”的开发体验:
- 在 Vercel 控制台导入 GitHub 仓库,它自动识别框架(Next.js、TanStack Start、Vite 等)和构建命令。
- 每次推送到主分支 → 生产部署;每个 Pull Request → 独立的预览部署(preview deployment),有自己的 URL。这对 Agent 工作流极其有用:Agent 提交 PR,你打开预览链接直接验收。
- 在控制台为 Production / Preview / Development 分别设置环境变量;本地用
vercel env pull同步。 - 数据库、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 防护。
你需要自己负责的事:系统和依赖的安全更新、数据库备份(并演练恢复!)、监控与告警、日志收集、磁盘空间、扩容。这些是“成本可预测、完全可控”的代价。
怎么选
- 业务逻辑写在普通 TS 模块里,平台相关代码(绑定、Server Action)只在适配层。
- 优先使用 Web 标准 API(fetch、Request、Response、crypto)和可移植的存储(Postgres、S3 兼容对象存储)。
- 把平台特定能力(Durable Objects、ISR)视为“有意识的锁定”:用它时写下为什么。
- 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。
本章词汇:常驻进程 / 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 创建一个数据分支),并使用单独的、权限受限的密钥。