zlmediakit的hls高性能之旅
夏楚 edited this page 2022-04-07 14:59:47 +08:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

事情的起因

北京冬奥会前夕zlmediakit的一位用户完成了iptv系统的迁移; 由于zlmediakit对hls的支持比较完善支持包括鉴权、统计、溯源等独家特性所以他把之前的老系统都迁移到zlmediakit上了。

但是很不幸在冬奥会开幕式当天zlmediakit并没有承受起考验当hls并发数达到3000左右时zlmediakit线程负载接近100%,延时非常高,整个服务器基本不可用:

图片.png

思考

zlmediakit定位是一个通用的流媒体服务器主要精力聚焦在rtsp/rtmp等协议对hls的优化并不够重视hls之前在zlmediakit里面实现方式跟http文件服务器实现方式基本一致都是通过直接读取文件的方式提供下载。所以当hls播放数比较高时每个用户播放都需要重新从磁盘读取一遍文件这时文件io承压由于磁盘慢速度的特性不能承载太高的并发数。

有些朋友可能会问如果用内存虚拟磁盘能不能提高性能答案是能但是由于内存拷贝带宽也存在上限所以就算hls文件都放在内存目录每次读取文件也会存在多次memcopy性能并不能有太大的飞跃。前面冬奥会直播事故那个案例就是把hls文件放在内存目录但是也就能承载2000+并发而已。

歧途: sendfile

为了解决hls并发瓶颈这个问题我首先思考到的是sendfile方案。我们知道,nginx作为http服务器的标杆就支持sendfile这个特性。很早之前我就听说过sendfile多牛逼,它支持直接把文件发送到socket fd;而不用通过用户态和内核态的内存互相拷贝,可以大幅提高文件发送的性能。

我们查看sendfile的资料有如下介绍

图片.png

于是在事故反馈当日2022年春节期间的某天深夜我在严寒之下光着膀子在zlmediakit中把sendfile特性实现了一遍 图片.png

实现的代码如下:

//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优化
        return;
    }
    GET_CONFIG(uint32_t, sendBufSize, Http::kSendBufSize);
    if (body->remainSize() > sendBufSize) {
        //在非https的情况下通过sendfile优化文件发送性能
        setSocketFlags();
    }

    //发送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只能直接发送文件明文内容所以并不适用于需要文件加密的https场景这个优化https是无法开启的很遗憾这次hls事故中用户恰恰用的就是https-hls。所以本次优化并没起到实质作用https时关闭sendfile特性是在用户反馈tls解析异常才加上的

优化之旅一共享mmap

很早之前zlmediakit已经支持mmap方式发送文件了但是在本次hls直播事故中并没有发挥太大的作用原因有以下几点

  • 1.每个hls播放器访问的ts文件都是独立的每访问一次都需要建立一次mmap映射这样导致其实每次都需要内存从文件加载一次文件到内存并没有减少磁盘io压力。

  • 2.mmap映射次数太多导致内存不足mmap映射失败则会回退为fread方式。

  • 3.由于hls m3u8索引文件是会一直覆盖重写的而mmap在文件长度发送变化时会触发SIGBUS的错误之前为了修复这个bug在访问m3u8文件时zlmediakit会强制采用fread方案。

于是在sendfile优化方案失败时我想到了共享mmap方案其优化思路如下

图片.png

共享mmap方案主要解决以下几个问题

  • 防止文件多次mmap时被多次加载到内存降低文件io压力。

  • 防止mmap次数太多导致mmap失败回退到fread方式。

  • mmap映射内存在http明文传输情况下直接写socket时不用经过内核用户态间的互相拷贝可以降低内存带宽压力。

于是大概在几天后,我新增了该特性:

图片.png

实现代码逻辑其实比较简单同时也比较巧妙通过弱指针全局记录mmap实例在无任何访问时mmap自动回收其代码如下

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;
}

通过本次优化zlmediakit的hls服务有比较大的性能提升性能上限大概提升到了6K左右(压测途中还发现拉流压测客户端由于mktime函数导致的性能瓶颈问题在此不展开描述),但是还是离预期有些差距:

图片.png

小插曲: mktime函数导致拉流压测工具性能受限

图片.png

优化之旅二去除http cookie互斥锁

在开启共享mmap后发现性能上升到6K并发时还是上不去于是我登录服务器使用gdb -p调试进程,通过info threads 查看线程情况发现大量线程处于阻塞状态这也就是为什么zlmediakit占用cpu不高但是并发却上不去的原因

图片.png

为什么这么多线程都处于互斥阻塞状态zlmediakit在使用互斥锁时还是比较注意缩小临界区的一些复杂耗时的操作一般都会放在临界区之外经过一番思索我才恍然大悟原因是:

压测客户端由于是单进程共享同一份hls cookie在访问zlmediakit时这些分布在不同线程的请求其cookie都相同导致所有线程同时大规模操作同一个cookie而操作cookie是要加锁的于是这些线程疯狂的同时进行锁竞争虽然不会死锁但是会花费大量的时间用在锁等待上导致整体性能降低。

虽然在真实使用场景下用户cookie并不一致这种几千用户同时访问同一个cookie的情况并不会存在但是为了考虑不影响hls性能压测也为了杜绝一切隐患针对这个问题我于是对http/hls的cookie机制进行了修改在操作cookie时不再上锁

图片.png 图片.png

之前对cookie上锁属于过度设计当时目的主要是为了实现在cookie上随意挂载数据。

优化之旅三hls m3u8文件内存化

经过上面两次优化zlmediakit的hls并发能力可以达到8K了但是当hls播放器个数达到在8K 左右时zlmediakit的ts切片下载开始超时可见系统还是存在性能瓶颈联想到在优化cookie互斥锁时有线程处于该状态

图片.png

所以我严重怀疑原因是m3u8文件不能使用mmap优化(而是采用fread方式)导致的文件io性能瓶颈问题后面通过查看函数调用栈发现果然是这个原因。

由于m3u8是易变的使用mmap映射时如果文件长度发生变化会导致触发SIGBUS的信号查看多方资料此问题无解。所以最后只剩下通过m3u8文件内存化来解决于是我修好了m3u8文件的http下载方式改成直接从内存获取

图片.png

结果:性能爆炸

通过上述总共3大优化我们在压测zlmediakit的hls性能时随着一点一点增加并发量发现zlmediakit总是能运行的非常健康在并发量从10K慢慢增加到30K时并不会影响ffplay播放的流畅性和效果以下是压测数据

压测16K http-hls播放器时流量大概7.5Gb/s (大概需要32K端口由于我测试机端口不足只能最大压测到这个数据)

图片.png 图片.png 图片.png

后面用户再压测了30k https-hls播放器:

图片.png 图片.png

后记:用户切生产环境

在完成hls性能优化后该用户把所有北美节点的hls流量切到了zlmediakit

图片.png 图片.png

状况又起:

今天该用户又反馈给我说zlmediakit的内存占用非常高在30K hls并发时内存占用30+GB:

图片.png

但是用zlmediakit的getThreadsLoad接口查看,却发现负载很低:

图片.png

同时使用zlmediakit的getStatistic接口查看,发现BufferList对象个数很高初步怀疑是由于网络带宽不足导致发送拥塞内存暴涨通过询问得知公网hls访问确实存在ts文件下载缓慢的问题

图片.png

同时让他通过局域网测试ts下载却发现非常快

图片.png

后来通过计算发现确实由于网络带宽瓶颈每个用户积压一个Buffer包而每个Buffer包用户设置的为1MB这样算下来30K用户确实会积压30GB的发送缓存

图片.png

图片.png

图片.png

结论

通过上面的经历我们发现zlmediakit已经足以支撑30K/50Gb级别的https-hls并发能力, 理论上http-hls相比https-hls要少1次内存拷贝和1次加密性能应该要好很多那么zlmediakit的性能上限在哪里天知道毕竟我已经没有这么豪华的配置供我压测了在此我们先立一个保守的flag吧

单机 100K/100Gb级别 hls并发能力。

那其他协议呢? 我觉得应该不输hls。