java网关服务性能提升利器:内存优化

SONA 是一个由比心语音技术团队开发,用于快速搭建语音房产品的全端解决方案,包含了房间管理、实时音视频、房间IM、长连接网关等能力,支撑了比心聊天室、直播、游戏房等业务。


前言

Sona 平台是一个搭建语音房产品的全端解决方案,包含了房间管理、实时音视频、房间IM、长连接网关等能力。其中最基础核心的就是长连接网关。

对于Java应用来说,内存的分配是由程序完成的,而内存的释放是通过GC完成的,这种方式简化了程序员的工作,但也增加了JVM的压力。有很多Java程序员过分依赖GC,但是无论JVM的垃圾回收机制做得多好,内存总归是有限的资源,因此就算GC会为我们完成了大部分的垃圾回收,但适当地注意编码过程中的内存优化还是非常有必要的。优化内存的主要目的是为了降低 youngGC 的频率、减少 fullGC 的次数 ,过多的 youngGC 和 fullGC 会占用比较多的系统资源(主要是CPU),影响整个系统的吞吐量。

对于网关这样追求高性能的服务,更是有必要去关注内存方面的优化。这样可以有效的减少GC次数,同时提升内存利用率,最大限度地提高程序的效率。


一、Netty ChannelHandler

在 Netty 中 每个 Channel 都有自己的 ChannelPipeline,每个 ChannelPipeline里面管理着一系列 ChannelHandler。

当客户端连接到服务器时,Netty 会新建一个 ChannelPipeline 处理其中的事件,而一个ChannelPipeline 中会添加若干个自定义或者Netty提供的 ChannelHandler。如果每来一个客户端连接都去新建一个 ChannelHandler 实例,当有大量连接时,服务器需要保存大量的ChannelHandler 实例。比如,如果建立了十万个连接,就会创建 10w * (添加的 ChannelHandler 的数量)个对象。这将是非常大的内存消耗。

好在 Netty 里面提供了一种方式来解决这个问题, 只要 ChannelHandler 是无状态的(即不需要保存任何状态数据),那么可以将其标注为 @Sharable。这样无论有多少个连接,也只需 new 一个 ChannelHandler 实例,被所有 ChannelPipeline 共享。

所以我们在实现自定义的 ChannelHandler 的时候,最好将其设计成无状态的(有一点需要注意,对于像 ByteToMessageDecoder 之类的编解码器是有状态的,是不能使用 Sharable 注解的)。

而且ChannelPipeline 中添加的那些 ChannelHandler 是以串行方式依次调用的,所以最好也要减少 ChannelHandler 的创建,在 SONA网关里面除了编解码相关的 ChannelHandler ,我只实现了一个无状态的 handler。

二、@Sharable 原理

在 io.netty.channel.DefaultChannelPipeline#addLast() 添加 ChannelHandler 的方法里面,会有一个 @Sharable 注解使用的检查。如果这个 ChannelHandler 实例已经被添加过了,并且没有标注 @Sharable 注解,就会抛出异常。

Netty 只是做了一个很简单的检查,防止没有@sharable注解的实例被当成单例使用,并没有那么智能。对于 ChannelHandler 实例到底是不是无状态的,它其实是不知道的,这一点需要开发自己确保,否则可能会出现线程不安全的问题。这也是上面提到过的,对于 Netty 里面提供的一些编解码的 ChannelHandler 实例,是绝对不能弄成单例被所有 Channel 共享的。

三、AtomicXXXFieldUpdater

在一些高并发场景下,很多时候会使用  AtomicXXX  对象保证线程安全,像常用的 AtomicInteger 或者 AtomicLong,底层都是通过 CAS 实现。

但在很多开源框架中会看到 AtomicXXXFieldUpdater  的身影,比如 Netty 中就大量使用了,这么做的目的也是为了节约内存。

这里以 AtomicIntegerAtomicIntegerFieldUpdater 为例来说明:

AtomicInteger 成员变量只有一个int value,似乎并没有占用太多内存,但是我们的 AtomicInteger 是一个对象,一个对象的正确计算应该是:

对象头 + 实际数据大小 + 对齐填充

名称(单位byte) 32位 64位 开启指针压缩后(指针对64位有效且默认开启)
对象头(Header) 8 16 12
数组对象头 12 24 16
引用(reference) 4 8 4

在64位机器上 AtomicInteger 对象占用内存如下:

  • 关闭指针压缩: 16(对象头)+4(实例数据)=20 不是8的倍数,因此需要对齐填充 16+4+4(padding)=24
  • 开启指针压缩(-XX:+UseCompressedOop): 12+4=16已经是8的倍数了,不需要再padding。

由于我们的AtomicInteger是一个对象,还需要被引用,那么真实的占用为:

关闭指针压缩:24 + 8 = 32

开启指针压缩: 16 + 4 = 20

像在 Netty 中的 AbstractReferenceCountedByteBuf,熟悉 Netty 的同学都知道 Netty 是自己管理内存的,所有的 ByteBuf 都会继承 AbstractReferenceCountedByteBuf,在Netty中ByteBuf会被大量的创建。

如果使用  AtomicInteger , 那么在开启指针压缩的情况下需要占用 :

(ByteBuf的数量)* 20 字节

而  AtomicIntegerFieldUpdater  是配合  volatile int 来使用的,不管有多少对象都只需要创建一个 AtomicIntegerFieldUpdater  即可。

AtomicIntegerFieldUpdater 是16字节,volatile int  是4字节 ,总的占用的内存是:

(ByteBuf的数量)* 4 字节 + 16字节

这个在少量对象的情况下可能不明显,当我们对象有几十万,几百万,或者几千万的时候,节约的可能就是几十M,几百M,甚至几个G。

SONA 网关中需要维护大量的 Channel 连接,并且涉及到很多并发场景,我都使用了 AtomicIntegerFieldUpdater 来优化,在之前的压测过程中,效果非常好。


总结

本文详细介绍了SONA长连接网关中的内存优化,在后续的系列文章中会对网关中的其他技术细节进行详细的介绍。

目前sona已经在比心的github仓库上开源,仓库地址:

GitHub - BixinTech/sona: Sona 平台是一个搭建语音房产品的全端解决方案,包含了房间管理、实时音视频、房间IM、长连接网关等能力。Sona 平台是一个搭建语音房产品的全端解决方案,包含了房间管理、实时音视频、房间IM、长连接网关等能力。 - GitHub - BixinTech/sona: Sona 平台是一个搭建语音房产品的全端解决方案,包含了房间管理、实时音视频、房间IM、长连接网关等能力。https://github.com/BixinTech/sona

欢迎你访问我们的项目,有任何想交流的想法可以留言联系我们。

你可能感兴趣的:(SONA聊天室,后端,java,websocket,实时音视频)