Cómo hice que la pornografía fuera 20 veces más eficiente con Python Video Streaming
Publicado: 2022-03-11Introducción
La pornografía es una gran industria. No hay muchos sitios en Internet que puedan competir con el tráfico de sus principales jugadores.
Y hacer malabarismos con este inmenso tráfico es difícil. Para complicar aún más las cosas, gran parte del contenido de los sitios de pornografía se compone de transmisiones de video en vivo de baja latencia en lugar de contenido de video estático simple. Pero a pesar de todos los desafíos involucrados, rara vez he leído acerca de los desarrolladores de python que los enfrentan. Así que decidí escribir sobre mi propia experiencia en el trabajo.
¿Cuál es el problema?
Hace unos años, trabajaba para el sitio web número 26 (en ese momento) más visitado del mundo, no solo de la industria del porno: del mundo.
En ese momento, el sitio atendía solicitudes de transmisión de videos pornográficos con el protocolo de mensajería en tiempo real (RTMP). Más específicamente, usó una solución Flash Media Server (FMS), creada por Adobe, para proporcionar a los usuarios transmisiones en vivo. El proceso básico fue el siguiente:
- El usuario solicita acceso a alguna transmisión en vivo
- El servidor responde con una sesión RTMP que reproduce el metraje deseado.
Por un par de razones, FMS no fue una buena opción para nosotros, comenzando por sus costos, que incluían la compra de ambos:
- Licencias de Windows para cada máquina en la que ejecutamos FMS.
- ~Licencias específicas de FMS de $4k, de las cuales tuvimos que comprar varios cientos (y más cada día) debido a nuestra escala.
Todas estas tarifas comenzaron a acumularse. Y aparte de los costos, FMS era un producto deficiente, especialmente en su funcionalidad (más sobre esto en un momento). Así que decidí descartar FMS y escribir mi propio analizador Python RTMP desde cero.
Al final, logré que nuestro servicio fuera aproximadamente 20 veces más eficiente.
Empezando
Había dos problemas principales involucrados: en primer lugar, RTMP y otros protocolos y formatos de Adobe no estaban abiertos (es decir, disponibles públicamente), lo que dificultaba trabajar con ellos. ¿Cómo puede revertir o analizar archivos en un formato del que no sabe nada? Afortunadamente, hubo algunos esfuerzos de reversión disponibles en la esfera pública (no producidos por Adobe, sino por un grupo llamado OS Flash, ahora desaparecido) en los que basamos nuestro trabajo.
Nota: Adobe publicó más tarde "especificaciones" que no contenían más información que la que ya se había revelado en los documentos y wiki de inversión no producidos por Adobe. Sus especificaciones (las de Adobe) eran de una calidad absurdamente baja y hacían casi imposible usar sus bibliotecas. Además, el protocolo en sí parecía intencionalmente engañoso a veces. Por ejemplo:
- Usaron enteros de 29 bits.
- Incluían encabezados de protocolo con formato big endian en todas partes, excepto en un campo específico (aún sin marcar), que era little endian.
- Exprimieron los datos en menos espacio a costa de la potencia computacional al transportar fotogramas de video de 9k, lo que tenía poco o ningún sentido, porque estaban recuperando bits o bytes a la vez, ganancias insignificantes para un tamaño de archivo de este tipo.
Y en segundo lugar: RTMP está muy orientado a la sesión, lo que hacía prácticamente imposible la multidifusión de una transmisión entrante. Idealmente, si varios usuarios quisieran ver la misma transmisión en vivo, podríamos simplemente pasarles punteros a una sola sesión en la que se está transmitiendo esa transmisión (esto sería una transmisión de video de multidifusión). Pero con RTMP, tuvimos que crear una instancia completamente nueva de la transmisión para cada usuario que quisiera acceder. Esto fue un completo desperdicio.
Mi solución de transmisión de video multidifusión
Con eso en mente, decidí volver a empaquetar/analizar el flujo de respuesta típico en 'etiquetas' FLV (donde una 'etiqueta' es solo un video, audio o metadatos). Estas etiquetas FLV podrían viajar dentro del RTMP sin problemas.
Los beneficios de tal enfoque:
- Solo necesitábamos volver a empaquetar una secuencia una vez (reempaquetar fue una pesadilla debido a la falta de especificaciones y peculiaridades del protocolo descritas anteriormente).
- Podríamos reutilizar cualquier flujo entre clientes con muy pocos problemas al proporcionarles simplemente un encabezado FLV, mientras que un puntero interno a las etiquetas FLV (junto con algún tipo de compensación para indicar dónde se encuentran en el flujo) permitió el acceso a el contenido.
Empecé el desarrollo en el idioma que mejor conocía en ese momento: C. Con el tiempo, esta elección se volvió engorrosa; así que comencé a aprender los conceptos básicos de Python mientras transfería mi código C. El proceso de desarrollo se aceleró, pero después de algunas demostraciones, rápidamente me encontré con el problema de agotar los recursos. El manejo de sockets de Python no estaba destinado a manejar este tipo de situaciones: específicamente, en Python nos encontramos haciendo múltiples llamadas al sistema y cambios de contexto por acción, agregando una gran cantidad de gastos generales.
Mejora del rendimiento de transmisión de video: combinación de Python, RTMP y C
Después de perfilar el código, opté por mover las funciones críticas para el rendimiento a un módulo de Python escrito completamente en C. Esto era algo de nivel bastante bajo: específicamente, hizo uso del mecanismo epoll del kernel para proporcionar un orden de crecimiento logarítmico. .
En la programación de sockets asíncronos, existen funciones que pueden proporcionarle información sobre si un socket dado es legible/escribible/lleno de errores. En el pasado, los desarrolladores han utilizado la llamada al sistema select() para obtener esta información, que escala mal. Poll () es una mejor versión de select, pero aún no es tan bueno ya que tiene que pasar un montón de descriptores de socket en cada llamada.

Epoll es increíble ya que todo lo que tiene que hacer es registrar un socket y el sistema recordará ese socket distinto, manejando todos los detalles arenosos internamente. Por lo tanto, no hay sobrecarga de paso de argumentos con cada llamada. También escala mucho mejor y devuelve solo los sockets que le interesan, lo que es mucho mejor que ejecutar una lista de descriptores de socket de 100k para ver si tuvieron eventos con máscaras de bits, lo que debe hacer si usa las otras soluciones.
Pero por el aumento en el rendimiento, pagamos un precio: este enfoque siguió un patrón de diseño completamente diferente al anterior. El enfoque anterior del sitio era (si no recuerdo mal) un proceso monolítico que bloqueaba la recepción y el envío; Estaba desarrollando una solución basada en eventos, por lo que también tuve que refactorizar el resto del código para que se ajustara a este nuevo modelo.
Específicamente, en nuestro nuevo enfoque, teníamos un ciclo principal, que manejaba la recepción y el envío de la siguiente manera:
- Los datos recibidos se pasaron (como mensajes) hasta la capa RTMP.
- Se diseccionó el RTMP y se extrajeron las etiquetas FLV.
- Los datos FLV se enviaban a la capa de almacenamiento en búfer y multidifusión, que organizaba los flujos y llenaba los búfer de bajo nivel del remitente.
- El remitente mantuvo una estructura para cada cliente, con un índice de último envío, y trató de enviar la mayor cantidad de datos posible al cliente.
Esta era una ventana móvil de datos e incluía algunas heurísticas para descartar cuadros cuando el cliente era demasiado lento para recibir. Las cosas funcionaron bastante bien.
Problemas a nivel de sistemas, arquitectura y hardware
Pero nos encontramos con otro problema: los cambios de contexto del núcleo se estaban convirtiendo en una carga. Como resultado, elegimos escribir solo cada 100 milisegundos, en lugar de hacerlo instantáneamente. Esto agregó los paquetes más pequeños y evitó una ráfaga de cambios de contexto.
Tal vez un problema mayor residía en el ámbito de las arquitecturas de servidor: necesitábamos un clúster con capacidad de equilibrio de carga y conmutación por error; perder usuarios debido a fallas en el servidor no es divertido. Al principio, optamos por un enfoque de director separado, en el que un "director" designado intentaría crear y destruir las transmisiones de las emisoras mediante la predicción de la demanda. Esto fracasó espectacularmente. De hecho, todo lo que intentamos falló sustancialmente. Al final, optamos por un enfoque relativamente de fuerza bruta de compartir emisoras entre los nodos del clúster de forma aleatoria, igualando el tráfico.
Esto funcionó, pero con un inconveniente: aunque el caso general se manejó bastante bien, vimos un rendimiento terrible cuando todos en el sitio (o una cantidad desproporcionada de usuarios) vieron una sola emisora. La buena noticia: esto nunca sucede fuera de una campaña de marketing. Implementamos un clúster separado para manejar este escenario, pero en realidad razonamos que poner en peligro la experiencia del usuario que paga por un esfuerzo de marketing no tenía sentido; de hecho, este no era realmente un escenario genuino (aunque hubiera sido bueno manejar cada imaginable). caso).
Conclusión
Algunas estadísticas del resultado final: el tráfico diario en el clúster fue de aproximadamente 100 000 usuarios en su punto máximo (60 % de carga), ~50 000 en promedio. Manejé dos clústeres (HUN y US); cada uno de ellos manejaba unas 40 máquinas para compartir la carga. El ancho de banda agregado de los clústeres fue de alrededor de 50 Gbps, de los cuales usaron alrededor de 10 Gbps durante la carga máxima. Al final, logré sacar 10 Gbps/máquina fácilmente; teóricamente 1 , este número podría haber llegado hasta los 30 Gbps/máquina, lo que se traduce en unos 300 000 usuarios que miran transmisiones simultáneamente desde un servidor.
El clúster FMS existente contenía más de 200 máquinas, que podrían haber sido reemplazadas por mis 15, de las cuales solo 10 harían algún trabajo real. Esto nos dio una mejora de aproximadamente 200/10 = 20x.
Probablemente, lo que más me llevé del proyecto de transmisión de video de Python fue que no debería dejarme detener por la perspectiva de tener que aprender un nuevo conjunto de habilidades. En particular, Python, la transcodificación y la programación orientada a objetos fueron conceptos con los que tuve una experiencia muy subprofesional antes de asumir este proyecto de video de multidifusión.
Eso, y que implementar su propia solución puede pagar mucho.
1 Más tarde, cuando pusimos el código en producción, nos encontramos con problemas de hardware, ya que usamos servidores Intel sr2500 más antiguos que no podían manejar tarjetas Ethernet de 10 Gbit debido a sus bajos anchos de banda PCI. En cambio, los usamos en enlaces Ethernet de 1-4x1 Gbit (agregando el rendimiento de varias tarjetas de interfaz de red en una tarjeta virtual). Eventualmente, obtuvimos algunos de los Intel sr2600 i7 más nuevos, que servían 10 Gbps a través de la óptica sin problemas de rendimiento. Todos los cálculos proyectados se refieren a este hardware.