Consistent Hasing を Ruby で試す
Consistent Hashing は、複数のコンピュータ(ノード)にレコードを分散するのに使われるアルゴリズムです。
Consistent Hashing を試すではプログラムサンプルが Perl で書かれていますが、それを Ruby に書き直してみました。それだけではなんなので、二分探索を用いてキーの検索の高速化も試みています。
プログラムの説明については、Consistent Hashing を試すも併せて参照してください。
サーバー台数で割った余り (mod) を使用する
省略
Consistent Hashing
Perl版と同じアルゴリズムで、キーとノードのハッシュ値を計算し、キーのハッシュ値に近い値を持つノードを選択します。このプログラムを ch1.rb とします。
require 'digest/md5'
def search circle, key
for i in 1...circle.size
if circle[i-1][:key] < key and key < circle[i][:key]
return circle[i][:name]
end
end
return circle[0][:name]
end
nodes = {}
ARGV.each {|arg| nodes[arg] = [arg]}
circle = nodes.keys.map {|name|
{:key=>Digest::MD5.hexdigest(name), :name=>name}}.sort_by {|h| h[:key]}
for rec in 'A'..'Z'
name = search circle, Digest::MD5.hexdigest(rec)
nodes[name].push rec
end
puts nodes.keys.sort.map {|key| nodes[key].join(' ')}.join("\n")
実行結果は、当然 Perl版と同じになります。この段階では、ノードによってレコードの数が偏っています。
$ ruby ch1.rb n1 n2 n3 n4 n1 H T n2 A B F K M N P S U V W Y n3 C D E J O Q X Z n4 G I L R
ノードをひとつ減らしてみます。
$ ruby ch1.rb n1 n2 n3 n1 H T n2 A B F K M N P S U V W Y n3 C D E G I J L O Q R X Z
仮想ノード
ノードごとの偏りを少なくするため、仮想ノードの概念を取り入れてみます。このプログラムを ch2.rb とします。
require 'digest/md5'
def search circle, key
for i in 1...circle.size
if circle[i-1][:key] < key and key < circle[i][:key]
return circle[i][:name]
end
end
return circle[0][:name]
end
number_of_replicants = ARGV.shift
nodes = {}
ARGV.each {|arg| nodes[arg] = [arg]}
circle = nodes.keys.map {|name|
(0..number_of_replicants.to_i).map {|n|
suffix = n.zero? ? '' : "_#{n}"
{:key=>Digest::MD5.hexdigest(name+suffix), :name=>name}
}
}.flatten.sort_by {|h| h[:key]}
for rec in 'A'..'Z'
name = search circle, Digest::MD5.hexdigest(rec)
nodes[name].push rec
end
puts nodes.keys.sort.map {|key| nodes[key].join(' ')}.join("\n")
仮想ノードを100にした場合の実行結果は以下のようになります。先ほどよりはだいぶ偏りが少なくなりました。
$ ruby ch2.rb 100 n1 n2 n3 n4 n1 A E F O P Q U V n2 B H S T Y n3 C K L M N Z n4 D G I J R W X
ノードを一つ減らすと以下のようになります。
$ ruby ch2.rb 100 n1 n2 n3 n1 A D E F O P Q U V n2 B H I S T W Y n3 C G J K L M N R X Z
二分探索
これまでのプログラムでは、キーの値に近いノードを検索するのに線形探索を行っていました。これではノードの数が増えるにしたがって遅くなるので二分探索を用いるように変更します。このプログラムを ch3.rb とします。
require 'digest/md5'
def search circle, key
left = 0
return circle[0][:name] if circle[-1][:key] < key or key <= circle[0][:key]
right = circle.size - 1
while left <= right do
mid = (left + right) / 2
return circle[mid][:name] if circle[mid-1][:key] < key and key <= circle[mid][:key]
if circle[mid][:key] < key
left = mid + 1
else
right = mid - 1
end
end
return nil
end
number_of_replicants = ARGV.shift
nodes = {}
ARGV.each {|arg| nodes[arg] = [arg]}
circle = nodes.keys.map {|name|
(0..number_of_replicants.to_i).map {|n|
suffix = n.zero? ? '' : "_#{n}"
{:key=>Digest::MD5.hexdigest(name+suffix), :name=>name}
}
}.flatten.sort_by {|h| h[:key]}
for rec in 'A'..'Z'
name = search circle, Digest::MD5.hexdigest(rec)
nodes[name].push rec
end
puts nodes.keys.sort.map {|key| nodes[key].join(' ')}.join("\n")
実行結果は(あたりまえですが)線形探索版と同じです。
$ ruby ch3.rb 1000 n1 n2 n3 n4 n1 A F G K L N P Q Z n2 T U V X Y n3 C D M W n4 B E H I J O R S
線形探索版と二分探索版の速度を比較してみました。レコード A..Z に対する検索部分をそれぞれ1000回実行し、合計を求めました。二分探索のほうが圧倒的に速いですが、ノードの数が増えるほどその差は顕著になるのがわかります。
仮想ノードが100の場合
user system total real
ch2.rb: 4.203125 0.234375 4.437500 ( 4.797802)
ch3.rb: 0.242188 0.000000 0.242188 ( 0.268957)
仮想ノードが1000の場合
user system total real
ch2.rb: 44.007812 0.109375 44.117188 (105.273196)
ch3.rb: 0.375000 0.015625 0.390625 ( 0.408695)
