使用 keepalive 的 HttpServer 非常慢
HttpServer very slow with keepalive
以下 HttpServer 程序在没有 HTTP keepalive 的情况下轻松处理 8000 requests/s,但在有 keepalive 的情况下只能处理可怜的 22 requests/s。
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
public class HSTest {
public static void main(String[] args) throws IOException {
HttpServer hs = HttpServer.create(new InetSocketAddress(30006), 1000);
hs.createContext("/", new HttpHandler() {
public void handle(HttpExchange he) throws IOException {
byte[] FILE = "xxxxx".getBytes();
he.sendResponseHeaders(200, FILE.length);
OutputStream os = he.getResponseBody();
os.write(FILE);
os.flush();
he.close();
}
});
hs.start();
}
}
下面是使用 keepalive 时的样子:
请注意数据包 6、12 和 17 的巨大延迟。在第一个数据包之后,它们总是略高于 40 毫秒。相比之下,没有 keepalive 一切都很好:
在第一毫秒结束之前,这是 3 个完整的请求!
我在 debian sid Linux amd64 上使用 OpenJDK 8,客户端和服务器都在同一台机器上,并通过本地主机接口进行通信。为了进行测试,我使用 ab -n 100000 -c 1 http://localhost:30006/
(无保活)和 ab -n 100000 -c 1 -k http://localhost:30006/
(保活),以及 curl 和 chromium(默认情况下都使用保活)。
那么是什么导致了 HTTP keepalive 请求的 40 毫秒延迟,我该如何让我的服务器变快?
就像评论中暗示的那样,我认为这里引起关注的主要原因是 "normal" 要求 HTTP 吞吐量在单个连接上达到极高的吞吐量(不改变默认设置)。如果您在允许多个客户端时会得到类似的灾难性数字(例如 -c 100
标志到 ab),那将是一个不同的问题。 KeepAlive 总体上具有在每个连接一个线程的服务器上占用线程的效果。
我认为您所观察到的与 TCP_NODELAY(Nagle 算法)有关,可能伴随着 "delayed acks"。就您永远不会被它击中的数据包数量而言,无保活情况足够短。
https://eklitzke.org/the-caveats-of-tcp-nodelay 特别提到 "up to 40 ms" 在 Linux 上的延迟
http://bugs.java.com/bugdatabase/view_bug.do?bug_id=7068416 提到 Java 属性 用于在基本 Java HTTP 服务器中启用 TCP_NODELAY。我非常有信心,如果启用此标志,您会看到不同的行为。
另一种方法是将延迟确认超时更改为不同于 40 毫秒的值。参见例如https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_MRG/1.3/html/Realtime_Tuning_Guide/sect-Realtime_Tuning_Guide-General_System_Tuning-Reducing_the_TCP_delayed_ack_timeout.html
以下 HttpServer 程序在没有 HTTP keepalive 的情况下轻松处理 8000 requests/s,但在有 keepalive 的情况下只能处理可怜的 22 requests/s。
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
public class HSTest {
public static void main(String[] args) throws IOException {
HttpServer hs = HttpServer.create(new InetSocketAddress(30006), 1000);
hs.createContext("/", new HttpHandler() {
public void handle(HttpExchange he) throws IOException {
byte[] FILE = "xxxxx".getBytes();
he.sendResponseHeaders(200, FILE.length);
OutputStream os = he.getResponseBody();
os.write(FILE);
os.flush();
he.close();
}
});
hs.start();
}
}
下面是使用 keepalive 时的样子:
请注意数据包 6、12 和 17 的巨大延迟。在第一个数据包之后,它们总是略高于 40 毫秒。相比之下,没有 keepalive 一切都很好:
在第一毫秒结束之前,这是 3 个完整的请求!
我在 debian sid Linux amd64 上使用 OpenJDK 8,客户端和服务器都在同一台机器上,并通过本地主机接口进行通信。为了进行测试,我使用 ab -n 100000 -c 1 http://localhost:30006/
(无保活)和 ab -n 100000 -c 1 -k http://localhost:30006/
(保活),以及 curl 和 chromium(默认情况下都使用保活)。
那么是什么导致了 HTTP keepalive 请求的 40 毫秒延迟,我该如何让我的服务器变快?
就像评论中暗示的那样,我认为这里引起关注的主要原因是 "normal" 要求 HTTP 吞吐量在单个连接上达到极高的吞吐量(不改变默认设置)。如果您在允许多个客户端时会得到类似的灾难性数字(例如 -c 100
标志到 ab),那将是一个不同的问题。 KeepAlive 总体上具有在每个连接一个线程的服务器上占用线程的效果。
我认为您所观察到的与 TCP_NODELAY(Nagle 算法)有关,可能伴随着 "delayed acks"。就您永远不会被它击中的数据包数量而言,无保活情况足够短。
https://eklitzke.org/the-caveats-of-tcp-nodelay 特别提到 "up to 40 ms" 在 Linux 上的延迟 http://bugs.java.com/bugdatabase/view_bug.do?bug_id=7068416 提到 Java 属性 用于在基本 Java HTTP 服务器中启用 TCP_NODELAY。我非常有信心,如果启用此标志,您会看到不同的行为。
另一种方法是将延迟确认超时更改为不同于 40 毫秒的值。参见例如https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_MRG/1.3/html/Realtime_Tuning_Guide/sect-Realtime_Tuning_Guide-General_System_Tuning-Reducing_the_TCP_delayed_ack_timeout.html