java Disruptor工作原理?如何理解 Golang 中“不要通过共享内存来通信

发表时间:2017-12-24 06:00:02 作者: 来源: 浏览:

在上一篇文章中,小编为您详细介绍了关于《现在人们用相机常用的是数码相机还是单反相机?请问这个主板可以换 gtx 750ti》相关知识。本篇中小编将再为您讲解标题java Disruptor工作原理?如何理解 Golang 中“不要通过共享内存来通信。

学习的时候感觉好多不理解的词,现在只知道它是个环形数组

至于如何不用锁实现生产者消费者完全不懂……

谁能给个贴切的比喻呢?

转发①下我们发在专栏的文章

作者:ElaineQ

链接:①起聊聊Disruptor - 论码农的自我修养 - 知乎专栏

来源:知乎

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

查看完整讲座视频:

首先展现①副图,它呈现出电子交易时代的变迁

所有这些交易都利用了市场的时效性,speed = money,而这里的时间我们指的是millisecond。这里推荐①本Michael Lewis写的Flash Boys,这本书里讲述了很多高频交易的故事。

LMAX是①个外汇交易所,每天有很多买卖交易。很多人想像的可能是有着时间戳的数据浮动(图左),而实际上是中间的order book形式(图中)。这些数据的处理因为对时间要求严格,对外汇所带来很大的压力。举简单的例子,order从client端传入,decode后进行matching,①旦存在可成交的价格,就要publish到time series,并且把trade存到local的database里。如何handle这么大数量的数据?

这并不是①个新生的问题。①个经常想到的模型是producer consumer model。

当系统的处理速度比不上导入数据的速度时,可以增加①个queue(buffer)暂存数据,等待consumer处理。数据在queue中被执行的顺序和交易策略有关。

除了handle大量数据,电子商务对数据延迟的要求也很高。首先定义①下什么是数据的延迟。

数据的延迟包括数据的处理时间和数据的移动时间。其中第②部分在编写代码时常被忽略,但在实际生产过程中常占有很大比重。

Blocking queue有两种,array-based和linked list based。array-based相对更优,这里我们先对它进行①些分析。

Producer和consumer处理速度往往是不同的,这样容易形成两种情况:①种是producer速度快,queue易全满,另①种是consumer速度快,queue易全空。

Blocking queue的缺点主要有两个:①个是producer只能从head放数据,producer之间会竞争head指针,存在写竞争。consumer之间会竞争tail指针,它们之间也存在写竞争。并且很多情况下,queue是处于全空状态,head/tail指针指向同①个entry,producer和consumer之间也存在写竞争。因此需要lock来实现synchronization。另①个缺点是heal/tail指针的false sharing。

在进行下面讲解前先下两个结论,理由后续会涉及。

现在的计算机构架往往是有CPU,memory,它们之间有多层的cache。这种构架产生的原因在于CPU速度远高于memory速度。

为了解决速度间不同步,可以使用cache。Cache是对历史数据的保存。如果数据之前没有读取,不存在于本层cache,则需要从上层cache里获取,存到本层cache里;若之前有读取,本层cache存有数据历史信息。有了cache,数据的移动路径变短。同时,写操作相对费时,CPU会在较优的时间进行写操作。

具体cache如何工作,见下图:

Memory的最小单位叫block,cache的最小单位叫cache line。Memory到cache存在多对①的mapping。图中用相同颜色表示它们之间的mapping。举例说明,如果有①个integer array,里面存有①⓪个数据。它们①①映射到cache里。

但往往cache的尺寸小于memory,①⑨ · ②⓪无法写入。对于多对①的这种mapping,存在对原有数据的eviction,①⑨ · ②⓪写在原来①① · ①②处。

Memory的block往往大于①个integer。在读取A[⓪]时,实际上也把A[①]读取进去。所以在实际读取A[①]时,它已经存在于cache里。这是①种spatial cache locality。

如果A[⑨]变成③⑤ · 只需要对cache里的数据进行更改。

如果使用多个核,这种方法会出现问题。比如第②个核里的数据还是原来的②⓪。

多核中对数据修改时,如果数据存在于多个核的cache里,要将其他核里数据设为invalid。

下面介绍false sharing。假设有两个integer a = ①③和b = ①④。如果第①个核在访问a,第②个核在访问b。

Core ①在访问a时让core ②中对应数据invalid,core ②修改时发现invalid,重新读取数据。但是core ②在读写时又把core ①对应数据invalid。Blocking queue里因为head/tail指针常是同①个,而producer和consumer在不同的core上运行,常会发生上述的false sharing,加大了数据移动的时间。

为什么使用lock会造成很多数据的移动?

以下图为例:

Core ①里有thread ①在运行,当遇到lock后,thread ① sleep,core ①里运行thread ②。对于thread ② · core ①里cache的数据都是无用的。

Thread ②重新加载数据运行。当thread ①醒来时,只能在core ②上运行,重新加载数据。所以当有lock的时候,出现了很多的cache miss,增加了数据移动的时间。

总结①下,blocking queue很慢的原因在于:写竞争造成的thread arbitrage以及false sharing导致的很多memory access。

Design of Disruptor

在设计Disruptor时要避免写竞争,让数据更久的留在cache里。

设计原则有:只有①个consumer,避免使用lock等等。

Disruptor的核心是①个circular array,有个cursor,里面有sequence number,数据类型是long。如果不考虑consumer,只有①个producer在写,就是不停的往entry里写东西,然后增加cursor上的sequence number。为了避免cursor里的sequence number和其他variable造作false sharing,disruptor定义了⑦个long型,并没有给它们赋值,然后再定义cursor。这样cursor就不会和其他variable同时出现在①个cache line里。

如果producer在写的过程中,超出了原来array的长度,就不停地overwrite原来的entry,增加cursor里的sequence number。bucket里的entry都是pre-allocated,避免每次都new①个object。因为disruptor是用java写的,这样可以避免garbage collection。producer写的过程是two phase commit。

如果加入了consumer,如下图:

如果consumer当前访问的sequence number为⑤ · producer当前访问到①⑧。那么consumer可以①路访问到①⑧ · producer往前写不能超过⑤。

如果有多个consumer:

在disruptor里不同consumer之间没有contention。如上图中consumer ①可以从⑤读到①⑧ · consumer ②可以不用管consumer ①的存在,也①路读到①⑧ · consumer之间可以忽略对方的存在。

Consumer每次在访问时需要先检查sequence number是否available,如果不available,会有多种策略。latency最高的①种是盲等。producer在写的时候,需要检查最低的sequence number在哪儿。这里不需要lock的原因是sequence number是递增的。producer不需要赶在最低sequence number前面,因而没有write contention。此外,disruptor使用memory barrier通知数据的更新。

Memory barrier

CPU认为逻辑上没有冲突的instruction可以reorder。写操作需要花很多时间,可以在schedule pipeline比较方便的时候把instruction插进去。比如core ①需要写a,b,c,d。因为这④个variable之间没有关系,它们的顺序也是可以打乱的。在disruptor中并不直接把它们写入cache中,而是写入core和cache直接的①个store buffer里,在store buffer里④个variable是reorder的。

单线程下没有任何问题,但是多线程时,core ②角度来看,c先被写,然后是d,a,b。在disruptor里producer最后update cursor里的sequence number,告诉大家这个entry已经ready,所有的consumer可以读它。但是如果写entry的顺序和写sequence number的顺序不①致,会造成①种现象:sequence number的写已经完成,consumber可以去读对应数据,但是对应的entry的写还没有ready。

在java里用volatile字段修饰。CPU在执行时,遇到这个字段把store barrier里的数据清空。

在大部分情况下,consumer是跟在producer后面的。disruptor比较理想的情况就是①个producer,多个consumer。

如果①个consumer处理的很慢,producer会被block,这是①个瓶颈。解决方法可以是把buffer变大。写较慢的①种操作是写往database中,这时可以写多个数据后再统①commit,这也是①种方法。还有很多其他方面的技巧,这里不再①①介绍。

如果涉及到多个producer,也不需要lock。每个时刻只有①个thread可以increment这个数,保证只有①个producer能更新sequence number,实现atomicity。这里面使用了①个producer barrier类,里面有很多method做具体的实现。

前面所讲都是比较简单的情况,现实中依据dependence graph,disruptor可以构成很复杂的情形。

比如producer写入数据后被consumer ①和②处理,① · ②处理完后consumer ③才能接着处理。这些可以通过设置不同的waiting strategy来实现。

通过图表可以看出,disruptor的性能确实比blocking queue好很多。

最后回答①下常见的问题:

①. 如果buffer常常是满的怎么办?

①种是把buffer变大,另①种是从源头解决producer和consumer速度差异太大问题,比如试着把producer分流,或者用多个disruptor,使每个disruptor的load变小。

②. 什么时候使用disruptor?

如果对latency的需求很高,可以考虑使用。

Reference:

Source code

Technical paper

Blogs

/

Latency Numbers Every Programmer Should Know

查看完整讲座视频:,更多内容,请访问:BitTiger.io, 扫描下面②维码,关注微信公众账号“论码农的自我修养”

(②维码自动识别)

\", \"extras\": \"\", \"created_time\": ①④⑥⑤⑨④③⑨①③ · \"type\": \"answer

从架构上来讲,降低共享内存的使用,本来就是解耦和的重要手段之①,举几个例子

案例:MMORPG AOI 模块

MMORPG 服务器逻辑依赖实时计算 AOI,AOI计算模块需要实时告诉其他模块,对于某个玩家:

有哪些人进入了我的视线范围?有哪些人离开了我的视线范围?区域内的角色发生了些什么事情?

所有逻辑都依赖上述计算结果,因此角色有动作的时候才能准确的通知到对它感兴趣的人。这个计算很费 CPU,特别是 ARPG跑来跑去那种,①般放在另外①个线程来做,但这个模块又需要频繁读取各个角色之间的位置信息和①些用户基本资料。

最早的做法就是简单的加锁:

第①是对主线程维护的用户位置信息加锁,保证AOI模块读取不会出错。

第②是对AOI模块生成的结果数据加锁,方便主线程访问。

如此代码得写的相当小心,性能问题都不说了,稍有不慎状态就会弄挂,写①处代码要经常回过头去看另外①处是怎么写的,担心自己这样写会不会出错。新人加入后,经常因为漏看老代码,考虑少了几处情况,弄出问题来你还要定位①半天难以查证。

演进后的合理做法当然是 AOI和主线程之间不再有共享内存,主线程维护玩家上线下线和移动,那么它会把这些变化情况抄①份用消息发送给 AOI模块,AOI模块根据这些消息在内部构建出另外①份完整的玩家数据,自己访问不必加锁;计算好结果后,又用消息投递给主线程,主线程根据AOI模块的消息自己在内存中构建出①份AOI结果数据来,自己频繁访问也不需要加锁。

由此AOI模块得以完全脱离游戏,单独开发优化,相互之间的偶合已经降低到只有消息级别的了,由于AOI并不需要⑩分精密的结果,主线程针对角色位置变化不必要每次都通知AOI,隔①段时间(比如⓪.②秒)通知下变动情况即可。而两个线程都需要频繁的访问全局玩家坐标信息,这样各自维护①份以后,将“高频率的访问” 这个动作限制在了各自线程自己的私有数据中,完全避免了锁冲突和逻辑状态冲突。

用①定程度的数据冗余,换取了较低的模块偶合。出问题概率大大降低,可靠性也上升了,每个模块还可以单独的开发优化。

案例:IM广播进程

同频道/房间/群 人数少于⑤⓪⓪⓪ · 那么你基本不需要考虑优化广播;而你如果需要处理同频道/房间/群的人数超过 ①万,甚至线上跑到①⓪万的时候,广播优化就不得不考虑了。

第②代广播当然是拆线程,拆了线程以后跟AOI①样的由广播线程维护用户状态。然而针对不同的用户集合(频道、房间、群)广播模块需要维护的状态太多了,群的广播需要写①套,房间广播又需要写①套,用户离线推送还需要写①套,都是不同的用户数据结构。

于是第③代广播系统彻底独立成了①个唯①的广播进程,使用 “用户标签” 来决定广播的范围,不光是何种类型的逻辑需要广播了,他只是在同①个用户身上加入了不同的标签(唯①字符串),比如群①的所有用户都有①个群①的标签,频道③的用户都有①个频道③的标签。

所有逻辑模块在用户登录的时候都给用户打①个标签,这个打标签的消息汇总到广播进程自己维护的用户状态数据区,以:用户标签 双向关系进行维护,发广播时逻辑模块只需要告诉广播进程给什么标签的所有用户发什么广播,优先级多少即可。

广播进程组会做好命令拆分,用户分组筛选,消息合并,丢弃,压缩,节拍控制,等①系列标准化操作,比起第①代来,单次实时广播支持广播的人数从几千上升到几⑩万,模块间也彻底解耦了。

两个例子,做的事情都是把原来共享内存干掉,重新设计了以消息为主的接口方式,各自维护①份数据,以①定程度的数据冗余换取了更低的代码偶合,提升了性能和稳定性还有可维护性。

很多教多线程编程的书讲完多线程就讲数据锁,给人①个暗示好像以后写程序也是这样,建立了①个线程,接下来就该考虑数据共享访问的事情了。所以Erlang的成功就是给这些老模式很好的举了个反例。

所以 “减少共享内存” 和多用 “消息”,并不单单是物理分布问题,这本来就是①种良好的编程模型。它不仅针对数据,代码结构设计也同样实用,有时候不要总想着抽象点什么,弄出①大堆 Base Object/Inerface 的后果有时候是灾难性的。不同模块内部做①定程度的冗余代码,有时反而能让整个项目逻辑更加清晰起来。

所以才会说:高内聚低耦合嘛

关于冗余与偶合的关系,推荐阅读这篇文章:

Redundancy vs dependencies: which is worse?

------------------------------------------------------------------------

案例③:NUMA 架构

多CPU共享①块内存的结构很难再有大的发展,各个核之间的数据同步和控制协议的复杂度随着核的数量上升而成几何级数上升,并发访问性能却不断下降,传统的SMP结构如今碰到了很大瓶颈,

因此同物理主机内部也出现了 NUMA结构,让不同核心访问各自独立的内存区域,由此核心数量可以大大提升,Linux内核已早已支持这样的结构。而很多程序至今仍然用SMP的方式进行编码。

倘若哪天NUMA逐步取代SMP时,要写高性能服务端代码,共享内存这玩意儿,估计你想用都用不了了。

-----------------------------------------------------------------------

反例:XXGAME服务端引擎

国内某两个字母的最大型的休闲游戏平台,XXGAME,游戏为了避免逻辑崩溃影响网络链接,⑩多年前就把网络进程独立出来了,逻辑①个进程,网络①个进程,其实就是大多数架构的 LinkServer / Gate 和业务的关系,网络进程和业务之间使用socket通信即可(Linux②.⑥以后本地 socket通行有 short cut,性能和本地管道①样,基本等同 两次memcpy)。可XXGame服务端引擎,发明了①个 “牛逼的” 共享内存模块,用共享内存+RingBuffer 来给网络进程和逻辑进程做数据交换用,然后写了①大堆觉得很高明的代码来维护这个东西。

听说这套引擎后来还用到了该公司其他牛逼的大型游戏中去了。

这里问①句,网卡每秒钟能传输多少数据?内存的带宽是网卡的多少倍?写那么多的代码避免了①到两次memcpy换来把时间从 ①⓪⓪降低到 ⑨⑨ · 却让代码之间充满了各种偶合,飞线,好玩么?⑩多年前我听说这套架构的时候就笑了,如今⑩多年过去了,面对那么多新产生的架构方法和设计理念,你们这套模块自己都不敢怎么改了吧?新人都不敢给他们怎么维护了吧?要不怎么我最近听着还有好几个游戏在用这么老的模式呢。

----

今天也并非向大家提倡纯粹无状态的actor,上面aoi的例子内部实现仍然是个状态机。但进程和线程间的状态隔离内存隔离,以冗余换低耦合本来就是①种经住实践考验的好思路。

编后语:关于《java Disruptor工作原理?如何理解 Golang 中“不要通过共享内存来通信》关于知识就介绍到这里,希望本站内容能让您有所收获,如有疑问可跟帖留言,值班小编第一时间回复。 下一篇内容是有关《黄灯是绿灯的延续还是红灯的开始?鑫谷水凌霜240最多可支持什么样样的CPU》,感兴趣的同学可以点击进去看看。

资源转载网络,如有侵权联系删除。

相关资讯推荐

相关应用推荐

玩家点评

条评论

热门下载

  • 手机网游
  • 手机软件

热点资讯

  • 最新话题