SONA 是一个由比心语音技术团队开发,用于快速搭建语音房产品的全端解决方案,包含了房间管理、实时音视频、房间IM、长连接网关等能力,支撑了比心聊天室、直播、游戏房等业务。
Sona 平台是一个搭建语音房产品的全端解决方案,包含了房间管理、实时音视频、房间IM、长连接网关等能力。其中最基础核心的就是长连接网关。
对于Java应用来说,内存的分配是由程序完成的,而内存的释放是通过GC完成的,这种方式简化了程序员的工作,但也增加了JVM的压力。有很多Java程序员过分依赖GC,但是无论JVM的垃圾回收机制做得多好,内存总归是有限的资源,因此就算GC会为我们完成了大部分的垃圾回收,但适当地注意编码过程中的内存优化还是非常有必要的。优化内存的主要目的是为了降低 youngGC 的频率、减少 fullGC 的次数 ,过多的 youngGC 和 fullGC 会占用比较多的系统资源(主要是CPU),影响整个系统的吞吐量。
对于网关这样追求高性能的服务,更是有必要去关注内存方面的优化。这样可以有效的减少GC次数,同时提升内存利用率,最大限度地提高程序的效率。
在 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。
在 io.netty.channel.DefaultChannelPipeline#addLast() 添加 ChannelHandler 的方法里面,会有一个 @Sharable 注解使用的检查。如果这个 ChannelHandler 实例已经被添加过了,并且没有标注 @Sharable 注解,就会抛出异常。
Netty 只是做了一个很简单的检查,防止没有@sharable注解的实例被当成单例使用,并没有那么智能。对于 ChannelHandler 实例到底是不是无状态的,它其实是不知道的,这一点需要开发自己确保,否则可能会出现线程不安全的问题。这也是上面提到过的,对于 Netty 里面提供的一些编解码的 ChannelHandler 实例,是绝对不能弄成单例被所有 Channel 共享的。
在一些高并发场景下,很多时候会使用 AtomicXXX 对象保证线程安全,像常用的 AtomicInteger 或者 AtomicLong,底层都是通过 CAS 实现。
但在很多开源框架中会看到 AtomicXXXFieldUpdater 的身影,比如 Netty 中就大量使用了,这么做的目的也是为了节约内存。
这里以 AtomicInteger 和 AtomicIntegerFieldUpdater 为例来说明:
在 AtomicInteger 成员变量只有一个int value
,似乎并没有占用太多内存,但是我们的 AtomicInteger 是一个对象,一个对象的正确计算应该是:
对象头 + 实际数据大小 + 对齐填充
名称(单位byte) | 32位 | 64位 | 开启指针压缩后(指针对64位有效且默认开启) |
---|---|---|---|
对象头(Header) | 8 | 16 | 12 |
数组对象头 | 12 | 24 | 16 |
引用(reference) | 4 | 8 | 4 |
在64位机器上 AtomicInteger 对象占用内存如下:
由于我们的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
欢迎你访问我们的项目,有任何想交流的想法可以留言联系我们。