色情業是個大行業。網際網路上沒有多少網站的流量能和最大的色情網站相匹敵。
要搞定這巨大的流量很難。更困難的是,在色情網站上提供的很多内容都是低延遲的實時流媒體而不是簡單的靜态視訊。但是對于所有碰到過的挑戰,我很少看到有搞定過它們的開發人員寫的東西。是以我決定把自己在這方面的經驗寫出來。
<a target="_blank"></a>
幾年前,我正在為當時全世界通路量排名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的話,我們必須為每一個要求通路特定流的使用者建立全新的一個執行個體。這是完全的浪費。

how i made porn 20x more efficient with python
想到了這些,我決定把典型的響應流重新打包和解析為flv“标簽”(這裡的“标簽”指某個視訊、音頻或者中繼資料)。這些flv标簽可以在rtmp下順利地傳輸。
這樣一個方法的好處是:
我們隻需要給流重新打包一次(重新打包是一個噩夢,因為缺少規格說明,還有前面說到的惡心協定)。
通過套用一個flv頭,我們可以在用戶端之間順暢地重用任何流,而用内部的flv标簽指針(配以某種聲明其在流内部确切位置的位移值)就可以通路到真正的内容。
我一開始用我當時最熟悉的c語言進行開發。一段時間後,這個選擇變得麻煩了,是以我開始學習python并移植我的c代碼。開發過程加快了,但在做了一些示範版本後,我很快遇到了資源枯竭的問題。python的socket處理并不适合處理這些類型的情況,具體說,我們發現在自己的python代碼裡,每個action都進行了多次系統調用和context切換,這增加了巨大的系統開銷。
在對代碼進行梳理之後,我選擇将性能最關鍵的函數移植到内部完全用c語言編寫的一個python子產品中。這基本是底層的東西,具體地說,它利用了核心的epoll機制提供了一個o(log n)的算法複雜度。
在異步socket程式設計方面,有一些機制可以提供有關特定socket是否可讀/可寫/出錯之類的資訊。過去,開發人員們可以用select()系統調用擷取這些資訊,但很難大規模使用。poll()是更好的選擇,但它仍然不夠好,因為你每次調用的時候都要傳遞一大堆socket描述符。
epoll的神奇之處在于你隻需要登記一個socket,系統會記住這個特定的socket并處理所有内部的雜亂的細節。這樣在每次調用的時候就沒有傳遞參數的開銷了。而且它适用的規模也大有可觀,它隻傳回你關心的那些socket,相比用其他技術時必須從10萬個socket描述符清單裡挨個檢查是否有帶位元組掩碼的事件,其優越性真是非同小可啊。
不過,為了性能的提高,我們也付出了代價:這個方法采用了完全和以前不同的設計模式。該網站以前的方法是(如果我沒記錯的話)單個原始程序,在接收和發送時會阻塞。我開發的是一套事件驅動方案,是以為了适應這個新模型,我必須重構其他的代碼。
具體地說,在新方法中,我們有一個主循環,它按如下方式處理接收和發送:
how i made porn 20x more efficient with python 2
接收到的資料(作為消息)被傳遞到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帶寬。所有上述彙總的結果都是基于這樣的硬體條件來計算的。

<b> 原文釋出時間為:2013-05-11</b>
<b>本文來自雲栖社群合作夥伴“linux中國”</b>