考虑以下客户端和服务器组件:
import java.io.InputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
public class client {
public static void main(String[] args) throws IOException {
while (true) {
URL url = new URL("http://localhost:8000");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
int statusCode = connection.getResponseCode();
System.out.println("Status Code: " + statusCode);
connection.disconnect();
}
}
}
import java.io.OutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class server {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8000);
while (true) {
Socket clientSocket = serverSocket.accept();
OutputStream outputStream = clientSocket.getOutputStream();
outputStream.write("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n".getBytes());
outputStream.flush();
clientSocket.close();
}
}
}
在服务器运行时运行客户端时,您很快就会看到客户端开始在 TCP 级别上挂起
SYN_SENT
(总共大约 30 秒):
$ watch -n 0.1 "ss -on state syn-sent '( dport = :8000 )'"
Every 0.1s: ss -on state syn-sent '( dport = :8000 )' myhost: Tue Jul 16 04:08:52 2024
Netid Recv-Q Send-Q Local Address:Port Peer Address:Port Process
tcp 0 1 [::ffff:127.0.0.1]:60418 [::ffff:127.0.0.1]:8000 timer:(on,3.731ms,2)
$ pkill -3 java
# Stack trace of client's main thread while hanging outputted in Java terminal...
"main" #1 prio=5 os_prio=0 cpu=2429.68ms elapsed=40.96s tid=0x000079e6c40266c0 nid=0x18a1c6 runnable [0x000079e6cb9fd000]
java.lang.Thread.State: RUNNABLE
at sun.nio.ch.Net.connect0([email protected]/Native Method)
at sun.nio.ch.Net.connect([email protected]/Net.java:579)
at sun.nio.ch.Net.connect([email protected]/Net.java:568)
at sun.nio.ch.NioSocketImpl.connect([email protected]/NioSocketImpl.java:593)
at java.net.Socket.connect([email protected]/Socket.java:633)
at java.net.Socket.connect([email protected]/Socket.java:583)
at sun.net.NetworkClient.doConnect([email protected]/NetworkClient.java:183)
at sun.net.www.http.HttpClient.openServer([email protected]/HttpClient.java:533)
at sun.net.www.http.HttpClient.openServer([email protected]/HttpClient.java:638)
at sun.net.www.http.HttpClient.<init>([email protected]/HttpClient.java:281)
at sun.net.www.http.HttpClient.New([email protected]/HttpClient.java:386)
at sun.net.www.http.HttpClient.New([email protected]/HttpClient.java:422)
at sun.net.www.protocol.http.HttpURLConnection.setNewClient([email protected]/HttpURLConnection.java:831)
at sun.net.www.protocol.http.HttpURLConnection.setNewClient([email protected]/HttpURLConnection.java:819)
at sun.net.www.protocol.http.HttpURLConnection.writeRequests([email protected]/HttpURLConnection.java:759)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream0([email protected]/HttpURLConnection.java:1708)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream([email protected]/HttpURLConnection.java:1611)
at java.net.HttpURLConnection.getResponseCode([email protected]/HttpURLConnection.java:529)
at client.main(client.java:13)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0([email protected]/Native Method)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke([email protected]/NativeMethodAccessorImpl.java:77)
at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke([email protected]/DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke([email protected]/Method.java:568)
at com.sun.tools.javac.launcher.Main.execute([email protected]/Main.java:419)
at com.sun.tools.javac.launcher.Main.run([email protected]/Main.java:192)
at com.sun.tools.javac.launcher.Main.main([email protected]/Main.java:132)
我正在构建一个Java应用程序(当我遇到这个问题时,我尝试了10毫秒的轮询间隔 - 30毫秒似乎对我有用),我需要在一段时间内像这样快速发送HTTP请求(我无法使用网络套接字)。所以,我的问题是为什么会发生这种挂断以及如何修复它?
到目前为止,我修复此问题的最佳尝试是增加双方可用文件描述符的数量(
uname -n unlimitied
),但无济于事。
现在测试更多...我也可以用 Python 重现同样的事情:
import requests
while True:
response = requests.get("http://localhost:8000")
print(f"Status Code: {response.status_code}")
然后
python -m http.server
对于服务器,您将获得 SYN_SENT
挂起。因此,看起来这个问题可能比我最初预期的要更深,但我很好奇并愿意听到任何潜在的补救措施。
我期望的行为是不要发生这个
SYN_SENT
悬挂问题。我希望能够将 HTTP 请求轮询间隔调整为非常低(甚至本地网络上的请求之间为 1 毫秒;只要没有资源泄漏并且所有请求都连续发生,我不明白为什么会这样)无法实现),同时仍然让我的 Java 或其他应用程序以完美稳健的方式工作。不过,我也想抓住这个问题。 Wireshark 的视图让我感到困惑,因为它显示服务器已返回其 HTTP 200 OK 响应,但由于某种原因,客户端在读取它时挂起(我尝试包含重现我所看到的内容所需的所有内容,因为我一直在调试)这几个小时)。谢谢您的宝贵时间。
我很高兴地说我已经找到了这个问题的根本原因!我注意到每当我遇到这些
SYN_SENT
挂起(如 ss
所示)时,我也会将其登录到我的 dmesg
:
nf_conntrack: nf_conntrack: table full, dropping packet
连接跟踪表已满!我发出的每个 HTTP 1.1 请求都在其自己的 TCP 流上运行。现在一切都变得非常有意义了。
我正在运行一个相当独特的 Linux 发行版,所以我不确定我的连接跟踪表是否小于平均水平。我的应用程序中还存在一些套接字/资源泄漏问题,甚至在我的系统上的单独程序之间也加剧了该问题。我还没有考虑增加这个表的大小。如果您遇到此问题,请记住问题也可能是网络上其他设备(例如路由器、交换机或防火墙)的小连接表。
我让我的应用程序以非常低的轮询间隔运行一夜,看看会发生什么,当我醒来时,Java 编译器失败了,因为 systemd 似乎已经用这些
/tmp
日志填充了 nf_conntrack
。就在那时我想检查一下dmesg
,瞧!然后我重新启动以清除tmpfs
。