October 16th, 2014

笔者在项目中处理大文件上传的需求,仿照七牛云存储的接口设计。然而,在服务器端文件合并时遇到了很大的问题:合并太慢。本文记录了当时的思路和解决的方案

大文件的需求

文件上传是个很常见的需求。尽管HTTP是基于TCP上层的协议,但是HTTP协议本身并不适合处理超大的请求体,文件上传有很大的稳定性问题,如果中途断开了,将前功尽弃。为了改善用户体验或者缓解服务器压力,通常会考虑将文件分成小片,将小片一个个上传,如果中途断开了也能从某个失败的小片开始继续上传。

在前端的处理上,对于Web页面,可以采用plupload作为上传组件,该组件支持html5、flash、sl等多种上传方式,因此,可以提供较好的浏览器兼容性。七牛云存储的js-sdk就是基于这个组件开发的。不过本文的重点并不是讨论前端技术,关于前端就到此为止。

服务器端的策略

既然文件被分成片上传,那么自然在服务器端需要将分片合并成原始的文件,那么这里存在两种策略

  • 边传边合并:每上传一个分片,就将分片合并到文件的后面
  • 传完一起合并:先将分片保存起来,客户端发起一个合并请求时,再将分片合并成一个文件

边传边合并

这种方式要注意:

  • 由于HTTP的无状态性,这种方式需要客户端和服务器端维持一个标识。服务器端根据标识,才能知道分片应该向哪个目标文件Append
  • 如果客户端中途停止上传,那么保存在服务器上的目标文件将成为垃圾文件,文件即不能被删除又没有利用价值。因为服务端无法知道文件究竟的完成状态还是正在进行状态
  • 由于分片直接合并进了文件,无法管理分片

传完一起合并

七牛云存储就是使用的这种策略,具体的实现方式如下:

  1. 每上传一个分片,服务端将分片保存下来,并返回客户端一个唯一标识ctx,这个标识ctx与当前这个分片关联
  2. 客户端应当妥善保存每个分片的标识,以及它们的先后顺序
  3. 服务端需要提供一个接口,客户端用这个接口请求分片合并,在请求时候将标识按顺序告知服务端,ctx0,ctx1,ctx2
  4. 服务端根据标识的顺序找到对应的分片,并合并成完成的文件

这种设计解决了客户端中途停止上传带来的服务器端资源的浪费,因为分片都是正在进行态,可以对时间很早的分片进行清理。而且分片被记录了下来,容易对分片进行一些管理。

服务器端的分片合并

现在进入本文的重点。文件合并是IO操作,IO操作是最耗时的工作了。上文的第一种策略,有一个好处是文件的合并是在上传的过程中完成的,对于用户来说几乎感知不到文件Append时的延时。然而,第二种策略的文件合并却是在一个时刻同时进行的。笔者测试过,即使是4-5个4MB的分片,也会使客户端有明显的延迟感。如果分片再多的话,延迟将更大,甚至请求超时,这是不能接受的。

但是笔者在七牛云上的测试,合并的请求在七牛的服务器端几乎没有延时。为此,笔者还发了一问:七牛云mkfile如何实现将大量的文件chunk快速合并的,但是七牛的技术太“吝啬”,一点也不透露。那该怎么办呢?

并行处理?

由于是将分片合并,那么很容易会想到并行。类似归并排序的思想,将合并任务分开,然后通过集群服务器的协调完成合并。但是笔者对这方面是知之甚少,而且这种方案会使原本简单的架构变的异常复杂,不敢采用这种方案。而且感觉会有坑:

  • 并行处理往往是异步的,如何处理好与web服务器的同步
  • 再怎么分任务,合并终究是IO操作,IO操作总是要耗费时间的

文件系统底层处理?

仔细思考合并这个动作,实际上是将多个文件在文件系统里面复制了一次,而文件的内容并没有任何的变化。如果能够在文件系统层面将分片直接连接起来话,合并文件仅仅是修改一些指针,速度将十分的快。不过文件系统各不相同,能不能实现还需要看。而且,由于笔者使用nfs作为数据存储,nfs的文件读写完全是通过接口提供的,接口也不提供底层的文件系统操作,所以似乎是无法实现。

为什么非要合并!

再思考下去,如果文件系统无法做到将分片直接连接起来的的话,那么从用户接口层(HTTP)是否能做到呢?试想,通过HTTP的方式提供文件的访问,如果HTTP服务器能够知道这个文件是由多个小文件按何种顺序组成的,那么就可以按照顺序将分片依次放在同一个HTTP流中返回,对用户来说一次请求还是得到一个文件,好像文件是合并好的一样,但实际上文件在文件系统并不存在。

这样做需要单独将分片的顺序维护好,每次都要读出分片的顺序和位置,然后依次一个个写入HTTP流中。但是高层的Web编程框架似乎无法支持这种做法。

巧用Nginx避免文件合并

笔者立刻想到了之前用过的Nginx模块mod_zip,这个模块能够将多个文件打包以zip流的方式返回。现在的需求其实跟这个模块的工作几乎差不多,甚至还要更简单。苦苦寻觅网上的Nginx模块,似乎没有找到笔者需要的,于是决定自己基于mod_zip开发一个。幸好,之前用mod_zip的时候看过一些源码。

目前这个模块笔者命名为mod_pieces,已经开发完成并在windows和linux两个平台下测试通过,唯一不好的是无法支持HTTP Range,HTTP Range有点难弄,以后可能有时间再慢慢实现。代码还没有整理,有时间放到Github上去共享。

mod_pieces的工作方式和原理和mod_zip很相似,有进一步需求的读者可以移步至:利用Nginx第三方模块,实现附件打包下载

后续还是要合并的

现在用户那头可以”欺骗”成功了,但是如果系统本身需要对文件进一步处理,比如视频的格式转换,那么还是需要将文件合并起来的,不过这个时候就可以用一个后台的服务异步的慢慢做了,用户不会感知。基于这些复杂的问题,笔者已经把文件上传下载和处理作为的一个全新的产品功能独立出来,以支持主产品对文件的各种功能需求。

后记

本文没有一张图片,没有一行代码,有些不适应。文字虽短,但是这些东西都是经过笔者的实践并且有感而发,希望有个总结,并能够带来更多的交流。


1块2块也是钱,小额赞助