咱们项目有一个文件上传需求,须要从客户端上传到七牛云的对象存储和本身的应用服务器上。这里使用七牛云主要是实现下载分发。应用服务器须要留一份是由于后续须要作文件分析(而且是上传后须要立马分析出结果展示给客户端)。另外,因为是初期项目,暂时没考虑用独立服务器来分析。php
服务器:Centos7
开发语言:PHP
框架:Laravel
前端上传组件:百度的WebUploadercss
准确的说我通过了三个阶段才真正完美的实现了需求(主要解决上传速度)。html
初期面对需求很容易想到的思路是:客户端先上传文件到应用服务器(由于上传完成能够及时作分析),而后再上传到七牛云上。前端
因此个人解决方案是:前端用webuploader,后端的七牛云文件处理方面使用了Laravel的一个插件:overtrue/flysystem-qiniu (https://github.com/overtrue/f...,该插件的接口很简洁好用(可是有坑,后面会说到)。git
而后为了解决性能问题,我还作了如下工做:
1,使用分片上传
2,后续上传七牛云使用异步的方式(由于文件上传到其余应用来下载这个文件,中间有许多时间来让上传任务的完成)github
这里讲下分片上传的实现思路,客户端主要是把大文件按必定size进行分片,而后上传到服务器,因此会有多个请求,而且每一个请求还需带上关键的信息:当前chunk(从0开始)和chunks(总分片数)。因为我用的是WebUploader组件,因此客户端不用本身作什么,只需配置下简单信息(是否分片及分片大小)。web
服务端处理逻辑为:
客户端一个请求过来,分两种状况:
1,文件总size小于要分片的size,这时候直接处理文件。
2,处理分片状况。shell
具体逻辑是判断chunk和chunks,若是相等说明为第一种状况,直接处理上传,其余走处理分片逻辑。后端
处理分片的逻辑为:保存当前分片到临时目录(按分片命名),而后判断当全部分片完成时,就合并文件。具体逻辑是判断 chunk + 1 是否等于chunks。 合并逻辑就是循环读取临时文件,而后写入到一个新的文件(合并后的),这里能够顺便删除临时文件。服务器
所遇的坑:
这里处理碎片文件时,当初图方便使用了Laravel的文件处理接口Storage::append,可是这个接口有个坑就是它自做主张的文件结尾加入换行符。致使合并后的文件还原不成原始文件。解决办法是老老实实使用php的fopen、fwrite、fclose这一套。
关于PHP的异步实现能够参考鸟哥写的文章:http://www.laruence.com/2008/...
主要方法为:客户端AJAX、popen函数、curl、fsocketopen等
不过这篇文章比较老了,局限性也大,如今有了协程等处理方案(如今Swoole也提供协程方案了,而且client-server task分发这种也能够用swoole的),并且往架构方面考虑可使用队列等(感受靠谱的仍是队列)。
PS: 我这里前期用的是简单粗暴的popen,后来使用的是Laravel提供的队列。
经过上述所说的方案,很容易就实现了一个版本。可是没高兴多久。。,在后续测试时遇到一个诡异bug,当文件过大时,任务脚本上传到七牛云失败。
这里脚本是写在Laravel的artisan中的,当我把脚本命令直接在终端调试时也是没有任何异常(准确讲是看不了任何异常)
。前面我说过七牛这块SDK用的是overtrue/flysystem-qiniu ,而且为了考虑性能问题用的是他的writeStream接口。
$disk = Storage::disk('qiniu'); $stream = fopen($localFileName, 'r'); $disk->writeStream($fileName, $stream); if (is_resource($stream)) { fclose($stream); }
代码表面上看起来很理想,用的是文件流上传(怕吃内存)。但结果证实一切只是表面上的。。
当我遇到大文件没法上传到七牛云时,断点调试到$disk->writeStream这里,发现返回的是false。 继而调试到overtrue/flysystem-qiniu这个扩展的源代码。而后发现了一个大坑。。
主要是两个问题:
1,writeStream只是个假的流写入
具体源码在扩展的QiniuAdapter.php文件中,这里贴段代码:
public function writeStream($path, $resource, Config $config) { $contents = ''; while (!feof($resource)) { $contents .= fread($resource, 1024); } $response = $this->write($path, $contents, $config); if (false === $response) { return $response; } return compact('path'); }
注意这里的$contents变量,最终仍是等价于一个大文件内容的大小(服务器为此变量开辟的内存)。而且后续还要在方法间传递。因此这里是假的流!
2,接口对调试不友好
还有在write方法中,屏蔽了$error,只返回false,这样不便于咱们查问题,最终我是断点打印这个$error才知道报的错误是:“invalid multipart format: multipart: message too large”,这个应该是七牛那边真正返回的,但这么重要的信息被这个扩展屏蔽了。
知道了一期方案的具体问题所在,我就一直在思考(那个扩展就不提了。。我如今怀疑它的存在乎义。。),甚至在想也许一开始整个思路就错了(经过SDK上传文件的方案)。后来还真被我找到了,七牛云官方提供一个脚本工具:Qshell(https://github.com/qiniu/qshell)。这个是命令行运行脚本,具体操做看文档就能够了。放到个人项目也是集成到七牛的任务脚本中。
后来测试能够了,整个流程能够跑通。
可是无心中发现二期的重要问题,这个上传走的是服务器的上行带宽!而咱们日常付费买的带宽就是买的上行带宽!(下行是通常是免费的)。这还怎么搞!因为咱们上传业务是商户端使用的,平时使用频次也不会太少,这会致使在上传时影响前端网站的访问速度。
这里具体讲下服务器带宽问题(网上查询后整理的):
首先对服务器带宽方向的描述通常是用上行和下行,上传和下载是指动做。
上行是指从服务器流出的带宽,若是是在其余机器下载服务器上的文件,用的主要是服务器的上行带宽(这里说下咱们平时的网页浏览,其实也是不一样客户端从服务器下数据, html文件、css等而后渲染,因此网页浏览占用的也是上行带宽)。
下行是指流入到服务器的带宽,若是是在其余机器上传文件到服务器,好比用FTP上传文件,用的主要是服务器的下行带宽(服务器上下载文件用的也是下行带宽)。
如今的云提供商好比阿里云不限制的是下行带宽,大部分服务器的使用环境,都是上行带宽用的多,下行带宽用的少。
经过对带宽的理解,再回到咱们项目的上传实现思路,能够看到一开始就错了(不应用应用服务器做为中转)!
当初为了节省时间,直接跳过官方文档,而使用第三方扩展。 如今看来,不得不又回到官方文档了。
经过把七牛的文档过一遍,发现是有方案能够避开那个占用服务器上行带宽的问题的。
主体思路是要避开应用服务器上行带宽的使用,由于上行带宽很宝贵,尽可能使用下行带宽(免费、速度很快!阿里的大概60M多每秒)。
具体实现是经过七牛的表单上传方案直接把客户端的文件先上传到七牛(这一步根本不关应用服务器什么事,因此避开了,并且直接上传到七牛的速度很是快,基本只取决于用户端的网速,并且对于通常需求,七牛提供了对于到咱们应用服务器的回调方法)。而后因为咱们应用服务器也须要文件,因此方案是直接在咱们应用服务器直接下载七牛的文件(这里能够同步阻塞住,前端作个等待效果解决用户体验问题)。由于前面说到流入到服务器占用的是下行带宽。因此这里速度也会很是快(并且是免费的^_^)。
这种方案基本是完美的了。
首先是对我的的检讨,前期调研不充足,可是项目初期有点紧,这里也说明投入时间的重要性。
其次关于项目经验:上传第三方云存储,千万不要使用应用服务器作中转!能够直接上传到第三方云服务器,若是有后续处理逻辑的,可使用他们的回调接口。