概述
在linux环境中,对于文件进行读写操作的时候,我们可以采用libc提供的fread/fwrite系列的一套函数,也可以采用操作系统提供的read/write系列的一套系统api函数。
对于libc提供的文件读写函数,首先它可移植性比较好,因为libc为我们屏蔽了操作系统的底层差异,在linux、windows等不同的操作系统环境下面都有标准的接口实现,因此不需要我们为不同的操作系统进行适配。其次,libc提供了带缓冲功能的读写能力,而操作系统底层文件读写API却不提供这种能力,缓冲能力在大多数情况下能够为我们带来文件i/o性能的提升。
当然libc的文件读写api函数也存在不足之处,缺少了writev/readv之类的函数。不过readv/writev的功能无非就是将多个缓冲区的内容合并成一次批量读写操作,而不需要进行多次API调用,从而减少实际物理I/O的次数,我想libc没有提供这类函数主要也是因为其缓冲功能已经能够将本来需要多次的小块物理I/O操作合并成了一次更大块的物理i/o操作,所以就没有必要再提供readv/writev了。
在应用SRS的视频DVR录存功能进行压力测试的时候,我就碰到了原生SRS在进行录存的时候大并发量文件写入导致CPU消耗过大的问题。究其原因,是因为SRS进行录存文件写入操作的SrsFileWriter类采用了操作系统提供的系统文件写入函数write,它没有缓冲功能,导致DVR录制的时候大量音视频碎片数据写入引起大量的系统调用,可能引起系统在内核态和用户态进行大量迁移,从而引起CPU消耗过高的问题。
下面就测试的原理和方法、测试的环境、测试的结果和相关的结论和大家进行分享。
测试的原理和方法
压测工具,用srs_bench套件中的sb_rtmp_publish模拟推流客户端进行大并发量推流模拟,一台机器压测能力不够可以开启多台机器进行压测。
在srs上面开启DVR录存功能,在srs的配置文件中添加如下代码:
vhost __defaultVhost__ { • dvr { • enabled on; • dvr_path /data/ssd/[app]/[stream].[timestamp].ts; • dvr_plan session; • } }
启动srs后,用压测工具进行压测,观察测试过程中的CPU、网络IO、磁盘IO相关数据,并进行对比。
测试环境
SRS dvr服务器配置如下:
-
CPU: INTEL Xeon 4110 双路16和32线程
-
内存:32G
-
网卡:10Gb
-
磁盘:两块980G的SSD盘做成RAID0(可用空间共1.8T)
-
操作系统:CentOS 7.6。
这里需要说明一下,采用SSD盘主要是为了确保磁盘性能足够,以确保能够支撑大的并发压力,从而在大并发压测的情况下观察系统性能情况,如果本身磁盘I/O性能比较低下,大量的I/O等待可能导致观察不到CPU瓶颈的现象。
另外,在我的测试环境中,SRS经过了多进程改造,能够支持推流进来后自动将不同的流均衡到不同的SRS进程上面,从而能够充分利用服务器多核的能力,但是由此得出的结论同样适合于单进程SRS。
测试过程
-
SRS使用write文件写入SSD盘操作进行压力测试,压测1000路并发3M码流的视频并记录测试结果。
-
改写SRS代码,使用fwrite文件写入SSD盘操作进行压力测试,压测1000路并发3M码流的视频并记录测试结果。
-
重复1的过程,将写入SSD盘改成写内存盘进行压力测试,其他不变。
-
重复2的过程,将写入SSD盘改成写内存盘进行压力测试,其他不变。
需要说明一下,由于我手上的服务器只有32G内存,只能分配16G内存给内存盘使用,
采用如下命令挂载内存盘:
mkdir /data/tmp mount -t tmpfs -o size=16G,mode=0755 tmpfs /data/memdisk
并且修改srs的配置文件将文件写入到内存盘:
vhost __defaultVhost__ { dvr { enabled on; dvr_path /data/memdisk/[app]/[stream].[timestamp].ts; dvr_plan session; } }
由于内存盘比较小,按照3Gb的写入速度,最多能写42s的DVR。
测试数据
测试1:write写入ssd盘
从上图可以看到,1000路3M的DVR录制已经将系统的CPU都跑满了,特别需要关注的是cpu的时间主要消耗在了内核空间上面,占了87.5%。
用nload查看当时的输入带宽情况,发现系统输入带宽平均只有2.17Gb,没有达到预期的3Gb的带宽,应该是CPU负载过高导致SRS来不及处理网络I/O引起的性能下降。
用perf工具对其中一个srs 进程进行性能采样分析,得到下面的火焰图:
可以发现,sys_write操作占用的时间消耗是最多的,对比上面用top看到的内核态消耗的时长占比可以得出的结论是一致的。
最后看磁盘I/O情况:
从上图看磁盘的利用率没有到100%,虽然有一定的波动,但是总体上还是在合理的可以接受的性能范围内。
测试2:fwrite写入ssd盘
从上图可以看到,1000路3M的DVR录制已经将系统的CPU整体来说还有很多空闲(这里说明一下,部分进程的SRS占比高的原因是因为当时任务分配的不够均衡引起的)。特别值得注意的是本次测试内核时间占比大幅下降,只有9.1%。
再用nload看网络i/o情况,如下图:
网络i/o相当平稳,和预期的3Gb完全吻合。
再看磁盘i/o的情况:
从上图看磁盘的利用率没有到100%,虽然有一定的波动,但是总体上还是在合理的可以接受的性能范围内。
最后看火焰图:
系统调用的时间占比大幅度缩短了,在上图几乎找不到sys_write的位置了。
测试3:write写入内存盘
从CPU的情况看,采用内存盘也比较理想,load average只有 7.5,性能非常理想。
测试4:fwrite写入内存盘
对比测试3的CPU情况,可以看到CPU内核时间有更进一步缩短, load-average也有明显变小,相比测试3的性能更好。
测试3和测试4的nload图和测试2基本一致,不再贴出。
测试结论
从以上4个测试可以得出以下结论:
-
无论ssd盘还是内存盘,采用fwrite的性能比采用write的性能有明显的提升,其主要得益于fwrite内置的缓存功能减少了系统调用的数量,带来内核时间消耗的减少,从而提升了性能。
-
在ssd盘情况下,fwrite的缓冲能力可以大幅度降低对于CPU的消耗,但是在采用内存盘的情况下,CPU的消耗虽然也能够降低,但是不是那么明显。
存在的疑问:
之前想当然地认为用write写内存盘,因为系统调用引起的用户态到核心态的切换还是会导致cpu大量消耗,一样会导致CPU消耗高居不下,但是事实看到是采用内存盘以后cpu消耗明显下降了,是不是可以认为系统调用引起的用户态到核心态的切换消耗实际上并没有想象的那么大,而是内核态在处理小块的文件write写入磁盘的时候还存在着其他因素引起消耗大量的cpu。譬如,因为最终写入磁盘都是按照扇区写入的,而小块写入需要操作系统将这个小块对齐并填充到一个完整的磁盘扇区,从而引起性能大幅下降,而内存盘是不是就不会存在这个问题?由于我自己没有内核方面的经验,所以只能存疑了,也请懂内核的朋友给予指点。
对于未来的展望
不管SRS也好,还是NGINX也好,虽然前者采用st-thread框架的协程能力来实现网络异步i/o,但是和后者一样,最终还是采用epoll事件循环来实现网络异步i/o的,但是对于文件i/o,目前存在的问题是,无论是write还是fwrite都是同步操作,在磁盘请求比较繁忙的情况下,必然会导致进程或者线程阻塞,从而引起系统并发性能的下降。
由于操作系统本身不支持epoll异步(linux下的ext4本身没有实现poll的回调),所以寄希望于epoll来实现文件i/o的异步操作是行不通的。NGINX对于文件异步i/o采用了aio+多线程的方式来实现的,个人感觉是由于和epoll模型来说是一套独立的框架,还是相对比较复杂。
不过,好在linux在5.1内核以后提供了io_uring的异步i/o框架,它可以统一网络i/o和磁盘i/o的异步模型,并支持buffer IO,值得我们去关注学习一下,也值得我们后面一起去探讨一下未来如何在srs上采用io_uring来实现带有fwrite一样的缓冲能力的磁盘i/o的操作,来彻底解决磁盘i/o引起的性能瓶颈的问题。