Aeron性能异常:90分位延迟<5ms但吞吐量仅2k,求排查优化方案
Aeron往返测试性能低于预期的问题分析与优化方案
我基于Aeron实现了单客户端、单服务端的往返延迟测试:客户端发送数据至服务端,服务端返回后统计耗时,但最终结果远低于预期。以下是问题代码、配置及监控数据,以及对应的优化建议:
客户端发布代码
// client publication @Scheduled(fixedRate = 1000) private void start() { if (!publishTaskEnable) { return; } ConcurrentPublication publication = aeron.addPublication( aeronProperties.getClient().getPublication().getChannel(), aeronProperties.getClient().getPublication().getStream()); while (!publication.isConnected()) { try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } } AtomicLong counter = new AtomicLong(0); LongStream.range(0, aeronProperties.getRound()).forEach(i -> publish(publication, counter)); } private void publish(Publication publication, AtomicLong counter) { UnsafeBuffer buffer = new UnsafeBuffer(BufferUtil.allocateDirectAligned(16, 8)); long count = counter.incrementAndGet(); buffer.putLong(0, count); publication.offer(buffer, 0, 16); startTimeMap.put(count, System.currentTimeMillis()); }
客户端订阅代码
@SneakyThrows public void receive(String channel, Integer stream) { clientPublication.setAeron(Aeron.connect(new Aeron.Context() .aeronDirectoryName(aeronProperties.getDirname()) .idleStrategy(new NoOpIdleStrategy()))); try (Subscription subscription = clientPublication.getAeron().addSubscription(channel, stream)) { IdleStrategy idleStrategy = new BusySpinIdleStrategy(); FragmentHandler fragmentHandler = this::onMessage; while (true) { int fragmentsRead = subscription.poll(fragmentHandler, 1000); idleStrategy.idle(fragmentsRead); } } } @SneakyThrows public void onMessage(DirectBuffer buffer, int offset, int length, Header header) { long data = buffer.getLong(offset); Long startTime = clientPublication.getStartTimeMap().get(data); if (startTime == null) { return; } long interval = System.currentTimeMillis() - startTime; clientPublication.getTimeList().add(interval); clientPublication.getStartTimeMap().remove(data); }
Media Driver启动命令
java --add-opens=java.base/sun.nio.ch=ALL-UNNAMED \ -XX:+UseBiasedLocking \ -XX:BiasedLockingStartupDelay=0 \ -XX:+UnlockExperimentalVMOptions \ -XX:+UseParallelGC \ -XX:+TrustFinalNonStaticFields \ -XX:+UnlockDiagnosticVMOptions \ -XX:GuaranteedSafepointInterval=300000 \ -Djava.net.preferIPv4Stack=true \ -Daeron.dir=$AERON_DIR \ -Daeron.threading.mode=DEDICATED \ -Daeron.conductor.idle.strategy=org.agrona.concurrent.BusySpinIdleStrategy \ -Daeron.sender.idle.strategy=org.agrona.concurrent.NoOpIdleStrategy \ -Daeron.receiver.idle.strategy=org.agrona.concurrent.NoOpIdleStrategy \ -Daeron.term.buffer.length=16M \ -Daeron.socket.so_sndbuf=2m \ -Daeron.socket.so_rcvbuf=2m \ -Daeron.rcv.initial.window.length=2m \ -Dagrona.disable.bounds.checks=true \ -Daeron.pre.touch.mapped.memory=true \ -cp aeron-all-1.44.1.jar io.aeron.driver.MediaDriver
监控数据
iostat
Linux 5.15.0-112-generic (VM-0-2-ubuntu) 06/23/2024 _x86_64_ (8 CPU) avg-cpu: %user %nice %system %iowait %steal %idle 20.43 0.01 5.24 0.13 0.00 74.19 Device tps kB/s rqm/s await areq-sz aqu-sz %util loop0 0.02 0.15 0.00 0.98 7.33 0.00 0.00 loop1 0.02 0.15 0.00 1.16 8.05 0.00 0.00 loop2 0.09 0.92 0.00 0.66 10.14 0.00 0.01 loop3 0.02 0.46 0.00 1.04 21.18 0.00 0.00 loop4 0.03 0.47 0.00 1.23 15.56 0.00 0.00 loop5 0.02 0.15 0.00 1.00 8.39 0.00 0.00 loop6 0.81 31.72 0.00 0.22 39.30 0.00 0.27 loop7 0.01 0.01 0.00 0.00 1.36 0.00 0.00 sr0 1.54 54.31 0.00 0.24 35.33 0.00 0.05 vda 15.58 418.32 12.74 1.85 26.85 0.03 1.44
sar -n UDP
Linux 5.15.0-112-generic (VM-0-2-ubuntu) 06/23/2024 _x86_64_ (8 CPU) 02:18:57 PM idgm/s odgm/s noport/s idgmerr/s 02:18:58 PM 7436.00 303.00 0.00 0.00 02:18:59 PM 14266.00 482.00 0.00 0.00 02:19:00 PM 18488.00 151.00 0.00 0.00 02:19:01 PM 13230.00 149.00 0.00 0.00 02:19:02 PM 16710.00 153.00 0.00 0.00 02:19:03 PM 15754.00 173.00 0.00 0.00
pidstat -t -r -p 4517
Linux 5.15.0-112-generic (VM-0-2-ubuntu) 06/23/2024 _x86_64_ (8 CPU) 01:47:56 PM UID TGID TID minflt/s majflt/s VSZ RSS %MEM Command 01:47:56 PM 1000 4517 - 18.98 0.01 661284 111056 0.70 aeronmd 01:47:56 PM 1000 - 4517 18.98 0.01 661284 111056 0.70 |__aeronmd 01:47:56 PM 1000 - 4518 0.00 0.00 661284 111056 0.70 |__aeron_executor 01:47:56 PM 1000 - 4519 0.00 0.00 661284 111056 0.70 |__sender 01:47:56 PM 1000 - 4520 0.00 0.00 661284 111056 0.70 |__receiver
问题分析
资源重复创建:
- 每次定时任务执行都新建
ConcurrentPublication,重复建立连接、初始化资源,带来额外开销。 - 每次
publish都新建UnsafeBuffer,频繁分配直接内存,触发GC并增加分配延迟。
- 每次定时任务执行都新建
消息发送未处理背压:
publication.offer()直接调用未处理返回值,遇到背压(如BACK_PRESSURED)会丢消息,导致统计数据缺失且内存泄漏。
计时精度不足:
- 使用
System.currentTimeMillis()计时,精度仅为毫秒级,无法准确统计Aeron的微秒级延迟。
- 使用
线程安全与资源管理问题:
receive方法重复创建Aeron实例,破坏单例设计,导致资源浪费。- 未明确
startTimeMap和timeList的线程安全实现,多线程操作可能引发异常或数据错乱。
Media Driver配置不合理:
- Sender/Receiver线程使用
NoOpIdleStrategy,低负载时会空转CPU,高负载时无法最优利用资源;应使用低延迟的BusySpinIdleStrategy。 - 16M的Term Buffer对于16字节小消息过大,增加内存占用与缺页概率。
- Sender/Receiver线程使用
优化方案
代码层面优化
复用核心资源:
- 全局初始化
ConcurrentPublication,避免重复创建:private ConcurrentPublication publication; @PostConstruct public void initPublication() { publication = aeron.addPublication( aeronProperties.getClient().getPublication().getChannel(), aeronProperties.getClient().getPublication().getStream()); while (!publication.isConnected()) { try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); } } } - 复用
UnsafeBuffer,避免频繁内存分配:private final UnsafeBuffer sendBuffer = new UnsafeBuffer(BufferUtil.allocateDirectAligned(16, 8)); private void publish(Publication publication, AtomicLong counter) { long count = counter.incrementAndGet(); sendBuffer.putLong(0, count); // 处理背压,重试发送 long result; IdleStrategy idleStrategy = new BusySpinIdleStrategy(); do { result = publication.offer(sendBuffer, 0, 16); if (result > 0) { startTimeMap.put(count, System.nanoTime()); break; } if (result == Publication.NOT_CONNECTED) { throw new IllegalStateException("Publication disconnected"); } idleStrategy.idle(); } while (true); }
- 全局初始化
提升计时精度:
- 改用
System.nanoTime()记录时间,统计时转换为微秒:// 记录发送时间 startTimeMap.put(count, System.nanoTime()); // 计算耗时(微秒) long interval = (System.nanoTime() - startTime) / 1000; timeList.add(interval);
- 改用
线程安全与单例优化:
- 使用
ConcurrentHashMap和ConcurrentLinkedQueue保证线程安全:private final ConcurrentHashMap<Long, Long> startTimeMap = new ConcurrentHashMap<>(); private final ConcurrentLinkedQueue<Long> timeList = new ConcurrentLinkedQueue<>(); - 全局单例
Aeron实例,避免重复创建:private final Aeron aeron = Aeron.connect(new Aeron.Context() .aeronDirectoryName(aeronProperties.getDirname()) .idleStrategy(new NoOpIdleStrategy())); public void receive(String channel, Integer stream) { try (Subscription subscription = aeron.addSubscription(channel, stream)) { IdleStrategy idleStrategy = new BusySpinIdleStrategy(); FragmentHandler fragmentHandler = this::onMessage; while (!Thread.currentThread().isInterrupted()) { int fragmentsRead = subscription.poll(fragmentHandler, 1000); idleStrategy.idle(fragmentsRead); } } }
- 使用
Media Driver配置优化
调整Idle策略:
将Sender/Receiver线程改为BusySpinIdleStrategy以降低延迟:-Daeron.sender.idle.strategy=org.agrona.concurrent.BusySpinIdleStrategy \ -Daeron.receiver.idle.strategy=org.agrona.concurrent.BusySpinIdleStrategy \减小Term Buffer大小:
针对小消息场景,将Term Buffer调整为1M:-Daeron.term.buffer.length=1M \优化GC参数:
改用G1GC降低GC停顿对延迟的影响:-XX:+UseG1GC \ -XX:MaxGCPauseMillis=10 \
系统层面优化
绑定CPU核心:
将Media Driver和客户端线程绑定到固定CPU核心,减少上下文切换:# Media Driver绑定核心0-2 taskset -c 0-2 java ... io.aeron.driver.MediaDriver确认Socket Buffer生效:
检查系统参数确保Socket Buffer配置生效:sysctl -w net.core.wmem_max=2097152 sysctl -w net.core.rmem_max=2097152
内容的提问来源于stack exchange,提问作者litchi
相关产品推荐
相关产品推荐

