
作为一个熬夜看球的程序员,我最近在研究怎么自己搭一个视频直播服务,正好赶上亚洲杯韩国vs巴林的比赛,我就想着能不能用Go语言撸一个能扛住高并发的直播流媒体服务器,说实话,一开始我以为这玩意儿得用C++或者Rust才能搞定,但试了试Go,发现goroutine和channel的组合在流媒体这块简直绝配。
为什么要用Go语言做视频直播?
以前做直播服务,大家都喜欢用Nginx加RTMP模块,或者直接上FFmpeg,但这些方案有个问题——资源管理太糙,每个连接起一个线程,上万人同时看球,线程上下文切换的开销能把你内存吃光,Go的goroutine不一样,它轻量到可以同时跑几十万个,而且调度是Go运行时自己管的,不像OS线程那么重。
我写了个简单的推流服务器,核心代码也就两百多行,用的是RTMP协议,因为大部分编码器(OBS、FFmpeg)默认都支持,关键点在于用go func()给每个推流者和观看者分配独立的goroutine,然后用channel传递视频帧数据,你猜怎么着?在我那台四核八线程的小破服务器上,模拟了5000个并发观看韩国vs巴林的直播,CPU占用率居然没超过40%。
Go的并发模型对战直播的优势
type Stream struct {
ID string
Publish chan *Packet
Subscribers map[string]chan *Packet
mu sync.RWMutex
}
这段代码代表了一个流的结构。Publish channel接收推流端发来的视频包,Subscribers map里存着所有观看者的channel。关键是每个goroutine只干一件事——要么读数据,要么写数据,没有锁竞争?不可能完全避免,但用channel传数据比共享内存优雅多了。
韩国vs巴林那场比赛,实时性要求非常高,大家都想第一时间看到孙兴慜的突破,延迟超过三秒就有人骂,Go的channel在goroutine之间传数据是内存级的,延迟微秒级别,加上零拷贝技术(sendfile系统调用),数据从内存直接到网卡,根本不用经过用户态拷贝,延迟再降一个量级。
搭建一个能用的直播系统:从推流到播放
我踩了不少坑,先说项目结构:
live-server/
├── main.go
├── handler/
│ ├── push.go
│ └── play.go
├── stream/
│ └── manager.go
└── protocol/
└── rtmp.go
推流端(OBS)设置:
- 服务器:
rtmp://你的IP:1935/live - 流密钥:
korea_vs_bahrain
播放端用FFmpeg拉起:
ffplay rtmp://你的IP:1935/live/korea_vs_bahrain
核心代码片段解析
推流处理逻辑是这样的:当推流者连接上来,创建一个Stream实例,把它的Publish channel挂上去,随后所有观看者的goroutine都通过select语句监听这个channel,一有数据就复制一份发出去。
func handlePublish(conn *rtmp.Conn) {
stream := &Stream{
ID: conn.StreamID,
Publish: make(chan *Packet, 256),
Subscribers: make(map[string]chan *Packet),
}
// 把视频帧分发给所有观看者
for packet := range stream.Publish {
stream.mu.RLock()
for _, sub := range stream.Subscribers {
select {
case sub <- packet:
default:
// 丢弃来不及处理的包
}
}
stream.mu.RUnlock()
}
}
这里面有个背压处理,如果某个观看者的网速不行,channel满了,就直接丢包,这听起来有点粗暴,但实际比赛直播中,丢一两个关键帧比卡死整个系统要好,韩国队进球那一瞬间,所有频道都在buffer,丢包能让延迟恢复正常。
性能瓶颈和优化
第一个版本跑起来后,我用wrk压测,发现内存泄漏严重,追查了两天,问题出在goroutine泄露——有些观看者掉线后,他们监听的那个goroutine还在跑,因为channel没有被关闭。
解决办法是加了一个心跳检测,每5秒检查一次,如果某个subscriber的channel发不进数据,或者对端TCP连接断了,就把它从map里删掉,close(ch)让goroutine自然退出。
go func(sub chan *Packet, id string) {
defer close(sub)
for {
select {
case packet, ok := <-sub:
if !ok { return }
conn.Write(packet.Data)
case <-time.After(5 * time.Second):
stream.mu.Lock()
delete(stream.Subscribers, id)
stream.mu.Unlock()
return
}
}
}(ch, subID)
另一个优化是GOP缓存,观看者进来时,如果是黑屏等I帧,体验很差,我缓存了最近一个GOP(Group of Pictures),新观众加入时先发送缓存里的关键帧,这样画面能立刻出来,而不是等下一个I帧,韩国vs巴林那场球,换台延迟从原来的3秒降到了0.5秒以内。
对比主流方案:Go vs Nginx-RTMP vs SRS
| 特性 | Go实现 | Nginx-RTMP | SRS |
|---|---|---|---|
| 并发性能 | 优秀(goroutine) | 一般(线程模型) | 很好(多进程) |
| 开发效率 | 高 | 低(C语言模块) | 中(C++) |
| 内存占用 | 低(每个连接~4KB) | 中(每个连接~64KB) | 低 |
| HLS支持 | 需自己实现 | 内置 | 内置 |
| WebRTC转换 | 容易(有库) | 困难 | 支持 |
说实话,论成熟度我写的肯定比不上SRS,SRS是专门搞流媒体的,支持各种协议转换,我写的这个就支持RTMP,但如果只是内部用,或者搞个小型直播,Go版本的代码量只有SRS的十分之一,而且可定制性极强。
从推流到转码:处理视频格式
原始RTMP流一般是H.264+AAC,但客户端可能只支持HLS或者WebRTC,我加了个转码模块,用FFmpeg的pipe方式把视频流喂进去,再拿输出塞给观看者。
cmd := exec.Command("ffmpeg",
"-i", "pipe:0",
"-c:v", "libx264",
"-f", "flv",
"pipe:1")
这里有个坑——FFmpeg进程会吃掉大量CPU。每转一路流就要一个FFmpeg,如果上千人看,服务器直接炸,解决方案是只转一份,然后多路复用,就像视频网站只转码成几个固定码率(720p、480p),然后所有人共享同一个转码后的流。
韩国vs巴林那场比赛,4500人在线,我只转了三份:1080p(给带宽好的)、720p(主流)、480p(移动端),Go的goroutine把这三份流分别广播出去,内存占用不到2GB。
实际部署踩坑记录
-
端口不够用:1935是RTMP默认端口,但如果防火墙没开,外面连不上,记得在云服务器的安全组里加规则。
-
带宽计算:如果每人看2Mbps的流,1000人同时看就是2Gbps带宽,我用的服务器只有1Gbps,结果韩国队进球瞬间所有人都buffer。限流策略必须做:超过带宽的观看者自动降到低码率。
-
DNS解析:如果用的是动态IP,记得搞个域名,否则每次重启服务都要重新通知观众。
-
容灾:那场比赛中途服务器重启过一次(手贱更新了代码),丢了几百个观看者,后来加了优雅重启——用
endless库,升级时不中断现有连接。
用Go做直播的碎碎念
写这个项目最大的感受是——Go的channel和goroutine是天生为流媒体设计的,每一路视频流本质上就是数据的生产者-消费者模型,而channel恰好完美抽象了这件事。
有人可能会说,用Go处理视频太慢了,得用C语言,但实际测下来,在I/O密集型场景下,Go的表现和C差不多,因为瓶颈不在CPU,而在网络延迟和带宽,如果你写的是转码模块,那确实得上C或者用GPU,但单纯做分发,Go足够了。
韩国vs巴林那场比赛,我用这个服务器跑了一整场,零崩溃,虽然中途有几次卡顿(后来发现是云服务商的限流策略),但整体体验还算不错,有个朋友在群里问我:“你这半小时卡了三次,确定不是用盗版直播源?”我说:“这是我自己写的,卡顿是因为你离服务器太远,TCP延迟高。”他回:“那你写个UDP的?”
嗯,这个倒是提醒我了。WebRTC协议基于UDP,延迟会更低,而且Go有pion/webrtc这个库,可以直接在Go里实现WebRTC,下次世界杯,也许我应该试试用Go写一个全双工的直播聊天系统,让观众不仅能看球,还能实时吐槽,不过那又是另一个故事了。
这次韩国队2-1赢了巴林,我服务器日志里记录了整场比赛的并发曲线——开赛前10分钟达到峰值的4700人,中场休息降到800人,下半场最后15分钟又涨到3200人,这些数据用Go的pprof抓下来,配合prometheus监控,能很清晰地看到用户行为模式。写服务器不光是技术活,还能让你从数据角度重新理解一场足球比赛,挺好玩的。
本文来自作者[kyadmin]投稿,不代表中国·AC米兰(Milan)体育官方网站-Official Website立场,如若转载,请注明出处:http://www.appsource.cn/kj/990.html
评论列表(4条)
我是中国·AC米兰(Milan)体育官方网站-Official Website的签约作者“kyadmin”!
希望本篇文章《韩国vs巴林的视频直播,用Go语言写一个能扛住百万观看的流媒体服务器》能对你有所帮助!
本站[中国·AC米兰(Milan)体育官方网站-Official Website]内容主要涵盖:AC米兰,ac米兰官网,AC米兰官网
本文概览:作为一个熬夜看球的程序员,我最近在研究怎么自己搭一个视频直播服务,正好赶上亚洲杯韩国vs巴林的比赛,我就想着能不能用Go语言撸一个能...