在生产环境中闪耀的 Ruby

即使是最漂亮的 Rails 应用程序,如果部署不当,也会失去其优雅。与 Sinatra 等其他 Ruby 框架或语言一样,Rails 也是基于 Rack 接口的。本文将基本介绍 Rack 托管和基于 Rack 的应用程序部署。

当 Rails 于 2005 年首次发布时,开发者们欢欣鼓舞。终于,一个全面的 Web 应用程序开源框架问世了,它包含一系列工具,使 Web 开发变得快速、高效且有趣。Rails 以“开发者天堂”而闻名,但尽管它提供了许多避免典型和重复性任务的便利,但仍然存在一个弱点:部署。部署 Rails 应用程序并非易事。每个人都知道 Rails 应用程序终有一天会被发布到网上,但具体如何发布却不清楚。

平台即服务 (PaaS)

开发者通常选择购买平台即服务 (Platform as a Service, PaaS) 托管空间(例如,Heroku、OpenShift 或 EngineYard)。PaaS 非常棒,因为它提供了一个包含完整软件依赖堆栈的即用型环境。一般来说,在 PaaS 平台上发布很容易、很快,而且一切往往(几乎)立即就能工作。但是,至少在两种情况下,PaaS 将无法满足您的需求:当应用程序必须保存在客户的私有基础设施中时,或者当应用程序具有更高的硬件或软件要求时——例如,当您需要 PaaS 提供商不支持的特定软件服务时。

在这种情况下,您必须实施自定义虚拟服务器配置和自定义部署程序。您可以将 Rails 应用程序部署在服务器或虚拟机上。像 Amazon Web Services (AWS) 这样的完整云服务的可用性正在大幅增长,这些服务允许您创建由多个 Web 服务器、数据库服务器和前端负载均衡机器组成的复杂基础设施。这种方法非常灵活,但您必须访问、安装和管理操作系统和发行包,配置网络,激活服务等等。在本文中,我将介绍基于 Rack 的托管软件要求以及一些基本示例配置,以在 GNU/Linux 服务器上实现自动化的 Ruby 托管。

RVM

首先,如果您想托管 Ruby 软件,您必须安装 Ruby 平台。您可以使用 apt-get 或 yum 安装 Ruby 和 gems。这很简单,但是当您的应用程序需要特定的 gem 版本或特定的解释器版本时,您将面临一个常见问题。如果您的 GNU/Linux 发行版没有打包这些特定版本,您如何满足这些请求?此外,您如何以干净且可重复的方式维护多个 Ruby 版本?

您可能会认为您可以直接下载 Ruby 平台并手动编译它。可以保证您可以安装所需的解释器版本和 gem 版本。不幸的是,这完全不方便。这种软件管理方式使您的配置难以更新。

有几种解决方案可以克服这些常见问题。我发现对于服务器环境更可靠的一种解决方案名为 Ruby enVironment Manager (RVM)。RVM 包含一组脚本,可帮助您安装和更新 Ruby 生态系统。

以 root 用户身份执行以下命令下载 RVM


# \curl -L https://get.rvm.io | bash -s stable

尽管建议您使用 sudo 等安全工具来使用 RVM,但 rvm 可执行文件必须在您的 root $PATH 环境中可用,因此请以 root 用户身份安装它。对于服务器典型的多用户 RVM 安装,该软件默认保存在 /usr/local/rvm 目录中,因此您可以使用 rm -fr /usr/local/rvm 命令安全地删除整个发行版。

在继续 Ruby 安装之前,请确保您的系统已准备好编译 Ruby。检查您的 PATH 中是否有可用的 rvm 命令(如果没有,请注销并重新登录或使用 bash -s 重新加载您的 shell),然后执行以下命令


$ sudo rvm requirements

RVM 将通过 yum 或 apt-get 安装编译 Ruby 发行版所需的软件包。在本文中,我使用名为 MRI (Matz Ruby Interpreter) 的稳定官方 Ruby 发行版(源自 Ruby 创建者 Yukihiro Matsumoto 的名字)。

现在,您可能需要在您未来的 Ruby 版本中添加一些基本库,这些库通常是某些复杂 gems 或软件所需要的。立即设置这些库将保证 Ruby 软件永远不会抱怨系统库过旧或不兼容,从而产生烦人的错误。以前,您会通过 rvm pkg install <pkg> 命令安装这些额外的软件包,但现在 RVM 弃用了这种做法。相反,只需启用 autolibs,将构建连贯且无错误的发行版的责任委托给 RVM


$ sudo rvm autolibs enable

您终于可以为您的环境提供完整的 Ruby 发行版了。例如,让我们安装官方 MRI 解释器的最新稳定版本,即 2.0.0 版本


$ sudo rvm install 2.0.0

如果一切顺利,该发行版可供 root 用户和系统用户使用。如果不是,通常是 $PATH 问题,因此在 /etc/profile.d 中调整它,并且为了避免部署陷阱,请验证 $GEM_HOME 变量是否已导出到正确的 gem 路径。实际上,如果某些东西工作不正常,请像这样设置以下变量


if [ -d "/usr/local/rvm/bin" ] ; then
    PATH="/usr/local/rvm/gems/ruby-2.0.0-p353@global/bin:
↪/usr/local/rvm/bin:$PATH"
    GEM_HOME="/usr/local/rvm/gems/ruby-2.0.0-p353@global"
fi

您可以使用以下命令列出可用的 Ruby 版本


$ rvm list known

在运行多个 Ruby 版本的系统上,用户和系统进程可以使用如下命令加载其他环境版本


$ rvm use jruby-1.7.1

并以这种方式设置默认系统发行版


$ rvm --default use 2.0
Web 服务器

Ruby on Rails,像 Sinatra 和许多其他流行的 Ruby 框架或领域特定语言一样,都是基于名为 Rack 的接口。Rack 在支持 Ruby 的 Web 服务器和 Ruby 框架之间提供了最小的抽象。Rack 负责调用在启动文件 config.ru 中指定的应用程序主实例。

因此,托管 Ruby Web 应用程序的 Web 服务器必须了解 Rack 如何通信。有了稳定且干净的 Ruby 环境,您就可以构建能够与 Rack 通信的 Web 服务器了。

使用 Ruby,您可以在许多 Web 服务器之间进行选择。您可能听说过 Mongrel、Unicorn、Thin、Reel 或 Goliath。对于典型的 Rails 部署,Passenger 是最受欢迎的选择之一。它与 Apache 和 Nginx 集成良好,因此在本例中,让我们设置一个 Apache + Passenger 配置。

Passenger 安装

Passenger 由 Phusion 开发,以前也称为 mod_rails 或 mod_rack,是一个模块,允许您在流行的 Web 服务器容器 Apache 或 Nginx 中发布 Ruby 应用程序。Passenger 提供“社区”免费版和企业版,企业版包括商业支持和高级功能。

如果您选择通过软件包安装 Ruby,则可以通过 RPM 或 DEB 存储库方便地获得 Passenger,并且 yum 或 apt-get 将安装所有必需的软件。

在 RVM 自定义的系统上,要安装 Passenger 的免费版本,您需要通过 Ruby gems 添加 gem


$ sudo gem install passenger

现在,您可以通过执行 gem 提供的脚本来安装服务器模块(撰写本文时最新版本为 4.0.33)


# passenger-install-apache2-module

让我们仅选择 Ruby,并跳过 Python、Node.js 和 Meteor 支持。如果您的系统缺少软件要求,该脚本将为您提供确切的 yum 或 apt-get 命令行提示,以满足这些依赖项。

经过一些编译时间后,您将看到 Passenger 配置,其中包含有用且不言自明的输出。具体而言,将加载 Passenger 到 Apache 的指令复制到您的主 Apache 配置文件 (apache2.conf 或 httpd.conf) 中


LoadModule passenger_module
/usr/local/rvm/gems/ruby-2.0.0-p353/gems/passenger-4.0.33/
↪buildout/apache2/mod_passenger.so
PassengerRoot /usr/local/rvm/gems/ruby-2.0.0-p353/gems/
↪passenger-4.0.33
PassengerDefaultRuby /usr/local/rvm/wrappers/ruby-2.0.0-p353/ruby

最后,重启 Apache。Et voilà,现在您可以托管 Ruby Web 应用程序了。

虚拟主机

如果您的目标是在同一服务器上托管一个或多个 Ruby 应用程序,则应将每个实例激活为虚拟主机。Ruby 托管最重要的指令是 DocumentRoot。它必须指向应用程序根项目目录中的 public/ 目录。public/ 目录是 Rails 应用程序的默认公共路径。假设您有一个用 Rails 制作的 Kolobok 应用程序,并且您必须将其部署到 kolobok.example.com 服务器上的 DNS 区域 kolobok.example.com。这是一个 VirtualHost 示例


<VirtualHost *:80>
      ServerName kolobok.example.com
      DocumentRoot /srv/www/kolobok/public
      <Directory /srv/www/kolobok/public>
         # This relaxes Apache security settings.
         AllowOverride all
         # MultiViews must be turned off.
         Options -MultiViews
      </Directory>
</VirtualHost>
]]>
</code></pre>


Now, if you have put your application in /srv/www/kolobok, and it's well configured (configured and binded to the database and so on), enable the virtual host, reload Apache, and your application is published.

Automating Software Deployments

Ages ago, it was common to deploy Web applications by doing a bulk copy of files via FTP, from the developer's desktop to the server hosting space, or by downloading through Subversion or Git. Although this approach still works for simpler PHP applications, it won't fit more complex projects made using more complex frameworks, such as Rails.

In fact, a Rails application is not made only of the source code files. To make a Rails application ready, you have to download and compile its dependencies as gems (by running bundle), safely manage database access and other configurations, migrate the database (create the database and the schema by executing a list of files containing SQL instructions in the Ruby language), adjust paths for shared content (like images, videos and so on), precompile the assets (that is, optimizing static content, such as JavaScript and CSS), and perform many other steps in a large and complex work flow. You can execute these steps by writing your own scripts, maybe in Ruby or bash, but this task is tedious and wastes your time. You should instead invest your time by writing good tests.

The Ruby community provides several ways to accomplish the whole deploy task, and one very popular method uses Capistrano. Capistrano lets you write a set of "recipes" that will "cook" your application in the production environment. Common tasks executed by Capistrano are: 1) pulling the source code from a git or svn repository; 2) putting it in the right location; 3) checking if a bundle is needed and, if yes, bundling your gems; 4) checking if migrations are required and, if yes, running them; 5) checking if assets precompile is required and, if yes, precompiling; and 6) checking other Rake tasks you have defined and running them in order. If the whole recipe fails, Capistrano will keep the current software release in production; otherwise, it will substitute the latest release with the one you've just deployed. Capistrano is a largely tested and very reliable tool. You definitely can trust it.

Configuring Capistrano

To use Capistrano, you just need to install it through Ruby gems on the system where the deploy will be done (not on the server):


$ gem install capistrano

When Capistrano is available, you'll have two new binaries in your PATH: capify and cap. With capify, you build your deploy skeleton. So, cd to the project directory and type:


$ capify .

This command creates a file named Capfile and a config/deploy.rb file. Capfile tells Capistrano where the right deploy.rb configuration file is. This is the file that includes your recipes, and typically it's kept in the project's config/ directory.

Next, verify that Capistrano is installed correctly, and see the many useful tasks it comes with:


$ cap -T
cap deploy                # Deploys your project.
cap deploy:check          # Tests deployment dependencies.
cap deploy:cleanup        # Cleans up old releases.
cap deploy:cold           # Deploys and starts a 'cold' application.
cap deploy:create_symlink # Updates the symlink to the most recently
                          # deployed...
cap deploy:migrations     # Deploys and runs pending migrations.
cap deploy:pending        # Displays the commits since your last 
                          # deploy.
cap deploy:pending:diff   # Displays the 'diff' since your last 
                          # deploy.
cap deploy:rollback       # Rolls back to a previous version and 
                          # restarts.
cap deploy:rollback:code  # Rolls back to the previously deployed 
                          # version.
cap deploy:setup          # Prepares one or more servers for 
                          # deployment.
cap deploy:symlink        # Deprecated API.
cap deploy:update         # Copies your project and updates the 
                          # symlink.
cap deploy:update_code    # Copies your project to the remote 
                          # servers.
cap deploy:upload         # Copies files to the currently deployed 
                          # version.
cap invoke                # Invokes a single command on the remote 
                          # servers.
cap link_shared           # Link cake, configuration, themes, upload, 
                          # tool
cap shell                 # Begins an interactive Capistrano session.

The user that will deploy the application will need valid SSH access to the server (in order to perform remote commands with Capistrano) and write permissions to the directory where the project will be deployed. The directory structure created on the server in this directory allows you to maintain software releases. In the project's document root, Capistrano keeps two directories, one that contains the released software (releases/, by default it keeps the latest ten releases), and another that contains shared or static data (shared/). Moreover, Capistrano manages a symbolic link named current that always points to the most recent successfully deployed release.

In practice, each time Capistrano is invoked to deploy an application, it connects via SSH, creates a temporary release directory named with the current timestamp (for example, releases/20140115120050), and runs the process (pull, bundle, migrate and so on). If it finishes with no errors, as final step, Capistrano links the symlink "current" to releases/20140115120050. Otherwise, it keeps "current" symlinked with the latest directory where the deploy was successful.

So with Capistrano, the system administrator will set the virtual server DocumentRoot directive to the current directory of the released application version:


DocumentRoot /srv/www/kolobok/current/public
The Anatomy of a deploy.rb File

A deploy.rb file is virtually made of two parts: one that defines the standard configurations, like the repository server or the path to deploy files physically, and another that includes custom tasks defined by the developer responsible for deploying the application.

Let's deploy the Kolobok application. Open the kolobok/config/deploy.rb file with your favourite editor, delete the example configuration and begin to code it from scratch. A deploy.rb file is programmed in Ruby, so you can use Ruby constructs in your tasks, beyond the Capistrano "keywords".

Let's start by requiring a library:


require "bundler/capistrano"

This statement orders Capistrano to do the gem bundle each time it's necessary. Good gem files separate required dependency gems in this way:


group :test do
  gem 'rspec-rails'
  gem 'capybara'
  gem 'factory_girl_rails'
end


group :production do    
  gem 'execjs'
  gem 'therubyracer'  
  gem 'coffee-rails', '~> 3.1.1'
end

Only the gems common to all environments and included in the :production group are bundled. Gems belonging to :development and :test environments are not. And the first time you deploy your application, a bundle install is executed to bundle all the requirements as specified. The next time you deploy the software, gems are downloaded, compiled or removed only if the Gemfile and the Gemfile.lock have changed. The complete bundle is installed in shared/ and soft-linked into the current instance. By following this approach, less disk space is required.

Then, from Rails 3.1, it's common to release applications with the assets pipeline. The pipeline is active if in config/environments/production.rb the following variable is set to true:


config.assets.compile = true

If your application will use the pipeline, you need to precompile it. The Rake task to precompile assets is bundle exec rake assets:precompile. To insert this task into your work flow and keep the generated assets pipeline in shared/ and linked into the current release, load the standard assets functionality:


load "deploy/assets"

After loading the main requirements, specify the application name, the path on the server where it will be deployed, and the user allowed to SSH:


set :application, "kolobok"
set :deploy_to, "/srv/www/kolobok" 
et :user, "myuser"

With Rails > 3, it's recommended to invoke Rake (it's used to do the database migrations and to precompile the assets pipeline) with the correct bundled Rake version in the bundle. So, specify the exact rake command:


set :rake, 'bundle exec rake'

Now it's time to configure the repository from which to pull the project source code:


set :scm, :git
set :branch, "master"
set :repository, "git://github.com/myusername/kolobok.git"

Finally, set the server names:


role :web, "kolobok.example.com"
role :app, "kolobok.example.com"
role :db,  "mydb.example.com", :primary => true 

web is the address of the responding Web server, and app is where the application will be deployed. These roles are the same if the application runs on only one host rather than on a cluster. db is the address of the database, and primary => true means that migrations will be run there.

Now you have a well-defined deploy.rb and the right server configurations. Begin by creating the structure tree (releases/ and static/) on the server, from the desktop host:


$ cap deploy:setup
Releasing Software

After having set up the project directory on the server, run the first deploy:


$ cap deploy:cold

The actions performed by Capistrano follow the standard pattern: git checkout, bundle, execute migrations, assets precompile. If everything is fine, your application is finally published as a reliable versioned release, with a current symlink.

Normal deploys (skipping the first Rails app configuration, such as creating the database) will be done in the future by invoking:


$ cap deploy

If you notice that some errors occurred with the current application in production, you immediately can roll back to the previous release by calling Capistrano like this:


$ cap deploy:rollback

Easy, reliable and smart, isn't it?

Custom Tasks

When you deploy a more complex application, you'll normally be handling more complex recipes than the standard Capistrano procedure. For example, if you want to publish an application on GitHub and release it open source, you won't put configurations there (like credentials to access databases or session secret tokens). Rather, it's preferable to copy them in shared/ on the server and link them on the fly before modifying the database or performing your tasks.

In Capistrano, you can define hooks to actions to force the tool to execute required actions before or after other actions. It might be useful, for instance, to link a directory where users of kolobok have uploaded files. If you move the current directory to another release path, you might discover that those files are no longer available to users. So, you can define a final task that, after having deployed code, links the shared/uploads into your current release in public/uploads directory. Notice how this can be managed with ease by exploiting the presence of the shared_path and release_path paths variables:


desc "Link uploaded directory"
task :link_uploads do
  run "ln -nfs #{shared_path}/uploads 
   ↪#{release_path}/public/uploads"
end

Finally, another common task to perform is to restart the application instance into the server container. In case of Passenger, it's enough to touch the tmp/restart.txt file. So, you can write:


desc "Restart Passenger" 
task :restart do
  run "cd #{current_path} && touch tmp/restart.txt" 
end

You execute these two tasks automatically by hooking them at the end of the deploy flow. So add this extra line just before the tasks definitions:


after "deploy:update_code", :link_uploads, :restart
Performance Issues?

People often complain of Rails' performance in production environments. This is a tricky topic. Tuning servers and application responsiveness are rather hard tasks that cannot be discussed briefly, so I don't cover them here. To make your application faster, you should involve several technologies and engineering patterns, like setting intermediate caching services, serving static and dynamic content with different server containers and monitoring the application with tools like New Relic to find bottlenecks. After having set up the right environment to host the application, this is the next challenge—optimizing. Happy deploys!

Resources

Rack: http://rack.github.com

RVM: https://rvm.ruby-lang.org.cn

Phusion Passenger: https://www.phusionpassenger.com

Capistrano: https://github.com/capistrano/capistrano

加载 Disqus 评论