Comment et pourquoi nous avons écrit un service évolutif à forte charge pour 1C : Entreprise : Java, PostgreSQL, Hazelcast

Dans cet article, nous parlerons de comment et pourquoi nous avons développé Système d'interaction – un mécanisme qui transfère les informations entre les applications clientes et les serveurs 1C:Enterprise - de la définition d'une tâche à la réflexion sur l'architecture et les détails de mise en œuvre.

Le système d'interaction (ci-après dénommé SV) est un système de messagerie distribué et tolérant aux pannes avec une livraison garantie. SV est conçu comme un service à charge élevée et hautement évolutif, disponible à la fois sous forme de service en ligne (fourni par 1C) et sous forme de produit produit en série pouvant être déployé sur vos propres serveurs.

SV utilise le stockage distribué Noisette et moteur de recherche ElasticSearch. Nous parlerons également de Java et de la façon dont nous mettons à l'échelle horizontalement PostgreSQL.
Comment et pourquoi nous avons écrit un service évolutif à forte charge pour 1C : Entreprise : Java, PostgreSQL, Hazelcast

Formulation du problème

Pour expliquer pourquoi nous avons créé le système d'interaction, je vais vous expliquer un peu comment fonctionne le développement d'applications métiers en 1C.

Pour commencer, un peu de nous pour ceux qui ne savent pas encore ce que nous faisons :) Nous réalisons la plateforme technologique 1C:Enterprise. La plateforme comprend un outil de développement d'applications métier, ainsi qu'un runtime qui permet aux applications métier de s'exécuter dans un environnement multiplateforme.

Paradigme de développement client-serveur

Les applications métiers créées sur 1C:Enterprise fonctionnent sur trois niveaux serveur client architecture « SGBD – serveur d'applications – client ». Code d'application écrit en langage 1C intégré, peut être exécuté sur le serveur d'application ou sur le client. Tous les travaux avec les objets applicatifs (répertoires, documents, etc.), ainsi que la lecture et l'écriture de la base de données, sont effectués uniquement sur le serveur. La fonctionnalité des formulaires et de l'interface de commande est également implémentée sur le serveur. Le client effectue la réception, l'ouverture et l'affichage de formulaires, la « communication » avec l'utilisateur (avertissements, questions...), les petits calculs dans des formulaires nécessitant une réponse rapide (par exemple multiplier le prix par la quantité), le travail avec des fichiers locaux, travailler avec du matériel.

Dans le code d'application, les en-têtes de procédures et de fonctions doivent indiquer explicitement où le code sera exécuté - en utilisant les directives &AtClient / &AtServer (&AtClient / &AtServer dans la version anglaise du langage). Les développeurs 1C vont maintenant me corriger en disant que les directives sont en fait plus, mais pour nous, ce n'est plus important maintenant.

Vous pouvez appeler le code serveur à partir du code client, mais vous ne pouvez pas appeler le code client à partir du code serveur. Il s’agit d’une limitation fondamentale que nous avons imposée pour un certain nombre de raisons. En particulier, parce que le code du serveur doit être écrit de telle manière qu'il s'exécute de la même manière, peu importe où il est appelé - depuis le client ou depuis le serveur. Et dans le cas d'un appel de code serveur à partir d'un autre code serveur, il n'y a pas de client en tant que tel. Et parce que lors de l'exécution du code du serveur, le client qui l'appelait pourrait se fermer, quitter l'application, et le serveur n'aurait plus personne à appeler.

Comment et pourquoi nous avons écrit un service évolutif à forte charge pour 1C : Entreprise : Java, PostgreSQL, Hazelcast
Code qui gère un clic sur un bouton : l'appel d'une procédure serveur depuis le client fonctionnera, l'appel d'une procédure client depuis le serveur ne fonctionnera pas

Cela signifie que si nous voulons envoyer un message du serveur à l'application client, par exemple, indiquant que la génération d'un rapport « de longue durée » est terminée et que le rapport peut être visualisé, nous n'avons pas une telle méthode. Vous devez utiliser des astuces, par exemple, interroger périodiquement le serveur à partir du code client. Mais cette approche charge le système d’appels inutiles et n’a généralement pas l’air très élégante.

Et il est également nécessaire, par exemple, lorsqu'un appel téléphonique arrive SIP- lors d'un appel, en informer l'application client afin qu'elle puisse utiliser le numéro de l'appelant pour le retrouver dans la base de données des contreparties et afficher à l'utilisateur les informations sur la contrepartie appelante. Ou, par exemple, lorsqu'une commande arrive à l'entrepôt, informez-en l'application cliente du client. En général, il existe de nombreux cas où un tel mécanisme serait utile.

La production elle-même

Créez un mécanisme de messagerie. Rapide, fiable, avec livraison garantie, avec la possibilité de rechercher des messages de manière flexible. Sur la base du mécanisme, implémentez une messagerie (messages, appels vidéo) fonctionnant dans les applications 1C.

Concevez le système pour qu’il soit évolutif horizontalement. La charge croissante doit être couverte en augmentant le nombre de nœuds.

exécution

Nous avons décidé de ne pas intégrer la partie serveur de SV directement dans la plateforme 1C:Enterprise, mais de la mettre en œuvre 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, dont la principale était que je voulais permettre l'échange de messages entre différentes applications 1C (par exemple, entre Trade Management et Accounting). Différentes applications 1C peuvent s'exécuter sur différentes versions de la plateforme 1C:Enterprise, être situées sur différents serveurs, etc. Dans de telles conditions, la mise en œuvre de SV en tant que produit distinct situé « sur le côté » des installations 1C est la solution optimale.

Nous avons donc décidé de faire de SV un produit distinct. Nous recommandons aux petites entreprises d'utiliser le serveur CB que nous avons installé dans notre cloud (wss://1cdialog.com) pour éviter les frais généraux associés à l'installation et à la configuration locales du serveur. Les gros clients peuvent juger judicieux d'installer leur propre serveur CB dans leurs installations. Nous avons utilisé une approche similaire dans notre produit cloud SaaS 1cFrais – il est produit en série pour être installé sur les sites des clients, et est également déployé dans notre cloud https://1cfresh.com/.

Application

Pour répartir la charge et la tolérance aux pannes, nous déploierons non pas une application Java, mais plusieurs, avec un équilibreur de charge devant elles. Si vous devez transférer un message d'un nœud à l'autre, utilisez la publication/abonnement dans Hazelcast.

La communication entre le client et le serveur se fait via websocket. Il est bien adapté aux systèmes temps réel.

Cache distribué

Nous avons choisi entre Redis, Hazelcast et Ehcache. Nous sommes en 2015. Redis vient de sortir un nouveau cluster (trop nouveau, effrayant), il y a Sentinel avec pas mal de restrictions. Ehcache ne sait pas s'assembler en cluster (cette fonctionnalité est apparue plus tard). Nous avons décidé de l'essayer avec Hazelcast 3.4.
Hazelcast est assemblé en cluster dès la sortie de la boîte. En mode nœud unique, il n'est pas très utile et ne peut être utilisé que comme cache - il ne sait pas comment vider les données sur le disque, si vous perdez le seul nœud, vous perdez les données. Nous déployons plusieurs Hazelcasts, entre lesquels nous sauvegardons les données critiques. Nous ne sauvegardons pas le cache – cela ne nous dérange pas.

Pour nous, Hazelcast c'est :

  • Stockage des sessions utilisateur. Il faut beaucoup de temps pour accéder à la base de données pour une session à chaque fois, nous mettons donc toutes les sessions dans Hazelcast.
  • Cache. Si vous recherchez un profil utilisateur, vérifiez le cache. J'ai écrit un nouveau message - mettez-le dans le cache.
  • Rubriques de communication entre les instances d'application. Le nœud génère un événement et le place dans le sujet Hazelcast. Les autres nœuds d'application abonnés à cette rubrique reçoivent et traitent l'événement.
  • Verrous de cluster. Par exemple, nous créons une discussion à l'aide d'une clé unique (discussion singleton au sein de la base de données 1C) :

conversationKeyChecker.check("БЕНЗОКОЛОНКА");

      doInClusterLock("БЕНЗОКОЛОНКА", () -> {

          conversationKeyChecker.check("БЕНЗОКОЛОНКА");

          createChannel("БЕНЗОКОЛОНКА");
      });

Nous avons vérifié qu'il n'y a pas de chaîne. Nous avons pris le verrou, l'avons vérifié à nouveau et l'avons créé. Si vous ne vérifiez pas le verrou après avoir pris le verrou, il est alors possible qu'un autre fil de discussion ait également vérifié à ce moment-là et essaie maintenant de créer la même discussion - mais elle existe déjà. Vous ne pouvez pas verrouiller à l'aide d'un verrouillage Java synchronisé ou régulier. Via la base de données - c'est lent et c'est dommage pour la base de données ; via Hazelcast - c'est ce dont vous avez besoin.

Choisir un SGBD

Nous avons une expérience vaste et réussie de travail avec PostgreSQL et de collaboration avec les développeurs de ce SGBD.

Ce n'est pas facile avec un cluster PostgreSQL - il y a XL, XC, agrumes, mais en général, ce ne sont pas des NoSQL prêts à l'emploi. Nous n'avons pas considéré NoSQL comme stockage principal, il nous a suffi de prendre Hazelcast, avec lequel nous n'avions pas travaillé auparavant.

Si vous avez besoin de faire évoluer une base de données relationnelle, cela signifie fragmentation. Comme vous le savez, avec le partitionnement, nous divisons la base de données en parties distinctes afin que chacune d'entre elles puisse être placée sur un serveur distinct.

La première version de notre partitionnement supposait la possibilité de distribuer chacune des tables de notre application sur différents serveurs dans des proportions différentes. Il y a beaucoup de messages sur le serveur A - s'il vous plaît, déplaçons une partie de cette table vers le serveur B. Cette décision criait simplement à une optimisation prématurée, nous avons donc décidé de nous limiter à une approche multi-tenant.

Vous pouvez en savoir plus sur le multi-tenant, par exemple, sur le site Web Données sur les agrumes.

SV a les concepts d'application et d'abonné. Une application est une installation spécifique d'une application métier, telle qu'un ERP ou une Comptabilité, avec ses utilisateurs et ses données métiers. Un abonné est une organisation ou un individu au nom duquel 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 locataire de notre système. Les messages de plusieurs abonnés peuvent être localisés dans une seule base de données physique ; si nous constatons qu'un abonné a commencé à générer beaucoup de trafic, nous le déplaçons vers une base de données physique distincte (ou même un serveur de base de données distinct).

Nous avons une base de données principale dans laquelle une table de routage est stockée avec des informations sur l'emplacement de toutes les bases de données d'abonnés.

Comment et pourquoi nous avons écrit un service évolutif à forte charge pour 1C : Entreprise : Java, PostgreSQL, Hazelcast

Pour éviter que la base de données principale ne constitue un goulot d'étranglement, nous conservons la table de routage (et d'autres données fréquemment nécessaires) dans un cache.

Si la base de données de l'abonné commence à ralentir, nous la découperons en partitions à l'intérieur. Sur d'autres projets que nous utilisons pg_pathman.

Puisque la perte des messages utilisateur est une mauvaise chose, nous maintenons nos bases de données avec des répliques. La combinaison de répliques synchrones et asynchrones permet de s'assurer en cas de perte de la base de données principale. La perte de message ne se produira que si la base de données principale et sa réplique synchrone échouent simultanément.

Si une réplique synchrone est perdue, la réplique asynchrone devient synchrone.
Si la base de données principale est perdue, le réplica synchrone devient la base de données principale et le réplica asynchrone devient un réplica synchrone.

Elasticsearch pour la recherche

Puisque, entre autres, SV est aussi un messager, il nécessite une recherche rapide, pratique et flexible, prenant en compte la morphologie, utilisant des correspondances imprécises. Nous avons décidé de ne pas réinventer la roue et d'utiliser le moteur de recherche gratuit Elasticsearch, créé à partir de la bibliothèque Lucene. Nous déployons également Elasticsearch dans un cluster (master – data – data) pour éliminer les problèmes en cas de panne des nœuds applicatifs.

Sur github nous avons trouvé Plugin de morphologie russe pour Elasticsearch et utilisez-le. Dans l'index Elasticsearch, nous stockons les racines des mots (que le plugin détermine) et les N-grammes. Lorsque l'utilisateur saisit le texte à rechercher, nous recherchons le texte saisi parmi les N-grammes. Une fois enregistré dans l'index, le mot « textes » sera divisé en N-grammes suivants :

[ceux, tek, tex, texte, textes, ek, ex, ext, textes, ks, kst, ksty, st, sty, vous],

Et la racine du mot « texte » sera également conservée. Cette approche vous permet de rechercher au début, au milieu et à la fin du mot.

La grande image

Comment et pourquoi nous avons écrit un service évolutif à forte charge pour 1C : Entreprise : Java, PostgreSQL, Hazelcast
Répétition de l'image du début de l'article, mais avec explications :

  • Balancer exposé sur Internet ; nous avons nginx, ça peut être n'importe lequel.
  • Les instances d'application Java communiquent entre elles via Hazelcast.
  • Pour travailler avec un socket Web, nous utilisons Netty.
  • L'application Java est écrite en Java 8 et se compose de bundles OSGi. Les plans incluent la migration vers Java 10 et la transition vers des modules.

Développement et test

Au cours du processus de développement et de test du SV, nous avons découvert un certain nombre de fonctionnalités intéressantes des produits que nous utilisons.

Tests de charge et fuites de mémoire

La sortie de chaque version SV implique des tests de charge. C'est réussi quand :

  • Le test a fonctionné pendant plusieurs jours et il n'y a eu aucune panne de service
  • Le temps de réponse pour les opérations clés n’a pas dépassé un seuil confortable
  • La détérioration des performances par rapport à la version précédente ne dépasse pas 10 %

Nous remplissons la base de données de test avec des données - pour ce faire, nous recevons des informations sur l'abonné le plus actif du serveur de production, multiplions ses chiffres par 5 (le nombre de messages, de discussions, d'utilisateurs) et le testons de cette façon.

Nous effectuons des tests de charge du système d'interaction dans trois configurations :

  1. test de stress
  2. Connexions uniquement
  3. Inscription des abonnés

Lors du stress test, nous lançons plusieurs centaines de threads, et ils chargent le système sans arrêt : rédaction de messages, création de discussions, réception d'une liste de messages. Nous simulons les actions d'utilisateurs ordinaires (obtenir une liste de mes messages non lus, écrire à quelqu'un) et de solutions logicielles (transmettre un package d'une configuration différente, traiter une alerte).

Par exemple, voici à quoi ressemble une partie du test de résistance :

  • L'utilisateur se connecte
    • Demande vos discussions non lues
    • 50 % de chances de lire des messages
    • 50 % de chances d'envoyer des SMS
    • Utilisateur suivant :
      • A 20 % de chances de créer une nouvelle discussion
      • Sélectionne au hasard l'une de ses discussions
      • Va à l'intérieur
      • Messages de requêtes, profils d'utilisateurs
      • Crée cinq messages adressés à des utilisateurs aléatoires à partir de cette discussion
      • Quitte la discussion
      • Se répète 20 fois
      • Se déconnecte, revient au début du script

    • Un chatbot entre dans le système (émule la messagerie du code de l'application)
      • A 50 % de chances de créer un nouveau canal d’échange de données (discussion spéciale)
      • 50 % de chances d'écrire un message sur l'un des canaux existants

Le scénario « Connexions uniquement » est apparu pour une raison. Il existe une situation : les utilisateurs ont connecté le système, mais ne se sont pas encore impliqués. Chaque utilisateur allume l'ordinateur à 09h00 du matin, établit une connexion au serveur et reste silencieux. Ces types sont dangereux, ils sont nombreux - les seuls paquets dont ils disposent sont PING/PONG, mais ils gardent la connexion au serveur (ils ne peuvent pas la maintenir - et s'il y a un nouveau message). Le test reproduit une situation dans laquelle un grand nombre de ces utilisateurs tentent de se connecter au système en une demi-heure. C'est similaire à un test de résistance, mais il se concentre précisément sur cette première entrée - afin qu'il n'y ait pas d'échec (une personne n'utilise pas le système et il tombe déjà en panne - il est difficile de penser à quelque chose de pire).

Le script d'inscription des abonnés démarre dès le premier lancement. Nous avons effectué un test de résistance et étions sûrs que le système ne ralentissait pas pendant la correspondance. Mais les utilisateurs sont venus et l’enregistrement a commencé à échouer en raison d’un délai d’attente. Lors de l'inscription, nous avons utilisé / dev / aléatoire, qui est liée à l'entropie du système. Le serveur n'a pas eu le temps d'accumuler suffisamment d'entropie et lorsqu'un nouveau SecureRandom était demandé, il se figeait pendant des dizaines de secondes. Il existe de nombreuses façons de sortir de cette situation, par exemple : passer au moins sécurisé /dev/urandom, 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 du pool, mais depuis lors, nous effectuons un test distinct pour l'enregistrement de nouveaux abonnés.

Nous utilisons comme générateur de charge Jmètre. Il ne sait pas comment travailler avec websocket ; il a besoin d'un plugin. Les premiers résultats de recherche pour la requête « jmeter websocket » sont : articles de BlazeMeter, qui recommande plugin par Maciej Zaleski.

C'est par là que nous avons décidé de commencer.

Presque immédiatement après avoir commencé des tests sérieux, nous avons découvert que JMeter commençait à perdre de la mémoire.

Le plugin est une grande histoire à part ; avec 176 étoiles, il compte 132 forks sur github. L'auteur lui-même ne s'y est pas engagé depuis 2015 (nous l'avons pris en 2015, puis cela n'a pas éveillé de soupçons), plusieurs problèmes de github concernant des fuites de mémoire, 7 pull request non fermées.
Si vous décidez d'effectuer des tests de charge à l'aide de ce plugin, veuillez prêter attention aux discussions suivantes :

  1. Dans un environnement multithread, une LinkedList standard a été utilisée et le résultat a été NPE au moment de l'exécution. Cela peut être résolu soit en passant à ConcurrentLinkedDeque, soit par des blocs synchronisés. Nous avons choisi la première option pour nous-mêmes (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Fuite de mémoire ; lors de la déconnexion, les informations de connexion ne sont pas supprimées (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. En mode streaming (lorsque le websocket n'est pas fermé à la fin de l'échantillon, mais est utilisé plus tard dans le plan), les modèles de réponse ne fonctionnent pas (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

C'est l'un de ceux sur github. Ce que nous avons fait:

  1. Ont pris fourchette Elyran Kogan (@elyrank) – il résout les problèmes 1 et 3
  2. Problème résolu 2
  3. Jetée mise à jour du 9.2.14 au 9.3.12
  4. Encapsulé SimpleDateFormat dans ThreadLocal ; SimpleDateFormat n'est pas thread-safe, ce qui a conduit à NPE au moment de l'exécution
  5. Correction d'une autre fuite de mémoire (la connexion était mal fermée lors de la déconnexion)

Et pourtant ça coule !

La mémoire a commencé à s'épuiser non pas en un jour, mais en deux. Il ne restait absolument plus de temps, nous avons donc décidé de lancer moins de threads, mais sur quatre agents. Cela aurait dû suffire pour au moins une semaine.

Deux jours se sont écoulés...

Maintenant, Hazelcast manque de mémoire. Les journaux ont montré qu'après quelques jours de tests, Hazelcast a commencé à se plaindre d'un manque de mémoire, et après un certain temps, le cluster s'est effondré et les nœuds ont continué à mourir un par un. Nous avons connecté JVisualVM à Hazelcast et avons vu une "scie montante" - elle appelait régulièrement le GC, mais ne parvenait pas à effacer la mémoire.

Comment et pourquoi nous avons écrit un service évolutif à forte charge pour 1C : Entreprise : Java, PostgreSQL, Hazelcast

Il s'est avéré que dans Hazelcast 3.4, lors de la suppression d'une carte/multiMap (map.destroy()), la mémoire n'est pas complètement libérée :

github.com/hazelcast/hazelcast/issues/6317
github.com/hazelcast/hazelcast/issues/4888

Le bug est maintenant corrigé dans la version 3.5, mais c'était un problème à l'époque. Nous avons créé de nouveaux multiMaps avec des noms dynamiques et les avons supprimés selon notre logique. Le code ressemblait à ceci :

public void join(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.put(auth.getUserId(), auth);
}

public void leave(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.remove(auth.getUserId(), auth);

    if (sessions.size() == 0) {
        sessions.destroy();
    }
}

Вызов :

service.join(auth1, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");
service.join(auth2, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");

multiMap a été créé pour chaque abonnement et supprimé lorsqu'il n'était pas nécessaire. Nous avons décidé de lancer Map , la clé sera le nom de l'abonnement, et les valeurs seront des identifiants de session (à partir desquels vous pourrez ensuite obtenir des identifiants 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 améliorés.

Comment et pourquoi nous avons écrit un service évolutif à forte charge pour 1C : Entreprise : Java, PostgreSQL, Hazelcast

Qu'avons-nous appris d'autre sur les tests de charge ?

  1. JSR223 doit être écrit en langage groovy et inclure un cache de compilation - c'est beaucoup plus rapide. Lien.
  2. Les graphiques Jmeter-Plugins sont plus faciles à comprendre que les graphiques standard. Lien.

À propos de 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, maintenant notre serveur de production exécute la version 3.9.2 (au moment de la rédaction, la dernière version de Hazelcast est la 3.10).

Génération d'identifiant

Nous avons commencé avec des identifiants entiers. Imaginons que nous ayons besoin d'un autre Long pour une nouvelle entité. La séquence dans la base de données n'est pas adaptée, les tables sont impliquées dans le sharding - il s'avère qu'il y a un ID de message=1 dans DB1 et un ID de message=1 dans DB2, vous ne pouvez pas mettre cet ID dans Elasticsearch, ni dans Hazelcast , mais le pire est que vous souhaitiez combiner les données de deux bases de données en une seule (par exemple, décider qu'une seule base de données suffit pour ces abonnés). Vous pouvez ajouter plusieurs AtomicLongs à Hazelcast et y conserver le compteur, puis la performance d'obtention d'un nouvel identifiant est IncreaseAndGet plus le temps d'une requête à Hazelcast. Mais Hazelcast a quelque chose de plus optimal : FlakeIdGenerator. Lorsqu'ils contactent chaque client, ils reçoivent une plage d'identifiants, par exemple le premier – de 1 à 10 000, le second – de 10 001 à 20 000, et ainsi de suite. Désormais, le client peut émettre lui-même de nouveaux identifiants jusqu'à la fin de la plage qui lui a été attribuée. Cela fonctionne rapidement, mais lorsque vous redémarrez l'application (et le client Hazelcast), une nouvelle séquence commence - d'où les sauts, etc. De plus, les développeurs ne comprennent pas vraiment pourquoi les identifiants sont des nombres entiers, mais sont si incohérents. Nous avons tout pesé et sommes passés aux UUID.

À propos, pour ceux qui veulent ressembler à Twitter, il existe une telle bibliothèque Snowcast - il s'agit d'une implémentation de Snowflake au-dessus de Hazelcast. Vous pouvez voir ça ici:

github.com/noctarius/snowcast
github.com/twitter/flocon de neige

Mais nous n’y sommes plus parvenus.

TransactionalMap.replace

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();
        }        
    });
}

Expected : newValue
Actual : oldValue

J'ai dû écrire mon propre remplacement 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] Replacing value in a transactional map");
        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] Replacing value in a not transactional map");
    IMap<K, V> map = hazelcastInstance.getMap(mapName);
    return map.replace(key, oldValue, newValue);
}

Testez non seulement les structures de données classiques, mais également leurs versions transactionnelles. Il arrive qu'IMap fonctionne, mais TransactionalMap n'existe plus.

Insérez un nouveau JAR sans temps d'arrêt

Tout d’abord, nous avons décidé d’enregistrer les objets de nos cours dans Hazelcast. Par exemple, nous avons une classe Application, nous voulons la sauvegarder et la lire. Sauvegarder:

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
map.set(id, application);

Lisez:

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
return map.get(id);

Tout fonctionne. Ensuite, nous avons décidé de créer un index dans Hazelcast pour rechercher par :

map.addIndex("subscriberId", false);

Et lors de l'écriture d'une nouvelle entité, ils ont commencé à recevoir ClassNotFoundException. Hazelcast a essayé d'ajouter à l'index, mais ne savait rien de notre classe et voulait qu'un JAR avec cette classe lui soit fourni. C'est exactement ce que nous 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 récupère pas le nouveau JAR lors d'une mise à jour nœud par nœud. À ce stade, nous avons décidé que nous pouvions vivre sans recherche d'index. Après tout, si vous utilisez Hazelcast comme magasin clé-valeur, alors tout fonctionnera ? Pas vraiment. Là encore le comportement d'IMap et de TransactionalMap est différent. Là où IMap s'en fiche, TransactionalMap renvoie une erreur.

IMap. Nous écrivons 5000 objets, les lisons. Tout est attendu.

@Test
void get5000() {
    IMap<UUID, Application> 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());
    }
}

Mais cela ne fonctionne pas dans une transaction, nous obtenons une ClassNotFoundException :

@Test
void get_transaction() {
    IMap<UUID, Application> 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<UUID, Application> transactionalMap = context.getMap("application_t");
            Application retrievedInside = transactionalMap.get(id);

            assertEquals(id, retrievedInside.getId());
            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }
    });
}

Dans la version 3.8, le mécanisme de déploiement de classes d'utilisateurs est apparu. Vous pouvez désigner un nœud maître et mettre à jour le fichier JAR dessus.

Maintenant, nous avons complètement changé notre approche : nous le sérialisons nous-mêmes en JSON et le 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 contrôlée par l'application. Différentes versions de l'application peuvent s'exécuter en même temps, et une situation est possible lorsque la nouvelle application écrit des objets avec de nouveaux champs, mais que l'ancienne ne connaît pas encore ces champs. Et en même temps, la nouvelle application lit les objets écrits par l'ancienne application qui n'ont pas de nouveaux champs. Nous traitons de telles situations dans l'application, mais par souci de simplicité, nous ne modifions ni ne supprimons les champs, nous élargissons uniquement les classes en ajoutant de nouveaux champs.

Comment nous garantissons des performances élevées

Quatre voyages vers Hazelcast - bons, deux vers la base de données - mauvais

Il est toujours préférable d’accéder au cache pour les données plutôt que d’accéder à la base de données, mais vous ne souhaitez pas non plus stocker les enregistrements inutilisés. Nous laissons la décision sur ce qu'il faut mettre en cache jusqu'à la dernière étape du développement. Lorsque la nouvelle fonctionnalité est codée, nous activons la journalisation de toutes les requêtes dans PostgreSQL (log_min_duration_statement à 0) et exécutons des tests de charge pendant 20 minutes. En utilisant les journaux collectés, des utilitaires comme pgFouine et pgBadger peuvent créer des rapports analytiques. Dans les rapports, nous recherchons principalement les requêtes lentes et fréquentes. Pour les requêtes lentes, nous construisons un plan d'exécution (EXPLAIN) et évaluons si une telle requête peut être accélérée. Les requêtes fréquentes pour les mêmes données d'entrée s'intègrent bien dans le cache. Nous essayons de garder les requêtes « plates », une table par requête.

exploitation

SV en tant que service en ligne a été mis en service au printemps 2017 et en tant que produit distinct, SV a été lancé en novembre 2017 (à l'époque en version bêta).

En plus d'un an d'exploitation, aucun problème grave n'a été constaté dans le fonctionnement du service CB en ligne. Nous surveillons le service en ligne via Zabbix, collecter et déployer à partir de Bambou.

La distribution du serveur SV est fournie sous forme de packages natifs : RPM, DEB, MSI. De plus, pour Windows, nous fournissons un seul programme d'installation sous la forme d'un seul EXE qui installe le serveur, Hazelcast et Elasticsearch sur une seule machine. Nous avions initialement appelé cette version de l'installation la version « démo », mais il est désormais clair qu'il s'agit de l'option de déploiement la plus populaire.

Source: habr.com

Ajouter un commentaire