【问题标题】:A simple Java HTTP server fails with ApacheBench but works fine on a browser一个简单的 Java HTTP 服务器在 ApacheBench 上失败,但在浏览器上运行良好
【发布时间】:2021-01-22 16:35:06
【问题描述】:

作为并发博客系列的一部分,我正在用不同的语言(Java、Kotlin、Rust、Go、JS、TS)构建最简单的 HTTP 服务器,并且除了 Java/Kotlin(也就是在 JVM 上)之外的所有东西都可以正常工作。所有代码都可以找到here。下面是 Java 中的服务器代码,我尝试了一个基于传统线程的代码和一个基于 AsynchronousServerSocketChannel 的代码,但是无论我使用 ApacheBench 运行基准测试时,它都以 Broken pipeapr_socket_recv: Connection reset by peer (104) 失败,这很奇怪,因为类似的设置其他语言工作正常。这里的问题只发生在 ApacheBench 上,因为当我在浏览器中访问 URL 时它工作正常。所以我正在努力弄清楚发生了什么。我试图玩keep-alive等,但似乎没有帮助。我看了一堆类似的例子,我没有看到任何地方有什么特别之处。我希望有人能弄清楚这里出了什么问题,因为这肯定与 JVM + APacheBench 有关。我已经用 Java 11 和 15 尝试过,但结果相同。

Java 线程示例hello.html 可以是任何 HTML 文件)

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class JavaHTTPServerCopy {
    public static void main(String[] args) {
        int port = 8080;
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("Server is listening on port " + port);
            while (true) {
                new ServerThreadCopy(serverSocket.accept()).start();
            }
        } catch (IOException ex) {
            System.out.println("Server exception: " + ex.getMessage());
        }
    }
}

class ServerThreadCopy extends Thread {

    private final Socket socket;

    public ServerThreadCopy(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        var file = new File("hello.html");
        try (
                // we get character output stream to client (for headers)
                var out = new PrintWriter(socket.getOutputStream());
                // get binary output stream to client (for requested data)
                var dataOut = new BufferedOutputStream(socket.getOutputStream());
                var fileIn = new FileInputStream(file)
        ) {
            var fileLength = (int) file.length();
            var fileData = new byte[fileLength];
            int read = fileIn.read(fileData);
            System.out.println("Responding with Content-length: " + read);
            var contentMimeType = "text/html";
            // send HTTP Headers
            out.println("HTTP/1.1 200 OK");
            out.println("Connection: keep-alive");
            out.println("Content-type: " + contentMimeType);
            out.println("Content-length: " + fileLength);
            out.println(); // blank line between headers and content, very important !
            out.flush(); // flush character output stream buffer

            dataOut.write(fileData, 0, fileLength);
            dataOut.flush();
        } catch (Exception ex) {
            System.err.println("Error with exception : " + ex);
        } finally {
            try {
                socket.close(); // we close socket connection
            } catch (Exception e) {
                System.err.println("Error closing stream : " + e.getMessage());
            }
        }
    }
}

控制台出错

Responding with Content-length: 176
Error with exception : java.net.SocketException: Broken pipe (Write failed)
Error with exception : java.net.SocketException: Broken pipe (Write failed)
Error with exception : java.net.SocketException: Broken pipe (Write failed)
Error with exception : java.net.SocketException: Broken pipe (Write failed)
Error with exception : java.net.SocketException: Broken pipe (Write failed)

ApacheBench 输出

ab -c 100 -n 1000 http://localhost:8080/ 

This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
apr_socket_recv: Connection reset by peer (104)

Java 异步示例

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;

public class JavaAsyncHTTPServer {

    public static void main(String[] args) throws Exception {
        new JavaAsyncHTTPServer().go();
        Thread.currentThread().join();//Wait forever
    }

    private void go() throws IOException {
        AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open();
        InetSocketAddress hostAddress = new InetSocketAddress("localhost", 8080);
        server.bind(hostAddress);
        server.setOption(StandardSocketOptions.SO_REUSEADDR, true);
        System.out.println("Server channel bound to port: " + hostAddress.getPort());

        if (server.isOpen()) {
            server.accept(null, new CompletionHandler<>() {
                @Override
                public void completed(final AsynchronousSocketChannel result, final Object attachment) {
                    if (server.isOpen()) {
                        server.accept(null, this);
                    }
                    handleAcceptConnection(result);
                }

                @Override
                public void failed(final Throwable exc, final Object attachment) {
                    if (server.isOpen()) {
                        server.accept(null, this);
                        System.out.println("Connection handler error: " + exc);
                    }
                }
            });
        }
    }

    private void handleAcceptConnection(final AsynchronousSocketChannel ch) {
        var content = "Hello Java!";
        var message = ("HTTP/1.0 200 OK\n" +
                "Connection: keep-alive\n" +
                "Content-length: " + content.length() + "\n" +
                "Content-Type: text/html; charset=utf-8\r\n\r\n" +
                content).getBytes();
        var buffer = ByteBuffer.wrap(message);
        ch.write(buffer);
        try {
            ch.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

控制台没有错误

ApacheBench 输出

❯ ab -c 100 -n 1000 http://localhost:8080/

This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
apr_socket_recv: Connection reset by peer (104)

保持活动状态的 ApacheBench 输出

 ab -k -c 100 -n 1000 http://localhost:8080/
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
Send request failed!
apr_socket_recv: Connection reset by peer (104)
Total of 37 requests completed

【问题讨论】:

  • 在 Twitter 上,有人建议在 ServerSocket 上使用 100 的积压,所以我尝试了 new ServerSocket(port, 100) 并按照建议将 -r 标志添加到 ab 命令,结果更好但仍然超时。见以下结果Completed 100 requests apr_pollset_poll: The timeout specified has expired (70007) Total of 101 requests completed
  • 我从实验中学到的更多东西。似乎如果我将所有请求作为并发发送,它们就会成功。例如ab -r -c 100 -n 100 http://127.0.0.1:8080/ 成功,ab -r -c 1000 -n 1000 http://127.0.0.1:8080/ 在超时前至少发送了大约 700 个请求。所以这似乎是类似于serverfault.com/questions/146605/… 的问题

标签: java kotlin jvm webserver apachebench


【解决方案1】:

对我来说,这就像一个错误。

您的响应是 HTTP/1.0 + "Connection: keep-alive",这意味着您正在向客户端宣传它可以重用连接来执行其他请求。然而,您在编写响应后立即关闭了套接字。

结果,由于网络不是即时的,客户端试图重用套接字并编写第二个请求,只是为了让门砰的一声关上。

在每次响应时停止关闭套接字,或者停止执行“Connection: keep-alive”(关闭是 HTTP/1.0 上的默认设置)。

【讨论】:

  • 我实际上最初尝试过 "Connection: close" 部分,但所有请求都失败了 apr_socket_recv: Connection refused (111)。不过,您关闭套接字是完全正确的。我完全忽略了那部分。我终于找到了答案。我将其添加为问题的答案
【解决方案2】:

想问是否尝试设置 127.0.0.1 而不是 localhost

InetAddress 地址 = InetAddress.getByName("127.0.0.1"); ServerSocket sock = new ServerSocket(1234, 50, addr);

【讨论】:

  • 那没有任何区别
【解决方案3】:

我获取了您的“Java 线程示例”代码并在 IntelliJ IDEA 中的 macOS 11.1 上运行它,然后运行我机器上安装的默认 ab 二进制文件 - 测试运行的输出与您的类似:

➜ ab -c 100 -n 1000 http://127.0.0.1:8080/
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
apr_socket_recv: Connection reset by peer (54)

经过一番研究,我收集到的主要信息向我指出了 macOS 上的 ab 二进制文件可能被破坏的方向。我走上了在 docker 容器中运行 ab 二进制文件的道路。

➜ docker run --rm jordi/ab -k -c 100 -n 1000 http://host.docker.internal:8080/
This is ApacheBench, Version 2.3 <$Revision: 1826891 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking host.docker.internal (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:
Server Hostname:        host. docker.internal
Server Port:            8080

Document Path:          /
Document Length:        7 bytes

Concurrency Level:      100
Time taken for tests:   0.343 seconds
Complete requests:      1000
Failed requests:        495
   (Connect: 0, Receive: 0, Length: 495, Exceptions: 0)
Keep-Alive requests:    505
Total transferred:      45657 bytes
HTML transferred:       3591 bytes
Requests per second:    2913.39 [#/sec] (mean)
Time per request:       34.324 [ms] (mean)
Time per request:       0.343 [ms] (mean, across all concurrent requests)
Transfer rate:          129.90 [Kbytes/sec] received

Connection Times (ms)
               min  mean[+/-sd] median   max
Connect:        0   15  15.5     20      48
Processing:     0   15  11.1     11      43
Waiting:        0   13  13.6     10      43
Total:          0   31  25.4     33      75

Percentage of the requests served within a certain time (ms)
  50%     33
  66%     46
  75%     57
  80%     59
  90%     65
  95%     68
  98%     73
  99%     74
 100%     75 (longest request)

可能值得在您的测试中尝试不同版本/修订版的 ApacheBench。

编辑:在运行 ApacheBench 时省略 -k(keep-alive 标志)时,输出为

➜ docker run --rm jordi/ab -c 100 -n 1000 
http://host.docker.internal:8080/
This is ApacheBench, Version 2.3 <$Revision: 1826891 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, 
http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking host.docker.internal (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:
Server Hostname:        host.docker.internal
Server Port:            8080

Document Path:          /
Document Length:        7 bytes

Concurrency Level:      100
Time taken for tests:   0.934 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      90157 bytes
HTML transferred:       7091 bytes
Requests per second:    1070.17 [#/sec] (mean)
Time per request:       93.444 [ms] (mean)
Time per request:       0.934 [ms] (mean, across all concurrent requests)
Transfer rate:          94.22 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        2   40  15.1     35      85
Processing:    26   48  14.2     44     110
Waiting:       13   36  12.0     33      79
Total:         31   88  20.1     86     144

Percentage of the requests served within a certain time (ms)
  50%     86
  66%     92
  75%    101
  80%    105
  90%    123
  95%    129
  98%    131
  99%    138
 100%    144 (longest request)

所以没有失败的请求。

【讨论】:

  • 我也会尝试 docker,但如果是这种情况,我会感到惊讶,因为我在 Fedora 上,并且相同的 ApacheBench 二进制文件对于 Golang、Rust、NodeJS 和 Deno 以及看看我原来的问题中的评论。
  • 我用 docker 试过了,结果一样。同样在您的输出中,一半的请求似乎也失败了。所以我仍然认为这与JVM有关
  • 你是对的。但是,当我省略 keepalive 标志时,所有请求都成功。
  • 有趣 - 我尝试按照您的 cmets 使用 -c 100 -n 100 运行,仍然得到 apr_socket_recv: Connection reset by peer (54)。我只能得出结论,我确实在 ApacheBench 中遇到了一些问题,而您看到的是完全不同的问题。
  • 是的,问题似乎也与操作系统有关,无论 ApacheBench 版本如何,我都看不到任何区别。并且错误会根据传递的-k 或 -r 标志而变化。
【解决方案4】:

因此,感谢此处和 Twitter 上的 cmets 和答案,第一个代码示例现在已修复。问题是在读取 TCP 流之前对其进行写入。感谢Ganesh 对此here 的原始解决方案,并在this 上进行了解释,所以答案

因此,这是适用于 Java 线程示例

的更新代码
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class JavaHTTPServer {
    public static void main(String[] args) {
        var count = 0;
        var port = 8080;
        try (var serverSocket = new ServerSocket(port, 100)) {
            System.out.println("Server is listening on port " + port);
            while (true) {
                count++;
                new ServerThread(serverSocket.accept(), count).start();
            }
        } catch (IOException ex) {
            System.out.println("Server exception: " + ex.getMessage());
        }
    }
}

class ServerThread extends Thread {

    private final Socket socket;
    private final int count;

    public ServerThread(Socket socket, int count) {
        this.socket = socket;
        this.count = count;
    }

    @Override
    public void run() {
        var file = new File("hello.html");
        try (
                var in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                // we get character output stream to client (for headers)
                var out = new PrintWriter(socket.getOutputStream());
                // get binary output stream to client (for requested data)
                var dataOut = new BufferedOutputStream(socket.getOutputStream());
                var fileIn = new FileInputStream(file)
        ) {
            // add 2 second delay to every 10th request
            if (count % 10 == 0) {
                System.out.println("Adding delay. Count: " + count);
                Thread.sleep(2000);
            }

            // read the request fully to avoid connection reset errors and broken pipes
            while (true) {
                String requestLine = in.readLine();
                if (requestLine == null || requestLine.length() == 0) {
                    break;
                }
            }

            var fileLength = (int) file.length();
            var fileData = new byte[fileLength];
            fileIn.read(fileData);

            var contentMimeType = "text/html";
            // send HTTP Headers
            out.println("HTTP/1.1 200 OK");
            out.println("Content-type: " + contentMimeType);
            out.println("Content-length: " + fileLength);
            out.println("Connection: keep-alive");

            out.println(); // blank line between headers and content, very important !
            out.flush(); // flush character output stream buffer

            dataOut.write(fileData, 0, fileLength); // write the file data to output stream
            dataOut.flush();
        } catch (Exception ex) {
            System.err.println("Error with exception : " + ex);
        }
    }
}

和 apacheBench 输出

ab -r -c 100 -n 1000 http://127.0.0.1:8080/
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        
Server Hostname:        127.0.0.1
Server Port:            8080

Document Path:          /
Document Length:        176 bytes

Concurrency Level:      100
Time taken for tests:   2.385 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      260000 bytes
HTML transferred:       176000 bytes
Requests per second:    419.21 [#/sec] (mean)
Time per request:       238.546 [ms] (mean)
Time per request:       2.385 [ms] (mean, across all concurrent requests)
Transfer rate:          106.44 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1   1.8      0       8
Processing:     0  221 600.7     21    2058
Waiting:        0  220 600.8     21    2057
Total:          0  221 600.8     21    2058

Percentage of the requests served within a certain time (ms)
  50%     21
  66%     33
  75%     38
  80%     43
  90%   2001
  95%   2020
  98%   2036
  99%   2044
 100%   2058 (longest request)

我将尝试以相同的方式修复第二个 Async 示例

编辑:也修复了异步示例

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutionException;

public class JavaAsyncHTTPServer {

    public static void main(String[] args) throws Exception {
        new JavaAsyncHTTPServer().start();
        Thread.currentThread().join(); // Wait forever
    }

    private void start() throws IOException {
        // we shouldn't use try with resource here as it will kill the stream
        var server = AsynchronousServerSocketChannel.open();
        var hostAddress = new InetSocketAddress("127.0.0.1", 8080);
        server.bind(hostAddress, 100);   // bind listener
        server.setOption(StandardSocketOptions.SO_REUSEADDR, true);
        System.out.println("Server is listening on port 8080");

        final int[] count = {0}; // count used to introduce delays

        // listen to all incoming requests
        server.accept(null, new CompletionHandler<>() {
            @Override
            public void completed(final AsynchronousSocketChannel result, final Object attachment) {
                if (server.isOpen()) {
                    server.accept(null, this);
                }
                count[0]++;
                handleAcceptConnection(result, count[0]);
            }

            @Override
            public void failed(final Throwable exc, final Object attachment) {
                if (server.isOpen()) {
                    server.accept(null, this);
                    System.out.println("Connection handler error: " + exc);
                }
            }
        });
    }

    private void handleAcceptConnection(final AsynchronousSocketChannel ch, final int count) {
        var file = new File("hello.html");
        try (var fileIn = new FileInputStream(file)) {
            // add 2 second delay to every 10th request
            if (count % 10 == 0) {
                System.out.println("Adding delay. Count: " + count);
                Thread.sleep(2000);
            }
            if (ch != null && ch.isOpen()) {
                // Read the first 1024 bytes of data from the stream
                final ByteBuffer buffer = ByteBuffer.allocate(1024);
                // read the request fully to avoid connection reset errors
                ch.read(buffer).get();

                // read the HTML file
                var fileLength = (int) file.length();
                var fileData = new byte[fileLength];
                fileIn.read(fileData);

                // send HTTP Headers
                var message = ("HTTP/1.1 200 OK\n" +
                        "Connection: keep-alive\n" +
                        "Content-length: " + fileLength + "\n" +
                        "Content-Type: text/html; charset=utf-8\r\n\r\n" +
                        new String(fileData, StandardCharsets.UTF_8)
                ).getBytes();

                // write the to output stream
                ch.write(ByteBuffer.wrap(message)).get();

                buffer.clear();
                ch.close();
            }
        } catch (IOException | InterruptedException | ExecutionException e) {
            System.out.println("Connection handler error: " + e);
        }
    }
}

【讨论】:

    猜你喜欢
    • 2023-04-01
    • 1970-01-01
    • 2013-06-08
    • 1970-01-01
    • 2020-04-10
    • 2017-06-16
    • 2020-08-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多