zlmediakit的hls高性能之旅

事情的起因

北京冬奧會(huì)前夕,zlmediakit的一位用戶完成了iptv系統(tǒng)的遷移; 由于zlmediakit對(duì)hls的支持比較完善,支持包括鑒權(quán)、統(tǒng)計(jì)、溯源等獨(dú)家特性,所以他把之前的老系統(tǒng)都遷移到zlmediakit上了。

但是很不幸,在冬奧會(huì)開(kāi)幕式當(dāng)天,zlmediakit并沒(méi)有承受起考驗(yàn),當(dāng)hls并發(fā)數(shù)達(dá)到3000左右時(shí),zlmediakit線程負(fù)載接近100%,延時(shí)非常高,整個(gè)服務(wù)器基本不可用:


圖片.png

思考

zlmediakit定位是一個(gè)通用的流媒體服務(wù)器,主要精力聚焦在rtsp/rtmp等協(xié)議,對(duì)hls的優(yōu)化并不夠重視,hls之前在zlmediakit里面實(shí)現(xiàn)方式跟http文件服務(wù)器實(shí)現(xiàn)方式基本一致,都是通過(guò)直接讀取文件的方式提供下載。所以當(dāng)hls播放數(shù)比較高時(shí),每個(gè)用戶播放都需要重新從磁盤讀取一遍文件,這時(shí)文件io承壓,由于磁盤慢速度的特性,不能承載太高的并發(fā)數(shù)。

有些朋友可能會(huì)問(wèn),如果用內(nèi)存虛擬磁盤能不能提高性能?答案是能,但是由于內(nèi)存拷貝帶寬也存在上限,所以就算hls文件都放在內(nèi)存目錄,每次讀取文件也會(huì)存在多次memcopy,性能并不能有太大的飛躍。前面冬奧會(huì)直播事故那個(gè)案例,就是把hls文件放在內(nèi)存目錄,但是也就能承載2000+并發(fā)而已。

歧途: sendfile

為了解決hls并發(fā)瓶頸這個(gè)問(wèn)題,我首先思考到的是sendfile方案。我們知道,nginx作為http服務(wù)器的標(biāo)桿,就支持sendfile這個(gè)特性。很早之前,我就聽(tīng)說(shuō)過(guò)sendfile多牛逼,它支持直接把文件發(fā)送到socket fd;而不用通過(guò)用戶態(tài)和內(nèi)核態(tài)的內(nèi)存互相拷貝,可以大幅提高文件發(fā)送的性能。

我們查看sendfile的資料,有如下介紹:

圖片.png

于是,在事故反饋當(dāng)日,2022年春節(jié)期間的某天深夜,我在嚴(yán)寒之下光著膀子在zlmediakit中把sendfile特性實(shí)現(xiàn)了一遍:


圖片.png

實(shí)現(xiàn)的代碼如下:

//HttpFileBody.cpp
int HttpFileBody::sendFile(int fd) {
#if  defined(__linux__) || defined(__linux)
    off_t off = _file_offset;
    return sendfile(fd, fileno(_fp.get()), &off, _max_size);
#else
    return -1;
#endif

//HttpSession.cpp
void HttpSession::sendResponse(int code,
                               bool bClose,
                               const char *pcContentType,
                               const HttpSession::KeyValue &header,
                               const HttpBody::Ptr &body,
                               bool no_content_length ){
    //省略大量代碼
    if (typeid(*this) == typeid(HttpSession) && !body->sendFile(getSock()->rawFD())) {
        //http支持sendfile優(yōu)化
        return;
    }
    GET_CONFIG(uint32_t, sendBufSize, Http::kSendBufSize);
    if (body->remainSize() > sendBufSize) {
        //在非https的情況下,通過(guò)sendfile優(yōu)化文件發(fā)送性能
        setSocketFlags();
    }

    //發(fā)送http body
    AsyncSenderData::Ptr data = std::make_shared<AsyncSenderData>(shared_from_this(), body, bClose);
    getSock()->setOnFlush([data]() {
        return AsyncSender::onSocketFlushed(data);
    });
    AsyncSender::onSocketFlushed(data);
}

由于sendfile只能直接發(fā)送文件明文內(nèi)容,所以并不適用于需要文件加密的https場(chǎng)景;這個(gè)優(yōu)化,https是無(wú)法開(kāi)啟的;很遺憾,這次hls事故中,用戶恰恰用的就是https-hls。所以本次優(yōu)化并沒(méi)起到實(shí)質(zhì)作用(https時(shí)關(guān)閉sendfile特性是在用戶反饋tls解析異常才加上的)。

優(yōu)化之旅一:共享mmap

很早之前,zlmediakit已經(jīng)支持mmap方式發(fā)送文件了,但是在本次hls直播事故中,并沒(méi)有發(fā)揮太大的作用,原因有以下幾點(diǎn):

  • 1.每個(gè)hls播放器訪問(wèn)的ts文件都是獨(dú)立的,每訪問(wèn)一次都需要建立一次mmap映射,這樣導(dǎo)致其實(shí)每次都需要內(nèi)存從文件加載一次文件到內(nèi)存,并沒(méi)有減少磁盤io壓力。

  • 2.mmap映射次數(shù)太多,導(dǎo)致內(nèi)存不足,mmap映射失敗,則會(huì)回退為fread方式。

  • 3.由于hls m3u8索引文件是會(huì)一直覆蓋重寫的,而mmap在文件長(zhǎng)度發(fā)送變化時(shí),會(huì)觸發(fā)SIGBUS的錯(cuò)誤,之前為了修復(fù)這個(gè)bug,在訪問(wèn)m3u8文件時(shí),zlmediakit會(huì)強(qiáng)制采用fread方案。

于是在sendfile優(yōu)化方案失敗時(shí),我想到了共享mmap方案,其優(yōu)化思路如下:

圖片.png

共享mmap方案主要解決以下幾個(gè)問(wèn)題:

    1. 防止文件多次mmap時(shí)被多次加載到內(nèi)存,降低文件io壓力。
  • 2.防止mmap次數(shù)太多,導(dǎo)致mmap失敗回退到fread方式。

  • 3.mmap映射內(nèi)存在http明文傳輸情況下,直接寫socket時(shí)不用經(jīng)過(guò)內(nèi)核用戶態(tài)間的互相拷貝,可以降低內(nèi)存帶寬壓力。

于是大概在幾天后,我新增了該特性:


圖片.png

實(shí)現(xiàn)代碼邏輯其實(shí)比較簡(jiǎn)單,同時(shí)也比較巧妙,通過(guò)弱指針全局記錄mmap實(shí)例,在無(wú)任何訪問(wèn)時(shí),mmap自動(dòng)回收,其代碼如下:

static std::shared_ptr<char> getSharedMmap(const string &file_path, int64_t &file_size) {
    {
        lock_guard<mutex> lck(s_mtx);
        auto it = s_shared_mmap.find(file_path);
        if (it != s_shared_mmap.end()) {
            auto ret = std::get<2>(it->second).lock();
            if (ret) {
                //命中mmap緩存
                file_size = std::get<1>(it->second);
                return ret;
            }
        }
    }

    //打開(kāi)文件
    std::shared_ptr<FILE> fp(fopen(file_path.data(), "rb"), [](FILE *fp) {
        if (fp) {
            fclose(fp);
        }
    });
    if (!fp) {
        //文件不存在
        file_size = -1;
        return nullptr;
    }
    //獲取文件大小
    file_size = File::fileSize(fp.get());

    int fd = fileno(fp.get());
    if (fd < 0) {
        WarnL << "fileno failed:" << get_uv_errmsg(false);
        return nullptr;
    }
    auto ptr = (char *)mmap(NULL, file_size, PROT_READ, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED) {
        WarnL << "mmap " << file_path << " failed:" << get_uv_errmsg(false);
        return nullptr;
    }
    std::shared_ptr<char> ret(ptr, [file_size, fp, file_path](char *ptr) {
        munmap(ptr, file_size);
        delSharedMmap(file_path, ptr);
    });
    {
        lock_guard<mutex> lck(s_mtx);
        s_shared_mmap[file_path] = std::make_tuple(ret.get(), file_size, ret);
    }
    return ret;
}

通過(guò)本次優(yōu)化,zlmediakit的hls服務(wù)有比較大的性能提升,性能上限大概提升到了6K左右(壓測(cè)途中還發(fā)現(xiàn)拉流壓測(cè)客戶端由于mktime函數(shù)導(dǎo)致的性能瓶頸問(wèn)題,在此不展開(kāi)描述),但是還是離預(yù)期有些差距:


圖片.png

小插曲: mktime函數(shù)導(dǎo)致拉流壓測(cè)工具性能受限


圖片.png

優(yōu)化之旅二:去除http cookie互斥鎖

在開(kāi)啟共享mmap后,發(fā)現(xiàn)性能上升到6K并發(fā)時(shí),還是上不去;于是我登錄服務(wù)器使用gdb -p調(diào)試進(jìn)程,通過(guò)info threads 查看線程情況,發(fā)現(xiàn)大量線程處于阻塞狀態(tài),這也就是為什么zlmediakit占用cpu不高,但是并發(fā)卻上不去的原因:

圖片.png

為什么這么多線程都處于互斥阻塞狀態(tài)?zlmediakit在使用互斥鎖時(shí),還是比較注意縮小臨界區(qū)的,一些復(fù)雜耗時(shí)的操作一般都會(huì)放在臨界區(qū)之外;經(jīng)過(guò)一番思索,我才恍然大悟,原因是:

壓測(cè)客戶端由于是單進(jìn)程,共享同一份hls cookie,在訪問(wèn)zlmediakit時(shí),這些分布在不同線程的請(qǐng)求,其cookie都相同,導(dǎo)致所有線程同時(shí)大規(guī)模操作同一個(gè)cookie,而操作cookie是要加鎖的,于是這些線程瘋狂的同時(shí)進(jìn)行鎖競(jìng)爭(zhēng),雖然不會(huì)死鎖,但是會(huì)花費(fèi)大量的時(shí)間用在鎖等待上,導(dǎo)致整體性能降低。

雖然在真實(shí)使用場(chǎng)景下,用戶cookie并不一致,這種幾千用戶同時(shí)訪問(wèn)同一個(gè)cookie的情況并不會(huì)存在,但是為了考慮不影響hls性能壓測(cè),也為了杜絕一切隱患,針對(duì)這個(gè)問(wèn)題,我于是對(duì)http/hls的cookie機(jī)制進(jìn)行了修改,在操作cookie時(shí),不再上鎖:

圖片.png

圖片.png

之前對(duì)cookie上鎖屬于過(guò)度設(shè)計(jì),當(dāng)時(shí)目的主要是為了實(shí)現(xiàn)在cookie上隨意掛載數(shù)據(jù)。

優(yōu)化之旅三:hls m3u8文件內(nèi)存化

經(jīng)過(guò)上面兩次優(yōu)化,zlmediakit的hls并發(fā)能力可以達(dá)到8K了,但是當(dāng)hls播放器個(gè)數(shù)達(dá)到在8K 左右時(shí),zlmediakit的ts切片下載開(kāi)始超時(shí),可見(jiàn)系統(tǒng)還是存在性能瓶頸,聯(lián)想到在優(yōu)化cookie互斥鎖時(shí),有線程處于該狀態(tài):


圖片.png

所以我嚴(yán)重懷疑原因是m3u8文件不能使用mmap優(yōu)化(而是采用fread方式)導(dǎo)致的文件io性能瓶頸問(wèn)題,后面通過(guò)查看函數(shù)調(diào)用棧發(fā)現(xiàn),果然是這個(gè)原因。

由于m3u8是易變的,使用mmap映射時(shí),如果文件長(zhǎng)度發(fā)生變化,會(huì)導(dǎo)致觸發(fā)SIGBUS的信號(hào),查看多方資料,此問(wèn)題無(wú)解。所以最后只剩下通過(guò)m3u8文件內(nèi)存化來(lái)解決,于是我修好了m3u8文件的http下載方式,改成直接從內(nèi)存獲取:

圖片.png

結(jié)果:性能爆炸

通過(guò)上述總共3大優(yōu)化,我們?cè)趬簻y(cè)zlmediakit的hls性能時(shí),隨著一點(diǎn)一點(diǎn)增加并發(fā)量,發(fā)現(xiàn)zlmediakit總是能運(yùn)行的非常健康,在并發(fā)量從10K慢慢增加到30K時(shí),并不會(huì)影響ffplay播放的流暢性和效果,以下是壓測(cè)數(shù)據(jù):

壓測(cè)16K http-hls播放器時(shí),流量大概7.5Gb/s:
(大概需要32K端口,由于我測(cè)試機(jī)端口不足,只能最大壓測(cè)到這個(gè)數(shù)據(jù))


圖片.png

圖片.png

圖片.png

后面用戶再壓測(cè)了30k https-hls播放器:


圖片.png

圖片.png

后記:用戶切生產(chǎn)環(huán)境

在完成hls性能優(yōu)化后,該用戶把所有北美節(jié)點(diǎn)的hls流量切到了zlmediakit,


圖片.png

圖片.png

狀況又起:

今天該用戶又反饋給我說(shuō)zlmediakit的內(nèi)存占用非常高,在30K hls并發(fā)時(shí),內(nèi)存占用30+GB:


圖片.png

但是用zlmediakit的getThreadsLoad接口查看,卻發(fā)現(xiàn)負(fù)載很低:

圖片.png

同時(shí)使用zlmediakit的getStatistic接口查看,發(fā)現(xiàn)BufferList對(duì)象個(gè)數(shù)很高,初步懷疑是由于網(wǎng)絡(luò)帶寬不足導(dǎo)致發(fā)送擁塞,內(nèi)存暴漲,通過(guò)詢問(wèn)得知,公網(wǎng)hls訪問(wèn),確實(shí)存在ts文件下載緩慢的問(wèn)題:

圖片.png

同時(shí)讓他通過(guò)局域網(wǎng)測(cè)試ts下載,卻發(fā)現(xiàn)非??欤?/p>

圖片.png

后來(lái)通過(guò)計(jì)算,發(fā)現(xiàn)確實(shí)由于網(wǎng)絡(luò)帶寬瓶頸每個(gè)用戶積壓一個(gè)Buffer包,而每個(gè)Buffer包用戶設(shè)置的為1MB,這樣算下來(lái),30K用戶,確實(shí)會(huì)積壓30GB的發(fā)送緩存:


圖片.png
圖片.png
圖片.png

結(jié)論

通過(guò)上面的經(jīng)歷,我們發(fā)現(xiàn)zlmediakit已經(jīng)足以支撐30K/50Gb級(jí)別的https-hls并發(fā)能力, 理論上,http-hls相比https-hls要少1次內(nèi)存拷貝,和1次加密,性能應(yīng)該要好很多;那么zlmediakit的性能上限在哪里?天知道!畢竟,我已經(jīng)沒(méi)有這么豪華的配置供我壓測(cè)了;在此,我們先立一個(gè)保守的flag吧:

單機(jī) 100K/100Gb級(jí)別 hls并發(fā)能力。

那其他協(xié)議呢? 我覺(jué)得應(yīng)該不輸hls。

最后編輯于
?著作權(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ù)。

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

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