Winnyネットワークの規模(同時稼動ノード数)の推計については、古くは3年前のネットアーク社の松本氏によるもの、そしてネットエージェント社の杉浦氏によるもの、さらに最近ではeEye Digital Security社の鵜飼氏らによるものがある。
ネットアークは14日、P2Pノードの自動探索システム「P2P FINDER」を稼働開始したと発表した。WinMXとWinnyの国内ノードが対象となっており、6月3日から7月14日までに20万5,597ノードが発見されたとしている。
株式会社ネットアークは、同社のP2Pノードの自動探索システム「P2P FINDER」の稼動状況を発表し、8月1日時点で「Winny」と「WinMX」の利用者数が561,459ノードに達したと報告した。
(略)比率としてはWinMXとWinnyが3対1程度だとしている。
ネットアークは6月16日に「P2Pネットワーク実態調査2003」を発表しており、その時点ではWinMXが3万2,882ノード、Winnyが3万 2,496ノードで、合計6万5,378ノードだった。わずか1カ月で3倍以上に増加したように見えるが、そうではないようだ。今回稼働を開始したP2P FINDERは、前回の調査に利用したシステムをチューニングしたものにあたり、性能向上によって発見ノードが大幅に増加したという。つまり、「潜在する P2Pノード数は膨大にあり、それをすべて調べきれていない」(松本直人代表取締役)のが実状らしい。
同社がWinny検知システムを開発した当初、2004年5月時点の接続数は27万5,133ノード(ネットエージェント調べ)だった。2005年12月になってもWinnyネットワークには約30万ノードが接続しており、Winny経由の情報流出が相次いでいるにも関わらず、利用者は減少していない状況だ。
ネットエージェントは25日、P2P型ファイル共有ソフト「Winny」のノード数を発表した。4月10日から23日にかけて独自のWinny検知システムを使って調査したもの。平日で44万〜49万、土日だと50万〜53万以上のノード数を観測したという。
(略)ネットエージェントでは同社独自の検知システムを11台稼働することで、平日で延べ約350万台のノード情報を取得。IPアドレスなどで重複分を削除してユニークノード数を確定しているという。
鵜飼氏らは、Winnyのプロトコルや挙動を解析し、Winnyネットワークに参加しているユーザーの一覧を数時間で取得できるシステムも開発した。現在、常時数十万のユーザーがWinnyネットワークに参加していることが観測されており、潜在的には100万人程度のユーザーが存在するのではないかと推測している。
今や、金子勇氏の著書「Winnyの技術」(アスキー)や、日経NETWORK誌の記事「Winnyの通信解読に挑戦!」などによって、Winnyプロトコルの仕様は公知となっており、こうした調査は平均的なプログラマならば誰でも可能な状況にあるといえる。私も、これらの既存の調査の確かさを確認するため、追試を行ってみた。
使用した機材及び環境は以下である。
調査に使用したプログラムのアルゴリズムを図2に示す。指定されたノード(ホスト名とポート番号の組)に対してTCP/IPで接続し、Winnyプロトコルにしたがってコマンド10(「拡散クエリ送信要求」)を一個送信しながら、接続先からのコマンドを受信する。受信したコマンド4および13から他のノードについての情報を抽出し、新たに見つかったノードを優先度付きタスク待ち行列に投入して、スレッド数800のスレッドプールを用いて同時並行的にそれらについて同じ処理を繰り返す。優先度は、そのノード情報の作成時刻の新しいものを優先するものとした。タスク待ち行列が空になり、処理中のタスクが終了すると停止する*1。接続については、接続不能エラー発生時は1回で中止するものとし、正常接続時には30秒以上経過すると切断するものとした。
実行中にWindowsのタスクマネージャで観測したところによると、CPUの使用率は 60% 〜 80% 程度、インターネット側ネットワークの通信使用速度は最大 2 Mbps 程度、データ記録側ネットワークは平均 0.8 Mbps程度と、いずれもボトルネックとはなっていないようだった。
このときの実験結果を図1に示す。モニター出力をグラフ化したもので、横軸は経過時間(秒)、縦軸はノード数、赤色(上)のプロットはその時刻における見つかったノード総数(重複除く)であり、緑色(真ん中)は接続を試み終えた数、青色(下)は接続が正常に完了(コマンド97、0、1、2、3のハンドシェイクシーケンスを正常に完了)し終えた数である。
このように、3時間20分ほどで39万ノードを発見しそのすべてに接続を試行した。そのうち、57 % のノードが接続に成功した。1秒当たり平均 32.5ノードに対して接続試行できたことになる。
以下はモニター出力の冒頭の部分と最後の部分である。
0; 0, 0, 0, 0, NaN%, NaN%; 0; 2; NaN, NaN 10; 2492, 33, 0, 0, 0.0%, 0.0%; 1659; 802; 3.500, 0.000 20; 16565, 90, 34, 33, 37.8%, 36.7%; 15676; 802; 4.500, 1.700 30; 19826, 203, 58, 57, 28.6%, 28.1%; 18823; 802; 6.767, 1.933 40; 24152, 736, 556, 525, 75.5%, 71.3%; 22797; 802; 18.425, 13.925 50; 30748, 910, 686, 649, 75.4%, 71.3%; 29038; 802; 18.200, 13.720 60; 34058, 1086, 794, 757, 73.1%, 69.7%; 32177; 802; 18.100, 13.233 70; 38849, 1424, 1051, 1009, 73.8%, 70.9%; 36632; 802; 20.343, 15.014 80; 42730, 1757, 1322, 1273, 75.2%, 72.5%; 40173; 802; 21.962, 16.525 90; 44970, 1932, 1451, 1399, 75.1%, 72.4%; 42238; 802; 21.467, 16.122 100; 48513, 2332, 1721, 1654, 73.8%, 70.9%; 45384; 802; 23.320, 17.210 110; 51283, 2636, 1971, 1896, 74.8%, 71.9%; 47850; 802; 23.964, 17.918 120; 53646, 2865, 2097, 2019, 73.2%, 70.5%; 49984; 802; 23.875, 17.475 130; 56392, 3217, 2354, 2268, 73.2%, 70.5%; 52379; 802; 24.746, 18.108 140; 58784, 3487, 2566, 2477, 73.6%, 71.0%; 54498; 802; 24.907, 18.329 150; 61268, 3767, 2747, 2655, 72.9%, 70.5%; 56706; 802; 25.113, 18.313 160; 63774, 4094, 2996, 2899, 73.2%, 70.8%; 58894; 802; 25.587, 18.725 170; 66206, 4382, 3194, 3093, 72.9%, 70.6%; 61024; 802; 25.776, 18.788 180; 68231, 4652, 3385, 3277, 72.8%, 70.4%; 62780; 802; 25.844, 18.806 190; 70664, 4967, 3630, 3518, 73.1%, 70.8%; 64905; 802; 26.142, 19.105 200; 72863, 5300, 3859, 3743, 72.8%, 70.6%; 66765; 802; 26.500, 19.295 210; 74593, 5572, 4051, 3932, 72.7%, 70.6%; 68221; 802; 26.533, 19.290 220; 76438, 5894, 4250, 4128, 72.1%, 70.0%; 69744; 802; 26.791, 19.318 (略) 11890; 388941, 386167, 219832, 215572, 56.9%, 55.8%; 1977; 802; 32.478, 18.489 11900; 388978, 386476, 220028, 215767, 56.9%, 55.8%; 1703; 802; 32.477, 18.490 11910; 389023, 386790, 220225, 215961, 56.9%, 55.8%; 1439; 802; 32.476, 18.491 11920; 389071, 387119, 220429, 216157, 56.9%, 55.8%; 1156; 802; 32.476, 18.492 11930; 389120, 387448, 220622, 216346, 56.9%, 55.8%; 876; 802; 32.477, 18.493 11940; 389162, 387741, 220787, 216510, 56.9%, 55.8%; 630; 802; 32.474, 18.491 11950; 389201, 388087, 220994, 216714, 56.9%, 55.8%; 332; 802; 32.476, 18.493 11960; 389235, 388393, 221188, 216907, 56.9%, 55.8%; 47; 802; 32.474, 18.494 11970; 389254, 388660, 221363, 217080, 57.0%, 55.9%; 0; 802; 32.470, 18.493 11980; 389261, 388954, 221565, 217282, 57.0%, 55.9%; 0; 802; 32.467, 18.495 11990; 389265, 389161, 221748, 217459, 57.0%, 55.9%; 0; 802; 32.457, 18.494 12000; 389273, 389246, 221828, 217539, 57.0%, 55.9%; 0; 802; 32.437, 18.486 12010; 389282, 389264, 221844, 217555, 57.0%, 55.9%; 0; 802; 32.412, 18.472 12020; 389286, 389270, 221847, 217558, 57.0%, 55.9%; 0; 802; 32.385, 18.456 12030; 389287, 389275, 221851, 217562, 57.0%, 55.9%; 0; 802; 32.359, 18.441 12040; 389288, 389282, 221857, 217568, 57.0%, 55.9%; 0; 802; 32.332, 18.427 12050; 389288, 389284, 221859, 217570, 57.0%, 55.9%; 0; 802; 32.306, 18.412 12060; 389288, 389287, 221862, 217573, 57.0%, 55.9%; 0; 802; 32.279, 18.397 12070; 389288, 389288, 221863, 217574, 57.0%, 55.9%; 0; 802; 32.253, 18.381 12080; 389288, 389288, 221863, 217574, 57.0%, 55.9%; 0; 802; 32.226, 18.366 12090; 389288, 389288, 221863, 217574, 57.0%, 55.9%; 0; 802; 32.199, 18.351
このように、接続に成功するノードの割合は最初のうちは 70 % 程度あるものが、後に減少していた。この原因についてはまだ検討していない。接続に成功しないノードは、「Port0」モードで稼動しているもの(もしくは「Port0」に設定していないがNATルータのポートフォワード設定ができていないもの)と推定できる。
5番目の列は、接続に完了してそこから他のノード情報を取得できたノード数を示しており、接続したノードの 98 % から取得ができたことがわかる。今回は、全体を巡回することを優先して接続を早期に切断しているが、接続を継続することによってこの割合は100 % に近づくと思われる。
今回の巡回(日曜日の昼に実施)で見つかった 38万9288ノードという数は、ネットエージェント社が発表した「平日で44万〜49万、土日だと50万〜53万以上」という値よりいささか少ない。同社の7月3日の発表でも、
2006年7月1日(土)のWinnyノード数は471449、2006年7月2日(日)のWinnyノード数が481359であることが当社Winny調査システムにより観測されました。
となっている。今回のプログラムが全部のノードに到達することができなかった可能性と、7月中旬で利用者が減少している可能性が考えられる。
全部のノードに到達しない可能性の原因として、Winnyネットワークの「クラスタ化」動作により、いくつかのWinnyノードクラスタに到達できなかったということが考えられる。しかし、Winnyの「クラスタ化」は、ユーザにより指定された3個以下のキーワードの類似性により構成されるものであるため、「A, B」を指定しているユーザと「A, C」を指定しているユーザがいれば、Aのクラスタを経由してBのそれとCのそれはつながっていることになること、また、クラスタ分けは緩やかに構成されているものであることから、完全に分離独立したクラスタというものは存在しないと考えられるし、非常に疎に結合したクラスタの存在も考えにくい。
図1のグラフで、発見ノード数が急激に増加する現象を示すところが2箇所の時刻でみられるが、これは、新たなクラスタが見つかったというタイミングを示しているように見える。今回のアルゴリズムは、ノード情報の新しいものを優先しており、ほぼLIFO(スタック)の順序で実行している*2ため、先に同じクラスタのノードを探し切った後に別のクラスタのノードを探すという傾向の動作をしていることになる。ノードが飽和して新しいノードがほとんど見つからなくなると、古いノード情報を使い始め、古いものをいくらか使っているうちにこのような急激な新規ノード発見増加の現象が生じたようである。
今回の実験では 57 % のノードにしか接続していないのだから、接続できなかった残りのノードに接続していれば到達していたクラスタの存在というものも考えられるが、その可能性は低いように思われる。たしかに、急激にノード数が増加する現象がただ1つのノード(もしくはごくわずか)によってのみ引き起こし得るものだと仮定すれば、そのような可能性も信憑性が出てくるが、これは、最初にそのようなノードが処理された時点で、先にそこから辿られる新しいクラスタのノードを優先して処理することになるため、急激な形で見えているだけで、スタックのすぐ奥には他にも同様のノード(新しいクラスタへリンクしているノード)が多数存在していると考えるのが自然と思われる。そうすると、半分程度のノードを調べないことによって到達できないクラスタが生ずるという可能性は、きわめて小さいのではないか。
今後、優先度の評価方法を変更して実験をすることにより、違うパターンの順序でのノード巡回を試してみたい。
package jp.takagi_hiromitsu.winny.crawler;
import java.io.*;
import java.net.*;
import java.util.Set;
import java.util.HashSet;
import java.util.Map;
import java.util.HashMap;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.TimeUnit;
import jp.takagi_hiromitsu.winny.net.WinnyProtocolInterpreter;
import jp.takagi_hiromitsu.winny.net.WinnyProtocolException;
import jp.takagi_hiromitsu.winny.net.WinnyCommandVisitor;
import jp.takagi_hiromitsu.winny.net.WinnyProtocolCommander;
import jp.takagi_hiromitsu.winny.net.Command3;
import jp.takagi_hiromitsu.winny.net.Command4;
import jp.takagi_hiromitsu.winny.net.Command13;
import jp.takagi_hiromitsu.winny.net.Command10;
import jp.takagi_hiromitsu.winny.Node;
import jp.takagi_hiromitsu.winny.WinnyKey;
import jp.takagi_hiromitsu.winny.WinnyFileID;
import jp.takagi_hiromitsu.winny.WinnyConfiguration;
public class WinnyWalker {
Set<Node> foundNodes = new HashSet<Node>();
final int startTime = currentTime();
int connectionTriedCount = 0;
int handshakeSucceededCount = 0;
int extractionSucceededCount = 0;
static final int MAX_THREADS = 800;
ThreadPoolExecutor threadPool;
final Timer timer = new Timer(true);
WinnyConfiguration conf = new WinnyConfiguration();
WinnyWalker(Node initialNode) {
threadPool = new ThreadPoolExecutor(
MAX_THREADS, MAX_THREADS, 0L, TimeUnit.SECONDS,
new PriorityBlockingQueue<Runnable>()
);
timer.scheduleAtFixedRate(new MonitorTask(), 0L, 10 * 1000L);
addNode(initialNode, 0);
}
boolean addNode(Node node, int priority) {
if (!node.isValid()) return false;
boolean added = foundNodes.add(node);
if (!added) return false;
int elapsed = currentTime() - startTime;
threadPool.execute(new Task(node, priority - elapsed));
return true;
}
class Task implements Runnable, Comparable<Task> {
Node target;
int priority;
int taskSubmittedTime;
int taskStartTime;
Task(Node target, int priority) {
this.target = target;
this.priority = priority;
taskSubmittedTime = currentTime();
}
public int compareTo(Task another) {
if (this.priority < another.priority) return -1;
if (this.priority == another.priority) return 0;
return 1;
}
int handshakeCount = 0;
int extractedNodeCount = 0;
int newNodeCount = 0;
Exception lastException;
String[] clusterWords;
public void run() {
taskStartTime = currentTime();
int errorCount = 0;
for (int i = 0; i < 4; i++) {
if (errorCount >= 1) break;
if (currentTime() - taskStartTime >= 20) break;
try {
connect(30);
} catch (IOException e) {
errorCount++;
}
}
connectionTriedCount++;
if (handshakeCount > 0) {
handshakeSucceededCount++;
}
if (extractedNodeCount > 0) {
extractionSucceededCount++;
}
printTasklog();
}
private void connect(int timeLimit) throws IOException {
InputStream is = null;
OutputStream os = null;
Socket s = null;
TimerTask closer = null;
try {
s = new Socket(target.addr, target.port);
closer = new SocketCloser(s);
timer.schedule(closer, timeLimit * 1000L);
is = new BufferedInputStream(s.getInputStream());
os = new BufferedOutputStream(s.getOutputStream());
new WinnyProtocolCommander(os, conf).sendCommand(new Command10());
WinnyCommandVisitor visitor = new NodeAddingVisitor();
new WinnyProtocolInterpreter(is, visitor).interpret();
} catch (WinnyProtocolException e) {
lastException = e;
} catch (IOException e) {
lastException = e;
throw e;
} finally {
if (closer != null) closer.cancel();
try {
if (os != null) os.close();
if (is != null) is.close();
if (s != null) s.close();
} catch (IOException e2) {
}
}
}
class NodeAddingVisitor extends WinnyCommandVisitor {
public void visit(Command3 c) {
handshakeCount++;
clusterWords = c.clusterWords;
}
public void visit(Command4 c) {
extractedNodeCount++;
boolean added = addNode(new Node(c.addr, c.port), 0);
if (added) newNodeCount++;
}
public void visit(Command13 c) {
for (int i = 1; i < c.nodePath.length; i++) {
Node n = c.nodePath[i];
extractedNodeCount++;
boolean added = addNode(n, 0);
if (added) newNodeCount++;
}
Map<Node,Integer> minimum = new HashMap<Node,Integer>();
for (WinnyKey k: c.keys) {
dumpKey(target, k);
Node node;
if (k.addr.isSiteLocalAddress() || k.addr.isLinkLocalAddress()) {
node = target;
} else {
node = new Node(k.addr, k.port);
}
int pri = 1500 - k.keyExpirationTimer;
if (pri < 0) pri = 0;
Integer old = minimum.get(node);
if (old == null || old > pri) {
minimum.put(node, pri);
}
}
for (Node n: minimum.keySet()) {
extractedNodeCount++;
boolean added = addNode(n, minimum.get(n));
if (added) newNodeCount++;
}
}
}
class SocketCloser extends TimerTask {
Socket socket;
int lastExtractedNodeCount = 0;
SocketCloser(Socket socket) {
this.socket = socket;
}
public void run() {
try {
socket.getInputStream().close();
} catch (IOException e) {
}
try {
socket.close();
} catch (IOException e) {
}
}
}
private void printTasklog() {
taskOut.printf(
"%21s pri: %4d life: %3d con: %d new: %5.1f%% (%4d/%4d) %d %d %s %s\n",
target.toString(),
priority + (taskStartTime - startTime),
currentTime() - taskStartTime,
handshakeCount,
(float)newNodeCount / extractedNodeCount * 100,
newNodeCount,
extractedNodeCount,
currentTime() - startTime,
currentTime(),
clusterWords == null ?
"" :
clusterWords[0] + "|" + clusterWords[1] + "|" + clusterWords[2],
lastException == null ? "" : lastException
);
taskOut.flush();
}
}
class MonitorTask extends TimerTask {
public void run() {
int elapsed = currentTime() - startTime;
monitorOut.printf(
// "Elapsed time: %7d; Nodes found: %6d, tried: %6d, " +
// "connected: %6d, succeeded: %6d, live: %5.1f%%, " +
// "extracted: %5.1f; %Queue lingth: %6d; Threads: %4d;" +
// " /sec tried: %5.3f, connected: %5.3f\n",
"%7d; %6d, %6d, %6d, %6d, %5.1f%%, %5.1f%%; %6d; %4d; %5.3f, %5.3f\n",
elapsed,
foundNodes.size(),
connectionTriedCount,
handshakeSucceededCount,
extractionSucceededCount,
(float)handshakeSucceededCount / connectionTriedCount * 100,
(float)extractionSucceededCount / connectionTriedCount * 100,
threadPool.getQueue().size(),
Thread.activeCount(),
(float)connectionTriedCount / elapsed,
(float)handshakeSucceededCount / elapsed
);
monitorOut.flush();
}
}
private void dumpKey(Node node, WinnyKey k) {
keydumpOut.printf(
"%s %s:%d %s %d %d %d %d \"%s\" %d %s\n",
node.toString(),
k.addr.getHostAddress().toString(),
k.port,
k.fileid.toString(),
k.keyExpirationTimer,
currentTime(),
k.keyUpdateTime,
k.referenceCount,
k.trip,
k.filesize,
k.filename
);
}
private static int currentTime() {
return (int)(System.currentTimeMillis() / 1000);
}
static final String usage = "Usage: java WinnyWalker host port [outputdir]";
static PrintStream keydumpOut, taskOut, monitorOut;
public static void main(String[] args) throws Exception {
if (args.length < 2 || args.length > 3) {
System.err.println(usage);
System.exit(1);
}
String host = args[0];
int port = Integer.parseInt(args[1]);
Node initialNode = new Node(InetAddress.getByName(host), port);
File dir = new File(".");
if (args.length > 2) {
dir = new File(args[2]);
}
keydumpOut = newPrintStream(dir, "keydump.log");
taskOut = newPrintStream(dir, "task.log");
monitorOut = newPrintStream(dir, "monitor.log");
new WinnyWalker(initialNode);
}
static PrintStream newPrintStream(File dir, String file) throws Exception {
OutputStream os = new FileOutputStream(new File(dir, file));
return new PrintStream(new BufferedOutputStream(os));
}
}
上記には誤りがあり、18日の日記で訂正した。