天天看點

B站下行CDN架構的探索與應用

作者:閃念基因

背景介紹

B站的下行CDN舊架構如下圖所示,可以看到邊緣CDN節點與中心排程服務有緊密協作,簡單說是先由排程服務進行流量排程(負責均衡的排程到每個網關元件節點),再由回源元件進行叢集内的回源收斂,最終到對應的回源節點進行回源。随着業務體量的增加,這種模式所帶來的風險也不斷的被暴露出來。

B站下行CDN架構的探索與應用

注:同叢集指在相同機房;節點指實體機器或者容器

這種模式存在以下幾種弊端:

  • 中心排程服務的負載均衡政策與邊緣同叢集内的回源收斂策之間的協作難度大(回源節點選擇不一緻、資源冷熱判斷不一緻等等),導緻"事故"頻繁
  • 邊緣節點出故障後,中心排程服務從感覺到故障事件,再到流量切走,再到長尾流量幹涸,這一過程起碼需要20分鐘,遇到直播業務那就更慘
  • 資源(cpu、緩存)使用率不符合預期,存在毛刺多、不均衡和回源率高等問題,頻繁觸發SLO告警
  • 治理/提升使用者播放體驗的開發難度大且複雜性高,比如資源預熱,需要提前知道直播流會配置設定到哪幾個網關節點上等等

根據以上幾個點以及點/直播業務特性,我們提出了幾個最基本的要求,以滿足日益增長的流量和對播放品質的追求

  • 網關元件必須具備流量分流能力,具備7層負載均衡加4層負載均衡能力
  • 一套叢集内元件服務狀态檢查機制,做到及時發現過載元件進行擴容以及能敏感發現故障元件進行踢出
  • 所有政策功能都收斂到中控元件來完成,避免同個資源在不同節點上出現兩種不同流量政策,同時也讓原有的其他元件改動最小化(保持單體架構的開發思維,提升開發效率)

新架構設計

B站下行CDN架構的探索與應用

網關元件:對外提供使用者的通路協定(H1、H2、H3),具備對通路流量進行鑒權、收斂等功能

緩存元件:聚焦磁盤IO技術棧、檔案系統以及緩存淘汰政策等

回源元件:聚焦回源協定(私有/通用)以及如何提升回源速率等

中控元件:聚焦叢集内流量路由政策(熱流打散、異常元件踢出等等)以及針對業務的定制化優化政策等

網關元件具備流量分流能力

部分可以拆分成4層負載均衡和7層負載均衡,其中4層負載均衡是由其他組同學完成的(見:)

4層負載均衡

主要解決由于排程不均衡導緻的節點負載不均衡問題,同時又能及時切走故障的7層節點,提升系統的整體容錯性

B站下行CDN架構的探索與應用

7層負載均衡

主要解決當緩存元件或者回源元件出現問題時,踢掉問題元件并且重試到其他節點(回源依舊會優先回到之前的回源元件節點:圖中5-1路線),保障使用者的播放體驗

B站下行CDN架構的探索與應用

中控元件

現在流量入口的問題已經解決了,部分節點故障已經影響不到服務品質,接下來我們重點看一下中控元件(中控元件本身是多節點,且保證查詢接口是資料一緻的)是如何将其他元件節點串起來的

負載均衡政策

直播和點播的業務場景還是有些差別的,疊合一些曆史問題,這次改造将點/直播負載均衡政策拆成兩個單獨子產品實作,後續計劃還是會整合到一起

直播負載均衡政策

我們将負載能力劃分成一個個小長方條,當某個資源(流)占據的條數>=N時,則會觸發流量打散政策,将溢出的流量劃分給其他元件,如圖中“元件-1”内的深綠色大長方條,被平均切割成3個中長方條

B站下行CDN架構的探索與應用

接着,我們可以看到網關元件會将每個請求的實時帶寬同步到中控,這也是所有政策的資料源。中控元件對資料進行清洗、整合和觀測,實時調整網關元件的流量鍊路,如圖中綠色的實線鍊路

B站下行CDN架構的探索與應用

點播負載均衡政策

因點播檔案大小普遍大于緩存元件的分片大小,是以在網關元件這邊根據分片大小進行切分,并用一緻性哈希方式進行負載均衡,雖然做法很糙,但也足夠用

B站下行CDN架構的探索與應用

這裡我們在nginx http slice 子產品上進行了改造,原有的http_slice_module存在以下幾個問題:

1)對齊後的offset會導緻讀放大(存在備援資料),原設計是為了緩存,但我們架構有專門的緩存元件,并不需要nginx做緩存

2)一定是前面的子請求結束後,才能發起下一輪的子請求,這可能導緻非預期的“等待”(假設使用者請求被切成N個請求,則需要多等待N-1個http header的結果)

我們的做法是增加預取視窗,在subrequest-2收到http header時,會觸發subrequest-3請求的發送,同時控制同一時間隻會存在兩個subrequest,這樣保證nginx記憶體不會過于膨脹,同時也不會對後端節點造成過大壓力

B站下行CDN架構的探索與應用

代碼如下:

typedef struct {
 ...
    ngx_http_request_t*  prefetch[2];
} ngx_http_slice_ctx_t;
 
ngx_http_slice_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
...
    if (ctx == NULL || r != r->main) {
        // 确認前面的預取子請求已經完成header處理,可進行下一個分片預取
        if (ctx && ctx->active) {
            rc = ngx_http_slice_prefetch(r->main, ctx);
            if (rc != NGX_OK) {
                return rc;
            }
        }
        return ngx_http_next_body_filter(r, in);
    }
 
...
    rc = ngx_http_next_body_filter(r, in);
 
    if (rc == NGX_ERROR || !ctx->last) {
        return rc;
    }
 
    if (ctx->start >= ctx->end) {
        ngx_http_set_ctx(r, NULL, ngx_http_slice_filter_module);
        ngx_http_send_special(r, NGX_HTTP_LAST);
        return rc;
    }
 
    if (r->buffered) {
        return rc;
    }
    if (ctx->active) {
        // 分片預取
        rc = ngx_http_slice_prefetch(r->main, ctx);
    }
    return rc;
}
 
ngx_http_slice_prefetch(ngx_http_request_t*r,ngx_http_slice_ctx_t* ctx)
{
   // control prefetch win
    if (ctx->prefetch[1]) {
        if (!ctx->prefetch[0]->done) {
            return NGX_OK;
        }
        ctx->prefetch[0] = ctx->prefetch[1];
        ctx->prefetch[1] = NULL;
    }
 
    if (ctx->start >= ctx->end) {
        return NGX_OK;
    }
     
...
    if (ngx_http_subrequest(r, &r->uri, &r->args, &ctx->prefetch[1], ps, NGX_HTTP_SUBREQUEST_CLONE)
        != NGX_OK) {
        return NGX_ERROR;
    }
    ngx_http_set_ctx(ctx->prefetch[1], ctx, ngx_http_slice_filter_module);
    ctx->active = 0;
 
...
    // init once
    if (!ctx->prefetch[0]) {
        ctx->prefetch[0] = ctx->prefetch[1];
        ctx->prefetch[1] = NULL;
    }
        ngx_http_slice_loc_conf_t* slcf = ngx_http_get_module_loc_conf(r, ngx_http_slice_filter_module);
 
    off_t cur_end = ctx->start + get_slice_size(ctx->start, slcf);
    // 判斷對齊後末尾數值如果大于使用者請求的末尾數值,則取使用者的末位數值,避免讀放大
    if (slcf->shrink_to_fit) {
        cur_end = ngx_min(ctx->end, cur_end);
    }
    gen_range(ctx->start, cur_end - 1, &ctx->range);
...
}           

我們做了一個簡單的場景模拟,可以看到改造後請求的處理耗時比改造前處理耗時減少了40%,也就能更快響應使用者的請求

B站下行CDN架構的探索與應用
B站下行CDN架構的探索與應用

最終成效

在點/直播負載政策共同生效下,可以看到相比之前舊架構有很大的穩定性提升

B站下行CDN架構的探索與應用

我們再看看SLO監控資料,在新架構未全量前,每個周末都會有部分叢集觸發SLO告警,在新架構逐漸擴量後,SLO的抖動也減少了

B站下行CDN架構的探索與應用

聊聊新架構如何業務賦能

業務混跑

點播、直播業務對于機器資源要求不同,點播更傾向擁有更多的磁盤來做IO吞吐和存儲的橫向擴容,而直播更傾向擁有更多的cpu資源來做協定的轉封裝或資料遷移拷貝。這就出現很微妙的關系,點播場景下可以利用DMA來減少cpu參與磁盤資料拷貝(我有空閑的cpu資源),而直播場景對于機械硬碟基本不用(我有空閑的磁盤資源),一拍即合

在點直播混跑情況下,點播回源率得到下降,直播擁有更多cpu資源能跑更多流,不管點播還是直播都能對外提供更好更快的服務,并且我們營運同學再也不需要為節點角色的轉換(點播 或 直播?)而苦惱

B站下行CDN架構的探索與應用
B站下行CDN架構的探索與應用

直/點播預熱

在舊架構模式下,開啟預熱很費勁,因不知道使用者會通路哪個節點,是以隻能采用廣撒網方式,這種方式效率低不說,還可能空增加元件負擔,并且預熱任務也隻能手動下發(并不知道誰是熱的)

B站下行CDN架構的探索與應用

新架構模式下,每個叢集會選出一個中控元件來實時觀測叢集熱流,發現有熱流後會觸發預熱政策,将預熱任務下發到其中的一個網關元件

B站下行CDN架構的探索與應用

存儲資源最大化利用和編排

顯而易見在舊架構模式下,網關元件隻被允許通路本節點的緩存元件,是以隻能保證單節點的存儲資源是互斥的,并非整個叢集的存儲資源是互斥的。而新架構下,我們會根據每個緩存節點的存儲容量和磁盤IO性能,重新規劃了每個節點應被配置設定到的資源通路,盡可能保證每個緩存節點緩存的資源都是互斥的。再聊細點,我們知道機械硬碟的外圈吞吐量是大于内圈的,那在叢集閑時期間,我們是不是可以做一些存儲資源的重新編排,比如把熱的資源移動到外圈以求獲得更穩定的服務品質

B站下行CDN架構的探索與應用

作者:蔡尚志/劉勇江/楊成進/張建鋒

來源-微信公衆号:哔哩哔哩技術

出處:https://mp.weixin.qq.com/s/ccUR8mEBFltwlDvnUof9yA