srs
srs copied to clipboard
Support Multiple-CPUs(or Threads) to improve concurrency. 支持多线程/多进程/多核。
Remark
多线程准备暂时先放一放,虽然ST支持多线程已经改造得差不多了,RTC多线程部分也改造得差不多了,但是有几个因素让我觉得应该再好好思考下,是否多线程是目前必须要实现的。
首先,多线程分支在srs仓库删除了,在我的仓库还有保留feature/threads,主要包括以下提交:
- ST: Simplify it, only Linux/Darwin, epoll/kqueue, single process 简化ST,只支持有限的平台。
- ST: Support thread-local state-threads 优化ST,支持多线程。
- Threads: Support Hybrid+API+LOG threads 改造SRS支持多线程。
- Threads: Remove reload for streams 禁用部分Reload,多线程下Reload很复杂。
主要是以下的原因,需要时间再好好考虑是否应该支持多线程:
- RTC的级联可以扩展SRS的能力,这样和直播的Edge/Origin机制,简单可靠的扩展。
- 单进程的并发目前在1000到1200并发,再优化QoS后估计能保持在1000左右的并发,对于开源也够用了。
- 多线程只有ST和RTC支持比较完善,直播和API支持还不够完善,需要投入较多精力,不适合我们时间有限的社区做。
- 多线程主要的目标是扩展单机的并发到万级别,一般在商业大规模系统中会用到,一般开源的SFU没有这个场景(何况SRS可以用级联做扩展)。
虽然如此,简化ST和提升ST的性能,还是可以考虑合并的。包括:
- [x] #2547
- [ ] #2548
- [ ] #2949
- [ ] #2950
- [ ] #2951
- [ ] #2112
- [ ] #2952
- [ ] #2953
- [ ] #2954
- [ ] #2916
Summary
SRS支持多线程,是一次非常大的架构升级,本质上是要解决性能不足的问题。
关于性能问题,展开描述如下:
- 在直播播放场景中,单进程单线程能跑到3000并发或更高,因为直播无加密,一对多时可以直接将音视频数据分发。而且可以通过Edge水平扩展。
- 在直播推流场景中,无法做到完全水平扩展,源站集群规模也不能做到很大,参考#464。如果是高码率的流,单进程单线程也很难做到1000路以上。
- 在RTC场景中,除了加密和QoS,UDP收发的性能也会更低,粗略估计很难到500并发。这导致SRS不再是单纯的IO密集型服务器,而是IO和CPU密集型服务器。
这个问题为何重要呢?
- RTC场景,如果单进程只能到100或300,那么就需要10倍的核。而这些服务器之间的传输的流也是十倍。这时候并发能力就完全不够了。
- 直播场景,目前通过多进程、Origin和Edge集群扩展能力,如果能支持多线程,就可以单机达到很高并发,减少管理的机器数目,降低系统复杂度。
- 监控场景,不再是一个流非常多人消费,而是流很多,而且需要加密。那么一个进程如果支持的路数不够,就会造成进程很多的问题。
因此,多线程架构可以说是多协程架构之后的一次革命,不过这次是革自己的命。
Arch
- 单进程单线程架构,直播支持Origin-Edge集群,如果部署在单机就是多进程单线程架构。
- RTC由于是计算和IO密集型,所以多核能力很重要,主要问题就是这个帖子所描述的问题。
- 尽管这样,SRS4也做了很多单核的性能优化,参考b431ad7、c5d2027、14bfc98和36ea673
最终的目标架构,是水平扩展的Hybrid线程,也就是少锁多线程结构(SRS/v5.0.3):
- 合并SRTP线程到Hybrid线程,两个线程同时调用openssl,有Crash风险。
- UDP RECV和SEND线程是可选的,默认关闭,也就是由Hybrid线程收发数据。
- ST改为thread-local结构(ST#19),每个线程有自己隔离的ST,必须严格保证线程的ST结构不会传递到另外一个线程,也就是不能读写其他线程的FD。
- Hybrid线程默认1个,也就是和SRS4的完全单线程结构基本接近,保持结构的简单性;如果需要开启高性能,可以开启多个线程,架构基本上也是不变的(从一个线程变成多个独立的线程)。
- Hybrid线程支持水平扩展,注意是用的多端口方式,RTC通过SDP返回不同端口,RTMP和HTTP需要通过302跳转,具体实现要看文档了。
- Hybrid水平扩展时,还是根据流锁定连接到一个线程,这样能支持推流的扩展。几乎可以水平扩展到百或千核,核数增多时线程之间通信增多,也并非完全无代价,代价较小。
- Hybrid水平扩展时,一般情况下多路推流多路播放,可以支持得很好,比如1000推流每个流10个播放总共1万路流。
这个架构的缺点:
- 单流播放扩展问题:Hybrid水平扩展时,由于每个流锁定在一个线程,所以一个流的播放路数,受限于单线程支持的路数。解法:未来计划通过级联解决下行扩展问题,比如下行一个流一台机器(无论多少核)支持1000路,可以通过级联1000个服务器支持100万路播放。
- 全局变量和静态变量清理问题:多线程虽然是隔离的,但是他们还是有机会通过全局和静态变量犯错误,所以所有的全局变量,都必须检查和修改一遍,要么改成thread-local,要么改成thread-safe,这给稳定性带来了隐患,而且出现问题时往往很难排查。解法:默认只开启1个线程,用足够长的时间过渡和改善。
- 库的线程安全问题:比如OpenSSL有多线程问题,OpenSSL 1.1号称是线程安全,需要修改编译脚本,不使用选项
-no-threads
,但有时候可能忘记开修改这个选项造成问题。解法:默认只开启1个SRTP线程,用足够长的时间过渡和改善。
单流播放扩展问题:如果一定要修改多线程,让单个机器多核支持播放的水平扩展,可以让推流线程广播给拉流线程。这样的改动是可以接受,但是开源上并不完全追求性能,还是要保持非常简单架构的性能优化,个人认为单机1000路播放,通过级联解决扩展是合理的,所以未来不会使用这种优化。
Note:图片来源在这里。
通信机制
线程之间通信,第一种是有锁的chan ,第二种就是传递fd 。第二种,可以依赖第一种。
这两种方式,都应该避免传递音视频数据。当然也可以传,但不高效。比如可以启动转码线程,用chan通信,这种也不需要多少并发。
SRS会有多个ST线程,他们之间通过chan 通信,但他们不传递音视频数据,而是一些协调的消息。
目前SRS的线程通信,使用的是pipe实现,这样可以避免锁。因此,使用时注意是很低效的机制,不可以直接传递音视频包。目前主要使用在Master(API)线程,和Hybrid(服务)线程之间的通信,Hybrid返回SDP给API。
线程类型
每个线程都会有自己的ST,ST是thread-local的,也就是独立隔离的ST。
Remark: 最关键的风险和变化,就是要避免由一个线程创建的FD(或其他ST资源),传递到另外一个线程处理,一定就会出问题。
最终线程会分成几种:
1,主线程。它主要是管理配置,管理线程,传递消息,侦听API。
2,日志线程。从queue 读取日志,写入磁盘。避免磁盘io 阻塞st ,其实录制和HLS写磁盘也可以放这个线程。
3,一或多个Hybrid线程。侦听网络fd ,创建epoll ,启动st ,主要的音视频业务线程。线程之间不通信。直播使用REUSE PORT,RTC使用多端口隔离。
4,可选,srt 线程。和目前一样,独立的srt,通过本机socket推rtmp到st线程。
5,可选,如果自己实现srt 协议,那么就可以在st 线程中处理srt客户端了。
6,可选,转码或ai 线程,通过本机socket从st线程拉音视频数据,或者通过Chan传递数据,实现连麦合流等能力。
里程碑
4.0不会启用多线程,还是保持单线程能力。
5.0会实现大部分的多线程能力,包括改进ST的thread-local能力。但hybrid只会默认为1个线程,进程有多个线程,但整体看起来和之前单线程差别不算很大。
6.0默认会开启和CPU核数一样多的线程,全部完成多线程架构的改造。
和Go的差异
Go的多线程损耗太大了,性能不够,它要做的是通用服务。
在多个核时,比如16核,Go差不多有5个核用于切换。因为多个线程之间有锁和数据拷贝,尽管用的是chan 。
另外,Go是真的多线程,要随时考虑竞争和线程切换,而srs要做的还是真单线程。用起来go也更复杂,srs还是可以基本保持单线程的简单。
Srs是基于业务优化的多线程和协程,本质上还是单线程,线程之间基本没关系。
和Source的关系
一个st线程,会有多个source。
一个source也就是推流,以及对应的consumer即播放,只在一个st线程。
这样推流和播放都在一个st线程完成,不需要锁,不需要切换。
由于客户端连接上来时,不知道它的url,也就不知道属于哪个流,所以可能会被错误的st线程accept,这就需要迁移fd 了。
在多线程之间迁移fd ,相对比较简单。难点是st ,要支持多线程,迁移fd 时也需要考虑在新的st线程的epoll 中重建这个fd。不过这个也不是特别难,比多进程容易多了。
为何不是多进程
多进程的fd 迁移太难做了,进程之间通信,不如线程之间通信容易,效率也没有线程高。
Nginx之所以多进程,它多个进程之间完全不用迁移fd 。所以做直播时,NginxRTMP 进程之间会推送流,这就太难维护了。
如果不迁移,就需要转发音视频包,肯定还是基于流做fd 迁移更好,更合适流媒体的特点。
See https://github.com/ossrs/srs/issues/2188#thread-local
See https://github.com/ossrs/srs/issues/2188#udp-binding
centos 8(4.18.0-193.28.1.el8_2.x86_64) 下使用原代码帧听在 0.0.0.0:8000
也是迁移两次, 但是参考手册, 在迁移前先 connect 一次 AF_UNSPEC 后, 可以不断迁移:
$ ./udp-connect-server 0.0.0.0 8000 2
Start 2 workers, at 0.0.0.0:8000
listen at 0.0.0.0:8000, fd=4, migrate_fd=3 ok
listen at 0.0.0.0:8000, fd=3, migrate_fd=4 ok
fd #4, peer 127.0.0.1:51161, got 13B, Hello world!
dissolve the association with 127.0.0.1:51161 of #4, r0=0, errno=0
Transfer 127.0.0.1:51161 from #4 to #3, r0=0, errno=0
fd #3, peer 127.0.0.1:51161, got 13B, Hello world!
dissolve the association with 127.0.0.1:51161 of #3, r0=0, errno=0
Transfer 127.0.0.1:51161 from #3 to #4, r0=0, errno=0
fd #4, peer 127.0.0.1:51161, got 13B, Hello world!
dissolve the association with 127.0.0.1:51161 of #4, r0=0, errno=0
Transfer 127.0.0.1:51161 from #4 to #3, r0=0, errno=0
fd #3, peer 127.0.0.1:51161, got 13B, Hello world!
dissolve the association with 127.0.0.1:51161 of #3, r0=0, errno=0
Transfer 127.0.0.1:51161 from #3 to #4, r0=0, errno=0
fd #4, peer 127.0.0.1:51161, got 13B, Hello world!
dissolve the association with 127.0.0.1:51161 of #4, r0=0, errno=0
Transfer 127.0.0.1:51161 from #4 to #3, r0=0, errno=0
fd #3, peer 127.0.0.1:51161, got 13B, Hello world!
dissolve the association with 127.0.0.1:51161 of #3, r0=0, errno=0
Transfer 127.0.0.1:51161 from #3 to #4, r0=0, errno=0
fd #4, peer 127.0.0.1:51161, got 13B, Hello world!
dissolve the association with 127.0.0.1:51161 of #4, r0=0, errno=0
Transfer 127.0.0.1:51161 from #4 to #3, r0=0, errno=0
同样测试适用于绑定固定地址:
$ ./udp-connect-server 127.0.0.1 8000 2
Start 2 workers, at 127.0.0.1:8000
listen at 127.0.0.1:8000, fd=4, migrate_fd=3 ok
listen at 127.0.0.1:8000, fd=3, migrate_fd=4 ok
fd #3, peer 127.0.0.1:44481, got 13B, Hello world!
dissolve the association with 127.0.0.1:44481 of #3, r0=0, errno=0
Transfer 127.0.0.1:44481 from #3 to #4, r0=0, errno=0
fd #4, peer 127.0.0.1:44481, got 13B, Hello world!
dissolve the association with 127.0.0.1:44481 of #4, r0=0, errno=0
Transfer 127.0.0.1:44481 from #4 to #3, r0=0, errno=0
fd #3, peer 127.0.0.1:44481, got 13B, Hello world!
dissolve the association with 127.0.0.1:44481 of #3, r0=0, errno=0
Transfer 127.0.0.1:44481 from #3 to #4, r0=0, errno=0
fd #4, peer 127.0.0.1:44481, got 13B, Hello world!
dissolve the association with 127.0.0.1:44481 of #4, r0=0, errno=0
Transfer 127.0.0.1:44481 from #4 to #3, r0=0, errno=0
fd #3, peer 127.0.0.1:44481, got 13B, Hello world!
dissolve the association with 127.0.0.1:44481 of #3, r0=0, errno=0
Transfer 127.0.0.1:44481 from #3 to #4, r0=0, errno=0
https://man7.org/linux/man-pages/man2/connect.2.html
Some protocol sockets (e.g., TCP sockets as well as datagram sockets in the UNIX and Internet domains) may dissolve the association by connecting to an address with the sa_family member of sockaddr set to AF_UNSPEC; thereafter, the socket can be connected to another address. (AF_UNSPEC is supported on Linux since kernel 2.2.)
See https://github.com/ossrs/srs/issues/2188#udp-migration
日志已经由单独的线程写:
- Threads-Log: Refine API and main workflow: db18157d313c2899fac2be8d95d44906934096a5
- Threads-Log: Run hybrid server in thread: 11d68988eaa94aac9bff5dac86749f851ea8f5ee
- Threads-Log: Refine thread lock type to ERRORCHECK: 80b6936d75a2ef4a05abc0d5c5c24ffeea6960c2
- Threads-Log: Use thread to write and reopen logs: 439a9f3b2c99fa81d4921e246cfef787fa5879b7
- Threads-Log: Remove utest for reload log configs: a3e2d09ac2c1ce0d226dcf76e89343d820b416b3
- Threads-Log: Support thread-safe queue SrsThreadQueue: 3afb483ee501d5d59fa065a1d84221e45778c847
- Threads-Log: Support dual queue cache for async logs: 5eda21b8df0336c4cdf5c6abf0f68a07083871e7
- Threads-Log: Refine dual queue for log thread: f337a278499bfc8a43a900ece6d01f4ba61fead0
- Threads-Log: Refine comments for global variable: 40ad0eed0501739695831de2153cf437cbb53a8a
- Threads-Log: Refine stat for sync wait, in log thread: 2e3689de8e228cab07d64ba2588887dca82ee432
独立线程写日志的机制如下:
- 每次写日志都会加锁,放到队列。
- 每隔一定时间,配置
srs_log_flush_interval
默认为1.3秒,会将日志写入磁盘。 - 真正有锁等待的机会,只有1.3秒才会有一次,而且等待时间很短,有
sync
日志显示在10~100us。
Thread: cycle threads=3, logs=1/0/0, sync=0,1,0,0
如果需要写多份日志文件,那么这个优化优势会更好。比如:
- 可能需要写各种统计日志。
- 可能需要写各种计费日志。
- 可能需要写媒体文件,DVR或HLS。
未来会将DVR和HLS改成独立线程写,还需要考虑几个因素:
- 媒体文件不能丢内容,否则会造成解码失败。
- 关闭时必须将缓存的数据写入磁盘,否则造成文件异常。
- 网络磁盘可能用协程更合适。
Note: 最终架构,我们确定是由可水平扩展的Hybrid线程,不会单独拆分SRTP和SEND/RECV线程,因此隐藏相关评论。
收发和SRTP启动专门的线程做,相关的Commit如下:
- Threads-SRTP: Config and add files for the async-srtp : 28504c0183c644560f4d7837bb498342d0391cc8
- Threads-SRTP: Support decrypt RTP by async SRTP: 9a82baf902b5fa5e2c78dd7c752d1290f0cc3011
- Threads: Fix bug for SRTP and Log thread nanosleep: 85570cfdf162d96c8e61bf395c5923f4a72107cf
- Threads-RECV: Support dedicate thread to recv UDP packets: e20eedfd9fa7f6308829b621106686804369090f
- Threads-RECV: Refine the stat for SNMP UDP recv/error: 93fd8bca94e6a2f819642caec9156ee8ff8221a3
- Threads: Set the threads name display in top: 3c9c9b1cb558b3edb658155e67cd1d31e697b8d7
- Threads: Support cpu affinity for threads: a3ea734e8475784a71fa1d7d9167f1539fce5430
- Threads-RECV: Drop received packet if exceed max queue size: 981446ae4c4ed7059e49d3cc07e61854ebc92e43
- Threads-RECV: Show the dropped packets pps: 1d8d8ab323ca562636defe97d6422e41acc20c61
- Threads: Use thread-local buffer for log: d49928df2732b5c796fab09698a9572e0814c2c7
- Threads: Use coroutine to consume recv/srtp packets: a7e7ba2b555d60bfffe5934207228b47822b310c
- Threads: Support Circuit-Breaker to work in storms: 89b7ef52eb259caef9118d768299728d144e7e78
- Threads: Refine variables and do dispose: a179a40baff0f0197bfc15b585d439032f8ff848
- Threads: Merge recv and srtp consume to one timer: f6036119e6ae1623a9e89943ff709952fb872345
- Threads-RECV: Change UDP recv max size from 6k to 1500 bytes: 246f7276c7d9939fc289fe78b350b721be81a449
- Threads-SRTP: Support async decrypt RTCP: ac7c2770ae08fc99ab69da402996845d3d5e3df1
- Threads-SEND: Support async send UDP packets: 5bfbc4ac079fa645e80e660e90c41cd4bc2ffa15
- Threads-SRTP: Use async encrypt SRTP packet: 837a04ab7f2a819b67d2c809604eb0ee9beea58c
在开启加解密、NACK和TWCC前提下,单线程能支持到500路并发流(推或拉),多线程能支持到1000路并发(起步),未来的优化方向:
- 目前Hybrid线程,也就是之前的SRS主线程,集中了服务器的处理逻辑,其他线程也通过这个线程通信(批量队列)。
- 尽量减少Hybrid线程的中转,比如RECV/SEND线程和SRTP线程可以直接通信,除非是新的连接才需要通过中转建立关联。
- 改造ST变成thread_local的ST,ST目前是非线程安全,改造后才能保障多线程下不会出现问题。通信需要使用类似Go的chan机制,线程+协程通信效率很低但可能是必须的。
- 可以通过RECV线程做逻辑判断,将同一个流的包转给特定的Hybrid线程,这样Hybrid线程也可以水平扩展。
- 使用系统UDP五元组绑定机制,Hybrid线程直接独立完成服务,避免线程之间的锁(改动较大可能需要时间和稳定性考虑),性能是否比目前批量队列的方式高,也需要验证。
Note: 最终架构,我们确定是由可水平扩展的Hybrid线程,不会单独拆分SRTP和SEND/RECV线程,因此隐藏相关评论。
尽量减少Hybrid线程的中转,比如RECV/SEND线程和SRTP线程可以直接通信,除非是新的连接才需要通过中转建立关联。
- Threads-RECV: Support tunnel for recv-srtp. https://github.com/ossrs/srs/commit/f1c672642824d511b54cca201207595154c825fc
- Threads-SEND: Support tunnel for srtp-send. https://github.com/ossrs/srs/commit/35e209e75eb7e02da0e511018b3bce91741d5a16
Note: 最终架构,我们确定是由可水平扩展的Hybrid线程,不会单独拆分SRTP和SEND/RECV线程,因此隐藏相关评论。
开启多线程性能优化时,需要打开配置,默认这些配置都是关闭的:
threads {
async_srtp on;
async_recv on;
async_send on;
async_tunnel on;
cpu_affinity {
hybrid 1; srtp 2; recv 3; send 3; master 3; log 3;
}
}
Note:线程的亲和性(affinity)设置中,CPU-0预留给软中断。
熔断器默认是开启的,能保护服务器在高负载时不会被打趴下:
circuit_breaker {
max_recv_queue 5000;
high_threshold 90;
high_pulse 2;
critical_threshold 95;
critical_pulse 1;
dying_threshold 99;
dying_pulse 5;
}
Note: 配置项的具体函数,请参考
full.conf
,可以关闭熔断器,推荐开启。
Note: 这是中间结果,已经合并到了这个Issue的描述中,所以折叠相关评论。
- 单进程单线程架构,直播支持Origin-Edge集群,如果部署在单机就是多进程单线程架构。
- RTC由于是计算和IO密集型,所以多核能力很重要,主要问题就是这个帖子所描述的问题。
- 尽管这样,SRS4也做了很多单核的性能优化,参考b431ad7、c5d2027、14bfc98和36ea673
实现过一种有锁多线程结构(SRS/v5.0.2),是个中间状态的版本:
- TUNNEL隧道,是RECV-SRTP和SRTP-SEND线程直接通信的通道。
- TUNNEL隧道由Hybrid构建,只有Hybrid才知道这些上下文信息。
- 启动时无TUNNEL,包通过Hybrid中转,在DTLS成功结束后Hybrid建立TUNNEL。
Note: 优化的方向是扩展Hybrid线程,实现多线程(少锁)架构,达到Envoy这样最高的性能。
Remark: 这个版本的优势是改动小,稳定性高,但是hybrid还是瓶颈,无法水平扩展。这个版本的代码只会作为临时方案供参考,会从主干中移除,参考feature/threads_with_locks。
The RTC benchmark data, by srs-bench:
Update | Server | Clients | Type | CPU | Memory | Threads | Commit |
---|---|---|---|---|---|---|---|
2021-03-31 | SRS/5.0.2 | 1400 | publishers | ~90% x 4 | 3.1GB | 6 | #2188 |
2021-03-31 | SRS/5.0.2 | 1400 | players | ~93% x 4 | 1.0GB | 6 | #2188 |
2021-03-31 | SRS/4.0.87 | 550 | publishers | ~86% x 1 | 1.3GB | 1 | |
2021-03-31 | SRS/4.0.87 | 800 | players | ~94% x 1 | 444MB | 1 |
Note: The benchmark tool for Janus is srs-bench, and startup script by janus-docker.
Note: 这是中间结果,已经合并到了这个Issue的描述中,所以折叠相关评论。
- 合并SRTP线程到Hybrid线程,两个线程同时调用openssl,有Crash风险。
- UDP RECV和SEND线程是可选的,默认关闭,也就是由Hybrid线程收发数据。
- ST改为thread-local结构(ST#19),每个线程有自己隔离的ST,必须严格保证线程的ST结构不会传递到另外一个线程,也就是不能读写其他线程的FD。
- Hybrid线程默认1个,也就是和SRS4的完全单线程结构基本接近,保持结构的简单性;如果需要开启高性能,可以开启多个线程,架构基本上也是不变的(从一个线程变成多个独立的线程)。
- Hybrid线程支持水平扩展,注意是用的多端口方式,RTC通过SDP返回不同端口,RTMP和HTTP需要通过302跳转,具体实现要看文档了。
- Hybrid水平扩展时,还是根据流锁定连接到一个线程,这样能支持推流的扩展。几乎可以水平扩展到百或千核,核数增多时线程之间通信增多,也并非完全无代价,代价较小。
- Hybrid水平扩展时,一般情况下多路推流多路播放,可以支持得很好,比如1000推流每个流10个播放总共1万路流。
这个架构的缺点:
- 单流播放扩展问题:Hybrid水平扩展时,由于每个流锁定在一个线程,所以一个流的播放路数,受限于单线程支持的路数。解法:未来计划通过级联解决下行扩展问题,比如下行一个流一台机器(无论多少核)支持1000路,可以通过级联1000个服务器支持100万路播放。
- 全局变量和静态变量清理问题:多线程虽然是隔离的,但是他们还是有机会通过全局和静态变量犯错误,所以所有的全局变量,都必须检查和修改一遍,要么改成thread-local,要么改成thread-safe,这给稳定性带来了隐患,而且出现问题时往往很难排查。解法:默认只开启1个线程,用足够长的时间过渡和改善。
- 库的线程安全问题:比如OpenSSL有多线程问题,OpenSSL 1.1号称是线程安全,需要修改编译脚本,不使用选项
-no-threads
,但有时候可能忘记开修改这个选项造成问题。解法:默认只开启1个SRTP线程,用足够长的时间过渡和改善。
单流播放扩展问题:如果一定要修改多线程,让单个机器多核支持播放的水平扩展,可以使用虚拟的Consumer,在线程之间共享Packet的指针,在另外一个线程消费Packet,这样的改动是可以接受,但是开源上并不完全追求性能,还是要保持非常简单架构的性能优化,个人认为单机1000路播放,通过级联解决扩展是合理的,所以未来不会使用这种优化。
So, does it support multi-threading for WebRTC now? After starting multiple processes and adding port reuse, only one core is used on a multi-core machine.
I've been working with Node.js for almost a year now, and I found that it is very similar to SRS's coroutine + multithreading, and I can basically see the future of SRS's multithreading.
The simplicity is quite good, which is what we want. We can't refer to Go for multithreading because it is true multithreading, while Node.js's multithreading is actually single-threaded for each thread, without locks and such. Go actually has locks.
Multithreading without thread synchronization is more suitable for maintenance.
I still insist on business separation for multithreading, with the stream still in one thread. This doesn't solve performance issues, but it does solve some CPU-intensive and freezing issues, such as:
- Audio transcoding: For example, AAC to Opus, and vice versa.
- DNS resolution: Currently implemented using system functions, multithreading can avoid blocking (of course, using UDP to implement it yourself is also a solution, but it's more complicated).
- Writing logs: Writing to disk, usually not a problem, but who knows? Experts say that if you can rely on the disk, everything should be on the tree.
- HLS, DASH, and DVR: Writing a lot of data to disk, some friends have tested that it has an impact when there are more than 10 channels, because the disk is really unreliable.
After solving these problems, the stability will be improved, and sometimes it is definitely affected by these factors.
I don't want to do multithreading for streams, because from the perspective of ease of use, for example, the API needs to double the maintenance cost for clustering, requires scheduling logic (no matter how simple), increases the steps for troubleshooting, and cannot simply evaluate the load. All these factors will greatly reduce the maintenance level of the entire project.
The only advantage of multithreading for streams is to increase multi-core capabilities, which can be achieved through cascading (which will be supported in the future) and business scheduling. If you think that running one process on one machine is too wasteful, you can use multiple ports or implement it with Pods. In any case, if you have already reached the point of focusing on high performance, it must be a large business volume of tens of thousands or even hundreds of thousands. If such a large business volume does not have research and development capabilities, it will either be a pitfall or a death trap.