流媒體服務(wù)器的性能可以從抗抖動能力、擁塞率和卡頓率等方面進(jìn)行分析。當(dāng)用戶訪問的頻率過高或者并發(fā)的數(shù)量超過流媒體服務(wù)器所能承受的范圍時,必須考慮通過限流來保證接口的可用性,從而防止出現(xiàn)因請求數(shù)量過多、壓力過大造成服務(wù)器癱瘓等現(xiàn)象。因此,怎樣提高流媒體服務(wù)器的抗抖動能力,平滑發(fā)送流量,降低卡頓率,減少丟包率是本文描述的重點。
1.流媒體服務(wù)器框架設(shè)計
前面的CDN的文章對一些相關(guān)算法進(jìn)行了比較分析,對CDN流媒體的關(guān)鍵技術(shù)也進(jìn)行了選取。本文就基于Nginx 服務(wù)器,結(jié)合 HLS 流媒體協(xié)議與令牌桶算法,針對流媒體服務(wù)器的數(shù)據(jù)處理模塊、緩存模塊與發(fā)送模塊進(jìn)行設(shè)計。

流媒體服務(wù)器的數(shù)據(jù)處理邏輯可分為以下幾個部分:
(1) 數(shù)據(jù)處理模塊:
數(shù)據(jù)處理模塊主要負(fù)責(zé)以下幾個功能:
(a) 接收來自客戶端的請求和來自上級服務(wù)器的發(fā)送數(shù)據(jù)。
(b) 負(fù)責(zé)數(shù)據(jù)代理轉(zhuǎn)發(fā),在本地沒有直播緩存數(shù)據(jù)時,需要向上級 CDN 或者源站發(fā)送請求從而獲取數(shù)據(jù),再把數(shù)據(jù)轉(zhuǎn)發(fā)給客戶端。
(c) 將錄制頻道的直播流文件寫入存儲系統(tǒng),支持回看功能。
(2) 緩存模塊(Cache 模塊):Cache模塊用來從磁盤或者內(nèi)存中讀取實體文件,解析出文件碼率,分包后對數(shù)據(jù)包進(jìn)行時戳標(biāo)記,并將標(biāo)記好時戳的數(shù)據(jù)包壓入數(shù)據(jù)發(fā)送模塊。
(3) 發(fā)送模塊(Shaper 模塊):Shaper 模塊的主要功能是將 Cache 模塊輸入的數(shù)據(jù)發(fā)送至客戶端。Shaper 模塊在進(jìn)行數(shù)據(jù)處理時會根據(jù)當(dāng)前的發(fā)送狀態(tài)控制發(fā)送速率。Shaper 模塊中的隊列提供 FIFO(先進(jìn)先出)的數(shù)據(jù)重傳功能,保證數(shù)據(jù)在重傳時不會亂序。流媒體服務(wù)器框架設(shè)計結(jié)構(gòu)圖如下所示:

數(shù)據(jù)處理模塊在收到來自客戶端的請求后,會根據(jù)請求內(nèi)容從 Cache 模塊中尋找相應(yīng)資源,如果 Cache 模塊有相應(yīng)的資源,則直接將數(shù)據(jù)交由 Shaper 模塊進(jìn)行發(fā)送處理;如果 Cache 模塊中沒有相應(yīng)資源,則會主動到本地磁盤進(jìn)行資源查找;如果本地磁盤也沒有相應(yīng)資源,則會通過數(shù)據(jù)處理模塊向源站發(fā)送請求信息,數(shù)據(jù)處理模塊接收源站的相應(yīng)數(shù)據(jù)并將數(shù)據(jù)存放在存儲服務(wù)器中,最終 Shaper 模塊負(fù)責(zé)將用戶所請求的內(nèi)容返回給客戶端。有時,對于一些熱門的視頻業(yè)務(wù),數(shù)據(jù)處理模塊會提前向源站請求資源存入存儲服務(wù)器中,當(dāng)用戶請求到來時可以及時作出響應(yīng)。
2.流媒體服務(wù)器數(shù)據(jù)處理模塊設(shè)計
2.1 數(shù)據(jù)接收和轉(zhuǎn)發(fā)模塊設(shè)計與實現(xiàn)
在處理多客戶端請求時,可以在服務(wù)器端創(chuàng)建多個進(jìn)程,但這種做法顯然有一些弊端,創(chuàng)建進(jìn)程本身是一個消耗計算機資源的過程,一味地創(chuàng)建進(jìn)程也無法應(yīng)對快速發(fā)展的業(yè)務(wù)量。I/O 多路復(fù)用技術(shù)讓一個進(jìn)程可以處理多個客戶端需求,使得上述問題得以解決。這里也會涉及到很多優(yōu)化,所以我們需要考慮到可以使用更優(yōu)秀的商用框架。
I/O 多路復(fù)用技術(shù)可以理解為通過一種機制來監(jiān)視多個文件描述符(文件描述符是計算機術(shù)語,用來表示指向文件的引用),當(dāng)某個描述符就緒,對程序進(jìn)行相應(yīng)通知來完成讀寫操作。在 I/O 多路復(fù)用的解決方案中,有 select、poll 和 epoll 三種方式,這三種方式的本質(zhì)都是同步 I/O。當(dāng)然也可以做到異步。
epoll 模型不僅會告知應(yīng)用程序有 I/O 時間到來,還會告知 I/O 流的相關(guān)信息,因此,應(yīng)用程序不必遍歷整個文件描述符集合就能定位到 I/O 事件,其時間復(fù)雜度為 O(1)。在性能上,select 模型和 poll 模型都會隨著連接數(shù)的增加性能急劇下降,而 epoll 模型在處理成千上萬的并發(fā)時,性能沒有很大的改變,且其對連接數(shù)上限沒有限制。epoll 模型最大的優(yōu)點在于它只關(guān)心活躍的連接,而不關(guān)心總連接數(shù),因此在實際的網(wǎng)絡(luò)環(huán)境中,epoll 的效率遠(yuǎn)超于 select 模型與 poll 模型。epoll 除了在開發(fā)復(fù)雜度上略有難度,在內(nèi)部使用數(shù)據(jù)結(jié)構(gòu)、內(nèi)核空間以及內(nèi)在處理文件描述符機制等方面都優(yōu)于 select,最重要的是,epoll 是實現(xiàn)了高并發(fā)的基礎(chǔ)。select、poll 與 epoll比較如下圖所示:

Nginx 已有的數(shù)據(jù)處理模塊就是基于 epoll 技術(shù)實現(xiàn)的,采用回調(diào)機制(callback)實現(xiàn)所有接收數(shù)據(jù)相關(guān)的函數(shù),數(shù)據(jù)處理模塊結(jié)構(gòu)圖如下所示:

代理轉(zhuǎn)發(fā)模式下,nginx_http_upstream_init_request 函數(shù)作為該模式的入口函數(shù),將此函數(shù)注冊到 nginx_http_upstream_process 中,當(dāng)進(jìn)入調(diào)用鏈時,將需要監(jiān)控的 epoll 句柄加入到nginx_add_event 中,在和上級源站建立連接成功后或者有數(shù)據(jù)交互時,epoll 監(jiān)控到的句柄被觸發(fā),從建立連接,發(fā)送請求,接收數(shù)據(jù)(此過程稱為代理回源)。
在數(shù)據(jù)接收流程中,ngx_event_accept函數(shù)作為該模式的入口函數(shù),在系統(tǒng)啟動時,通過注冊到 nginx_event_process_init 函數(shù)中,將需要監(jiān)控的 epoll 句柄加入到 nginx_add_event中。當(dāng)有客戶端請求時,epoll 監(jiān)控的句柄被觸發(fā),進(jìn)而對連接請求進(jìn)行處理。
2.2 數(shù)據(jù)錄制模塊設(shè)計與實現(xiàn)
Nginx 目前沒有提供支持 HLS 或者 TS 協(xié)議的錄制模塊,本文采用 epoll 異步通知機制,設(shè)計錄制模塊,數(shù)據(jù)錄制模塊異步線程調(diào)用關(guān)系圖如下所示:

上圖中,Proxy 線程與文件 I/O 線程都是 Nginx 自帶的模塊。Proxy 線程負(fù)責(zé)接收來自于源站的請求數(shù)據(jù),它將每個請求的文件描述符 fd 句柄加載到 epoll 中,再分發(fā)到各個錄制線程中,同時通過調(diào)用 submit_task()接口把錄制任務(wù)傳遞到錄制線程中。錄制線程則是把接收到的錄制任務(wù)數(shù)據(jù)進(jìn)行整合(包含對錄制任務(wù)數(shù)據(jù)進(jìn)行拼裝、去重、校驗、容錯等步驟)后,同樣將對應(yīng)的文件描述符 fd 句柄加載到 epoll 中為 I/O 線程進(jìn)行調(diào)用,同時調(diào)用 submit_task()接口進(jìn)行異步寫盤。在進(jìn)行異步寫盤時,默認(rèn)寫盤數(shù)據(jù)大小一般設(shè)置 1MB 作為基本單位,這是因為當(dāng)寫盤數(shù)據(jù)大小高于或是低于 1MB 時,內(nèi)部存儲器調(diào)用 read()函數(shù)次數(shù)過多,會導(dǎo)致 CPU過高,且數(shù)據(jù)排列組合多,浪費時間。因此選擇 1MB 作為基本單位進(jìn)行數(shù)據(jù)寫盤時效率最高。文件 I/O 線程負(fù)責(zé)接收錄制線程發(fā)送來的數(shù)據(jù),是 Nginx 已封裝好的模塊,可以直接調(diào)用。
2.3 流媒體服務(wù)器數(shù)據(jù)緩存模塊設(shè)計
在傳統(tǒng)的 Nginx 流媒體服務(wù)器中,只有直播業(yè)務(wù)會采用緩存機制(Cache),但對于點播業(yè)務(wù)來講,傳統(tǒng)的 Nginx 流媒體服務(wù)器沒有 Cache 機制。點播業(yè)務(wù)會采用直接讀取磁盤上數(shù)據(jù)文件的方式,這種方式會導(dǎo)致磁盤 I/O 讀寫頻繁,不僅影響系統(tǒng)效率,還會加速磁盤老化。因此,本節(jié)基于 Nginx 流媒體服務(wù)器,在點播業(yè)務(wù)上新增緩存模塊,流媒體服務(wù)器會先從 Cache模塊中獲取所需要的數(shù)據(jù),如果 Cache 模塊中沒有找到,才會從本地磁盤進(jìn)行讀取,這大大減少了磁盤的讀寫,從而減少系統(tǒng)開銷,使性能得以優(yōu)化與提升。
(1)Cache 模塊模型架構(gòu)
Cache 模塊包含了 Cache 模塊與本地磁盤的交互方式。當(dāng)客戶端發(fā)起請求后,數(shù)據(jù)接收模塊會根據(jù)請求內(nèi)容在 Cache 模塊中尋找對應(yīng)文件內(nèi)容。Cache模塊中的 Cache_read()函數(shù)和 Cache_open()函數(shù)分別負(fù)責(zé)對內(nèi)容資源的讀取與打開。請求來臨時,服務(wù)器會根據(jù)用戶請求所對應(yīng)的文件描述符 fd 調(diào)用 Cache_read()函數(shù)進(jìn)行內(nèi)容讀取,之后調(diào)用 Cache_open()函數(shù)打開文件。若此時從 Cache 模塊中獲取到了相應(yīng)資源,則直接將資源交由 Shaper 模塊進(jìn)行發(fā)送處理;若沒有從 Cache 模塊中獲取到相應(yīng)資源,則會根據(jù)文件描述符 fd 從本地磁盤中進(jìn)行查找。
(2)本地磁盤包含兩大模塊,分別為 IO_open 模塊和 IO_read 模塊,其功能與 Cache 模塊中的 Cache_read()函數(shù)和 Cache_open()函數(shù)類似,負(fù)責(zé)對內(nèi)容資源的讀取與打開,但調(diào)用方向有所區(qū)別。
當(dāng)在 Cache 模塊中沒有獲取到索要找尋的數(shù)據(jù)時,會先調(diào)用本地磁盤的 IO_open 模塊將文件打開,并將文件描述符 fd 返回給 Cache 模塊中的 Cache_open()函數(shù),之后調(diào)用 IO_read模塊進(jìn)行文件內(nèi)容讀取,并將讀取的內(nèi)容返回給 Cache 模塊中的 Cache_read()函數(shù)。這樣一來,Cache 模塊中的數(shù)據(jù)會得到及時更新,當(dāng)請求相同的資源時會直接從 Cache 模塊中進(jìn)行讀取,從而減少了對本地磁盤的操作。對于長時間沒有被請求的內(nèi)容 Cache 模塊會對其定期清理。Cache 模塊模型架構(gòu)圖如下所示:

Cache 模塊從功能層面來講可以分為兩大部分:
(3) Cache_block:用于存儲文件數(shù)據(jù)塊的內(nèi)容和相關(guān)信息。
(4) Cache_file:是應(yīng)用層進(jìn)行文件操作的基本單元,使用文件句柄 Cache_fd 作為基本憑證。Cache_file 包括兩種 block,分別為 cache_block_list 和 uncached_block_list,如下圖所示:

2.4 Cache 模塊的主要接口設(shè)計
Cache 模塊的主要接口設(shè)計如下:
(5) Cache_open()
文件打開接口,主要功能如下:
(a) 查看文件狀態(tài)。
(b) 在文件 hash 表中查找文件是否已經(jīng)被打開,如果已經(jīng)被打開,則刷新引用計數(shù)(ref),并直接返回;如果未被打開,則在 hash 表里增加一個表項,并使用 dump_file()接口打開該文件。
(6) Cache_read()
讀文件接口,主要功能如下:
(a) 增加文件引用數(shù)。
(b) 在 cache_file 的 block_list 中查找塊是否存在,如果存在則直接返回給調(diào)用者;如果未找到,則向空閑塊列表中申請一個新的塊。
(c) 調(diào)用 dump_file()接口進(jìn)行文件讀取。
(d) 將塊加入到 used_list 和 cache_file 中。
(3) File_closed()
文件關(guān)閉接口,主要功能如下:
(a) 減少引用計數(shù),一旦引用計數(shù)為 0,則執(zhí)行文件關(guān)閉流程。
(b) 同步調(diào)用 dump_file()接口來關(guān)閉文件。
(c) 同步和異步銷毀 cache_file 文件,其中需要清空 cache_file 文件所擁有的 block_list。
(4) Io_read() 文件預(yù)讀接口,主要功能如下:
(a) 檢查 block 是否已經(jīng)在 cache 中存在,如果存在則無需進(jìn)行預(yù)讀。
(b) 向預(yù)讀線程提交預(yù)讀任務(wù),預(yù)讀從線程中增加文件的引用計數(shù),防止預(yù)讀過程中,文件被關(guān)閉。
(c) 調(diào)用 cache_read()接口,同時回滾文件的引用計數(shù)。
(5) Block_aging()
塊老化接口,主要功能如下:
(a) 遍歷 used_list,將引用數(shù)為 0 的 block 塊加入老化列表,并按照時間排序。
(b) 遍歷老化列表,將排名靠前的 N 個塊從 used_list 中摘除,放入 free_list,一旦空閑塊數(shù)量達(dá)到要求(閾值的兩倍),則停止老化。
2.5 Cache 模塊的功能設(shè)計與實現(xiàn)
Cache_file 用來存儲一個個的緩存數(shù)據(jù)塊,數(shù)據(jù)塊包括兩種,cache_block_list 和 uncached_block_list。在對這兩個模塊進(jìn)行數(shù)據(jù)存儲時,可供選擇的數(shù)據(jù)結(jié)構(gòu)有鏈表、二叉樹、哈希結(jié)構(gòu)等
(1) 鏈表(含單向鏈表、雙向鏈表)鏈表的特點是便于元素的增加與刪除,開銷較小,但在對元素的查找時效率很低。因此其多用在數(shù)據(jù)量較小,且不需要排序的場景。
(2) 二叉樹(含紅黑樹、平衡二叉樹)
二叉樹的特點是在插入任意元素時都可以自動進(jìn)行排序,使元素保持有一定的特性。由于二叉樹的空間占用與輸入的數(shù)據(jù)保持一致,所以不需要為二叉樹預(yù)先分配固定的空間。因此,二叉樹多是用在頻繁對數(shù)據(jù)進(jìn)行增刪和查詢操作的場景下,在增刪改查的同時需要時刻保證數(shù)據(jù)的有序排列,且無需預(yù)先知道數(shù)據(jù)量的大小。二叉樹的時間復(fù)雜度是 O(log(n))。
(3) 哈希(hash) 哈希結(jié)構(gòu)使用鏈表和數(shù)組來管理元素,在理想的哈希算法的支持下可以達(dá)到接近數(shù)組的隨機訪問效率,因此其使用場景為無需保證元素的有序性且頻繁對數(shù)據(jù)進(jìn)行操作的場景。流媒體服務(wù)器應(yīng)用在多用戶直播或點播場景中,此場景的數(shù)據(jù)特點是數(shù)據(jù)量大且保存媒體文件時無需排序。當(dāng)一個用戶請求某一路直播(比如 CCTV2),哈希結(jié)構(gòu)可以快速根據(jù) CCTV2 的 hash 值很快找到對應(yīng)的緩存塊,從中讀取數(shù)據(jù),時間復(fù)雜度可以近似達(dá)到 O(1)。若使用鏈表或二叉樹,需對緩存中的每項內(nèi)容進(jìn)行遍歷,時間復(fù)雜度較高。其次,媒體文件本身并無排序需求,不符合二叉樹的使用場景。另外,哈希結(jié)構(gòu)的代碼開發(fā)簡便且可維護(hù)性較強。綜上選擇使用哈希數(shù)據(jù)結(jié)構(gòu)對流媒體服務(wù)器的 Cache 模塊進(jìn)行設(shè)計開發(fā)。
在數(shù)據(jù)量較大時,哈希結(jié)構(gòu)會產(chǎn)生沖突。解決哈希沖突采用的方案有兩種:第一種是線性探索,相當(dāng)于在沖突之后建立一個單鏈表,這種情況下,插入和查找以及刪除操作消耗的時間會達(dá)到 O(n);第二種方法是開放尋址,這種方法不需要更多的空間,在最壞的情況下,時間復(fù)雜度也只會達(dá)到 O(n)。
Cache_file 的哈希結(jié)構(gòu)在 Cache 模塊中的實現(xiàn)方式。哈希表的 key 值是由點播或者直播的媒體文件名通過哈希表達(dá)式計算得來。每一個 file 文件后又以鏈表的形式存有若干block,當(dāng)需要查找資源時,只需按照哈希表達(dá)式來進(jìn)行查找即可,這樣的數(shù)據(jù)查找方式在一定程度上提高了查找效率。Cache_file 的哈希實現(xiàn)方式如下所示:

(4) 在讀取數(shù)據(jù)入隊之前,Cache 模塊會將數(shù)據(jù)打上時間戳,時間戳的計算方法模擬令牌桶原理,每一個數(shù)據(jù)包的時間戳計算公式如下:

T 為當(dāng)前數(shù)據(jù)包的 RTC 時間戳,size 為當(dāng)前數(shù)據(jù)包的大小,TS 格式下的媒體元數(shù)據(jù)單位是 1316 字節(jié),speed 為當(dāng)前流媒體文件碼率,t 為上一個數(shù)據(jù)包的 RTC 時間戳。從公式可以看出,計算單位時間 1 秒內(nèi)發(fā)送的報文數(shù) n,可利用如下公式:

即給 n 個數(shù)據(jù)包打上時間戳。即在 time 秒內(nèi)理論發(fā)送數(shù)據(jù)可以達(dá)到如下公式:

從上面的步驟可以看出,通過給數(shù)據(jù)包打 RTC 時戳的方式,可以計算單位時間內(nèi)的發(fā)包個數(shù),從而對其進(jìn)行精確控制,實現(xiàn)均勻控制碼率的效果,與傳統(tǒng)令牌桶的實現(xiàn)方式相比,這種時間戳控制方式,在保證速率控制的同時,減少了發(fā)送模塊對令牌結(jié)構(gòu)出隊入隊(保證一定的數(shù)據(jù)包個數(shù)),申請內(nèi)存等操作,提升了系統(tǒng)性能。
3.發(fā)送模塊設(shè)計
Shaper 模塊即數(shù)據(jù)發(fā)送模塊,主要負(fù)責(zé)將 Cache 模塊入隊的數(shù)據(jù)進(jìn)行發(fā)送,并計算發(fā)送結(jié)果,根據(jù)單位時間內(nèi)發(fā)送的字節(jié)數(shù) B 計算出當(dāng)前的鏈路帶寬 b,實時更新每一個發(fā)送對端的鏈路速度,針對每一路用戶的數(shù)據(jù)流進(jìn)行數(shù)據(jù)降速或者升速。上面和下面這些公式就說明怎樣去控制發(fā)送速率,非常重要。
3.1Shaper 模塊模型架構(gòu)
流媒體服務(wù)器發(fā)送模塊的結(jié)構(gòu)如下圖所示,圖中說明了數(shù)據(jù)的流程。

(1)本系統(tǒng)結(jié)合具體的業(yè)務(wù)場景增加了桶水位線的優(yōu)化:如果當(dāng)前媒體文件的碼率為 s,流媒體服務(wù)器數(shù)據(jù)緩存模塊以每 1/s 的精度,從當(dāng)前 RTC 時鐘自加,給每個數(shù)據(jù)包作時戳標(biāo)記。數(shù)據(jù)發(fā)送模塊預(yù)先開辟大小為 m 的隊列,將做好時間戳標(biāo)記且需要發(fā)送出去的數(shù)據(jù)入隊,當(dāng)數(shù)據(jù)包到達(dá)發(fā)送時間后,獲得令牌,出隊發(fā)送,每個隊列有一條上水位線 H 和下水位線 L,當(dāng)網(wǎng)絡(luò)阻塞時,若隊列內(nèi)的數(shù)據(jù)大于等于 H,則新到的數(shù)據(jù)不入隊,隊列中的每個數(shù)據(jù)時戳向后移動 d 個精度,這樣降低了發(fā)送速率,減少了網(wǎng)絡(luò)擁塞。降低后的速率為 如下計算公式:

當(dāng)網(wǎng)絡(luò)狀況良好,無發(fā)送失敗記錄,并且隊列中的數(shù)據(jù)量在[L,H]之間,則將令牌生成間隔相對提高%d,按緩慢升速發(fā)送,如下計算公式:

直到隊列水位線下降到 L 處,為了保留緩存,數(shù)據(jù)發(fā)送速率恢復(fù)到媒體文件碼率 s,當(dāng)數(shù)據(jù)發(fā)送成功后,該數(shù)據(jù)的令牌被銷毀。所以令牌桶的原理就是通過控制令牌產(chǎn)生的速度,將數(shù)據(jù)發(fā)送碼率平穩(wěn)在 s 左右,對于每一個連接,令牌桶的數(shù)據(jù)流量為 B,計算公式如下:


3.2 Shaper 模塊運行機制
Shaper 模塊設(shè)計采用異步多線程運行機制,應(yīng)用添加線程包中的數(shù)據(jù)來自于本地磁盤或是 Cache 模塊中的數(shù)據(jù)文件。Shaper 模塊中創(chuàng)建若干個發(fā)包線程,這些線程采用異步方式可以對應(yīng)用添加包中的數(shù)據(jù)同時進(jìn)行操作,這樣做的好處就是,當(dāng)多個請求發(fā)來且訪問同一資源時,無需等待資源訪問結(jié)束,在空閑等待時同時可以訪問其他資源,提高了服務(wù)器的發(fā)送效率。如下圖所示:

3.3 令牌桶在 Shaper 模塊中的應(yīng)用
將具體敘述如何將令牌桶算法應(yīng)用在 Shaper 模塊中以及應(yīng)用令牌桶算法對流量整形產(chǎn)生的影響。在服務(wù)器的內(nèi)部存儲池中設(shè)計令牌產(chǎn)生模塊,實現(xiàn)以一定速率產(chǎn)生令牌。這些令牌在代碼中的具體實現(xiàn)是一些虛擬的數(shù)據(jù)包,當(dāng)流媒體服務(wù)器接收數(shù)據(jù)時,會根據(jù)系統(tǒng)設(shè)定的匹配規(guī)則對接收的數(shù)據(jù)進(jìn)行分類,符合規(guī)則的數(shù)據(jù)交由令牌桶處理,而不符合規(guī)則的數(shù)據(jù)包,直接拒之門外。Shaper 模塊的數(shù)據(jù)來源有兩處,一處是來自 Cache 模塊,一處是來自本地磁盤。不論數(shù)據(jù)來自哪里,Shaper 模塊都會將收到的數(shù)據(jù)打包并作時戳標(biāo)記之后放入隊列。數(shù)據(jù)包根據(jù)時戳?xí)Q定何時出隊發(fā)送,出隊后的數(shù)據(jù)包會在內(nèi)部存儲池中求取令牌,只有拿到令牌的數(shù)據(jù)包才可發(fā)送給客戶端。當(dāng)數(shù)據(jù)包沒有獲得令牌(通常是令牌桶中的令牌不足的原因造成),服務(wù)器為隊列設(shè)置阻塞模式,數(shù)據(jù)包無法出隊,阻塞在隊列中。若阻塞數(shù)據(jù)包超過了隊列的上線,則新的數(shù)據(jù)包無法入隊。因此,我們可以通過控制令牌的產(chǎn)生速率來控制數(shù)據(jù)包的出隊入隊速率,從而達(dá)到對流量的控制。令牌桶在 Shaper 模塊中的應(yīng)用如下圖所示:

假設(shè)令牌桶的容量為 D(單位:字節(jié)),產(chǎn)生令牌的速率為 v(單位:字節(jié)/秒),輸出數(shù)據(jù)的最大速率為 V(單位:字節(jié)/秒),突發(fā)的時間為 s(單位:秒)。當(dāng)突發(fā)數(shù)據(jù)來臨前假設(shè)桶中的容量為 D-C,則有:

經(jīng)過變型,則有:

由此可以計算出所能承受的突發(fā)時間,從而可以根據(jù)系統(tǒng)突發(fā)時間上限在代碼中進(jìn)行設(shè)置。
令牌桶實現(xiàn) Shaper 模塊的主要接口如下:
(1) bool Udp Shaper Channel()
此函數(shù)實現(xiàn)的功能是以 UDP 方式發(fā)送媒體數(shù)據(jù),首先根據(jù)時間戳下發(fā) ret 數(shù)據(jù),之后發(fā)送媒體數(shù)據(jù),如果成功則返回 True,失敗則返回 False。
(2) void Interleaved Shaper Channel()
此函數(shù)首先發(fā)送信令(發(fā)送信令的時戳為當(dāng)前時間),再根據(jù)時戳決定當(dāng)前是否要發(fā)送媒體數(shù)據(jù)。若到達(dá)發(fā)送媒體數(shù)據(jù)的時間則進(jìn)行發(fā)送;若沒有到達(dá),則等待。由于 TCP 有擁塞控制,當(dāng)發(fā)送失敗時,數(shù)據(jù)包的時戳?xí)r間會改為當(dāng)前時間加上延遲時間。
(3) void Download Shaper Channel()
此函數(shù)功能為下載媒體數(shù)據(jù),負(fù)責(zé)發(fā)送碼率的升速與降速。在函數(shù)中設(shè)置發(fā)送碼率的上線發(fā)送碼率,同級周期內(nèi)(1s)若發(fā)送失敗率為 0,則提速 5%;若發(fā)送失敗率小于 10%,則降速 5%。設(shè)置升速時,升速不能超過發(fā)送碼率的上線,降速不能低于 12Kbps。
3.4 數(shù)據(jù)重傳
Shaper 模塊在發(fā)送數(shù)據(jù)包的過程中極有可能發(fā)生包丟失等異常情況,本系統(tǒng)在 Shaper 模塊中增加重傳邏輯用以應(yīng)對此類情況。設(shè)計邏輯圖如下圖所示:

Shaper 模塊在進(jìn)行數(shù)據(jù)傳輸時,會根據(jù) send()函數(shù)返回值對數(shù)據(jù)是否有丟包進(jìn)行判斷。例如,在傳輸 10 個數(shù)據(jù)包的情況下,當(dāng) send()返回值為 7 * 1316 字節(jié)(1316字節(jié)為 TS 格式下的媒體元數(shù)據(jù)單位),即說明有 3 個數(shù)據(jù)包在傳輸時發(fā)生丟失,此時根據(jù)時戳將待傳輸?shù)臄?shù)據(jù)包加入重傳隊列,在下次傳輸數(shù)據(jù)時優(yōu)先傳輸重傳隊列中的數(shù)據(jù)包,之后再對 Cache 模塊中的新數(shù)據(jù)隊列進(jìn)行傳輸。具體重傳時間的計算公式如下:

4 流媒體服務(wù)器參數(shù)優(yōu)化
Nginx 服務(wù)器的配置系統(tǒng)很靈活。只需通過對配置項的增加、修改即可實現(xiàn)第三方模塊的信息提供。將對 Nginx 服務(wù)器與內(nèi)核的配置文件進(jìn)行相關(guān)設(shè)置,從而實現(xiàn)流媒體服務(wù)器的參數(shù)優(yōu)化。
4.1 流媒體服務(wù)器配置文件優(yōu)化
為了提高 Nginx 服務(wù)器的性能,需對流媒體服務(wù)器進(jìn)行參數(shù)調(diào)優(yōu)。Nginx 可以通過修改默認(rèn)安裝路徑下/usr/local/nginx/conf/nginx.conf 的 nginx.conf 文件來進(jìn)行參數(shù)配置。調(diào)優(yōu)模塊分為三類,全局塊、Events 塊和 HTTP 塊。
(1)全局塊,配置文件優(yōu)化如下:
worker Process_num:4;
worker_cpu_affinity 0001 0010 0100 01000;
(a) worker Process_num 是 Nginx 創(chuàng)建的 worker 進(jìn)程的個數(shù)。Nginx 中基本的網(wǎng)絡(luò)事件都由 worker 進(jìn)程處理,所以 worker 進(jìn)程的數(shù)量對 Nginx 的性能會有很大的影響。如果設(shè)置太小,則 Nginx 服務(wù)器很難應(yīng)對高并發(fā)的請求數(shù)量,浪費了資源;如果設(shè)置過多會 導(dǎo) 致 進(jìn) 程 之 間 的 頻 繁 切 換 , 造成了不必要的開銷 。一 般來說 ,通常將worker Process_num 設(shè)置為服務(wù)器CPU的內(nèi)核數(shù)量。
(b) worker_cpu_affinity 是為每個 work 進(jìn)程設(shè)置一個固定 CPU 內(nèi)核。當(dāng) Nginx 中的每個worker 進(jìn)程都處于工作狀態(tài)時,可能產(chǎn)生多個 worker 進(jìn)程爭搶一個內(nèi)核的現(xiàn)象,設(shè)置 worker_cpu_affinity 可以減少因選擇不同 CPU 內(nèi)核產(chǎn)生的開銷。
(2)Events 塊,配置文件優(yōu)化如下:
worker_connections:102400;
use epoll;
Events 塊對 Nginx 中事件模塊進(jìn)行配置來控制和處理訪問連接。
(a) worker_connections 用來設(shè)置每個 worker 進(jìn)程的最大連接數(shù),最大連接數(shù)應(yīng)與全局塊中的 worker File_limit Num 值一樣。
(b) use 用來設(shè)置事件驅(qū)動模型。在不同操作系統(tǒng)下選取的事件驅(qū)動模型也有所不同,對于 linux 系統(tǒng)來說,epoll 模型是較為高效的事件管理機制,在性能方面相對于標(biāo)準(zhǔn)事件模型 select 和 poll 要高很多。
(3)HTTP 塊主要從網(wǎng)絡(luò)連接設(shè)置、文件緩存設(shè)置、Socket 優(yōu)化設(shè)置三部分組成。
HTTP 塊是 Nginx 配置的核心部分,對連接處理行為的設(shè)置很多都需要在此塊中進(jìn)行設(shè)置,其中包括負(fù)責(zé)負(fù)載均衡的 upstream 塊,對虛擬主機進(jìn)行配置的 server 塊,以及反向代理location 塊等。網(wǎng)絡(luò)連接設(shè)置,配置文件優(yōu)化如下:
(a) 網(wǎng)絡(luò)連接超時設(shè)置通過設(shè)置連接超時時間上限,可以在請求連接超時后,對其進(jìn)行關(guān)閉,將超時連接所占用的系統(tǒng)資源進(jìn)行釋放。在面對高并發(fā)數(shù)量請求時,可以提供更好的資源利用率和服務(wù)效率。
tcp_nodelay on;
SO_REUSEPORT
(4)文件緩存設(shè)置,配置文件優(yōu)化如下:
sendfile 指令指定 Nginx 是否調(diào)用 Linux 的系統(tǒng)函數(shù) sendfile()來發(fā)送文件。如果服務(wù)器用來進(jìn)行下載等對磁盤 I/O 消耗高的操作,可以設(shè)置為 off;如果對于普通應(yīng)用必須設(shè)定為 on,sendfile()函數(shù)用以提高文件的發(fā)送效率。
open_file_cache 指令用于開啟文件緩存功能。 max 為緩存的數(shù)量上限,應(yīng)和worker_connections數(shù)量保持一致,inactive 設(shè)定一個時間,當(dāng)文件在此時間內(nèi)沒被請求,則系統(tǒng)會刪除緩存。
open_file_cache_valid 用來指定檢查緩存有效性的時間間隔。
open_file_cache_min_uses 表示 inactive 時間內(nèi)文件的最小使用次數(shù),如果小于此值,文件將被刪除。
send_file on;
open_file_cache max = 1024 inactive = 30s;
open_file_cache_valid 60s;
open_file_cache_min_uses 3;
(5)Socket 優(yōu)化設(shè)置,配置文件優(yōu)化如下:
tcp_nodelay 指令開啟后,服務(wù)器將不會對數(shù)據(jù)進(jìn)行緩存。reuseport指令用來設(shè)置共享 socket。
reuseport 的設(shè)置是用來解決 Nginx 中“驚群”問題。“驚群”問題是指一個連接來臨時會喚醒多個進(jìn)程對其進(jìn)行搶奪處理。
在連接來臨時,多個 worker 線程共享一個套接字,這時便會出現(xiàn)多個線程去搶占資源的情況。傳統(tǒng)的 Nginx 在處理這個問題時有兩種解決方式:一是配置 accept_mutex on,在 accept()函數(shù)調(diào)用之前采用鎖機制,獲得鎖的進(jìn)程才有權(quán)對套接字進(jìn)行處理,但鎖機制會影響服務(wù)器的性能,另外,這種方式的弊端是所有的 worker 進(jìn)程都會被喚醒去爭奪鎖,會對內(nèi)核資源造成浪費;二是設(shè)置 accept_mutex off,這種處理方式雖然少了鎖機制,提高了系統(tǒng)響應(yīng)能力,但會出現(xiàn)“驚群”問題,在 worker 進(jìn)程數(shù)增多時會對系統(tǒng)帶來一定性能影響。
tcp_nodelay on;
SO_REUSEPORT
注意:以上這些配置文件僅供參考,根據(jù)實際情況去優(yōu)化。
(6)reuseport 允許多個套接字監(jiān)聽同一端口,圖中 80 端口為 HTTP 默認(rèn)端口,內(nèi)核能夠在套接字中對傳入的連接進(jìn)行負(fù)載均衡,自動選擇 worker 進(jìn)程進(jìn)行處理,而不必喚醒所有 worker 進(jìn)程。采用此種方式進(jìn)行 SO_REUSEPORT 配置時,需將 accept_mutex 關(guān)閉。配置如下圖所示:

在 未 開 啟SO_REUSEPORT 配置時,請求來臨時,由一個套接字 socket 進(jìn)行綁定和監(jiān)聽并將連接交給各個進(jìn)程處理;當(dāng)開啟了 SO_REUSEPORT 配置時,多個進(jìn)程可以同時綁定和監(jiān)聽同一個TCP/UDP 端口,請求交由哪個進(jìn)程處理由內(nèi)核決定,實現(xiàn)了在內(nèi)核層面的負(fù)載均衡。未開啟和開啟SO_REUSEPORT對比圖如下:

5.Linux 內(nèi)核參數(shù)優(yōu)化
默認(rèn)的 Linux 內(nèi)核參數(shù)無法滿足高并發(fā)訪問量情況對服務(wù)器系統(tǒng)的要求,為了滿足特定場景的使用需要同時對 Linux 內(nèi)核參數(shù)進(jìn)行設(shè)置,而且當(dāng) Nginx 服務(wù)器用于不同目的時,對于 Linux 參數(shù)的要求都會有很大的不同。針對以上情況,本文介紹對 Linux 內(nèi)核參數(shù)的優(yōu)化,使其能夠提高 Nginx 對并發(fā)請求的服務(wù)性能。
Linux 內(nèi)核參數(shù)選項是通過修改/etc 目錄下的 sysctl.conf 文件來更改
(1) fs.file--max = 999999 此參數(shù)用來限制一個進(jìn)程可以同時建立的最大連接數(shù)。Nginx 服務(wù)器通過每個 worker 進(jìn)程來進(jìn)行實際的請求響應(yīng),所以此參數(shù)的設(shè)置會影響到 Nginx 的最大并發(fā)連接數(shù)量。
(2) net.ipv4.tcp_tw_reuse = 1 此參數(shù)用來開啟重用功能,可以設(shè)置為 1 或 0。當(dāng)設(shè)置為 1 時,服務(wù)器上處于 TIME_WAIT狀態(tài)的請求連接會被系統(tǒng)重新建立 TCP 連接。
(3) net.ipv4.tcp_fin_timeout = 10 這個參數(shù)是用來設(shè)置保持在 FIN_WAIT_2 狀態(tài)的時間。在一個 TCP 連接關(guān)閉過程中,套接字會有一個 FIN_WAIT2 狀態(tài)到 TIME_WAIT 狀態(tài)的變化,將此參數(shù)設(shè)置為 10,可以使套接字更快的進(jìn)入 TIME_WAIT 狀態(tài)。
6.總結(jié)
本文主要介紹了在CDN與流媒體服務(wù)器合作下,當(dāng)流媒體數(shù)據(jù)存儲在服務(wù)器上,是如何處理,主要劃分為三大模塊,分別為數(shù)據(jù)處理模塊、數(shù)據(jù)緩存模塊、數(shù)據(jù)發(fā)送模塊,分別進(jìn)行了模塊的架構(gòu)設(shè)計、模塊的主要功能的講解,特別在數(shù)據(jù)發(fā)送模塊中設(shè)計了令牌桶算法(這個很重要),對于流量控制很有幫助。歡迎關(guān)注,收藏,轉(zhuǎn)發(fā),分享。