【问题标题】:SocketChannel: Why if I write msgs quickly the latency of each message is low, but when I write one msg every 30 seconds the latency is high?SocketChannel:为什么我写msg很快,每条消息的延迟很低,但是每30秒写一个msg,延迟很高?
【发布时间】:2017-09-08 17:02:48
【问题描述】:

这个问题的发展现在在这个新问题中明确描述:Why does the JVM show more latency for the same block of code after a busy spin pause?

我在下面包含了一个简单的服务器和客户端的源代码,用于演示和隔离问题。基本上我正在计时乒乓(客户端-服务器-客户端)消息的延迟。我首先每 1 毫秒发送一条消息。我等待发送 200k 条消息,以便 HotSpot 有机会优化代码。然后我将暂停时间从 1 毫秒更改为 30 秒。令我惊讶的是,我的读写操作变得相当慢。

我认为这不是 JIT/HotSpot 问题。我能够确定本地 JNI 调用写入 (write0) 和读取的较慢方法。看起来你暂停的时间越长它变得越慢。

我正在寻找有关如何调试、理解、解释或解决此问题的指示。

Server.java:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class Server {

    private final ServerSocketChannel serverSocketChannel;
    private final ByteBuffer readBuffer = ByteBuffer.allocateDirect(1024);
    private final int port;
    private final int msgSize;

    public Server(int port, int msgSize) throws IOException {
        this.serverSocketChannel = ServerSocketChannel.open();
        this.port = port;
        this.msgSize = msgSize;
    }

    public void start() throws IOException {
        serverSocketChannel.socket().bind(new InetSocketAddress(port));
        final SocketChannel socketChannel = serverSocketChannel.accept(); // blocking mode...
        System.out.println("Client accepted!");
        socketChannel.configureBlocking(false);
        socketChannel.socket().setTcpNoDelay(true);
        Thread t = new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    while(true) {
                        int bytesRead = socketChannel.read(readBuffer);
                        if (bytesRead == -1) {
                            System.out.println("Client disconnected!");
                            return;
                        } else if (bytesRead > 0) {
                            if (readBuffer.position() == msgSize) {
                                // have a full message there...
                                readBuffer.flip();
                                int bytesSent = socketChannel.write(readBuffer);
                                if (bytesSent != msgSize) throw new RuntimeException("Could not send full message out: " + bytesSent);
                                readBuffer.clear();
                            }
                        }
                    }
                } catch(Exception e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        serverSocketChannel.close();
    }

    public static void main(String[] args) throws Exception {

        Server s = new Server(9999, 8);
        s.start();
    }
}

Client.java:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class Client implements Runnable {

    private static final int WARMUP = 200000;

    private final SocketChannel socketChannel;
    private final String host;
    private final int port;
    private final ByteBuffer outBuffer;
    private final ByteBuffer inBuffer = ByteBuffer.allocateDirect(1024);
    private final int msgSize;
    private final StringBuilder sb = new StringBuilder(1024);

    private int interval;
    private int totalMessagesSent;
    private long timeSent;
    private int mod;


    public Client(String host, int port, int msgSize) throws IOException {
        this.socketChannel = SocketChannel.open();
        this.host = host;
        this.port = port;
        this.outBuffer = ByteBuffer.allocateDirect(msgSize);
        this.msgSize = msgSize;
        for(int i = 0; i < msgSize; i++) outBuffer.put((byte) i);
        outBuffer.flip();
        this.interval = 1;
        this.mod = 20000;
    }

    public static long busySleep(long t) {
        long x = 0;
        for(int i = 0; i < t * 20000; i++) {
            x += System.currentTimeMillis() / System.nanoTime();
        }
        return x;
    }

    public void start() throws Exception {
        this.socketChannel.configureBlocking(false);
        this.socketChannel.socket().setTcpNoDelay(true);
        this.socketChannel.connect(new InetSocketAddress(host, port));

        while(!socketChannel.finishConnect()) {
            System.out.println("Waiting to connect");
            Thread.sleep(1000);
        }
        System.out.println("Please wait as output will appear every minute or so. After " + WARMUP + " messages you will see the problem.");
        Thread t = new Thread(this);
        t.start();
    }

    private final void printResults(long latency, long timeToWrite, long timeToRead, long zeroReads, long partialReads, long realRead) {
        sb.setLength(0);
        sb.append(new java.util.Date().toString());
        sb.append(" Results: totalMessagesSent=").append(totalMessagesSent);
        sb.append(" currInterval=").append(interval);
        sb.append(" latency=").append(latency);
        sb.append(" timeToWrite=").append(timeToWrite);
        sb.append(" timeToRead=").append(timeToRead);
        sb.append(" realRead=").append(realRead);
        sb.append(" zeroReads=").append(zeroReads);
        sb.append(" partialReads=").append(partialReads);
        System.out.println(sb);
    }

    @Override
    public void run() {

        try {

            while(true) {

                busySleep(interval);

                outBuffer.position(0);

                timeSent = System.nanoTime();

                int bytesSent = socketChannel.write(outBuffer);
                long timeToWrite = System.nanoTime() - timeSent;
                if (bytesSent != msgSize) throw new IOException("Can't write message: " + bytesSent);

                inBuffer.clear();
                long zeroReads = 0;
                long partialReads = 0;
                long timeToRead = System.nanoTime();
                long realRead = 0;
                while(inBuffer.position() != msgSize) {
                    realRead = System.nanoTime();
                    int bytesRead = socketChannel.read(inBuffer);
                    if (bytesRead == 0) {
                        zeroReads++;
                    } else if (bytesRead == -1) {
                        System.out.println("Other side disconnected!");
                        return;
                    } else if (bytesRead != msgSize) {
                        partialReads++;
                        realRead = -1;
                    } else {
                        realRead = System.nanoTime() - realRead;
                    }
                }

                long now = System.nanoTime();

                timeToRead = now - timeToRead;

                long latency = now - timeSent;

                if (++totalMessagesSent % mod == 0 || totalMessagesSent == 1) {
                    printResults(latency, timeToWrite, timeToRead, zeroReads, partialReads, realRead);
                }

                if (totalMessagesSent == WARMUP) {
                    this.interval = 30000;
                    this.mod = 1;
                }
            }

        } catch(Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws Exception {

        Client client = new Client("localhost", 9999, 8);
        client.start();
    }
}

我执行java -server -cp . Serverjava -server -cp . Client。客户端的输出是:


根据@dunni 请求,将延迟更改为 1 秒而不是 30 秒。同样的问题:

【问题讨论】:

  • 我猜你在连接或套接字级别遇到了一些超时,并且每次写入都必须创建一个新连接。如果您选择例如,您会得到相同的结果吗? 5 或 10 秒的暂停时间?
  • 谢谢@dunni。使用 1 秒延迟而不是 30 秒延迟发布图片。同样的问题 :( setTcpNoDelay(true) 是否有可能被忽略,而应该归咎于 Nagle 算法?很难相信这样的 Java 错误会存在。
  • 你是对的,你的问题不可能是 Nagle 算法,因为你使用的是setTcpNoDelay( true ),而我在浏览你的代码时没有注意到。
  • 很难确定发生了什么,但我认为您的下一个嫌疑人可能是socketChannel.read(inBuffer);,它正在尝试读取未指定数量的字节。 TCP/IP 堆栈(或者可能是 java 网络层)很可能正在缓冲数据,直到它有 1024 个字节可以产生,或者直到它超时。尝试在每条消息前加上消息长度,这样您就可以始终准确地读取 4 个字节以获得消息长度,然后准确地读取 字节以避免发生缓冲的可能性。
  • 我可以确认它也发生在 Windows 上。

标签: java sockets networking jvm real-time


【解决方案1】:

您遇到的一个问题是,当没有数据要读取时,JVM、CPU 和它的缓存都处于休眠状态。一旦发生这种情况,机器必须做更多的事情才能获取数据,而不是当您的问题很严重时。

  • CPU 速度可能已降低以节省电量。例如一半正常。它可以在愚蠢的繁忙循环中执行此操作。
  • 线程未运行,必须在新 CPU 上重新启动。 (在您的情况下,这种情况应该很少见)
  • CPU 的缓存可能已断电,必须从 L3 缓存或主内存逐步加载
  • 即使在您的线程返回后,它也会比正常运行慢达 100 微秒,因为缓存会提取更多数据/代码。
  • 您将获得每秒 100 多次无法关闭的不可屏蔽中断。

简而言之,如果您需要一致的延迟,则需要

  • 关闭电源管理。
  • 不要放弃 CPU,即忙等待。 (你在做什么)
  • 在独立的 CPU 上运行,将线程与亲和性绑定。
  • 禁用该内核上的所有可屏蔽中断。
  • 使用用户空间驱动程序代替内核进行网络连接。

注意:鉴于每个操作似乎都需要大约 2 倍的时间,我会先看看电源管理。

【讨论】:

  • 谢谢@PeterLawrey。但是,我从我的 Ubuntu 服务器框中禁用了电源管理(在我的 grub 配置中使用acpi=off apm=off,如here 所述)并且仍然有同样的问题。你能在你的盒子里运行我简单的服务器和客户端并得到不同的结果吗? 这对于交易尤为重要,因为您希望在经过一段时间后以尽可能快的速度向交易所发送订单。 “使用用户空间驱动程序进行网络”是什么意思?也尝试了没有运气的亲和力。也许是一个糟糕的 TCP 拥塞算法?
  • 除非您使用的是低延迟网卡,否则几微秒不会产生太大影响。 solarflare.com/electronic-trading 这些可以帮助减少为降低吞吐量使用而增加的延迟。例如FX、FI 或商品。
  • 除非您使用的是低延迟网卡,否则几微秒不会产生太大影响我同意,但我真的需要深入了解这个问题.如果你在你的机器上执行提供的服务器和客户端,你可能会看到同样的神秘行为。
  • @LatencyFighter 是的,尽管这些网卡的值较低。我怀疑它与 Java 无关,因为它在网络层中。
  • 我同意它可能与 Java 无关,但我仍然需要修复它:/ 这在延迟方面是一种不好的行为。尝试了一个新的内核版本,但没有成功。我会继续研究。让我知道您是否还有其他可疑之处。它发生在 MacOS 和 Linux 上。完全神秘!
【解决方案2】:

我在看SocketChannelImpl的代码 并注意到 read() 涉及两个监视器 - 一个读锁和一个状态锁。

我的观点是,锁在热且无竞争时表现得更好。

以下类基于您的客户端,并且只进行一些锁定,类似于在 SocketChannelImpl 中所做的。从不可观察的情况来看,我的盒子(win8,jdk8)的延迟变为〜5000

import java.util.concurrent.TimeUnit;

public class Locker implements Runnable {

private static final int WARMUP = 40000;

private final Object readLock = new Object();
private final Object writeLock = new Object();
private final Object stateLock = new Object();

private final StringBuilder sb = new StringBuilder(1024);

private long interval;
private int totalMessagesSent;
private long timeSent;
private int mod;
private long totalOps;
private long readerThread;
private long writerThread;


public Locker() {
    this.interval = 1;
    this.mod = 20000;
}

public static long busySleep(long t) throws InterruptedException {
    long until = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(t);
    while(System.nanoTime() < until);
    return until;
}

public void start() throws Exception {
    Thread t = new Thread(this);
    t.start();
}

private final void printResults(long latency, long timeToRead) {
    sb.setLength(0);
    sb.append(new java.util.Date().toString());
    sb.append(" Results: totalMessagesSent=").append(totalMessagesSent);
    sb.append(" currInterval=").append(interval);
    sb.append(" latency=").append(latency);
    sb.append(" timeToRead=").append(timeToRead);
    sb.append(" totalOps=").append(totalOps);
    sb.append(" reader=").append(readerThread);
    sb.append(" writer=").append(writerThread);
    System.out.println(sb);
}

@Override
public void run() {

    try {
        while(true) {

            busySleep(interval);

            timeSent = System.nanoTime();

            try {
                synchronized (writeLock) {
                    synchronized (stateLock) {
                        writerThread = Thread.currentThread().getId();
                    }
                    totalOps++;
                }
            }
            finally {
                synchronized (stateLock) {
                    writerThread = 0;
                }
            }

            long timeToRead = System.nanoTime();

            try {
                synchronized (readLock) {
                    synchronized (stateLock) {
                        readerThread = Thread.currentThread().getId();
                    }
                    totalOps++;
                }
            } finally {
                synchronized (stateLock) {
                    readerThread = 0;
                }
            }

            long now = System.nanoTime();

            timeToRead = now - timeToRead;

            long latency = now - timeSent;

            if (++totalMessagesSent % mod == 0 || totalMessagesSent == 1) {
                printResults(latency, timeToRead);
            }

            if (totalMessagesSent == WARMUP) {
                this.interval = 5000;
                this.mod = 1;
            }
        }

    } catch(Exception e) {
        throw new RuntimeException(e);
    }
}

public static void main(String[] args) throws Exception {
    Locker locker = new Locker();
    locker.start();
}
}

编辑:根据 OP 的建议修改的代码表现出相同的延迟增加:

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

public class Locker {
    static final int WARMUP = 20000;
    final Object readLock = new Object();
    final Object writeLock = new Object();
    final Object stateLock = new Object();

    long interval = 1;
    int totalMessagesSent;
    long totalOps;
    long readerThread;
    long writerThread;
    final long[] measures = new long[WARMUP + 20];

    static long busySleep(long t) {
        long until = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(t);
        while(System.nanoTime() < until);
        return until;
    }
    void printResults(long latency, long timeToRead) {
        if (readerThread != 0 || writerThread != 0 || totalMessagesSent  > totalOps || timeToRead < 0) throw new Error();
        measures[totalMessagesSent] = latency;
    }

    void run() {
        while(totalMessagesSent < measures.length) {
            busySleep(interval);
            long timeSent = System.nanoTime();
            try {
                synchronized (writeLock) {
                    synchronized (stateLock) {
                        writerThread = Thread.currentThread().getId();
                    }
                    totalOps++;
                }
            }
            finally {
                synchronized (stateLock) {
                    writerThread = 0;
                }
            }
            long timeToRead = System.nanoTime();
            try {
                synchronized (readLock) {
                    synchronized (stateLock) {
                        readerThread = Thread.currentThread().getId();
                    }
                    totalOps++;
                }
            } finally {
                synchronized (stateLock) {
                    readerThread = 0;
                }
            }
            long now = System.nanoTime();
            timeToRead = now - timeToRead;
            long latency = now - timeSent;
            printResults(latency, timeToRead);
            ++totalMessagesSent;
            this.interval = (totalMessagesSent/WARMUP * 5000) + 1;
        }
        System.out.println("last measures = " + Arrays.toString(Arrays.copyOfRange(measures, WARMUP - 20, measures.length - 1)));
    }

    public static void main(String[] args) {
        new Locker().run();
    }
}

【讨论】:

  • 感谢 Nikolay,但这里的问题是 IF 更改间隔变量。它在触发时会与 JIT 内联混淆。如果您永远保留mod = 1,将结果写入文件并生成this.interval = (totalMessagesSent / WARMUP * 5000) + 1;,您将看到当间隔更改为 5000 时延迟是相同的。但是,即使进行了更改,SocketChannel 仍然表现出糟糕的延迟增加,所以谜团还在继续。用C++重写SocketChannel程序,希望能给我们更多的线索。
  • 我按照你的建议做了,并观察到延迟从 0 到 ~5k 的相同增加
  • 我运行了您的代码并看到了您所说的,但随后我继续注释掉所有同步以删除所有锁定,因此不再锁定。猜猜看:问题仍然存在。很奇怪!
  • 这是一个明确描述此问题的新问题:stackoverflow.com/questions/43696948/…
猜你喜欢
  • 1970-01-01
  • 2017-07-05
  • 2023-03-29
  • 2018-12-02
  • 1970-01-01
  • 2011-06-02
  • 2019-01-13
  • 1970-01-01
  • 2014-12-04
相关资源
最近更新 更多