基于Disruptor游戏服务器消息总线的设计

发表于2017-10-19
评论0 5k浏览

LMAX是一种新型零售金融交易平台,它能够以很低的延迟(latency)产生大量交易(吞吐量). 这个系统是建立在JVM平台上,核心是一个业务逻辑处理器,它能够在一个线程里每秒处理6百万订单,用1微秒的延迟获得吞吐量为100K . 业务逻辑处理器完全是运行在内存中(in-memory),使用事件源驱动方式(event sourcing). 业务逻辑处理器的核心是Disruptors。Disruptor是一个高性能低延迟的异步处理框架,或者可以认为是最快的消息框架(轻量的JMS),也可以认为是一个观察者模式实现,或者事件-监听模式的实现,直接称disruptor模式。


一款ARPG游戏,它的服务器设计是按照多线程处理业务,分为个人初始化业务(init bus),个人常用业务(cache bus),场景(stage),公共业务(publish)等,根据投递到业务线程的消息来源不同,可以分为 客户端触发(nioEventLoopGroup-X-Y),异步事件触发(async-event-X),别的业务线程触发(内部指令和事件分发),定时调度触发(gameScheduler-X),线程之间会无时无刻频繁地进行消息的生产和消费,JMS消息总线的效率对服务器的性能影响是全方位的,一款高性能低延迟的框架是我们游戏业务的基础保障。

我们原先的设计是基于LinkedBlockingQueue进行投递的,生产和消费都用到了锁,生产用的是putLock,消费是takeLock。替换成Disruptor后,生产时锁消失,消费时由于用的是BlockingWaitStrategy策略,所以还存在锁,但由于是单线程消费所以,基本没什么锁竞争的消耗。

以下是测试测试环境:

model nameIntel(R) Xeon(R) CPU E3-1230 V2 @ 3.30GHz
cpu core8
cache size15360 KB
OSWindows 7 64bit
JDK1.8.60
Disruptor3.3.2
ringBufferSize33554432
producerTypeProducerType.MULTI
WaitStrategy      BlockingWaitStrategy

从消息处理量进行对比:

处理的消息数量:32*1024*1024=33554432

耗时对比:


Duration(ms)LinkedBlockingQueueDisruptor对比
15771420973%
25725347960.77%
35433322859.41%
45297280552.95%
55745247743.12%
66124242839.65%




QPSLinkedBlockingQueueDisruptor对比
158143187972067137%
258610369644850165%
3617604110394805168%
4633461111962364189%
5584063213546400232%
6547916913819783252%



分析:当线程较少时,性能提升并不是很明显,单线程时耗时降为原先的73%,QPS提升到原先的137%,随着线程数增多,性能提升幅度越来越大,当生产者为六根线程时,耗时已经降为了原先的39.65%,QPS处理量已经达到了252%,可以预见,线程越多,性能提升越大。

而LinkedBlockingQueue,当线程数<=4时,随着线程数增多,多线程的处理消息的正面效果大于锁竞争带来的负面效果,所以性能慢慢提升,但超过5根后,效果反转,性能表现越来越差,可以预见,线程越多,性能会越来越差,在各方面表现上都完败Disruptor.


从CPU占用来看:

LinkedblockedQueue压测时的CPU表现:


Disruptor压测时的CPU表现:


分析LinkedblockedQueue的CPU占用在30%~35%上下浮动,Disruptor的CPU占用在25%~30%上下浮动,CPU占用降低5%左右占用。

线程CPU耗时对比:


分析:

第1组:LinkedblockedQueue-produce-1 和 :LinkedblockedQueue-produce-2 为生产者,对应的LinkedblockedQueue为消费者;

第2组:Disruptor--produce-1 和 :Disruptor-produce-2 为生产者,对应的Disruptor-1为消费者;

很明显,处理同样的消息量33554432,LBQ耗时达到了Disruptor的2.26倍左右。


最后,再说下消息缓冲区提前分配的问题,以下是网上一段精彩的描述:"避免GC:写Java程序的时候,很多人习惯随手new各种对象,虽然Java的GC会负责回收,但是系统在高压力情况下频繁的new必定导致更频繁的GC,Disruptor避免这个问题的策略是:提前分配。在创建RingBuffer实例时,参数中要求给出缓冲区元素类型的Factory,创建实例时,Ring Buffer会首先将整个缓冲区填满为Factory所产生的实例,后面生产者生产时,不再用传统做法(顺手new一个实例出来然后add到buffer中),而是获得之前已经new好的实例,然后设置其中的值。举个形象的例子就是,若缓冲区是个放很多纸片的地方,纸片上记录着信息,以前的做法是:每次加入缓冲区时,都从系统那现准备一张纸片,然后再写好纸片放进缓冲区,消费完就随手扔掉。现在的做法是:实现准备好所有的纸片,想放入时只需要擦掉原来的信息写上新的即可"

由于压力测试过程中,会瞬间创建海量消息(33554432),所以导致年老代将近被占满而导致会不断的进行fullGC,所以压测时,务必要调大内存分配,尽量少让FULLGC来影响测试结果,毕竟生产环境不会需要这么大的消息缓存的(目前生产环境容量仅为压测时的1/100)。

总之,优化后,消息总线的处理性能至少翻倍。


http://blog.csdn.net/jiangguilong2000/article/details/49493105


如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引