Tech Racho エンジニアの「?」を「!」に。
  • 開発

Railsのurl_helperの速度低下を防ぐコツ(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Railsのurl_helperの速度低下を防ぐコツ(翻訳)

ある少年がナイフを一本もらいました。「これはとてもよく切れるナイフだから」という触れ込みでしたが、いざ少年が肉などを切ろうとしてみるとどうもうまく切れないので、少年は「このナイフ、言うほど大したことないな」と思ってしまいました。しかしナイフをくれた人が「ちょい待ち、ナイフの刃をもう少し傾けて。それからナイフの持ち方がよくないからこう持ってごらん」とアドバイスしたところ、今度は見事ナイフで肉を切ることができました。

オープンソースソフトウェアや人生はちょうどこのナイフのように、正しい使い方を学ばないとうまくいかないことがあります。しかも、ナイフそのものに避けがたい問題があることもあります。さらに、ネットで見つけたナイフの使い方の情報がひどいしろもので、始末の悪いことに特定の状況ではその情報が適切だったりすることもあります。人生が面倒なのは今に始まったことではありませんが、ともあれ本記事ではRails.application.routes.url_helpersというナイフについて書いてみたいと思います。

Railsコントローラのコンテキストの外でURLを生成する状況は非常に多いので、シリアライザやジョブなど、その機能が自動的には使えないような場所でこのモジュールの機能が必要になることも非常によくあります。ネットの情報では、このモジュールに直接アクセスすることを何年もの間気軽に勧めていて、しばらくの間これで何の問題も生じませんでした。

しかし運の悪いことに、最近のバージョンのRailsではこの方法で問題が生じるようになりました(問題を指摘しているGithub issue修正のPRを参照)。これによる問題を明らかにするため、簡単なテスト用Railsアプリをセットアップしてみました。関連するコードを以下に示します。

# routes.rb
Rails.application.routes.draw do
  resources :things do
    collection do
      get :faster
    end
  end
end
# app/whatever/url_helper.rb
class UrlHelper
  include Singleton
  include Rails.application.routes.url_helpers
end
# app/controllers/things_controller.rb
class ThingsController < ApplicationController
  def index
    things_json = (1..100).map do |i|
      {
        id: i,
        url: Rails.application.routes.url_helpers.thing_url(i, host: 'localhost')
      }
    end

    render json: things_json
  end

  def faster
    things_json = (1..100).map do |i|
      {
        id: i,
        url: UrlHelper.instance.thing_url(i, host: 'localhost')
      }
    end

    render json: things_json
  end
end

このindexアクションでは、StackOverflowのアドバイスどおりにコードでモジュールを直接呼び出しています。fasterアクションの方では、このモジュールをincludeするヘルパークラスを使っています。Railsではこのモジュールをincludeして使うことが推奨されているようです。

2つのアプローチの実行結果を並べて見てみましょう。abと単一のRailsインスタンスで小さなテストを実行してみました(不要な出力が大量にあったので省略してあります)。

➜ ab -n 1000 http://127.0.0.1:3000/things

Concurrency Level:      1
Time taken for tests:   18.155 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      4815000 bytes
HTML transferred:       4485000 bytes
Requests per second:    55.08 [#/sec] (mean)
Time per request:       18.155 [ms] (mean)
Time per request:       18.155 [ms] (mean, across all concurrent requests)
Transfer rate:          259.00 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       1
Processing:    15   18   2.2     17      42
Waiting:       15   18   2.2     17      42
Total:         15   18   2.2     18      42
 ➜  ab -n 1000 http://127.0.0.1:3000/things/faster

 Concurrency Level:      1
 Time taken for tests:   8.540 seconds
 Complete requests:      1000
 Failed requests:        0
 Total transferred:      4815000 bytes
 HTML transferred:       4485000 bytes
 Requests per second:    117.09 [#/sec] (mean)
 Time per request:       8.540 [ms] (mean)
 Time per request:       8.540 [ms] (mean, across all concurrent requests)
 Transfer rate:          550.58 [Kbytes/sec] received

 Connection Times (ms)
               min  mean[+/-sd] median   max
 Connect:        0    0   0.1      0       1
 Processing:     7    8   1.3      8      23
 Waiting:        7    8   1.3      8      23
 Total:          7    8   1.3      8      23

最初の例(indexメソッド)ではモジュールを直接呼び出していますが、平均して18msを要し、スループットは55リクエスト/秒です。本番で調子の良いときにはリクエストに5秒かかるのであれば「そんなに悪くないんじゃ?」とお思いかもしれません。しかしモジュールを直接呼び出すのではなく、単にincludeする方はどうでしょうか?こちら(fasterメソッドの方)は平均して8msでスループットは117リクエスト/秒と、最初のアプローチのほぼ倍速になっています。私は名シェフではありませんが、このナイフを正しく持てば適切に肉を切れるのです。

結論: Rails.application.routes.url_helpersを直接呼ばず、このモジュールをクラスにincludeすることで、コードは高速化します。

関連記事

Rails tips: belongs_to関連付けをリファクタリングしてDRYにする(翻訳)

Rails tips: モデルのクエリをカプセル化する2つの方法(翻訳)

RailsのモデルIDにUUIDを使う(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。