Comment j'ai rendu le porno 20 fois plus efficace avec le streaming vidéo Python
Publié: 2022-03-11Introduction
Le porno est une grande industrie. Il n'y a pas beaucoup de sites sur Internet qui peuvent rivaliser avec le trafic de ses plus grands acteurs.
Et jongler avec cet immense trafic est rude. Pour rendre les choses encore plus difficiles, une grande partie du contenu diffusé à partir de sites pornographiques est constituée de flux vidéo en direct à faible latence plutôt que d'un simple contenu vidéo statique. Mais malgré tous les défis impliqués, j'ai rarement entendu parler des développeurs Python qui les relèvent. J'ai donc décidé d'écrire sur ma propre expérience au travail.
Quel est le problème?
Il y a quelques années, je travaillais pour le 26e (à l'époque) site Web le plus visité au monde, pas seulement l'industrie du porno : le monde.
À l'époque, le site servait des demandes de streaming de vidéos pornographiques avec le protocole de messagerie en temps réel (RTMP). Plus précisément, il a utilisé une solution Flash Media Server (FMS), conçue par Adobe, pour fournir aux utilisateurs des flux en direct. Le processus de base était le suivant :
- L'utilisateur demande l'accès à une diffusion en direct
- Le serveur répond par une session RTMP lisant le métrage souhaité
Pour plusieurs raisons, FMS n'était pas un bon choix pour nous, à commencer par ses coûts, qui comprenaient l'achat des deux :
- Licences Windows pour chaque machine sur laquelle nous avons exécuté FMS.
- ~ 4 000 $ de licences spécifiques à FMS, dont nous avons dû acheter plusieurs centaines (et plus chaque jour) en raison de notre taille.
Tous ces frais ont commencé à s'accumuler. Et les coûts mis à part, FMS était un produit qui manquait, en particulier dans sa fonctionnalité (plus à ce sujet dans un instant). J'ai donc décidé de supprimer FMS et d'écrire mon propre analyseur Python RTMP à partir de zéro.
Au final, j'ai réussi à rendre notre service environ 20 fois plus efficace.
Commencer
Il y avait deux problèmes principaux impliqués : premièrement, RTMP et d'autres protocoles et formats Adobe n'étaient pas ouverts (c'est-à-dire accessibles au public), ce qui les rendait difficiles à utiliser. Comment pouvez-vous inverser ou analyser des fichiers dans un format dont vous ne savez rien ? Heureusement, il y avait des efforts d'inversion disponibles dans la sphère publique (non produits par Adobe, mais plutôt par un groupe appelé OS Flash, aujourd'hui disparu) sur lesquels nous avons basé notre travail.
Remarque : Adobe a ensuite publié des « spécifications » qui ne contenaient pas plus d'informations que ce qui était déjà divulgué dans le wiki et les documents d'inversion non produits par Adobe. Leurs spécifications (d'Adobe) étaient d'une qualité absurdement basse et rendaient presque impossible l'utilisation réelle de leurs bibliothèques. De plus, le protocole lui-même semblait parfois intentionnellement trompeur. Par exemple:
- Ils ont utilisé des entiers de 29 bits.
- Ils incluaient partout des en-têtes de protocole au format big endian, à l'exception d'un champ spécifique (mais non marqué), qui était little endian.
- Ils ont comprimé les données dans moins d'espace au détriment de la puissance de calcul lors du transport d'images vidéo de 9 000, ce qui n'avait guère de sens, car ils récupéraient des bits ou des octets à la fois, des gains insignifiants pour une telle taille de fichier.
Et deuxièmement : RTMP est fortement orienté session, ce qui rend pratiquement impossible la multidiffusion d'un flux entrant. Idéalement, si plusieurs utilisateurs souhaitaient regarder le même flux en direct, nous pourrions simplement leur renvoyer des pointeurs vers une seule session au cours de laquelle ce flux est diffusé (il s'agirait d'un flux vidéo multidiffusion). Mais avec RTMP, nous avons dû créer une toute nouvelle instance du flux pour chaque utilisateur souhaitant y accéder. C'était un vrai gâchis.
Ma solution de streaming vidéo multicast
Dans cet esprit, j'ai décidé de reconditionner/analyser le flux de réponse typique dans des « balises » FLV (où une « balise » n'est que de la vidéo, de l'audio ou des métadonnées). Ces balises FLV pourraient voyager dans le RTMP sans problème.
Les avantages d'une telle approche :
- Nous n'avions besoin de reconditionner un flux qu'une seule fois (le reconditionnement était un cauchemar en raison du manque de spécifications et des bizarreries de protocole décrites ci-dessus).
- Nous pourrions réutiliser n'importe quel flux entre les clients avec très peu de problèmes en leur fournissant simplement un en-tête FLV, tandis qu'un pointeur interne vers les balises FLV (avec une sorte de décalage pour indiquer où ils se trouvent dans le flux) permettait l'accès à le contenu.
J'ai commencé le développement dans la langue que je connaissais le mieux à l'époque : C. Au fil du temps, ce choix est devenu lourd ; j'ai donc commencé à apprendre les bases de Python lors du portage de mon code C. Le processus de développement s'est accéléré, mais après quelques démos, j'ai rapidement rencontré le problème de l'épuisement des ressources. La gestion des sockets de Python n'était pas destinée à gérer ces types de situations : en particulier, en Python, nous nous sommes retrouvés à effectuer plusieurs appels système et changements de contexte par action, ce qui ajoutait une énorme surcharge.
Amélioration des performances de streaming vidéo : mélange de Python, RTMP et C
Après avoir profilé le code, j'ai choisi de déplacer les fonctions critiques pour les performances dans un module Python entièrement écrit en C. C'était des choses assez basiques : plus précisément, il utilisait le mécanisme epoll du noyau pour fournir un ordre de croissance logarithmique. .
Dans la programmation de sockets asynchrones, il existe des fonctionnalités qui peuvent vous fournir des informations sur le fait qu'un socket donné est lisible / inscriptible / rempli d'erreurs. Dans le passé, les développeurs utilisaient l'appel système select() pour obtenir ces informations, qui évoluent mal. Poll() est une meilleure version de select, mais ce n'est toujours pas génial car vous devez passer un tas de descripteurs de socket à chaque appel.

Epoll est incroyable car tout ce que vous avez à faire est d'enregistrer une prise et le système se souviendra de cette prise distincte, gérant tous les détails granuleux en interne. Il n'y a donc pas de surcharge de passage d'arguments à chaque appel. Il évolue également beaucoup mieux et ne renvoie que les sockets qui vous intéressent, ce qui est bien mieux que de parcourir une liste de descripteurs de socket 100k pour voir s'ils avaient des événements avec des masques de bits - ce que vous devez faire si vous utilisez les autres solutions.
Mais pour l'augmentation des performances, nous avons payé un prix : cette approche a suivi un modèle de conception complètement différent qu'auparavant. L'approche précédente du site était (si je me souviens bien) un processus monolithique qui bloquait la réception et l'envoi ; Je développais une solution événementielle, j'ai donc dû refactoriser le reste du code également pour l'adapter à ce nouveau modèle.
Plus précisément, dans notre nouvelle approche, nous avions une boucle principale, qui gérait la réception et l'envoi comme suit :
- Les données reçues ont été transmises (sous forme de messages) à la couche RTMP.
- Le RTMP a été disséqué et les balises FLV ont été extraites.
- Les données FLV étaient envoyées à la couche de mise en mémoire tampon et de multidiffusion, qui organisait les flux et remplissait les mémoires tampons de bas niveau de l'expéditeur.
- L'expéditeur a conservé une structure pour chaque client, avec un dernier index envoyé, et a essayé d'envoyer autant de données que possible au client.
Il s'agissait d'une fenêtre de données glissante et comprenait certaines heuristiques pour supprimer des images lorsque le client était trop lent à recevoir. Les choses ont plutôt bien fonctionné.
Problèmes au niveau du système, de l'architecture et du matériel
Mais nous nous sommes heurtés à un autre problème : les changements de contexte du noyau devenaient un fardeau. En conséquence, nous avons choisi d'écrire uniquement toutes les 100 millisecondes, plutôt qu'instantanément. Cela a regroupé les plus petits paquets et a empêché une rafale de changements de contexte.
Un problème plus important résidait peut-être dans le domaine des architectures de serveur : nous avions besoin d'un cluster capable d'équilibrer la charge et de basculement : perdre des utilisateurs en raison de dysfonctionnements du serveur n'est pas amusant. Au début, nous avons opté pour une approche de réalisateur séparé, dans laquelle un «réalisateur» désigné tentait de créer et de détruire les flux des diffuseurs en prédisant la demande. Cela a échoué de façon spectaculaire. En fait, tout ce que nous avons essayé a pratiquement échoué. En fin de compte, nous avons opté pour une approche relativement brutale consistant à partager les diffuseurs entre les nœuds du cluster de manière aléatoire, en égalisant le trafic.
Cela a fonctionné, mais avec un inconvénient : bien que le cas général ait été plutôt bien géré, nous avons constaté des performances terribles lorsque tout le monde sur le site (ou un nombre disproportionné d'utilisateurs) regardait un seul diffuseur. La bonne nouvelle : cela n'arrive jamais en dehors d'une campagne marketing. Nous avons implémenté un cluster séparé pour gérer ce scénario, mais en vérité, nous avons estimé que mettre en péril l'expérience de l'utilisateur payant pour un effort marketing était insensé - en fait, ce n'était pas vraiment un scénario authentique (bien qu'il aurait été agréable de gérer tous les scénarios imaginables Cas).
Conclusion
Quelques statistiques du résultat final : le trafic quotidien sur le cluster était d'environ 100 000 utilisateurs au pic (60 % de charge), ~ 50 000 en moyenne. J'ai géré deux clusters (HUN et US) ; chacun d'eux a manipulé environ 40 machines pour partager la charge. La bande passante agrégée des clusters était d'environ 50 Gbps, à partir de laquelle ils utilisaient environ 10 Gbps lors des pics de charge. Au final, j'ai réussi à sortir facilement 10 Gbps/machine ; théoriquement 1 , ce nombre aurait pu atteindre 30 Gbps/machine, ce qui se traduit par environ 300 000 utilisateurs regardant des flux simultanément à partir d'un serveur.
Le cluster FMS existant contenait plus de 200 machines, qui auraient pu être remplacées par mes 15, dont seulement 10 feraient un vrai travail. Cela nous a donné une amélioration d'environ 200/10 = 20x.
Ce que je retiens probablement le plus du projet de streaming vidéo Python, c'est que je ne devrais pas me laisser arrêter par la perspective de devoir acquérir de nouvelles compétences. En particulier, Python, le transcodage et la programmation orientée objet étaient tous des concepts avec lesquels j'avais une expérience très sous-professionnelle avant d'entreprendre ce projet de vidéo multidiffusion.
Cela, et le fait que lancer votre propre solution peut rapporter gros.
1 Plus tard, lorsque nous avons mis le code en production, nous avons rencontré des problèmes matériels, car nous utilisions des serveurs Intel sr2500 plus anciens qui ne pouvaient pas gérer les cartes Ethernet 10 Gbit en raison de leurs faibles bandes passantes PCI. Au lieu de cela, nous les avons utilisés dans des liaisons Ethernet 1-4x1 Gbit (regroupant les performances de plusieurs cartes d'interface réseau dans une carte virtuelle). Finalement, nous avons obtenu certains des nouveaux processeurs Intel sr2600 i7, qui servaient 10 Gbps sur l'optique sans aucun problème de performances. Tous les calculs projetés se réfèrent à ce matériel.