關於七牛雲正確使用姿式探索

業務場景

需求

咱們項目有一個文件上傳需求,須要從客戶端上傳到七牛雲的對象存儲和本身的應用服務器上。這裏使用七牛雲主要是實現下載分發。應用服務器須要留一份是由於後續須要作文件分析(而且是上傳後須要立馬分析出結果展示給客戶端)。另外,因爲是初期項目,暫時沒考慮用獨立服務器來分析。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異步處理

關於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多每秒)。

具體實現是經過七牛的表單上傳方案直接把客戶端的文件先上傳到七牛(這一步根本不關應用服務器什麼事,因此避開了,並且直接上傳到七牛的速度很是快,基本只取決於用戶端的網速,並且對於通常需求,七牛提供了對於到咱們應用服務器的回調方法)。而後因爲咱們應用服務器也須要文件,因此方案是直接在咱們應用服務器直接下載七牛的文件(這裏能夠同步阻塞住,前端作個等待效果解決用戶體驗問題)。由於前面說到流入到服務器佔用的是下行帶寬。因此這裏速度也會很是快(並且是免費的^_^)。

這種方案基本是完美的了。

總結

首先是對我的的檢討,前期調研不充足,可是項目初期有點緊,這裏也說明投入時間的重要性。

其次關於項目經驗:上傳第三方雲存儲,千萬不要使用應用服務器作中轉!能夠直接上傳到第三方雲服務器,若是有後續處理邏輯的,可使用他們的回調接口。

相關文章
相關標籤/搜索