我如何使用 Python 视频流将 Porn 效率提高 20 倍
已发表: 2022-03-11介绍
色情是一个大产业。 互联网上没有多少网站可以与其最大玩家的流量相媲美。
处理这种巨大的流量是很困难的。 更难的是,色情网站提供的大部分内容都是由低延迟的实时视频流组成,而不是简单的静态视频内容。 但是对于所涉及的所有挑战,我很少读到接受它们的 python 开发人员。 所以我决定写下我自己在工作中的经历。
有什么问题?
几年前,我为世界上第 26 个(当时)访问量最大的网站工作——不仅仅是色情行业:世界。
当时,该网站使用实时消息协议 (RTMP) 提供色情视频流媒体请求。 更具体地说,它使用 Adobe 构建的 Flash Media Server (FMS) 解决方案为用户提供实时流。 基本流程如下:
- 用户请求访问某些直播流
- 服务器以播放所需素材的 RTMP 会话进行回复
出于几个原因,FMS 对我们来说不是一个好的选择,首先是它的成本,其中包括购买两者:
- 我们运行 FMS 的每台机器的 Windows 许可证。
- 约 4,000 美元的 FMS 特定许可证,由于我们的规模,我们不得不购买数百个(每天甚至更多)。
所有这些费用都开始上涨。 撇开成本不谈,FMS 是一个缺乏产品,尤其是在其功能方面(稍后会详细介绍)。 所以我决定放弃 FMS 并从头开始编写我自己的 Python RTMP 解析器。
最后,我设法使我们的服务效率提高了大约 20 倍。
入门
这涉及到两个核心问题:首先,RTMP 和其他 Adobe 协议和格式不开放(即公开可用),这使得它们难以使用。 如何以您一无所知的格式反转或解析文件? 幸运的是,在公共领域(不是由 Adobe 制作,而是由一个名为 OS Flash 的小组制作,现已不复存在)中进行了一些扭转工作,我们以此为基础。
注意:Adobe 后来发布了“规范”,其中包含的信息不超过非 Adobe 制作的反向 wiki 和文档中已经披露的信息。 他们(Adobe)的规范质量低得离谱,几乎不可能实际使用他们的库。 此外,该协议本身有时似乎有意误导。 例如:
- 他们使用 29 位整数。
- 它们在任何地方都包含带有大端格式的协议头——除了一个特定的(但未标记的)字段,它是小端。
- 在传输 9k 视频帧时,他们以计算能力为代价将数据压缩到更小的空间中,这几乎没有意义,因为它们一次可以赚回比特或字节——对于这样的文件大小来说,收益微不足道。
其次:RTMP 是高度面向会话的,这使得对传入流进行多播几乎是不可能的。 理想情况下,如果多个用户想要观看同一个直播流,我们可以将它们传回指向正在播放该流的单个会话(这将是多播视频流)。 但是使用 RTMP,我们必须为每个想要访问的用户创建一个全新的流实例。 这完全是浪费。
我的多播视频流解决方案
考虑到这一点,我决定将典型的响应流重新打包/解析为 FLV“标签”(其中“标签”只是一些视频、音频或元数据)。 这些 FLV 标签可以毫无问题地在 RTMP 中传播。
这种方法的好处:
- 我们只需要重新打包一次流(由于缺乏上述规范和协议怪癖,重新打包是一场噩梦)。
- 我们可以通过简单地为它们提供一个 FLV 标头来重用客户端之间的任何流而几乎没有问题,而指向 FLV 标记的内部指针(以及某种偏移量以指示它们在流中的位置)允许访问内容。
我开始使用我当时最熟悉的语言进行开发:C。随着时间的推移,这个选择变得很麻烦; 所以我在移植我的 C 代码时开始学习 Python 的基础知识。 开发进程加快了,但是在几次demo之后,我很快就遇到了资源枯竭的问题。 Python 的套接字处理并不意味着处理这些类型的情况:具体来说,在 Python 中,我们发现自己在每个操作中进行多个系统调用和上下文切换,从而增加了大量的开销。
提高视频流性能:混合 Python、RTMP 和 C
在对代码进行分析之后,我选择将性能关键函数移动到一个完全用 C 编写的 Python 模块中。这是相当低级的东西:具体来说,它利用内核的 epoll 机制来提供对数增长顺序.
在异步套接字编程中,有一些工具可以为您提供给定套接字是否可读/可写/错误填充的信息。 过去,开发人员使用 select() 系统调用来获取此信息,但扩展性很差。 Poll() 是 select 的更好版本,但它仍然不是很好,因为您必须在每次调用时传入一堆套接字描述符。

Epoll 非常棒,因为您只需注册一个套接字,系统就会记住这个不同的套接字,并在内部处理所有的细节。 因此,每次调用都没有参数传递开销。 它还可以更好地扩展并且只返回您关心的套接字,这比运行 100k 套接字描述符列表以查看它们是否有带有位掩码的事件要好得多——如果您使用其他解决方案,您需要这样做。
但是为了性能的提升,我们付出了代价:这种方法遵循了与以前完全不同的设计模式。 该站点以前的方法是(如果我没记错的话)一个阻塞接收和发送的单一进程; 我正在开发一个事件驱动的解决方案,所以我不得不重构其余的代码以适应这个新模型。
具体来说,在我们的新方法中,我们有一个主循环,它按如下方式处理接收和发送:
- 接收到的数据(作为消息)向上传递到 RTMP 层。
- 剖析 RTMP 并提取 FLV 标签。
- FLV 数据被发送到缓冲和多播层,该层组织流并填充发送方的低级缓冲区。
- 发送者为每个客户端保留一个结构,带有最后发送的索引,并尝试向客户端发送尽可能多的数据。
这是一个滚动的数据窗口,其中包括一些启发式方法,以在客户端接收速度太慢时丢帧。 事情进展得很好。
系统级、架构和硬件问题
但是我们遇到了另一个问题:内核的上下文切换正在成为一种负担。 因此,我们选择仅每 100 毫秒写入一次,而不是立即写入。 这聚合了较小的数据包并防止了上下文切换的爆发。
也许更大的问题在于服务器架构领域:我们需要一个负载平衡和故障转移功能的集群——由于服务器故障而失去用户并不好玩。 起初,我们采用了独立导演的方法,其中指定的“导演”将尝试通过预测需求来创建和销毁广播公司的提要。 这非常失败。 事实上,我们尝试的一切都失败了。 最后,我们选择了一种相对蛮力的方法,在集群的节点之间随机共享广播器,从而使流量相等。
这行得通,但有一个缺点:虽然一般情况处理得很好,但当网站上的每个人(或不成比例的用户)观看一个广播时,我们看到了糟糕的表现。 好消息:这在营销活动之外永远不会发生。 我们实现了一个单独的集群来处理这种情况,但事实上,我们认为为了营销工作而危害付费用户的体验是没有意义的——事实上,这并不是真正的情况(尽管处理所有可以想象的情况会很好案子)。
结论
最终结果的一些统计数据:集群上的每日流量在高峰期约为 10 万用户(60% 负载),平均约为 5 万。 我管理了两个集群(HUN 和 US); 他们每个人处理大约 40 台机器来分担负载。 集群的总带宽约为 50 Gbps,在峰值负载时它们使用的带宽约为 10 Gbps。 最后,我成功地轻松推出了 10 Gbps/机器; 理论上1 ,这个数字可能高达 30 Gbps/机器,这意味着大约 30 万用户同时从一台服务器观看流。
现有的 FMS 集群包含 200 多台机器,这些机器可以被我的 15 台取代——其中只有 10 台可以做任何实际工作。 这给了我们大约 200/10 = 20 倍的改进。
可能我从 Python 视频流项目中最大的收获是,我不应该让自己被必须学习新技能的前景所阻止。 特别是 Python、转码和面向对象编程,这些都是我在接手这个多播视频项目之前所拥有的非专业经验。
那,以及滚动你自己的解决方案可以付出高昂的代价。
1后来,当我们将代码投入生产时,我们遇到了硬件问题,因为我们使用的是旧的 sr2500 Intel 服务器,由于 PCI 带宽低,无法处理 10 Gbit 以太网卡。 相反,我们在 1-4x1 Gbit 以太网绑定中使用它们(将多个网络接口卡的性能聚合到一个虚拟卡中)。 最终,我们得到了一些较新的 sr2600 i7 英特尔,它们提供 10 Gbps 的光学性能,没有任何性能问题。 所有预计的计算均参考此硬件。