zlmediakit的hls高性能之旅

事情的起因

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

但是很不幸糙申,在冬奧會(huì)開幕式當(dāng)天,zlmediakit并沒有承受起考驗(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ì)問,如果用內(nèi)存虛擬磁盤能不能提高性能脉让?答案是能桂敛,但是由于內(nèi)存拷貝帶寬也存在上限冈绊,所以就算hls文件都放在內(nèi)存目錄,每次讀取文件也會(huì)存在多次memcopy埠啃,性能并不能有太大的飛躍死宣。前面冬奧會(huì)直播事故那個(gè)案例,就是把hls文件放在內(nèi)存目錄碴开,但是也就能承載2000+并發(fā)而已毅该。

歧途: sendfile

為了解決hls并發(fā)瓶頸這個(gè)問題,我首先思考到的是sendfile方案潦牛。我們知道眶掌,nginx作為http服務(wù)器的標(biāo)桿,就支持sendfile這個(gè)特性巴碗。很早之前朴爬,我就聽說(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ú)法開啟的;很遺憾瓷胧,這次hls事故中显拳,用戶恰恰用的就是https-hls。所以本次優(yōu)化并沒起到實(shí)質(zhì)作用(https時(shí)關(guān)閉sendfile特性是在用戶反饋tls解析異常才加上的)抖单。

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

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

  • 1.每個(gè)hls播放器訪問的ts文件都是獨(dú)立的刃永,每訪問一次都需要建立一次mmap映射货矮,這樣導(dǎo)致其實(shí)每次都需要內(nèi)存從文件加載一次文件到內(nè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阵具,在訪問m3u8文件時(shí),zlmediakit會(huì)強(qiáng)制采用fread方案定铜。

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

圖片.png

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

    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ú)任何訪問時(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;
            }
        }
    }

    //打開文件
    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)致的性能瓶頸問題痊硕,在此不展開描述)赊级,但是還是離預(yù)期有些差距:


圖片.png

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


圖片.png

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

在開啟共享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威蕉,在訪問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í)訪問同一個(gè)cookie的情況并不會(huì)存在育勺,但是為了考慮不影響hls性能壓測(cè)但荤,也為了杜絕一切隱患,針對(duì)這個(gè)問題涧至,我于是對(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切片下載開始超時(shí),可見系統(tǒng)還是存在性能瓶頸窄陡,聯(lián)想到在優(yōu)化cookie互斥鎖時(shí)炕淮,有線程處于該狀態(tài):


圖片.png

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

由于m3u8是易變的,使用mmap映射時(shí)币叹,如果文件長(zhǎng)度發(fā)生變化润歉,會(huì)導(dǎo)致觸發(fā)SIGBUS的信號(hào),查看多方資料套硼,此問題無(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ǎng)hls訪問蚯窥,確實(shí)存在ts文件下載緩慢的問題:

圖片.png

同時(shí)讓他通過(guò)局域網(wǎng)測(cè)試ts下載掸鹅,卻發(fā)現(xiàn)非常快:


圖片.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)沒有這么豪華的配置供我壓測(cè)了挺尿;在此,我們先立一個(gè)保守的flag吧:

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市窄俏,隨后出現(xiàn)的幾起案子蹂匹,更是在濱河造成了極大的恐慌,老刑警劉巖凹蜈,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件限寞,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡仰坦,警方通過(guò)查閱死者的電腦和手機(jī)履植,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)悄晃,“玉大人玫霎,你說(shuō)我怎么就攤上這事〈矗” “怎么了鼠渺?”我有些...
    開封第一講書人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)眷细。 經(jīng)常有香客問我拦盹,道長(zhǎng),這世上最難降的妖魔是什么溪椎? 我笑而不...
    開封第一講書人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任普舆,我火速辦了婚禮,結(jié)果婚禮上校读,老公的妹妹穿的比我還像新娘沼侣。我一直安慰自己,他們只是感情好歉秫,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開白布蛾洛。 她就那樣靜靜地躺著,像睡著了一般雁芙。 火紅的嫁衣襯著肌膚如雪轧膘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評(píng)論 1 289
  • 那天兔甘,我揣著相機(jī)與錄音谎碍,去河邊找鬼。 笑死洞焙,一個(gè)胖子當(dāng)著我的面吹牛蟆淀,可吹牛的內(nèi)容都是我干的拯啦。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼熔任,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼褒链!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起笋敞,我...
    開封第一講書人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤碱蒙,失蹤者是張志新(化名)和其女友劉穎荠瘪,沒想到半個(gè)月后夯巷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡哀墓,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年趁餐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片篮绰。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡后雷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出吠各,到底是詐尸還是另有隱情臀突,我是刑警寧澤,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布贾漏,位于F島的核電站候学,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏纵散。R本人自食惡果不足惜梳码,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望伍掀。 院中可真熱鬧掰茶,春花似錦、人聲如沸蜜笤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)把兔。三九已至沪伙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間垛贤,已是汗流浹背焰坪。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留聘惦,地道東北人某饰。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓儒恋,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親黔漂。 傳聞我的和親對(duì)象是個(gè)殘疾皇子诫尽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容