想要支持海量的客户端请求,首先要有一套高效的请求处理模型。本文以开源项目SONA为例,详解如何基于netty设计请求处理模型,帮助读者动手实践。本文最后附上开源项目地址。
背景
Sona 平台是一个搭建语音房产品的全端解决方案,包含了房间管理、实时音视频、房间IM、长连接网关等能力。其中最基础核心的就是长连接网关,作为网关,首先要支持处理海量的客户端请求,要做到这一点,需要设计一套稳定高效的请求处理模型。
一、Netty 处理模型
Netty 使用 主从Reactor模型
bossGroup 负责处理 accept 事件 , workerGroup 负责处理 read 、write 事件。
其中 ChannelPipeline 是一个双向链表,netty 里面定义了十几种事件,触发之后会顺序调用所有 ChannelHandler 的指定方法,ChannelHandler的调用都是由 workerGroup中的同一个 eventloop 线程执行,不存在线程之间的切换。
这种无锁化的设计,避免了上下文切换,在海量请求的情况下能提供很高的性能。但是也存在风险,如果执行某个 ChannelHandler 出现了阻塞,会拖累这个 eventloop 线程所负责的其他请求。
在实际场景中,ChannelHandler里面一般都会执行一些 IO 操作,比如RPC调用,MQ等,无法保证不会出现阻塞的情况。所以很多高性能的分布式框架中都会使用三层处理模型,额外增加一个业务线程池,将耗时的IO操作放在这个业务线程池里面执行,这样就不会阻塞 workerGroup 中的线程了。
二、sona-gateway 处理模型
1、NettyServerHandler
pipeline 中除了 encode 和 decode ,只实现了一个 NettyServerHandler ,避免过多的handler 影响执行效率。
NettyServerHandler 里面使用装饰器模式实现了不同分工的 handler
AccessChannelHandler |
基于 IP 的权限校验,不符合的连接直接拒绝 |
CatReportChannelHandler |
cat 数据打点上报 |
IdleChannelHandler |
心跳和探测消息、握手等任务处理 |
DispatchChannelHandler |
线程池分发请求 |
MercuryServerHandler |
最上层的业务数据处理 handler |
在上面也提到过,netty 里面的 ChannelHandler 有十几种事件,但对于我们来说,其实只需要关注其中的几种事件。因此,在NettyServerHandler中做了一层映射
channelActive |
connect |
建立连接 |
channelInactive |
disconnect |
断开连接 |
channelRead |
receive |
读取到数据 |
write |
send |
写入数据 |
exceptionCaught |
caught |
异常捕获 |
NettyChannel 是我对 netty 底层 channel 的封装,业务层直接操作 NettyChannel,无需关心底层channel的处理,屏蔽了netty的使用细节。里面实现了对连接的维护管理以及消息发送的优化
2、MercuryServerHandler
MercuryServerHandler 中会根据请求中的 Command 命令,将当前请求路由到对应的业务 handler 中处理,并处理 request-response 模型。
开源版本只提供了登录、房间相关的处理器,详细可见github 的wiki 文档:
https://github.com/BixinTech/sona/wiki/%E9%95%BF%E8%BF%9E%E6%8E%A5%E7%BD%91%E5%85%B3#%E9%95%BF%E9%93%BE-command-%E8%AF%B7%E6%B1%82
用户可以基于gateway中提供的扩展,自定义请求处理器,实现自己的业务。
这里着重介绍一下 DispatchChannelHandler ,前面说了这个 handler 是用于线程池分发请求的。
一般情况下,其实用 Jdk里面提供的线程池就可以了,比如说 Dubbo ,通过 spi 的方式提供了4种线程池,服务端默认使用 fixed ,客户端默认使用 cached。但底层还是直接使用的ThreadPoolExecutor
对于 IM 场景下,需要严格保证单个 channel 的处理顺序,但不同channel 之间可以不用保证顺序。
所以这里我是自己实现了一个 线程池 OrderedChannelExecutor
-
每个channel 都会生成一个 channelId (这里并没有使用 Netty里面默认的 id,而是通过一定规则生成的,主要是因为某些业务处理上的需要)
-
通过对 channelId 做一致性哈希,将这个channel 中的所有请求都路由到同一个 MpscQueue 里面。(MpscQueue 是 JCTools里面提供的 多生产者单消费者模式的 lock free 队列,能保证并发安全并且性能极高,Netty 底层使用的队列就是这个)
-
MpscQueue 会从线程池里挑选一个线程去执行任务,只有当前任务处理完成,才会再从队列里 poll 下一个任务执行
-
只要MpscQueue 里面有数据,就会一直霸占这个线程,等队列的任务都执行完了,才会将它放回线程池中
目前 MpscQueue 和线程数量是 1:1 ,每个队列都能拿到一个线程,不需要任何等待
设计的时候,没有参考 netty eventloop 那样将 Selector 和线程 强绑定,因为我觉得线程是比较珍贵的资源,生产机器的配置是4核8g,线程多了性能也不一定能提升,而队列相对来说还好,只是耗点内存,项目中在节省内存方面也做了很多优化,大量使用池化技术,8g内存完全足够了,所以队列数量是可以大于线程数量的。
当时想着后面实现一个分级队列,支持队列按照优先级划分,优先级越高则有更高的概率优先执行,优先级低的在系统负载过大时,则允许延迟处理、丢弃或者快速失败 。不过之前压测,单机可以支持的 qps 远超预期,目前业务上还远远没有达到这种量级,就暂时搁置了。
总结
本文详细介绍了SONA长连接网关中的请求处理模型,在后续的系列文章中会对网关中的其他技术细节进行详细的介绍。
目前sona已经在比心的github仓库上开源,仓库地址:
GitHub - BixinTech/sona: Sona 平台是一个搭建语音房产品的全端解决方案,包含了房间管理、实时音视频、房间IM、长连接网关等能力。Sona 平台是一个搭建语音房产品的全端解决方案,包含了房间管理、实时音视频、房间IM、长连接网关等能力。 - GitHub - BixinTech/sona: Sona 平台是一个搭建语音房产品的全端解决方案,包含了房间管理、实时音视频、房间IM、长连接网关等能力。https://github.com/BixinTech/sona
欢迎你访问我们的项目,有任何想交流的想法可以留言联系我们。
往期阅读:
从0到1快速了解netty长连接网关协议_聊天室程序猿的博客-CSDN博客
比心聊天室的架构演进_聊天室程序猿的博客-CSDN博客