色情業是個大行業。網際網路上沒有多少網站的流量能和最大的色情網站相匹敵。要搞定這巨大的流量很難。更困難的是,在色情網站上提供的很多內容都是低延遲的實時流媒體而不是簡單的靜態視訊。但是對於所有碰到過的挑戰,我很少看到有搞定過它們的開發人員寫的東西。所以我決定把自己在這方面的經驗寫出來。
問題是什麼?
幾年前,我正在為當時全世界訪問量排名26的網站工作 — 這裡不是說的色情網站排名,而是全世界排名。
當時,該網站通過RTMP(Real Time Messaging protocol)協議響應對色情流媒體的請求。更具體地說,它使用了Adobe的FMS(Flash Media Server)技術為使用者提供實時流媒體。基本過程是這樣的:
- 使用者請求訪問某個實時流媒體
- 伺服器通過一個RTMP session響應,播放請求的視訊片段
因為某些原因,FMS對我們並不是一個好的選擇,首先是它的成本,包括了購買以下兩者:
- 為每一臺執行FMS的伺服器購買Windows的版權
- 大約4000美元一個的FMS特定版權,由於我們的規模,我們必須購買的版權量數以百計,而且每天都在增加。
所有這些費用開始不斷累積。撇開成本不提,FMS也是一個比較挫的產品,特別是在它的功能方面(我過一會再詳細說這個問題)。所以我決定拋棄FMS,自己從頭開始寫一個自己的RTMP解析器。
最後,我終於把我們的服務效率提升了大約20倍。
開始
這裡涉及到兩個核心問題:首先,RTMP和其他的Adobe協議及格式都不是開放的,這就很難使用它們。要是對檔案格式都一無所知,你如何能對它進行反向工程或者解析它呢?幸運的是,有一些反向工程的嘗試已經在公開領域出現了(並不是Adobe出品的,而是osflash.org,它破解了一些協議),我們的工作就是基於這些成果。
注:Adobe後來釋出了所謂的“規格說明書”,比起在非Adobe提供的反向工程wiki和文件中披露的內容,這個說明書裡也沒有啥新東西。他們給的規格說明書的質量之低劣達到了荒謬的境地,近乎不可能通過該說明書來使用它們的庫。而且,協議本身看起來常常也是有意做成具有誤導性的。例如:
- 他們使用29位的整形數。
- 他們在協議頭上所有地方都採用低地址存放最高有效位元組(big endian)的格式,除了在某一個欄位(而且未標明)上採用低地址存放最低有效位元組(little endian)的格式。
- 他們在傳輸9K的視訊時,不惜耗費計算能力去壓縮資料減少空間,這基本上是沒意義的,因為他們這麼折騰一次也就是減少幾位或幾個位元組,對這樣的一個檔案大小可以忽略不計了。
還有,RTMP是高度以session為導向的,這使得它基本上不可能對流進行組播。理想狀態下,如果多個使用者要求觀看同一個實時視訊流,我們可以直接向他們傳回指向單個session的指標,在該session裡傳輸這個視訊流(這就是組播的概念)。但是用RTMP的話,我們必須為每一個要求訪問特定流的使用者建立全新的一個例項。這是完全的浪費。
我的解決辦法
想到了這些,我決定把典型的響應流重新打包和解析為FLV“標籤”(這裡的“標籤”指某個視訊、音訊或者後設資料)。這些FLV標籤可以在RTMP下順利地傳輸。
這樣一個方法的好處是:
- 我們只需要給流重新打包一次(重新打包是一個噩夢,因為缺少規格說明,還有前面說到的噁心協議)。
- 通過套用一個FLV頭,我們可以在客戶端之間順暢地重用任何流,而用內部的FLV標籤指標(配以某種宣告其在流內部確切位置的位移值)就可以訪問到真正的內容。
我一開始用我當時最熟悉的C語言進行開發。一段時間後,這個選擇變得麻煩了,所以我開始學習Python並移植我的C程式碼。開發過程加快了,但在做了一些演示版本後,我很快遇到了資源枯竭的問題。Python的socket處理並不適合處理這些型別的情況,具體說,我們發現在自己的Python程式碼裡,每個action都進行了多次系統呼叫和context切換,這增加了巨大的系統開銷。
改進效能:混合使用Python和C
在對程式碼進行梳理之後,我選擇將效能最關鍵的函式移植到內部完全用C語言編寫的一個Python模組中。這基本是底層的東西,具體地說,它利用了核心的epoll機制提供了一個O(log n)的演算法複雜度。
在非同步socket程式設計方面,有一些機制可以提供有關特定socket是否可讀/可寫/出錯之類的資訊。過去,開發人員們可以用select()系統呼叫獲取這些資訊,但很難大規模使用。Poll()是更好的選擇,但它仍然不夠好,因為你每次呼叫的時候都要傳遞一大堆socket描述符。
Epoll的神奇之處在於你只需要登記一個socket,系統會記住這個特定的socket並處理所有內部的雜亂的細節。這樣在每次呼叫的時候就沒有傳遞引數的開銷了。而且它適用的規模也大有可觀,它只返回你關心的那些socket,相比用其他技術時必須從10萬個socket描述符列表裡挨個檢查是否有帶位元組掩碼的事件,其優越性真是非同小可啊。
不過,為了效能的提高,我們也付出了代價:這個方法採用了完全和以前不同的設計模式。該網站以前的方法是(如果我沒記錯的話)單個原始程式,在接收和傳送時會阻塞。我開發的是一套事件驅動方案,所以為了適應這個新模型,我必須重構其他的程式碼。
具體地說,在新方法中,我們有一個主迴圈,它按如下方式處理接收和傳送:
- 接收到的資料(作為訊息)被傳遞到RTMP層
- RTMP包被解析,從中提取出FLV標籤
- FLV資料被傳輸到快取和組播層,在該層對流進行組織並填充到底層傳輸快取中
- 傳送程式為每個客戶端儲存一個結構,包含了最後一次傳送的索引,並儘可能多地向客戶端傳送資料
這是一個滾動的資料視窗,幷包含了某些試探性演算法,當客戶端速度太慢無法接收時會丟棄一些幀。總體來說執行的很好。
系統層級,架構和硬體問題
但是我們又遇到另外一個問題:核心的context切換成為了一個負擔。結果,我們選擇每100毫秒傳送一次而不是實時傳送。這樣可以把小的資料包彙總起來,也避免了context切換的爆炸式出現。
也許更大的一個問題在於伺服器架構方面:我們需要一個具備負載均衡和容錯能力的伺服器叢集,畢竟因為伺服器功能異常而失去使用者不是件好玩的事情。一開始,我們採用了專職總管伺服器的方法,它指定一個”總管“負責通過預測需求來產生和消除播放流。這個方法華麗麗地失敗了。實際上,我們嘗試過的每個方法都相當明顯地失敗了。最後,我們採用了一個相對暴力的方法,在叢集的各個節點之間隨機地共享播放的流,使流量基本平衡了。
這個方法是有效的,但是也有一些不足:雖然一般情況下它處理的很好,我們也碰到了當所有網站使用者(或者相當大比例的使用者)觀看單個廣播流的時候,效能會變得非常糟糕。好訊息是,除了一次市場宣傳活動(marketing campaign)之外,這種情況再也沒出現過。我們部署了另外一套單獨的叢集來處理這種情況,但真實的情況是我們先分析了一番,覺得為了一次市場活動而犧牲付費使用者的體驗是說不過去的,實際上,這個案例也不是一個真實的事件(雖然說能處理所有想象得到的情況也是很好的)。
結論
這裡有最後結果的一些統計數字:每天在叢集裡的流量在峰值時是大約10萬使用者(60%負載),平均是5萬。我管理了2個叢集(匈牙利和美國),每個裡有大約40臺伺服器共同承擔這個負載。這些叢集的總頻寬大約是50 Gbps,在負載達到峰值時大約使用了10 Gbps。最後,我努力做到了讓每臺伺服器輕鬆地能提供10 Gbps頻寬,也就等於一臺伺服器可以承受30萬使用者同時觀看視訊流。
已有的FMS叢集包含了超過200臺伺服器,我只需要15臺就可以取代他們,而且其中只有10臺在真正提供服務。這就等於200除以10,等於20倍的效能提高。大概我在這個專案裡最大的收穫就是我不應讓自己受阻於學習新技能的困難。具體說來,Python、轉碼、物件導向程式設計,這些都是我在做這個專案之前缺少專業經驗的概念。
這個信念,以及實現你自己的方案的信心,會給你帶來很大的回報。
【1】後來,當我們把新程式碼投入生產,我們又遇到了硬體問題,因為我們使用老的sr2500 Intel架構伺服器,由於它們的PCI匯流排頻寬太低,不能支援10 Gbit的乙太網卡。沒轍,我們只好把它們用在1-4×1 Gbit的乙太網池中(把多個網路卡的效能彙總為一個虛擬網路卡)。最終,我們獲得了一些更新的sr2600 i7 Intel架構伺服器,它們通過光纖達到了無效能損耗的10 Gbps頻寬。所有上述彙總的結果都是基於這樣的硬體條件來計算的。
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式