第 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套餐中使用。

预览应遵守三个原则。

  1. 预览环境使用测试用密钥和测试数据库,确保不会发生真实支付,也不会发送真实邮件。
  2. 向外部人员展示时,应使用平台提供的访问方式,而不是关闭保护。
  3. 分享之前,应确认预览界面中没有显示真实的个人信息。

各环境分别设置环境变量

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章),以免下一次部署再次将相同的问题交付出去。

故障应对顺序

发生故障时,处理顺序就像急诊室的处置顺序:在查明原因之前,要先止血。

  1. 确认影响。 哪一项功能从什么时候开始失败,又有多少用户受到影响?
  2. 恢复。 如果最近的一次部署最可能是原因,而且没有修改数据结构,就回滚到上一个正常部署。
  3. 查找原因。 通过运行时日志和diff,逐步缩小原因范围。在分支中工作,不要直接修改生产界面。
  4. 修复并验证。 在预览环境中确认修复结果。
  5. 恢复正常流程。 提升修复后的部署,恢复自动绑定,并执行冒烟测试。
  6. 留下记录。 写明发生了什么、为什么发生,以及准备修改什么来防止再次发生。即使只是在检查表中增加一行,也是有价值的结果。

深入阅读

代码能回去,数据回不去

回滚看起来如同魔法,但它的这种效果仅限于代码。如果新部署改了数据库的列名,或删除了某一列,那么回滚后的旧代码在查询已经不存在的列时,就会报错失败。新代码按照新格式写入的数据,旧代码也未必能够读取。这就像即使把时钟拨回过去,在这段时间里已经送达并堆积起来的包裹,也仍然留在原处。

因此,数据结构变更应拆成多次部署,确保在任何时点回滚,应用都不会损坏。这通常称为扩展—收缩(expand–contract)方式。

  1. 扩展:添加新列,保留旧列。
  2. 双写:新代码同时写新旧列,读时优先新列,没有则读旧列。
  3. 迁移:将现有数据复制到新列。
  4. 切换:改为只读写新列的代码。
  5. 收缩:确认没有代码再用旧列后删除。

每个阶段都可以与前一阶段的代码共同运行,因此,无论在哪次部署回滚,代码与数据都能保持匹配。删除操作要放在最后,等确定不再需要回退时才进行。如果部署会删除或覆盖数据,之前还必须单独完成备份,并确认能够恢复。如果预览与生产环境共用数据库,应避免在构建阶段执行迁移,因为仅仅运行一次预览构建,就可能修改生产数据。

逐步发布

也可以不一次把新版本交给全部用户。

功能开关(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小时。

易错点

  1. “预览中可以运行,生产环境中也一定可以。” 因为是相同的代码,很容易觉得结果也会相同。然而,环境变量、外部集成和数据,在不同环境中都有差异。预览通过,只是一个必要条件。
  2. “修改环境变量,就会立即生效。” 在设置界面保存了,似乎事情就结束了。然而,修改只会应用于新的部署。修改之后,必须重新部署。
  3. “回滚以后,数据也会回到以前的状态。” 名称叫作“回退”,容易让人觉得所有内容都会恢复。然而,回滚只会恢复代码与当时部署的设置。如果数据结构发生过变更,应当先确认兼容性。
  4. “发生故障,就直接在生产环境中修复。” 似乎只要找到原因,就能很快结束。然而,在原因不明时动手修改,会延长故障时间,仓促修复还可能引发新的故障。应先恢复到上一个正常部署,再在分支中查找原因。
  5. “回滚之后,只需把修好的代码推送到main。” 人们容易以为,像平时一样推送就会生效。然而,回滚之后自动绑定已经关闭。必须提升修复后的部署,才能恢复平时的工作流程。
  6. “构建成功了,部署就成功了。” 绿色标记让人安心。然而,构建成功并不能保证运行过程中没有错误。部署之后,还需要单独执行冒烟测试,并检查运行时日志。

要点速览

主题 应记住的内容
部署 成功构建的结果,每个部署有独立URL;改变的是域名指向
预览条件 非生产分支推送、PR、不带--prod的CLI;新项目首次部署始终为生产
URL 分支URL指向最新,提交URL指向固定时点
保护 Standard Protection保护生产域名以外全部部署;预览用测试密钥和数据
环境变量 各环境独立,修改只对新部署生效;新变量加入所有必需环境
构建与日志 部署前本地构建;构建失败看构建日志,运行错误看运行时日志
部署前后 检查表写已确认、未确认;部署后冒烟测试;分阶段部署后提升
回滚 无需重建、立即恢复;设置回当时,数据不变;关闭自动绑定,提升后恢复
故障顺序 确认影响 → 回滚 → 查因 → 修复、预览 → 提升 → 记录
数据变更 用扩展—收缩拆分,确保任意阶段回滚都兼容

查阅官方文档

自测

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分钟)

  1. 将练习应用连接Vercel,创建首次生产部署。
  2. 添加一个在界面中显示环境变量值的功能(使用非机密值,例如APP_LABEL),并为Preview与Production分别设置不同的值。
  3. 推送功能分支,在预览URL中确认显示的是Preview值;再合并到main,确认生产环境中显示的是Production值。
  4. 修改Production中的值,观察重新部署之前与之后,界面上显示的值有何不同。
  5. 通过Instant Rollback回到上一次部署,然后向main推送一项小变更,确认它没有自动应用到生产环境;再提升新部署,恢复平时的工作流程。

完成标准: 每个步骤都留下界面显示值与部署状态的截图记录;能够通过截图展示,第4步中重新部署前后的值有何差异,以及第5步中自动更新先停止、在提升之后又恢复的过程。

练习

用日志追踪一次故障(40分钟)

  1. 在练习应用创建仅特定条件(例如地址中?fail=1)触发服务器错误并记录日志的路由。
  2. 创建预览部署,发送几次正常请求与失败请求。
  3. 在运行时日志中,按照状态码500与对应路径筛选,找到第一次出现错误的时间与错误文字。
  4. 也通过CLI的vercel logs查找同样的内容。如果在默认范围中看不到,就使用--since。
  5. 根据找到的内容,写出三行故障记录(发生了什么、为什么发生、要修改什么)。

完成标准: 在控制台与CLI中找到的首次错误时间一致,并且三行故障记录均以日志中的实际内容为依据。