第 5 章 部署与运维
学习目标
- 解释预览部署与生产部署分别在什么条件下创建,以及各自的用途。
- 为不同环境分别设置环境变量,并理解修改会在什么时候生效。
- 根据自己的应用,制定并使用部署前的检查与部署后立即执行的确认流程。
- 出现故障时,通过日志缩小发生时间与位置的范围,并优先恢复到上一个正常版本。
- 区分回滚后会发生哪些变化,以及哪些内容(数据、设置)无法通过回滚还原。
引言
周五晚上7点,一位独自运营预约应用的开发者部署了新功能,10分钟后就收到“无法登录”的消息。此时有两个选择:一直在生产环境中修改,直到找到原因;或者在1分钟内回退到昨天正常运行的版本,然后再冷静地调查原因。第一个选择会延长用户遭遇故障的时间,而仓促修改出来的代码,还可能引发第二次故障。
部署是运维的开始。从交付给用户的那一刻起,真实用户、真实数据与费用就会参与其中,因此必须具备三项准备:交付之前,如何在与用户相同的条件下检查;出现问题时,如何及时发现;以及出了问题之后,如何回退。
本章以个人开发者常用的Vercel为例,讨论这三件事。我们将依次学习预览与生产环境的差别、环境变量、部署前后的检查、日志与监控,以及回滚和故障应对。“深入阅读”部分则考察数据变更、渐进式发布,以及免费套餐对运维的限制。即使使用的平台不同,原理仍然相同。
核心概念
部署做什么
部署(deployment)是把在自己电脑运行的应用放上互联网,让任何人都能使用。真正接待顾客的店面是生产环境(production),开业前只请熟人来看的彩排舞台是预览(preview)。在彩排阶段发现问题,顾客就无需承受这些故障。
Vercel文档将部署定义为“项目成功构建的结果”,并说明每创建一次部署,都会生成一个唯一的URL。可以通过Git推送、CLI、部署钩子或REST API创建部署。其中一个重要特征是,每一次部署都会单独保存。即使创建了新部署,之前的部署也仍会保持原样,直到依照保留策略被删除;改变的只是域名究竟指向哪一次部署。回滚之所以能够如此迅速,原因就在这里。
预览与生产环境
Vercel官方文档确认的默认行为如下。
| 情况 | 结果 |
|---|---|
推送到非生产分支(通常生产分支为main) |
预览部署 |
| 创建拉取请求 | 预览部署 |
CLI执行vercel(不带--prod) |
预览部署 |
推送或合并到生产分支,或执行vercel --prod |
生产部署 |
| 新项目首次部署 | 无论哪个分支、即使不带--prod,始终为生产部署 |
首次部署的规则,可能带来意料之外的结果。即使从功能分支首次连接并部署,这一次部署也会成为生产部署。这样设计,是为了让新项目能够立即获得生产域名。
预览URL分为两类。分支URL会始终指向该分支最新的一次部署,提交URL则固定指向某一个特定提交的部署。需要展示“始终最新的工作成果”时,使用分支URL;需要比较或记录某个特定时点时,使用提交URL。Pro与Enterprise套餐还允许单独创建staging等自定义环境。
分享预览时
预览虽然是彩排的舞台,但这个舞台也开放在互联网上。如果任何知道URL的人都能进入,那么预览中显示的数据和功能,就会原样暴露。Vercel的Deployment Protection用于决定谁可以访问部署URL。根据文档,推荐采用的Standard Protection会保护生产域名之外的所有部署,并且所有套餐都可以使用。在各种保护方式中,Vercel Authentication只允许具有相应权限的Vercel用户进入,而密码保护则无法在Hobby套餐中使用。
预览应遵守三个原则。
- 预览环境使用测试用密钥和测试数据库,确保不会发生真实支付,也不会发送真实邮件。
- 向外部人员展示时,应使用平台提供的访问方式,而不是关闭保护。
- 分享之前,应确认预览界面中没有显示真实的个人信息。
各环境分别设置环境变量
Vercel会分别为Production、Preview、Development(本地)以及各个自定义环境保存环境变量。即使变量名称相同,也可以在每个环境中赋予不同的值,例如在预览环境使用测试支付密钥,在生产环境使用真实支付密钥。Preview变量还可以只针对某个分支设置不同的值,分支专用值会覆盖同名的一般Preview值。本地开发所需的值可以通过vercel env pull下载,但应先确认下载得到的env文件已经列入.gitignore。对于每个变量,还可以选择Config(具有权限的成员可以读取)或Secret(保存后无法在管理界面读取,只能更新)类型。
注意
Vercel文档说明,环境变量修改不会应用于旧部署,只对新部署生效。因此修改后必须重新部署。这是“值改了,为什么还是老样子”的常见原因。尤其第4章中构建时嵌入代码的公开前缀变量,必须重新构建才会改变。
新功能使用新变量时,确认已加入所有必需环境。只加Preview、忘记Production,预览正常的功能就会在生产失败;反过来则预览失败。
开发服务器与构建不同
应用在开发服务器(npm run dev)中运行正常,却在部署时失败,这是很常见的情况。原因是,部署过程需要执行构建(npm run build),而不是运行开发服务器;有些错误只有在构建阶段才会暴露出来。例如,类型错误导致构建停止、在构建期间预渲染的页面尝试读取尚不存在的数据,或者缺少构建所需的环境变量。因此,在部署之前,也应先在本地运行一次构建命令。
Vercel的构建日志在部署页面查看,运行时日志(应用执行时留下的记录)在Logs页面查看。构建成功但某功能返回500,应查运行时日志。
部署前检查
| 检查项 | 问题 |
|---|---|
| 检查 | 测试、类型检查、构建是否全部通过 |
| 预览 | 是否在预览URL亲自走过核心流程(注册、支付、保存) |
| 环境变量 | 新变量是否也加入Production |
| 数据 | 数据库结构修改后,是否仍兼容旧代码 |
| 外部集成 | 支付、邮件、webhook是否指向生产地址与生产密钥 |
| 恢复 | 是否知道出问题后应退回哪个上一次正常部署 |
检查表不能只有勾选,也要写已确认与未能确认的内容(第2章)。
部署后立即检查
部署成功的标记,只表示构建已经结束,并不表示用户已经能够正常使用。在部署后几分钟之内,快速确认核心流程的检查,称为冒烟测试(smoke test)。这个说法源于启动一台新机器时,先检查它是否会冒烟。应访问生产地址、登录、执行一次核心功能,再刷新页面确认保存的结果,并查看控制台与运行时日志中是否出现新的错误。
如果希望先确认使用生产设置创建的部署,再将它交付给用户,可以使用Vercel的分阶段生产部署。根据文档,在Environments设置的Production中打开Branch Tracking,并关闭Auto-assign Custom Production Domains,这样,即使生产分支生成了部署,也不会立即绑定到生产域名,而是等待手动提升。先通过生成的部署URL进行确认,随后提升(promote),就不会再次构建,而只会将域名指向这一次部署。不过,要记住,这个部署使用的是生产环境变量,因此会实际访问生产服务与数据。
日志与监控
日志是应用运行留下的记录,监控是持续关注错误率、响应时间等指标。两者作用不同:监控告诉你“有问题”,日志告诉你“什么在何处出了问题”。
Vercel运行时日志会按请求,将函数调用记录与console.log输出归为一组。文档说明,状态码为4xx的请求会标为警告,状态码为5xx的请求则会标为错误;还可以根据路径、状态码、环境、分支、部署等条件筛选日志。CLI中的vercel logs默认显示最近30分钟内的最新100条记录。因此,如果要查看更早的记录,应使用--since扩大时间范围;如果只想查看某一次部署,则使用--deployment指定它。
发生故障时,不要先凭猜测修改,而要在日志中确认三件事:从什么时候开始、在哪些请求中、发生了什么错误。如果错误开始增加的时间与部署时间重合,这次部署就是最可能的原因。
日志有保留期限,如果查看得太晚,记录可能已经消失。因此,需要设置定期确认核心地址的外部检查、错误率上升时发出的提醒,以及用来保存长期记录的外部存储。还必须事先确定,收到提醒后由谁采取什么行动,监控才算完整。
回滚:恢复上一个正常版本
回滚(rollback)是用此前正常运行的部署,替换出现问题的部署。Vercel的Instant Rollback不需要重新构建,就能够立即将域名指向之前的生产部署。以下是通过官方文档核实的注意事项。
- 回滚后的部署会恢复为它当时的配置。在项目设置中后来修改的环境变量不会生效,cron任务的定义也会恢复为该部署所包含的版本。
- 回滚之后,生产域名的自动绑定会关闭。即使生产分支中加入了新的提交,生产域名仍然停留在回滚后的部署上。要恢复平时的工作流程,需要在控制台使用Undo Rollback,或者通过
vercel promote提升另一个部署。 - Hobby套餐只能回滚到紧邻的上一次部署;Pro与Enterprise套餐则可以回滚到任何曾经绑定过生产域名的部署。从来没有绑定过生产域名的预览部署,不属于回滚对象。
- 回滚确认环节会提醒你注意外部API与数据库行为的变化。回滚只恢复代码,不会恢复数据。
回滚只会恢复平台上的部署,Git历史记录则保持原样。导致问题的提交仍然留在main中,因此必须追加一个修复提交,或者对问题提交执行revert(第3章),以免下一次部署再次将相同的问题交付出去。
故障应对顺序
发生故障时,处理顺序就像急诊室的处置顺序:在查明原因之前,要先止血。
- 确认影响。 哪一项功能从什么时候开始失败,又有多少用户受到影响?
- 恢复。 如果最近的一次部署最可能是原因,而且没有修改数据结构,就回滚到上一个正常部署。
- 查找原因。 通过运行时日志和diff,逐步缩小原因范围。在分支中工作,不要直接修改生产界面。
- 修复并验证。 在预览环境中确认修复结果。
- 恢复正常流程。 提升修复后的部署,恢复自动绑定,并执行冒烟测试。
- 留下记录。 写明发生了什么、为什么发生,以及准备修改什么来防止再次发生。即使只是在检查表中增加一行,也是有价值的结果。
深入阅读
代码能回去,数据回不去
回滚看起来如同魔法,但它的这种效果仅限于代码。如果新部署改了数据库的列名,或删除了某一列,那么回滚后的旧代码在查询已经不存在的列时,就会报错失败。新代码按照新格式写入的数据,旧代码也未必能够读取。这就像即使把时钟拨回过去,在这段时间里已经送达并堆积起来的包裹,也仍然留在原处。
因此,数据结构变更应拆成多次部署,确保在任何时点回滚,应用都不会损坏。这通常称为扩展—收缩(expand–contract)方式。
- 扩展:添加新列,保留旧列。
- 双写:新代码同时写新旧列,读时优先新列,没有则读旧列。
- 迁移:将现有数据复制到新列。
- 切换:改为只读写新列的代码。
- 收缩:确认没有代码再用旧列后删除。
每个阶段都可以与前一阶段的代码共同运行,因此,无论在哪次部署回滚,代码与数据都能保持匹配。删除操作要放在最后,等确定不再需要回退时才进行。如果部署会删除或覆盖数据,之前还必须单独完成备份,并确认能够恢复。如果预览与生产环境共用数据库,应避免在构建阶段执行迁移,因为仅仅运行一次预览构建,就可能修改生产数据。
逐步发布
也可以不一次把新版本交给全部用户。
功能开关(feature flag)是代码中包含功能,再用开关决定是否向用户显示。未完成的功能也可保持关闭、频繁合入main,减少长期功能分支的合并冲突;出问题时无需部署,只关开关。
若整套应用一起变化,而非单一功能,可按用户分批发布。Vercel的Rolling Releases(Pro、Enterprise)先将新部署发给部分访客(例如10%),其余仍用旧部署,并排比较两个部署的指标。可分阶段提升至100%,或发现问题后用Instant Rollback恢复。
在部署切换的瞬间,还可能产生版本错位(version skew):用户浏览器里保存的是旧版本的界面代码,而服务器已经运行新版本。Vercel文档将其解释为客户端与服务器运行不同版本,从而产生错误的现象,并说明Skew Protection(Pro与Enterprise)可以让属于同一次部署的客户端与服务器相互通信。这种绑定适用于框架管理的请求,包括静态文件、页面跳转与Server Actions等;对于自行编写的fetch()请求,则需要额外附上部署ID。在上述套餐中,2024年11月19日后使用受支持框架创建的项目,默认启用Skew Protection;文档还建议,在使用Rolling Releases时一同开启它。
小应用的运维限制
使用免费套餐起步的应用,运维工具也受到限制。如果不了解这些限制,往往等到发生故障之后才会发现。按照本书核实时的Vercel文档,典型限制如下。
| 项目 | Hobby | Pro |
|---|---|---|
| 运行时日志保留 | 1小时 | 1天(Observability Plus为30天) |
| Instant Rollback范围 | 紧邻的上一个部署 | 曾绑定生产域名的部署 |
| cron最小间隔 | 每天一次 | 1分钟 |
| cron执行时间精度 | 指定小时内的某一分钟(浮动可达59分钟) | 分钟级 |
套餐的日志保留期限越短,提醒与外部记录就越重要。Hobby的cron会在指定小时的59分钟范围内某个时刻运行,因此不适合必须精确在某个时刻执行的任务。这些数值可能变化,与其死记,不如在选择套餐时,通过官方文档逐一核对自己应用的运维需求与这些限制。
工作场景
星光天文台:绑定域名前
接受观测体验预约的“星光天文台”运营者池经理,准备首次接入订金支付功能,并确定了上线日期。在预览中,测试支付运行正常,测试和构建也都通过了。池经理原本准备直接合并到main,却又停了下来。预览使用的是测试密钥,而生产环境将首次使用真实支付密钥和真实短信发送密钥。这个组合,还没有任何人确认过。
池经理逐一考虑了三个选项。
- 直接合并:这是期待预览既然通过,生产环境也能运行。然而,环境变量与外部集成在各个环境中并不相同。这样做,第一个真实顾客就会成为试验对象。因此否决了这个选项。
- 在生产域名上测试,出问题再回滚:虽然能够确认结果,但测试过程中,顾客也会使用同一个界面。因此也否决了这个选项。
- 分阶段生产部署:使用生产设置创建部署,但暂缓绑定域名。池经理选择了这个办法。
池经理在Production环境的Branch Tracking中,关闭了Auto-assign Custom Production Domains,然后才将代码合并到main。生产部署虽然创建出来,生产域名却仍然指向之前的部署。池经理通过生成的部署URL,使用小额真实支付试着预约,发现支付与预约均能完成,但本该发给顾客的预约确认短信却没有收到。运行时日志中,短信发送阶段出现了缺少API密钥的错误。原来,SMS_API_KEY只设置在Preview环境中。
池经理在Production中填入这个值,并想到变量修改只会在新部署中生效,于是重新部署。在新部署URL中,依次确认支付、预约与确认短信都正常后,才提升这个部署。没有重新构建,改变的只是域名指向。最后,池经理退还了测试支付的款项,并重新开启自动绑定。检查表中的环境变量一行,也从“新变量是否已加入Production”,改成了“使用新变量的所有环境是否都已配置,是否通过生产部署URL确认过”。
波纹游泳馆:周六早晨的500
运营课程申请应用的“波纹游泳馆”开发者玄工程师,在周六上午9点部署了“候补申请”功能。9点12分,提醒来了:/api/lessons路径上的500错误,增加到了平时的几十倍。
玄工程师先确认了影响范围。按状态码500与该路径筛选运行时日志后,发现从9点02分开始,同一个错误就在反复发生,时间与部署重合。课程列表完全无法打开,大部分会员都受到了影响。这次部署中,没有修改数据库表的操作。
玄工程师没有先追查原因,而是立即在控制台中执行Instant Rollback,恢复到紧邻的上一个正常部署。不到1分钟,课程列表就重新打开了。由于知道回滚后自动绑定会关闭,玄工程师在检查表的“回滚中”一栏作了标记。
随后,玄工程师才开始查看原因。错误是Cannot read properties of undefined (reading 'length'),发生在新代码逐一读取各节课程的候补名单的那一行。名单本来是可选字段,新代码却假定它始终存在。在候补申请功能推出之前创建的课程,根本没有这份名单。预览环境中的测试数据只有新创建的课程,因此这个问题没有显现出来。
玄工程师在功能分支中修改代码,让不存在的名单按空列表处理,并增加模拟旧课程的测试数据,编写了测试。在预览中分别打开旧课程与新课程,确认后才合并。正如预期,生产界面仍然是回滚后的版本。玄工程师提升新的生产部署,解除回滚状态,随后执行冒烟测试,最后才清除检查表中的标记。
故障记录中留下了三行:发生了什么(读取现有数据中不存在的字段)、为什么在预览中遗漏了问题(测试数据只有新数据)、要修改什么(在预览数据中加入旧格式的样本)。故障持续了12分钟,恢复花了1分钟,修复花了1小时。如果顺序相反,故障就会持续整整1小时。
易错点
- “预览中可以运行,生产环境中也一定可以。” 因为是相同的代码,很容易觉得结果也会相同。然而,环境变量、外部集成和数据,在不同环境中都有差异。预览通过,只是一个必要条件。
- “修改环境变量,就会立即生效。” 在设置界面保存了,似乎事情就结束了。然而,修改只会应用于新的部署。修改之后,必须重新部署。
- “回滚以后,数据也会回到以前的状态。” 名称叫作“回退”,容易让人觉得所有内容都会恢复。然而,回滚只会恢复代码与当时部署的设置。如果数据结构发生过变更,应当先确认兼容性。
- “发生故障,就直接在生产环境中修复。” 似乎只要找到原因,就能很快结束。然而,在原因不明时动手修改,会延长故障时间,仓促修复还可能引发新的故障。应先恢复到上一个正常部署,再在分支中查找原因。
- “回滚之后,只需把修好的代码推送到
main。” 人们容易以为,像平时一样推送就会生效。然而,回滚之后自动绑定已经关闭。必须提升修复后的部署,才能恢复平时的工作流程。 - “构建成功了,部署就成功了。” 绿色标记让人安心。然而,构建成功并不能保证运行过程中没有错误。部署之后,还需要单独执行冒烟测试,并检查运行时日志。
要点速览
| 主题 | 应记住的内容 |
|---|---|
| 部署 | 成功构建的结果,每个部署有独立URL;改变的是域名指向 |
| 预览条件 | 非生产分支推送、PR、不带--prod的CLI;新项目首次部署始终为生产 |
| URL | 分支URL指向最新,提交URL指向固定时点 |
| 保护 | Standard Protection保护生产域名以外全部部署;预览用测试密钥和数据 |
| 环境变量 | 各环境独立,修改只对新部署生效;新变量加入所有必需环境 |
| 构建与日志 | 部署前本地构建;构建失败看构建日志,运行错误看运行时日志 |
| 部署前后 | 检查表写已确认、未确认;部署后冒烟测试;分阶段部署后提升 |
| 回滚 | 无需重建、立即恢复;设置回当时,数据不变;关闭自动绑定,提升后恢复 |
| 故障顺序 | 确认影响 → 回滚 → 查因 → 修复、预览 → 提升 → 记录 |
| 数据变更 | 用扩展—收缩拆分,确保任意阶段回滚都兼容 |
查阅官方文档
- Vercel: Deployments — 部署定义、方式、提升
- Vercel: Environments — 预览、生产条件,首次部署规则,URL种类,分阶段生产部署
- Vercel: Environment variables — 各环境变量、仅对新部署生效的修改
- Vercel: Deployment Protection — 保护范围、方式、各套餐提供情况
- Vercel: Runtime Logs — 日志筛选、
vercel logs、各套餐保留期限 - Vercel: Instant Rollback — 回滚行为、自动绑定关闭与恢复、套餐范围
- Vercel: Rolling Releases — 先向部分流量发布
- Vercel: Usage & Pricing for Cron Jobs — 各套餐cron间隔与精度
自测
5-1. 首次将一个新项目连接到Vercel时,你在功能分支中执行了不带--prod的vercel。这一次首次部署属于预览,还是生产部署?此后的部署又遵循什么规则?
5-2. 你想让一位外包设计师查看正在制作的界面。设计师希望在接下来的几天内随时访问,并且“始终看到最新状态”。应分享分支URL还是提交URL?另一种URL适合在什么时候使用?
5-3. 回滚到上周的部署之后,你今天在项目设置中修改了一个环境变量的值。这个新值会应用于当前正在生产环境中运行的回滚部署吗?要让新值在生产环境中生效,应当怎么做?
5-4. 在Hobby套餐与Pro套餐中,通过Instant Rollback可以选择的部署范围分别是什么?
5-5. 为了查看两小时前发生的错误,你在终端执行了vercel logs --environment production,但没有看到那个时刻的记录。这条命令默认的查询范围是什么?如果想查看更早的记录,需要增加什么选项?
5-6. 对于使用生产环境变量构建的部署,你希望在绑定生产域名之前亲自确认。在Vercel中,应该修改什么设置,又应按什么顺序操作?确认结束后,提升部署是否会重新构建?
5-7. 请为自己的应用(或练习用应用)编写一份六行的部署前检查表。检查、预览流程、环境变量、数据、外部集成、恢复节点各写一行,并写入自己应用中实际的功能名称。
实操练习
练习
预览、各环境变量、回滚与提升(60分钟)
- 将练习应用连接Vercel,创建首次生产部署。
- 添加一个在界面中显示环境变量值的功能(使用非机密值,例如
APP_LABEL),并为Preview与Production分别设置不同的值。 - 推送功能分支,在预览URL中确认显示的是Preview值;再合并到
main,确认生产环境中显示的是Production值。 - 修改Production中的值,观察重新部署之前与之后,界面上显示的值有何不同。
- 通过Instant Rollback回到上一次部署,然后向
main推送一项小变更,确认它没有自动应用到生产环境;再提升新部署,恢复平时的工作流程。
完成标准: 每个步骤都留下界面显示值与部署状态的截图记录;能够通过截图展示,第4步中重新部署前后的值有何差异,以及第5步中自动更新先停止、在提升之后又恢复的过程。
练习
用日志追踪一次故障(40分钟)
- 在练习应用创建仅特定条件(例如地址中
?fail=1)触发服务器错误并记录日志的路由。 - 创建预览部署,发送几次正常请求与失败请求。
- 在运行时日志中,按照状态码500与对应路径筛选,找到第一次出现错误的时间与错误文字。
- 也通过CLI的
vercel logs查找同样的内容。如果在默认范围中看不到,就使用--since。 - 根据找到的内容,写出三行故障记录(发生了什么、为什么发生、要修改什么)。
完成标准: 在控制台与CLI中找到的首次错误时间一致,并且三行故障记录均以日志中的实际内容为依据。