在Docker容器中使用Rails运行多个容器实例的迁移

33

我看过很多为Rails应用程序创建Docker容器的例子。通常它们会运行一个Rails服务器,并具有运行迁移/设置然后启动Rails服务器的CMD。

如果我同时生成5个这些容器,Rails如何处理多个进程尝试初始化迁移?我可以看到Rails在通用查询日志中检查当前模式版本(它是一个MySQL数据库):

 SELECT `schema_migrations`.`version` FROM `schema_migrations`

如果在不同的Rails实例上同时发生这种情况,我可以看到这里存在竞态条件。

考虑到MySQL中DDL不支持事务,并且在运行迁移时,除了每个迁移事务之外,一般查询日志中没有任何锁定情况,因此启动并行迁移似乎是一个不好的想法。事实上,如果我在本地同时启动三次迁移,当第三个Rails实例完成迁移时,我会看到两个Rails实例尝试创建已经存在的表格而崩溃。如果这是将某些内容插入数据库的迁移,则非常不安全。

那么,运行单个容器以运行迁移/设置,然后生成(例如)一个Unicorn实例,该实例又会生成多个Rails工作进程,是否更好呢?

我应该生成N个Rails容器和一个“迁移容器”,用于运行迁移,然后退出吗?

还有更好的选择吗?


这篇文章似乎很相关:http://blog.carbonfive.com/2015/03/17/docker-rails-docker-compose-together-in-your-development-workflow/ - Max Williams
从他的 Dockerfile 看来,@MaxWilliams 很好地展示了如何无意中引起问题,但并没有提供解决方案。他的示例用于测试很好(但可能会慢一些)。我无法相信每个人都在生产环境中运行单个 Rails 服务器! - Martin Foot
嗯,对的,不好意思 :/ 并不是每个人都在生产环境中运行单个Rails服务器!例如,我们有三个盒子,每个都是Amazon AWS实例,共享一些文件夹,每个运行12个Mongrels,因此实际上我们在3个盒子上有36个Rails服务器。但是,我们不使用Docker :) - Max Williams
使用Capistrano,迁移仅在数据库服务器上运行,其余部分在应用服务器上部署。WEB服务器只接收资产。我想你可以/会做类似的事情吧? - omarvelous
2
在这个问题发布几个月后,Rails 实现了数据库锁定以避免竞争条件:https://github.com/rails/rails/pull/22122 - claasz
显示剩余2条评论
4个回答

31

特别是在Rails方面,我没有任何经验,但从docker和软件工程的角度来看。

Docker团队极力宣传容器是用于应用程序的交付。在这篇非常好的声明中,Jerome Petazzoni说,这完全是关注点的分离。我觉得这正是您已经发现的要点。

运行启动迁移或设置的rails容器可能适用于初始部署,并且在开发过程中通常是必需的。然而,在进入生产阶段时,您确实应该考虑分离关注点。

因此,我会建议使用一个镜像,用于运行N个rails容器,并添加一个工具/迁移/设置任何容器,用于执行管理任务。看看官方rails镜像的开发人员对此的看法:

它旨在被用作一次性容器(挂载源代码并启动容器以启动应用程序),以及构建其他映像的基础。

当您查看该镜像时,没有设置或迁移命令。用户如何使用完全由用户决定。因此,如果您需要运行多个容器,请继续。

从我的mysql经验来看,这很好用。您可以运行一个仅数据的容器来托管数据,运行一个带有mysql服务器的容器,最后运行一个管理任务(如备份和还原)的容器。对于这三个容器,您可以使用相同的镜像。现在您可以自由地从几个Wordpress容器访问数据库。这意味着关注点明确的分离。当您使用docker-compose时,管理所有这些容器并不那么困难。当然,已经有许多第三方容器和工具,也支持您设置由几个容器组成的复杂应用程序。

最后,您应该决定是否使用 docker微服务架构 来解决问题。正如在这篇文章中所概述的那样,有一些反对意见。其中一个核心问题是增加了一个全新的复杂层次。然而,许多解决方案都存在这种情况,我想您已经意识到并愿意接受这一点。


13
docker run <container name> rake db:migrate

启动标准应用容器,但不运行CMD(rails server),而是运行rake db:migrate

更新: Roman建议的命令现在应该是:

docker exec <container> rake db:migrate

现在的命令将是 docker exec <container> rake db:migrate - Roman
5
如果我没记错的话,exec在已经运行的容器中执行命令。因此,您必须在没有迁移的情况下启动应用程序容器,并稍后应用迁移。这可能会导致问题。最好先通过运行(run)应用迁移,然后再启动应用程序容器,这样会更加清晰。Docker compose 可能有所帮助。 - brejoc
命令的另一个更新:现在应该是 docker container exec <container> rake db:migrate - Jordan Brough

2

我遇到了在docker swarm中发布相同的问题,这里提供一个部分来源于他人的解决方案。

Rails已经有一种机制来检测并发迁移,它使用数据库锁。但它会触发ConcurrentException,而不是等待。

一种解决方案是使用循环,每当抛出ConcurrentException时,只需等待5秒钟,然后重新进行迁移。 这尤其重要,因为所有容器都执行迁移,如果迁移失败,所有容器都必须失败。

来自coffejumper的解决方案

  namespace :db do
    namespace :migrate do
      desc 'Run db:migrate and monitor ActiveRecord::ConcurrentMigrationError errors'
      task monitor_concurrent: :environment do
        loop do
          puts 'Invoking Migrations'
          Rake::Task['db:migrate'].reenable
          Rake::Task['db:migrate'].invoke
          puts 'Migrations Successful'
          break
        rescue ActiveRecord::ConcurrentMigrationError
          puts 'Migrations Sleeping 5' 
          sleep(5)
        end
      end
    end
  end

有时您还需要按顺序执行其他进程,例如 after_party、cron 设置等来执行迁移。解决方案是使用与 Rails 相同的机制,在数据库锁定周围嵌入 rake 任务:
下面是基于 Rails 6 代码的示例,migrate_without_lock 执行所需的迁移,而 with_advisory_lock 获取数据库锁定(如果无法获取锁定,则触发 ConcurrentMigrationError)。
module Swarm
  class Migration
    def migrate
      with_advisory_lock { migrate_without_lock }
    end

    private

    def migrate_without_lock
      **puts "Database migration"
      Rake::Task['db:migrate'].invoke
      puts "After_party migration"
      Rake::Task['after_party:run'].invoke
      ...
      puts "Migrations successful"**
    end

    def with_advisory_lock
      lock_id = generate_migrator_advisory_lock_id
      MyAdvisoryLockBase.establish_connection(ActiveRecord::Base.connection_config) unless MyAdvisoryLockBase.connected?
      connection = MDAdvisoryLockBase.connection
      got_lock = connection.get_advisory_lock(lock_id)
      raise ActiveRecord::ConcurrentMigrationError unless got_lock
      yield
    ensure
      if got_lock && !connection.release_advisory_lock(lock_id)
        raise ActiveRecord::ConcurrentMigrationError.new(
          ActiveRecord::ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE
        )
      end
    end

    MIGRATOR_SALT = 1942351734

    def generate_migrator_advisory_lock_id
      db_name_hash = Zlib.crc32(ActiveRecord::Base.connection_config[:database])
      MIGRATOR_SALT * db_name_hash
    end
  end

  # based on rails 6.1 AdvisoryLockBase
  class MyAdvisoryLockBase < ActiveRecord::AdvisoryLockBase # :nodoc:
    self.connection_specification_name = "MDAdvisoryLockBase"
  end
end

和之前一样,做一个循环来等待。
namespace :swarm do
  desc 'Run migrations tasks after acquisition of lock on database'
  task migrate: :environment do
    result = 1
    (1..10).each do |i|
      **Swarm::Migration.new.migrate**
      puts "Attempt #{i} sucessfully terminated"
      result = 0
      break
    rescue ActiveRecord::ConcurrentMigrationError
      seconds = rand(3..10)
      puts "Attempt #{i} another migration is running => sleeping #{seconds}s"
      sleep(seconds)
    rescue => e
      puts e
      e.backtrace.each { |m| puts m }
      break
    end
    exit(result)
  end
end

然后在您的启动脚本中,只需启动rake任务即可。

set -e
bundle exec rails swarm:migrate
exec bundle exec rails server -b "0.0.0.0"


在最后,当所有容器运行您的迁移任务时,它们必须有一种机制来判断是否已经完成,以便不做任何操作(例如db:migrate)。
使用这个解决方案,Swarm启动容器的顺序不再重要,如果出现问题,所有容器都知道问题的所在 :-)

0

对于单个容器 ID:

docker exec -it <container ID> bundle exec rails db:migrate

对于多个容器,我们可以重复执行该过程,如果数量达到1000,则需要脚本来执行。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接