2

Rails.cache.write現在、dalli を使用してmemcachierクラウドにアイテムを書き込む際のパフォーマンスを改善することを考えています。

キャッシュに関連するスタックは、現在次のとおりです。

heroku, memcachier heroku addon, dalli 2.6.4, rails 3.0.19

パフォーマンス監視に newrelic を使用しています。

現在、「アクティブな学生」のリストを必要とするリクエストを処理するコントローラーからメソッドが呼び出されたBusinessUserときに、インスタンスで表される特定のログイン ユーザーの「アクティブな学生」を取得しています。active_students

class BusinessUser < ActiveRecord::Base
  ...
  def active_students
    Rails.cache.fetch("/studio/#{self.id}/students") do
      customer_users.active_by_name
    end
  end
  ...
end

newrelic を調べた後、基本的に memcachier に重要な値を設定することで、アプリの大きなパフォーマンス ヒットを 1 つ絞り込みました。毎回平均225ミリ秒かかります。さらに、memcache キー値を設定するとメイン スレッドがブロックされ、最終的にリクエスト キューが中断されるようです。明らかに、これは望ましくありません。特に、キャッシング戦略の要点がパフォーマンスのボトルネックを減らすことである場合はなおさらです。

さらに、プレーン dalli と Rails.cache.write を使用してキャッシュ ストレージのベンチマークを行い、同じ値の 1000 個のキャッシュ セットを使用しました。

heroku run console -a {app-name-redacted}
irb(main):001:0> require 'dalli'
=> false
irb(main):002:0> cache = Dalli::Client.new(ENV["MEMCACHIER_SERVERS"].split(","),
irb(main):003:1*                     {:username => ENV["MEMCACHIER_USERNAME"],
irb(main):004:2*                      :password => ENV["MEMCACHIER_PASSWORD"],
irb(main):005:2*                      :failover => true,
irb(main):006:2*                      :socket_timeout => 1.5,
irb(main):007:2*                      :socket_failure_delay => 0.2
irb(main):008:2>                     })
=> #<Dalli::Client:0x00000006686ce8 @servers=["server-redacted:11211"], @options={:username=>"username-redacted", :password=>"password-redacted", :failover=>true, :socket_timeout=>1.5, :socket_failure_delay=>0.2}, @ring=nil>
irb(main):009:0> require 'benchmark'
=> false
irb(main):010:0> n = 1000
=> 1000
irb(main):011:0> Benchmark.bm do |x|
irb(main):012:1*   x.report { n.times do ; cache.set("foo", "bar") ; end }
irb(main):013:1>   x.report { n.times do ; Rails.cache.write("foo", "bar") ; end }
irb(main):014:1> end
       user     system      total        real
 Dalli::Server#connect server-redacted:11211
Dalli/SASL authenticating as username-redacted
Dalli/SASL: username-redacted
  0.090000   0.050000   0.140000 (  2.066113)

Dalli::Server#connect server-redacted:11211
Dalli/SASL authenticating as username-redacted
Dalli/SASL: username-redacted

  0.100000   0.070000   0.170000 (  2.108364)

通常の dallicache.setでは、2.066113 秒を使用して 1000 エントリをキャッシュに書き込み、平均cache.set時間は 2.06 ミリ秒です。

ではRails.cache.write、2.108364 秒を使用して 1000 エントリをキャッシュに書き込み、平均Rails.cache.write時間は 2.11 ミリ秒です。

⇒ memcachier の問題ではなく、単に保存しようとしているデータの量に問題があるようです。

#fetch メソッドのドキュメントによると、キャッシュセットをwritereadのスレッドまたはワーカーにスローしたい場合、それは私が行きたい方法ではないようです。そして自明のことですが、私は非同期で読みたくないのです。

Rails.cache.writeキーの値を設定するときに、ワーカーにスローすることでボトルネックを減らすことはできますか? または、より一般的には、実行するたびにメインスレッドをブロックしないように、これを行うためのより良いパターンはありRails.cache.writeますか?

4

2 に答える 2

2

通常の状況で全体的なレイテンシーに影響を与える要因は 2 つあります。それは、クライアント側のマーシャリング/圧縮とネットワーク帯域幅です。

Dalli はデータをマッシュアップし、必要に応じて圧縮しますが、これには非常にコストがかかる可能性があります。以下は、ランダムな文字のリスト (一種の人為的なユーザー ID のリストなど) のマーシャリングと圧縮のベンチマークです。どちらの場合も、結果の値は約 200KB です。両方のベンチマークは Heroku dyno で実行されました。パフォーマンスは明らかに CPU とマシンの負荷に依存します。

irb> val = (1..50000).to_a.map! {rand(255).chr}; nil
# a list of 50000 single character strings
irb> Marshal.dump(val).size
275832
# OK, so roughly 200K. How long does it take to perform this operation
# before even starting to talk to MemCachier?
irb> Benchmark.measure { Marshal.dump(val) }
=>   0.040000   0.000000   0.040000 (  0.044568)
# so about 45ms, and this scales roughly linearly with the length of the list.


irb> val = (1..100000).to_a; nil # a list of 100000 integers
irb> Zlib::Deflate.deflate(Marshal.dump(val)).size
177535
# OK, so roughly 200K. How long does it take to perform this operation
irb>  Benchmark.measure { Zlib::Deflate.deflate(Marshal.dump(val)) }
=>   0.140000   0.000000   0.140000 (  0.145672)

したがって、データのマーシャリングおよび/または圧縮だけで、基本的に 40 ミリ秒から 150 ミリ秒のパフォーマンス ヒットが見られます。文字列のマーシャリングははるかに安くなりますが、複雑なオブジェクトのようなものをマーシャリングするとコストが高くなります。圧縮は、データのサイズだけでなく、データの冗長性にも依存します。たとえば、すべて「a」文字の 1MB の文字列を圧縮するには、約 10 ミリ秒しかかかりません。

ここでは、ネットワーク帯域幅がある程度の役割を果たしますが、それほど重要ではありません。MemCachier の値には 1MB の制限があり、MemCachier との間の転送には約 20 ミリ秒かかります。

irb(main):036:0> Benchmark.measure { 1000.times { c.set("h", val, 0, :raw => true) } }
=>   0.250000  11.620000  11.870000 ( 21.284664)

これは約 400Mbps (1MB * 8MB/Mb * (1000ms/s / 20ms)) に相当します。ただし、200KB という比較的大きいが、それでも小さい値であっても、5 倍の高速化が期待できます。

irb(main):039:0> val = "a" * (1024 * 200); val.size
=> 204800
irb(main):040:0> Benchmark.measure { 1000.times { c.set("h", val, 0, :raw => true) } }
=>   0.160000   2.890000   3.050000 (  5.954258)

したがって、速度を上げるためにできることがいくつかあります。

  1. より高速なマーシャリング メカニズムを使用します。たとえば、Array#pack("L*")50,000 個の 32 ビット符号なし整数のリストを (最初のベンチマークのように) 長さ 200,000 (整数ごとに 4 バイト) の文字列にエンコードするために を使用すると、40 ミリ秒ではなく 2 ミリ秒しかかかりません。同じマーシャリング方式で圧縮を使用すると、同様のサイズの値を取得するのも非常に高速です (約 2 ミリ秒) が、圧縮はランダム データに対しては何も役に立ちません (Ruby のマーシャルは、リスト上でもかなり冗長な文字列を生成します)。ランダムな整数の)。

  2. より小さい値を使用してください。これにはおそらくアプリケーションの大幅な変更が必要になりますが、リスト全体が本当に必要ない場合は、設定する必要があります。たとえば、memcache プロトコルには操作がappendありprependます。長いリストに新しいものを追加するだけの場合は、代わりにそれらの操作を使用できます。

最後に、示唆されているように、クリティカル パスから set/get を削除すると、遅延が HTTP リクエストのレイテンシに影響するのを防ぐことができます。ワーカーにデータを送信する必要があるため、ワーク キューのようなものを使用している場合、ワーカーに送信するメッセージには、データ自体ではなく、どのデータを作成するかについての指示のみを含める必要があります (または、システムが違うだけで、再び同じ穴にいる)。非常に軽量 (コーディング作業の観点から) は、単純にプロセスを fork することです。

mylist = Student.where(...).all.map!(&:id)
...I need to update memcache with the new list of students...
fork do
  # Have to create a new Dalli client
  client = Dalli::Client.new
  client.set("mylistkey", mylist)
  # this will block for the same time as before, but is running in a separate process
end

完全な例のベンチマークは行っていませんが、あなたはexecing を行っておらず、Linux fork はコピー オン ライトであるため、fork 呼び出し自体のオーバーヘッドは最小限に抑えられるはずです。私のマシンでは、約 500us (ミリ秒ではなくマイクロ秒) です。

于 2013-12-24T19:20:21.787 に答える