You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

AWS EC2大UDP数据包收发异常:仅前100次正常,后续丢包

UDP大数据包传输丢包问题排查与解决方案

问题背景

我用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();
        }
    }

}

问题分析与解决方案

一、代码层面的潜在问题

  1. UDP套接字接收缓冲区溢出
    客户端使用单个DatagramSocket处理所有发送和接收,UDP套接字默认接收缓冲区大小有限(通常几十KB到几百KB)。40KB数据包的回包会快速占满缓冲区,后续数据包直接被丢弃,无法触发receive()方法。
    修复:在客户端和服务器创建DatagramSocket后,手动设置更大的接收缓冲区:

    socket.setReceiveBufferSize(1024 * 1024 * 5); // 设置为5MB
    
  2. 线程资源耗尽
    每次发送都新建线程执行receive(),1000次发送会创建1000个线程,大量线程阻塞在receive()上会耗尽系统资源,导致后续线程无法正常运行。
    修复:改用线程池复用线程,替换new Thread()为线程池提交任务:

    // 在UDPSender构造方法中初始化线程池
    ExecutorService executor = Executors.newFixedThreadPool(10);
    // 在send方法中使用线程池
    executor.submit(() -> { /* 原线程逻辑 */ });
    
  3. IP分片丢失
    40KB数据包远超以太网MTU(通常1500字节),会被IP层拆分为27个左右的分片。只要任一分片丢失,整个UDP包就会被丢弃,小数据包分片数少,丢失概率低。
    修复:添加重传机制,对未收到回包的请求进行重试;或限制UDP包大小在MTU范围内(建议不超过1472字节,留足IP和UDP头部空间)。

二、AWS或网络层面的限制

  1. AWS EC2带宽/速率限制
    40KB包按10ms间隔发送,带宽需求约320Mbps,若EC2实例带宽不足以支撑,AWS会触发丢包。可通过CloudWatch监控实例的网络带宽使用情况,确认是否达到上限。此外,AWS对UDP大流量可能存在隐性速率限制。

  2. 家庭网络瓶颈
    家庭宽带上传带宽通常较低,320Mbps的需求远超多数家庭宽带的上传能力,会导致上传队列溢出丢包。同时,家庭NAT设备的会话表容量有限,大量UDP会话快速建立会占满NAT表,导致后续会话无法创建。

三、验证步骤

  1. 先修改代码,设置缓冲区大小并改用线程池,重新测试看是否解决丢包问题;
  2. 降低发送速率(如将间隔从10ms改为50ms),若丢包消失,说明是带宽/速率限制导致;
  3. 在EC2和家庭网络侧分别用iftop/nload工具监控实时带宽,确认是否达到上限。

内容的提问来源于stack exchange,提问作者YouHoGeon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 04:52:51