Mise en cache. Partie 2: 60 jours avant la sortie

salut! Je vous ai déjà écrit sur la façon de promouvoir les initiatives dans une entreprise. Plus précisément, comment (parfois) cela réussit et quelles difficultés peuvent survenir: une rétrospective de râteau. Comment une solution self-made s'est avérée plus cool qu'une solution payante et Comment nous avons choisi un système de mise en cache. Partie 1 .

Aujourd'hui, je veux continuer et parler du moment psychologiquement le plus stressant de ce projet, dont les deux premiers articles - lorsque le résultat du projet a été déterminé non pas tant par les compétences techniques de l'équipe que par la confiance dans ses calculs et la volonté d'aller jusqu'au bout.

Je dois dire - je pense que pour amener le projet à un moment aussi intense - c'est une erreur bien utilisée à propos de lshaya que tout héroïsme en étirant le projet de ce problème ...
Mais, je ne cache pas cette expérience et la partage volontiers - car je considère:

  • précisément les zones à problèmes sont des points de croissance
  • les plus gros problèmes "arrivent" précisément d'où vous ne vous attendez pas

La combinaison de ces points - vous oblige simplement à partager la merveilleuse expérience de «comment gagner une gouttière à l'improviste». Mais, il faut le noter, une situation similaire est exceptionnelle dans la société Sportmaster. Autrement dit, il est possible que cette situation se reproduise - planification et définition de la responsabilité maintenant - à un niveau complètement différent.

Donc, il semble que l'introduction soit suffisante, si vous êtes prêt - bienvenue au chat.



Juin 2017 Nous modifions le panneau d'administration. Le panneau d'administration n'est pas seulement un ensemble de formulaires et de tableaux dans l'interface Web - les valeurs saisies doivent être collées avec des dizaines d'autres données que nous obtenons de systèmes tiers. De plus, transformez-le et, finalement, envoyez-le aux consommateurs (dont le principal est le site ElasticSearch de Sportmaster).

La principale difficulté est simplement de convertir et d'envoyer. À savoir:

  1. vous devez fournir des données sous la forme de json, qui pèse 100 Ko chacune, et certaines apparaissent pour 10 Mo (recherchez la disponibilité et les critères de livraison des marchandises dans les magasins)
  2. il y a json avec une structure qui a des attachements récursifs de n'importe quel niveau d'imbrication (par exemple, un menu à l'intérieur d'un élément de menu, dans lequel il y a à nouveau des éléments de menu, etc.)
  3. la déclaration finale n'est pas approuvée et est en constante évolution (par exemple, le travail avec des marchandises par modèle est remplacé par une approche lorsque nous travaillons par modèle couleur). Constamment - c'est plusieurs fois par semaine, avec un taux de pointe de 2 fois par jour pendant une semaine.

Si les 2 premiers points sont purement techniques et sont dictés par la tâche elle-même, alors avec le 3ème point, bien sûr, vous devez le traiter de manière organisationnelle. Mais, le monde réel est loin d'être idéal, alors nous travaillons avec ce que nous avons.

À savoir, ils ont compris comment riveter rapidement les formulaires Web et leurs objets côté serveur.

Une personne de l'équipe a été nommée «claque de formulaire» professionnelle et, à l'aide de composants Web préparés, a déployé une démo pour l'interface utilisateur plus rapidement que les analystes n'ont corrigé les dessins de cette interface utilisateur.

Mais pour changer le schéma des transformations, la complexité est apparue ici.

Tout d'abord, nous avons suivi la voie habituelle - pour effectuer la transformation de la requête sql en Oracle. Il y avait un spécialiste DB dans l'équipe. Cela a duré jusqu'au moment où la demande était de 2 pages de texte sql continu. Je pourrais continuer encore et encore, mais lorsque les changements venaient des analystes - objectivement, la chose la plus difficile était de trouver l'endroit où faire les changements. Les

analystes ont exprimé leur règle dans les régimes, qui, bien qu'ils aient été peints quelque chose détaché du code (quelque chose d'un visio / draw.io / Gliffy), mais il y avait doncsimilaire aux carrés et flèches dans les systèmes ETL (par exemple, Pentaho Kettle, qui à l'époque était utilisé pour fournir des données au site Web de Sportmaster). Maintenant, si nous n'avions pas une requête SQL, mais un schéma ETL! Ensuite, l'instruction et la solution seraient exprimées de manière topologique identique, ce qui signifie que la modification du code pourrait prendre autant de temps que la modification de l'instruction!

Mais avec les systèmes ETL, il y a une autre difficulté. La même Pentaho Kettle - est idéale lorsque vous devez créer un nouvel index dans ElasticSearch, dans lequel écrire toutes les données collées à partir de plusieurs sources (remarque: en fait, c'est Pentaho Kettle qui ne fonctionne pas très bien, car il n'utilise pas javascript dans les transformations liés aux classes java par lesquelles le consommateur accède aux données - à cause de cela, vous pouvez écrire quelque chose qui ne peut pas être transformé en objets pojo nécessaires, mais ceci est un sujet séparé, loin du cours principal de l'article).

Mais que faire lorsque dans le panneau d'administration, l'utilisateur a corrigé un champ dans un document? Pour apporter cette modification au site Web ElasticSearch de Sportmaster, ne créez pas un nouvel index dans lequel remplir tous les documents de ce type, y compris un mis à jour!

Je voulais que lorsqu'un objet des données d'entrée change, puis envoyer une mise à jour à ElasticSearch du site uniquement pour le document de sortie correspondant.

D'accord, le document d'entrée lui-même, mais après tout, selon le schéma de transformation, il pourrait être attaché à des documents d'un type différent via join! Nous devons donc analyser le schéma de transformation et calculer les documents de sortie qui seront affectés par la modification des données dans les sources.

La recherche de produits en boîte pour résoudre ce problème n'a abouti à rien. Pas trouvé.
Et quand ils désespéraient de trouver, ils l'ont compris, mais comment cela devrait-il fonctionner à l'intérieur, et comment cela peut-il être fait?

L'idée est venue tout de suite.

Si l'ETL final peut être décomposé en ses parties constituantes, dont chacune a un certain type à partir d'un ensemble fini (par exemple, filtrer, joindre, etc.), alors, il suffira peut-être de créer le même ensemble final de nœuds spéciaux qui correspondent aux nœuds originaux, mais à la différence qu'ils travaillent non pas avec les données elles-mêmes, mais avec leur changement?

Dans les moindres détails, avec des exemples et des points clés dans la mise en œuvre, notre solution - je veux couvrir dans un article séparé. Pour gérer les positions de soutien - cela nécessitera une immersion sérieuse, la capacité de penser de manière abstraite et de s'appuyer sur ce qui ne s'est pas encore manifesté. En effet, il sera intéressant précisément d'un point de vue mathématique et n'intéressera que les Habrovites qui s'intéressent aux détails techniques .
Ici, je peux seulement dire que nous avons créé un modèle mathématique dans lequel nous avons décrit 7 types de nœuds et montré que ce système est complet - c'est-à-dire en utilisant ces 7 types de nœuds et les connexions entre eux - tout schéma de transformation de données peut être exprimé. La mise en œuvre est basée sur l'utilisation active de l'obtention et de l'enregistrement de données par clé (à savoir par clé, sans conditions supplémentaires).

Ainsi, notre solution avait un point fort concernant toutes les difficultés d'introduction:

  1. les données doivent être fournies sous forme de json -> nous travaillons avec des objets pojo (simple vieil objet java, si quelqu'un n'a pas trouvé les moments où une telle désignation était utilisée), qui sont faciles à dépasser dans json
  2. il y a json avec une structure qui a des incorporations récursives de n'importe quel niveau d'imbrication -> encore une fois, pojo (l'essentiel est qu'il n'y a pas de boucles, mais combien de niveaux d'imbrication n'est pas important, il est facile de traiter en java par récursivité)
  3. l'énoncé final est en constante évolution -> excellent, car nous modifions le schéma de transformation plus rapidement que les analystes ne souhaitent (dans les diagrammes) souhaiter des expériences

Parmi les moments risqués, un seul - nous écrivons la solution à partir de zéro, par nous-mêmes.

En fait, les pièges ne tardèrent pas à venir.

Moment spécial N1. Prendre au piège. “Bien extrapolé”


Une autre surprise de nature organisationnelle a été qu'en même temps que notre développement, le référentiel maître principal passait à une nouvelle version, et le format dans lequel ce référentiel fournit les données a changé. Et ce serait bien si notre système fonctionnait immédiatement avec le nouveau stockage, et non avec l'ancien. Mais le nouveau stockage n'est pas encore prêt. Mais alors, les structures de données sont connues et elles peuvent nous donner un stand de démonstration sur lequel une petite quantité de données connexes sera versée. En train d'aller?

Ici, dans l'approche produit, lorsque vous travaillez avec le flux de valeur ajoutée, un avertissement est sans équivoque lancé à tous les optimistes: il y a un bloqueur -> la tâche ne fonctionne pas, point final.

Mais alors, une telle dépendance n'a même pas éveillé les soupçons. En effet, nous avons été euphoriques du succès avec le prototype du processeur Delta - un système de traitement des données sur les deltas (mise en œuvre d'un modèle mathématique lorsque les changements dans les données de sortie sont calculés en utilisant le schéma de transformation en réponse à un changement dans les données d'entrée).

Parmi tous les schémas de transformation, l'un était le plus important. En plus du fait que le circuit lui-même était le plus grand et le plus complexe, il y avait également une exigence stricte pour que la transformation soit effectuée selon ce circuit - une limite de temps pour l'exécution de la quantité totale de données.

Ainsi, la transformation doit être effectuée 15 minutes et pas une seconde de plus. L'entrée principale est un tableau avec 5,5 millions d'enregistrements. Au stade du développement, le tableau n'est pas encore rempli. Plus précisément, il est rempli d'un petit ensemble de données de test d'un montant de 10 000 lignes.

Eh bien, commençons. Dans la première implémentation, le processeur Delta a travaillé sur le HashMap comme stockage de valeur-clé (permettez-moi de vous rappeler que nous devons lire et écrire des objets beaucoup par clé). Bien sûr, sur les volumes de production, tous les objets intermédiaires ne tiendront pas en mémoire - par conséquent, au lieu de HashMap, nous passons à Hazelcast.

Pourquoi exactement Hazelcast - donc parce que ce produit était familier, a été utilisé dans le backend du site du Sportmaster. De plus, il s'agit d'un système distribué et, comme il nous a semblé - si un ami fait quelque chose de mal avec les performances - nous ajoutons plus d'instances à quelques machines et le problème est résolu. Dans les cas extrêmes - une douzaine de voitures. Mise à l'échelle horizontale et tout le reste.

Et donc, nous lançons notre processeur Delta pour une transformation ciblée. Cela fonctionne presque instantanément. Cela est compréhensible - les données ne sont que de 10 000 au lieu de 5,5 millions. Par conséquent, nous multiplions le temps mesuré par 550, et nous obtenons le résultat: quelque chose d'environ 2 minutes. Bien! En fait - une victoire!

C'était au tout début des travaux du projet - juste au moment où vous devez décider de l'architecture, confirmer les hypothèses (effectuer des tests qui les confirment), intégrer la solution pilote verticalement.

Étant donné que les tests ont montré un excellent résultat - c'est-à-dire que nous avons confirmé toutes les hypothèses, nous avons rapidement retourné le pilote - assemblé un «squelette» verticalement intégré pour un petit morceau de fonctionnalité. Et ils ont commencé le codage principal - en remplissant le «squelette de viande».

Ce qui a réussi et vigoureusement engagé. Jusqu'à ce beau jour, où un ensemble complet de données a été téléchargé dans le magasin principal .

Exécutez le test sur cet ensemble.

Après 2 minutes n'a pas fonctionné. Je ne travaillais pas non plus après 5, 10, 15 minutes. Autrement dit, ils ne rentrent pas dans le cadre nécessaire. Mais, avec qui cela ne se produit pas, il faudra peaufiner quelque chose en détail et s'adapter.

Mais le test n'a pas fonctionné une heure plus tard. Et même après 2 heures, il y avait de l'espoir qu'il travaillerait, et nous chercherons quoi resserrer. Des restes d'espoir étaient même après 5 heures. Mais, après 10 heures, quand ils sont rentrés chez eux, mais le test n'a toujours pas fonctionné - il n'y avait plus d'espoir.

Le problème était que le lendemain, quand ils sont arrivés au bureau, le test continuait de fonctionner avec diligence. En conséquence, il a défilé pendant 30 heures, n'a pas attendu, s'est éteint.
Catastrophe!

Le problème a été localisé assez rapidement.

Hazelcast - lorsque vous travaillez sur une petite quantité de données - fait tout défiler en mémoire. Mais quand il fallait vider des données sur un disque - les performances diminuaient des milliers de fois.

La programmation serait une occupation ennuyeuse et insipide, sinon pour les autorités et l'obligation de livrer le produit fini. Donc nous, littéralement un jour plus tard, après avoir reçu un ensemble complet de données - nous devons aller aux autorités avec un rapport sur la façon dont le test sur les volumes de production a réussi.

C'est un choix très sérieux et difficile:

  1. dire «tel quel» = abandonner le projet
  2. dire "comme je voudrais" = risquer, peut-être, on ne sait pas si nous pouvons résoudre le problème

Pour comprendre quels sentiments surgissent dans ce cas, il est seulement possible d'investir pleinement dans l'idée, de réaliser le plan pendant six mois, de créer un produit qui aidera les collègues à résoudre une énorme couche de problèmes.

Et donc, abandonner votre création bien-aimée est très difficile.
C'est caractéristique de tout le monde - nous aimons ce dans quoi nous avons mis beaucoup d'efforts. Par conséquent, il est difficile d'entendre des critiques - vous devez consciemment faire des efforts pour percevoir correctement les commentaires.

En général, nous avons décidé qu'il existe encore de très, très nombreux systèmes différents qui peuvent être utilisés comme stockage de valeur-clé, et si Hazelcast ne convient pas, alors quelque chose fonctionnera certainement. Autrement dit, ils ont décidé de tenter leur chance. Pour notre justification, nous pouvons dire que ce n'était pas encore un «délai sanglant» - en général, il y avait encore une marge de temps pour «passer» à une solution de sauvegarde.

Lors de cette réunion avec les autorités, notre responsable a indiqué que «le test a montré que le système fonctionne de manière stable à des volumes de production, il ne plante pas». En effet, le système fonctionnait de manière stable. 60 jours

pour libérer .

Moment spécial N2. Pas un piège, mais pas une découverte. "Moins est plus"


Pour trouver un remplaçant pour Hazelcast avec le rôle d'entrepôt de données clé-valeur, nous avons compilé une liste de tous les candidats - nous avons obtenu une liste de 31 produits. C'est tout ce que j'ai réussi à trouver sur Google et à découvrir auprès de mes amis. En outre, Google a proposé des options absolument obscènes, telles que le mémoire de fin d'études d'un étudiant.

Pour tester les candidats plus rapidement, nous avons préparé un petit test qui, en quelques minutes de lancement, a montré des performances sur les bons volumes. Et ils ont parallélisé le travail - tout le monde a pris le système suivant de la liste, configuré, exécuté le test, a pris le suivant.
Ils ont travaillé rapidement, cassé plusieurs systèmes par jour.

Sur le 18ème système, il est devenu clair que c'était inutile. Sous notre profil de charge - aucun de ces systèmes n'est affûté. Ils ont beaucoup de volants et de révérences pour le rendre pratique à utiliser, de nombreuses belles approches de la mise à l'échelle horizontale - mais cela ne nous donne aucun profit.

Nous avons besoin d'un système qui _fast_ enregistre la clé dans un objet sur le disque et lit rapidement la clé.

Si oui, nous décrivons l'algorithme de la façon dont cela peut être mis en œuvre. En général, cela semble tout à fait réalisable - si en même temps: a) sacrifiez la quantité de données qui occupera le disque, b) avez approximativement des estimations de la quantité et de la taille caractéristique des données dans chaque tableau.
Quelque chose de style, allouez de la mémoire (sur disque) pour des objets avec une marge, des morceaux d'un volume maximum fixe. Puis en utilisant les tables d'index ... et ainsi de suite ...
Il a eu de la chance de ne pas en arriver là.

Le salut est venu sous la forme de RocksDB.
Il s'agit d'un produit de Facebook conçu pour une lecture rapide et l'enregistrement d'un tableau d'octets sur le disque. Dans le même temps, l'accès aux fichiers est fourni via une interface similaire au stockage de valeurs-clés. En fait, la clé est un tableau d'octets, la valeur est un tableau d'octets. Optimisé pour effectuer ce travail rapidement et de manière fiable. Tout. Si vous avez besoin de quelque chose de plus beau et de haut niveau - vissez-le vous-même.
Exactement ce dont nous avons besoin!

RocksDB, boulonné dans le rôle de stockage de valeur-clé, a amené l'indicateur de test cible au niveau de 5 heures. C'était loin de 15 minutes, mais l'essentiel était fait. L'essentiel était de comprendre ce qui se passait, de comprendre que l'écriture sur le disque était aussi rapide que possible, plus rapide qu'impossible. Sur SSD, dans des tests raffinés, RocksDB a comprimé 400Mb / s, et c'était suffisant pour notre tâche. Retards - quelque part dans le nôtre, dans un code contraignant.

Dans notre code, ce qui signifie que nous pouvons le gérer. Prenons-le à part, mais nous pouvons le gérer.

Moment spécial N3. Soutien. "Calcul théorique"


Nous avons un algorithme et une entrée. Nous prenons la plage de données d'entrée, calculons le nombre d'actions que le système doit effectuer, comment ces actions sont exprimées dans les coûts d'exécution JVM (attribuez une valeur à une variable, entrez une méthode, créez un objet, copiez un tableau d'octets, etc.), plus le nombre d'appels à RocksDB devrait être tenu.

Selon les calculs, il s'avère qu'ils devraient se réunir 2 minutes (environ, comme l'a montré le test pour HashMap au tout début, mais ce n'est qu'une coïncidence - l'algorithme a changé depuis lors).

Et pourtant, le test dure 5 heures.

Et maintenant, avant la sortie de 30 jours.

Il s'agit d'une date spéciale - il sera désormais impossible de s'effondrer - nous n'aurons pas le temps de passer à l'option de sauvegarde.
Bien sûr, ce jour-là, le chef de projet est convoqué aux autorités. La question est la même - avoir le temps, tout va bien?



Voici la meilleure façon de décrire cette situation - une image de couverture étendue pour cet article. Autrement dit, les patrons sont montrés cette partie de l'image qui est rendue dans le titre. Mais en réalité - comme ça.

Bien que, en réalité, bien sûr - nous n'étions pas du tout drôles. Et dites que "Tout est cool!" - ceci n'est possible que pour une personne ayant une très forte maîtrise de soi.
Grand, immense respect pour le manager, pour croire, faire confiance aux développeurs.

Code vraiment, vraiment disponible - montre 5 heures. Un calcul théorique - montre 2 minutes. Comment peut-on croire cela?

Mais c’est possible si: le modèle est formulé clairement, comment compter est compréhensible et quelles valeurs substituer sont également compréhensibles. Autrement dit, le fait qu'en réalité l'exécution prend plus de temps signifie qu'en réalité ce n'est pas exactement le code que nous nous attendons à y exécuter qui est en cours d'exécution.

La tâche centrale est de trouver «ballast» dans le code. Autrement dit, certaines actions sont effectuées en plus du flux principal de création des données finales.

Se précipita. Tests unitaires, compositions fonctionnelles, fragmentation des fonctions et localisation des lieux avec un temps disproportionné consacré à l'exécution. Beaucoup de choses ont été faites.
En cours de route, nous avons formulé de tels endroits où vous pouvez vous resserrer sérieusement.

Par exemple, la sérialisation. D'abord utilisé le standard java.io. Mais si nous fixons Cryo, dans notre cas, nous obtenons une augmentation de 2,5 fois de la vitesse de sérialisation et une réduction de 3 fois de la quantité de données sérialisées (ce qui signifie que les E / S sont 3 fois plus petites, ce qui ne fait que consommer les principales ressources). Mais, plus en détail, il s'agit d'un sujet pour un article technique distinct.

Mais le point clé, ou «où l'éléphant s'est caché» - je vais essayer de le décrire dans un paragraphe.

Point spécial 4. Accueil pour trouver une solution. "Problème = Solution"


Lorsque nous obtenons / définissons par clé - dans les calculs, cela s'est passé comme une opération, affecte les entrées-sorties dans le volume égal à clé + valeur-objet (sous forme sérialisée, bien sûr).
Mais que faire si l'objet lui-même sur lequel nous appelons get / set est une carte, que nous obtenons également par get / set à partir du disque. Quelle quantité d'E / S sera effectuée dans ce cas?

Dans nos calculs, cette fonctionnalité n'a pas été prise en compte. Autrement dit, il a été considéré comme 1 E / S pour clé + valeur-objet. Mais en fait?

Par exemple, dans le stockage de valeur-clé, par clé-1, il y a un objet obj-1 de type Carte, dans lequel un certain objet obj-2 doit être stocké sous la clé clé-2. Ici, nous pensions que l'opération nécessiterait un IO pour key-2 + obj-2. Mais en réalité, vous devez considérer obj-1, le manipuler et l'envoyer à IO: key-1 + obj-1. Et s'il s'agit d'une carte dans laquelle il y a 1000 objets, la consommation d'E / S sera environ 1000 fois plus élevée. Et si 10 000 objets, alors ... C'est comme ça qu'ils ont obtenu le "ballast".

Lorsqu'un problème est identifié, la solution est généralement évidente.

Dans notre cas, cela est devenu une structure spéciale pour les manipulations à l'intérieur de Map imbriquée. Autrement dit, une telle valeur-clé, qui pour get / set prend deux clés à la fois, qui devrait être appliquée séquentiellement: clé-1, clé-2 - c'est-à-dire pour le premier niveau et pour le niveau imbriqué. Comment mettre en œuvre une telle structure - Je vous le dirai en détail avec plaisir, mais encore une fois, dans un article technique distinct.
Ici, à partir de cet épisode, je souligne et promouvais une telle fonctionnalité: un problème extrêmement détaillé est une bonne solution.

Achèvement


Dans cet article, j'ai essayé de montrer les points d'organisation et les pièges qui peuvent survenir. De tels pièges sont très clairement visibles «de côté» ou au fil du temps, mais il est très facile d'y pénétrer lorsque vous vous retrouvez à côté d'eux. J'espère que quelqu'un se souviendra d'une telle description, et au bon moment le rappel fonctionnera "J'ai déjà entendu quelque chose comme ça quelque part."

Et, plus important encore - maintenant que tout est dit sur le processus, sur les moments psychologiques, sur les moments organisationnels. Maintenant que nous avons une idée de quelles tâches et dans quelles conditions le système a été créé. Maintenant - vous pouvez et devez parler du système du point de vue technique - de quel type de modèle mathématique il s'agit, et quelles astuces dans le code nous avons utilisées et quelles solutions innovantes nous avons pensé.

À ce sujet dans le prochain article.

En attendant, Happy New Code!

All Articles