Netty优化

常用的参数优化

option()和childOption()

option()用于配置服务端的 ServerChannel,也就是接受客户端连接的Channel,通过 option 设置的配置项将应用到服务端的底层 Socket,影响整个服务器的行为;【全局性的配置】

childOption()用于配置服务端接受到的连接所创建的 Channel,也就是客户端连接到服务器后所产生的 Channel,通过 childOption 设置的配置项只会影响到具体的客户端连接,不会影响到服务端的底层 Socke。

ChannelOption.CONNECT_TIMEOUT_MILLIS

它是用于控制连接建立的超时时间,只影响客户端与服务器建立连接的过程。当客户端尝试连接到服务器时,如果在指定的时间内未能成功建立连接,则会抛出连接超时的异常(ConnectTimeoutException)。

通过设置连接超时时间,可以避免长时间等待连接建立而导致的阻塞,提高系统的响应性和可靠性。过长的连接超时时间可能会导致客户端长时间阻塞在连接建立阶段,影响系统的响应速度;而过短的连接超时时间可能会导致连接失败率的增加,需要根据具体的网络环境进行调整。

ChannelOption.SO_BACKLOG

它是设置服务器端接受连接的队列大小,也就是TCP连接的backlog大小。当服务器正在处理连接请求的时候,新来的连接请求会被放到等待队列backlog中,直到服务器有空闲的资源去处理这些连接请求,当等待连接队列已满时,新的连接请求会被拒绝,抛出AnnotatedConnectException。

如果应用预计有大量的并发连接,或者需要处理大量的连接请求,可以适当增大 SO_BACKLOG 的值,以确保服务器能够及时处理所有的连接请求。例如128、256 。如果你的应用并发连接较少,或者对连接的响应速度要求不高,可以适当减小 SO_BACKLOG 的值,以节省系统资源。例如 32。需要注意的是,SO_BACKLOG 设置得太小可能会导致连接被拒绝或者连接排队等待时间过长,从而影响用户体验。而设置得太大则可能会占用过多的系统资源,导致系统性能下降。Windows默认为200,其他系统默认为128。

ChannelOption.SO_KEEPALIVE

它设置了TCP连接是否开启TCP keep-alive机制。TCP keep-alive机制是一种用来检测连接是否仍然存活的机制,它会定期发送保活探测包给对端,如果对端在一定时间内没有响应,就认为连接已经失效。在网络环境不稳定或者长时间没有数据交互的情况下,TCP keep-alive机制可以帮助及时检测并处理断开的连接,避免资源的浪费。

看到网上有很多说SO_KEEPALIVE是开启长连接的配置,这不完全正确。TCP 长连接通常是指在一个 TCP 连接上保持长时间的持续通信,而不是简单地检测连接的存活状态。实现 TCP 长连接需要应用层协议的支持,例如Netty支持通过心跳机制来维护连接的活跃状态,而 SO_KEEPALIVE 机制只是 TCP 协议的一部分,主要用于检测连接的存活性。SO_KEEPALIVE是TCP层面的,主要用于检测连接是否断开,心跳机制是应用层面的,通常用于检测连接的健康状态,如果TCP因为网络丢包、网络故障等原因导致连接断开,但是还没有到发送保活探测包的时间,对端会认为连接依然正常,对于服务器来说,过多的“假死”连接会造成性能下降和服务崩溃,客户端的话会使得发送给服务器的消息全部超时。

ChannelOption.SO_RCVBUF和ChannelOption.SO_RCVBUF

它是用来设置服务器的发送和接收缓冲区大小的,是一个socket配置,一般来说,默认的缓冲区大小就已经可以满足大多数应用程序的需求了。在连接建立之后,服务器和客户端会根据吞吐量自动调整滑动窗口大小。这是基于TCP的流量控制机制,TCP使用滑动窗口来控制数据流量,确保发送方和接收方之间的通信能够有效进行。滑动窗口大小会根据网络的拥塞情况、带宽和延迟等因素进行自动调整,以优化数据传输的效率。

ChannelOption.TCP_NODELAY

控制是否启用 Nagle 算法,Nagle 算法将小的数据包组合成更大的数据包来减少网络上的负载,但会引入一定的延迟。当启用 Nagle 算法时,如果有未确认的数据包在传输,那么新的数据将被缓冲起来,直到之前的数据被确认或者缓冲区满了才发送。这样可以减少网络上的小数据包数量,提高网络带宽的利用率。禁用 Nagle 算法意味着即使有未确认的数据包,新的数据也会立即发送,不再等待缓冲区满或者之前的数据被确认。这可能会导致大量的小数据包被发送到网络上,增加了网络传输的开销和带宽消耗。在某些场景下,禁用 Nagle 算法可能会提高数据传输的实时性和响应速度,特别是对于需要立即发送数据并且数据量较小的场景。但在其他场景下,如果大量小数据包被发送到网络上,可能会导致网络拥塞和性能下降。

ChannelOption.TCP_FASTOPEN

启用TCP Fast Open机制,TCP Fast Open是一种加速TCP三次握手过程的机制,可以减少连接建立的延迟。在内核版本 3.7 以及更高版本的Linux系统上,TCP Fast Open 默认开启,可以通过 sysctl 或者 /proc/sys/net/ipv4/tcp_fastopen 文件来配置相应的参数,Windows原生不支持 TCP Fast Open。

TCP Fast Open 机制允许在发送 SYN 数据包时携带数据,从而避免了传统的 TCP 三次握手过程中的额外往返时间。这样可以显著减少连接建立的延迟,特别是对于网络延迟较高的情况下,可以加速连接建立过程。并且由于减少了连接建立的延迟,服务器可以更快地处理客户端的请求,从而降低了服务器的负载和响应时间。但是,因为在 TCP 连接建立过程中携带数据可能会被中间人攻击者利用,攻击者可以篡改 SYN 数据包中的数据或者伪造 SYN+ACK 数据包,导致数据泄露或者篡改,也会受到网络设备和操作系统的支持程度的限制,一般在内网环境使用。

ChannelOption.SO_LINGER

用于设置 Socket 关闭时的行为,将 SO_LINGER 设置为一个正整数值,表示在关闭 Socket 时,操作系统会等待指定的时间(以秒为单位),直到发送或接收缓冲区中的数据发送完毕,或者超时后才关闭连接。这时,操作系统会发送一个 RST复位报文给对端,告知对端连接已经被强制关闭。将 SO_LINGER 设置为 false,表示在关闭 Socket 时,不会等待数据发送完毕,而是立即关闭连接。这时,未发送完的数据会被丢弃,不会被发送到对端。启用这个配置可以在一定程度上确保数据的完整性并优雅的关闭连接。

编码技巧

复用EventLoopGroup

我们只需要在启动引导上设置boss和worker两个事件循环组就能达到复用的目的,EventLoopGroup 中的线程是由 Netty 自动管理和调度的,它们会根据系统负载和网络事件的数量来动态调整线程的分配,减少资源的使用和线程的切换。

使用EventLoop的任务调度

如果在handler之外有需要向对端回写消息的需求,建议提前使用Map将Channel缓存起来,在在EventLoop的支持线程外使用channel.eventLoop().execute(),而不是直接使用channel.writeAndFlush()。channel.eventLoop().execute()方法会将任务提交到与该 Channel 相关联的 EventLoop 中执行,如果当前线程就是该 EventLoop 的线程,则任务会直接在当前线程中执行,不会发生线程切换。但如果当前线程不是该 EventLoop 的线程,任务会被添加到 EventLoop 的任务队列中,等待 EventLoop 的线程执行。channel.writeAndFlush()会将数据写入到 Channel 中并进行刷新,Netty 会在底层进行异步的写操作导致线程切换,而减少线程切换的开销是Netty的优化重点。

减少ChannelPipline的调用长度
回写消息时减少ChannelPipline的调用长度

通过channel获取到pipeline的时候,pipeline中已经存在head、tail两个处理器了,后续通过addLast添加的处理器是添加在head、tail这两个处理器中间的。ctx.channel().write(msg) 和 ctx.write(msg) 都是调用出栈处理器,不同的是,ctx.channel().write(msg)是从tail向head方向找出站处理器,ctx.write(msg)是从调用这个方法的处理器开始向head方向找出站处理器的,相比之下调用链更短的是ctx.write(msg)。

对于服务端来说,每次将消息解码后会在多个handler中经过业务处理,并最终在一个handler中完成业务,如果我们将所有的业务相关的handler封装到Map中,每次只让它经过必要的handler,而不是整个ChannelPipline,那么便可以提高Netty处理请求的性能。

使用策略模式减少ChannelPipline的调用长度
// 策略模式
@ChannelHandler.Sharable
public class BizHandlers extends SimpleChannelInboundHandler<Message> {
	
	@Autowired
	private BizOneHandler bizOneHandler;
	
	@Autowired
	private BizTwoHandler bizTwoHandler;
	
	@Autowired
	private BizThreeHandler bizThreeHandler;
	
	private final Map<MessageTypeEnum, SimpleChannelInboundHandler<Message>> handlerMap;
	
	public BizHandlers() {
		
		handlerMap = new HashMap<>();
		handlerMap.put(MessageTypeEnum.BIZ_ONE, bizOneHandler);
		handlerMap.put(MessageTypeEnum.BIZ_TWO, bizTwoHandler);
		handlerMap.put(MessageTypeEnum.BIZ_THREE, bizThreeHandler);
	
	}
	
	// 别的事件也可以这样去处理
	@Override
    protected void channelRead0(ChannelHandlerContext ctx, Message msg) throws Exception {
       
        handlerMap.get(msg.getMessageType()).channelRead(ctx, msg);
    }

}

只需要在Pipline中增加这个Handler,消息流转到这里的时候会根据消息类型找到对应业务Handler去处理,显著缩短调用链。

合并编解码器减少ChannelPipline的调用长度

继承MessageToMessageCodec类是可以单例共享的,需要确保数据流到这里后是完整可靠的,并且处理器是线程安全的,一般配合LTC(LengthFieldBasedFrameDecoder)使用,解决粘包半包的问题。合并之后不需要在业务处理完成后再把数据编码成ByteBuf回写到对端了,缩短了调用链。

LTC核心参数:
    int maxFrameLength:数据的最大长度,数据超过此长度会自动分包
    int lengthFieldOffset:长度字段的偏移量,标识描述数据长度的信息从第N个字节开始(记得N-1,否则报错)
    int lengthFieldLength:长度字段的占位大小,标识数据中使用了N个字节标识长度(int 占4个字节)
    int lengthAdjustment:长度字段的调整数,标识长度字段后面N个字节后才是正文数据
    int initialBytesToStrip:头部剥离字节数,标识先将数据去掉N个字节后,是正文

@ChannelHandler.Sharable
public class MyServerCodec extends MessageToMessageCodec<ByteBuf, Message> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Message msg, List<Object> out) throws Exception {
        ByteBuf buffer = ctx.alloc().buffer();
        // 1.声明魔数,用来在第一时间判定是否是无效数据包,4字节
        buffer.writeBytes("9211".getBytes())
                // 2.声明版本,1字节
                .writeByte(1)
                // 3.声明序列化方式(0,jdk;1,json),1字节
                .writeByte(0)
                // 4.声明指令类型(如,0指登录消息,1指聊天消息),1字节
                .writeByte(msg.getMessageType())
                // 5.声明指令请求序号,4字节
                .writeInt(msg.getSequenceId())
                // 6.声明对齐填充的无意义字段,1字节
                .writeByte(0xff);
        // 7.获取内容的字节数组,序列化后的
        byte[] bytes = MessageSerializableUtils.serializable(msg, 0);
        // 8.写入内容
        buffer.writeInt(bytes.length) // 字节数组的长度 4字节
                .writeBytes(bytes); // 字节数组 会变不可控
        // 9.传给下一个出栈处理器
        out.add(buffer);
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        // 1.魔数 4字节
        int magicNum  = in.readInt();
        // 2.版本 1字节
        byte version = in.readByte();
        // 3.序列化方式 1字节
        byte way = in.readByte();
        // 4.消息类型
        byte type = in.readByte();
        // 5.指令请求序号 4字节
        int sequenceId = in.readInt();
        // 6.无意义的填充字节 1字节
        in.readByte();
        // 7.内容长度 4字节
        int length = in.readInt();
        // 8.读取内容到字节数组并反序列化
        byte[] bytes = new byte[length];
        in.readBytes(bytes, 0, length);
        Message message = MessageSerializableUtils.deserialization(bytes, 0);
        log.debug("魔数:【{}】,版本:【{}】,序列化方式:【{}】,消息类型:【{}】,指令请求序号:【{}】,内容长度:【{}】,消息体:【{}】", magicNum, version, way, type, sequenceId, length, message);
        // 传给下一个入栈处理器
        out.add(message);
	}
}
及时释放ByteBuf防止内存泄漏和资源浪费

对于是直接内存且池化的ByteBuf,使用release()会回归池中,并不是最终释放。

对原始的ByteBuf没做处理,并且调用ctx.fireChannelRead(msg)向后传递,这种无须release,因为ByteBuf可能会被多个ChannelHandler共享,如果在当前 ChannelHandler中释放了ByteBuf,而其他ChannelHandler还需要使用它,可能会出现内存访问异常。

将ByteBuf转换为其他类型的Java对象,且没有将原始的ByteBuf向下传递时,意味着不再需要原始的ByteBuf,此时手动释放可以避免内存泄漏,因为这时候ByteBuf 对象不再被Netty所管理,不会自动释放了。

当不调用ctx.fireChannelRead(msg)向后传递消息时,意味着当前ChannelHandler不再需要这个消息,此时释放消息对应的ByteBuf可以避免内存泄漏。

服务器限制连接数量

如果不对客户端接入数量进行限制,Netty 服务器将尽可能地接受所有连接请求,直到达到操作系统所允许的最大连接数为止。那无论服务器性能怎么优化,大量的连接都会将服务器的资源消耗殆尽,导致服务器性能下降,甚至崩溃。为了保证服务器能够正常运行并提供良好的性能,必需要对客户端接入数量进行合理的限制,如果达到限制就拒绝后续连接。因为这个handler是处理连接的,可以利用channelActive()事件,在事件中将当前数量+1后和总连接数比对,超过的话则拒绝,在channelInactive()事件中将总连接数-1。

  • 22
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值