事情的起因
北京冬奧會(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ù)器基本不可用:
思考
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的資料逸爵,有如下介紹:
于是具滴,在事故反饋當(dāng)日,2022年春節(jié)期間的某天深夜师倔,我在嚴(yán)寒之下光著膀子在zlmediakit中把sendfile特性實(shí)現(xiàn)了一遍:
實(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)化思路如下:
共享mmap方案主要解決以下幾個(gè)問題:
- 防止文件多次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)存帶寬壓力丁恭。
于是大概在幾天后奉狈,我新增了該特性:
實(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ù)期有些差距:
小插曲: mktime函數(shù)導(dǎo)致拉流壓測(cè)工具性能受限
優(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ā)卻上不去的原因:
為什么這么多線程都處于互斥阻塞狀態(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í),不再上鎖:
之前對(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):
所以我嚴(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)存獲染怕琛:
結(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ù))
后面用戶再壓測(cè)了30k https-hls播放器:
后記:用戶切生產(chǎn)環(huán)境
在完成hls性能優(yōu)化后,該用戶把所有北美節(jié)點(diǎn)的hls流量切到了zlmediakit蠢莺,
狀況又起:
今天該用戶又反饋給我說(shuō)zlmediakit的內(nèi)存占用非常高寒匙,在30K hls并發(fā)時(shí),內(nèi)存占用30+GB:
但是用zlmediakit的getThreadsLoad
接口查看躏将,卻發(fā)現(xiàn)負(fù)載很低:
同時(shí)使用zlmediakit的getStatistic
接口查看锄弱,發(fā)現(xiàn)BufferList
對(duì)象個(gè)數(shù)很高,初步懷疑是由于網(wǎng)絡(luò)帶寬不足導(dǎo)致發(fā)送擁塞祸憋,內(nèi)存暴漲会宪,通過(guò)詢問得知,公網(wǎng)hls訪問蚯窥,確實(shí)存在ts文件下載緩慢的問題:
同時(shí)讓他通過(guò)局域網(wǎng)測(cè)試ts下載掸鹅,卻發(fā)現(xiàn)非常快:
后來(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ā)送緩存:
結(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熟史。