sendfile实质是linux系统中一项优化技术,用以发送文件和网络通信时,减少用户态空间与磁盘倒换数据,而直接在内核级做数据拷贝,这项技术是linux2.4之后就有的,现在已经很普遍的用在了C的网络端服务器上了,而对于java而言,因为java是高级语言中的高级语言,至少在C语言的层面上可以提供sendfile级别的接口,举个例子,java中可以通过jni的方式调用c的库,而这种在tomcat中其实就是APR通道,通过tomcat-native去调用类似于APR库,这种调用思路虽然增大了java调用链条,但可以在java层级中获得如sendfile的这种linux系统级优化的支持,可谓是一举多得。
上述的内容,实际就是本文的背景,本文就从系统调用的层级,逐步讲解tomcat中的sendfile是怎么实现的。
1. 介绍linux的sendfile机制
sendfile是一个系统调用,可以man一下,看到其函数细节:
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
sendfile()是作用于数据拷贝在两个文件描述符之间的操作函数.这个拷贝操作是内核中操作的,所以称为"零拷贝".sendfile函数比起read和write函数高效得多,因为read和write是要把数据拷贝到用户应用层操作.
参数说明:
out_fd 是已经打开了,用于写操作(write)的文件描述符;
in_fd 是已经打开了,用于读操作(read)的文件描述符;
offset 偏移量;表示sendfile函数从in_fd中的哪一偏移量开始读取数据.如果是零表示从文件的开始读,否则从相应的便宜量读取.如果是循环读取的时候,下一次offset值应为sendfile函数返回值加上本次的offset的值.
count是在两个描述符之间拷贝的字节数(bytes)
返回值:
如果成功的拷贝,返回写操作到out_fd的字节数,错误返回-1,并相应的设置error信息.
EAGAIN 无阻塞I/O设置O_NONBLOCK时,写操作(write)阻塞了.
EBADF 输出或者输入的文件描述符没有打开.
EFAULT 错误的地址.
EINVAL 描述符不可用或者锁定了,或者用mmap()函数操作的in_fd不可用.
EIO 当读取(read)in_fd时发生未知错误.
ENOMEM 读(read)in_fd时内存不足.
总结一下,实际sendfile就是一个高效的函数,用于替换write,我们看看程序和普通的网络send系统调用程序的区别:
发送端传统方式代码段如下:
fd = open(FILENAME, O_RDONLY);
while((len =read(fd, buff, sizeof(buff))) >0)
{
send(sockfd, buff, len ,0);
}
close(fd);
再看看使用sendfile:
使用sendfile()传输代码段.
off_t offset = 0;
stat(FILENAME, &filestat);
fd = open(FILENAME, O_RDONLY);
sendfile(sockfd, fd, &offset, filestat.st_size) );
close(fd);
sendfile调用和send调用非常类似,很容易就可以在API级别进行替换,程序几乎不用修改什么东西,而换成了高级的优化调用,再高并发的场景下,会明显从数据看出来,性能提升不少。
2.sendfile优化效果
介绍完系统调用之后,来看看sendfile究竟优化省略了哪几步的操作,
传统的网络发送请求,肯定会走内存,因为内存中可能有一些数据需要处理,例如数据的一些加工啊,抽取之类的,
但对于纯文本字符,例如文件这种的,不需要进行修改,直接发送,那么其实就没有必要再走内存了,也就是上面的方框部分虚线引入的部分直接就不走了,直接可以从磁盘到内核缓冲区,然后在内核级别直接将这块数据流转到网卡缓冲区,然后直接由网络介质发送了,可见,这就是sendfile的功效所在。
因此,sendfile的使用场景,我们也应该非常清楚了,对于例如web服务器中的静态资源,静态文件这种的http请求,不需要再内存之中进行加工的,sendfile是最优的选择。
3.Defaultservlet的sendfile逻辑
对于Tomcat中的静态资源处理,直接对应的就是DefaultServlet了,这个类是嵌入在Tomcat源码中,专门处理静态资源的类,我们来看其比较关键的doget(之后调用的serveReource方法)的源码:
对于上述的代码逻辑是,当checkSendfile方法不为true,说明该请求就是普通的请求,那么按照此逻辑,需要将请求的文件输入到ostream流中,最后将这个流通过从copy方法,转接到Outputstream中,通过网络传输出去。
但是,如果请求request中设置了 org.apache.tomcat.sendfile.support 设为Boolean.TRUE,则表示支持 sendfile,那么这个属性就代表着该reqeuest请求发送的文件吗,是通过sendfile系统调用来进行发送,而不是通过send系统调用(默认的java网络socket发送流,实际jvm底层调用的就是send系统调用)。
对于上述的这个 org.apache.tomcat.sendfile.support 属性来说,相当于每一个request请求都可以通过设置该属性,告诉服务器,我这个请求要使用sendfile,而不是send,这相当于是非常的灵活了。
除了 org.apache.tomcat.sendfile.support 属性,通过代码的分析,还有几个属性也可以在request中进行设置,分别为:
org.apache.tomcat.sendfile.filename
作为字符串发送的标准文件名。org.apache.tomcat.sendfile.start
开始位置偏移值,长整型值。org.apache.tomcat.sendfile.end
结束位置偏移值,长整型值。
当然,如果不进行设置,那么默认的文件名就是该request请求的文件,start是0,end是length,从上述的代码中也可以看得出来。
我们回头看一下,这几个参数为什么需要设置,对应sendfile的几个参数就可以明白了。
如果是checkSendfile方法为true,那么在DefaultServlet中不进行流的转接,该处理是在Tomcat前端中的不同XXXEndpoint类中,请继续往下看。
值得注意的一点是,一般http响应的数据包都会进行压缩,这样的好处是能极大的减小带宽占用,而响应头中发现了compression压缩属性,浏览器会自动首先进行解压缩,从而正确的将response响应主体刷到页面中。
但是,当sendfile属性开启后,这个compression压缩属性就不生效了,因此,当需要传输的文件非常大的时候,而网络带宽又是瓶颈的时候,sendfile显然并不是合适之举。
4.sendfile在BIO通道中的实现
以Tomcat8为例,不同的Tomcat前端通道中的sendfile的java包装是不同的,但实际上都是在调用系统调用sendfile。
对于,BIO来说,JIOEndpoint是不支持sendfile的,这个可以通过代码中看出来:
5.sendfile在NIO通道中的实现
在NIO通道中,有一个useSendfile属性,这个useSendfile属性是做什么的呢?
这个是可以设置在Connector中的,以NIO通道为例,配置为:
这个useSendfile属性是允许request进行sendfile的总体开关(前面讲的org.apache.tomcat.sendfile.support 属性是针对于每一个request的),这个useSendfile属性在NIO通道中默认就是打开的,当reqeust设置org.apache.tomcat.sendfile.support 属性为true的时候,response就会准备一个SendFileData的数据结构,这个数据结构就是NIO通道下的sendfile的媒介:
这个数据结构是用于传递给sendfile系统调用,用于发送。
因此,NIO的sendfile实现可以分为三个阶段:
第一阶段,实际上就是前面的XXXDefaultServlet中(不仅仅是DefaultServlet,其它的Servlet只要设置这个属性也可以调用sendfile)对Request的sendfile属性的设置,当该请求设置上述的属性后,证明该请求为sendfile请求。
第二阶段,servlet处理完之后,业务逻辑完成,对应的Response该commit了,而在Response的准备阶段,会初始化这个SendFileData的数据结构,这块的代码逻辑都在Http11NioProcessor类中:
从上述的代码逻辑来看,prepareSendfile方法是从前面DefaultServlet中设置的reqeust属性中,拿到file名称,字符位置的start,end,然后将这些属性作为传入的参数,初始化SendFileData实例;
第三阶段,我们记得NIO前端通道的Acceptor,Poller线程,Worker线程的三个线程,当Worker线程干完活之后,返回给客户端,依然要通过Poller线程,也就是会重新注册KeyEvent,读取KeyAttachment,这个时候当为sendfile的时候,前面初始化的SendFileData实例是会注册在KeyAttachment上的:
上述的processSendfile就是Poller线程的run中的一个判断分支,当为sendfile的时候,Poller线程就对SendFileData数据结构中的file名字取出,通过FileChannel的transferTo方法。
对于这个transferTo方法,我们可以看到其中的一个重要的解释:
上述的解释中就是sendfile系统调用。
6.sendfile在APR通道中的实现
在NIO通道中sendfile实现算是比较复杂的了,在APR通道中更加的复杂,我们可以回过头先看看NIO通道中的sendfile,实际是通过每一个Poller线程中的FileChannel的transferTo方法来实现的,对于transferTo方法是阻塞的,这也就意味着,当文件进行sendfile的时候,Poller线程是阻塞的,而我们前面研究过Tomcat前端,Poller线程是很珍贵的,不仅仅是为某几个sendfile服务的,这样会导致Poller线程产生瓶颈,从而拖慢了整个Tomcat前端的效率。
对于APR来讲,基于上述更进一步,通过下面的配置就可以看出端倪:
useSendfile属性没什么可说的,就是全局的sendfile开关;
sendfileThreadCount对应的就是APR通道中,将sendfile的功能从Poller线程中剥离开来,
这相当于sendfileData的数据结构,直接加入到Sendfile线程中了:
好处不言自明,Poller就干Poller的事,而遇到Sendfile的需求的时候,sendfile线程就挺身而出,把活给接了;
最后,对于APR通道是通过JNI调用的APR库,sendfile自然就不是java的API了:
总结:
SendFile实际上是操作系统的优化,Tomcat中基于在不同的通道中有不同的实现,配置也不尽相同,但实际上都是调用操作系统的SendFile的系统调用!
参考资料:http://www.linuxjournal.com/article/6345?page=0,0