对于拥有少量客户端的大流量UDP / TCP服务器,Netty速度很慢?

时间:2013-09-08 15:31:22

标签: java networking netty

我是Netty的新手,对我来说这似乎很重要,所以我决定对它的性能进行一些研究。我试图实现一个执行以下操作的服务器:

  1. 从某些来源(许多UDP端口)接收UDP数据报。
  2. 侦听TCP连接。
  3. 将收到的数据报发送到连接的TCP客户端。
  4. 客户端的数量非常少,我在测试中使用了10个,但UDP流非常繁重 - 每个大约200千字节/秒的50个流,大约10 MB /秒。我使用单线程应用程序模仿这些流,每个程序包发送200个1440字节的数据包(每个端口4个数据包),然后休眠28毫秒等等(这真的给了我大约8000 kB / s,我猜是因为高负载和不准确的睡眠时间。)

    现在,我发现它不是一个非常高的负载,但我的PC也很慢 - 一些旧的2核Intel E4600。 Windows 7 x64在板上。

    我启动了三个程序:发送者(模仿者),服务器和客户端。所有这些都在同一台机器上,我猜这不是测试它的最好方法,但至少它应该允许我比较不同的服务器实现如何与同一个模仿者和客户端一起工作。

    数据包结构如下所示:8字节时间戳,8字节数据包编号(从0开始),1字节端口标识符和1字节“子流”标识符。这个想法是50个端口中的每一个都有4个子流,所以我实际上有200个独立的数据包流,分为50个UDP流。

    结果有点令人惊讶。使用普通的旧每线程客户端服务器,我的吞吐量大约为7500 kB / s,丢包率很低。它实际上是每个客户端两个线程(另一个在read()上阻塞,以防客户端发送一些东西,但它没有)和50个线程用于UDP接收。 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%,12%等待锁定(如果这是sun.misc.Unsafe.park()所做的那样),大约4.5%用于阻止TCP写入。

    OIO服务器在阻止UDP读取方面花费大约75%,在阻止TCP读取时占11%(为什么?),在我的UDP处理程序中为6%(为什么那么多?)和4%阻止TCP写入。

    NIO服务器在选择上花费97.5%,这应该是一个好兆头。缺乏损失也是一个好兆头,CPU负载与我的琐碎服务器相同,看起来一切都很好,只有吞吐量不是差不多2倍!

    所以这是我的问题:

    1. Netty对这样的任务有效还是仅对大量连接/请求有用?
    2. 为什么OIO实现会占用大量CPU并丢失数据包?与每个客户端的普通旧2线程有什么不同?我怀疑这只是因为管道等公用事业数据结构引起的一些开销。
    3. 当我切换到NIO时,地球上发生了什么?怎么可能减速而不是丢失任何数据包?我肯定会认为我的代码有问题,但如果我只是在没有修改任何内容的情况下切换到OIO,它似乎可以获得所有8000 kB / s的流量。那么我的代码中是否只有NIO才会出现错误?

1 个答案:

答案 0 :(得分:1)

对于网络繁重的任务,网络带宽通常是个问题。对于100 Mb / s的TCP连接,您可以在充分利用率的情况下获得高达11 MB / s的速率,但是如果利用率低于50%(即大约5 MB / s或更低),您将获得更好的结果。 UDP对路由器和网络适配器中的缓冲区非常敏感。除非您拥有专业硬件,否则您可以预期网络丢失率超过30%左右。理想情况下,您将拥有一个专用的UDP网络,以避免缓冲区溢出。

简而言之,对于100 Mb / s网络,您的数字是现实的。如果您拥有1 + Gb网络和体面的网络路由器,我会期待更多。