AWS EC2大UDP数据包收发异常:仅前100次正常,后续丢包
问题背景
我用Java编写了UDP传输速度测试代码,在家中网络部署UDP回显服务器(UDPSendBack),AWS EC2作为客户端(UDPSender)。已开放家庭网络防火墙端口,AWS安全组允许所有来源流量。测试现象如下:
- 数据包大小为400B或4KB时,仅存在UDP本身特性导致的少量丢包,无其他异常;
- 数据包大小为40KB时,按10ms间隔发送1000次请求,仅前100次收发正常,后续全部丢包。
测试代码
UDPSender(EC2客户端)
import java.net.*; public class UDPSender { //Send and receive private final DatagramSocket socket; private final String RECEVER_IP; private final int PORT; private final int SIZE; private final int REPEAT; private int COUNT = 0; private int WRONG_COUNT = 0; private int TIME_SUM = 0; private synchronized void COUNT() { COUNT++; } private synchronized void WRONG_COUNT() { WRONG_COUNT++; } private synchronized void TIME_SUM(int time) { TIME_SUM += time; } public static void main(String[] args) throws Exception { if (args.length == 0) args = new String[]{"192.168.1.33", "53135", "40000", "1000"}; new UDPSender(args[0], Integer.parseInt(args[1]), Integer.parseInt(args[2]), Integer.parseInt(args[3])); } public UDPSender(String ip, int port, int size, int repeat) throws Exception { this.RECEVER_IP = ip; this.PORT = port; this.SIZE = size; this.REPEAT = repeat; this.socket = new DatagramSocket(); for (int i = 1; i <= REPEAT; i++) { send(i); Thread.sleep(10); } } private void send(int id) throws Exception { new Thread(() -> { try { byte[] buffer = createData(); InetAddress address = InetAddress.getByName(RECEVER_IP); DatagramPacket packet = new DatagramPacket(buffer, buffer.length, address, PORT); socket.send(packet); System.out.println(id + " messages sent"); receive(socket); } catch (Exception e) { e.printStackTrace(); } }).start(); } private void receive(DatagramSocket socket) throws Exception { byte[] buffer = new byte[SIZE]; DatagramPacket packet = new DatagramPacket(buffer, buffer.length); socket.receive(packet); long sendTime = validateData(packet.getData()); long taken = System.currentTimeMillis() - sendTime; COUNT(); if (sendTime == -1) { WRONG_COUNT(); return; } TIME_SUM((int)taken); System.out.println(COUNT + " messages received (including " + WRONG_COUNT + " wrong message) / Time taken " + taken + "ms (Average " + String.format("%.3f", (double)TIME_SUM / COUNT) + "ms)"); } private byte[] createData() { long currentTime = System.currentTimeMillis(); byte[] buffer = new byte[SIZE]; for (int i = 0; i < SIZE; i += 8) { for (int j = 0; j < 8; j++) { buffer[i + j] = (byte) (currentTime >> (56 - (8 * j))); } } return buffer; } private long validateData(byte[] data) { long sendTime = -1; for (int i = 0; i < SIZE; i += 8) { long time = 0; for (int j = 0; j < 8; j++) { time |= ((long) data[i + j] & 0xff) << (56 - (8 * j)); } if (sendTime != -1 && time != sendTime) { return -1; } sendTime = time; } return sendTime; } }
UDPSendBack(家庭服务器)
import java.net.*; class UDPSendBack { //Receive and send private final DatagramSocket socket; private final int SIZE; public static void main(String[] args) throws Exception { if (args.length == 0) args = new String[]{"53135", "40000"}; new UDPSendBack(Integer.parseInt(args[0]), Integer.parseInt(args[1])); } public UDPSendBack(int port, int size) throws Exception { this.SIZE = size; this.socket = new DatagramSocket(port); while (true) { byte[] buffer = new byte[SIZE]; DatagramPacket packet = new DatagramPacket(buffer, buffer.length); socket.receive(packet); new Thread(() -> { DatagramPacket sendPacket = new DatagramPacket(buffer, buffer.length, packet.getAddress(), packet.getPort()); try { socket.send(sendPacket); System.out.println("Received message and send back"); } catch (Exception e) { e.printStackTrace(); } }).start(); } } }
问题分析与解决方案
一、代码层面的潜在问题
UDP套接字接收缓冲区溢出
客户端使用单个DatagramSocket处理所有发送和接收,UDP套接字默认接收缓冲区大小有限(通常几十KB到几百KB)。40KB数据包的回包会快速占满缓冲区,后续数据包直接被丢弃,无法触发receive()方法。
修复:在客户端和服务器创建DatagramSocket后,手动设置更大的接收缓冲区:socket.setReceiveBufferSize(1024 * 1024 * 5); // 设置为5MB线程资源耗尽
每次发送都新建线程执行receive(),1000次发送会创建1000个线程,大量线程阻塞在receive()上会耗尽系统资源,导致后续线程无法正常运行。
修复:改用线程池复用线程,替换new Thread()为线程池提交任务:// 在UDPSender构造方法中初始化线程池 ExecutorService executor = Executors.newFixedThreadPool(10); // 在send方法中使用线程池 executor.submit(() -> { /* 原线程逻辑 */ });IP分片丢失
40KB数据包远超以太网MTU(通常1500字节),会被IP层拆分为27个左右的分片。只要任一分片丢失,整个UDP包就会被丢弃,小数据包分片数少,丢失概率低。
修复:添加重传机制,对未收到回包的请求进行重试;或限制UDP包大小在MTU范围内(建议不超过1472字节,留足IP和UDP头部空间)。
二、AWS或网络层面的限制
AWS EC2带宽/速率限制
40KB包按10ms间隔发送,带宽需求约320Mbps,若EC2实例带宽不足以支撑,AWS会触发丢包。可通过CloudWatch监控实例的网络带宽使用情况,确认是否达到上限。此外,AWS对UDP大流量可能存在隐性速率限制。家庭网络瓶颈
家庭宽带上传带宽通常较低,320Mbps的需求远超多数家庭宽带的上传能力,会导致上传队列溢出丢包。同时,家庭NAT设备的会话表容量有限,大量UDP会话快速建立会占满NAT表,导致后续会话无法创建。
三、验证步骤
- 先修改代码,设置缓冲区大小并改用线程池,重新测试看是否解决丢包问题;
- 降低发送速率(如将间隔从10ms改为50ms),若丢包消失,说明是带宽/速率限制导致;
- 在EC2和家庭网络侧分别用
iftop/nload工具监控实时带宽,确认是否达到上限。
内容的提问来源于stack exchange,提问作者YouHoGeon

