Comment déployer un refactoring dangereux pour produire avec un million d'utilisateurs?


Le film "Avion", 1980.

C'est comme ça que je me sentais quand j'ai versé un autre refactoring sur la prod. Même si vous couvrez tout le code avec des métriques et des journaux, testez la fonctionnalité sur tous les environnements - cela n'économisera pas 100% des fakaps après le déploiement.

First Fakap


Nous avons en quelque sorte refactorisé notre traitement de l'intégration avec Google Sheets. Pour les utilisateurs, il s'agit d'une fonctionnalité très utile, car ils utilisent de nombreux outils en même temps qui doivent être liés ensemble - envoyer des contacts à une table, télécharger des réponses aux questions, exporter des utilisateurs, etc.

Le code d'intégration n'a pas été refactorisé à partir de la première version et il est devenu de plus en plus difficile à maintenir. Cela a commencé à affecter nos utilisateurs - d'anciens bogues ont été révélés que nous avions peur d'éditer en raison de la complexité du code. Il est temps de faire quelque chose. Aucune modification logique n'était supposée - il suffit d'écrire des tests, de déplacer des classes et des noms de peigne. Bien sûr, nous avons testé la fonctionnalité sur l'environnement de développement et sommes allés déployer.

Après 20 minutes, les utilisateurs ont écrit que l'intégration ne fonctionnait pas. La fonctionnalité d'envoi de données à Google Sheet est tombée - il s'est avéré que pour le débogage, nous envoyons des données dans différents formats pour les ventes et les environnements locaux. Lors de la refactorisation, nous avons atteint le format de vente.

Nous avons fixé l'intégration, mais néanmoins, les sédiments du joyeux vendredi soir (et vous pensiez!) Sont restés. Rétrospectivement (rencontrer l'équipe pour terminer le sprint), nous avons commencé à réfléchir à la façon de prévenir de telles situations à l'avenir - nous devons améliorer la pratique des tests manuels, des auto-tests, travailler avec des métriques et des alarmes, et en plus de cela, nous avons eu l'idée d'utiliser des indicateurs de fonctionnalité pour tester la refactorisation sur prode, en fait, cela sera discuté.

la mise en oeuvre


Le schéma est simple: si l'utilisateur a le drapeau activé, passez au code avec la nouvelle version, sinon, au code avec l'ancienne version:

if ($user->hasFeature(UserFeatures::FEATURE_1)) {
  // new version
} else {
  // old version
}

Avec cette approche, nous avons la possibilité de tester la refactorisation sur prod d'abord sur nous-mêmes, puis de la verser aux utilisateurs.

Presque dès le début du projet, nous avons eu une implémentation primitive de la fonction drapeaux. Dans la base de données de deux entités de base, utilisateur et compte, des champs de fonctionnalités ont été ajoutés, qui étaient un masque de bits . Dans le code, nous avons enregistré de nouvelles constantes pour les fonctionnalités, que nous avons ensuite ajoutées au masque si une fonctionnalité spécifique devient disponible pour l'utilisateur.

public const ALLOW_FEATURE_1 = 0b0000001;
public const ALLOW_FEATURE_2 = 0b0000010;
public const ALLOW_FEATURE_3 = 0b0000100;

L'utilisation dans le code ressemblait à ceci:

If ($user->hasFeature(UserFeatures::ALLOW_FEATURE_1)) {
  // feature 1 logic
}

Lors de la refactorisation, nous ouvrons généralement le drapeau à l'équipe pour les tests, puis à plusieurs utilisateurs qui utilisent activement la fonctionnalité, et enfin ouverts à tous, mais parfois des schémas plus complexes apparaissent, plus à leur sujet ci-dessous.

Refactoring des lieux surchargés


L'un de nos systèmes accepte les webhooks Facebook et les traite via la file d'attente. Le traitement des files d'attente a cessé de fonctionner et les utilisateurs ont commencé à recevoir certains messages avec un retard, ce qui pourrait affecter de manière critique l'expérience des abonnés au bot. Nous avons commencé à refactoriser cet endroit en transférant le traitement vers un schéma de file d'attente plus complexe. L'endroit est critique - il est dangereux de verser une nouvelle logique sur tous les serveurs, nous avons donc fermé la nouvelle logique sous le drapeau et avons pu la tester sur la prod. Mais que se passe-t-il lorsque nous ouvrons ce drapeau? Comment se comportera notre infrastructure? Cette fois, nous avons déployé l'ouverture du drapeau sur les serveurs et suivi les métriques.

Tous les traitements de données critiques que nous avons divisés en clusters. Chaque cluster a un identifiant. Nous avons décidé de simplifier les tests d'une refactorisation aussi complexe en ouvrant la fonctionnalité de drapeau uniquement sur certains serveurs, la vérification dans le code ressemble à ceci:

If ($user->hasFeature(UserFeatures::CGT_REFACTORING) ||
    \in_array($cluster, Configurator::get('cgt_refactoring_cluster_ids'))) {
  // new version
} else {
  // old version
}

Tout d'abord, nous avons versé du refactoring et ouvert les drapeaux à l'équipe. Ensuite, nous avons trouvé plusieurs utilisateurs qui utilisaient activement la fonction cgt, leur ouvraient des drapeaux et cherchaient si tout fonctionnait pour eux. Et enfin, ils ont commencé à ouvrir des drapeaux sur les serveurs et à suivre les métriques.

L'indicateur cgt_refactoring_cluster_ids peut être modifié via le panneau d'administration. Initialement, nous attribuons la valeur cgt_refactoring_cluster_ids à un tableau vide, puis ajoutons un cluster à la fois - [1], examinons les métriques pendant un certain temps et ajoutons un autre cluster - [1, 2] jusqu'à ce que nous testions l'ensemble du système.

Implémentation du configurateur


Je vais parler un peu de ce qu'est le configurateur et de la façon dont il est implémenté. Il a été écrit pour pouvoir changer la logique sans déploiement, par exemple, comme dans le cas ci-dessus, lorsque nous devons faire reculer la logique. Nous l'utilisons également pour les configurations dynamiques, par exemple, lorsque vous devez tester différents temps de mise en cache, vous pouvez le retirer pour un test rapide. Pour le développeur, cela ressemble à une liste de champs avec des valeurs d'administration qui peuvent être modifiées. Nous stockons tout cela dans une base de données, nous mettons en cache dans Redis et dans une statique pour nos travailleurs.

Refactorisation d'emplacements obsolètes


Au cours du prochain trimestre, nous avons refondu la logique d'inscription, la préparant à la transition vers la possibilité d'inscription via plusieurs services. Dans nos conditions, il est impossible de regrouper la logique d'enregistrement afin qu'un certain utilisateur soit lié à une certaine logique, et nous n'avons rien trouvé de mieux que de tester la logique, en déployant un pourcentage de toutes les demandes d'enregistrement. Ceci est facile à faire d'une manière similaire avec des drapeaux:

If (Configurator::get('auth_refactoring_percentage') > \random_int(0, 99)) {
  // new version
} else {
  // old version
}

En conséquence, nous avons défini la valeur de auth_refactoring_percentage dans le panneau d'administration de 0 à 100. Bien sûr, nous avons «étalé» toute la logique d'autorisation avec des métriques afin de comprendre que nous n'avons pas réduit la conversion à la fin.

Métrique


Pour dire comment nous suivons les métriques dans le processus d'ouverture des indicateurs, nous examinerons un autre cas plus en détail. ManyChat accepte les hooks Facebook de Facebook lorsqu'un abonné envoie un message à Facebook Messenger. Nous devons traiter chaque message conformément à la logique métier. Pour la fonctionnalité cgt, nous devons déterminer si l'abonné a commencé la conversation via un commentaire sur Facebook afin de lui envoyer un message pertinent en réponse. Dans le code, cela ressemble à déterminer le contexte de l'abonné actuel, si nous pouvons déterminer le widgetId, alors nous en déterminons le message de réponse.

En savoir plus sur la fonctionnalité
Facebook api. — . Widget, :

—> —> —> Facebook:



:
—> —>



“ , !” , . , “ !” id , — , id.

Auparavant, nous avons défini le contexte de 3 façons, il ressemblait à ceci:

function getWidgetIdContext(User $user, WatchService $watcher): int?
{
  //      
  if (null !== $user->gt_widget_id_context) {
    $watcher->logTick('cgt_match_processor_matched_via_context');

    return $user->gt_widget_id_context;
  }

  //      
  if (null !== $user->name) {
    $widgetId = $this->cgtMatchByThread($user);
    if (null !== $widgetId) {
      $watcher->logTick('cgt_match_processor_matched_via_thread');

      return $widgetId;
    }

    $widgetId = $this->cgtMatchByConversation($user);
    if (null !== $widgetId) {
      $watcher->logTick('cgt_match_processor_matched_via_conversation');

      return $widgetId;
    }
  }

  return null;
}

Le service de surveillance envoie des analyses au moment de la correspondance, respectivement, nous avions des mesures pour les trois cas: le


nombre de fois où le contexte a été trouvé par différentes méthodes de liaison dans le temps.

Ensuite, nous avons trouvé une autre méthode de correspondance qui devrait remplacer toutes les anciennes options. Pour tester cela, nous avons obtenu une autre métrique:

function getWidgetIdContext(User $user, WatchService $watcher): int?
{
  //    
  $widgetId = $this->cgtMatchByEcho($user);
  
  if (null !== $widgetId) {
      $watcher->logTick('cgt_match_processor_matched_via_echo_message');
  }

  //    
  // ...
}

À ce stade, nous voulons nous assurer que le nombre de nouveaux hits est égal à la somme des anciens hits, il suffit donc d'écrire la métrique sans retourner $ widgetId: le


nombre de contextes trouvés par la nouvelle méthode couvre complètement la somme des liaisons par les anciennes méthodes

mais cela ne nous garantit pas la logique de correspondance correcte dans tous les cas. L'étape suivante consiste à tester progressivement à travers l'ouverture des drapeaux:

function getWidgetIdContext(User $user, WatchService $watcher): int?
{
  //    
  $widgetId = $this->cgtMatchByEcho($user);
  
  if (null !== $widgetId) {
    $watcher->logTick('cgt_match_processor_matched_by_echo_message');
  
    //    ,   
    If ($this->allowMatchingByEcho($user)) {
      return $widgetId;
    }
  }

  // ...
}

function allowMatchingByEcho(User $user): bool
{
  //    
  If ($user->hasFeature(UserFeatures::ALLOW_CGT_MATCHING_BY_ECHO)) {
    return true;
  }
  //     
  If (\in_array($this->clusterId, Configurator::get('cgt_matching_by_echo_cluster_ids'))) {
    return true;
  }

  return false;
}

Ensuite, le processus de test a commencé: au début, nous avons testé la nouvelle fonctionnalité par nous-mêmes sur tous les environnements et sur des utilisateurs aléatoires qui utilisent souvent la correspondance en ouvrant l'indicateur UserFeatures :: ALLOW_CGT_MATCHING_BY_ECHO. À ce stade, nous avons détecté quelques cas où le match a mal fonctionné et les avons réparés. Puis ils ont commencé à se déployer sur les serveurs: en moyenne, nous avons déployé un serveur en 1 jour au cours de la semaine. Avant les tests, nous avertissons le support qu'ils examinent attentivement les tickets liés à la fonctionnalité et nous écrivent sur toute anomalie. Grâce au support et aux utilisateurs, plusieurs cas d'angle ont été corrigés. Et enfin, la dernière étape est la découverte de tous sans condition:

function getWidgetIdContext(User $user, WatchService $watcher): int?
{
  $widgetId = $this->cgtMatchByEcho($user);
  
  if (null !== $widgetId) {
    $watcher->logTick('cgt_match_processor_matched_by_echo_message');
  
    return $widgetId;
  }

  return null;
}

Implémentation de la nouvelle fonctionnalité de drapeau


La mise en œuvre de la fonctionnalité de drapeau décrite au début de l'article nous a servi pendant environ 3 ans, mais avec la croissance des équipes, cela est devenu inconfortable - nous avons dû déployer lors de la création de chaque drapeau et ne pas oublier d'effacer la valeur des drapeaux (nous avons réutilisé des valeurs constantes pour différentes fonctionnalités). Récemment, le composant a été réécrit et nous pouvons maintenant gérer de manière flexible les indicateurs via le panneau d'administration. Les drapeaux ont été détachés du masque binaire et stockés dans une table distincte, ce qui facilite la création de nouveaux drapeaux. Chaque entrée a également une description et un propriétaire, la gestion des drapeaux est devenue plus transparente.

Inconvénients de ces approches


Cette approche a un gros inconvénient - il existe deux versions du code et elles doivent être prises en charge en même temps. Lors des tests, vous devez considérer qu'il existe deux branches de la logique et que vous devez toutes les vérifier, ce qui est très douloureux. Au cours du développement, il y a eu des situations où nous avons introduit un correctif dans une logique, mais en avons oublié une autre et à un moment donné, il a tourné. Par conséquent, nous n'appliquons cette approche que dans des endroits critiques et essayons de nous débarrasser de l'ancienne version du code le plus rapidement possible. Nous essayons de faire le reste du refactoring en petites itérations.

Total


Le processus actuel ressemble à ceci - nous fermons d'abord la logique sous la condition des drapeaux, puis déployons et commençons à ouvrir progressivement les drapeaux. Lors de l'expansion des indicateurs, nous surveillons étroitement les erreurs et les mesures, dès qu'un problème se produit - annulez immédiatement l'indicateur et résolvez le problème. Le plus est que l'ouverture / fermeture du drapeau est très rapide - c'est juste un changement de valeur dans le panneau d'administration. Après un certain temps, nous avons supprimé l'ancienne version du code, ce devrait être le temps minimum pour empêcher les modifications dans les deux versions du code. Il est important d'avertir ses collègues de ce refactoring. Nous effectuons un examen via github et utilisons des propriétaires de code lors d'une telle refactorisation afin que les modifications ne pénètrent pas dans le code à l'insu de l'auteur du refactoring.

Plus récemment, j'ai déployé une nouvelle version de l'API Facebook Graph. En une seconde, nous faisons plus de 3000 requêtes à l'API et toute erreur nous coûte cher. Par conséquent, j'ai déployé le changement sous le drapeau avec un impact minimal - il s'est avéré attraper un bug désagréable, tester la nouvelle version et finalement y basculer complètement sans soucis.

All Articles