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

業務場景

需求

我們項目有一個文件上傳需求,需要從客戶端上傳到七牛雲的對象存儲和自己的應用服務器上。這裏使用七牛雲主要是實現下載分發。應用服務器需要留一份是因爲後續需要做文件分析(並且是上傳後需要立馬分析出結果展現給客戶端)。另外,由於是初期項目,暫時沒考慮用獨立服務器來分析。

所用技術棧

服務器:Centos7
開發語言:PHP
框架:Laravel
前端上傳組件:百度的WebUploader

解決方案

準確的說我經過了三個階段才真正完美的實現了需求(主要解決上傳速度)。

一期解決方案及細節

初期面對需求很容易想到的思路是:客戶端先上傳文件到應用服務器(因爲上傳完成可以及時做分析),然後再上傳到七牛雲上。

所以我的解決方案是:前端用webuploader,後端的七牛雲文件處理方面使用了Laravel的一個插件:overtrue/flysystem-qiniu (https://github.com/overtrue/f...,該插件的接口很簡潔好用(但是有坑,後面會說到)。

然後爲了解決性能問題,我還做了以下工作:
1,使用分片上傳
2,後續上傳七牛雲使用異步的方式(因爲文件上傳到其他應用來下載這個文件,中間有許多時間來讓上傳任務的完成)

關於分片上傳

這裏講下分片上傳的實現思路,客戶端主要是把大文件按一定size進行分片,然後上傳到服務器,所以會有多個請求,並且每個請求還需帶上關鍵的信息:當前chunk(從0開始)和chunks(總分片數)。由於我用的是webuploader組件,所以客戶端不用自己做什麼,只需配置下簡單信息(是否分片及分片大小)。

服務端處理邏輯爲:
客戶端一個請求過來,分兩種情況:
1,文件總size小於要分片的size,這時候直接處理文件。
2,處理分片情況。

具體邏輯是判斷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多每秒)。

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

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

總結

首先是對個人的反省,前期調研不充足,但是項目初期有點緊,這裏也說明投入時間的重要性。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章