zero copy场景实践

内存文件映射的应用场景

在很多高可用的分布式系统中(如搜索引擎,消息队列。。),为了做到容灾和备份,在对内存进行读写追求实时性的同时,也会选择将数据持久化到文件系统里。
因此文件的频繁读写中带来的IO消耗是不可避免的,在这个过程中会用到内存文件映射这一方式。
在讲内存映射文件这个概念之前,我们通过一个具体场景来一步一步深入了解下具体是怎么做到文件的快速读写的。

场景

在搜索系统中,会预先定义一份数据结构,也就是scheme文件,类似于数据库的表结构。
比如商品表:

  • docId[Int](内部的文档ID);
  • 商品id[Long];
  • 描述信息[字符,为了演示方便,我设置length最长12];

在之前讲过倒排索引的原理,这里不详细讲了,大概流程就是:预先建立关键词对应docId的对联关系,用户输入一个Query关键字,得到docIds,通过docid查询正向索引,得到文档的详细信息返回。
我们这次来看下文件目录下的正向索引可以怎么实现。

传统IO方式

文件里面的数据结构如下:


zero copy场景实践_第1张图片
  • 采用unicode,2个字节(16位)来表示一个字符
  • 每条记录分三个字段,docid(int), 商品id(long),描述(字符) ,一条记录会占用 36个字节, 代码如下
public class WriteDoc {

    public static void main(String[] arg) throws Exception {
        RandomAccessFile randomFile = new RandomAccessFile(new File("/Users/qianzhang/Downloads/WriteDoc.txt"), "rw");
        String s = "Hello  World";
        randomFile.writeInt(1);
        randomFile.writeLong(Long.parseLong(String.valueOf(1)));
        randomFile.writeChars(s);
        randomFile.writeInt(2);
        randomFile.writeLong(Long.parseLong(String.valueOf(2)));
        randomFile.writeChars(s);
        randomFile.close();
    }
}

二进制文件
0000 0001 0000 0000 0000 0001 0048 0065
006c 006c 006f 0020 0020 0057 006f 0072
006c 0064 0000 0002 0000 0000 0000 0002
0048 0065 006c 006c 006f 0020 0020 0057
006f 0072 006c 0064 

写了两条记录,所以生成文件大小为72byte,可以查看生成的二进制文件。

下面再读取文件,比如需要拿到docid为1的文档信息,代码如下:

public class DocReader {
    public static void main(String[] args) throws Exception {
    RandomAccessFile randomFile = new RandomAccessFile(new File("/Users/qianzhang/Downloads/WriteDoc.txt"), "rw");
        long current = System.currentTimeMillis();
        int docId = 1;
        randomFile.seek(docId * 36);
        System.out.println(randomFile.readInt());
        System.out.println(randomFile.readLong());
        for (int i = 0; i < 12; i++) {
            System.out.print("" + randomFile.readChar());
        }
    }
}

打印出:
2
2
Hello  World

可以看到通过这种方式可以很快速准确的得到文档id=1的信息。
现在看下如果文件比较大,频繁读会什么样的?
我在4核8G,ssd硬盘的机器里面,随机读取100w次,机器表现如下:

可以发现cpu已经达到90%了,总共耗时139732ms。

FileChannel形式

public class DocChannelReader {
    public static void main(String[] args) throws Exception {
        RandomAccessFile randomFile = new RandomAccessFile(new File("/Users/qianzhang/Downloads/WriteDoc.txt"), "rw");
        FileChannel channel = randomFile.getChannel();

        MappedByteBuffer byteBuffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());

        long current = System.currentTimeMillis();

        for (int i = 0; i < 1000000; i++) {
            int docid = (int) (Math.random() * 20000000);
            byteBuffer.position(docid * 36);
            System.out.println(byteBuffer.getInt());
            System.out.println(byteBuffer.getLong());
            for (int j = 0; j < 12; j++) {
                System.out.print("" + byteBuffer.getChar());
            }
        }

        System.out.println("耗时:" + (System.currentTimeMillis() - current) / 1);

        randomFile.close();
    }
}

cpu占用



总共耗时40201ms,足足提高了(139732/40201)= 3.4倍,在非SSD下这个差距会更大。

为什么访问速度提高了

要解释这个,就得详细理解下zero copy概念,这篇文章介绍的比较通俗易懂。
http://www.linuxjournal.com/article/6345

1)mmap 方式
优点:适用小块文件传输,即使频繁调用,效率也很高
缺点:会多消耗 CPU,内存安全性控制复杂,需要避免 JVM Crash 问题。
2)sendfile方式
优点:消耗 CPU 较少,大块文件传输效率高,无内存安全新问题。
缺点:小块文件效率低于 mmap 方式。
消息系统里面一般选择了第一种方式,mmap+write 方式,因为有小块数据传输的需求,效果会比 sendfile 更好

你可能感兴趣的:(zero copy场景实践)