私は Netty を初めて使用し、かなり重いように思えるので、そのパフォーマンスについて調査することにしました。次のことを行うサーバーを実装しようとしていました。
- 一部のソース (多数の UDP ポート) から UDP データグラムを受信します。
- TCP 接続をリッスンします。
- 受信したデータグラムを接続された TCP クライアントに送信します。
クライアントの数はかなり少なく、テストでは 10 を使用しましたが、UDP ストリームは非常に重く、50 ストリームはそれぞれ約 200 kbyte/秒で、約 10 MB/秒になります。私はこれらのストリームを、それぞれ 1440 バイトの 200 パケット (各ポートに 4 パケット) を送信する単一のスレッド化されたアプリケーションで模倣し、その後 28 ミリ秒の間スリープします (実際には約 8000 kB/s が得られました。負荷が高いためだと思います)。不正確な睡眠時間)。
今、私はそれがひどく高負荷ではないことを理解していますが、私のPCもかなり遅いです.古い2コアIntel E4600. Windows 7 x64 オンボード。
送信者 (模倣者)、サーバー、クライアントの 3 つのプログラムを開始します。すべて同じマシン上でテストするのは最善の方法ではないと思いますが、少なくとも、サーバーのさまざまな実装が同じ模倣者とクライアントでどのように機能するかを比較できるはずです。
パケット構造は次のようになります: 8 バイトのタイムスタンプ、8 バイトのパケット番号 (0 から始まる)、1 バイトのポート識別子、および 1 バイトの「サブストリーム」識別子。50 個のポートのそれぞれに 4 つのサブストリームがあるため、実際には 50 個の UDP ストリームにグループ化されたパケットの 200 個の独立したストリームがあります。
結果はやや驚くべきものでした。クライアントごとに単純なスレッド サーバーを使用すると、パケット損失がほとんどなく、約 7500 kB/秒のスループットが得られました。実際には、クライアントごとに 2 つのスレッド (クライアントが何かを送信した場合に備えて read() で別のスレッドがブロックされますが、送信されません) と UDP 受信用に 50 のスレッドでした。CPU 負荷は約 60% でした。
OIO Netty サーバーを使用すると、クライアント側で約 6000 kB/s になり、多くのパケット損失が発生します。そして、最低水位標を 50 MB に設定し、最高水位標を 100 MB に設定しました! CPU 負荷は 80% ですが、これも良い兆候ではありません。
NIO Netty サーバーを使用すると、約 4500 kB/s を取得できますが、何らかの理由で損失はありません。送信プロセスが遅くなったのではないでしょうか? しかし、それは意味がありません: CPU 負荷は約 60% であり、NIO は送信者のスケジューリングを妨げる可能性のある多くのスレッドを使用することは想定されていません...
これが私のNettyサーバーの実装です:
public class NettyServer {
public static void main(String[] args) throws Exception {
new NettyServer(Integer.parseInt(args[0])).run();
}
private final int serverPort;
private NettyServer(int serverPort) {
this.serverPort = serverPort;
}
private void run() throws InterruptedException {
boolean nio = false;
EventLoopGroup bossGroup;
EventLoopGroup workerGroup;
EventLoopGroup receiverGroup;
if (nio) {
bossGroup = new NioEventLoopGroup();
workerGroup = new NioEventLoopGroup();
receiverGroup = new NioEventLoopGroup();
} else {
bossGroup = new OioEventLoopGroup();
workerGroup = new OioEventLoopGroup();
receiverGroup = new OioEventLoopGroup();
}
final List<ClientHandler> clients
= Collections.synchronizedList(new LinkedList<ClientHandler>());
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup, workerGroup).channel(
nio ? NioServerSocketChannel.class : OioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.config().setWriteBufferHighWaterMark(1024 * 1024 * 100);
ch.config().setWriteBufferLowWaterMark(1024 * 1024 * 50);
final ClientHandler client = new ClientHandler(clients);
ch.pipeline().addLast(client);
}
});
server.bind(serverPort).sync();
Bootstrap receiver = new Bootstrap();
receiver.group(receiverGroup);
receiver.channel(nio ? NioDatagramChannel.class : OioDatagramChannel.class);
for (int port = 18000; port < 18000 + 50; ++port) {
receiver.handler(new UDPHandler(clients));
receiver.bind(port).sync();
}
}
}
class UDPHandler extends SimpleChannelInboundHandler<DatagramPacket> {
private final Collection<ClientHandler> clients;
private static final long start = System.currentTimeMillis();
private static long sum = 0;
private static long count = 0;
private final Long[][] lastNum = new Long[50][4];
public UDPHandler(Collection<ClientHandler> clients){
this.clients = clients;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception {
final ByteBuf content = msg.content();
final int length = content.readableBytes();
synchronized (UDPHandler.class) {
sum += length;
if (++count % 10000 == 0) {
final long now = System.currentTimeMillis();
System.err.println((sum / (now - start)) + " kB/s");
}
}
long num = content.getLong(8);
// this basically identifies the sender port
// (0-50 represents ports 18000-18050)
int nip = content.getByte(16) & 0xFF;
// and this is "substream" within one port (0-3)
int stream = content.getByte(17) & 0xFF;
// the last received number for this nip/stream combo
Long last = lastNum[nip][stream];
if (last != null && num - last != 1) {
// number isn't incremented by 1, so there's packet loss
System.err.println("lost " + (num - last - 1));
}
lastNum[nip][stream] = num;
synchronized (clients) {
for (ClientHandler client : clients) {
final ByteBuf copy = content.copy();
client.send(copy);
}
}
}
}
public class ClientHandler extends ChannelInboundHandlerAdapter {
private final static Logger logger
= Logger.getLogger(ClientHandler.class.getName());
private ByteBuf buffer;
private final Collection<ClientHandler> clients;
private Channel channel;
ClientHandler(Collection<ClientHandler> clients) {
this.clients = clients;
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
channel = ctx.channel();
clients.add(this);
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
clients.remove(this);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (!(cause instanceof IOException)) {
logger.log(Level.SEVERE, "A terrible thing", cause);
}
}
void send(ByteBuf msg) {
if (channel.isWritable()) {
channel.writeAndFlush(msg);
} else {
msg.release();
}
}
}
プロファイリングによると、私の些細なサーバー実装では、UDP 読み取りのブロックに約 83%、ロックの待機 (sun.misc.Unsafe.park() がそうする場合) に 12%、TCP 書き込みのブロックに約 4.5% を費やしています。
OIO サーバーは、UDP 読み取りのブロックに約 75%、TCP 読み取りのブロックに 11% (なぜ?)、UDP ハンドラーに 6% (なぜそんなに?)、TCP 書き込みのブロックに 4% を費やしています。
NIO サーバーは選択に 97.5% を費やしています。これは良い兆候です。損失がないことも良い兆候であり、CPU 負荷が私の単純なサーバーと同じであるため、スループットがほぼ 2 倍遅くない場合にのみ、すべてが問題ないように見えます!
だからここに私の質問があります:
- Netty はこのようなタスクに効果的ですか、それとも多数の接続/リクエストに対してのみ有効ですか?
- OIO 実装が CPU を大量に消費し、パケットを失うのはなぜですか? クライアントごとに従来の 2 スレッドとの違いは何ですか? パイプラインなどのユーティリティ データ構造によって引き起こされるオーバーヘッドが原因であるとは思えません。
- NIO に切り替えると一体何が起こるのでしょうか? パケットを失わずに速度を落とすにはどうすればよいでしょうか? 私のコードに何か問題があると思いますが、何も変更せずに OIO に切り替えるだけで、8000 kB/s のトラフィックがすべて取得されるようです。私のコードには、NIO でのみ発生するバグがありますか?