Dans cet article, nous expliquerons comment et pourquoi nous avons dĂ©veloppĂ© â un mĂ©canisme qui transmet des informations entre les applications clientes et les serveurs 1C:Enterprise â allant de la formulation de la tĂąche Ă la conception de l'architecture et des dĂ©tails de mise en Ćuvre.
Le SystĂšme d'Interaction (ci-aprĂšs â SI) â est un systĂšme de messagerie distribuĂ© et tolĂ©rant aux pannes avec une livraison garantie. Le SI est conçu comme un service Ă forte charge avec une grande Ă©volutivitĂ©, disponible Ă la fois en tant que service en ligne (proposĂ© par la sociĂ©tĂ© 1C) et en tant que produit sous licence, qui peut ĂȘtre dĂ©ployĂ© sur ses propres serveurs.
Le SI utilise un stockage distribué et un systÚme de recherche . Nous parlerons également de Java et de la maniÚre dont nous étendons horizontalement PostgreSQL.
Définition du problÚme
Pour comprendre pourquoi nous avons créé le SystÚme d'Interaction, je vais vous expliquer un peu comment se déroule le développement d'applications commerciales chez 1C.
Pour commencer â un peu sur nous pour ceux qui ne savent pas encore ce que nous faisons :) Nous dĂ©veloppons la plateforme technologique «1C:Enterprise». Cette plateforme comprend un outil de dĂ©veloppement d'applications commerciales ainsi qu'un runtime, permettant aux applications commerciales de fonctionner dans un environnement multi-plateforme.
La paradigme client-serveur de développement
Les applications commerciales créées sur «1C:Enterprise» fonctionnent dans une architecture Ă trois niveaux «SGBD â serveur d'applications â client». Le code applicatif, Ă©crit en , peut s'exĂ©cuter sur le serveur d'applications ou sur le client. Tout le travail avec les objets applicatifs (rĂ©fĂ©rentiels, documents, etc.), ainsi que la lecture et l'Ă©criture de la base de donnĂ©es, s'effectuent uniquement sur le serveur. La fonctionnalitĂ© des formulaires et de l'interface utilisateur est Ă©galement rĂ©alisĂ©e sur le serveur. Sur le client, les tĂąches consistent Ă recevoir, ouvrir et afficher les formulaires, Ă interagir avec l'utilisateur (avertissements, questionsâŠ), Ă effectuer des calculs mineurs dans les formulaires nĂ©cessitant une rĂ©action rapide (par exemple, multiplication du prix par la quantitĂ©), Ă travailler avec des fichiers locaux et Ă interagir avec le matĂ©riel.
Dans le code applicatif, les en-tĂȘtes des procĂ©dures et fonctions doivent explicitement indiquer oĂč le code sera exĂ©cutĂ© â en utilisant les directives &SurClient / &SurServeur (&AtClient / &AtServer dans la version anglaise du langage). Les dĂ©veloppeurs sur 1C vont maintenant me corriger en disant que les directives en rĂ©alitĂ© , mais cela n'est pas essentiel pour nous actuellement.
Il est possible d'appeler du code serveur depuis le code client, mais l'inverse n'est pas vrai. C'est une limitation fondamentale que nous avons mise en place pour plusieurs raisons. En particulier, le code serveur doit ĂȘtre Ă©crit de maniĂšre Ă s'exĂ©cuter de la mĂȘme façon, qu'il soit appelĂ© depuis le client ou le serveur. Dans le cas d'un appel de code serveur par un autre code serveur, le client n'existe pas en tant que tel. De plus, au cours de l'exĂ©cution du code serveur, le client ayant lancĂ© cet appel pourrait se fermer ou quitter l'application, laissant le serveur sans appel.
Le code qui gÚre le clic sur le bouton : appeler une procédure serveur depuis le client fonctionnera, mais appeler une procédure client depuis le serveur ne fonctionnera pas.
Cela signifie que si nous voulons envoyer un message depuis le serveur vers l'application cliente, par exemple, pour indiquer que la gĂ©nĂ©ration d'un rapport « long » est terminĂ©e et que le rapport peut ĂȘtre consultĂ©, nous n'avons pas de moyen de le faire. Nous devons recourir Ă des dĂ©tours, comme interroger le serveur pĂ©riodiquement depuis le code client. Mais cette approche surcharger le systĂšme avec des appels inutiles et ne paraĂźt pas trĂšs Ă©lĂ©gante.
Il y a aussi la nĂ©cessitĂ©, par exemple, d'informer l'application cliente lors de la rĂ©ception d'un appel tĂ©lĂ©phonique pour que celle-ci puisse trouver dans sa base de donnĂ©es le contact correspondant au numĂ©ro de l'appelant et afficher des informations sur ce dernier. Ou, par exemple, lorsqu'une commande arrive Ă l'entrepĂŽt, notifier l'application cliente de l'acheteur. En somme, de nombreux cas oĂč un tel mĂ©canisme serait utile.
La problématique en soi
CrĂ©er un mĂ©canisme d'Ă©change de messages. Rapide, fiable, avec une garantie de livraison, et une possibilitĂ© de recherche flexible des messages. Sur la base de ce mĂ©canisme, mettre en Ćuvre un messager (messages, appels vidĂ©o), fonctionnant Ă l'intĂ©rieur des applications 1C.
Concevoir un systĂšme horizontalement Ă©volutif. L'augmentation de charge doit ĂȘtre compensĂ©e par une augmentation du nombre de nĆuds.
Mise en Ćuvre
Nous avons dĂ©cidĂ© de ne pas intĂ©grer la partie serveur de la SV directement dans la plateforme 1C:Enterprise, mais de la rĂ©aliser en tant que produit distinct, dont l'API peut ĂȘtre appelĂ©e Ă partir du code des solutions applicatives 1C. Cela a Ă©tĂ© fait pour plusieurs raisons, la principale Ă©tant de permettre l'Ă©change de messages entre diffĂ©rentes applications 1C (par exemple, entre la Gestion Commerciale et la ComptabilitĂ©). DiffĂ©rentes applications 1C peuvent fonctionner sur diffĂ©rentes versions de la plateforme 1C:Enterprise, ĂȘtre sur diffĂ©rents serveurs, etc. Dans ces conditions, la mise en Ćuvre de la SV en tant que produit distinct, situĂ© « Ă cĂŽtĂ© » des installations 1C, constitue la solution optimale.
Ainsi, nous avons dĂ©cidĂ© de dĂ©velopper la SV comme un produit sĂ©parĂ©. Pour les petites entreprises, nous recommandons d'utiliser le serveur SV que nous avons installĂ© dans notre cloud (wss://1cdialog.com) afin d'Ă©viter les coĂ»ts liĂ©s Ă l'installation locale et Ă la configuration du serveur. Les grands clients, en revanche, peuvent juger utile d'installer leur propre serveur SV sur leurs propres infrastructures. Une approche similaire a Ă©tĂ© utilisĂ©e dans notre produit SaaS cloud. â il est publiĂ© en tant que produit standard pour installation chez les clients, et est Ă©galement dĂ©ployĂ© dans notre cloud. .
L'application
Pour la rĂ©partition de la charge et la rĂ©sistance aux pannes, nous dĂ©ploierons plusieurs applications Java plutĂŽt qu'une seule, et nous placerons un Ă©quilibrateur de charge devant elles. Si un message doit ĂȘtre transmis d'un nĆud Ă un autre, nous utiliserons le publish/subscribe dans Hazelcast.
La communication entre le client et le serveur se fait par websocket. Ce dernier est bien adapté aux systÚmes en temps réel.
Cache distribué
Nous avons hĂ©sitĂ© entre Redis, Hazelcast et Ehcache. Nous sommes en 2015. Redis vient juste de sortir un nouveau cluster (trop nouveau, trop effrayant), il y a Sentinel avec de nombreuses limitations. Ehcache ne peut pas ĂȘtre configurĂ© en cluster (cette fonctionnalitĂ© est apparue plus tard). Nous avons dĂ©cidĂ© d'essayer avec Hazelcast 3.4.
Hazelcast se regroupe en cluster « out of the box ». En mode Ă un seul nĆud, il n'est pas trĂšs utile et ne peut servir que de cache â il ne peut pas Ă©crire des donnĂ©es sur disque, si vous perdez le seul nĆud, vous perdez les donnĂ©es. Nous dĂ©ployons plusieurs instances de Hazelcast, entre lesquelles nous sauvegardons les donnĂ©es critiques. Nous ne sauvegardons pas le cache â cela ne vaut pas la peine.
Pour nous, Hazelcast, c'est :
- Un stockage des sessions utilisateur. Aller chercher chaque fois la session dans la base de données prend du temps, donc nous stockons toutes les sessions dans Hazelcast.
- Cache. Si vous cherchez un profil utilisateur, vérifiez dans le cache. Si vous avez écrit un nouveau message, mettez-le dans le cache.
- Sujets de communication d'instances d'application. Le nĆud gĂ©nĂšre un Ă©vĂ©nement et le place dans le sujet Hazelcast. Les autres nĆuds de l'application, abonnĂ©s Ă ce sujet, reçoivent et traitent l'Ă©vĂ©nement.
- Verrous cluster. Par exemple, créons une discussion en utilisant une clé unique (discussion singleton dans le cadre de la base 1C) :
conversationKeyChecker.check("BENCOLONNE");
doInClusterLock("BENCOLONNE", () -> {
conversationKeyChecker.check("BENCOLONNE");
createChannel("BENCOLONNE");
});Nous avons vĂ©rifiĂ© qu'il n'y avait pas de canal. Nous avons pris le verrou, vĂ©rifiĂ© Ă nouveau, créé. Si aprĂšs avoir pris le verrou, nous ne vĂ©rifions pas, il y a un risque qu'un autre thread vĂ©rifie Ă©galement et essaie de crĂ©er la mĂȘme discussion - alors qu'elle existe dĂ©jĂ . Il est impossible de faire le verrouillage par synchronized ou par un verrou Java ordinaire. Par la base de donnĂ©es, c'est lent, et il est dommage d'utiliser la base, via Hazelcast - c'est ce qu'il faut.
Choisissons le SGBD
Nous avons une grande expérience réussie avec PostgreSQL et une collaboration avec les développeurs de ce SGBD.
Il est difficile de travailler avec un cluster PostgreSQL - il existe , , , mais en général, ce n'est pas du noSQL, qui se scale de maniÚre native. Nous n'avons pas envisagé NoSQL comme solution de stockage principale, Hazelcast étant suffisant, surtout que nous n'avions jamais travaillé avec auparavant.
Puisqu'il faut faire Ă©voluer une base de donnĂ©es relationnelle - cela signifie, . Comme vous le savez, avec le sharding, nous divisons la base de donnĂ©es en parties distinctes de sorte qu chacune d'elles puisse ĂȘtre placĂ©e sur un serveur sĂ©parĂ©.
La premiÚre version de notre sharding prévoyait de répartir chaque table de notre application sur différents serveurs dans différentes proportions. Beaucoup de messages sur le serveur A ? Bien, déplaçons une partie de cette table sur le serveur B. Cette solution était clairement une optimisation prématurée, donc nous avons décidé de nous en tenir à une approche multi-tenant.
Vous pouvez en lire davantage sur le multi-tenant, par exemple, sur le site .
Dans SV, il y a les concepts d'application et d'abonnĂ©. Une application est une installation spĂ©cifique d'une application mĂ©tier, par exemple, ERP ou ComptabilitĂ©, avec ses utilisateurs et ses donnĂ©es mĂ©tier. Un abonnĂ© est une organisation ou une personne physique au nom de laquelle l'application est enregistrĂ©e sur le serveur SV. Un abonnĂ© peut avoir plusieurs applications enregistrĂ©es, et ces applications peuvent Ă©changer des messages entre elles. L'abonnĂ© est devenu un locataire dans notre systĂšme. Les messages de plusieurs abonnĂ©s peuvent se trouver dans une seule base de donnĂ©es physique ; si nous constatons qu'un abonnĂ© gĂ©nĂšre beaucoup de trafic, nous le dĂ©plaçons dans une base de donnĂ©es physique sĂ©parĂ©e (ou mĂȘme un serveur de base de donnĂ©es sĂ©parĂ©).
Nous avons une base de donnĂ©es principale oĂč est stockĂ© un tableau de routage contenant des informations sur l'emplacement de toutes les bases de donnĂ©es d'abonnĂ©s.
Pour que la base de données principale ne devienne pas un goulot d'étranglement, nous maintenons le tableau de routage (et d'autres données souvent demandées) en cache.
Si la base de données de l'abonné commence à ralentir, nous allons effectuer une partitionnement interne. Sur d'autres projets, nous utilisons la partitionnement pour de grandes tables avec .
Comme il est mauvais de perdre des messages utilisateurs, nous soutenons nos bases de données par des répliques. Une combinaison de répliques synchrones et asynchrones permet de se protéger en cas de perte de la base de données principale. La perte d'un message ne se produira que si la base de données principale et sa réplique synchrone échouent simultanément.
Si la réplique synchrone est perdue, la réplique asynchrone devient synchrone.
Si la base de données principale est perdue, la réplique synchrone devient la base de données principale, la réplique asynchrone devient la réplique synchrone.
Elasticsearch pour la recherche
Puisque, parmi d'autres choses, SV est aussi un messager, nous avons besoin d'une recherche rapide, pratique et flexible, prenant en compte la morphologie, pour des correspondances inexactes. Nous avons dĂ©cidĂ© de ne pas rĂ©inventer la roue et d'utiliser le systĂšme de recherche libre Elasticsearch, basĂ© sur la bibliothĂšque . Nous dĂ©ployons Ă©galement Elasticsearch en cluster (master â data â data), afin d'Ă©viter des problĂšmes en cas de dĂ©faillance des nĆuds de l'application.
Sur github, nous avons trouvé pour Elasticsearch et l'utilisons. Dans l'index Elasticsearch, nous stockons les racines des mots (définies par le plugin) et les N-grammes. à mesure que l'utilisateur saisit du texte à rechercher, nous recherchons le texte saisi parmi les N-grammes. Lorsqu'il est enregistré dans l'index, le mot « textes » sera divisé en les N-grammes suivants :
[te, tek, teks, texte, textes, ek, eks, ekst, ekstes, ks, kst, ksty, st, sty, ty],
Ainsi, la racine du mot « texte » sera également enregistrée. Cette approche permet de rechercher au début, au milieu et à la fin du mot.
Vue d'ensemble
Répétition de l'image du début de l'article, mais avec des explications :
- Un Ă©quilibreur de charge exposĂ© sur Internet ; nous avons nginx, cela peut ĂȘtre n'importe quel autre.
- Les instances de l'application Java communiquent entre elles via Hazelcast.
- Pour travailler avec le websocket, nous utilisons .
- L'application Java est écrite en Java 8 et se compose de bundles . Nous prévoyons une migration vers Java 10 et un passage aux modules.
Développement et test
Au cours du développement et du test du SV, nous avons rencontré plusieurs caractéristiques intéressantes des produits que nous utilisons.
Test de charge et fuites de mémoire
Chaque version du SV est précédée d'un test de charge. Il est passé avec succÚs lorsque :
- Le test a fonctionné pendant plusieurs jours sans interruptions de service
- Le temps de réponse pour les opérations clés n'a pas dépassé un seuil confortable
- La dégradation des performances par rapport à la version précédente n'est pas supérieure à 10%
Nous remplissons la base de test avec des donnĂ©es â pour cela, nous obtenons depuis le serveur de production des informations sur l'abonnĂ© le plus actif, multiplions ses chiffres par 5 (nombre de messages, discussions, utilisateurs) et ainsi nous testons.
Nous effectuons le test de charge du systĂšme d'interaction dans trois configurations :
- Test de stress
- Seulement des connexions
- Enregistrement des abonnés
Lors du test de stress, nous lançons plusieurs centaines de threads, et ceux-ci chargent le systÚme sans interruption : ils envoient des messages, créent des discussions, obtiennent la liste des messages. Nous imitons les actions des utilisateurs normaux (obtenir la liste de mes messages non lus, écrire à quelqu'un) et des solutions logicielles (transmettre un paquet à une autre configuration, traiter une notification).
Par exemple, voici Ă quoi ressemble une partie du test de stress :
- Un utilisateur se connecte au systĂšme
- Demande ses discussions non lues
- Avec 50 % de probabilité, il lit les messages
- Avec 50 % de probabilité, il écrit des messages
- Ensuite, l'utilisateur :
- Crée une nouvelle discussion avec 20 % de probabilité
- Sélectionne au hasard l'une de ses discussions
- AccÚde à l'intérieur
- Demande des messages, des profils d'utilisateurs
- Crée cinq messages adressés à des utilisateurs aléatoires de cette discussion
- Sort de la discussion
- RépÚte 20 fois
- Se déconnecte, revient au début du scénario
- Un chatbot se connecte au systÚme (émule l'échange de messages depuis le code des solutions applicatives)
- Crée un nouveau canal d'échange de données (discussion spéciale) avec 50 % de probabilité
- Ăcrit un message dans l'un des canaux existants avec 50 % de probabilitĂ©
Le scĂ©nario « Connexions uniquement » n'est pas apparu par hasard. Il arrive que les utilisateurs aient connectĂ© le systĂšme, mais ne se soient pas encore impliquĂ©s. Chaque utilisateur allume son ordinateur le matin Ă 09h00, Ă©tablit une connexion au serveur et reste silencieux. Ces gens sont dangereux, ils sont nombreux â dans les paquets, il n'y a que PING/PONG, mais ils maintiennent la connexion au serveur (ils ne peuvent pas s'en passer â et si un nouveau message arrive). Le test reproduit une situation oĂč un grand nombre de ces utilisateurs tentent de se connecter au systĂšme en trente minutes. Cela ressemble Ă un test de stress, mais son accent est mis prĂ©cisĂ©ment sur cette premiĂšre connexion â pour Ă©viter les dĂ©faillances (la personne ne se sert pas du systĂšme, mais celui-ci tombe dĂ©jĂ en panne â difficile de penser Ă quelque chose de pire).
Le scénario d'enregistrement des abonnés commence dÚs le premier lancement. Nous avons effectué un test de stress et étions convaincus que le systÚme ne ralentissait pas lors de la messagerie. Mais les utilisateurs sont arrivés et l'enregistrement a commencé à tomber par timeout. Lors de l'enregistrement, nous avons utilisé , qui est lié à l'entropie du systÚme. Le serveur ne parvenait pas à accumuler suffisamment d'entropie et, lors de la demande d'un nouveau SecureRandom, se gelait pendant des dizaines de secondes. Il existe de nombreuses solutions à cette situation, par exemple : passer à un /dev/urandom moins sécurisé, installer une carte spéciale qui génÚre de l'entropie, générer des nombres aléatoires à l'avance et les stocker dans un pool. Nous avons temporairement résolu le problÚme avec un pool, mais depuis nous exécutons un test distinct pour l'enregistrement de nouveaux abonnés.
Comme générateur de charge, nous utilisons . Il ne sait pas travailler avec WebSocket, un plugin est nécessaire. Les premiers résultats de recherche pour « jmeter websocket » sont , qui recommandent .
C'est de là que nous avons décidé de commencer.
Peu aprÚs le début des tests sérieux, nous avons découvert que JMeter avait des fuites de mémoire.
Le plugin - c'est une grande histoire à part, avec 176 étoiles et 132 forks sur github. L'auteur n'y a pas commis depuis 2015 (nous l'avons pris en 2015, ce qui ne suscitait pas de soupçons), plusieurs problÚmes sur github concernant les fuites de mémoire, 7 pull requests non résolues.
Si vous décidez de faire des tests de charge avec ce plugin, faites attention aux discussions suivantes :
- Dans un environnement multithread, une LinkedList classique a Ă©tĂ© utilisĂ©e, et en consĂ©quence, nous avons obtenu en temps d'exĂ©cution. Cela peut ĂȘtre rĂ©solu soit en utilisant ConcurrentLinkedDeque, soit avec des blocs synchronisĂ©s. Nous avons choisi la premiĂšre option ().
- Fuite de mémoire, lors de la déconnexion, les informations sur la connexion ne sont pas supprimées ().
- En mode streaming (quand le websocket n'est pas fermé à la fin de l'échantillon, mais utilisé plus tard dans le plan), les modÚles de réponse ne fonctionnent pas ().
C'est l'un de ceux sur github. Ce que nous avons fait :
- Nous avons pris (@elyrank) - qui a résolu les problÚmes 1 et 3
- Nous avons résolu le problÚme 2
- Nous avons mis Ă jour jetty de 9.2.14 Ă 9.3.12
- Nous avons enveloppé SimpleDateFormat dans ThreadLocal ; SimpleDateFormat n'est pas thread-safe, ce qui a conduit à un NPE en temps d'exécution
- Nous avons résolu une autre fuite de mémoire (la connexion n'était pas fermée correctement lors de la déconnexion)
Et pourtant, il fuit !
La mémoire ne se terminait plus en un jour, mais en deux. Nous n'avions plus de temps, nous avons décidé de lancer moins de threads, mais sur quatre agents. Cela devait suffire, au moins, pour une semaine.
Deux jours se sont écoulés...
Maintenant, la mĂ©moire se termine chez Hazelcast. Dans les logs, il Ă©tait visible qu'aprĂšs quelques jours de tests, Hazelcast commençait Ă se plaindre d'un manque de mĂ©moire, et peu aprĂšs, le cluster s'effondrait, et les nĆuds continuaient Ă mourir un par un. Nous avons connectĂ© JVisualVM Ă Hazelcast et avons vu une « scie montante » â il appelait rĂ©guliĂšrement le GC, mais n'arrivait pas Ă libĂ©rer la mĂ©moire.
Il s'est avéré que dans hazelcast 3.4, lors de la suppression d'une map / multiMap (map.destroy()), la mémoire n'était pas complÚtement libérée :
Le problÚme est maintenant corrigé dans 3.5, mais à l'époque, c'était un problÚme. Nous créions de nouvelles multiMap avec des noms dynamiques et les supprimions selon notre logique. Le code ressemblait à peu prÚs à cela :
public void join(Authentication auth, String sub) {
MultiMap sessions = instance.getMultiMap(sub);
sessions.put(auth.getUserId(), auth);
}
public void leave(Authentication auth, String sub) {
MultiMap sessions = instance.getMultiMap(sub);
sessions.remove(auth.getUserId(), auth);
if (sessions.size() == 0) {
sessions.destroy();
}
}Appel :
service.join(auth1, "NOUVEAUX_MESSAGES_DANS_LE_DISCUS_UUID1");
service.join(auth2, "NOUVEAUX_MESSAGES_DANS_LE_DISCUS_UUID1");multiMap a été créé pour chaque abonnement et supprimé lorsqu'il n'était plus nécessaire. Nous avons décidé de créer un Map, avec comme clé le nom de l'abonnement et comme valeurs les identifiants de session (à partir desquelles il est possible d'obtenir les identifiants d'utilisateur si nécessaire).
public void join(Authentication auth, String sub) {
addValueToMap(sub, auth.getSessionId());
}
public void leave(Authentication auth, String sub) {
removeValueFromMap(sub, auth.getSessionId());
}Les graphiques se sont stabilisés.
Qu'avons-nous appris d'autre sur les tests de charge
- JSR223 doit ĂȘtre Ă©crit en groovy et inclure le cache de compilation - c'est beaucoup plus rapide. .
- Les graphiques Jmeter-Plugins sont plus faciles Ă comprendre que les standards. .
Notre expérience avec Hazelcast
Hazelcast était un nouveau produit pour nous, nous avons commencé à travailler avec lui à partir de la version 3.4.1, actuellement notre serveur de production utilise la version 3.9.2 (au moment de la rédaction de cet article, la derniÚre version de Hazelcast est 3.10).
Génération d'ID
Nous avons commencĂ© avec des identifiants numĂ©riques. Imaginons que nous avons besoin d'un nouveau Long pour une nouvelle entitĂ©. Les sĂ©quences dans la base de donnĂ©es ne conviennent pas, les tables participent au sharding - il pourrait y avoir un message ID=1 dans DB1 et un message ID=1 dans DB2 ; cela ne fonctionne pas dans Elasticsearch, ni dans Hazelcast, et le plus grave, c'est si vous voulez fusionner les donnĂ©es de deux bases de donnĂ©es en une seule (par exemple, en dĂ©cidant qu'une seule base suffit pour ces abonnĂ©s). On peut crĂ©er plusieurs AtomicLong dans Hazelcast et garder un compteur lĂ -bas, alors la performance d'obtention d'un nouvel ID est - incrementAndGet plus le temps de la requĂȘte dans Hazelcast. Mais dans Hazelcast, il y a quelque chose de plus optimal - FlakeIdGenerator. Ă chaque requĂȘte du client, un intervalle d'ID est attribuĂ©, par exemple au premier - de 1 Ă 10 000, au second - de 10 001 Ă 20 000, et ainsi de suite. Le client peut maintenant gĂ©nĂ©rer de nouveaux identifiants de maniĂšre autonome jusqu'Ă Ă©puisement de l'intervalle qui lui a Ă©tĂ© attribuĂ©. Cela fonctionne rapidement, mais lors du redĂ©marrage de l'application (et du client Hazelcast), une nouvelle sĂ©quence commence - d'oĂč les sauts, etc. De plus, il n'est pas trĂšs clair pour les dĂ©veloppeurs pourquoi les ID sont numĂ©riques, mais varient tant. Nous avons tout pesĂ© et sommes passĂ©s aux UUID.
Au fait, pour ceux qui veulent faire comme Twitter, il existe une bibliothĂšque Snowcast â c'est une implĂ©mentation de Snowflake sur Hazelcast. Vous pouvez la consulter ici :
Mais nous n'y avons pas encore eu le temps.
TransactionalMap.replace
Une autre surprise : TransactionalMap.replace ne fonctionne pas. Voici un test :
@Test
public void replaceInMap_putsAndGetsInsideTransaction() {
hazelcastInstance.executeTransaction(context -> {
HazelcastTransactionContextHolder.setContext(context);
try {
context.getMap("map").put("key", "oldValue");
context.getMap("map").replace("key", "oldValue", "newValue");
String value = (String) context.getMap("map").get("key");
assertEquals("newValue", value);
return null;
} finally {
HazelcastTransactionContextHolder.clearContext();
}
});
}
Attendu : newValue
Actuel : oldValueNous avons dû écrire notre propre replace, en utilisant getForUpdate :
protected <K,V> boolean replaceInMap(String mapName, K key, V oldValue, V newValue) {
TransactionalTaskContext context = HazelcastTransactionContextHolder.getContext();
if (context != null) {
log.trace("[CACHE] Remplacement de la valeur dans une carte transactionnelle");
TransactionalMap<K, V> map = context.getMap(mapName);
V value = map.getForUpdate(key);
if (oldValue.equals(value)) {
map.put(key, newValue);
return true;
}
return false;
}
log.trace("[CACHE] Remplacement de la valeur dans une carte non transactionnelle");
IMap<K, V> map = hazelcastInstance.getMap(mapName);
return map.replace(key, oldValue, newValue);
}Testez non seulement les structures de données classiques, mais aussi leurs versions transactionnelles. Parfois, IMap fonctionne, alors que TransactionalMap ne fonctionne pas.
DĂ©ployer un nouveau JAR sans temps d'arrĂȘt
Au départ, nous avons décidé d'enregistrer dans Hazelcast des objets de nos propres classes. Par exemple, nous avons une classe Application, nous souhaitons la sauvegarder et la lire. Sauvegarde :
IMap<UUID, Application> map = hazelcastInstance.getMap("application");
map.set(id, application);Lecture :
IMap<UUID, Application> map = hazelcastInstance.getMap("application");
return map.get(id);Tout fonctionne. Ensuite, nous avons décidé de construire un index dans Hazelcast pour pouvoir le rechercher :
map.addIndex("subscriberId", false);Et lors de l'enregistrement d'une nouvelle entitĂ©, nous avons commencĂ© Ă recevoir une ClassNotFoundException. Hazelcast tentait de complĂ©ter l'index, mais ne connaissait rien de notre classe et voulait qu'on lui fournisse le JAR avec cette classe. Nous l'avons fait, tout a fonctionnĂ©, mais un nouveau problĂšme est apparu : comment mettre Ă jour le JAR sans arrĂȘter complĂštement le cluster ? Hazelcast ne prend pas en charge le nouveau JAR lors d'une mise Ă jour progressive. Ă ce moment-lĂ , nous avons dĂ©cidĂ© que nous pouvions vivre sans recherche par index. AprĂšs tout, si l'on utilise Hazelcast comme un stockage clĂ©-valeur, tout devrait fonctionner, n'est-ce pas ? Pas tout Ă fait. Ici, il y a encore un comportement diffĂ©rent entre IMap et TransactionalMap. LĂ oĂč IMap s'en fiche, TransactionalMap gĂ©nĂšre une erreur.
IMap. Nous enregistrons 5000 objets, nous les lisons. Tout est comme prévu.
@Test
void get5000() {
IMap map = hazelcastInstance.getMap("application");
UUID subscriberId = UUID.randomUUID();
for (int i = 0; i < 5000; i++) {
UUID id = UUID.randomUUID();
String title = RandomStringUtils.random(5);
Application application = new Application(id, title, subscriberId);
map.set(id, application);
Application retrieved = map.get(id);
assertEquals(id, retrieved.getId());
}
}Et cela ne fonctionne pas dans une transaction, nous obtenons ClassNotFoundException :
@Test
void get_transaction() {
IMap map = hazelcastInstance.getMap("application_t");
UUID subscriberId = UUID.randomUUID();
UUID id = UUID.randomUUID();
Application application = new Application(id, "qwer", subscriberId);
map.set(id, application);
Application retrievedOutside = map.get(id);
assertEquals(id, retrievedOutside.getId());
hazelcastInstance.executeTransaction(context -> {
HazelcastTransactionContextHolder.setContext(context);
try {
TransactionalMap transactionalMap = context.getMap("application_t");
Application retrievedInside = transactionalMap.get(id);
assertEquals(id, retrievedInside.getId());
return null;
} finally {
HazelcastTransactionContextHolder.clearContext();
}
});
}La version 3.8 a introduit le mĂ©canisme de dĂ©ploiement de classes utilisateur. Vous pouvez dĂ©signer un nĆud principal et mettre Ă jour le fichier JAR dessus.
Nous avons complĂštement changĂ© d'approche : nous sĂ©rialisons nous-mĂȘmes en JSON et sauvegardons dans Hazelcast. Hazelcast n'a pas besoin de connaĂźtre la structure de nos classes, et nous pouvons mettre Ă jour sans temps d'arrĂȘt. La gestion des versions des objets de domaine est effectuĂ©e par l'application. DiffĂ©rentes versions de l'application peuvent ĂȘtre exĂ©cutĂ©es simultanĂ©ment, et il peut y avoir des situations oĂč la nouvelle application Ă©crit des objets avec de nouveaux champs, tandis que l'ancienne ne connaĂźt pas encore ces champs. En mĂȘme temps, la nouvelle application lit des objets Ă©crits par l'ancienne application qui ne contiennent pas de nouveaux champs. Nous gĂ©rons de telles situations Ă l'intĂ©rieur de l'application, mais pour simplifier, nous ne changeons ni ne supprimons les champs, nous Ă©largissons simplement les classes en ajoutant de nouveaux champs.
Comment nous assurons une haute performance
Quatre accĂšs Ă Hazelcast â c'est bien, deux Ă la base de donnĂ©es â c'est mauvais
Il est toujours prĂ©fĂ©rable de rĂ©cupĂ©rer des donnĂ©es depuis le cache plutĂŽt que depuis la base de donnĂ©es, mais nous ne voulons pas non plus conserver des enregistrements inutilisĂ©s. La dĂ©cision sur ce que nous devons mettre en cache est reportĂ©e Ă la derniĂšre Ă©tape du dĂ©veloppement. Une fois que la nouvelle fonctionnalitĂ© est codĂ©e, nous activons dans PostgreSQL l'enregistrement de toutes les requĂȘtes (log_min_duration_statement Ă 0) et exĂ©cutons des tests de charge pendant environ 20 minutes. Les outils comme pgFouine et pgBadger peuvent crĂ©er des rapports analytiques Ă partir des journaux collectĂ©s. Dans les rapports, nous recherchons d'abord les requĂȘtes lentes et frĂ©quentes. Pour les requĂȘtes lentes, nous Ă©tablissons un plan d'exĂ©cution (EXPLAIN) et Ă©valuons s'il est possible d'accĂ©lĂ©rer cette requĂȘte. Les requĂȘtes frĂ©quentes avec les mĂȘmes donnĂ©es d'entrĂ©e se cachent bien dans le cache. Nous essayons de garder les requĂȘtes "plates", en limitant chaque requĂȘte Ă une seule table.
Exploitation
SV, en tant que service en ligne, a Ă©tĂ© mise en service au printemps 2017, et le produit distinct SV est sorti en novembre 2017 (Ă l'Ă©poque en version bĂȘta).
Plus d'un an d'exploitation, il n'y a eu aucun problÚme grave avec le fonctionnement du service en ligne SV. Nous surveillons le service en ligne via , collectons et déployons depuis .
Le package serveur SV est livré sous forme de paquets natifs : RPM, DEB, MSI. De plus, pour Windows, nous proposons un installateur unique sous la forme d'un seul EXE, qui installe le serveur, Hazelcast et Elasticsearch sur une seule machine. Au départ, nous appelions cette version d'installation "démonstration", mais il est désormais clair que c'est l'option de déploiement la plus populaire.
Source : habr.com
