因为mqtt也是一种适合于物联网的mq,简单来说是比较轻量级的mq,对于消息的tps等没有像传统mq那么高,但是要求的是多终端,其中也涉及了很多多线程地方的处理。这里总结和记录一下。
一、netty的多线程处理与设置
在netty中,有两个EventLoopGroup,维护了两个线程组,如果不设置初始化的大小,那么默认初始化的线程数的大小为:系统内核数*2,如果现在主机是8核,那么初始化的大小为16个线程,这两个线程组可以看成一个是boss线程组,一个是worker线程组,其中boss线程组的作用是:接收连接,并将连接的io操作注册给worker进行处理,而worker线程组主要是进行io处理,那么:
对于boss线程组,只是用来接收连接并注册io时间给worker线程组,所以完全不需要接收那么大的线程组,一般设置为1到3个线程就可以了(例如RocketMQ设置为1个),对于mqtt这种多连接的,可能要更大一点。
对于worker线程组,一般用来处理io操作,在Netty中,一般只用来处理心跳,编解码,而真正的业务逻辑最好交给业务逻辑线程去做,因为业务逻辑的处理耗时一般较长,如果此时worker线程组阻塞(相当于io操作阻塞),就会导致后续没法再处理进来的请求,导致错误。一般来说,该线程组也不用太大,如果业务逻辑处理时间很少,可以完全交给该线程组来处理,这样可以减少线程的上下文切换,同时该值需要设置的稍微大一点,如果定义了业务逻辑线程进行处理。那么该线程组设置为3-8个线程就可以了,例如RocketMQ设置的为3个,同时RocketMQ将所有业务(包括心跳,事件管理,编解码,具体的业务)都交给了业务逻辑线程处理。
这里的业务逻辑线程指DefaultEventExecutorGroup,该线程组是netty默认的一个提供来处理业务逻辑的线程组,我们自己定义即可,不过在处理具体的请求时,最好用我们自己定义的线程池,这样可以方便维护线程池队列大小和后序的处理等。也防止因某些请求耗时过长导致其余业务不能进行即使的处理。
二、系统中线程数量的规划
在开发Mqtt中,在处理连接,处理订阅,处理心跳,处理发布都有自己的线程池,同时netty中也有自己的线程大小,还有一些定时任务线程,以及一些细节部分功能的异步线程,对于主机来说,线程设置太小,对cpu不能充分利用,线程数太多又会占用太多的栈空间。先看一下《Java并发编程实战》中给的一个线程池大小设置的公式:
线程池线程数=CPU核心数 * CPU利用率 * (1 + 线程等待时间/线程CPU时间)
一般来说,CPU利用率为75%比较好,具体的业务处理还是设置具体的线程数比较好,例如,对于mqtt这种系统,处理连接,发布消息比较多,那么相应的线程数可以设置的大一些,对于订阅和心跳等可以设置的相对较小。
三、系统中的线程都要命名
在系统中使用到的线程都要进行命名,这样,在进行性能优化处理和观察线程堆栈时,可以观察是那条线程或线程组耗时很长,同时,打印日志时最好也打印出线程名字。
四、耗时的业务异步处理
这里的耗时的任务如果不影响整个流程,那么最好进行异步处理,例如:在mqtt的处理连接的请求中,有一个业务是需要对重连的业务,去重发离线消息,未确认的消息等,这里需要从持久层获取数据再进行分发,针对大量连接的请求时,如果依然在一个方法中去处理,会导致连接的处理很慢,甚至让线程池的任务队列溢出。但是该业务处理跟连接是否成功并不是强相关,所以直接开异步线程进行处理就可以了。
五、用CAS代替传统的同步
传统的同步指的是synchronized与Locks,加锁的方式可以看成是悲观锁,而CAS相对来说是无锁的方式,在系统中使用CAS来替代传统的同步可以减少锁的竞争来提高性能,例如在mqtt的处理订阅时对订阅树节点的替换,一个节点可以看成是维护了该节点下的所有订阅关系,在新的订阅请求进来时(订阅请求是并发的),需要更新节点为新的节点,就是将父节点的指向的一个子节点,这里如果对父节点加锁再更新为新的子节点,相较于用CMS替换为新的节点要慢很多。对于对象用:AtomicReference 即可。例如在生成mqtt的消息id时,针对同一个客户端的qos1和qos2消息,在消息分发时候服务端需要生成消息id用于标识消息,该消息id是小于65535的正数,那么用AtomicInteger的而不是用Integer来同步要很多。
总结
线程池大小设置要合理并且一定要自己定义线程名字(有意义)
业务不是强相关的进行异步处理(可以提高响应速度)
最好不要加锁,实在不行最好看能不能用其它方式替换,同步类,同步集合或工具用JUC下面的包即可。
参考;
《Java并发编程实战》
http://ifeve.com/how-to-calculate-threadpool-size/