用java搞数据库

目的

  • 设计一个简化,高效的KV存储引擎。
  • 要求提供write,read,range搜索接口。

要求

  • 并发写入数据性能。
  • 任意执行kill -9来模拟进程意外退出而数据不丢失。

IO

  • key固定为8字节,可以用long表示。
  • value为4kb,4kb整数落盘是非常磁盘IO友好的。
  • 4kb可以在内存中做索引,可以使用int而不是long来记录数据偏移,内存占用会减少一半。

kill -9 数据不丢失

光使用内存做存储很难满足这一点。但是没有要求断电不丢失,也就是说:可以使用pageCache来做写入缓存。 所以想到使用pageCache来充当数据和索引的写入缓冲(两者策略不同)。 方案具体可以参考ES的pageCache方案。

随机读写

按照随机写,随机读,顺序读进行实验。 随机写阶段不需要在内存维护索引,可以直接落盘。 随机读和顺序读,磁盘均存在数据,恢复索引可以采用多线程并发恢复。

pageCache处理

由于采用了pageCache,采用脚本方式清空pageCache会比较耗时。 所以不能无节制的使用pageCache,所以准备引入Direct IO。

文件IO

由于key可以均匀分布,采用数据分区方式,可以大大减少顺序读写的锁冲突,key分布均匀可以按照key搞n位来做hash,可以确保key两个分区之间整体有序,于是可以尝试将数据分成1024,2048个分区。

架构设计

随机写入key时,可以根据key进行hash将随机写转换成对应分区的文件顺序写。

内存维护有序的 key[1024][625000] 数组和 offset[1024][625000] 数组。

利用数据分布均匀特性,将全局数据hash为1024分区,每个分区存放两类文件:索引文件和数据文件(对kafka熟悉的同学,是不是很亲切)。

在随机写入阶段,根据key获得该数据对应分区位置,按照时序,顺序追加到文件尾部,将全局随机转换为局部顺序。

利用索引和数据一一对应特性,不需要将数据的逻辑偏移落盘,在恢复阶段可以按照恢复key的测序,反推value的逻辑偏移量。

由于做了分区,在range查询阶段,partition(N)中任何一个数据一定大于partition(N-1)中任何一个数据,于是我们可以大块的读,将一个partition整体读进内存,给64个线程消费。

读盘线程负责按分区读盘进入内存,64个消费线程消费内存,按照key顺序访问内存,进行回调。

优化

使用pageCache实现写入缓冲区

磁盘IO类型的系统,第一步是测量磁盘IOPS及多少个线程一次读写多大的缓存能够打满IO,在固定64线程写入前提下,16kb,64kb均可达到理想IOPS,所以可以为每个分区分配一个写入缓存,凑齐4个value落盘。

由于要求kill -9不丢失数据,不能简单的在内存中分配一个ByteBuffer.allocate(4096*4);,可以考虑mmap内存映射一片写入缓冲,凑齐4个刷盘,这样kill -9之后,pageCache不会丢失。

索引文件落盘比较简单,key固定为8b,所以mmap可以发挥写小数据的优势,将pageCache利用起来,mmap相比filechannel写索引快3s左右。

随机写入后不会立即随机读,所以不需要在写入时维护内存索引,只需要在恢复阶段恢复索引顺序,反推出数据的逻辑偏移,因为key和value在同一个分区的位置是一一对应的。

恢复阶段

需要在数据库引擎启动时,将索引从数据文件恢复到内存中。

由于有1024个分区,可以使用64个线程并发恢复索引,使用快速排序对 key[1024][62500] 数组和 offset[1024][62500] 进行 sort,之后再 compact,对 key 进行去重。

数据随机读取

根据key定位到分区,之后在有序的key数据中进行二分查找key/offset,拿到数据的逻辑偏移和分区编号,可以随机读取了。

数据顺序读取

顺序读取思路是生产者消费者模型,n个生产者从磁盘读数据放入内存,64个消费线程消费同时判断内存数据以验证数据。

直接内存的使用和JVM调优

堆外内存的好处是大大减少了一份内存拷贝,并且对gc友好。

-
server 
-
Xms2560m
 
-
Xmx2560m
 
-
XX
:
MaxDirectMemorySize
=
1024m
 
-
XX
:
NewRatio
=
4
 
-
XX
:+
UseConcMarkSweepGC
 
-
XX
:+
UseParNewGC
 
-
XX
:-
UseBiasedLocking
  • young区过大,对象在年轻代呆的太久,多次拷贝。
  • old区太小,会频繁触发old区的cms gc。

对于那些需要反复new出来的东西,都可以池化,分配内存在回收也是不小的开销,直接可以使用threadlocal缓存搞定。

减少线程切换

io线程的切换成本很高,为了减少io线程的时间片流失可以考虑使用while(true)轮训,也可以采用sleep(1us)避免cpu空转带来的整体性能问题。

机器抖动在所难免,避免IO切换不能靠while(true),cpu级别的优化可以专门腾出4个核心专门给IO线程使用,避免IO线程的时间片征用(采用Affinity )。

猜你喜欢

转载自my.oschina.net/u/1000241/blog/2987305
今日推荐