1 秒あたり 50 万から 100 万のリモート関数呼び出しを実現したいと考えています。Central
計算を開始するコンピュータが1台と、計算Worker
を実行するコンピュータが 1 台あるとします。実際の構成では、多くのワーカー コンピューターが存在します。
ちょっとの間、私たちの仕事は を計算することだとしましょうsum of [(random int from 0 to MAX_VAL)*2], PROBLEM_SIZE times
非常に素朴なプロトタイプは
Worker:
//The real function takes 0.070ms to compute.
int compute(int input) {
return input * 2;
}
void go() {
try {
ServerSocket ss = new ServerSocket(socketNum);
Socket s = ss.accept();
System.out.println("Listening for " + socketNum);
DataInput di = new DataInputStream(s.getInputStream());
OutputStream os = s.getOutputStream();
byte[] arr = new byte[4];
ByteBuffer wrap = ByteBuffer.wrap(arr);
for (; ; ) {
wrap.clear();
di.readFully(arr);
int value = wrap.getInt();
int output = compute(value);
wrap.clear();
byte[] bytes = wrap.putInt(output).array();
os.write(bytes);
}
} catch (IOException e) {
System.err.println("Exception at " + socketNum);
e.printStackTrace();
}
}
Central:
void go(){
try {
Socket s = new Socket(ip, socketNum);
s.setSoTimeout(2000);
OutputStream os = s.getOutputStream();
DataInput di = new DataInputStream(s.getInputStream());
System.out.println("Central socket starting for " + socketNum);
Random r = new Random();
byte[] buf = new byte[4];
ByteBuffer wrap = ByteBuffer.wrap(buf);
long start = System.currentTimeMillis();
long sum = 0;
for(int i = 0; i < n; i++) {
wrap.clear();
int value = r.nextInt(10000);
os.write(wrap.putInt(value).array());
di.readFully(buf);
wrap.clear();
int answer = wrap.getInt();
sum += answer;
}
System.out.println(n + " calls in " + (System.currentTimeMillis() - start) + " ms");
} catch(SocketTimeoutException ste) {
System.err.println("Socket timeout at " + socketNum);
}
catch (Exception e) {
e.printStackTrace();
}
ping が 0.150 ミリ秒で、1 スレッドのワーカーと 1 スレッドのセントラルを実行する場合、各反復には約 0.150 ミリ秒かかります。パフォーマンスを向上させるためN
に、Worker と Central の両方でスレッドを実行します。 n
-th スレッドは port をリッスンします2000+n
。各スレッドが停止した後、結果を合計します。
ベンチマーク
最初に、私の仲間の学校のネットワークで上記のプログラムを実行しました。次に、2 つの Amazon EC2 クラスター インスタンスで実行しました。結果のギャップは非常に大きかった。
CHUNK_SIZE = 100_000
すべての実行で。
フェローのネットワーク:
3 年前は最上位の構成 (Xeon E5645) だったと思います。マシンが 20 台しかないため、並列計算用に大幅に最適化されており、LAN トポロジーが単純になっていると思います。
OS: Ubuntu
平均ping: ~0.165ms
N=1 total time=6 seconds
N=10 total time=9 seconds
N=20 total time=11 seconds
N=32 total time=14 seconds
N=100 total time=21 seconds
N=500 total time=54 seconds
アマゾンネットワーク:
同じ配置グループで開始された 2 つの Cluster Compute Eight Extra Large インスタンス (cc2.8xlarge) でプログラムを実行しました。
OSはアマゾンのLinuxです
平均 ping: ~0.170ms。
結果は少し残念でした:
N=1 total time=16 seconds
N=10 total time=36 seconds
N=20 total time=55 seconds
N=32 total time=82 seconds
N=100 total time=250 seconds
N=500 total time=1200 seconds
各構成を 2 ~ 4 回実行しましたが、結果はほぼ同じで、ほとんどが +-5% でした
関数呼び出しあたり 0.170 ミリ秒 = 1 秒あたり 6000 呼び出し = 16 秒あたり 100_000 呼び出しであるため、Amazon N=1 の結果は理にかなっています。Fellow のネットワークの 6 秒は、実際には驚くべきことです。
最新のネットワークでの 1 秒あたりの最大 TCP パケット数は、1 秒あたり約 40 ~ 70k だと思います。N=100、time=250 秒に相当します: N*CHUNK_SIZE / time = 100 * 100_000packets / 250sec = 10_000_000packets / 250sec = 40_000packets/second.
問題は、特に N 値が高い場合に、フェローのネットワーク/コンピューター構成がどのようにうまく機能したかということです。
私の推測では、最大 40 バイトのオーバーヘッドがあるため、4 バイトのリクエストと 4 バイトの応答をそれぞれ個別のパケットに配置するのは無駄です。これらすべての小さなリクエストを、たとえば 0.010 ミリ秒分プールして 1 つの大きなパケットに入れ、対応するソケットにリクエストを再分配するのが賢明です。アプリケーション レベルでプーリングを実装することは可能ですが、Fellow のネットワーク/OS はそれを行うように構成されているようです。
更新: java.net.Socket.setTcpNoDelay() で遊んだことがありますが、何も変わりませんでした。
究極の目標: 非常に大きなツリーを使用して、数百万の変数で方程式を近似します。現在、200_000 ノードのツリーが RAM に収まります。しかし、何百万ものノードを持つツリーを必要とする近似方程式に興味があります。数テラバイトの RAM が必要です。アルゴリズムの基本的な考え方は、ノードからリーフへのランダムなパスを取り、それに沿って値を改善することです。現在、プログラムは 32 スレッドで、各スレッドは 1 秒あたり 15000 回の反復を実行します。1秒あたりの反復回数が同じクラスターに移動したいと思います。