SOFA RPC "maybe write overflow!" 异常

使用 SOFA RPC 的 Bolt 协议在高并发、大数据量传输的场景中,有可能会遇到 “Maybe Write Overflow!” 这一异常。本文将分析该异常的成因,并提供可能的解决方案。

问题

线上环境的日志中,发现有 maybe write overflow! 的错误日志记录,日志内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2024-10-30 10:08:27.623 ERROR 13195 --- [http-nio-8080-exec-19] x.y.web.config.CustomExceptionHandler    : com.alipay.remoting.exception.RemotingException: Check connection failed for address: Origin url [bolt://192.168.88.141:12201?serialization=hessian2], Unique key [192.168.88.141:12201]., maybe write overflow!

com.alipay.sofa.rpc.core.exception.SofaRpcException: com.alipay.remoting.exception.RemotingException: Check connection failed for address: Origin url [bolt://192.168.88.141:12201?serialization=hessian2], Unique key [192.168.88.141:12201]., maybe write overflow!
at com.alipay.sofa.rpc.transport.bolt.BoltClientTransport.convertToRpcException(BoltClientTransport.java:363) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
at com.alipay.sofa.rpc.transport.bolt.BoltClientTransport.syncSend(BoltClientTransport.java:255) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
at com.alipay.sofa.rpc.client.AbstractCluster.doSendMsg(AbstractCluster.java:613) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
at com.alipay.sofa.rpc.client.AbstractCluster.sendMsg(AbstractCluster.java:584) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
at com.alipay.sofa.rpc.filter.ConsumerInvoker.invoke(ConsumerInvoker.java:63) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
at com.alipay.sofa.rpc.filter.ConsumerCustomHeaderFilter.invoke(ConsumerCustomHeaderFilter.java:47) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
at com.alipay.sofa.rpc.filter.FilterInvoker.invoke(FilterInvoker.java:100) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
at com.alipay.sofa.rpc.filter.PressureMarkTransformFilter.invoke(PressureMarkTransformFilter.java:63) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
at com.alipay.sofa.rpc.filter.FilterInvoker.invoke(FilterInvoker.java:100) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
at com.alipay.sofa.rpc.filter.sofatracer.ConsumerTracerFilter.invoke(ConsumerTracerFilter.java:66) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
at com.alipay.sofa.rpc.filter.FilterInvoker.invoke(FilterInvoker.java:100) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
at com.alipay.sofa.rpc.filter.RpcReferenceContextFilter.invoke(RpcReferenceContextFilter.java:80) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
at com.alipay.sofa.rpc.filter.FilterInvoker.invoke(FilterInvoker.java:100) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
at com.alipay.sofa.rpc.filter.ConsumerExceptionFilter.invoke(ConsumerExceptionFilter.java:37) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
at com.alipay.sofa.rpc.filter.FilterInvoker.invoke(FilterInvoker.java:100) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
at com.alipay.sofa.rpc.filter.FilterChain.invoke(FilterChain.java:269) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
at com.alipay.sofa.rpc.client.AbstractCluster.filterChain(AbstractCluster.java:558) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
at com.alipay.sofa.rpc.client.FailoverCluster.doInvoke(FailoverCluster.java:68) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
at com.alipay.sofa.rpc.client.AbstractCluster.invoke(AbstractCluster.java:298) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
at com.alipay.sofa.rpc.client.ClientProxyInvoker.invoke(ClientProxyInvoker.java:83) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
...
Caused by: com.alipay.remoting.exception.RemotingException: Check connection failed for address: Origin url [bolt://192.168.88.141:12201?serialization=hessian2], Unique key [192.168.88.141:12201]., maybe write overflow!
at com.alipay.remoting.DefaultConnectionManager.check(DefaultConnectionManager.java:411) ~[bolt-1.5.10.jar:na]
at com.alipay.remoting.rpc.RpcClientRemoting.invokeSync(RpcClientRemoting.java:63) ~[bolt-1.5.10.jar:na]
at com.alipay.remoting.rpc.RpcClient.invokeSync(RpcClient.java:355) ~[bolt-1.5.10.jar:na]
at com.alipay.sofa.rpc.transport.bolt.BoltClientTransport.doInvokeSync(BoltClientTransport.java:279) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
at com.alipay.sofa.rpc.transport.bolt.BoltClientTransport.syncSend(BoltClientTransport.java:252) ~[sofa-rpc-all-5.8.3.jar:5.8.3]
... 84 common frames omitted

分析

问题分析

经搜索,发现在官方 Repo 中有对该异常的提问以及开发人员的解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
原因

RPC在写出数据的时候,会检测当前的ChannelOutboundBuffer的大小,如果超过了WRITE_BUFFER_HIGH_WATER_MARK(默认64K),就会限流,报write overflow的异常。
这个是为了保护客户端,防止无界的ChannelOutboundBuffer被打爆,导致资源耗尽。
通常情况下,网络有问题,导致写出失败;或者下游压力太大,不能处理这么多网络包的时候会出现这样的异常。

解决办法:

建议首先排查网络环境(网卡打满也是一种)
网络环境没问题的情况下,看下是否是流量过大,导致下游处理不过来。,如果是这种,可能意味着,这就是压测的瓶颈了。可以评估下游扩容,性能优化等方式来解决。
如果确实认为64k 也不够用. 可以自行调整

-Dbolt.netty.buffer.low.watermark
-Dbolt.netty.buffer.high.watermark
值自己算一下,默认是32*1024 和64*1024

Issue #551 · sofastack/sofa-rpc · GitHub

以及遇到相似问题的文章 修复 netty 高水位溢出问题记录,经过对这些信息的收集与分析,在排除网络这一条件后,可知当参数大小与并发量达到一定程度后,会触发这个限流机制并抛出该异常,限流机制的目的是为了保护客户端,防止资源耗尽。

分析仅针对代码层面,网络因素需排查操作系统连接数设置,操作系统网络缓冲区设置,I/O,网络硬件,防火墙设置等等。特别是防火墙某些配置可能会影响连接传输速率或稳定性(断连后又恢复连接)。

源码分析

异常抛出代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// com.alipay.remoting.rpc.RpcClientRemoting#invokeSync
public Object invokeSync(Url url, Object request, InvokeContext invokeContext, int timeoutMillis) throws RemotingException, InterruptedException {
Connection conn = this.getConnectionAndInitInvokeContext(url, invokeContext); // 获取连接
this.connectionManager.check(conn); // 检查连接是否可用
return this.invokeSync(conn, request, invokeContext, timeoutMillis); // 调用服务
}

// com.alipay.remoting.DefaultConnectionManager#check
public void check(Connection connection) throws RemotingException {
if (connection == null) {
throw new RemotingException("Connection is null when do check!");
} else if (connection.getChannel() != null && connection.getChannel().isActive()) {
if (!connection.getChannel().isWritable()) {
throw new RemotingException("Check connection failed for address: " + connection.getUrl() + ", maybe write overflow!"); // 异常抛出行
}
} else {
this.remove(connection);
throw new RemotingException("Check connection failed for address: " + connection.getUrl());
}
}

经对源码分析,该异常只会在客户端调用服务端前,对连接是否可用做判断时,如连接(Connection 对象)存在,且连接处于活跃状态(isActive()),且发送缓冲区的已写入大小超过了高水位标记时,连接会被标记为不可写(isWritable()),继而抛出该异常。

com.alipay.remoting.Connection 是 SOFA RPC 的实现,引用的 io.netty.channel.Channel 是 Netty 的实现。

Connection 只会持有一个 ChannelChannel 不可写即 Connection 不可写。

可写性由以下因素判断:

  1. 当 Channel 的发送缓冲区未被填满时,Channel 可写。
  2. 当 Channel 的发送缓冲区大小大于高水位标记时,Channel 不可写。
  3. 当 Channel 的发送缓冲区大小小于低水位标记时,Channel 可写。

Netty 的水位机制主要通过定义高水位(high watermark)和低水位(low watermark)两种阈值来控制数据的流入和流出:

  • 高水位(High Watermark):当待发送的数据积累超过此值时,Netty 会暂停写操作,防止数据进一步的进入发送缓冲区。这相当于阻止“水位”继续升高。
  • 低水位(Low Watermark):当数据积压低于此值时,重新开启写操作,允许数据继续流动。

通过这两个“水位线”,避免发送队列过度积压,防止内存溢出,保障系统的正常运行。

又因为 SOFA RPC 对 Bolt 协议的通信实现是单一长连接。即,客户端启动与服务端建立连接后,就会将连接放在连接池中,连接池以地址(IP + Port)为键。调用时,根据服务端的地址获取连接,调用同一服务端的,都共享使用同一个连接用于发送数据,也即共享发送缓冲区。

其发送数据基本流程为:

  1. 请求消息数据序列化。
  2. 调用 write 方法写入发送缓冲区。
  3. 调用 flush 方法将发送缓冲区的数据发送出去。(flush 操作后,数据将被放入操作系统提供的底层 Socket 发送缓冲区,由操作系统决定何时发送。)

持续的请求同一服务端会将数据不断写入同一发送缓冲区,发送缓冲区的数据未及时通过网络发出,触发高水位限制,后续请求即会触发该异常,并中断请求。

分析总结

Netty 的可写性机制本身不会做任何卡控,即其发送缓冲区(io.netty.channel.ChannelOutboundBuffer)是近乎无界的。当发送缓冲区的数据量达到高水位时,仅只是将可写状态设为 false,实际产生该异常是 SOFA RPC 出于保护客户端的目的启用了可写性机制,保护发送端(客户端)不会因发送过多数据导致内存溢出或网络拥堵。

Netty 默认高水位为 64kb,低水位 32kb,SOFA RPC 沿用该默认值。

由此可知,产生该异常主要是由于发送端(客户端)消息堆积,超过默认高水位 64kb 的限制,造成消息堆积的原因主要有:

  1. 发送方发送数据流量过大,即请求过多、数据过大,直接触发了高水位限制。
  2. 接收方处理能力不够,发送方发送的数据,不能及时被接收方处理,导致数据在操作系统网络传输层面被阻塞,数据在缓冲区中等待发送,间接导致发送方不能及时将数据发出,进而触发了高水位限制。

解决方案

问题排查

  1. 日志分析:检查与 SOFA RPC 相关的日志,定位错误发生的上下文,关注高并发或数据量大的操作,避免不必要的并发与巨大数据传输。
  2. 系统性能检测:评估系统的 CPU、内存、I/O 等资源使用情况,确保硬件资源没有成为瓶颈。
  3. 网络状况监控:评估网络连接、传输等方面的配置,确保没有网络设备故障,比如防火墙设置、服务器网卡堵塞等问题。
  4. 线程状态检查:通过监测线程池和 GC 活动,识别是否因线程阻塞或 GC 导致服务暂停处理。

配置优化

  1. 适度调整客户端水位参数:通过配置 -Dbolt.netty.buffer.low.watermark-Dbolt.netty.buffer.high.watermark 来调整 Netty 的低水位和高水位,避免过早触发限流。

    1
    2
    # 单位:Byte, 65536Bytes = 64KB * 1024, 131072Bytes = 128KB * 1024
    -Dbolt.netty.buffer.low.watermark=65536 -Dbolt.netty.buffer.high.watermark=131072

    需根据业务实际情况调整,避免高水位调整过大带来的资源耗尽风险,调整水位后,应跟进观察 CPU 和内存占用情况。

  2. 优化服务端线程池设置:根据系统特性设置合理的线程池参数,避免请求过载。

    1
    2
    com.alipay.sofa.rpc.bolt.thread.pool.core.size=20
    com.alipay.sofa.rpc.bolt.thread.pool.max.size=200

    需根据业务实际情况调整,避免调整过小导致业务无法处理,调整过大带来的资源耗尽风险,调整线程数后,应跟进观察 CPU 和内存占用情况。

代码优化

  1. 减少客户端单次请求数据量:需进行分页或分批处理,避免长时间占用连接,减少大数据量瞬时写入。
  2. 减少客户端并发请求量:需对频繁的小请求进行合并减少请求次数,避免不合理的持续多次调用服务端。

总结

合理调整客户端水位参数、优化服务端线程池设置以及减少客户端单次请求数据量和并发请求量能有效避免该异常并提高系统性能和稳定性。

参考资料