歡迎光臨
每天分享高質量文章

美拍直播首屏耗時減少50%以上的最佳化實踐

導讀:直播行業的競爭越來越激烈,各廠商對使用者體驗的追求也越來越高,這其中首屏時間的體驗尤為重要。本文中美圖的包紅來同學從DNS解析最佳化、TCP連線耗時、HTTP響應耗時、音影片流的探測耗時、buffer緩衝的耗時等五個方面非常詳細的解說了美拍直播首屏時間減少50%,達到500ms左右的一個具體最佳化實踐,對做直播的同學非常有借鑒意義。

隨著移動直播的火爆,大量的業務都有直播需求,這就使直播成了一種基本的配置。在觀看直播過程中,首屏時間是最重要的體驗之一,它的快慢直接影響了使用者對該直播APP的體驗。為了提高使用者體驗性,美拍對DNS解析最佳化、TCP連線耗時、HTTP響應耗時、音影片流的探測耗時、buffer緩衝的耗時等方面進行了最佳化,使得首屏時間從2017年初還是秒級別以上的耗時,到現在是秒級別內,耗時減少50%以上,並且大部分請求落在0~500ms 和 500~1000ms 的區間範圍,從而使得大部分熱門影片達到瞬開的效果。後面我們將基於 ijkplayer 和 ffpmeg 的原始碼進行分析。

為什麼選擇ijkplayer播放器來剖析

ijkplayer 播放器是一款開源的基於 ffmpeg 的移動版的播放器,目前已經被很多網際網路公司直接採用。它的程式碼結構比較清晰,很多做移動端影片分析的都應該接觸過,所以基於它來分析應該跟容易理解。美拍直播的播放器並不是直接採用 ijkplayer 播放器,但也是基於 ffmpeg 來實現的,邏輯跟 ijkplayer 比較類似,原理上都是相通的,最佳化點也很類似,只是額外做了一些其他相關點的最佳化。所以基於 ijkplayer 展開,也方便大家從原始碼級別可以直接看到相關的關鍵點。

一、首屏時間的影響因素

首屏時間是指從使用者從進入到直播間到直播畫面出來的這部分時間,這是觀眾最簡單,直觀的體驗。它主要受直播播放器和CDN加速策略,以及移動端手機網路的影響。可以拆分為以下個方面:

  • 點選直播後,進入到直播間後,載入一些比如使用者頭像,觀眾串列,禮物之類的會佔用網路頻寬,影響到直播載入。

  • 移動端手機網路頻寬的限制,目前一般直播的頻寬都在1Mbps左右,所以如果下行頻寬小於1Mbps,或者更小,對直播的體驗影響就會很大。

  • 直播播放器拉流的速度,以及緩衝策略的控制,對於直播類,實時性的需求更高,需要動態的緩衝控制策略,能儘快的渲染出影片畫面,減少使用者等待時間。

  • CDN是否有快取直播流,以及快取的策略對首屏影響也很大。

  • 直播拉流協議的影響,以及CDN對不同的協議最佳化支援友好程度不一樣,當前流行的拉流協議主要有 rtmp 和 http-flv。經過大量的測試發現,移動端拉流時在相同的CDN策略以及播放器控制策略的條件下,http-flv 協議相比rtmp 協議,首屏時間要減少300~400ms 左右。主要是在 rtmp 協議建聯過程中,與服務端的互動耗時會更久,所以後面的分析會直接在 http-flv 協議的基礎上。

二、首屏耗時的“條分節解”

要想最佳化首屏時間,就必須清楚的知道所有的耗時分別耗在哪裡。下麵我們以移動版的 ffplay(ijkplayer)播放器為基礎,逐漸剖析直播拉流細節。下麵我們以 http-flv 協議為拉流協議分析,http-flv 協議就是專門拉去flv檔案流的 http 協議,所以它的請求流程就是一個http 的下載流程,如下圖:

從上圖中可以看出,首屏耗時的組成主要以下基本組成:

1,DNS耗時

DNS解析,是所有網路請求的第一步,在我們用基於ffmpeg實現的播放器ffplay中,所有的DNS解析請求都是 ffmpeg 呼叫`getaddrinfo`方法來獲取的。

  • 一般耗時多久?

如果在沒有快取的情況下,實測發現一次域名的解析會花費至少300ms 左右的時間,有時候更長,如果本地快取命中,耗時很短,幾個ms左右,可以忽略不計。快取的有效時間是在DNS 請求包的時候,每個域名會配置對應的快取 TTL 時間,這個時間不確定,根據各域名的配置,有些長有些短,不確定性比較大。

  • 為什麼是這麼久?

為什麼DNS的請求這麼久呢,一般理解,DNS包的請求,會先到附近的運營商的DNS伺服器上查詢,如果沒有,會遞迴到根域名伺服器,這個耗時就很久。一般如果請求過一次,這些伺服器都會有快取,而且其他人也在不停的請求,會持續更新,下次再請求的時候就會比較快。有時候透過抓包發現每次請求都會去請求`A`和`AAAA` 查詢,這是去請求IPv6的地址,但由於我們的域名沒有IPv6的地址,所以每次都要回根域名伺服器去查詢。為什麼會請求IPV6的地址呢,因為 ffmpeg 在配置DNS請求的時候是按如下配置的:

hints.ai_family = AF_UNSPEC;

它是一個相容IPv4和IPv6的配置,如果修改成`AF_INET`,那麼就不會有`AAAA`的查詢包了。透過實測發現,如果只有IPv4的請求,即使是第一次,也會在100ms內完成,後面會更短。這個地方的最佳化空間很大。

  • 如何統計?

以 ffmpeg 為例,可以在`libavformat/tcp.c`檔案中,`tcp_open`方法中,按以下方法統計:

int64_t start = av_gettime();

if (!hostname[0])

    ret = getaddrinfo(NULL, portstr, &hints;, &ai;);

else

    ret = getaddrinfo(hostname, portstr, &hints;, &ai;);

int64_t end = av_gettime();


2,TCP連線耗時

TCP 連線在這裡是隻呼叫 Socket 的 connect 方法,並連線成功的耗時,它是一個阻塞方法,它會一直等待TCP 的三次握手完成。它直接反應了客戶端到CDN伺服器節點,點對點的延時情況,實測在一般的 wifi 網路環境下耗時在50ms以內。耗時較短,基本是沒有什麼最佳化空間的,不過它的時間反應了客戶端的網路情況或者客戶端到節點的網路情況。

  • 如何統計?

以ffmpeg為例,也是在`libavformat/tcp.c`檔案中,`tcp_open`方法中,按以下方法統計:

int64_t start = av_gettime();

if ((ret = ff_listen_connect(fd, cur_ai->ai_addr, cur_ai->ai_addrlen,

                                     s->open_timeout / 1000, h, !!cur_ai->ai_next)) < 0) {

            if (ret == AVERROR_EXIT)

                goto fail1;

            else

                goto fail;

        }

int64_t end = av_gettime();

3,http響應耗時

  • 什麼是http響應耗時?

http 響應耗時是指客戶端發起一個http request 請求,然後等待http 響應的essay-header 傳回這部分耗時。直播拉流http-flv協議也是一個http 請求,客服端發起請求後,服務端會先將http的響應頭部傳回,不帶音影片流的資料,響應碼如果是200,表明影片流存在,緊接著就開始下發音影片資料。http 響應耗時非常重要,它直接反應了CDN服務節點處理請求的能力。它與CDN節點是否有快取這條流有關,如果在請求之前有快取這條流,節點就會直接響應客戶端,這個時間一般也在50ms左右,最多不會超過200ms,如果沒有快取,節點則會回直播源站拉取直播流,耗時就會很久,至少都在200ms 以上,大部分時間都會更長,所以它反應了這條直播流是否是冷流和熱流,以及CDN節點的快取命中情況。

  • 如何統計?

如果需要統計它的話,可以在`libavformat/http.c`檔案中的,`http_open`方法 

int64_t start = av_gettime();

ret = http_open_cnx(h, options);

int64_t end = av_gettime();

4,音影片流探測耗時

  • 什麼是音影片流探測耗時?

這個定義比較模糊,它在 ffplay 中對應的是`avformat_find_stream_info`的耗時,它是一個同步的方法。在播放器中它會阻塞整個流程,因為它的作用是找到初始化音影片解碼器的必要的資料。它有一些引數會印象到它的耗時,不過如果引數設定合適的話,一般是100ms 內完成。

  • 如何統計?

可以在 ijkplayer 的工程中`ff_ffplay.c`檔案中,`read_thread`方法

int64_t start = av_gettime();

avformat_find_stream_info(ic, opts);

int64_t end = av_gettime();

5,緩衝耗時

  • 什麼是緩衝耗時?

緩衝耗時是指播放器的緩衝的資料達到了預先設定的閾值,可以開始播放影片了。這個值是可以動態設定的,所以不同的設定給首屏帶來的影響是不一樣的。我們在美拍直播播放器最開始的設定是影片幀數和音訊幀數都達到10幀以上,才可以開始播放。所以這部分一般的耗時都比較大,同時它還跟播放器裡面的一個設定 `BUFFERING_CHECK_PER_MILLISECONDS` 值有關,因為播放器 check 緩衝區的資料是否達到標的值不是隨意檢測的,因為 check 本身會有一定的浮點數運算,所以 ijkplayer 最初給他設定了500ms 值,明顯比較大,所以會對緩衝耗時有比較大的影響。

  • 如何統計?

緩衝耗時的統計方法,不像前面幾個那麼簡單,因為它涉及到的程式碼有多處,所以需要再多個地方計時。 開始計時可以直接從前面的find後面開始,結束計時可以在第一幀影片渲染出來的時候結束計時。

avformat_find_stream_info(ic, opts);

start = av_gettime();

if (!ffp->first_video_frame_rendered) {

    ffp->first_video_frame_rendered = 1;

    ffp_notify_msg1(ffp, FFP_MSG_VIDEO_RENDERING_START);

    end = av_gettime();

}

至此,首屏耗時的拆解就完成了,剩下的最佳化就從具體每個階段著手最佳化。

三、 首屏時間的具體最佳化

在前面的分解之後,再來最佳化首屏時間,思路就比較清晰了。因為流程是序列的,所以只需要做到區域性最優,總體就會最優。

1,DNS的最佳化解析

  • 最佳化思路

DNS 的解析一直以來都是網路最佳化的首要問題,不僅僅有時間解析過長的問題,還有小運營商 DNS 劫持的問題,一般的解決方案都是採用 HttpDNS,但 HttpDNS 在部分地區也可能存在準確性問題,綜合各方面我們採用了HTTPDNS 和 LocalDNS 結合的方案,來提升解析的速度和準確率。前面已經提到了,一般來說如果只是解析IPV4來說,LocalDNS 的耗時並不算長。但我們也不能直接修改 ffmpeg,因為也要考慮到將來的 IPV6 的擴充套件問題。好在我們內部有專門做 DNS 的 SDK,他們的大概思路是,APP 啟動的時候就會先預解析我們指定的域名,因為拉流域名是固定的幾個,所以完全可以先快取起來。然後會根據各個域名解析的時候傳回的有效時間,過期後再去解析更新。至於 DNS劫持的問題,內部會有一個評估策略,如果 loacldns 出來的IP無法正常使用,或者延時太高,就會切換到 HttpDns 重新解析。這樣就保證了每次真正去拉流的時候,DNS 的耗時幾乎為0,因為可以定時更新快取池,使每次獲得的 DNS 都是來自快取池的。

  • 具體實現方式

如何替換掉 ffmpeg 中`tcp.c`檔案中的 `ret = getaddrinfo(hostname, portstr, &hints;, &ai;);` 方法,我們最開始想到了兩種方案:

方案A

比如我們的拉流url是這樣的 `http://a.meipai.com/m/c04.flv`,如果在傳遞url 給 ffmpeg 前將`a.meipai.com` 替換成DNS 預先解析出來的 ip 比如 `112.34.23.45` ,那替換後的url就是`http://112.34.23.45/m/c04.flv`。如果直接用這個url去發起http請求,在有些情況可以,很多情況是不行的。如果這個iP的機器只部署了 `a.meipai.com` 對應的服務,就能解析出來。如果有多個域名的服務,CDN 節點就無法正確的解析。所以這個時候一般是設定 http 請求的 essay-header裡面的 Host 欄位。一般可以透過以下程式碼傳遞給 ffmpeg 內部,這個引數的作用就是填充 http 的Host 頭部,具體的實現,可以 ffmpeg 原始碼,檔案`http.c`中`http_connect` 方法中。

    AVDictionary **dict = ffplayer_get_opt_dict(ffplayer, opt_category);

    av_dict_set(dict, “essay-headers”, “Host: hdl-test-meipai.com”, 0);

但這種方案有個 bug 就是,如果在發出請求 `http://a.meipai.com/m/c04.flv` 的時候,服務端透過302排程方式傳回了類似的結果 `http://112.34.23.45/a.meipei.com/m/c04.flv` ,指定了ip的url,這時客戶端並不知道跳轉的邏輯,因為http請求都是在 ffmpeg 內部進行的。這個時候再設定了Host,就會出現` http://112.34.23.45/a.meipai.com/a.meipai.com/m/c04.flv` 中間有兩個 host 的情況,導致服務端無法解析的 bug。這種情況也是在中途測試的時候偶爾發生的,目前沒有比較好的解決方案,除非讓服務端採用不下發302跳轉,但這樣就不通用了,會給將來留下隱患,所以這種簡單的方案不可行。

方案B

還有一種方案就是經常會用到的設定函式指標的方式,在 ffmpeg 中的 `tcp.c`中用函式指標替換掉 `getaddreinfo` 方法,因為這個方法就是實際解析 DNS的方法,比如下麵程式碼:

if(my_getaddreinfo) {

    ret = my_getaddreinfo(hostname, portstr, &hints;, &ai;);

} else {

    ret = getaddrinfo(hostname, portstr, &hints;, &ai;);

}

在` my_getaddreinfo` 方法中,可以呼叫 DNS SDK的解析方法,獲取到ip,然後填充到`ai`裡面,就實現了我們的需求。這種方案的優勢很明顯,就是靈活,容易擴充套件,而且沒有什麼風險。不過有個劣勢是需要修改ffmpeg原始碼,這對於一個大的APP裡面,有多個功能共用一個 `ffmpeg` 庫的情況來講,需要增加很多測試成本。

總體來說,DNS最佳化後,根據線上的資料首屏時間能減少 100ms~300ms 左右,特別是針對很多首次開啟,或者DNS本地快取過期的情況下,能有很好的最佳化效果。

2,TCP連線耗時的最佳化解析

TCP 連線耗時,這個耗時可最佳化的空間主要是針對建連節點鏈路的最佳化,主要受限於三個因素影響:使用者自身網路條件、使用者到 CDN 邊緣節點中間鏈路的影響、CDN 邊緣節點的穩定性。因為使用者網路條件有比較大的不可控性,所以最佳化主要會在後面兩個點。我們這邊會結合著使用者所對應的城市、運營商的情況,同時結合著服務端的 CDN 多融合排程體系,可以給使用者下發更合適的 CDN 服務域名,然後透過 HTTPDNS SDK 來最佳化 DNS 解析的結果。同時對於一些使用者被解析到比較偏遠的節點,或者質量不穩定的節點,那麼我們會透過監控機制來發現,並推動做些最佳化。。

3,http響應耗時的最佳化解析

目前 HTTP 響應耗時分兩種情況:1. 如果 CDN 節點沒有快取流,CDN收到HTTP請求後,就需要回源站去拉流,請求響應,並等待源站的響應結果。這個耗時就比較久了,一般是400ms左右,這塊和CDN內部的架構有關,有時更久,達到幾秒的情況都有,所以這種情況,一般需要推動CDN廠商做一些最佳化;2. 如果 CDN 節點有快取流,CDN 收到 HTTP 請求後,會理解傳迴響應頭部,一般是在100ms 以內,響應很快。這塊比較受限於 CDN 邊緣節點分發策略,不同的 CDN 廠商的表現會有些差異,在端層面可做的東西較少,所以主要是推動多 CDN 的融合策略來提升更好的體驗。

4,音影片流探測耗時的最佳化解析

音影片流的探測耗時,在 ffmpeg 中可以對應函式 `avformat_find_stream_info`函式。在 ijkplayer 的實現中,這個方法的耗時一般會比較久。在 ffmpeg 中的`utils.c` 檔案中的函式實現中有一行程式碼是 `int fps_analyze_framecount = 20;`,這行程式碼的大概用處是,如果外部沒有額外設定這個值,那麼 `avformat_find_stream_info ` 需要獲取至少20幀影片資料,這對於首屏來說耗時就比較長了,一般都要1s左右。而且直播還有實時性的需求,所以沒必要至少取20幀。這裡就有最佳化空間,可以去掉這個條件。設定方式:

av_dict_set_int(&ffp-;>format_opts, “fpsprobesize”, 0, 0);

這樣,`avformat_find_stream_info ` 的耗時就可以縮減到 100ms 以內。

5,buffer緩衝耗時的最佳化解析

這部分是純粹看播放器內部邏輯的實現,因為我們是基於ijkplayer來修改的,就以 ijkplayer 來講。先點出需要最佳化的兩個地方:1. BUFFERING_CHECK_PER_MILLISECONDS 值需要降低,2.MIN_MIN_FRAMES 值需要降低,3. CDN配置快啟最佳化。下麵具體分析:

  • BUFFERING_CHECK_PER_MILLISECONDS

這部分邏輯主要是在ijkplayer工程中`ff_ffplay.c`檔案中的`read_thread`方法中。用到的地方只有一處:

#define BUFFERING_CHECK_PER_MILLISECONDS        (300)

if (ffp->packet_buffering) {

    io_tick_counter = SDL_GetTickHR();

    if (abs((int)(io_tick_counter – prev_io_tick_counter)) > BUFFERING_CHECK_PER_MILLISECONDS){

        prev_io_tick_counter = io_tick_counter;

        ffp_check_buffering_l(ffp);

    }

}

從這個程式碼邏輯中可以看出,每次呼叫 `ffp_check_buffering_l` 去檢查 buffer是否滿足條件的時間間隔是 500ms 左右,如果剛好這次只差一幀資料就滿足條件了,那麼還需要再等 500ms 才能再次檢查了。這個時間,對於直播來說太長了。我們當前的做法是降低到 50ms,理論上來說可以降低 150ms 左右,根據我們線上灰度的資料來看,平均可以減少 200ms 左右,符合預期值。

  • MIN_MIN_FRAMES

這部分程式碼實現是在`ffp_check_buffering_l(ffp)`函式中。

#define MIN_MIN_FRAMES      10

if (is->buffer_indicator_queue && is->buffer_indicator_queue->nb_packets > 0) {

            if (   (is->audioq.nb_packets > MIN_MIN_FRAMES || is->audio_stream < 0 || is->audioq.abort_request)

                && (is->videoq.nb_packets > MIN_MIN_FRAMES || is->video_stream < 0 || is->videoq.abort_request)) {

                printf(“ffp_check_buffering_l buffering end \n”);

                ffp_toggle_buffering(ffp, 0);

            }

        }

這裡大概的意思需要緩衝的資料至少要有 11 幀影片,和 11 個音訊資料包,才能離開緩衝區,開始播放。我們知道音訊資料很容易滿足條件,因為如果取樣率是 44.1k 的採集音訊話,那麼1s,平均有44個音訊包。11 個音訊包,相當於0.25s 資料。但對於影片,如果是24幀的幀率,至少需要0.4s左右的資料,對於大部分 android 直播來說,因為美顏、AR 方面的處理消耗,所以他們的採集編碼幀率只有10~15s,那麼就需要接近1s的資料,這個耗時太長。緩衝區裡需要怎麼多資料,但實際上播放器已經下載了多少資料呢?我們深入 ff_ffplay.c 原始碼可以看到影片解碼後會放到一個 frame_queue 裡面,用於渲染資料。可以看到影片資料的流程是這樣的:下載到緩衝區->解碼->渲染。其中渲染的緩衝區就是 frame_queue。下載的資料會先經過解碼執行緒將資料輸出到 frame_queue 中,然後等 frame_queue 佇列滿了,才留在緩衝佇列中。在 ff_ffplay.c 中,可以找到如下程式碼:

#define VIDEO_PICTURE_QUEUE_SIZE_MIN        (3)

#define VIDEO_PICTURE_QUEUE_SIZE_MAX        (16)

#define VIDEO_PICTURE_QUEUE_SIZE_DEFAULT    (VIDEO_PICTURE_QUEUE_SIZE_MIN)

ffp->pictq_size                     = VIDEO_PICTURE_QUEUE_SIZE_DEFAULT; // option

/* start video display */

if (frame_queue_init(&is-;>pictq, &is-;>videoq, ffp->pictq_size, 1) < 0)

    goto fail;

所以目前來看,如果設定10,播放器開始播放時至少有14幀影片。對於低幀率的影片來說,也相當大了。在實踐中我們把它調整到5,首屏時間減少了300ms左右,並且卡頓率只上升了2個百分點左右。

  • CDN邊沿最佳化

CDN 邊沿的最佳化主要包括 GOP 快取技術及快啟最佳化技術。這項兩項技術基本原理是透過快速下發足夠的影片幀以填充滿播放器的緩衝區從而讓播放器在最短的時間內達到播放條件以最佳化首屏時間。影片快取會以完整 GOP 為單位,這個主要是為了防止影片出現破圖,快啟最佳化則是會在 GOP 快取基本上根據播放器緩衝區大小設定一定的 GOP 數量用於填充播放器緩衝區。

這個最佳化項並不是客戶端播放器來控制的,而是 CDN 下發影片資料的頻寬和速度。因為緩衝區耗時不僅跟緩衝需要的幀數有關,還跟下載資料的速度最佳化,以網宿 CDN 為例,他們可以配置快啟後,在拉流時,前面快取1s 的資料,服務端將以 5 倍於平時頻寬的速度下發。這樣的效果除了首屏速度跟快以外,首屏也會更穩定,因為有固定 1s 的快取快速下發。這個最佳化的效果是平均可以更快 100ms 左右。

四、小結

至此,美拍直播的首屏效果,已經基本跟業界主流直播效果相當,後面我們將在穩定性、卡頓率和卡頓時間上面做進一步最佳化。

需要註意的是:基礎資料的統計是一切最佳化的基礎。比如首屏時間最佳化的一個最基本的大前提就是需要有直播播放情況的各個階段的統計資料,這在我們工作開展的前期是不完善的,比如,DNS 的耗時和 http 響應的耗時。這個因為種種原因導致一直都沒有上報上來,所以最初是無法精準定位,只有一個大概的時間。還有一些更致命的問題是統計資料的不準確,因為歷史原因導致資料的準確性不夠,所以往往會因為錯誤的資料導致錯誤的分析。因此,我們需要重視基礎資料統計的準確性和完善程度。

美圖招人


2017年發展速度最快的行業是什麼?區塊鏈和人工智慧!

年終獎已經落袋了,是不是要考慮下給自己更好的一個機會呢?嘗試在未來的人工智慧或區塊鏈方面取得一定成就呢?

作為人工智慧見長的美圖公司,開始踏足區塊鏈,同時在人工智慧領域依然期望將更多的演演算法落實到應用中去。在此需要更多的技術同學一起取得更多的成就。重要的崗位需求如下:

  • GO 系統研發工程師:主要做區塊鏈、儲存、即時通訊、流媒體等方向。

  • 影片演演算法工程師:影片分類演演算法;影片編解碼演演算法等領域的研究以及落地

  • JAVA 系統研發工程師:主要做的方向核心業務方向研發,區塊鏈相關係統研發等

  • C/C++ 系統研發工程師:主要方向為儲存、快取、nginx、區塊鏈等方向

相關崗位不限高階、中級、初級,工作地點不限北京、廈門、深圳。勾搭郵箱:zl3@meitu.com

相關閱讀

高可用架構

改變網際網路的構建方式

贊(0)

分享創造快樂