Heroku发布阶段rails db:migrate和命令失败

185 阅读10分钟

如果你使用capistrano来部署Rails应用程序,它通常会在每次部署时运行rails db:migrate ,以应用任何数据库模式的变化。

如果你要部署到heroku,你可能想做同样的事情。heroku"发布阶段 "功能使这成为可能。(发布阶段功能于2017年推出,是heroku最近的主要功能之一,因为heroku开发似乎真的稳定和/或停滞了)。

发布阶段的文档提到了 "运行数据库模式迁移 "这一用例,网上有一些((1)(2)(3))的博文建议用Rails来做这件事。基本上就像把release: bundle exec rake db:migrate 添加到你的Procfile 一样简单。

虽然有些博文确实提醒你 "如果发布阶段失败,应用程序将不会被部署",但我发现这句话的含义在实践中比人们最初设想的更令人困惑。特别是因为在heroku上,改变一个配置变量就会触发一次发布;而当这样的发布失败时,就会令人困惑。

考虑一下细节是有好处的,这样你就会明白发生了什么,可能还会考虑比简单地调用rake db:migrate 更复杂的发布逻辑。

1)如果一个配置变量的改变使你的Rails应用无法启动,怎么办?

我不知道这有多不寻常,但我在设置heroku应用的过程中确实遇到了这样一个真实世界的错误。 在不混淆细节的情况下,我们可以简单地模拟这样一个bug,比如说,把这个放在config/application.rb

if ENV['FAIL_TO_BOOT']
  raise "I am refusing to boot"
end

显然,我真正的bug更奇怪,但结果是一样的--在一个或多个heroku配置变量的某些设置下,应用程序会在启动时引发一个异常。而在部署到heroku之前,我们在测试中并没有注意到这个问题。

现在,在heroku上,使用CLI或Web仪表板,将配置变量FAIL_TO_BOOT 设置为 "true"。

没有发布阶段,会发生什么?

  • 发布 成功了!如果你在仪表盘("活动 "标签)或heroku releases ,看一下发布的情况,它显示为成功。这意味着heroku带来了新的数据,并关闭了以前的数据,这就是发布的意义所在。
  • 当heroku试图在新的dynos中启动它时,应用程序就会崩溃。
  • 当在heroku ps 或仪表盘中查看时,朝向将处于 "crashed "状态。
  • 如果用户试图访问网络应用,他们会得到通用的heroku级别的 "无法启动应用 "的错误屏幕(除非你像往常一样,定制了你的heroku错误屏幕)。
  • 你可以查看你的heroku日志,看看阻止应用程序启动的错误和堆栈跟踪。

缺点:你的应用程序已经停机。

好处是:你的应用被关闭了,而且(相对而言)原因很明显。

db:migrate发布阶段,会发生什么?

Railsdb:migrate rake任务对rails:environment 任务有依赖性,这意味着它在执行前会启动Rails应用。你刚刚改变了你的配置变量FAIL_TO_BOOT: true ,使Rails应用无法启动。改变配置变量引发了一次发布。

作为发布的一部分,db:migrate发布阶段被运行......但却失败了:

  • 发布不是 成功的,而是失败的。
  • 在你的heroku config:add ,或者在仪表盘GUI的 "设置 "标签中,你并没有得到任何即时的反馈。你可以继续你的业务,假设它成功了。
  • 如果你在heroku releases 或仪表板的 "活动 "标签中查看发布,你会看到它失败了。
  • 你确实会收到一封电子邮件说它失败了。也许你马上就注意到了,也许你后来才注意到,并且必须弄清楚 "等等,哪个发布失败了?它的影响是什么?我应该担心吗?"
  • 效果是:
    • 配置变量出现在heroku的仪表板上,或在响应heroku config:get ,等等。
    • 没有配置变量变化的旧朝代仍在运行。他们没有改变。如果你打开一个一次性的dyno,它将使用旧的版本,并且有旧的(例如)ENV['FAIL_TO_BOOT']值。
    • 只要应用程序处于无法启动的状态(基于当前的配置变量),任何后续的发布尝试都将持续失败。

再说一遍,这真的发生在我身上这是一个相当混乱的情况。

好处是。你的应用程序实际上还在运行,尽管你破坏了它,正在运行的旧版本还在运行,这很好?

劣势:发生的事情真的很让人困惑。你可能一开始没有注意到。事情一直处于混乱的不一致和混乱的状态,直到你注意到,弄清楚发生了什么,哪个版本造成的,以及如何修复它。

任何配置变量的改变都可以做到这一点,这有点可怕。但我想大多数人不会像我一样遇到这种情况,因为我没有看到有人提到这个问题?

2) heroku pg:promotion是一个配置变量的变化,它将创建一个db:migrate发布阶段失败的版本

[heroku pg:promote](https://devcenter.heroku.com/articles/heroku-postgresql#pg-promote)是一个命令,它将改变多个附加的heroku postgreses中的哪一个被附加为 "主 "数据库,由DATABASE_URL 配置变量所指向。

对于一个只有一个数据库的典型应用,你仍然可以在数据库升级过程中使用pg:promote ;设置或改变postgres的高可用性领导/跟随者;或者,对于我正在试验的,使用heroku的基于postgres-log的回滚功能

我曾认为pg:promote 是一个零停机时间的操作。但是,在调试它与我的发布阶段的交互时,我注意到pg:promote 实际上创建了两个heroku版本:

  1. 首先,它创建了一个名为Detach DATABASE 的版本,其中根本就没有DATABASE_URL 的配置变量。
  2. 然后,它创建了另一个名为Attach DATABASE 的版本,其中的DATABASE_URL 配置变量被定义为新值。

为什么它要这样做,而不是在一个版本中只改变DATABASE_URL ? 我不知道。我的应用程序(就像大多数Rails和可能的其他应用程序一样)在没有设置DATABASE_URL 的情况下实际上无法运行,所以如果第一个版本真的运行,它就会出错。这是否意味着在部署了一个 "糟糕 "的版本后,pg:promote 并非真正的零延迟? 我不确定,这似乎不对(我确实提交了一张heroku支持票,询问....)。

但在正常情况下,要么这不是一个问题,要么大多数人(?)没有注意到。

但是如果你有一个db:migrate的发布阶段呢?

当它试图进行上面的发布(1)时,该发布将*失败。*因为它试图运行db:migrate ,而在没有设置DATABASE_URL 的情况下,它无法做到这一点,所以它提出,释放阶段在错误状态下退出,释放失败。

实际上发生的情况是,如果没有设置DATABASE_URL,Rails应用就会假定在一个 "默认 "的位置有一个postgres URL,试图连接到,然后失败,并有一个错误信息(hello googlers?

ActiveRecord::ConnectionNotEstablished: could not connect to server: No such file or directory
	Is the server running locally and accepting
	connections on Unix domain socket "/var/run/postgresql/.s.PGSQL.5432"?

现在,版本(2)几秒钟后就要发布了,这实际上是很好的,而且是零中断。我们有一个失败的版本(所以从未被部署),几秒钟后,下一个正确的版本就成功了。很好!

唯一的问题是,我们收到了一封电子邮件,通知我们第1版失败了,而且在heroku的发布列表中也可以看到它失败了,等等。

一个 "后台"(不是对git push 或其他代码推送到heroku的响应)发布失败已经是一个令人困惑的情况--一个 "假阳性",实际上意味着 "没有任何意外或问题发生,只要忽略它并继续。"这是......我真的不想要的。(我把这叫做 "错误通知狼来了",对吗?我试图确保我的错误通知永远不会这样做,因为它不必要地占用了你的时间,和/或使你更难对真正的错误保持警惕)。

现在,有一个相当简单的解决方案来解决这个特殊的问题。我是这样做的。我把我的heroku发布阶段从rake db:migrate 改为一个自定义的rake任务,例如release: bundle exec rake my_custom_heroku_release_phase ,这样定义。

.gist table { margin-bottom:0; }

task :my_custom_heroku_release_phase do
如果 ENV['DATABASE_URL']
Rake::Task["db:migrate"].invoke
否则
$stderr.put " `n'!!!警告,没有ENV['DATABASE_URL'],没有运行rake db:migrate作为heroku版本的一部分!!。n\n\n"
结束
结束

现在,上面的发布(1)至少不会失败,它的行为与没有发布阶段的 "传统 "heroku应用相同。

吞下并报告所有错误?

当一个发布失败是因为一个发布阶段由于git push 到heroku而失败时,这很清楚,也很好!但是,"后台 "的混乱使我们无法理解。

但 "后台 "发布失败的混乱情况,由一个配置变量的变化引发的,足以让我想在那里rescue StandardError ,并防止一个失败的发布阶段以失败代码退出,所以heroku永远不会使用db:migrate 发布阶段来中止发布。

只要把行为恢复到发布阶段前的heroku行为就可以了--你可以把你的应用放在一个会被崩溃而无法工作的情况下,但也许这比在后台发生的神秘的不一致的heroku应用状态要好,而你只能通过heroku的异步电子邮件通知来发现,这很难理解/诊断。这一切都更明显。

另一方面,如果db:migrate失败了,不是因为一些不相关的启动过程问题,即使它被释放了,也会使应用程序无法启动,而只是因为db:migrate本身失败了......你希望释放失败?这很好吗?让旧的版本继续运行,而不是让新的版本与期望数据库迁移的代码一起运行,但这并没有发生?

所以我不太确定。

如果你确实想拯救-吞噬-通知,为你的heroku发布逻辑定制rake任务--而不是仅仅告诉heroku在发布时运行一个标准的东西,如db:migrate --当然很方便。

另外,你真的总是想进行db:migrate吗?db:schema:load呢?

另一个选择......如果你在部署一个带有数据库的应用,标准的Rails惯例是运行rails db:schema:load instead of db:migrate 。db:migrate可能会起作用,但会更慢,而且更容易出错。

我想这个问题可能会出现在heroku的初始部署中,或者(出于某种原因)数据库被清空并重新启动,或者可能是Heroku的 "审查应用"?(我还没有使用这些)

stevenharman有一个解决方案,实际上是检查数据库,并根据状态运行适当的rails任务,在这个gist中。

如果我打算这样做,我可能会把它作为一个rake任务,而不是一个bash文件。我现在还没有这样做。

请注意,stevenharman的解决方案实际上会捕捉到一个不存在或无法连接的数据库,并且不会尝试运行迁移......但它会在这种情况下打印一个错误信息和exit 1 ,使发布失败--这意味着在上面提到的pg:promote 的情况下,你会得到一个失败的发布!