百万并发「零拷贝」技术系列之Java实现

原创
07/27 08:00
阅读数 120


上一篇推文中讲解了零拷贝思想在Linux系统中的实现,主要有mmap、sendfile、splice、tee等,但在Java中目前主要实现了mmap和sendfile。
Java I/O的发展史
第一篇的零拷贝的概述中,我们了解到为了降低内核接口调用的复杂度和提高编码效率,高级语言一般都为程序开发者提供了封装的类库,如C语言的标准库、Java的JDK等。

在JDK1.3之前Java的I/O一直比较传统,是采用Stream阻塞模式。在JDK1.4 的发布版中正式引入NIO,加入了缓冲区Buffer和通道Channel的概念,提供了非阻塞的方式。然而JDK1.4主要是为Socket通讯进行的优化,随后在JDK1.7版本中的NIO2不仅增强了文件系统的处理能力,还做到了真正的异步I/O—AIO。

mmap的实现 - MappedByteBuffer

JDK NIO提供的MappedByteBuffer底层就是调用mmap来实现的,FileChannel.map用来建立内存映射关系:把用户空间和内存空间的虚拟内存地址映射到同一块物理内存。mmap对大文件比较合适,对小文件则容易造成内存碎片,反而不是最佳使用场景。

编码示例如下

public void mmap4zeroCopy(String from, String to) throws IOException {
  FileChannel source = null;
  FileChannel destination = null;
  try {
    source = new RandomAccessFile(from, "r").getChannel();
    destination = new RandomAccessFile(to, "rw").getChannel();

    MappedByteBuffer inMappedBuf = 
      source.map(FileChannel.MapMode.READ_ONLY, 0, source.size());

    destination.write(inMappedBuf);
  } finally {
    if (source != null) {
      source.close();
    }
    if (destination != null) {
      destination.close();
    }
  }
}


sendfile的实现 - transferTo
NIO提供的FileChannel.transferTo方法可以直接将一个channel传递给另一个channel,结合上一篇推文看,channel像极了内核缓冲区。
编码示例如下
public void sendfile4zeroCopy(String from, String to) throws IOException{
  FileChannel source = null;
  FileChannel destination = null;
  try {
    source = new FileInputStream(from).getChannel();
    destination = new FileOutputStream(to).getChannel();
    source.transferTo(0, source.size(), destination);
  } finally {
    if (source != null) {
      source.close();
    }
    if (destination != null) {
      destination.close();
    }
  }
}

传统I/O vs mmap vs sendfile
通过实战来对比下传统I/O、mmap、sendfile的性能及在用户空间和内核空间中消耗的CPU时间,代码如下
import java.io.*;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

/**
 * 公众号:码农神说 示例代码
 */

public class JioChannel {
  public static void main(String[] args) {
    JioChannel channel = new JioChannel();
    try {
      if (args.length < 3) {
        System.out.println("usage: JioChannel <source> "+
                    "<destination> <mode>\n");
        return;
      }

      if ("1".equals(args[2])) { //传统方式的复制
        channel.copy(args[0], args[1]);
      } else if ("2".equals(args[2])) { //mmap的方式
        channel.mmap4zeroCopy(args[0], args[1]);
      } else if ("3".equals(args[2])) { //sendfile的方式
        channel.sendfile4zeroCopy(args[0], args[1]);
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  /**
   * 传统方式的复制
   *
   * @param from
   * @param to
   * @throws IOException
   */

  public void copy(String from, String to) throws IOException {
    byte[] data = new byte[8 * 1024];
    FileInputStream fis = null;
    FileOutputStream fos = null;
    long bytesToCopy = new File(from).length();
    long bytesCopied = 0;
    try {
      fis = new FileInputStream(from);
      fos = new FileOutputStream(to);

      while (bytesCopied < bytesToCopy) {
        fis.read(data);
        fos.write(data);
        bytesCopied += data.length;
      }
      fos.flush();
    } finally {
      if (fis != null) {
        fis.close();
      }
      if (fos != null) {
        fos.close();
      }
    }
  }

  /**
   * mmap的方式复制
   *
   * @param from
   * @param to
   * @throws IOException
   */

  public void mmap4zeroCopy(String from, String to) throws IOException {
    FileChannel source = null;
    FileChannel destination = null;
    try {
      source = new RandomAccessFile(from, "r").getChannel();
      destination = new RandomAccessFile(to, "rw").getChannel();

      MappedByteBuffer inMappedBuf = 
          source.map(FileChannel.MapMode.READ_ONLY, 0, source.size());

      destination.write(inMappedBuf);
    } finally {
      if (source != null) {
        source.close();
      }
      if (destination != null) {
        destination.close();
      }
    }
  }

  /**
   * sendfile的方式复制文件
   *
   * @param from
   * @param to
   * @throws IOException
   */

  public void sendfile4zeroCopy(String from, String to) throws IOException {
    FileChannel source = null;
    FileChannel destination = null;
    try {
      source = new FileInputStream(from).getChannel();
      destination = new FileOutputStream(to).getChannel();
      source.transferTo(0, source.size(), destination);
    } finally {
      if (source != null) {
        source.close();
      }
      if (destination != null) {
        destination.close();
      }
    }
  }
}

首先进行代码编译 java javac JioChannel.java,它的执行方法是 JioChannel <source> <destination> <mode>,其中mode值1为传统方式I/O,2为mmap方式I/O,3为sendfile方式I/O。


执行和输出如下(a.zip为130M的压缩文件)

$ time java JioChannel a.zip b.zip 1
real 0m0.199s
user 0m0.090s
sys  0m0.117s

$
 time java JioChannel a.zip b.zip 2
real 0m0.172s
user 0m0.074s
sys  0m0.102s
    
$ time java JioChannel a.zip b.zip 3
real 0m0.162s
user 0m0.057s
sys  0m0.108s

user+sys之和是该执行进程的耗费CPU的总时间,可见mmap和sendfile方式效率高于传统方式,而且用户空间user耗费CPU的时间占比总耗费时间也有所降低。

Linux的time命令
time是linux shell内置的命令,它用于统计/测量系统的资源使用情况,如CPU、内存、I/O等,用法如下
time [ -apqvV ] [ -f FORMAT ] [ -o FILE ]
      [ --append ] [ --verbose ] [ --quiet ] [ --portability ]
      [ --format=FORMAT ] [ --output=FILE ] [ --version ]
      [ --help ] COMMAND [ ARGS ]

内存、I/O等资源可参看time手册,不展开叙述。测量CPU的主要角度是其耗费的时间:实际总耗费时间、用户空间和内核空间各自耗费的时间。

  • real:实际总耗费时间,从会话开始到结束,包括其他进程的使用时间和本进程阻塞的时间;

  • user:该执行进程在用户空间耗费的CPU时间;

  • sys:该执行进程在内核空间耗费的CPU时间(CPU耗费在系统调用(system calls)执行上);

  • user + sys:该执行进程实际耗费的CPU总时间,real时间远大于user+sys,因为它不仅包含其他进程消耗的时间,还有文件寻址等时间消耗。
写在最后
虽然JDK没有实现所有的Linux零拷贝模式,但如果能把mmap和sendfile发挥到极致在性能上也能具有非常可观的提升,比如kafka、netty都是以零拷贝而业界瞩目。下一篇推文将简单介绍下kafka、netty的零拷贝思想及实现,这也是面试经常遇到的问题,敬请关注。

End


版权归@码农神说所有,转载须经授权,翻版必究

可回复关键字“转载联系助手开白



百万并发「零拷贝」技术系列之初探门径 2020-07-21
缓存穿透、缓存击穿、缓存雪崩看这篇就够了,文末还送福利哦! 2020-07-15
一口气讲透一致性哈希(Hash),助力「码农变身」 2020-07-13
漫画 | 架构设计中的那些事,文末送福利 2020-07-10
Java中异常处理的9个最佳实践 2020-07-08
Intellij IDEA必备插件,提高效率的“七种武器”! 2020-07-06
接住喽🤗,送你个装逼的技能: JDK动态代理 2020-07-04


本文分享自微信公众号 - 码农神说(codeceo)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部