为什么我看到 Java 8 和 Java 21 之间的网络流量性能下降?

问题描述 投票:0回答:1

我们正在创建一个以网络流量为中心的重负载应用程序,并在 Java 8 下运行这些服务器很多很多年。以网络流量为中心意味着服务器通常必须处理高达 700 MBit/s 的速度.

现在我们想切换到 Java 21。

我可以确认 Java 13 的性能表现与 Java 8 类似,而 Java 21 的表现与 Java 14 类似。因此,从 Java 13 到 Java 14 显然发生了变化。我使用 Azul Zulu 进行了测试,但也尝试了另一种实现来确保它的性能不是祖鲁语的问题。

在评估时我们发现,Java 21 的性能比 Java 8 更差,这让我们非常惊讶。

我创建了一个示例,您可以在其中看到效果:

主课

package senderreceiverbenchmark;

import java.io.*;
import java.net.*;
import java.util.concurrent.*;

public class SenderReceiverBenchmark
{
    public static void main(String[] args) throws IOException
    {
        ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
        Statistics statistics = null;
        
        switch (args.length)
        {
            case 1: //receiver mode
            {
                System.out.println( "Receiver waiting at port " + Integer.valueOf(args[0]));
        
                statistics = new Statistics("Received");
                executorService.scheduleAtFixedRate(statistics, 10, 10, TimeUnit.SECONDS);
        
                ServerSocket serverSocket = new ServerSocket(Integer.parseInt(args[0]));
                
                ExecutorService executorServiceReceiver = Executors.newCachedThreadPool();
                
                Socket socket;
                while((socket = serverSocket.accept()) != null)
                {
                    executorServiceReceiver.submit(new Receiver(socket.getInputStream(), statistics));
                }
                
                break;
            }
            case 4: //sender mode
            {
                System.out.println( "Sending to " + args[0] + ":" + Integer.valueOf(args[1]) + " with [" + Integer.valueOf(args[2]) + "] connections and framesize [" + Integer.valueOf(args[3]) + " KB]");
        
                statistics = new Statistics("Send");
                executorService.scheduleAtFixedRate(statistics, 10, 10, TimeUnit.SECONDS);
        
                ExecutorService executorServiceSender = Executors.newFixedThreadPool(Integer.parseInt(args[2]));
                long SLEEP_TIME_BETWEEN_SENDING = 50;
                for (int i = 0; i < Integer.parseInt(args[2]); i++) //creating independant sender ...
                {
                    executorServiceSender.submit(new Sender(args[0], Integer.parseInt(args[1]), Integer.parseInt(args[3]), SLEEP_TIME_BETWEEN_SENDING, statistics));
                }
                
                break;
            }
            default:
                System.out.println( "For Receiver use: LoopbackBenchmark <ServerSocket>" );
                System.out.println( "For Sender use: LoopbackBenchmark <host> <port> <NumberOfConnections> <Framesize KB>" );
                System.exit(-1);
                break;
        }
    }
}

发件人:

package senderreceiverbenchmark;

import java.io.*;
import java.net.Socket;
import java.net.SocketException;
import java.util.concurrent.Callable;

public class Sender implements Callable<Object>
{
    private final OutputStream outputStream;
    private final Statistics statistics;
    private final byte[] preallocatedRandomData = new byte[65535];
    private final long sleepTime;

    public Sender(String host, int port, int framesizeKB, long sleepTimeBetweenSend, Statistics statistics) throws SocketException, IOException
    {
        this.statistics = statistics;
        
        Socket socket = new Socket( host, port );

        outputStream = socket.getOutputStream();
        this.sleepTime = sleepTimeBetweenSend;
    }

    @Override
    public Object call() throws Exception
    {
        statistics.handledConections.addAndGet(1);
        
        while (true)
        {
            this.outputStream.write(preallocatedRandomData);
            statistics.overallData.addAndGet(preallocatedRandomData.length);
            Thread.sleep(sleepTime);
        }
    }
}

接收者:

package senderreceiverbenchmark;

import java.io.*;
import java.util.concurrent.Callable;

public class Receiver implements Callable<Object>
{
    private final InputStream inputStream;
    private final Statistics statistics;
    private final byte[] buffer = new byte[65535];
    
    public Receiver(InputStream inputStream, Statistics statistics)
    {
        this.inputStream = inputStream;
        this.statistics = statistics;
    }
    
    @Override
    public Object call() throws Exception
    {
        statistics.handledConections.addAndGet(1);
        
        while (true)
        {
            int readBytes = this.inputStream.read(buffer);
            if( readBytes > 0 )
            {
                statistics.overallData.addAndGet(readBytes);
            }
        }
    }
}

一些统计数据:

package senderreceiverbenchmark;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

public class Statistics implements Runnable
{
    public final AtomicLong overallData = new AtomicLong(0L);
    public final AtomicLong handledConections = new AtomicLong(0L);
    private final String mode;
    private long previousRun = System.currentTimeMillis();
    
    public Statistics(String tag)
    {
        this.mode = tag;
    }

    @Override
    public void run()
    {
        long dataSentPerSecond = overallData.get() / TimeUnit.MILLISECONDS.toSeconds((System.currentTimeMillis() - previousRun));

        System.out.println(mode + ", Connections: " + handledConections.get() + ", Sent overall: " + dataSentPerSecond / (1024*1024) + " MB/s" );

        overallData.set(0);
        previousRun = System.currentTimeMillis();
    }
}

请原谅我,该示例没有(良好的)错误处理功能,但用于演示目的应该没问题。

  1. 现在首先启动接收器:

    Benchmark.bat 4711
    
  2. 然后启动发送器:

    Benchmark.bat 127.0.0.1 4711 300 128
    

现在启动 300 个发送方线程,每 50 毫秒发送一个 128KB 数据包到接收方。

当您第一次使用 Java 8 作为运行时,然后使用 Java 21 作为运行时,您将看到类似这样的内容:

CPU 负载 Java 8 与 Java 21

前半部分显示在 Java 8 上运行的示例应用程序,后半部分显示在 Java 21 上运行的示例应用程序。
与 Java 8 相比,较新的 Java 21 需要多 10%-15% 的 CPU 能力。

有人可以解释一下这是从哪里来的以及我能做些什么吗?

java performance networking
1个回答
0
投票

我已尝试使用 InteliJ Profiler 多次运行您的应用程序,但无法一致地重现任何 CPU 性能问题以形成可靠的案例。

但是在您的示例中您使用

  • java.net.Socket
  • java.net.ServerSocket

以下变化可以解释使用 JDK13 及更高版本与 JDK8 相比在特定场景下的性能差异。

Socket
ServerSocket
已根据JEP 353重新实现,为项目织机的虚拟线程奠定基础。如果您检查关闭 JEP 353,您将发现以下内容:

除了行为差异之外,新产品的性能 运行某些工作负载时,实施可能与旧的有所不同。 在旧的实现中,多个线程调用accept方法 ServerSocket 将在内核中排队。在新的实施中, 一个线程将阻塞在接受系统调用中,其他线程将排队 等待获取 java.util.concurrent 锁。 性能 其他场景中的特征也可能有所不同。

© www.soinside.com 2019 - 2024. All rights reserved.