Contents
  1. 1. PolarDB阿里初赛问题记录
    1. 1.0.1. 测试流程
      1. 1.0.1.1. 并发问题
      2. 1.0.1.2. key-index文件是否需要顺序加载
      3. 1.0.1.3. key的覆盖和value的覆盖
      4. 1.0.1.4. mappedbyteBuffer的回收
      5. 1.0.1.5. int long溢出问题
    2. 1.0.2. 测试结果
  2. 1.1. 性能问题
    1. 1.1.1. 增加jvm参数
    2. 1.1.2. 无参数,使用intellij默认
    3. 1.1.3. 只增加内存大小
    4. 1.1.4. 订版本
    5. 1.1.5. 线上环境和线下环境的区别

PolarDB阿里初赛问题记录

这篇纯碎是碎碎念记录。

每个value都是4KB,总共最多会写6400W个value,算下来就是64 * 1000 * 1000 * 4 * 1024 Bytes ≈ 256G。

每个value存储到文件中的时候,需要知道它在文件中的位置,这个位置是一个长整型,8 Bytes。Key也是8 Bytes。这两个值要放在一起,以便我们能在内存中构建起一对一的索引。而它们的存储所耗的最大空间是(8 + 8) * 64 * 1000 * 1000 Bytes ≈ 1G。

Key和Value还需要落盘,所以它们也需要大约1GB的磁盘空间。

以上是大佬写的,我的初始实现是magic2 + key8 + magic2 + pos4落盘

感觉浪费了四个magic位,不过这四位只会存盘不会存到内存。
4*8的整型数值存储为是2GB地址空间[带符号位,可以删除扩充到4GB],然后2GB*4096位的长度,实际上可以存8T的磁盘,如果删除符号位可以存16T磁盘,应对测试环境的6400W条数据没有问题,灵位内存用key8+pos4,可以相对减少内存占用。

测试流程

  1. 写入64W数据,读取64W数据
  2. 重启进程,读取64W数据
  3. 重启进程,读取64W数据过程中kill -9进程,然后再行重启读取64W

测试过程发现

  • 1 失败 1成功 2 成功 1成功 2 成功 1成功 2 成功
  • 1 成功 1失败 2 失败 1失败 2 失败 1失败 2 失败
  • 初次操作,使用<byte[], Integer>组装concurrenthashmap,但是调整一直出错,自己的测试有毛病,提交测试之后还通过正确性测试。
  1. 以上两项测试的原因在于,同内存写入的时候,进行读取的操作,key值的内存数值一样,所以在第一次1里面成功。但是进程重启之后,byte[]的hashcode是会随着进程的不同而变化的,所以找不到key,进行get时候会报空。
  2. 没有覆盖的问题:我在写入key的时候,如果是覆盖性操作,就将值写入到oldPosition位置,但是发现连续进行1的测试流程时候,值并未覆盖到oldPosition,文件同样会增长,key数量也会增长,并未进行覆盖key操作。经过调试,发现byte[]的操作太过风骚,从文件读取出来的byte[]和测试生成的byte[]的来源不一样,所以hashcode不一样,同时也用到equal操作,于是也要重写equal操作,使用Array.equal代替。
  3. 发现byte[]直接写入hashmap会读取不到,对于基础类型数值,hashcode值每次jvm进程中数值都不一样,这影响key的分布和查找
    于是使用bytebuffer,正确性检查没问题,不过超时,主要是因为性能太差,byte[]与bytebuffer的转换过程不能忍受
  4. 回归使用byte[],解决了hashcode的问题[仿照bytebuffer重写hash]之后,发现equal函数也不一样,会调用原生的==进行判断,于是改用array.equal进行判断
  5. byte[]还是出问题,写入6400w的阶段总是读取校验出错,打印出数量不够6400W,总是差300-400个,然后发现,hashcode函数有问题,,重写,检验了3亿个hash值,确定不会重复之后提交,总算通过,最后成绩243.44秒,最好排35名,最差到55名

并发问题

Exception in thread "Thread-45" java.lang.ClassCastException: java.util.HashMap$Node cannot be cast to java.util.HashMap$TreeNode
    at java.util.HashMap$TreeNode.moveRootToFront(HashMap.java:1832)
    at java.util.HashMap$TreeNode.putTreeVal(HashMap.java:2012)
    at java.util.HashMap.putVal(HashMap.java:638)
    at java.util.HashMap.put(HashMap.java:612)
    at org.lee.MappedPageSource.lambda$initKeys$0(RaceConcurrentHashMap.java:1362)
    at java.lang.Thread.run(Thread.java:882)

多线程加载key-index文件的过程中,多线程put到一个hashmap会引发上述问题,多个线程修改同一映射,使用同步锁,顺序put解决

key-index文件是否需要顺序加载

不需要,因为key-pos内存的键值覆盖会直接覆盖value的pos位置,这样,一个键值映射的value文件位置是不变的,所以key-index文件中,不存在相同key的数据块。

key的覆盖和value的覆盖

目前使用洁净的文件策略,由于mappedbyteBuffer的分配尽量以大数据块为主,所以存在kill -9情况下没有进行truncate的问题

  1. 对于正常结束,调用truncat进行keyfile和valueFile的截断
  2. 强制退出情况下,下次初始化时候,加载key文件,对非MAGIC开始的字节,进行一刀切,后续字节被truncate,相应的后续value文件被截断,正确初始化count数量,便于下次写value的时候,写入count自增的地址

mappedbyteBuffer的回收

请求的操作无法在使用用户映射区域打开的文件上执行 java nio

使用mappedbyteBuffer的缓存之后,尝试去关闭,报错,原来还在读的地方使用了buffer没有及时关闭导致的问题。

int long溢出问题

public byte[] readValue(int address) {
    if(address % 50000 == 0 && System.currentTimeMillis()%5000==0)
      log.info("{}", address);
    long pos = address<<VALUE_BIT;
    byte[] result = new byte[1<<VALUE_BIT];
    long size= 0;
    try {
      size= raf.length();
      raf.seek(pos);
      raf.read(result);
    } catch (Exception e) {
      log.info("ADDRESS={} POS={} len={}", address, pos, size);
      e.printStackTrace();
    }
    return result;
  }

这里数量增长之后偶尔在seek操作时报错,java.io.IOException: Negative seek offset
因为存在内存中的position是int整型,映射文件position是乘以地址块4K(右移12),所以这里传参用整型,但是问题在于<<12之后是int这时候就会溢出了显示pos是一个负数。最后类型申明改为long就不会溢出了。【后来改用filechaanel进行pos位置的直接读取】

测试结果

640W的数据,调整了 initialCapacity 值之后,不会引起rehash操作,这个比较耗时

Connected to the target VM, address: '127.0.0.1:53096', transport: 'socket'
13:37:30,512 INFO  org.lee.MappedPageSource.initKeys(RaceConcurrentHashMap.java:1343) - 0
13:37:30,556 INFO  org.lee.disk.App.test(App.java:30) - start write
13:40:46,510 INFO  org.lee.disk.App.test(App.java:35) - write elapsed 3115 ms
13:43:34,460 INFO  org.lee.disk.App.test(App.java:43) - detach assert read elapsed 363903 ms

性能问题

https://blog.csdn.net/keketrtr/article/details/74448127
使用jvisualvm的插件GC进行判断,设置参数平截图,减少GC问题
以下是四张图,表示顺序写入+顺序读取640W次数的过程

增加jvm参数

-server -Xms1536m -Xmx1536m -XX:MaxDirectMemorySize=512m -XX:MaxMetaspaceSize=300m -XX:NewRatio=1 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:-UseBiasedLocking

无参数,使用intellij默认

只增加内存大小

-server -Xms1876m -Xmx1876m -XX:MaxDirectMemorySize=512m -XX:MaxMetaspaceSize=300m -XX:NewRatio=1 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:-UseBiasedLocking

订版本

-server -Xms1876m -Xmx1876m -XX:MaxDirectMemorySize=512m -XX:MaxMetaspaceSize=128m -XX:NewRatio=1 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:-UseBiasedLocking

线上环境和线下环境的区别

线上64CPU, ssd云盘,它的IO性能和CPU性能和本地机器没有任何可比性,于是本地640W写入过程中,速度改善的方法,在线上可能速度变慢。
最后经过总结,云盘SSD的IO通道能同时支持大写文件的并发写入读取,由于IO足够,写入的时候如果是多文件写入,可以减少文件写入锁带来的占用问题,而线下环境中,IO性能差劲,单文件的写入效果远胜于多文件的写入。对于读取性能,线上环境直接使用CPU缓存进行二进制的读取,不使用缓存的方式远胜于使用内存缓存MappedByteBuffer的方式,而线下环境中,如果使用内存缓存,读取的性能会更好,这里主要差别在于64核CPU的计算和缓存速度与本地CPU的不同。

Contents
  1. 1. PolarDB阿里初赛问题记录
    1. 1.0.1. 测试流程
      1. 1.0.1.1. 并发问题
      2. 1.0.1.2. key-index文件是否需要顺序加载
      3. 1.0.1.3. key的覆盖和value的覆盖
      4. 1.0.1.4. mappedbyteBuffer的回收
      5. 1.0.1.5. int long溢出问题
    2. 1.0.2. 测试结果
  2. 1.1. 性能问题
    1. 1.1.1. 增加jvm参数
    2. 1.1.2. 无参数,使用intellij默认
    3. 1.1.3. 只增加内存大小
    4. 1.1.4. 订版本
    5. 1.1.5. 线上环境和线下环境的区别