MyCAT线程模型分析
MyCAT线程模型
Mycat线程介绍
Timer
Timer单线程仅仅负责调度,任务的具体动作交给timerExecutor。
TimerExecutor线程池,
默认大小N=2
任务通过timer单线程和timerExecutor线程池共同完成。这个1+N的设计方式比较巧妙!
但是timerExecutor跟aioExecutor大小默认一样,不太合理,定时任务没有那么大的运算量。
NIOConnect主动连接事件分离器
一个线程,负责作为客户端连接MySQL的主动连接事件
Server被动连接事件分离器
一个线程,负责作为服务端接收来自业务系统的连接事件
Manager被动连接事件分离器
一个线程,负责作为服务端接收来自管理系统的连接事件
NIOReactor读写事件分离器
默认个数N=processor size,通道建立连接后处理NIO读写事件。
由于写是采用通道空闲时其它线程直接写,只有写繁忙时才会注册写事件,再由NIOReactor分发。所以NIOReactor主要处理读操作
BusinessExecutor线程池
默认大小N=processor size,任务队列采用的LinkedTransferQueue
所有的NIOReactor把读出的数据交给BusinessExecutor做下一步的业务操作
全局只有一个BusinessExecutor线程池,所有链接通道随机分成多个组,然后每组的多个通道共享一个Reactor,所有的Reactor读取且解码后的数据下一步处理操作,又共享一个BusinessExecutor线程池
一个SQL请求的线程切换
Cobar线程介绍
Timer
Timer单线程仅仅负责调度,任务的具体动作交给timerExecutor。
TimerExecutor线程池,
默认大小N=2
任务通过timer单线程和timerExecutor线程池共同完成。这个1+N的设计方式比较巧妙!
但是timerExecutor跟aioExecutor大小默认一样,不太合理,定时任务没有那么大的运算量。
Server被动连接事件分离器
一个线程,负责作为服务端接收来自业务系统的连接事件
Manager被动连接事件分离器
一个线程,负责作为服务端接收来自管理系统的连接事件
R读写事件分离器
客户端与Server连接后,由R线程负责读写事件(写事件大部分有W线程负责,只有在网络繁忙时才会由小部分写事件是由R线程完成的)。
Handler和Executor线程池
R线程接收到读事件后解码出一个完整的MySQL协议包,下一步由Handler线程池进行SQL解析、路由计算。然后执行任务从Handler线程池转移到Executor线程池,以阻塞方式发送给后端MySQL Server。Executor收到MySQL Server应答后,会由最后一个Executor线程进行聚合,然后交给W线程
W线程
W线程不停遍历LinkedBlockingQueue检查是否有写任务,若有则写入Socket Channel。当Channel繁忙时,W线程会注册OP_WRITE事件,通过R线程进行候补写操作。
ManageExecutor线程池
Cobar对来自Manager的请求和来自Server的请求做了分离,来自管理系统的请求,专门由ManageExecutor线程池处理。
InitExecutor线程池
用来进行后端链路初始化。
Cobar为什么那么多个线程池?
可以发现Cobar有下面这么多个线程池
- TimerExecutor线程池(一个)
- InitExecutor线程池(一个)
- ManageExecutor线程池(一个)
- Handler线程池(N个)
- Executor线程池(N个)
注意上面的个数单位是线程池,不是线程!所以看起来有些眼花缭乱吧?
我不是Cobar的原作者,只能猜测最什么设计这么多线程池?那就是因为后端采用了BIO!
因为后端BIO,所以每一个请求到后端查询,都要阻塞一个线程,前端NIO(Reactor-R线程)必须要把执行任务交给Executor线程池。
由于存在聚合要求,前端NIO的一个SQL请求可能会对应多个后端请求,所以不只要阻塞一个Executor线程。为此增加了Handler做中间SQL解析、路由计算,路由计算完毕后再交给Executor执行
由于后端是阻塞方式,在时,会导致Executor无空闲线程,为了避免管理端口输入名命令无任何响应的现象,为此增加一个ManageExecutor线程池,专门处理ManageExecutor线程
在后端BIO时,除了读写是阻塞方式外,链路建立过程也是阻塞方式,若同时链路建立请求多,也会阻塞大量线程。为避免业务、管理的相互干扰,为此增加了一个InitExecutor线程池专门做后端链路建立
所以如果后端BIO改为NIO,并优化逻辑执行过程,避免线程sleep或长时间阻塞,尽量通过Reactor直接计算,就可以大大降低线程上下文切换的损耗,上述各眼花缭乱的线程池就可以合并为一个业务线程池。
一个SQL请求的线程切换
下面是一个SQL请求执行过程的线程切换,可以看到Cobar的线程上下文切换还是比较多的
MyCAT与Cobar的比较
MyCAT比Cobar减少了线程切换
Cobar的后端采用BIO通信,后端读与后端写因为线程阻塞了,不存在线程切换,没有可比性,所以我们只比较NIO和业务逻辑部分。
Cobar的线程模型中存在着大量的上下文切换,MyCAT的线程调度尽量减少了线程间的切换,以写为例
Cobar是业务线程先把写请求交给专门的W线程,W线程再写过程中发现通道繁忙时再交给R线程;MyCAT对写的做法是业务线程发现通道空闲直接写,只有在通道繁忙时再交给Reactor线程。
减少线程切换与业务可能停顿的矛盾
MyCAT几乎已经达到了线程简化的最高境界,有一个看似可行的方法:可以配置多个NIOReactor,尽可能所有读、解码、业务处理都在Reactor线程中完成,而不必把任务交给BusinessExecutor线程池,从而减少线程的上下文切换,提高处理效率。
但是,不管配置几个Reactor,还是要求多个通道共享一个Reactor,(为什么?因为Reactor最多十几个、几十个,并发的链接通道可能上万个!)如果Reactor在读和解码请求后顺序处理业务逻辑,那么在处理业务逻辑过程中,Reactor就无法响应其它通道的事件了,这个时候如果正好有共享同一个Reactor的其它通道的请求过来,就会出现停顿的现象。
那么如何做呢,就需要具体问题具体分析,要对业务逻辑进行归类:
- 对于业务较重的,比如大结果集排序,则送到BusinessExecutor线程池进行下一步处理;
- 于业务较轻的,比如单库直接转发的情况,则由Reactor直接完成,不再送线程池,减少上下文切换。
特别说明ER分片机制
如果涉到ER分片,MyCAT目前的机制:计算路由时以阻塞同步方式调用FetchStoreNodeOfChildTableHandler,若由Reactor直接进行路由计算,会导致其它通道停顿现象。把ER分片同步改异步是个看似可行的方法,但这个改造工作量较大,会造成原来完整路由计算逻辑的碎片化。
即使ER分片同步改异步了,每次子表操作都要遍历父表对性能损耗较大,即使采用缓存也不能最终解决问题。个人觉得,ER分片这个功能比较鸡肋,建议生产部署时绕开这个功能,直接通过关联字段分片或表设计时增加冗余字段。
数据验证
1.测试sql从收到请求到下推的总时长,如果时间可容忍,则不必切换到线程池。忽略ER分片。
2.对于manager端口的命令,若存在执行时间比较的,也需要改为线程池来执行
3.对于收到的应答,大部分都不必切换到线程池。
4.对于大量数据排序,只有在排序时,构造执行任务,切换到线程池完成。