初期バージョン から バージョン 1 における更新: ConsistentHashingRuby

差分発生行の前後
無視リスト:
更新日時:
2008/06/29 13:04:30 (2 年 前)
更新者:
tam
コメント:

--

凡例:

変更なし
追加
削除
変更
  • ConsistentHashingRuby

    v1 v1  
     1= Consistent Hasing を Ruby で試す = 
     2 
     3Consistent Hashing は、複数のコンピュータ(ノード)にレコードを分散するのに使われるアルゴリズムです。 
     4 
     5[http://8-p.info/consistent-hashing/ Consistent Hashing を試す]ではプログラムサンプルが Perl で書かれていますが、それを Ruby に書き直してみました。それだけではなんなので、二分探索を用いてキーの検索の高速化も試みています。 
     6 
     7プログラムの説明については、[http://8-p.info/consistent-hashing/ Consistent Hashing を試す]も併せて参照してください。 
     8 
     9== サーバー台数で割った余り (mod) を使用する == 
     10省略 
     11 
     12== Consistent Hashing == 
     13Perl版と同じアルゴリズムで、キーとノードのハッシュ値を計算し、キーのハッシュ値に近い値を持つノードを選択します。このプログラムを ch1.rb とします。 
     14{{{ 
     15require 'digest/md5' 
     16 
     17def search circle, key 
     18  for i in 1...circle.size 
     19    if circle[i-1][:key] < key and key < circle[i][:key] 
     20      return circle[i][:name] 
     21    end 
     22  end 
     23  return circle[0][:name] 
     24end 
     25 
     26nodes = {} 
     27ARGV.each {|arg| nodes[arg] = [arg]} 
     28 
     29circle = nodes.keys.map {|name| 
     30  {:key=>Digest::MD5.hexdigest(name), :name=>name}}.sort_by {|h| h[:key]} 
     31 
     32for rec in 'A'..'Z' 
     33  name = search circle, Digest::MD5.hexdigest(rec) 
     34  nodes[name].push rec 
     35end 
     36 
     37puts nodes.keys.sort.map {|key| nodes[key].join(' ')}.join("\n") 
     38}}} 
     39実行結果は、当然 Perl版と同じになります。この段階では、ノードによってレコードの数が偏っています。 
     40{{{ 
     41$ ruby ch1.rb n1 n2 n3 n4 
     42n1 H T 
     43n2 A B F K M N P S U V W Y 
     44n3 C D E J O Q X Z 
     45n4 G I L R 
     46}}} 
     47ノードをひとつ減らしてみます。 
     48{{{ 
     49$ ruby ch1.rb n1 n2 n3         
     50n1 H T 
     51n2 A B F K M N P S U V W Y 
     52n3 C D E G I J L O Q R X Z 
     53 
     54}}} 
     55 
     56== 仮想ノード == 
     57ノードごとの偏りを少なくするため、仮想ノードの概念を取り入れてみます。このプログラムを ch2.rb とします。 
     58{{{ 
     59require 'digest/md5' 
     60 
     61def search circle, key 
     62  for i in 1...circle.size 
     63    if circle[i-1][:key] < key and key < circle[i][:key] 
     64      return circle[i][:name] 
     65    end 
     66  end 
     67  return circle[0][:name] 
     68end 
     69 
     70number_of_replicants = ARGV.shift 
     71nodes = {} 
     72ARGV.each {|arg| nodes[arg] = [arg]} 
     73 
     74circle = nodes.keys.map {|name| 
     75  (0..number_of_replicants.to_i).map {|n| 
     76    suffix = n.zero? ? '' : "_#{n}" 
     77    {:key=>Digest::MD5.hexdigest(name+suffix), :name=>name} 
     78  } 
     79}.flatten.sort_by {|h| h[:key]} 
     80 
     81for rec in 'A'..'Z' 
     82  name = search circle, Digest::MD5.hexdigest(rec) 
     83  nodes[name].push rec 
     84end 
     85 
     86puts nodes.keys.sort.map {|key| nodes[key].join(' ')}.join("\n") 
     87}}} 
     88仮想ノードを100にした場合の実行結果は以下のようになります。先ほどよりはだいぶ偏りが少なくなりました。 
     89{{{ 
     90$ ruby ch2.rb 100 n1 n2 n3 n4 
     91n1 A E F O P Q U V 
     92n2 B H S T Y 
     93n3 C K L M N Z 
     94n4 D G I J R W X 
     95}}} 
     96ノードを一つ減らすと以下のようになります。 
     97{{{ 
     98$ ruby ch2.rb 100 n1 n2 n3    
     99n1 A D E F O P Q U V 
     100n2 B H I S T W Y 
     101n3 C G J K L M N R X Z 
     102}}} 
     103 
     104== 二分探索 == 
     105これまでのプログラムでは、キーの値に近いノードを検索するのに線形探索を行っていました。これではノードの数が増えるにしたがって遅くなるので二分探索を用いるように変更します。このプログラムを ch3.rb とします。 
     106{{{ 
     107require 'digest/md5' 
     108 
     109def search circle, key 
     110  left = 0 
     111  return circle[0][:name] if circle[-1][:key] < key or key <= circle[0][:key] 
     112  right = circle.size - 1 
     113  while left <= right do 
     114    mid = (left + right) / 2 
     115    return circle[mid][:name] if circle[mid-1][:key] < key and key <= circle[mid][:key] 
     116    if circle[mid][:key] < key 
     117      left = mid + 1 
     118    else 
     119      right = mid - 1 
     120    end 
     121  end 
     122  return nil 
     123end 
     124 
     125number_of_replicants = ARGV.shift 
     126nodes = {} 
     127ARGV.each {|arg| nodes[arg] = [arg]} 
     128 
     129circle = nodes.keys.map {|name| 
     130  (0..number_of_replicants.to_i).map {|n| 
     131    suffix = n.zero? ? '' : "_#{n}" 
     132    {:key=>Digest::MD5.hexdigest(name+suffix), :name=>name} 
     133  } 
     134}.flatten.sort_by {|h| h[:key]} 
     135 
     136for rec in 'A'..'Z' 
     137  name = search circle, Digest::MD5.hexdigest(rec) 
     138  nodes[name].push rec 
     139end 
     140 
     141puts nodes.keys.sort.map {|key| nodes[key].join(' ')}.join("\n") 
     142}}} 
     143実行結果は(あたりまえですが)線形探索版と同じです。 
     144{{{ 
     145$ ruby ch3.rb 1000 n1 n2 n3 n4  
     146n1 A F G K L N P Q Z 
     147n2 T U V X Y 
     148n3 C D M W 
     149n4 B E H I J O R S 
     150}}} 
     151線形探索版と二分探索版の速度を比較してみました。レコード A..Z に対する検索部分をそれぞれ1000回実行し、合計を求めました。二分探索のほうが圧倒的に速いですが、ノードの数が増えるほどその差は顕著になるのがわかります。 
     152{{{ 
     153仮想ノードが100の場合 
     154             user     system      total        real 
     155ch2.rb:  4.203125   0.234375   4.437500 (  4.797802) 
     156ch3.rb:  0.242188   0.000000   0.242188 (  0.268957) 
     157 
     158仮想ノードが1000の場合 
     159             user     system      total        real 
     160ch2.rb: 44.007812   0.109375  44.117188 (105.273196) 
     161ch3.rb:  0.375000   0.015625   0.390625 (  0.408695) 
     162}}}