阿里PolarDB数据引擎比赛总结

参加了阿里PolarDB数据引擎比赛,全部使用JAVA实现,相比较使用C++的同学成绩差的比较远,只能寄希望于组内的C++同学继续加油啦。Java在内存控制和底层操作较C++相差较大,但通过多次优化速度提了上来,超过了阿里内部员工Java代码最好成绩,也是一次很有收获的比赛。

本次比赛要求实现put(byte[] key, byte[] value)、read(byte[] key)和range(byte[] lower, byte[] upper, AbstactVisitor visitor)方法,程序测评过程是进行:

    随机写;

    kill程序;

    随机读;

    两次range顺序读。    

要求如下:

1、支持多线程

2、在程序突然关闭的情况下保证数据不会丢失

3、可能有多个相同的key被put,引擎能根据key进行value更新

4、Java内存要求在2.5G内

5、64个线程并发随机写入和读取,每个线程1000000条数据(key 8B,value 4KB),总共写入64000000条数据,数据总量250G

6、每个线程两次range操作,按key值顺序读取所有写入的数据

7、代码语言:C++ 或 JAVA

写入

拿到题目最开始我们就卡在掉电如何保存的问题上,由于key值只有8B,如果每来一条数据就落盘可能会严重影响性能,同时两条key相同的数据如何能够保证写入后一个数据呢?因为项目中经常使用HBase,对其存储模型比较熟悉,我们借鉴了LSM模型的思想,将最近put的数据放在内存中,同时为了应对掉数据丢失问题,采用WAL(预写日志系统),在每put一条数据时写入一个日志记录当前事件。具体的写入模型如下图所示:

当数据到来的时候,首先写入Value并记录其文件中的位置seq,方便后续查找。随后将key与seq(int)通过Mmap写入日志文件,方便后续数据恢复,此处使用Mmap虚拟内存映射主要应对小数据量的读写,但其实本身是顺序写入,也可以使用FileChannel写入,有Page Cache缓冲性能相差不大,而且Map本身有一定的时间开销(此处将map操作异步放入线程池进行)。最后写入key,seq到TreeMap,目的主要是对key值进行排序,方便后续查找时索引的建立,同时对于部分key值相同的数据也能在内存中及时更新value的seq,treemap写到一定数据量落盘SST,并记录目前写入的seq,程序被突然掉电时,恢复的时候首先根据记录的最后一个seq,恢复之后的数据即可。这里需要考虑的问题在于write(value)是顺序操作,如果给某个线程的value先分配了seq,另一个线程后分配seq,但抢先将value写入了文件,两者写入位置对换,则查找时就会发生not match错误,需要对put方法加锁。但直接对方法加锁会极大的影响性能,64线程的写入会非常慢,可以将put操作分区,申请多个put实例,使用路由对不同的key转发到不同的put,极大的缓解了加锁的压力,具体如下图所示:

写入耗时124s

读取

读取也是64个线程并发随机读取,需要对所有key建立索引,第一步需要排序。64000000 * (8 + 4) = 768M,如此大的数据量无法同时加载到内存中进行排序,只有每个分区单独排序,所以在路由时按照桶排序的方式指引不同key去不同的分区。这样每个分区间的数据是排序的,每个分区内的数据有很多SST,使用归并排序算法,由于SST是已经排序好的结构,每个分区只要将SST合并成一块大的SST即可,归并排序比较好实现,使用多个三维byte数组和一个队列,所有分区排序完成只需13秒。由于数据量较大,无法建立一级索引,此处使用两级索引,内存中存放第一级索引,查找时找到对应的block,加载到内存,再查找seq。建立的大SST结构如下图所示:

那么每次查找只需要先在blockIndex进行二分查找找到block所在位置(在这里我们可以使用treemap.floorkey()方法),从磁盘读取这块block(4KB IO),使用二分查找找到对应的key和seq,按照seq读value 4kB。总的查找耗时,两次内存二分 + 两次磁盘4KB IO。

查找所有数据耗时160s。

range

range是64个线程同时发送请求,指定对应的边界(lower, upper)。使用visitor.visit(byte[] key, byte[] value)返回数据,visit调用需要按照key顺序进行。总的查找数据量是64 * 2 * 64000000,如此大的数据量查找实现时间很长,几乎不可能。此处方法是,接收来自线程的range请求并阻塞线程,等待一段时间,收到所有的线程range请求后才开始读取数据,所有数据读取完成后notify线程,由于64个线程range需要的数据相同,完整遍历数据一遍即可满足64个线程的一次range请求。

关键问题在于如何快速按顺序遍历一次数据呢?

我们已经对所有的key值建立索引,进行了排序,只需一个个SST加载并根据seq读取value即可,此处自己写个一个顺序读取key,seq的迭代器,并使用一个数组实现了队列,将key,seq放在队列并根据队列的seq多线程查找value即可,具体遍历过程如下图所示:

耗时220s * 2,线上读取速度1.1G/s,本地读取900M/s(wd black 3d)

第一次优化

在第一周完成了所有的要求后,发现这个模型还有较大的优化空间,虽然所有的读写都是使用Java NIO,但是在随机读取value时只是4KB读,完全不能打满磁盘。考虑linux系统的page cache预读取,随机读取能够命中的概率太低了,这产生了大量的IO冗余。所以第一次我的优化点在于range时如何将小文件的4KB读取修改为大文件的读取。回顾一下,数据在写入时是随机写入,需要一次性加载一个完整的存储value的文件放入内存,才能在range的时候不会重复加载该文件。并且由于内存不足,在最开始分区时我分了32个区,那么每个value文件的大小是64000000 * 4 * 1024B / 32 = 8G,不可能完全加载到内存,因此,我在写入value的时候对文件再次按照桶排序方式分片128个(key值随机分布,比较均衡),每个文件的大小8G/128=64M,这样就可以完全加载到内存了。非常高兴的一点是,阿里云的linux服务器支持65536个文件句柄,所以同时打开128 * 32 = 4096的文件完全没有问题,将随机读写修改后并测试,没有降低速度。接下来全新的range过程如下图所示:

ByteBuffer开的是堆外内存,每个64M,一共开了6个,value队列是384M,内存队列的长度考虑了GC频率,找到最合适的点。

经过该轮优化后,range总耗时360s,线上速度1.4G,本地测试1.5G

第二次优化

初见成效后进行了第二轮优化,优化点在于随机读取时索引的查询,目前需要两次内存二分查找和一次4KB IO操作,非常耗费时间,如果能够把全部索引放入内存中,就只有两次内存查找,速度能够迅速提升。但是所有索引载入内存需要(8 + 4)* 64000000 = 800M(8byte key,4byte seq),在Java只有2.5G内存的情况下根本不可能申请如此大的空间,就算能够成功申请,由于剩余内存过少引发的频繁GC也会严重影响性能,得不偿失。开源不行,我们只能节流。之前一个分区一个文件,每个分区的seq最大是64000000/32 = 2000000,所以使用4byte(int)空间表示seq,现在文件进行了切片,每个文件被切成128份,每个文件的seq最大只有15000,完全可以使用2byte(short)来表示。这样我们索引占用空间能够减少20%。但依旧有600多M,远远不够。下一步我们对key进行动手,由于使用桶排序,在一个block中的key,前两个字节大部分完全相同,可以舍弃。我们于是在block中优化,如下图所示:

建立索引时取最后4个字节进行hash指向0~340,找到341个位置中该key存储的位置存放,如果地址冲突则后移一位(可以使用其他hash策略减少移地址次数)直到找到正确的key。查找key时先hash到对应位置,再使用第3和第4个字节进行校验,校验不成功则地址往后移位,直到找到为止。在本地测试的时候,能够通过,但在线上出现了冲突情况(两个key在hash后指向同一个地址,并且2byte校验码也相同),2byte用于校验可能不够,于是修改为4byte校验,程序通过。总的索引存储空间只有原来的一半,384M,这完全可以加载到内存中,随机读少了一次4KB IO。时间由原来的160s减少到120s。

优化总结

1、尽管目前的ssd对4K随机读写支持非常好,但是由于linux有page cache存在,随机读速度会受到较大影响,毕竟随机读取不可能命中缓存,或者人品大爆发。需要修改为大文件读写,或者使用C++代码,绕过page cache(Java 可以使用JNI调用C++)。本次比赛有C++代码同学,我全部使用JAVA,如果使用JNI那写JAVA就没什么意思了。

2、尽可能减少对象申请,初始化和分配空间以及回收空间都会大大减缓程序运行速度,使用对象池或ThreadLocal会更好。

3、尽可能使用local变量, full GC比young GC时间长。

4、正确使用jvm_parameter会让你的程序运行速度提升。

5、一个通用的程序去做一件专用的事会有大量冗余,所以尽可能根据场景对数据结构进行优化。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容

  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,928评论 2 89
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,096评论 1 32
  • 最近一段时间,接触了一些企业的老板与高管,给我印象最深的是一个KTV 老板与他们的高管。上星期六由于是校友会的邀请...
    下半辈子_阅读 506评论 0 0
  • 别人认为我是对是错的,其实都不重要。只是在那一刻感到人性的冷漠,明明是有人知道事情的真相,还任人诬蔑我…… 原来自...
    逆风追梦人阅读 156评论 0 0
  • 朝露点点洒春闺, 客逢深径赋婉约, 不慕公车为闲游, 牵伴已去笑红尘。
    理疗瑜伽邵阅读 207评论 0 1