2006年1月26日
Ruby, Pythonで並列に逆引きを行う
ウェブサーバのログを解析するときなど、IPアドレスからホスト名を引く処理 (逆引き) を大量に行いたいときがあります。DNS への逆引きの問い合わせには場合によっては数秒待たされることがあるため、大量の IP アドレスをひとつづつ順に処理していくとかなり時間がかかります。
本記事では Ruby または Python でマルチスレッドで並列に逆引きを行う方法を紹介します。
Ruby の場合
Ruby で逆引きを行うには socket ライブラリを使う方法と、Resolv ライブラリを使う方法があります。マルチスレッドで並列に逆引きを行うには Resolv ライブラリを使う必要があります。
socket ライブラリを使った場合、 Socket.gethostbyaddr か Socket.getaddrinfo を使って逆引きを行います。これらは同名の C の関数を内部的に呼びます。逆引きには数秒間かかることがありますが、Ruby のスレッドはユーザレベルスレッドであるため、C の関数を呼んでいる間は他のスレッドはまったく動きません。このため、マルチスレッドで並列に逆引きしようとしても実際には逐次的に行われてしまいます。
一方、 resolv ライブラリでは gethostbyaddr などに相当する部分が Ruby で書かれているため、Cの関数呼び出しで長時間ブロックされることなしに、並列に逆引きを行えます。
次のプログラムは Socket.gethostbyaddr を使った場合と Resolv.getname を切り替えられるようになっています。
require 'thread'
require 'thwait'
require 'socket'
require 'resolv'
def lookup_with_gethostbyaddr(ip_address)
addr = ip_address.split('.').map {|x| x.to_i}.pack('C4')
return Socket.gethostbyaddr(addr).first
rescue SocketError
return ip_address
end
def lookup_with_resolv(ip_address)
return Resolv.getname(ip_address)
rescue Resolv::ResolvError
return ip_address
end
def usage
puts "mt-resolver.rb METHOD NUM-THREADS FILE"
exit
end
def main
usage unless ARGV.length == 3
lookup_method = ARGV[0]
num_threads = ARGV[1].to_i
file_name = ARGV[2]
queue = Queue.new
File.readlines(file_name).map {|line|
ip_address = line.chomp
queue.push(ip_address)
}
mutex = Mutex.new
threads = []
num_threads.times {
queue.push(nil)
thread = Thread.new {
while ip_address = queue.pop
host_name = send(lookup_method, ip_address)
mutex.synchronize {
puts host_name
}
end
}
thread.abort_on_exception = true
threads.push(thread)
}
ThreadsWait::all_waits(*threads)
end
main
DNSのキャッシュが影響するため、速度を比較するのは難しいのですが、無作為にサンプリングした 100 の IP アドレスのリストを 2つ作り (重複するアドレスなし)、それぞれ lookup_with_gethostbyaddr を使った場合と lookup_with_resolv を使った場合で時間を計測してみました。
% time ruby mt-resolver.rb lookup_with_gethostbyaddr 100 list1 > /dev/null 670.04s total : 0.05s user 0.01s system 0% cpu % time ruby mt-resolver.rb lookup_with_resolv 100 list2 > /dev/null 130.20s total : 0.60s user 0.06s system 0% cpu
Socket.gethostbyaddr では 670秒かかりましたが、 Resolv.getname では 130 秒で終わりました。list1, list2 では重複しない IP アドレスを使っているため、DNSのキャッシュの影響はあまりないと思います。
Python の場合
Python のスレッドは OS のネイティブスレッド (pthreads) であるため、socket.gethostbyaddr を呼んでもブロックすることはありません。
import threading
import Queue
import socket
import sys
import fileinput
def lookup(ip_address):
try:
return socket.gethostbyaddr(ip_address)[0]
except socket.herror:
return ip_address
def resolver(queue, lock):
while True:
ip_address = queue.get()
if ip_address == None:
break
host_name = lookup(ip_address)
lock.acquire()
try:
print host_name
finally:
lock.release()
def main():
queue = Queue.Queue()
num_threads = int(sys.argv[1])
for line in fileinput.input(sys.argv[2]):
ip_address = line.strip()
queue.put(ip_address)
lock = threading.Lock()
for i in xrange(num_threads):
queue.put(None)
thread = threading.Thread(target = resolver, args = (queue, lock))
thread.start()
main()
ふたたび無作為にサンプリングした 100 の IP アドレスのリストを 2つ作り (重複するアドレスなし)、それぞれスレッドの数が 1の場合と 100 の場合で時間を計測してみました。
% time python mt-resolver.py 1 list3 584.74s total : 0.02s user 0.03s system 0% cpu % time python mt-resolver.py 100 list4 20.07s total : 0.10s user 0.09s system 0% cpu
スレッドの数が 1の場合は Ruby で gethostbyaddr を使ったときとそれほど変わらず 584 秒かかりましたが、スレッドを 100 にすると、20秒で終わりました。こちらも list3, list4 で重複しない IP アドレスを使っています。
まとめ
Ruby または Python を使ってマルチスレッドで並列に逆引きを行う方法を紹介しました。元々は Ruby でも resolv ライブラリを使えば Python と同程度の速さで並列に逆引きできると予想していたのですが、やってみると、Python 版の方が速いという結果になりました。今回のようなケースではネイティブスレッドが有効に働くようです。