简介: 文章中的许多常识点,都是经过云原生编程挑战赛学到的,在一些问题在表述办法、乃至了解上都或许存在一些问题,乃至会有一些谬论;勇于测验就会犯错,有犯错才会有生长,欢迎各位大佬不舍赐教,多多指正,让咱们一同变得更强!

作者:Ninety Percent

悸动

32 岁,码农的倒数第二个本命年,平淡无奇的日子总觉得缺少了点什么。

想要去创业,却害怕家庭承受不住再次失利的挫折,想要生二胎,带娃的压力让我想着还不如去创业;所以我只好在日子中寻找一些小感动,去看一些老掉牙的电影,然后把自己感动得稀里哗啦,去翻一些泛黄的书籍,在回忆里寻找一丝丝从前的厚意满满;去学习一些冷门的常识,最终把自己搞得晕头转向,去参与一些有意思的竞赛,捡起那 10 年走来,早已被刻在基因里的悸动。

那是去年夏末的一个傍晚,我和同事正闲聊着西湖的夸姣,他们说看到了阿里云发布云原生编程挑战赛,问我要不要试试。我说我只要九成的把握,别的一成得找我媳妇儿要;那一天,咱们绕着西湖走了良久,最终终于达成共同,Ninety Percent 战队应运而生,云原生 MQ 的赛道上,又多了一个艰难却坚强的选手。

人到中年,仍然会做出一些冲动的决议,那种屁股决议脑袋的做法,像极了领导们的睿智和 18 岁时我朝三暮四的日子;夏日的 ADB 竞赛,现已让我和女儿有些疏远,让老婆对我有些成见;此次参赛,必然是要暗度陈仓,发愤图强,不到关键时刻,不能让家里人知道我又在卖肝。

开工

你还甭说,或许是人类的赋性使然,这种背着老婆偷偷干坏作业的感觉还真不错,从上路到上分,一路顺风顺水,极速狂奔;断断续续花了大约两天的时刻,成功地在 A 榜拿下了 first blood;再一次把第一名和最终一名一同纳入囊中;快男总是不会让咱们失望了,800 秒的成果,成为了竞赛的 base line。

第一个版别并没有做什么规划,根本上便是拍脑门的计划,目的便是把流程跑通,尽快出分,然后在确保正确性的前提下,逐渐去优化计划,防止一开端就过度规划,导致迟迟不能出分,影响士气。

全体规划

先回忆下赛题:Apache RocketMQ 作为一款分布式的音讯中心件,历年双十一承载了万亿级的音讯流通,其间,实时读取写入数据和读取前史数据都是事务常见的存储拜访场景,针对这个混合读写场景进行优化,能够极大的提高存储体系的稳定性。

一个开发者自述:我是如何设计针对冷热读写场景的 RocketMQ 存储系统

根本思路是:当 append 办法被调用时,会将传入的相关参数包装成一个 Request 目标,put 到恳求行列中,然后其时线程进入等候状况。

聚合线程会循环从恳求行列里边消费 Request 目标,放入一个列表中,当列表长度到达必定数量时,就将该列表放入到聚合行列中。这样在后续的刷盘线程中,列表中的多个恳求,就能进行一次性刷盘了,增大刷盘的数据块的巨细,提高刷盘速度;当刷盘线程处理完一个恳求列表的耐久化逻辑之后,会顺次对列表中个各个恳求进行唤醒操作,使等候的测评线程进行回来。

一个开发者自述:我是如何设计针对冷热读写场景的 RocketMQ 存储系统

内存等级的元数据结构规划

一个开发者自述:我是如何设计针对冷热读写场景的 RocketMQ 存储系统

<![endif]–> 首先用一个二维数组来存储各个 topicId+queueId 对应的 DataMeta 目标,DataMeta 目标里边有一个 MetaItem 的列表,每一个 MetaItem 代表的一条音讯,里边包含了音讯地点的文件下标、文件方位、数据长度、以及缓存方位。

SSD 上数据的存储结构

一个开发者自述:我是如何设计针对冷热读写场景的 RocketMQ 存储系统

一共运用了 15 个 byte 来存储音讯的元数据,音讯的实践数据和元数据放在一同,这种混合存储的办法虽然看起来不太优雅,但比起独立存储,能够削减一半的 force 操作。

数据康复

顺次遍历读取各个数据文件,依照上述的数据存储协议生成内存等级的元数据信息,供后续查询时运用。

数据消费

数据消费时,经过 topic+queueId 从二维数组中定位到对应的 DataMeta 目标,然后依据 offset 和 fetchNum,从 MetaItem 列表中找到对应的 MetaItem 目标,经过 MetaItem 中所记载的文件存储信息,进行文件加载。

总的来说,第一个版别在大方向上没有太大的问题,运用 queue 进行异步聚合和刷盘,让整个程序更加灵活,为后续的一些功用扩展打下了很好的根底。

缓存

60 个 G的 AEP,我垂涎已久,国庆七天,没有出远门的计划,必定要好好卷一卷 llpl。下载了 llpl 的源码,一顿看,发现比我幻想的要简单得多,本质上和用 unsafe 拜访普通内存是一模相同的。卷完 llpl,缓存规划计划呼之欲出。

缓存分级

缓存的写入用了行列进行异步化,防止对主线程形成阻塞(到竞赛后期才发现云 SSD 的奥秘,就算同步写也不会影响全体的速度,后边我会讲原因);程序能够用作缓存的存储介质有 AEP 和 Dram,两者在拜访速度上有必定的差异,赛题所描绘的场景中,会有许多的热读,因而我对缓存进行了分级,分为了 AEP 缓存和 Dram 缓存,Dram 缓存又分为了堆内缓存、堆外缓存、MMAP 缓存(后期加入),在申请缓存时,优先运用 Dram 缓存,提高高功用缓存的运用频度。

Dram 缓存最终申请了 7G,AEP 申请了 61G,Dram 的容量占比为 10%;本次竞赛一共会读取(61+7)/2+50=84G 的数据,依据日志核算,整个测评过程中,有 30G 的数据运用了 Dram 缓存,占比 35%;由于前 75G 的数据不会有读取操作,没有缓存开释与复用动作,所以严厉意义上来讲,在写入与查询混合操作阶段,一共运用了 50G 的缓存,其间滚动运用了 30-7/2=26.5G 的 Dram 缓存,占比 53%。10%的容量占比,却滚动供给了 53%的缓存服务,阐明热读现象十分严重,阐明缓存分级十分有必要。

可是,实践总是残酷的,这些看似无懈可击的优化点在测评中作用并不大,究竟这种优化只能提高查询速度,在读写混合阶段,读缓存总耗时是 10 秒或许是 20 秒,对最终的成果其实没有任何影响!很神奇吧,后边我会讲原因。

缓存结构

一个开发者自述:我是如何设计针对冷热读写场景的 RocketMQ 存储系统

当获取到一个缓存恳求后,会依据 topic+queueId 从二维数组中获取到对应的缓存上下文目标;该目标中维护了一个缓存块列表、以及最终一个缓存块的写入指针方位;假如最终一个缓存块的余量满足放下其时的数据,则直接将数据写入缓存块;假如放不下,则申请一个新的缓存块,放在缓存块列表的最终,一同将写不下的数据放到新缓存块中;若申请不到新的缓存块,则直接按缓存写入失利进行处理。

在写完缓存后,需求将缓存的方位信息回写到内存中的Meta中;比方本条数据是从第三个缓存块中的 123B 开端写入的,则回写的缓存方位为:(3-1)*每个缓存块的巨细+123。在读取缓存数据时,依照 meta 数据中的缓存方位新,定位到对应的缓存块、以及块内方位,进行数据读取(需求考虑跨块的逻辑)。

由于缓存的写入是单线程完结的,关于一个 queueId,前面的缓存块的音讯必定早于后边的缓存块,所以当读取完缓存数据后,就能够将其时缓存块之前的一切缓存都开释掉(放入缓存资源池),这样 75G 中被越过的那 37.5G 的数据也能快速地被开释掉。

缓存功用加上去后,成果来到了 520 秒左右,程序的主体结构也根本完结了,接下来便是精装了。

优化

缓存准入战略

一个 32k 的缓存块,是放 2 个 16k 的数据适宜,仍是放 16 个 2k 的数据适宜?毫无疑问是后者,将小数据块尽量都放到缓存中,能够使得最终只要较大的块才会查 ssd,削减查询时 ssd 的 io 次数。

那么阈值为多少时,能够确保小于该阈值的数据块放入缓存,能够使得缓存刚好被填满呢?(若不填满,缓存利用率就低了,若放不下,就会有小块的数据无法放缓存,读取时必须走 ssd,io 次数就上去了)。

一般来说,经过多次参数调整和测评测验,就能找到这个阈值,可是这种办法不具备通用性,假如总的可用的缓存巨细呈现改变,就又需求进行测验了,不具备出产价值。

这个时分,中学时代的数学常识就派上用途了,如下图:

一个开发者自述:我是如何设计针对冷热读写场景的 RocketMQ 存储系统

由于音讯的巨细实践是以 100B 开端的,为了简化,直接依照从 0B 进行了核算,这样会导致算出来的阈值偏大,也便是最终会呈现缓存存不下然后小块走 ssd 查询的状况,所以我在算出来的阈值上减去了 100B*0.75(由于影响不大,根本是凭直觉拍脑门的)。假如要严厉核算真正准确的阈值,需求将上图中的三角形面积问题,转换成梯形面积问题,可是感觉意义不大,由于 100B 本来就只要 17K 的 1/170,份额十分小,所以影响也十分的小。

梯形面积和三角形面积的比为:(17K+100)(17K-100)/(17k17K)=0.999965,彻底在数据动摇的规模之内。

在程序运行时,依据动态核算出来的阈值,大于该阈值的就直接越过缓存的写入逻辑,最终不管缓存装备为多大,都能确保小于该阈值的数据块悉数写入了缓存,且缓存最终的利用率到达 99.5%以上。

同享缓存

在刚开端的时分,依照算出来的阈值进行缓存规划,仍然会呈现缓存容量不足的状况,实践用到的缓存的巨细总是比总缓存块的巨细小一些,经过各种排查,才茅塞顿开,每个 queueId 所拥有的最终一个缓存块大约率是不会被写满的,微观上来说,均匀只会被写一半。一个缓存块是32k,queueId 的数量大约是 20w,那么就会有 20w*32k/2=3G 的缓存没有被用到;3G/2=1.5G(前 75G 之后随机读一半,所以要除以 2),就算是次序读大块,1.5G 也会带来 5 秒左右的耗时,更甭说随机读了,所以不管有多杂乱,这部分缓存必定要用起来。

已然自己用不完,那就同享出来吧,全体计划如下:

一个开发者自述:我是如何设计针对冷热读写场景的 RocketMQ 存储系统

在缓存块竭尽时,对一切的 queueId 的最终一个缓存块进行自增编号,然后放入到一个一维数组中,缓存块的编号,即为该块在认为数字中的下标;然后依据缓存块的余量巨细,放到对应的余量调会集,余量大于等于 2k 小于 3k 的缓存块,放到 2k 的调会集,以此类推,余量大于最大音讯体巨细(赛题中为 17K)的块,一致放在 maxLen 的调会集。

当某一次缓存恳求获取不到私有的缓存块时,将依据其时音讯体的巨细,从同享缓存调会集获取同享缓存进行写入。比方其时音讯体巨细为 3.5K,将会从 4K 的调会集获取缓存块,若获取不到,则继续从 5k 的调会集获取,顺次类推,直到获取到同享缓存块,或许没有满足任何满足条件的缓存块停止。

往同享缓存块写入缓存数据后,该缓存块的余量将发生改变,需求将该缓存块从之前的调会集移除,然后放入新的余量调会集(若余量等级未发生改变,则不需求履行该动作)。

拜访同享缓存时,会依据Meta中记载的同享缓存编号,从索引数组中获取到对应的同享块,进行数据的读取。

在缓存的开释逻辑里,会直接疏忽同享缓存块(理论上能够经过一个计数器来操控何时该开释一个同享缓存块,但完成起来比较杂乱,由于要考虑到有些音讯不会被消费的状况,且收益也不会太大(由于二阶段缓存是彻底够用的,所以就没做测验)。

MMAP 缓存

测评程序的 jvm 参数不允许选手自己操控,这是拦在选手面前的一道妨碍,由于老时代和年青代之间的份额为 2 比 1,那意味着假如我运用 3G 来作为堆内缓存,加上内存中的 Meta 等目标,老时代根本要用 4G 左右,那就会有 2G 的新生代,这彻底是浪费,由于该赛题对新生代要求并不高。

所认为了防止浪费,必定要削减老时代的巨细,那也就意味着不能运用太多的堆内缓存;由于堆外内存也被限定在了 2G,假如减小堆内的运用量,那空余的缓存就只能给体系做 pageCache,但赛题的背景下,pageCache 的命中率并不高,所以这条路也是走不通的。

有没有什么内存既不是堆内,申请时又不受堆外参数的约束?自然而然想到了 unsafe,当然也想到官方导师说的那句:用 unsafe 申请内存直接撤销成果。。。这条路只好作罢。

花了一个下午的时刻,通读了 nio 相关的代码,意外发现 MappedByteBuffer 是不受堆外参数的约束的,这就意味着能够运用 MappedByteBuffer 来代替堆内缓存;由于缓存都会频繁地被进行写与读,假如运用 Write_read 形式,会导致刷盘动作,就得不偿失了,自然而然就想到了 PRIVATE 形式(copy on write),在该形式下,会在某个 4k 区首次写入数据时,和 pageCache 解耦,生成一个独享的内存副本;所以只要在程序初始化的时分,将 mmap 写一遍,就能得到一块独享的,和磁盘无关的内存了。

所以我将堆内缓存的巨细装备成了 32M(由于该功用现已开发好了,所以仍是要意思一下,用起来),堆外申请了 1700M(算上测评代码的 300M,差不多 2G)、mmap 申请了 5G;一共有 7G 的 Dram 作为了缓存(不运用 mmap 的话,大约只能用到 5G),内存中的Meta大约有700M左右,所以堆内的内存差不多在 1G 左右,2G+5G+1G=8G,操作体系给 200M 左右根本就够了,所以还剩 800M 没用,这800M其实是能够用来作为 mmap 缓存的,主要是考虑到咱们都只能用 8G,超越 8G 容易被挑战,所以最终最优成果里边总的内存的运用量并没有超越 8G。

依据结尾添补的 4K 对齐

由于 ssd 的写入是以 4K 为最小单位的,但每次聚合的音讯的总巨细又不是 4k 的整数倍,所以这会导致每次写入都会有额定的开支。

比较常规的计划是进行 4k 添补,当某一批数据不是 4k 对齐时,在结尾进行填充,确保写入的数据的总巨细是 4k 的整数倍。听起来有些不可思议,额定写入一些数据会导致全体效益更高?

是的,推导逻辑是这样的:“假如不添补,下次写入的时分,必定会写这未满的4k区,假如添补了,下次写入的时分,只要 50%的概率会往后多写一个 4k 区(由于前面添补,导致本次数据后移,尾部多垮了一个 4k 区)”,所以全体来说,添补后会赚 50%。或许换一个角度,添补关于其时的这次写入是没有副作用的(也就多 copy<4k 的数据),关于下一次写入也是没有副作用的,可是假如下一次写入是这种状况,就会由于添补而少写一个 4k。

一个开发者自述:我是如何设计针对冷热读写场景的 RocketMQ 存储系统

依据结尾剪切的 4k 对齐

添补的计划确实能带来不错的提高,可是最终落盘的文件大约有 128G 左右,比实践的数据量多了 3 个 G,假如能把这 3 个 G 用起来,又是一个不小的提高。

自然而然就想到了结尾剪切的计划,将尾部未 4k 对齐的数据剪切下来,放到下一批数据里边,剪切下来的数据对应的恳求,也在下一批数据刷盘的时分进行唤醒。

计划如下:

一个开发者自述:我是如何设计针对冷热读写场景的 RocketMQ 存储系统

添补与剪切共存

剪切的计划当然优秀,但在一些极端的状况下,会存在一些消沉的影响;比方聚合的一批数据全体巨细没有操作 4k,那就需求拘留整批的恳求了,在这一刻,这将变向导致刷盘线程大幅下降、恳求线程大幅下降;关于这种状况,剪切对齐带来的优势,无法弥补拘留恳求带来的劣势(依据直观感触),因而需求直接运用添补的办法来确保 4k 对齐。

严厉意义上来讲,应该有一个拘留线程数代价、和添补代价的量化公式,以决议何种时分需求进行添补,何种时分需求进行剪切;可是其本质太过杂乱,涉及到非同质因子的整合(要在磁盘吞吐、磁盘 io、测评线程耗时三个概念之间做转换);做了一些测验,作用都不是很理想,没能跑出最高分。

当然中心还有一些边界处理,比方当 poll 上游数据超时的时分,需求将拘留的数据进行填充落盘,防止收尾阶段,最终一批拘留的数据得不到处理。

SSD 的预写

得此优化点者,得前 10,该优化点能大幅提高写入速度(280m/s 到 320m/s),这个优化点许多同学在一些技术贴上看到过,或许自己意外发现过,可是大部分人应该对本质的原因不甚了解;接下来我便按部就班,依照自己的了解进行 yy 了。

假定某块磁盘上被写满了 1,然后文件都被删去了,这个时分磁盘上的物理状况必定都仍是 1(由于删去文件并不会对文件区域进行格式化)。然后你又新建了一个空白文件,将文件巨细设置成了 1G(比方经过 RandomAccessFile.position(1G));这个时分这 1G 的区域对应的磁盘空间上仍然仍是 1,由于在出产空白文件的时分也并不会对对应的区域进行格式化。

可是,当咱们此时对这个文件进行拜访的时分,读取到的会全是 0;这阐明文件体系里边记载了,关于一个文件,哪些当地是被写过的,哪些当地是没有被写过的(以 4k 为单位),没被写过的当地会直接回来 0;这些信息被记载在一个叫做 inode 的东西上,inode 当然也是需求落盘进行耐久化的。

所以假如咱们不预写文件,inode 会在文件的某个 4k 区首次被写入时发生性变更,这将形成额定的逻辑开支以及磁盘开支。因而,在结构办法里边一顿 for 循环,依照预估的总文件巨细,先写一遍数据,后续写入时就能起飞了。

大音讯体的优化战略

由于磁盘的读写都是以 4k 为单位,这就意味着读取一个 16k+2B 的数据,极端状况下会发生 16k+2*4k=24k 的磁盘 io,会多加载将近 8k 的数据。

显然假如能够在读取的时分都按 4k 对齐进行读取,且加载出来的数据都是有意义的(后续能够被用到),就能处理而上述的问题;我顺次做了以下优化(有些优化点在后边被废弃掉了,由于它和一些其他更好的优化点冲突了)。

1、大块置顶

<![endif]–> 由于每一批聚合的音讯都是 4k 对齐的落盘的(剪切拘留计划之前),所以我将每批数据中最大的那条音讯放在了头部(依据缓存规划战略,大音讯大约率是不会进缓存的,消费时会从 ssd 读取),这样这条音讯至少有一端是 4k 对齐的,读取的时分能缓解 50%的对齐问题,该种办法在剪切拘留计划之前确实带来了 3 秒左右的提高。

2、音讯次序重组

经过算法,让大块数据尽量少地呈现两头不对齐的状况,削减读取时额定的数据加载量;比方针对下面的比方:

一个开发者自述:我是如何设计针对冷热读写场景的 RocketMQ 存储系统

在收拾之前,加载三个大块一共会涉及到 8 个 4k 区,收拾之后,就变成了 6 个。

由于自己在算法这一块儿真实太弱了,加上这是一个 NP 问题,折腾了几个小时,作用总是差强人意,最终只好放弃。

3、依据内存的 pageCache

在数据读取阶段,每次加载数据时,若加载的数据两头不是 4k 对齐的,就自动向前后延伸打到 4k 对齐的当地;然后将首尾两个 4k 区放到内存里边,这样当后续要拜访这些4k区的时分,就能够直接从内存里边获取了。

该计划最终的作用和预估的相同差,一点惊喜都没有。由于只会有少数的数据会走 ssd,首尾两个 4k 里边大约率都是那些不需求走ssd的音讯,所以被复用的概率极小。

4、部分缓存

已然自己没才能对音讯的存储次序进行调整优化,那就把那些两头不对齐的数据剪下来放到缓存里边吧:

一个开发者自述:我是如何设计针对冷热读写场景的 RocketMQ 存储系统

某条音讯在落盘的时分,若某一端(也有或许是两头)没有 4k 对齐,且在未对齐的 4k 区的数据量很少,就将其剪切下来存放到缓存里,这样查询的时分,就不会由于这少数的数据,去读取一个额定的 4k 区了。

剪切的阈值设置成了 1k,由于数据巨细是随机的,所以从微观上来看,剪切下来的数据片的均匀巨细为 0.5k,这意味着只需求运用 0.5k 的缓存,就能削减 4k 的 io,是常规缓存效益的 8 倍,加上缓存部分的余量分级战略,会导致有许多碎片化的小内存用不到,该计划刚好能够把这些碎片内存利用起来。

测评线程的聚合战略

每次聚合多少条音讯进行刷盘适宜?是按音讯条数进行聚合,仍是依照音讯的巨细进行聚合?

刚开端的时分并没有想那么多,经过日志得知一共有 40 个线程,所以就写死了一次聚合 10 条,然后四个线程进行刷盘;但这会带来两个问题,一个是若线程数发生改变,功用会大幅下降;第二是在收尾阶段,会有一些跑得慢的线程还有不少数据未写入的状况,导致收尾时刻较长,特别是加入了尾部剪切与拘留逻辑后,该现象尤为严重。

为了处理收尾耗时长的问题,我测验了同步聚合的计划,在第一次写入之后的 500ms,对写入线程数进行核算,然后分组,后续就按组进行聚合;这种办法能够完美处理收尾的问题,由于同一个组里边的一切线程都是一同完结写入任务的,大约是由于每个线程的写入次数是固定的吧;可是运用这种办法,尾部剪切+拘留的逻辑就十分难交融进来了;加上在程序一开端就固定线程数,看起来也有那么一些不优雅;所以我就引入了“线程操控器”的概念。

一个开发者自述:我是如何设计针对冷热读写场景的 RocketMQ 存储系统

聚合战略迭代-针对剪切扣的留计划的定向优化

假定其时动态核算出来的聚合数量是 10,关于聚合出来的 10 条音讯,假如本批次被拘留了 2 条,下次聚合时应该聚合多少条?

在之前的战略里边,仍是会聚合 10 条,这就意味着一旦呈现了音讯拘留,聚合逻辑就会发生颤动,会呈现某个线程聚合不到指定的音讯数据量的状况(这种状况会有 poll 超时办法进行兜底,可是全体速度就慢了)。

所以聚合参数不能是一个单纯的、一致化的值,得针对不同的刷盘线程的拘留数,进行调整,假定聚合数为 n,某个刷盘线程的上批次拘留数量为 m,那针对这个刷盘线程的下批次的聚合数量就应该是 n-m。

那么问题就来了,聚合线程(出产者)只要一个,刷盘线程(顾客)有好几个,都是抢占式地进行消费,没办法将聚合到的特定数量的音讯,给到指定的刷盘线程;所以聚合音讯行列需求拆分,拆分成以刷盘线程为维度。

由于改动比较大,为了保存曾经的逻辑,就引入了聚合数量的“严厉形式”的概念,经过参数进行操控,假如是“严厉形式”,就运用上述的逻辑,若不是,则运用之前的逻辑;

规划图如下:

一个开发者自述:我是如何设计针对冷热读写场景的 RocketMQ 存储系统

将聚合行列换成了聚合行列数组,在非严厉形式下,数组里边的原始指向的是同一个行列目标,这样许多代码逻辑就能一致。

聚合线程需求先从拘留信息行列里边获取一个目标,然后依据拘留数和最新的聚合参数,决议要聚合多少条音讯,聚合好音讯后,放到拘留信息所描绘的行列中。

完美的收尾战略,一行代码带来 5s 的提高

引入了线程操控器后,收尾时刻被下降到了 2 秒多,两次收尾,也便是 5 秒左右(这些信息来源于最终一个晚上对 A 榜时的日志的剖析),在赛点方位上,这 5 秒的重要性不言而喻。

竞赛完毕前的最终一晚,分数徘徊在了 423 秒左右,前面的大佬在许多天前就从 430 一次性优化到了 420,然后分数就没有太大改变了;我其时抱着侥幸的态度,判定应该是 hack 了,直到那天晚上在钉钉群里和他聊了几句,直觉告诉我,420 的成果是有用的。其时是有些慌的,究竟竞赛第二天早上 10 点就完毕了。

我开端堕入深深的反思,我都卷到极致了,从 432 到 423 花费了许多的精力,为何大神能够一击致命?不对,必定是我疏忽了什么。

我开端回看前史提交记载,然后对照剖析每次提交后的测评得分(由于前史成果都有必定的颤动,所以这个作业十分的上头);花费了大约两个小时,总算发现了一个反常点,在 432 秒邻近的时分,我从同步聚合切换成了异步聚合,然后交融了剪切拘留+4k 添补的计划,按理说这个优化能削减 3G 多的落盘数据量,成果应该是能够提高 10 秒左右的,可是其时成果只提高了 5 秒多,由于其时还有不少没有落地的优化点,所以就没有太介意。

拘留战略会会将尾部的恳求拘留下来,尾部的恳求本来便是慢一拍(对应的测评线程慢)的恳求(行列是次序消费),这一拘留,进度就更慢了!!!

聚合到一批音讯后,依照音讯对应的线程被拘留的次数,从大到小排个序,让那些慢的、拘留多的线程,尽或许不被拘留,让那些快的、拘留少的恳求,尽或许被拘留;最终一切的线程几乎都是一同完结(依据设想)。

赶忙提交代码、开端测评,抖了两把就破 420 了,最好成果到达了 418,比优化前高出 5 秒左右,十分契合预期。

查询优化

由于只要少数的数据会读 ssd,这使得在读写混合阶段,sdd 查询的并发量并不大,所以在加载数据时进行了判别,假如需求从 ssd 加载的数量大于必定量时,则进行多线程加载,充分利用 ssd 并发随机读的才能。

为什么要大于必定的量才多线程加载,假如只需求加载两条数据,用两个线程来加载会有提高吗?当存储介质够快、加载的数据量够小时,多线程加载数据带来的 io 时刻的提高,还不足以弥补多线程履行自身带来的程序开支。

缓存的批量 copy

若某次查询时需求加载的数据,在缓存上是接连的,则不需求一条一条从缓存进行复制,能够以缓存块的巨细为最小粒度,进行复制,提高缓存读取的效益。

一个开发者自述:我是如何设计针对冷热读写场景的 RocketMQ 存储系统

上面的比方中,运用批量 copy 的办法,能够将 copy 的次数从 5 次降到 2 次。

这样做的前提是:用于回来的各条音讯对应的 byteBuffer,在内存上需求是接连的(经过反射完成,给每个 byteBuffer 都注入同一个 bytes 目标);批量复制完毕后,依据各条音讯的巨细,动态设置各自 byteBuffer 的 position 和 limit,以确保 retain 区域刚好指向自己所对应的内存区间。

该功用一向有偶现的 bug,本地又复现不了,A 榜的时分没太介意,B 榜的时分又不能看日志,一向没得到处理;怕由于代码质量影响最终的代码分,所以后来就注释掉了。

丢失的夸姣

在竞赛开端的时分,看了金融通的赛题解析,里边提到了一个对数据进行搬迁的点;10 月中旬的时分进行了测验,在开端读取数据时,连续把那些缓存中没有的数据读取到缓存中(由于一旦开端读取,就会有许多的缓存被开释出来,缓存容量彻底够用),一共进行了两个计划的测验:

1、依据次序读的异步搬迁计划

在第一阶段,当缓存竭尽时,记载其时存储文件的方位,然后搬迁的时分,从该方位开端进行次序读取,将后续的一切数据都读取到缓存中;这样做的优点是大幅下降查询阶段的随机读次数;可是也有不足,由于前 75G 数据中有一般的数据是不会被消费的,这意味着搬迁到缓存中的数据,有 50%都是没有意义的,其时测下来该计划根本没有提高(由于成果有必定的颤动,具体是有一部分提高、没提高、仍是负优化,也不得而知);后来引入了缓存准入战略后,该计划就彻底被废弃了,由于需求从 ssd 中读取的数据会彻底散列在存储文件中。

2、依据懒加载的异步搬迁计划

上面有讲到,由于一阶段的数据中有一半都不会被消费到,想要不做无用功,就必须要在确保搬迁的数据都是会被消费的数据。

所以加了一个逻辑,当某个 queueId 第一次被消费的时分,就异步将该 queueId 中不存在缓存中的音讯,从 ssd 中加载到缓存中;由于其时觉得就算是异步搬迁,也是要随机读的,读的次数并不会削减,一段时刻内磁盘的压力也并不会削减;所以对该计划就没怎么注重,彻底是抱着写着玩的态度;而且在搬迁的准入逻辑上加了一个判别:“当本次查询的音讯中包含有从磁盘中加载的数据时,才异步对该 queueId 中剩余的 ssd 中的数据进行搬迁”;至今我都没相透其时自己为什么要加上这个一个判别。也便是由于这个判别,导致搬迁作用仍然不理想(会导致搬迁不够会集、而且许多 queueId 在某次查询的时分读了 ssd,后续就没有需求从 ssd 上读取的数据了),对成果没有明显的提高;在一次版别回退中,彻底将搬迁的计划给抹掉了(信任打竞赛的小伙伴对版别回退深有感触,特别是关于这种有较大成果颤动的竞赛)。

竞赛完毕后我在想,假如其时在搬迁逻辑上没有加上那个神奇的逻辑判别,我的成果能到多少?或许能到 410,或许打破不了 420;正式由于错失了那个大的优化点,才让我在其他点上做到了极致;那些错失的夸姣,会让咱们在未来的日子里更加努力地奔跑。

接下来咱们讲一下为什么异步搬迁会快。

ssd 的多线程随机读是很快的,可是我上面有讲到,假如查询的数据量比较小,多线程分批查询作用并不必定就好,由于每一批的数据量真实太小了;所以想要在查询阶段开许多的线程来提高全体的查询速度并不能取的很好的作用。异步搬迁能够完美地处理这个问题,而且在 io 次数必定的状况下,会集进行 ssd 的随机读,比散列进行随机读,pageCache 命中率更高,且对写入速度形成的全体影响更小(这个观念纯属个人感悟,只确保 Ninety Percent 的正确率)。

SSD 云盘的奥秘

我也是个小白,以下内容许多都是猜想,咱们看一看就能够了。

1、云 ssd 的运作机制

SSD 云盘和传统的 ssd 盘拥有着相同的特性,可是却是不同的东西;能够了解成 SSD 云盘,是传统 ssd 盘的一个扩大版。

一个开发者自述:我是如何设计针对冷热读写场景的 RocketMQ 存储系统

SSD 云盘的底层存储介质是多个普通的物理硬盘,这些物理硬盘就类似于传统 ssd 中的存储颗粒,在进行写入或读取的时分,会将任务分配到多个物理设备上并行进行处理。一同,在云 ssd 中,对数据的更新采用了 append 的办法,即在进行更新时,是次序追加写一块数据,然后将方位的引证从原有的数据块指向新的数据块(咱们拜访的文件的position和硬盘的物理地址之间有一层映射,所以就算硬盘上有许多的碎片,咱们也仍然能获取到一个“接连”的大文件)。

阿里云官网上有云 ssd 的 iops 和吞吐的核算公式:
iops = min{1800+50 容量, 50000}; 吞吐= min{120+0.5 容量, 350}

咱们看到不管是 iops 和吞吐,都和容量呈正相关的联系,而且都有一个上限。这是由于,容量越大,底层的物理设备就会越多,并发处理的才能就越强,所以速度就越快;可是当物理设备多到必定的数量时,文件体系的“总控“就会成为瓶颈;这个总控必定也是需求存储才能的(比方存储方位映射、前史数据的 compact 等等),所以当给总控装备不同功用的存储介质时,就得到了 PL0、PL1 等不同功用的云盘(当然,除此之外,网络带宽、运算才能也是云 ssd 速度的影响因子)。

2、云 ssd 的 buffer 现象

在过程中发现了一个有趣的现象,就算是 force 落盘,在刚开端写入时,速度也是远大于 320m/s 的(能到达 400+),几秒之后,会降下来,稳定在 320 左右(像极了不 force 时,pageCache 带来的 buffer 现象)。

针对这种奇怪的现象,我进行了进一步的探索,每写 2 秒的数据,就 sleep 2 秒,结果是:在写入的这两秒时刻里,速度能到达 400+,全体均匀速度也远超越了 160m/s;后来我又做了许多试验,包含在每次写完数据之后直接进行时刻短的 sleep,可是这根本不会影响到 320m/s 的全体速度。测试代码中,虽然是 4 线程写入,可是总会有那么一些时刻,大部分乃至一切线程都处于 sleep 状况,这必然会使得在这个时刻点上,应用程序到硬盘的写入速度是极低的;可是时刻拉长了看,这个速度又是能恒定在 320m/s 的。这阐明云 ssd 上有一层 buffer,类似操作体系的 pageCache,只是这个“pageCache”是牢靠存储的,应用程序到这个 buffer 之间的速度是能够超越 320 的,320 的阈值,是下流所导致的(比方 buffer 到硬盘阵列)。

关于这个“pageCache”有几种猜想:

1、物理设备自身就有 buffer 效应,由于物理设备的存储状况本质上是经过电刺激,改变存储介质的化学状况或许物理状况的完成的,驱动这种改变的工业本质,发生了这种 buffer 现象‘;

2、云 ssd 里边有一块较小的高功用存介质作为缓冲区,以供给更好的突击写的功用;

3、逻辑限速,哈哈,这个纯属开玩笑了。

由于有了这个 buffer 效应,程序层面就能够为所欲为了,比方写缓存的动作,全体会花费几十秒,可是就算是在只要 4 个写入线程的状况下,不管是异步写仍是同步写,都不会影响全体的落盘速度,由于在同步写缓存的时分,云 ssd 能够进行时刻短的停歇,在接下来的写入时,速度会时刻短地超越 320m/s;查询的时分也类似,非 io 以外的时刻开支,不管长短,都不会影响全体的速度,这也便是我之前提到的,批量复制缓存,理论上有不小提高,可是实践上却没多大提高的原因。

当然,这个 buffer 现象其实是能够利用起来的,咱们能够在写数据的时分多花一些时刻来做一些其他的作业,反正这样的时刻开支并不会影响全体的速度;比方我之前提到的 NP 问题,能够 for 循环暴力破解

参赛总结

这次竞赛的体会十分好,赛题有挑战、导师们也十分热情。竞赛周期也适中,让我既有时刻冲刺,也防止了频繁熬夜导致的家庭矛盾,媳妇儿是在最终一周看到了天池的页面,才发现我在打竞赛。

2021 年参与了两次阿里云的竞赛,都取得了不错的成果,这再一次证明了我适合做技术,不适合做办理;今年作业的体会很欠好,大约也是由于代码写少了吧。

许多问题或许不需求答案,一路向前,有人陪伴,上坡下坡,都有快乐。这便是我参与大赛之后感触很深入的一点。

文章中的许多常识点,都是经过云原生编程挑战赛学到的,在一些问题在表述办法、乃至了解上都或许存在一些问题,乃至会有一些谬论;勇于测验就会犯错,有犯错才会有生长,欢迎各位大佬不舍赐教,多多指正,让咱们一同变得更强!

源码地址:code.aliyun.com/276059488/m…

今天,第三届云原生编程挑战赛正式启动了!作为上一届大赛冠军,我等待越来越多的朋友加入大赛、感触大赛,从大赛里真实地获取对自己有价值的东西,究竟每一次生长都是需求自己英勇的迈出第一步的。咱们共勉!

一个开发者自述:我是如何设计针对冷热读写场景的 RocketMQ 存储系统

点击此处报名参赛!

原文链接:click.aliyun.com/m/100034941…

本文为阿里云原创内容,未经允许不得转载。