第五届阿里天池中间件比赛经历分享-决赛

消息持久化存储引擎

题目

实现一个进程内消息持久化存储引擎,要求包含以下功能:

  • 发送消息功能

  • 根据一定的条件做查询或聚合计算,包括

A. 查询一定时间窗口内的消息 B. 对一定时间窗口内的消息属性某个字段求平均,以及求和

例子:t表示时间,时间窗口(1000, 1002)表示: t>1000 & t<1002

消息内容简化成两个字段,一个是业务字段a(整数),一个是时间戳(long)。

1800内接收20亿条,每条50B,总共100G,每秒大概100W条,50M

SSD读写300M/s

思路

初版

  • 分片:因为内存保存不下全部Msg,需要分片存储和读取。参考kafka的时间戳索引概念,按照时间戳分片&落盘,落盘文件名即为分片index。比如插入ts=0x 0111时,如果分区个数为4,则index=ts>>2 = 01

    • 当某一个分片满了之后先不落盘,因为有可能出现数据晚到,当晚到的数据到达是,对应的分片如果已经落盘了,还需要重新读出来、放入该分片中,再落盘(当然也可以直接追加到文件尾部,但是还不如一次性把文件写好效率高)。所以记录一个waterMark,这里代表已经落盘的shardId,当最新的分片Id超过waterMark2个时,再落盘waterMark++;

    • 分片内为了保证顺序,使用索引,可以用跳表存储Msg(顺序插入时TreeSet红黑树旋转开销大)。查询时,定位文件分片+文件内部二分

    • t可以相同,如何存储a,body?,所以每个t要对应一个List<Long> 和 List<body>

  • 底层存储:

    • t,a,body可以分别存储到不同的文件,在查询avg时,不需要查询body

    • t,a,body分片的粒度可以不同,考虑:t作为索引,是应该尽量保存到内存?还是可以缩小粒度,但是几乎不存储索引,而是让更多的body保存到内存,可以减少IO?

    • nio 落盘

      • 面向块,而不是字节

      • 通道(Channel),一种新的原生I/O抽象概念

      • 内存映射的文件

      • 通过Selector多路复用、非阻塞的I/O能力实现可伸缩的服务器架构

    • 序列化:Java自带序列化性能差、冗余多。考虑直接保存原始数据而不是对象。

  • 查询

    • lru:LRU或LFU,但是发现会发生大量minorGC,原因是生成了过多对象。所以就题目来讲,不需要使用对象封装消息,直接对t(long)或者body(byte数组)进行操作。

    • 为何何最终采用LRU

      • LFU淘汰一定时期内被访问次数最少的页, 但是比赛的getT是平均的,最近访问的最少,反而后面更有可能被访问

      • LRU淘汰最近最少使用的页,不考虑访问次数

实例代码

消息写入:

// 保存A
int shardId = getShardAId(msgT);
NavigableMap<Long, List<Long>> targetShardA;
// 当分片还未创建,则生成分片
if (!ShardAHasCreated.get(shardId)) {
    ConcurrentSkipListMap<Long, List<Long>> newShard = new ...
    ShardAHasNotSaved.put(shardId, newShard);
// t-->t时刻的所有a , BlockingQueue<NavigableMap<Long, List<Long>>>,  SaveAJob线程会负责将该queue落盘
    shardsA2SaveQueue.put(newShard);
    ShardAHasCreated.set(shardId);
}
targetShardA = ShardAHasNotSaved.get(shardId);
if (targetShardA == null) {// 数据晚到,但是分片早已落盘,则应当load
}

List<Long> arrA = targetShardA.computeIfAbsent(msgT, t -> new ArrayList<>());
arrA.add(message.getA());

// 保存Body
shardId = getShardBodyId(msgT);
NavigableMap<Long, List<byte[]>> targetShardBody;
if (!ShardBoydHasCreated.get(shardId)) {
    ConcurrentSkipListMap<Long, List<byte[]>> newShard = new ..
    ShardBodyHasNotSaved.put(shardId, newShard);
    shardsBody2SaveQueue.put(newShard);
    ShardBoydHasCreated.set(shardId);
}
targetShardBody = ShardBodyHasNotSaved.get(shardId);
if (targetShardBody == null){// 数据晚到,但是分片早已落盘,则应当load
}
List<byte[]> arrBody = targetShardBody.computeIfAbsent(msgT, t -> new ArrayList<>());
arrBody.add(message.getBody());

其中,获取分片的方法如下

private static int getShardAId(long t) {
    //取t的高 SHARD_A_SHIFT 位,作为分片的index
   return (int) (t >> SHARD_A_SHIFT);
}

private static long SHARD_A_NUM = 1L << 46;
private static int SHARD_A_SHIFT = 64 - (int) (Math.log(SHARD_A_NUM) / Math.log(2D));  

private static int getShardBodyId(long t) {    
    return (int) (t >> SHARD_BODY_SHIFT);
}  

private static long SHARD_BODY_NUM = 1L << 46;
private static int SHARD_BODY_SHIFT = 64 - (int) (Math.log(SHARD_BODY_NUM) / Math.log(2D));

消息落盘

private static final int SAVE_WAITING_INTERVAL_SHARD_NUM = 2;// 当第i+n个shard创建后,才落盘第i个分区
private static class SaveAJob implements Runnable {
    public static final String SHARD_A_FILE_PREFIX = SAVE_PATH + "A";
    private static final BitSet shardHasSaved = new BitSet();

    public void run() {
        while (true) {
                StringBuilder sb = new StringBuilder();
                NavigableMap<Long, List<Long>> shard = shardsA2SaveQueue.take();
                while(shard.size()==0)
                    Thread.sleep(50);
                int shard2SaveId = getShardAId(shard.firstKey());
                while (shard2SaveId > getShardAId(maxT) - SAVE_WAITING_INTERVAL_SHARD_NUM)
                    Thread.sleep(50);
                saveShard(ShardAHasNotSaved.get(shard2SaveId),
                          sb.append(SHARD_A_FILE_PREFIX).append(shard2SaveId).toString());
                shardHasSaved.set(shard2SaveId);
                ShardAHasNotSaved.remove(shard2SaveId);
                sb.setLength(0);
        }
    }

    private void saveShard(NavigableMap<Long, List<Long>> shard, String fileName) throws IOException {
        Instant s = Instant.now();

        ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
        FileChannel outFile = FileChannel.open(Paths.get(fileName), StandardOpenOption.APPEND,
                                               StandardOpenOption.CREATE);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(shard);
        byte[] shardBytes = baos.toByteArray();
        for (int offset = 0; offset < shardBytes.length; offset += 4096) {
            buffer.put(shardBytes, offset, shardBytes.length - offset < 4096 ? shardBytes.length - offset : 4096);
            buffer.flip();
            outFile.write(buffer);
            buffer.clear();
        }
        outFile.close();
        long l = Duration.between(s, Instant.now()).toMillis();
}

数据查询

public List<Message> getMessage(long aMin, long aMax, long tMin, long tMax) {
    if (tMin < minT) tMin = minT;
    if (tMax > maxT) tMax = maxT;

    List<Message> res =
            LongStream.rangeClosed(tMin, tMax)
                      .parallel()
                      .boxed()
                      .flatMap(t -> {
                          int shardId = getShardAId(t);
                          NavigableMap<Long, List<Long>> targetShard = loadShardA(shardId);
                          if (targetShard == null)
                              return null; //如果t不连续,有可能整个分片不存在
                          List<Long> longs = targetShard.get(t);
                          if (longs == null)
                              return null; //如果t不连续,有可能某个t不存在任何Msg

                          int shardIdB = getShardBodyId(t);
                          NavigableMap<Long, List<byte[]>> targetBodyShard = loadShardBody(shardIdB);
                          List<byte[]> bytes = targetBodyShard.get(t);
                          return IntStream.range(0, longs.size())
                                          .mapToObj(i -> new Message(longs.get(i), t, bytes.get(i)));
                      })
                      .filter(msg -> msg.getA() >= aMin && msg.getA() <= aMax)
                      .collect(toList());

    return res;
}
private static NavigableMap<Long, List<Long>> loadShardA(int shardId) {
        shardACount.increment();
        if (!SaveAJob.shardHasSaved.get(shardId)) {//还未落盘
            return ShardAHasNotSaved.get(shardId);
        }
        return cache4A.get(shardId);
}

private static final LinkedHashLRUCache<Integer, NavigableMap<Long, List<Long>>> cache4A =
        new LinkedHashLRUCache<>(SHARD_A_CACHE_SIZE)             
        {
            @Override
            protected NavigableMap<Long, List<Long>> load(Integer shardId) {
                NavigableMap<Long, List<Long>> targetShard = null;
                targetShard = doLoadAShard(SaveAJob.SHARD_A_FILE_PREFIX + shardId);
                return targetShard;
            }
        };

private static NavigableMap<Long, List<Long>> doLoadAShard(String fileName) throws IOException, ClassNotFoundException {
    NavigableMap<Long, List<Long>> navigableMap;
    FileChannel fileChannel = FileChannel.open(Paths.get(fileName), StandardOpenOption.READ);
    ByteBuffer buffer = ByteBuffer.allocate(4096);
    byte[] src = new byte[(int) fileChannel.size()];
    int index = 0;
    while (fileChannel.read(buffer) != -1) {
        buffer.flip();
        byte[] bytes = new byte[buffer.remaining()];
        buffer.get(bytes);
        for (int i = 0; i < bytes.length; i++) {
            src[index++] = bytes[i];
        }
        buffer.clear();
    }
    ByteArrayInputStream bais = new ByteArrayInputStream(src);
    ObjectInputStream ois = new ObjectInputStream(bais);
    navigableMap = (NavigableMap<Long, List<Long>>) ois.readObject();
    fileChannel.close();
    return navigableMap;
}

二版

学习kafka: 不缓存,直接落盘,顺序写,

  1. 这样就可以直接利用os页缓存,减少数据从JVM内存拷贝到页缓存的过程。

  2. 不占用JVM内存

发现效果一般,同步落盘还是异步落盘?

后期使用索引、学习ssd特性、读kafka源码

最终获得决赛83/200名

第五届中间件性能挑战赛-天池大赛-阿里云天池

总结与待优化:

插入

  • 分线程存储不同分片&文件,则不需要做排序。搜索时直接合并即可,但要读取多个文件。
  • 参考kafka的索引算法,分为baseIndex和relativeIndex,根据前者定位分片文件,根据后者直接定位到分片文件内的偏移量

存储

  • 存储时做压缩
  • 异步落盘

查询

  • 根据比赛测试程序阶段,在批量读取的时候,提前批量message对象

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值