性能優(yōu)化:SRS為何能做到同類的三倍

性能無疑是服務(wù)器的核心能力,幾乎每個(gè)開源服務(wù)器的介紹都是”高性能XXX服務(wù)器“。視頻服務(wù)器由于業(yè)務(wù)的超復(fù)雜度,特別是WebRTC服務(wù)器,要做到高性能是非常有挑戰(zhàn)的難點(diǎn)。

為何性能很重要?完備的功能需要用性能交換,安全性需要用性能交換,成本需要用性能交換,產(chǎn)品體驗(yàn)需要用性能交換,甚至系統(tǒng)彈性都需要性能交換。有了基礎(chǔ)性能,就有了競(jìng)爭(zhēng)力的資本;基礎(chǔ)性能若有問題,舉步維艱,想要干點(diǎn)啥都不容易,就像天生羸弱的身子板。

SRS雖然是單進(jìn)程單線程模型,性能一直都很高,比如:

  • 單進(jìn)程能跑滿千兆或萬兆網(wǎng)卡,一般的場(chǎng)景完全能覆蓋。
  • 性能是NginxRTMP或Janus的三倍左右,目前還沒有更高性能的開源同類產(chǎn)品。
  • 提供集群能力,水平擴(kuò)展性能,在開源項(xiàng)目中也不多見。

這并不是終點(diǎn),這個(gè)性能基準(zhǔn)只夠5年左右,隨著業(yè)務(wù)發(fā)展一定會(huì)有更高性能的需求。目前服務(wù)器屬于第二代高并發(fā)架構(gòu),也就是單線程架構(gòu):

  • 第一代高并發(fā)架構(gòu),1990~2010年,多線程架構(gòu),一般比較老的服務(wù)器都是這種架構(gòu),一般無法解決C10K問題,比如Adobe AMS,Apache HTTP Server,Janus WebRTC Server。核心問題是多線程的水平擴(kuò)展性問題,并發(fā)越多,線程之間的同步和競(jìng)爭(zhēng)開銷就越大(這個(gè)問題也是現(xiàn)代語言Go在性能方面的硬傷,特別是在超多CPU比如64核或128核時(shí),多線程的損耗會(huì)更大)。
  • 第二代高并發(fā)架構(gòu),2010~2020年,單線程架構(gòu),多進(jìn)程或單進(jìn)程單線程都是這種架構(gòu),C10K問題得到比較好的解決,比如Nginx,SRS,MediaSoup。核心問題是單線程引入的異步回調(diào)問題,新的語言比如Go引入輕量線程goroutine(協(xié)程)解決這個(gè)問題,老的語言比如C++20、JS await等都有對(duì)應(yīng)的機(jī)制。另外多進(jìn)程的進(jìn)程間通信也會(huì)引入額外復(fù)雜性,比如直播和RTC的流的跨進(jìn)程回源和拉流問題。
  • 第三代高并發(fā)架構(gòu),隔離的多線程架構(gòu),比如云原生的數(shù)據(jù)面反向代理Envoy線程模型,還有比Nginx更高性能的反向代理么,這就是Envoy了,如下圖所示。其實(shí)Envoy和Nginx都是事件驅(qū)動(dòng),但是Envoy是完全非阻塞。而Envoy的多線程實(shí)際上和第一代的多線程也不同,線程之間幾乎沒有交互,可以看作是隔離的進(jìn)程。由于是線程,所以它們之間的(少量)通信也很容易。同樣,多線程也可以和輕量線程結(jié)合使用。
Chart of Requests per Second over HTTP by Load Balancer and Concurrency Level

SRS目前還屬于第二代架構(gòu),第三代架構(gòu)驗(yàn)證過是可行的(#2188),由于目前SRS性能還不是短板,所以沒有合并到主干分支,有需要可以自己合并feature/threads。目前SRS的性能數(shù)據(jù)如下:

SFU Clients CPU Memory 線程 VM
SRS 4000 players ~94% x1 419MB 1 G5 8CPU
NginxRTMP 2400 players ~92% x1 173MB 1 G5 8CPU
SRS 2300 publishers ~89% x1 1.1GB 1 G5 8CPU
NginxRTMP 1300 publishers ~84% x1 198MB 1 G5 8CPU
SFU Clients CPU Memory 線程 VM
SRS 1000 players ~90% x1 180MB 1 G5 2CPU
Janus 700 players ~93% x2 430MB 24 G5 2CPU
SRS 950 publishers ~92% x1 132MB 1 G5 2CPU
Janus 350 publishers ~93% x2 405MB 23 G5 2CPU

Note: CentOS7, 600Kbps, ECS/G5-2.5GHZ(SkyLake), SRS/v4.0.105, NginxRTMP/v1.2.1。雖然系統(tǒng)有8CPU但只能使用單個(gè)CPU,選擇8CPU是因?yàn)橹挥?CPU的內(nèi)網(wǎng)帶寬才能到10Gbps。

當(dāng)我們提到性能,一般隱含條件是“滿足業(yè)務(wù)場(chǎng)景的體驗(yàn)”下的性能優(yōu)化,比如直播要求卡頓率低、延遲在3秒之內(nèi),比如WebRTC要求端到端延遲400ms之內(nèi)(服務(wù)器0延遲),一般現(xiàn)在服務(wù)器的內(nèi)存可以是4GB、8GB、16GB、32GB或64GB,這意味著我們可以盡量用內(nèi)存Cache來降低CPU運(yùn)算。

SRS Protocol VP6 H.264 VP6+MP3 H.264+MP3
2.0.72 RTMP 0.1s 0.4s 0.8s 0.6s
2.0.70 RTMP 0.1s 0.4s 1.0s 0.9s
1.0.10 RTMP 0.4s 0.4s 0.9s 1.2s
4.0.87 WebRTC x 80ms x x

Note: 在音視頻服務(wù)器的性能優(yōu)化中,延遲是必須要考慮的一個(gè)因素,特別是在RTC服務(wù)器,性能優(yōu)化不能以增大延遲為方法。

性能基準(zhǔn)

如果沒有壓測(cè)能力,就無法優(yōu)化性能。

SRS的基準(zhǔn)是并發(fā)流,比如使用srs-bench推流可以獲得支持的最高推流(發(fā)布)并發(fā),和最高拉流(播放)并發(fā)。壓測(cè)工具一般讀取文件,可以選擇典型的業(yè)務(wù)場(chǎng)景,錄制成樣本文件,這樣壓測(cè)可以盡量模擬線上場(chǎng)景。

性能優(yōu)化前,必須使用壓測(cè)獲得目前的性能基準(zhǔn),分析目前的性能瓶頸和優(yōu)化思路,然后修改代碼獲得新的性能基準(zhǔn),如此反復(fù)不斷提升性能。如下圖所示:

上圖就是性能分析的主面板,左三右二加一個(gè)瀏覽器:

  • 左上:服務(wù)器的top圖,命令是 top -H -p $(cat objs/srs.pid) ,看CPU和內(nèi)存。還有每個(gè)CPU的消耗情況(進(jìn)入top后按數(shù)字1),比如us是用戶空間函數(shù),sy是內(nèi)核函數(shù),si是網(wǎng)卡軟中斷。
  • 左中:系統(tǒng)的網(wǎng)絡(luò)帶寬圖,命令是 dstat ,看出入口帶寬。比如視頻平均碼率是600kbps,那么900個(gè)推流時(shí),網(wǎng)卡的recv流量應(yīng)該是600*900/8000.0KBps也就是67.5MBps,如果網(wǎng)卡吞吐率達(dá)不到預(yù)期,那么肯定會(huì)出現(xiàn)卡頓等問題,比如可能是系統(tǒng)的網(wǎng)卡隊(duì)列緩沖區(qū)太小導(dǎo)致丟包。
  • 左下:服務(wù)器關(guān)鍵日志,命令是 tail -f objs/srs.log |grep -e 'RTC: Server' -e Hybrid ,查看RTC的連接數(shù)和關(guān)鍵日志,以及進(jìn)程的CPU等信息。如果連接數(shù)達(dá)不到預(yù)期,或者CPU接近100%,也是有問題的。
  • 右上:服務(wù)器熱點(diǎn)函數(shù)列表,命令是 perf top -p $(cat objs/srs.pid) ,可以看到當(dāng)前主要的熱點(diǎn)函數(shù)列表,以及每個(gè)函數(shù)所占用的百分比。性能優(yōu)化一般的思路,就是根據(jù)這個(gè)表,優(yōu)化掉排名在前面的熱點(diǎn)函數(shù)。
  • 右下:壓測(cè)客戶端的top圖,如果壓測(cè)服務(wù)器的CPU滿載,也一樣達(dá)不到預(yù)期,會(huì)出現(xiàn)卡頓等情況。同樣也需要先檢查系統(tǒng)的網(wǎng)卡隊(duì)列緩沖區(qū),避免系統(tǒng)丟包。
  • 瀏覽器:在瀏覽器中播放流,比如webrtc://8.126.115.13:1985/live/livestream100?eip=8.126.115.13,可以通過eip指定外網(wǎng)ip,這樣壓測(cè)工具可以推內(nèi)網(wǎng)地址,而瀏覽器觀看可以看外網(wǎng)的地址。瀏覽器觀看可以隨機(jī)抽查某個(gè)流,判斷是否播放流暢,聲音和畫面是否正常等。

Note: 我們?cè)谧笊系膱D中,截圖時(shí)加上了標(biāo)注,可以更快的看出這個(gè)性能圖的摘要,比如ECS/C5 2CPU說明是ECS的C5機(jī)型一共是2個(gè)CPU,900 publish streams是RTC推流一共900個(gè)并發(fā)流。

工具鏈

沒有工具鏈就無法做性能優(yōu)化,前一章我們分享了壓測(cè)工具srs-bench,查看網(wǎng)絡(luò)帶寬工具dstat,查看熱點(diǎn)函數(shù)工具perf,查看CPU工具top。

還有一些工具鏈,總結(jié)在SRS性能(CPU)、內(nèi)存優(yōu)化工具用法,我們挑一些和性能優(yōu)化相關(guān)的工具重點(diǎn)介紹。包括:

  • sysctl:修改內(nèi)核UDP緩沖區(qū),防止內(nèi)核丟包。
  • GPERF: GCP:使用GCP分析熱點(diǎn)函數(shù)的調(diào)用鏈,圖形化展示。
  • taskset:進(jìn)程綁核后,避免軟中斷干擾,便于查看數(shù)據(jù)。

對(duì)于RTC,很重要的是需要把內(nèi)核協(xié)議棧的緩沖區(qū)改大,默認(rèn)只有200KB,必須改成16MB以上,否則會(huì)導(dǎo)致丟包:

sysctl net.core.rmem_max=16777216
sysctl net.core.rmem_default=16777216
sysctl net.core.wmem_max=16777216
sysctl net.core.wmem_default=16777216

可以直接修改文件/etc/sysctl.conf,重啟也能生效:

# vi /etc/sysctl.conf
net.core.rmem_max=16777216
net.core.rmem_default=16777216
net.core.wmem_max=16777216
net.core.wmem_default=16777216

如果perf熱點(diǎn)函數(shù)比較通用,比如是malloc,那我們可能需要分析調(diào)用鏈路,看是哪個(gè)執(zhí)行分支導(dǎo)致malloc熱點(diǎn),由于SRS使用的協(xié)程,perf無法正確獲取堆棧,我們可以用GPERF: GCP工具:

# Build SRS with GCP
./configure --with-gperf --with-gcp && make

# Start SRS with GCP
./objs/srs -c conf/console.conf

# Or CTRL+C to stop GCP
killall -2 srs

# To analysis cpu profile
./objs/pprof --text objs/srs gperf.srs.gcp*

圖形化展示時(shí),需要安裝依賴graphviz

yum install -y graphviz

然后就可以生成SVG圖,用瀏覽器打開就可以看了:

./objs/pprof --svg ./objs/srs gperf.srs.gcp >t.svg

還可以使用taskset綁定進(jìn)程到某個(gè)核,這樣避免在不同的核跳動(dòng),和軟中斷跑在一個(gè)核后干擾性能,比如一般軟中斷會(huì)在CPU0,我們綁定SRS到CPU1:

taskset -pc 1 $(cat objs/srs.pid)

Note:如果是多線程模式,可以增加參數(shù)-a綁定所有線程到某個(gè)核,或者在配置文件中,配置cpu_affinity指定線程的核。

內(nèi)存交換性能

現(xiàn)代服務(wù)器的內(nèi)存都很大,平均每個(gè)核有2GB內(nèi)存,比如:

還有其他型號(hào)的,比如G5每個(gè)核是4GB內(nèi)存,比如R5更是每個(gè)核高達(dá)8GB內(nèi)存。這么多內(nèi)存,對(duì)于無磁盤緩存型的網(wǎng)絡(luò)服務(wù)器,直播轉(zhuǎn)發(fā)或者SFU轉(zhuǎn)發(fā),一般內(nèi)存是用不了這么多的,收包然后轉(zhuǎn)發(fā),幾乎不需要緩存很久的數(shù)據(jù)。

因此,線上的視頻服務(wù)器一般內(nèi)存都是很充足的,有些情況下可以用內(nèi)存來優(yōu)化性能的地方,就可以果斷的上內(nèi)存緩存(Cache)策略。

比如,在直播播放時(shí),SRS有個(gè)配置項(xiàng)叫合并寫入(發(fā)送):

vhost __defaultVhost__ {
    play {
        # Set the MW(merged-write) min messages.
        # default: 0 (For Real-Time, min_latency on)
        # default: 1 (For WebRTC, min_latency off)
        # default: 8 (For RTMP/HTTP-FLV, min_latency off).
        mw_msgs         8;
    }
}

如果是非低延遲(默認(rèn))模式是8,也就是收到了8個(gè)音視頻包后,才會(huì)轉(zhuǎn)發(fā)給播放器。關(guān)鍵代碼如下:

srs_error_t SrsRtmpConn::do_playing(SrsLiveSource* source, SrsLiveConsumer* consumer, SrsQueueRecvThread* rtrd)
{
    mw_msgs = _srs_config->get_mw_msgs(req->vhost, realtime);
    mw_sleep = _srs_config->get_mw_sleep(req->vhost);

    while (true) {
        consumer->wait(mw_msgs, mw_sleep);

        if ((err = consumer->dump_packets(&msgs, count)) != srs_success) {
            return srs_error_wrap(err, "rtmp: consumer dump packets");
        }

        if (count > 0 && (err = rtmp->send_and_free_messages(msgs.msgs, count, info->res->stream_id)) != srs_success) {
            return srs_error_wrap(err, "rtmp: send %d messages", count);
        }

如果是25fps,那么8個(gè)包大約是在320ms,考慮音頻包大約是160ms延遲,這個(gè)隊(duì)列的額外延遲在直播中也是可以接受的。

如果是8個(gè)包一次發(fā)送,按照平均碼率1Mbps,差不多是300Mb也就是40KB的數(shù)據(jù)。如果按照峰值5Mbps碼率計(jì)算,那就是一次發(fā)送200KB的數(shù)據(jù)。我們可以用writev一次發(fā)送這些數(shù)據(jù),就可以極大的提高分發(fā)的性能了。

每個(gè)連接我們需要的內(nèi)存按照1MB來計(jì)算,那么4000個(gè)連接需要4GB內(nèi)存。如果是7000個(gè)連接,需要7GB的內(nèi)存??梢哉J(rèn)為直播分發(fā)的性能優(yōu)化,是典型的內(nèi)存(加少量延遲)來?yè)Q更低的CPU使用。

Note: 當(dāng)然服務(wù)器引入額外的160ms延遲對(duì)于RTC場(chǎng)景就是不可以接受的,只能在直播中使用這種優(yōu)化。

Note: RTC的UDP發(fā)送是否能使用類似的優(yōu)化?我們調(diào)研過UDP/sendmmsgUDP/GSO是可以提升一部分,但是由于UDP每個(gè)連接可合并發(fā)送的數(shù)據(jù)很少,目前壓測(cè)分析熱點(diǎn)也不在這里,所以優(yōu)化有限,目前SRS并沒有做這兩個(gè)優(yōu)化。

查找優(yōu)化

STL的vector和map的查找算法,已經(jīng)優(yōu)化得很好了,實(shí)際上還是會(huì)成為性能瓶頸。

比如,RTC由于實(shí)現(xiàn)了端口復(fù)用,需要根據(jù)每個(gè)UDP包的五元組(或其他信息),查找到對(duì)應(yīng)的Session處理包;Session需要根據(jù)SSRC找到對(duì)應(yīng)的track,讓track處理這個(gè)包。

比如,SRS的日志是可追溯的,打印時(shí)會(huì)打印出上下文ID,可以將多個(gè)會(huì)話的日志分離。這個(gè)Context ID是存儲(chǔ)在全局的map中的,每次切換上下文需要根據(jù)協(xié)程ID查找出對(duì)應(yīng)的上下文ID。

如果每個(gè)包都需要這么運(yùn)算一次,那開銷也是相當(dāng)可觀的??紤]根據(jù)UDP包查找Session,如下圖:

int SrsUdpMuxSocket::recvfrom(srs_utime_t timeout)
{
    nread = srs_recvfrom(lfd, buf, nb_buf, (sockaddr*)&from, &fromlen, timeout);
    getnameinfo((sockaddr*)&from, fromlen,  (char*)&address_string, 64, (char*)&port_string
    peer_ip = std::string(address_string);
    peer_port = atoi(port_string); 

srs_error_t SrsUdpMuxListener::cycle()
{
    while (true) {
        SrsUdpMuxSocket skt(lfd);
        int nread = skt.recvfrom(SRS_UTIME_NO_TIMEOUT);
        err = handler->on_udp_packet(&skt);

srs_error_t SrsRtcServer::on_udp_packet(SrsUdpMuxSocket* skt)
{
    string peer_id = skt->peer_id();
    ISrsResource* conn = _srs_rtc_manager->find_by_id(peer_id);
    session = dynamic_cast<SrsRtcConnection*>(conn);

ISrsResource* SrsResourceManager::find_by_id(std::string id)
{
    map<string, ISrsResource*>::iterator it = conns_id_.find(id);
    return (it != conns_id_.end())? it->second : NULL;
}

這個(gè)邏輯有幾個(gè)地方會(huì)有熱點(diǎn),通過壓測(cè)可以在perf上看到:

  • 每個(gè)UDP包都調(diào)用getnameinfo將sockaddr轉(zhuǎn)成字符串的ip:port,也就是地址標(biāo)識(shí),會(huì)有大量的string開辟和釋放。
  • 每個(gè)UDP包都需要根據(jù)ip:port,在map中查找出對(duì)應(yīng)的Session(Resource或Conneciton),字符串查找的速度是很慢的。

改進(jìn)其實(shí)也容易,查找時(shí)不轉(zhuǎn)成string,而是生成uint64_t的地址,目前支持的是IPv4地址只需要6字節(jié)就可以表達(dá)ip:port(如果是IPv6則需要兩個(gè)uint64_t),如下所示:

int SrsUdpMuxSocket::recvfrom(srs_utime_t timeout)
{
    nread = srs_recvfrom(lfd, buf, nb_buf, (sockaddr*)&from, &fromlen, timeout);
    sockaddr_in* addr = (sockaddr_in*)&from;
    fast_id_ = uint64_t(addr->sin_port)<<48 | uint64_t(addr->sin_addr.s_addr);

srs_error_t SrsRtcServer::on_udp_packet(SrsUdpMuxSocket* skt)
{
    uint64_t fast_id = skt->fast_id();
    session = (SrsRtcConnection*)_srs_rtc_manager->find_by_fast_id(fast_id);

雖然解決了string查找的熱點(diǎn),隨著并發(fā)的提升,map<key: uint64_t>的查找也變成了熱點(diǎn),在perf上可以看到map的不斷平衡,我們還可以改成vector查找:

ISrsResource* SrsResourceManager::find_by_fast_id(uint64_t id)
{
    SrsResourceFastIdItem* item = &conns_level0_cache_[(id | id>>32) % nn_level0_cache_];
    if (item->available && item->fast_id == id) {
        return item->impl;
    }

    map<uint64_t, ISrsResource*>::iterator it = conns_fast_id_.find(id);
    return (it != conns_fast_id_.end())? it->second : NULL;
}

Note: 首先我們?cè)趘ector中取余查找,如果取余碰撞了(兩個(gè)不同id但是取余后一樣,比如1001%1000和2001%1000是一樣的),那么就用map查找。

Note:快速查找,可以考慮C++11的unordered_set,實(shí)現(xiàn)原理是類似的。

通過不同的查找方式,string變uint64_t優(yōu)化了查找速度,而更快的優(yōu)化是不用map查找,直接使用數(shù)組取余就是無查找了。

當(dāng)然,這樣的優(yōu)化,讓邏輯變得復(fù)雜了。

無代碼優(yōu)化

當(dāng)我們優(yōu)化完明顯的熱點(diǎn),優(yōu)化完頭部熱點(diǎn),會(huì)發(fā)現(xiàn)perf顯示已經(jīng)沒有明顯的熱點(diǎn),有時(shí)候有些不太明顯的函數(shù)也會(huì)排在前頭,比如拷貝RTP Packet:

SrsRtpPacket* SrsRtpPacket::copy()
{
    SrsRtpPacket* cp = new SrsRtpPacket();

    cp->header = header;
    cp->payload_ = payload_? payload_->copy():NULL;
    cp->payload_type_ = payload_type_;

    cp->nalu_type = nalu_type;
    cp->shared_buffer_ = shared_buffer_? shared_buffer_->copy2() : NULL;
    cp->actual_buffer_size_ = actual_buffer_size_;
    cp->frame_type = frame_type;

    cp->cached_payload_size = cached_payload_size;
    // For performance issue, do not copy the unused field.
    cp->decode_handler = decode_handler;

    return cp;
}

這個(gè)函數(shù)有啥可以優(yōu)化的么?沒有什么可以優(yōu)化的,全都是賦值和拷貝(無深拷貝),但是在perf上它就是排名在前頭。

Note: 這時(shí)候千萬別懷疑perf有問題,確實(shí)熱點(diǎn)是這個(gè)拷貝是沒有錯(cuò)的,perf不會(huì)出錯(cuò),perf不會(huì)出錯(cuò),perf不會(huì)出錯(cuò),千萬不要把焦點(diǎn)挪開去優(yōu)化其他函數(shù)。

并不是函數(shù)性能高,就不會(huì)成為瓶頸,有個(gè)公式如下:

性能熱點(diǎn) = 函數(shù)執(zhí)行效率 x 函數(shù)執(zhí)行次數(shù)

一般我們會(huì)優(yōu)先優(yōu)化函數(shù)執(zhí)行效率,讓函數(shù)更高效。但是我們也不能忽略了函數(shù)的執(zhí)行次數(shù),如果一個(gè)高效的函數(shù)被反復(fù)的執(zhí)行,一樣也會(huì)變成性能熱點(diǎn)。這時(shí)候我們的優(yōu)化思路就是:如何讓代碼不執(zhí)行,或明顯減少執(zhí)行次數(shù)。

通過分析可以發(fā)現(xiàn),這個(gè)SrsRtpPacket::copy調(diào)用點(diǎn)有:

  • 從Publisher拷貝到每個(gè)Consumer,函數(shù)是SrsRtcSource::on_rtp。
  • 包發(fā)送后,放到Track的NACK隊(duì)列,函數(shù)是SrsRtcRecvTrack::on_nackSrsRtcSendTrack::on_nack

上面第二個(gè)拷貝可以省略,由于每個(gè)Player都有NACK,所以可以減少一倍的調(diào)用,優(yōu)化后這個(gè)熱點(diǎn)也就不排在前頭了:

srs_error_t SrsRtcRecvTrack::on_nack(SrsRtpPacket** ppkt)
{
    rtp_queue_->set(seq, pkt);
    *ppkt = NULL;

同樣的,這個(gè)優(yōu)化的代價(jià)就是增加了風(fēng)險(xiǎn),參數(shù)也從指針,變成了指針的指針,一路都改成了指針的指針,可以犯錯(cuò)的概率就大太多了。

UDP協(xié)議棧

在直播優(yōu)化中,我們使用writev一次寫入大量的數(shù)據(jù),大幅提高了播放的性能。

其實(shí)UDP也有類似的函數(shù),UDP的sendto對(duì)應(yīng)TCP的write,UDP的sendmmsg對(duì)應(yīng)TCP的writev,我們調(diào)研過UDP/sendmmsg是可以提升一部分性能,不過它的前提是:

  • 在Perf中必須看到UDP的相關(guān)函數(shù)成為熱點(diǎn),如果有其他的熱點(diǎn)比UDP更耗性能,那么上sendmmsg也不會(huì)有改善。
  • 一般并發(fā)要到2000以上,UDP協(xié)議棧才可能出現(xiàn)在perf的熱點(diǎn),較低并發(fā)時(shí)收發(fā)的包,還不足以讓UDP的函數(shù)成為熱點(diǎn)。
  • 由于不能增加延遲,需要改發(fā)送結(jié)構(gòu),集中發(fā)給多個(gè)地址的UDP包統(tǒng)一發(fā)送。這對(duì)可維護(hù)性上是比較大的影響。

還有一種優(yōu)化是GSO,延遲分包。我們調(diào)研過UDP/GSO,比sendmmsg提升還要大一些,它的前提是:

  • sendmmsg一樣,只有當(dāng)UDP相關(guān)函數(shù)成為perf的熱點(diǎn),優(yōu)化才有效。
  • GSO只能對(duì)一個(gè)客戶端的包延遲組包,所以他的作用取決于要發(fā)給某個(gè)客戶端的包數(shù)目,由于RTC的實(shí)時(shí)性要求,一般2到3個(gè)比較常見。

Note: 從上圖可見,開啟Padding后,UDP組包效能可以提升10%左右。GSO雖然不能減少實(shí)際網(wǎng)絡(luò)上UDP包的數(shù)目,但是可以讓內(nèi)核延遲到最后才組UDP包,可以把GSO發(fā)送的多個(gè)包認(rèn)為是一個(gè)包,相當(dāng)于減少了發(fā)送UDP包的次數(shù)。

還有一種優(yōu)化的可能,就是ZERO_COPY,其實(shí)TCP的零拷貝支持得比較早,但是UDP的支持得比較晚一些。收發(fā)數(shù)據(jù)時(shí),需要從用戶空間到內(nèi)核空間不斷拷貝,不過之前測(cè)試沒有明顯收益,參考ZERO-COPY。

多線程

文章開頭我們提到,第三代高并發(fā)架構(gòu),將是隔離的多線程架構(gòu),比如云原生的數(shù)據(jù)面反向代理Envoy線程模型

我們也調(diào)研過SRS可能的多線程架構(gòu),參考#2188。和Envoy不同,SRS涉及到了TCP和UDP,API和媒體服務(wù),級(jí)聯(lián)和QoS等問題,可能的架構(gòu)也比較多。

SRS 1/2/3/4一直都是單線程(第二代架構(gòu)),如下圖所示:

Note:這個(gè)架構(gòu)的風(fēng)險(xiǎn)一直都存在,寫磁盤可能是阻塞的,DNS解析可能是阻塞的,RTC無法使用多CPU的能力(直播可以用集群或REUSE_PORT擴(kuò)展多核能力)。

很顯然,寫磁盤應(yīng)該由單獨(dú)線程完成,可以避免阻塞,這就是SRS 5.0使用的架構(gòu):

Note: 其實(shí)DVR和HLS也是寫磁盤操作,未來也會(huì)由寫磁盤的線程實(shí)現(xiàn),目前還沒有實(shí)現(xiàn)。

Note: DNS解析也是阻塞的,和寫磁盤不同,DNS解析本質(zhì)上是UDP請(qǐng)求,是可以自己實(shí)現(xiàn)協(xié)議解析,不需要用多線程做。

針對(duì)RTC的多核擴(kuò)展能力,有一種很自然(也是改動(dòng)較小)的思路,就是將更多的能力拆分到線程中。比如SRTP加解密,占用了30%的CPU,如果能拆分到獨(dú)立線程肯定對(duì)并發(fā)能力有提升。比如UDP收發(fā)也可以放到獨(dú)立線程,也可以避免內(nèi)核UDP收發(fā)效率不高的問題。如下圖所示:

Note: 這個(gè)架構(gòu)是被標(biāo)記為廢棄,原因就是SRTP和UDP確實(shí)效率不高,但是Hybrid里面的QoS算法是瓶頸所在,而這部分不方便拆分多線程。

Note: 另外,就算QoS拆分成多線程,這個(gè)架構(gòu)最多用到大約3~4CPU,并不能用到32或64核CPU,也就是并發(fā)能力還是受限。

Note: 最后,這種多線程架構(gòu),線程之間交互較多,所以會(huì)有鎖的開銷,也不算第三代服務(wù)器架構(gòu)。

最終的多線程架構(gòu),是能水平擴(kuò)展的多線程架構(gòu),實(shí)現(xiàn)的原型參考feature/threads分支,如下圖所示:

Note: 我們實(shí)現(xiàn)的一個(gè)版本是多端口模型,通過端口分割不同的Hybrid線程,Hybrid線程之間獨(dú)立不需要交互。實(shí)際上多線程之間也是可以復(fù)用同樣端口的,只是切網(wǎng)時(shí)需要考慮新五元組的綁定。

Note: 這個(gè)架構(gòu)完全解決了水平擴(kuò)展的問題,也避免了線程之間需要交互數(shù)據(jù),壓測(cè)在32核C5機(jī)器上,能跑到10K左右并發(fā)(可以在更多核的機(jī)器上擴(kuò)展)。

當(dāng)然,最后這個(gè)架構(gòu)也并非沒有問題,目前看還需要解決以下問題,才能在線上使用:

  • API必須非常簡(jiǎn)單,如果是Janus那種復(fù)雜的API,就無法使用這種結(jié)構(gòu)。SRS目前的API是比較合適實(shí)現(xiàn)這種架構(gòu)。API實(shí)際上承擔(dān)了調(diào)度的能力。
  • 直播需要改進(jìn),適配這種多線程結(jié)構(gòu)。直播Edge相對(duì)比較容易改造,可以用REUSE_PORT,相當(dāng)于多個(gè)進(jìn)程。而直播Origin改造比較麻煩。
  • 統(tǒng)計(jì)和API需要改造,系統(tǒng)的CPU使用率,告警和水位統(tǒng)計(jì),限流策略,都會(huì)因?yàn)槎嗑€程有所不同。比如在線人數(shù),需要匯總每個(gè)線程的連接數(shù)。
  • 全局變量和局部靜態(tài)變量,必須仔細(xì)Check,保障是線程安全(thread-safe),或者是線程局部(thread-local),雖然我們?cè)谠椭幸呀?jīng)改造得差不多,但還是需要更多確認(rèn)。

由于多線程本質(zhì)上和集群的能力是有一部分重合的,只是多線程的效率更高。比如直播其實(shí)可以用多個(gè)Edge部署在一臺(tái)機(jī)器上(Edge后面掛一堆Edge),實(shí)現(xiàn)多核的擴(kuò)展。RTC如果實(shí)現(xiàn)了級(jí)聯(lián),也一樣是可以擴(kuò)展多核能力,比如單核支持800個(gè)并發(fā),每個(gè)SRS跑在一個(gè)Pod中,也可以用級(jí)聯(lián)擴(kuò)展能力(當(dāng)然會(huì)造成進(jìn)程之間的帶寬比多線程要高,多線程是不走帶寬)。

硬件加速

SRS沒有使用硬件加速,但這個(gè)是一個(gè)很不錯(cuò)的思路,包括:

  • CPU指令集優(yōu)化:加解密和編解碼的算法,有些可以用到CPU的特殊指令,增加批處理的能力,比如AVX512。
  • 專用加解密硬件卡,加解密是比較通用的算法,有專門硬件,可以調(diào)研看看。
  • UDP收發(fā)優(yōu)化,不經(jīng)過內(nèi)核協(xié)議棧,直接從用戶空間和網(wǎng)卡交互:DPDK。

總結(jié)

還有 0% 的精彩內(nèi)容
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
支付 ¥1.00 繼續(xù)閱讀

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容