Je m'appelle Danil Mukhametzyanov et je travaille en tant que développeur backend chez Badoo depuis sept ans. Pendant ce temps, j'ai réussi à créer et à modifier une grande quantité de code. Tellement grand qu'un jour un manager est venu me voir et m'a dit: «Le quota est terminé. Pour ajouter quelque chose, vous devez supprimer quelque chose. "
D'accord, c'est juste une blague - il n'a pas dit ça. C'est dommage! Au cours de toute l'existence de l'entreprise, Badoo a accumulé plus de 5,5 millions de lignes de code métier logique, à l'exclusion des lignes vides et des parenthèses fermantes.
La quantité elle-même n'est pas si effrayante: il ment, ne demande pas de nourriture. Mais il y a deux ou trois ans, j'ai commencé à remarquer que je lis de plus en plus souvent et que j'essaie de trouver du code qui ne fonctionne pas vraiment dans un environnement de production. C'est, en fait, mort.
Cette tendance a été remarquée non seulement par moi. Badoo s'est rendu compte que nos ingénieurs hautement rémunérés perdent constamment du temps avec du code mort.
J'ai donné cette conférence au Badoo PHP Meetup # 4
D'où vient le code mort?
Nous avons commencé à chercher les causes des problèmes. Les diviser en deux catégories:
- processus - ceux qui résultent du développement;
- historique - code hérité.
Tout d'abord, nous avons décidé de désassembler les sources de processus afin d'éviter l'apparition de nouveaux problèmes.
Test A / B
Badoo a commencé à utiliser activement les tests A / B il y a quatre ans. Nous avons maintenant environ 200 tests en cours d'exécution, et toutes les fonctionnalités du produit passent par cette procédure.
En conséquence, environ 2000 tests terminés ont été accumulés en quatre ans, et ce chiffre est en constante augmentation. Elle nous a fait peur que chaque test soit un morceau de code mort qui n'est plus exécuté et n'est pas du tout nécessaire.
La solution au problème est venue rapidement: nous avons commencé à créer automatiquement un ticket pour découper le code à la fin du test A / B.
Un exemple de ticket
Mais le facteur humain se déclenchait périodiquement. À maintes reprises, nous avons trouvé le code de test qui a continué à fonctionner, mais personne n'y a pensé et a terminé le test.
Ensuite, il y avait un cadre rigide: chaque test doit avoir une date de fin. Si le responsable oublie de lire les résultats du test, il s'arrête et s'éteint automatiquement. Et, comme je l'ai déjà mentionné, un ticket a été automatiquement créé pour le découper tout en conservant la version originale de la logique de la fonctionnalité.
Avec l'aide d'un mécanisme aussi simple, nous nous sommes débarrassés d'une grande couche de travail.
Diversité des clients
Plusieurs marques sont prises en charge dans notre entreprise, mais le serveur en est un. Chaque marque est représentée sur trois plateformes: web, iOS et Android. Sur iOS et Android, nous avons un cycle de développement hebdomadaire: une fois par semaine, avec une mise à jour, nous recevons une nouvelle version de l'application sur chaque plateforme.
Il est facile de deviner qu'avec cette approche, en un mois, nous avons environ une douzaine de nouvelles versions qui doivent être prises en charge. Le trafic des utilisateurs est inégalement réparti entre eux: les utilisateurs passent progressivement d'une version à une autre. Certaines versions plus anciennes ont du trafic, mais il est si petit qu'il est difficile de le maintenir. C'est dur et inutile.
Nous avons donc commencé à compter le nombre de versions que nous souhaitons prendre en charge. Deux limites sont apparues pour le client: soft limit et hard limit.
Lorsque la limite souple est atteinte (lorsque trois ou quatre nouvelles versions ont déjà été publiées et que l'application n'est toujours pas mise à jour), l'utilisateur voit un écran avec un avertissement indiquant que sa version est obsolète. Lorsque la limite stricte est atteinte (il s'agit d'environ 10-20 versions "manquées", selon l'application et la marque), nous supprimons simplement l'option de sauter cet écran. Cela devient bloquant: vous ne pouvez pas utiliser l'application avec.
Screen for the hard limit
Dans ce cas, il est inutile de continuer à traiter les requêtes provenant du client - il ne verra rien d'autre qu'un écran.
Mais ici, comme dans le cas des tests A / B, une nuance est apparue. Les développeurs clients sont aussi des personnes. Ils utilisent de nouvelles technologies, des puces de systèmes d'exploitation - et après un certain temps, la version de l'application n'est plus prise en charge sur la prochaine version du système d'exploitation. Cependant, le serveur continue de souffrir car il doit continuer à traiter ces demandes.
Nous avons proposé une solution distincte pour le cas lorsque la prise en charge de Windows Phone a pris fin. Nous avons préparé un écran qui informait l'utilisateur: «Nous vous aimons beaucoup! Tu es très cool! Mais pouvez-vous commencer à utiliser une autre plateforme? De nouvelles fonctions intéressantes seront disponibles pour vous, mais ici nous ne pouvons rien faire. " En règle générale, nous proposons une plate-forme Web comme plate-forme alternative, qui est toujours disponible.
Avec un mécanisme aussi simple, nous avons limité le nombre de versions client que le serveur prend en charge: environ 100 versions différentes de toutes les marques, de toutes les plates-formes.
Indicateurs de fonctionnalité
Cependant, en désactivant la prise en charge des anciennes plates-formes, nous n'avons pas pleinement compris s'il était possible de supprimer complètement le code qu'elles utilisaient. Ou les plates-formes qui restent pour les anciennes versions de système d'exploitation continuent d'utiliser la même fonctionnalité?
Le problème est que notre API n'a pas été construite sur la partie versionnée, mais sur l'utilisation d'indicateurs de fonctionnalités. Comment nous en sommes arrivés à cela, vous pouvez le découvrir dans ce rapport .
Nous avions deux types d'indicateurs de fonctionnalités. Je vais vous en parler avec des exemples.
Caractéristiques mineures
Le client dit au serveur: «Bonjour, c'est moi. Je soutiens les publications de photos. " Le serveur le regarde et répond: «Super, support! Maintenant, je suis au courant et je vais vous envoyer des messages photo. " La caractéristique clé ici est que le serveur ne peut en aucun cas influencer le client - il accepte simplement les messages de celui-ci et est obligé de les écouter.
Nous appelons ces indicateurs des fonctionnalités mineures. À l'heure actuelle, nous en avons plus de 600.
Quel est l'inconvénient d'utiliser ces drapeaux? Périodiquement, il existe des fonctionnalités lourdes qui ne peuvent pas être couvertes uniquement du côté client - vous souhaitez également les contrôler du côté serveur. Pour cela, nous avons introduit d'autres types de drapeaux.
Fonctionnalités de l'application
Le même client, le même serveur. Le client déclare: «Serveur, j'ai appris à prendre en charge le streaming vidéo. Allume ça? " Le serveur répond: "Merci, je vais garder cela à l'esprit." Et il ajoute: «Super. Montrons cette fonctionnalité à notre utilisateur bien-aimé, il sera heureux. " Ou: "Ok, mais nous ne l'inclurons pas encore."
Nous appelons ces fonctionnalités des fonctionnalités d'application. Ils sont plus lourds, donc nous en avons moins, mais encore assez: plus de 300.
Ainsi, les utilisateurs passent d'une version du client à une autre. Une sorte d'indicateur commence à être prise en charge par toutes les versions actives des applications. Ou, au contraire, non pris en charge. Il n'est pas tout à fait clair comment contrôler cela: 100 versions de client, 900 indicateurs! Pour faire face à cela, nous avons construit un tableau de bord.
Un carré rouge dessus signifie que toutes les versions de cette plate-forme ne prennent pas en charge cette fonctionnalité; vert - toutes les versions de cette plate-forme prennent en charge ce drapeau. Si le drapeau peut être activé et désactivé, il clignotera périodiquement. Nous pouvons voir ce qui se passe dans quelle version.
Écran du tableau de bord
Dans cette interface, nous avons commencé à créer des tâches pour supprimer les fonctionnalités. Il convient de noter que toutes les cellules rouges ou vertes de chaque ligne ne doivent pas être remplies. Il existe des indicateurs qui ne fonctionnent que sur une seule plateforme. Certains indicateurs ne sont remplis que pour une seule marque.
L'automatisation du processus n'est pas si pratique, mais, en principe, ce n'est pas nécessaire - il vous suffit de définir une tâche et de consulter périodiquement le tableau de bord. Dans la première itération, nous avons réussi à découper plus de 200 drapeaux. C'est presque un quart des drapeaux que nous avons utilisés!
C'est là que les sources de processus se sont terminées. Ils sont apparus à la suite de notre flux de développement, et nous avons réussi à intégrer le travail avec eux dans ce processus.
Que faire avec le code hérité
Nous avons arrêté l'émergence de nouveaux problèmes dans les sources de processus. Et nous avons été confrontés à une question difficile: que faire du code hérité accumulé au fil des ans? Nous avons abordé la solution du point de vue de l'ingénierie, c'est-à-dire que nous avons décidé de tout automatiser. Mais il n'était pas clair comment trouver le code qui n'était pas utilisé. Il s'est caché dans son petit monde douillet: il n'appelle en aucun cas, ne laisse personne se connaître.
Nous devions passer de l'autre côté: prendre tout le code que nous avions, collecter des informations sur les pièces exactement exécutées, puis faire l'inversion.
Ensuite, nous l'avons assemblé et mis en œuvre au niveau le plus minimal - sur des fichiers. De cette façon, nous pourrions facilement obtenir une liste de fichiers à partir du référentiel en exécutant la commande UNIX appropriée.
Il restait à rassembler une liste des fichiers utilisés en production. C'est assez simple: pour chaque requête à l'arrêt, appelez la fonction PHP correspondante. La seule optimisation que nous avons faite ici est de commencer à demander OPCache au lieu de demander chaque demande. Sinon, la quantité de données serait très importante.
En conséquence, nous avons découvert de nombreux artefacts intéressants. Mais avec une analyse plus approfondie, nous nous sommes rendu compte qu'il nous manquait des méthodes inutilisées: la différence de leur nombre était de trois à sept fois.
Il s'est avéré que le fichier pouvait être chargé, exécuté, compilé pour le bien d'une seule constante ou d'une paire de méthodes. Tout le reste restait inutile pour se trouver dans cette mer sans fond.
Préparer une liste de méthodes
Cependant, il s'est avéré assez rapidement pour rassembler une liste complète des méthodes. Nous avons juste pris l'analyseur de Nikita Popov , lui avons alimenté notre référentiel et obtenu tout ce que nous avons dans le code.
La question demeure: comment assembler ce qui se joue en production? Nous nous intéressons à la production, car les tests peuvent couvrir ce dont nous n'avons pas du tout besoin. Sans réfléchir à deux fois, nous avons pris XHProf. Il a déjà été exécuté en production pour une partie des requêtes, et nous avons donc des échantillons de profil qui sont stockés dans les bases de données. Il suffisait d'aller dans ces bases de données, d'analyser les instantanés générés - et d'obtenir une liste de fichiers.
Inconvénients de XHProf
Nous avons répété ce processus sur un autre cluster où XHProf n'a pas démarré, mais était absolument nécessaire. Il s'agit d'un cluster pour exécuter des scripts d'arrière-plan et un traitement asynchrone, ce qui est important pour une charge élevée, il exécute beaucoup de logique.
Et puis nous nous sommes assurés que XHProf ne nous gêne pas.
- Cela nécessite une modification du code PHP. Vous devez insérer le code de démarrage du traçage, terminer le traçage, récupérer les données collectées, les écrire dans un fichier. Après tout, il s'agit d'un profileur, mais nous avons de la production, c'est-à-dire qu'il y a beaucoup de demandes, vous devez également penser à l'échantillonnage. Dans notre cas, cela a été aggravé par un grand nombre de clusters avec des points d'entrée différents.
- . . , OPCache. : XHProf, . , core- .
- . . XHProf . ( XHProf): CPU, , . , , . - XHProf aggregator ( XHProf Live Profiler, open-source) , , , . , : «, , », CPU , , Live Profiler . , , .
- XHProf. , . . , . : , ( , youROCK, cela n'est pas requis par lsd , mais il était plus pratique de conserver un seul wrapper dessus). Corriger XHProf n'est pas ce que nous voulions faire, car il s'agit d'un profileur assez volumineux (et si nous cassons quelque chose par inadvertance?).
Il y avait une autre idée - exclure certains espaces de noms, par exemple, les espaces de noms de fournisseurs du composeur, qui sont exécutés en production, car ils sont inutiles: nous ne refactoriserons pas les packages de fournisseurs et ne supprimerons pas le code inutile.
Exigences de la solution
Nous nous sommes réunis à nouveau et avons examiné les solutions existantes. Et ils ont formulé la liste finale des exigences.
Premièrement: frais généraux minimes. Pour nous, XHProf était la barre: pas plus que nécessaire.
Deuxièmement, nous ne voulions pas changer le code PHP.
Troisièmement, nous voulions que la solution fonctionne partout - à la fois dans FPM et dans la CLI.
Quatrièmement, nous voulions gérer les fourches. Ils sont activement utilisés en CLI, sur des serveurs cloud. Je ne voulais pas leur faire de logique spécifique dans PHP.
Cinquième: échantillonnage hors de la boîte. En fait, cela découle de l'exigence de ne pas changer le code PHP. Ci-dessous, j'expliquerai pourquoi nous avions besoin d'échantillonnage.
Sixième et dernier:la capacité de forcer à partir du code. Nous adorons quand tout fonctionne automatiquement, mais il est parfois plus pratique de démarrer, d'ajuster, de regarder manuellement. Nous avions besoin de la possibilité d'activer et de désactiver tout directement à partir du code, et non par décision aléatoire du mécanisme plus général du module PHP, qui définit la probabilité d'inclusion via les paramètres.
Comment fonctionne funcmap
En conséquence, nous avons une solution que nous appelons funcmap.
Funcmap est essentiellement une extension PHP. En termes PHP, il s'agit d'un module PHP. Pour comprendre comment cela fonctionne, examinons le fonctionnement du processus PHP et du module PHP.
Alors, vous démarrez un processus. PHP permet de s'abonner à des hooks lors de la construction d'un module. Le processus démarre, le hook GINIT (Global Init) est lancé, où vous pouvez initialiser les paramètres globaux. Ensuite, le module est initialisé. Des constantes peuvent y être créées et allouées, mais uniquement pour un module spécifique, et non pour une demande, sinon vous vous tirerez une balle dans le pied.
Ensuite, la demande de l'utilisateur arrive, le hook RINIT (Request Init) est appelé. Lorsque la demande est terminée, son arrêt se produit, et à la toute fin - l'arrêt du module: MSHUTDOWN et GSHUTDOWN. Tout est logique.
Si nous parlons de FPM, alors chaque demande d'utilisateur arrive à un worker déjà existant. Fondamentalement, RINIT et RSHUTDOWN travaillent en rond jusqu'à ce que FPM décide que le travailleur est dépassé, il est temps de lui tirer dessus et d'en créer un nouveau. Si nous parlons de la CLI, alors ce n'est qu'un processus linéaire. Tout sera appelé une fois.
Comment fonctionne funcmap
Sur cet ensemble, nous nous sommes intéressés à deux crochets. Le premier est RINIT . Nous avons commencé à définir l'indicateur de collecte de données: c'est une sorte d'aléatoire qui a été appelée pour échantillonner les données. Si cela fonctionnait, nous avons traité cette demande: nous avons collecté des statistiques sur les appels aux fonctions et les méthodes correspondantes. Si cela n'a pas fonctionné, la demande n'a pas été traitée.
La prochaine chose à faire est de créer une table de hachage si elle n'existe pas. La table de hachage est fournie en interne par PHP lui-même. Il n'est pas nécessaire d'inventer quoi que ce soit ici - prenez-le et utilisez-le.
Ensuite, nous initialisons la minuterie. Je vais parler de lui ci-dessous, pour l'instant, rappelez-vous simplement qu'il est important et nécessaire.
Le deuxième hook est MSHUTDOWN... Je tiens à noter que c'est MSHUTDOWN, pas RSHUTDOWN. Nous ne voulions pas trouver quelque chose pour chaque demande - nous nous intéressions à l'ensemble du travailleur. Sur MSHUTDOWN, nous prenons notre table de hachage, la parcourons et écrivons un fichier (quoi de plus fiable, pratique et polyvalent que le bon vieux fichier?).
La table de hachage est remplie tout simplement par le même hook PHP zend_execute_ex, qui est appelé à chaque fois qu'une fonction définie par l'utilisateur est appelée. L'enregistrement contient des paramètres supplémentaires grâce auxquels vous pouvez comprendre de quel type de fonction il s'agit, son nom et sa classe. Nous l'acceptons, lisons le nom, l'écrivons dans la table de hachage, puis appelons le hook par défaut.
Ce hook n'écrit pas de fonctions en ligne. Si vous souhaitez remplacer les fonctions intégrées, il existe une fonctionnalité distincte pour celle appelée zend_execute_internal.
Configuration
Comment puis-je configurer cela sans changer le code PHP? Les réglages sont très simples:
- enabled: s'il est activé ou non.
- Le fichier dans lequel nous écrivons. Il existe un espace réservé pid pour exclure une condition de concurrence lorsque différents processus PHP écrivent dans le même fichier en même temps.
- Base de probabilité: notre indicateur de probabilité. Si vous le définissez sur 0, aucune requête ne sera écrite; si 100 - alors toutes les demandes seront enregistrées et incluses dans les statistiques.
- flush_interval. Il s'agit de la fréquence à laquelle nous vidons toutes les données dans un fichier. Nous voulons que la collecte de données soit exécutée dans la CLI, mais il existe des scripts qui peuvent prendre beaucoup de temps à exécuter, consommant de la mémoire si vous utilisez une grande quantité de fonctionnalités.
De plus, si nous avons un cluster moins chargé, FPM comprend que le worker est prêt à traiter davantage et ne tue pas le processus - il vit et consomme une partie de la mémoire. Après un certain temps, nous vidons tout sur le disque, réinitialisons la table de hachage et recommençons à la remplir. Si, cependant, le timeout n'est pas encore atteint, alors le hook MSHUTDOWN est déclenché, où nous écrivons tout finalement.
La dernière chose que nous voulions était la possibilité d'appeler funcmap à partir de code PHP. L'extension correspondante fournit la seule méthode qui vous permet d'activer ou de désactiver la collecte de statistiques quel que soit le fonctionnement de la probabilité.
Les frais généraux
Nous nous sommes demandé comment tout cela affecte nos serveurs. Nous avons construit un graphique qui montre le nombre de requêtes provenant d'une vraie machine de combat de l'un des clusters PHP les plus chargés.
Il peut y avoir de nombreuses machines de ce type, donc le graphique montre le nombre de requêtes, pas le CPU. L'équilibreur se rend compte que la machine a commencé à consommer plus de ressources que d'habitude et tente d'égaliser les demandes afin que les machines soient chargées uniformément. C'était suffisant pour comprendre à quel point le serveur est dégradé.
Nous avons activé notre extension de manière séquentielle à 25%, 50% et 100% et avons vu l'image suivante:
La ligne pointillée représente le nombre de demandes que nous attendons. La ligne principale est le nombre de demandes reçues. Nous avons constaté une dégradation d'environ 6%, 12% et 23%: ce serveur a commencé à traiter près d'un quart de demandes entrantes en moins.
Ce graphique prouve tout d'abord que l'échantillonnage est important pour nous: nous ne pouvons pas consacrer 20% des ressources du serveur à la collecte de statistiques.
Faux résultat
L'échantillonnage a un effet secondaire: certaines méthodes ne sont pas incluses dans les statistiques, mais sont en fait utilisées. Nous avons essayé de lutter contre cela de plusieurs manières:
- . -, . , , , , .
- . , : , , .
Nous avons essayé deux solutions pour la gestion des erreurs. La première consiste à activer la collecte de statistiques de force à partir du moment où l'erreur a été générée: collectez le journal des erreurs et analysez-le. Mais il y a un piège ici: lorsqu'une ressource tombe, le nombre d'erreurs augmente instantanément. Vous commencez à les traiter, il y a beaucoup plus de travailleurs - et le cluster commence à mourir lentement. Par conséquent, ce n'est pas tout à fait correct.
Comment faire autrement? Nous avons lu et, à l'aide de l'analyseur de Nikita Popov, nous sommes passés par les enjeux, en notant quelles méthodes y sont appelées. Ainsi, nous avons éliminé la charge sur le serveur et réduit le nombre de faux positifs.
Mais il y avait encore des méthodes qui étaient rarement appelées et dont on ne savait pas si elles étaient nécessaires ou non. Nous avons ajouté une aide qui aide à déterminer le fait d'utiliser de telles méthodes: si l'échantillonnage a déjà montré que la méthode est rarement appelée, alors vous pouvez activer le traitement à 100% et ne pas penser à ce qui se passe. Toute exécution de cette méthode sera enregistrée. Vous le saurez.
Si vous savez avec certitude que la méthode est utilisée, cela peut être excessif. C'est peut-être une fonctionnalité nécessaire, mais rare. Imaginez que vous ayez l'option «Se plaindre», qui est rarement utilisée, mais qui est importante - vous ne pouvez pas la supprimer. Pour de tels cas, nous avons appris à étiqueter manuellement ces méthodes.
Nous avons créé une interface qui montre quelles méthodes sont utilisées (elles sont sur fond blanc) et lesquelles ne sont potentiellement pas utilisées (elles sont sur fond rouge). Ici, vous pouvez également marquer les méthodes nécessaires.
Écran d'interface
L'interface est excellente, mais revenons au début, qui est le problème que nous résolvions. Cela consistait en ce que nos ingénieurs lisaient du code mort. Où le lisent-ils? Dans l'IDE. Imaginez ce que ce serait de forcer un fan de son métier à quitter le monde IDE dans une sorte d'interface Web et à y faire quelque chose! Nous avons décidé que nous devions rencontrer nos collègues à mi-chemin.
Nous avons créé un plugin pour PhpStorm qui charge toute la base de données des méthodes inutilisées et affiche si cette méthode est utilisée ou non. De plus, vous pouvez marquer la méthode comme étant utilisée dans l'interface. Tout cela ira au serveur et deviendra disponible pour le reste des contributeurs de la base de code.
Ceci conclut l'essentiel de notre travail avec Legacy. Nous avons commencé à remarquer plus rapidement que nous n'exécutions pas, à réagir plus rapidement et à ne pas perdre de temps à rechercher manuellement du code inutilisé.
L'extension funcmap est disponible sur GitHub . Nous serons heureux si cela est utile à quelqu'un.
Alternatives
De l'extérieur, il peut sembler que chez Badoo, nous ne savons pas quoi faire de nous-mêmes. Pourquoi ne pas jeter un œil à ce qui est sur le marché?
C'est une bonne question. Nous avons cherché - et il n'y avait rien sur le marché à ce moment-là. Ce n'est que lorsque nous avons commencé à implémenter activement notre solution que nous avons découvert qu'au même moment, un homme du nom de Joe Watkins, qui vit dans le brouillard britannique, a mis en œuvre une idée similaire et créé l'extension Tombs.
Nous ne l'avons pas étudié très attentivement, car nous avions déjà notre propre solution, mais nous avons néanmoins trouvé plusieurs problèmes:
- Manque d'échantillonnage. Ci-dessus, j'ai expliqué pourquoi nous en avons besoin.
- . , APCu ( ), .
- CLI. , , CLI-, .
- . Tombs, , , , , , . funcmap («» , ): , . Tombs , , FPM CLI. - , .
Tout d'abord, réfléchissez à la manière dont vous allez supprimer les fonctionnalités implémentées pendant une courte période, surtout si le développement est très actif. Dans notre cas, il s'agissait de tests A / B. Si vous n'y pensez pas à l'avance, vous devrez nettoyer les gravats.
Deuxièmement: connaissez vos clients à vue. Peu importe qu'ils soient internes ou externes, vous devez les connaître. À un moment donné, vous devez leur dire: «Cher, arrêtez! Non".
Troisièmement: nettoyez votre API. Cela conduit à une simplification de l'ensemble du système.
Et quatrièmement: vous pouvez tout automatiser, même la recherche de code mort. C'est ce que nous avons fait.