网站建站 用户注册,大学生兼职网站策划书,做网站找哪家公司比较好,网站建设 项目背景悸动
32 岁#xff0c;码农的倒数第二个本命年#xff0c;平淡无奇的生活总觉得缺少了点什么。
想要去创业#xff0c;却害怕家庭承受不住再次失败的挫折#xff0c;想要生二胎#xff0c;带娃的压力让我想着还不如去创业#xff1b;所以我只好在生活中寻找一些小感动码农的倒数第二个本命年平淡无奇的生活总觉得缺少了点什么。
想要去创业却害怕家庭承受不住再次失败的挫折想要生二胎带娃的压力让我想着还不如去创业所以我只好在生活中寻找一些小感动去看一些老掉牙的电影然后把自己感动得稀里哗啦去翻一些泛黄的书籍在回忆里寻找一丝丝曾经的深情满满去学习一些冷门的知识最后把自己搞得晕头转向去参加一些有意思的比赛捡起那 10 年走来早已被刻在基因里的悸动。
那是去年夏末的一个傍晚我和同事正闲聊着西湖的美好他们说看到了阿里云发布云原生编程挑战赛问我要不要试试。我说我只有九成的把握另外一成得找我媳妇儿要那一天我们绕着西湖走了好久最后终于达成一致Ninety Percent 战队应运而生云原生 MQ 的赛道上又多了一个艰难却坚强的选手。
人到中年仍然会做出一些冲动的决定那种屁股决定脑袋的做法像极了领导们的睿智和 18 岁时我朝三暮四的日子夏季的 ADB 比赛已经让我和女儿有些疏远让老婆对我有些成见此次参赛必然是要暗度陈仓卧薪尝胆不到关键时刻不能让家里人知道我又在卖肝。
开工
你还别说或许是人类的本性使然这种背着老婆偷偷干坏事情的感觉还真不错从上路到上分一路顺风顺水极速狂奔断断续续花了大概两天的时间成功地在 A 榜拿下了 first blood再一次把第一名和最后一名同时纳入囊中快男总是不会让大家失望了800 秒的成绩成为了比赛的 base line。
第一个版本并没有做什么设计基本上就是拍脑门的方案目的就是把流程跑通尽快出分然后在保证正确性的前提下逐步去优化方案避免一开始就过度设计导致迟迟不能出分影响士气。
整体设计
先回顾下赛题Apache RocketMQ 作为一款分布式的消息中间件历年双十一承载了万亿级的消息流转其中实时读取写入数据和读取历史数据都是业务常见的存储访问场景针对这个混合读写场景进行优化可以极大的提升存储系统的稳定性。 基本思路是当 append 方法被调用时会将传入的相关参数包装成一个 Request 对象put 到请求队列中然后当前线程进入等待状态。
聚合线程会循环从请求队列里面消费 Request 对象放入一个列表中当列表长度到达一定数量时就将该列表放入到聚合队列中。这样在后续的刷盘线程中列表中的多个请求就能进行一次性刷盘了增大刷盘的数据块的大小提升刷盘速度当刷盘线程处理完一个请求列表的持久化逻辑之后会依次对列表中个各个请求进行唤醒操作使等待的测评线程进行返回。 内存级别的元数据结构设计 ![endif]– 首先用一个二维数组来存储各个 topicIdqueueId 对应的 DataMeta 对象DataMeta 对象里面有一个 MetaItem 的列表每一个 MetaItem 代表的一条消息里面包含了消息所在的文件下标、文件位置、数据长度、以及缓存位置。
SSD 上数据的存储结构 总共使用了 15 个 byte 来存储消息的元数据消息的实际数据和元数据放在一起这种混合存储的方式虽然看起来不太优雅但比起独立存储可以减少一半的 force 操作。
数据恢复
依次遍历读取各个数据文件按照上述的数据存储协议生成内存级别的元数据信息供后续查询时使用。
数据消费
数据消费时通过 topicqueueId 从二维数组中定位到对应的 DataMeta 对象然后根据 offset 和 fetchNum从 MetaItem 列表中找到对应的 MetaItem 对象通过 MetaItem 中所记录的文件存储信息进行文件加载。
总的来说第一个版本在大方向上没有太大的问题使用 queue 进行异步聚合和刷盘让整个程序更加灵活为后续的一些功能扩展打下了很好的基础。
缓存
60 个 G的 AEP我垂涎已久国庆七天没有出远门的计划一定要好好卷一卷 llpl。下载了 llpl 的源码一顿看发现比我想象的要简单得多本质上和用 unsafe 访问普通内存是一模一样的。卷完 llpl缓存设计方案呼之欲出。
缓存分级
缓存的写入用了队列进行异步化避免对主线程造成阻塞到比赛后期才发现云 SSD 的奥秘就算同步写也不会影响整体的速度后面我会讲原因程序可以用作缓存的存储介质有 AEP 和 Dram两者在访问速度上有一定的差异赛题所描述的场景中会有大量的热读因此我对缓存进行了分级分为了 AEP 缓存和 Dram 缓存Dram 缓存又分为了堆内缓存、堆外缓存、MMAP 缓存(后期加入)在申请缓存时优先使用 Dram 缓存提升高性能缓存的使用频度。
Dram 缓存最后申请了 7GAEP 申请了 61GDram 的容量占比为 10%本次比赛总共会读取617/25084G 的数据根据日志统计整个测评过程中有 30G 的数据使用了 Dram 缓存占比 35%因为前 75G 的数据不会有读取操作没有缓存释放与复用动作所以严格意义上来讲在写入与查询混合操作阶段总共使用了 50G 的缓存其中滚动使用了 30-7/226.5G 的 Dram 缓存占比 53%。10%的容量占比却滚动提供了 53%的缓存服务说明热读现象非常严重说明缓存分级非常有必要。
但是现实总是残酷的这些看似无懈可击的优化点在测评中作用并不大毕竟这种优化只能提升查询速度在读写混合阶段读缓存总耗时是 10 秒或者是 20 秒对最后的成绩其实没有任何影响很神奇吧后面我会讲原因。
缓存结构 当获取到一个缓存请求后会根据 topicqueueId 从二维数组中获取到对应的缓存上下文对象该对象中维护了一个缓存块列表、以及最后一个缓存块的写入指针位置如果最后一个缓存块的余量足够放下当前的数据则直接将数据写入缓存块如果放不下则申请一个新的缓存块放在缓存块列表的最后同时将写不下的数据放到新缓存块中若申请不到新的缓存块则直接按缓存写入失败进行处理。
在写完缓存后需要将缓存的位置信息回写到内存中的Meta中比如本条数据是从第三个缓存块中的 123B 开始写入的则回写的缓存位置为3-1*每个缓存块的大小123。在读取缓存数据时按照 meta 数据中的缓存位置新定位到对应的缓存块、以及块内位置进行数据读取(需要考虑跨块的逻辑)。
由于缓存的写入是单线程完成的对于一个 queueId前面的缓存块的消息一定早于后面的缓存块所以当读取完缓存数据后就可以将当前缓存块之前的所有缓存都释放掉(放入缓存资源池)这样 75G 中被跳过的那 37.5G 的数据也能快速地被释放掉。
缓存功能加上去后成绩来到了 520 秒左右程序的主体结构也基本完成了接下来就是精装了。
优化
缓存准入策略
一个 32k 的缓存块是放 2 个 16k 的数据合适还是放 16 个 2k 的数据合适毫无疑问是后者将小数据块尽量都放到缓存中可以使得最后只有较大的块才会查 ssd减少查询时 ssd 的 io 次数。
那么阈值为多少时可以保证小于该阈值的数据块放入缓存能够使得缓存刚好被填满呢若不填满缓存利用率就低了若放不下就会有小块的数据无法放缓存读取时必须走 ssdio 次数就上去了。
一般来说通过多次参数调整和测评尝试就能找到这个阈值但是这种方式不具备通用性如果总的可用的缓存大小出现变化就又需要进行尝试了不具备生产价值。
这个时候中学时代的数学知识就派上用途了如下图 由于消息的大小实际是以 100B 开始的为了简化直接按照从 0B 进行了计算这样会导致算出来的阈值偏大也就是最后会出现缓存存不下从而小块走 ssd 查询的情况所以我在算出来的阈值上减去了 100B*0.75由于影响不大基本是凭直觉拍脑门的。如果要严格计算真正准确的阈值需要将上图中的三角形面积问题转换成梯形面积问题但是感觉意义不大因为 100B 本来就只有 17K 的 1/170比例非常小所以影响也非常的小。
梯形面积和三角形面积的比为(17K100)(17K-100)/(17k17K)0.999965完全在数据波动的范围之内。
在程序运行时根据动态计算出来的阈值大于该阈值的就直接跳过缓存的写入逻辑最后不管缓存配置为多大都能保证小于该阈值的数据块全部写入了缓存且缓存最后的利用率达到 99.5%以上。
共享缓存
在刚开始的时候按照算出来的阈值进行缓存规划仍然会出现缓存容量不足的情况实际用到的缓存的大小总是比总缓存块的大小小一些通过各种排查才恍然大悟每个 queueId 所拥有的最后一个缓存块大概率是不会被写满的宏观上来说平均只会被写一半。一个缓存块是32kqueueId 的数量大概是 20w那么就会有 20w*32k/23G 的缓存没有被用到3G/21.5G(前 75G 之后随机读一半所以要除以 2)就算是顺序读大块1.5G 也会带来 5 秒左右的耗时更别说随机读了所以不管有多复杂这部分缓存一定要用起来。
既然自己用不完那就共享出来吧整体方案如下 在缓存块用尽时对所有的 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 左右2G5G1G8G操作系统给 200M 左右基本就够了所以还剩 800M 没用这800M其实是可以用来作为 mmap 缓存的主要是考虑到大家都只能用 8G超过 8G 容易被挑战所以最后最优成绩里面总的内存的使用量并没有超过 8G。
基于末尾填补的 4K 对齐
由于 ssd 的写入是以 4K 为最小单位的但每次聚合的消息的总大小又不是 4k 的整数倍所以这会导致每次写入都会有额外的开销。
比较常规的方案是进行 4k 填补当某一批数据不是 4k 对齐时在末尾进行填充保证写入的数据的总大小是 4k 的整数倍。听起来有些不可思议额外写入一些数据会导致整体效益更高
是的推导逻辑是这样的“如果不填补下次写入的时候一定会写这未满的4k区如果填补了下次写入的时候只有 50%的概率会往后多写一个 4k 区(因为前面填补导致本次数据后移尾部多垮了一个 4k 区)”所以整体来说填补后会赚 50%。或者换一个角度填补对于当前的这次写入是没有副作用的(也就多 copy4k 的数据)对于下一次写入也是没有副作用的但是如果下一次写入是这种情况就会因为填补而少写一个 4k。 基于末尾剪切的 4k 对齐
填补的方案确实能带来不错的提升但是最后落盘的文件大概有 128G 左右比实际的数据量多了 3 个 G如果能把这 3 个 G 用起来又是一个不小的提升。
自然而然就想到了末尾剪切的方案将尾部未 4k 对齐的数据剪切下来放到下一批数据里面剪切下来的数据对应的请求也在下一批数据刷盘的时候进行唤醒
方案如下 填补与剪切共存
剪切的方案固然优秀但在一些极端的情况下会存在一些消极的影响比如聚合的一批数据整体大小没有操作 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 为单位这就意味着读取一个 16k2B 的数据极端情况下会产生 16k2*4k24k 的磁盘 io会多加载将近 8k 的数据。
显然如果能够在读取的时候都按 4k 对齐进行读取且加载出来的数据都是有意义的后续能够被用到就能解决而上述的问题我依次做了以下优化(有些优化点在后面被废弃掉了因为它和一些其他更好的优化点冲突了)。
1、大块置顶
![endif]– 由于每一批聚合的消息都是 4k 对齐的落盘的剪切扣留方案之前所以我将每批数据中最大的那条消息放在了头部(基于缓存规划策略大消息大概率是不会进缓存的消费时会从 ssd 读取)这样这条消息至少有一端是 4k 对齐的读取的时候能缓解 50%的对齐问题该种方式在剪切扣留方案之前确实带来了 3 秒左右的提升。
2、消息顺序重组
通过算法让大块数据尽量少地出现两端不对齐的情况减少读取时额外的数据加载量比如针对下面的例子 在整理之前加载三个大块总共会涉及到 8 个 4k 区整理之后就变成了 6 个。
由于自己在算法这一块儿实在太弱了加上这是一个 NP 问题折腾了几个小时效果总是差强人意最后只好放弃。
3、基于内存的 pageCache
在数据读取阶段每次加载数据时若加载的数据两端不是 4k 对齐的就主动向前后延伸打到 4k 对齐的地方然后将首尾两个 4k 区放到内存里面这样当后续要访问这些4k区的时候就可以直接从内存里面获取了。
该方案最后的效果和预估的一样差一点惊喜都没有。因为只会有少量的数据会走 ssd首尾两个 4k 里面大概率都是那些不需要走ssd的消息所以被复用的概率极小。
4、部分缓存
既然自己没能力对消息的存储顺序进行调整优化那就把那些两端不对齐的数据剪下来放到缓存里面吧 某条消息在落盘的时候若某一端(也有可能是两端)没有 4k 对齐且在未对齐的 4k 区的数据量很少就将其剪切下来存放到缓存里这样查询的时候就不会因为这少量的数据去读取一个额外的 4k 区了。
剪切的阈值设置成了 1k由于数据大小是随机的所以从宏观上来看剪切下来的数据片的平均大小为 0.5k这意味着只需要使用 0.5k 的缓存就能减少 4k 的 io是常规缓存效益的 8 倍加上缓存部分的余量分级策略会导致有很多碎片化的小内存用不到该方案刚好可以把这些碎片内存利用起来。
测评线程的聚合策略
每次聚合多少条消息进行刷盘合适是按消息条数进行聚合还是按照消息的大小进行聚合
刚开始的时候并没有想那么多通过日志得知总共有 40 个线程所以就写死了一次聚合 10 条然后四个线程进行刷盘但这会带来两个问题一个是若线程数发生变化性能会大幅下降第二是在收尾阶段会有一些跑得慢的线程还有不少数据未写入的情况导致收尾时间较长特别是加入了尾部剪切与扣留逻辑后该现象尤为严重。
为了解决收尾耗时长的问题我尝试了同步聚合的方案在第一次写入之后的 500ms对写入线程数进行统计然后分组后续就按组进行聚合这种方式可以完美解决收尾的问题因为同一个组里面的所有线程都是同时完成写入任务的大概是因为每个线程的写入次数是固定的吧但是使用这种方式尾部剪切扣留的逻辑就非常难融合进来了加上在程序一开始就固定线程数看起来也有那么一些不优雅所以我就引入了“线程控制器”的概念。 聚合策略迭代-针对剪切扣的留方案的定向优化
假设当前动态计算出来的聚合数量是 10对于聚合出来的 10 条消息如果本批次被扣留了 2 条下次聚合时应该聚合多少条
在之前的策略里面还是会聚合 10 条这就意味着一旦出现了消息扣留聚合逻辑就会产生抖动会出现某个线程聚合不到指定的消息数据量的情况这种情况会有 poll 超时方式进行兜底但是整体速度就慢了。
所以聚合参数不能是一个单纯的、统一化的值得针对不同的刷盘线程的扣留数进行调整假设聚合数为 n某个刷盘线程的上批次扣留数量为 m那针对这个刷盘线程的下批次的聚合数量就应该是 n-m。
那么问题就来了聚合线程(生产者)只有一个刷盘线程消费者有好几个都是抢占式地进行消费没办法将聚合到的特定数量的消息给到指定的刷盘线程所以聚合消息队列需要拆分拆分成以刷盘线程为维度。
由于改动比较大为了保留以前的逻辑就引入了聚合数量的“严格模式”的概念通过参数进行控制如果是“严格模式”就使用上述的逻辑若不是则使用之前的逻辑
设计图如下 将聚合队列换成了聚合队列数组在非严格模式下数组里面的原始指向的是同一个队列对象这样很多代码逻辑就能统一。
聚合线程需要先从扣留信息队列里面获取一个对象然后根据扣留数和最新的聚合参数决定要聚合多少条消息聚合好消息后放到扣留信息所描述的队列中。
完美的收尾策略一行代码带来 5s 的提升
引入了线程控制器后收尾时间被降低到了 2 秒多两次收尾也就是 5 秒左右(这些信息来源于最后一个晚上对 A 榜时的日志的分析)在赛点位置上这 5 秒的重要性不言而喻。
比赛结束前的最后一晚分数徘徊在了 423 秒左右前面的大佬在很多天前就从 430 一次性优化到了 420然后分数就没有太大变化了我当时抱着侥幸的态度断定应该是 hack 了直到那天晚上在钉钉群里和他聊了几句直觉告诉我420 的成绩是有效的。当时是有些慌的毕竟比赛第二天早上 10 点就结束了。
我开始陷入深深的反思我都卷到极致了从 432 到 423 花费了大量的精力为何大神能够一击致命不对一定是我忽略了什么。
我开始回看历史提交记录然后对照分析每次提交后的测评得分由于历史成绩都有一定的抖动所以这个工作非常的上头花费了大概两个小时总算发现了一个异常点在 432 秒附近的时候我从同步聚合切换成了异步聚合然后融合了剪切扣留4k 填补的方案按理说这个优化能减少 3G 多的落盘数据量成绩应该是可以提升 10 秒左右的但是当时成绩只提升了 5 秒多由于当时还有不少没有落地的优化点所以就没有太在意。
扣留策略会会将尾部的请求扣留下来尾部的请求本来就是慢一拍(对应的测评线程慢)的请求队列是顺序消费这一扣留进度就更慢了
聚合到一批消息后按照消息对应的线程被扣留的次数从大到小排个序让那些慢的、扣留多的线程尽可能不被扣留让那些快的、扣留少的请求尽可能被扣留最后所有的线程几乎都是同时完成基于假想。
赶紧提交代码、开始测评抖了两把就破 420 了最好成绩到达了 418比优化前高出 5 秒左右非常符合预期。
查询优化
多线程读 ssd
由于只有少量的数据会读 ssd这使得在读写混合阶段sdd 查询的并发量并不大所以在加载数据时进行了判断如果需要从 ssd 加载的数量大于一定量时则进行多线程加载充分利用 ssd 并发随机读的能力。
为什么要大于一定的量才多线程加载如果只需要加载两条数据用两个线程来加载会有提升吗当存储介质够快、加载的数据量够小时多线程加载数据带来的 io 时间的提升还不足以弥补多线程执行本身带来的程序开销。
缓存的批量 copy
若某次查询时需要加载的数据在缓存上是连续的则不需要一条一条从缓存进行复制可以以缓存块的大小为最小粒度进行复制提升缓存读取的效益。 上面的例子中使用批量 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 盘的一个放大版。 SSD 云盘的底层存储介质是多个普通的物理硬盘这些物理硬盘就类似于传统 ssd 中的存储颗粒在进行写入或读取的时候会将任务分配到多个物理设备上并行进行处理。同时在云 ssd 中对数据的更新采用了 append 的方式即在进行更新时是顺序追加写一块数据然后将位置的引用从原有的数据块指向新的数据块我们访问的文件的position和硬盘的物理地址之间有一层映射所以就算硬盘上有很多的碎片我们也仍然能获取到一个“连续”的大文件。
阿里云官网上有云 ssd 的 iops 和吞吐的计算公式 iops min{180050 容量, 50000}; 吞吐 min{1200.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 循环暴力破解
原文链接
本文为阿里云原创内容未经允许不得转载。