La réplication fournit la tolérance aux pannes et augmente les performances d'une topologie eXtreme Scale répartie. La réplication est activée en associant des mappes de sauvegarde à un groupe de mappes.
Un groupe de mappes est un ensemble de mappes catégorisées par une clé de partition. Cette clé de partition provient de la clé de la mappe en prenant son hachage modulo le nombre de partitions. Si un groupe de mappes au sein du groupe de mappes a une clé de partition X, ces mappes sont stockées dans la partition X correspondant dans la grille de données. Si un autre groupe a une clé de partition Y, toutes les mappes sont stockées dans la partition Y, et ainsi de suite. Les données dans les mappes sont répliquées en fonction de la stratégie définie dans le groupe de mappes. La réplication a lieu dans des topologies distribuées.
Les groupes de mappes sont affectés du nombre de partition et d'une règle de réplication. La configuration de la réplication du groupe de mappes identifie le nombre de fragments de réplique synchrone et asynchrone que doit avoir le groupe de mappes en plus du fragment primaire. Par exemple, s'il existe une réplique synchrone et une réplique asynchrone, chacune des mappes de sauvegarde BackingMaps affectée au groupe de mappes ont un fragment de réplique distribué automatiquement dans le groupe de serveurs de conteneur disponibles pour la grille de données. La configuration de la réplication peut également permettre aux clients de lire les données depuis les serveurs répliqués de manière synchrone. Cela peut étaler la charge des demandes de lecture sur d'autres serveurs de la grille eXtreme Scale. La réplication a un impact sur le modèle de programmation lorsque vous préchargez les mappes de sauvegarde.
void preloadMap(Session session, BackingMap backingMap) throws LoaderException;
Les mappes peuvent être partitionnées en N partitions. Elles peuvent donc s'étendre sur plusieurs serveurs, chaque entrée étant identifiée par une clé qui n'est stockée que sur l'un de ces serveurs. Les mappes de très grande taille peuvent être détenues sur un eXtreme Scale car l'application n'est plus limitée par la taille du segment d'une seule JVM pour contenir toutes les entrées d'une mappe. Les applications qui veulent effectuer un préchargement avec la méthode preloadMap de l'interface Loader doivent identifier le sous-ensemble des données à précharger. Les partitions existent toujours en nombre fixe. Il est possible de déterminer ce nombre à l'aide de cet exemple de code :
int numPartitions = backingMap.getPartitionManager().getNumOfPartitions();
int myPartition = backingMap.getPartitionId();
Cet exemple de code montre qu'une application peut identifier le sous-ensemble des données préchargées depuis la base de données. Les applications doivent toujours utiliser ces méthodes
même si la mappe n'est pas initialement partitionnée. Ces méthodes permettent
la flexibilité : si la mappe est ultérieurement partitionnée par les administrateurs,
le chargeur continuera à opérer correctement.L'application doit émettre des requêtes pour extraire du dorsal le sous-ensemble myPartition. Si une base de données est utilisée, il peut être plus facile d'avoir une colonne avec l'identificateur de partition d'un enregistrement donné à moins qu'il n'y ait une requête naturelle permettant aux données de la table de se partitionner facilement.
L'implémentation du préchargement copie dans la mappe les données à partir du dorsal en stockant plusieurs objets dans la mappe en une seule transaction. Le nombre optimal d'enregistrements à stocker par transaction dépend de plusieurs facteurs, notamment la complexité et la taille. Ainsi, après que la transaction comprend des blocs de plus de 100 entrées, les avantages en termes de performances diminuent au fur et à mesure que l'on augmente le nombre des entrées. Pour déterminer le nombre optimal, commencez par 100 entrées, puis augmentez le nombre jusqu'à ce que les performances deviennent nulles. Les transactions de grande taille donnent de meilleures performances de réplication. N'oubliez pas que seul le fragment primaire exécute le code de préchargement. Les données préchargées sont répliquées depuis le fragment primaire vers les fragments réplique qui sont en ligne.
Si l'application utilise un groupe de mappes avec plusieurs mappes, chaque mappe a son propre chargeur. Chaque chargeur a une méthode de préchargement. Chaque mappe est chargée en série par eXtreme Scale. Ce sera plus efficace de précharger toutes les mappes en désignant une mappe comme la mappe de préchargement. Ce processus est une convention d'application. On pourrait, par exemple, avoir deux mappes, Department et Employee, qui utilisent le chargeur Department pour précharger les deux mappes. Cette procédure garantit que, de manière transactionnelle, si une application veut un département, les salariés de ce département seront dans le cache. Lorsque le chargeur Department précharge un département depuis le système dorsal, il récupère également les salariés de ce département. L'objet Department et les objets Employee qui lui sont associés sont ajoutés à la mappe à l'aide d'une seule transaction.
Il arrive que les clients aient des ensembles de données de très grosse taille et qui nécessitent d'être mis en cache. Précharger ces données peut prendre énormément de temps. Parfois, le préchargement doit être terminé pour que l'application puisse aller en ligne. C'est là que rendre récupérable le préchargement devient intéressant. Supposons qu'il y ait un million d'enregistrements à précharger. Le fragment primaire les télécharge et échoue au 800 000e enregistrement. En principe, le fragment réplique choisi pour être le nouveau fragment primaire efface tout état répliqué et repart du début. eXtreme Scale peut utiliser une interface ReplicaPreloadController. Le chargeur de l'application aura également besoin d'implémenter cette interface. Notre exemple ajoute une seule méthode au chargeur : Status checkPreloadStatus(Session session, BackingMap bmap);. Cette méthode est appelée par l'environnement d'exécution eXtreme Scale avant la méthode de préchargement de l'interface Loader. eXtreme Scale teste le résultat de cette méthode (Status) pour déterminer son comportement au cas où un fragment réplique passe au statut de fragment primaire.
Valeur de statut retournée | Réponse d'eXtreme Scale |
---|---|
Status.PRELOADED_ALREADY | eXtreme Scale n'appelle pas du tout la méthode de préchargement car ce statut indique que la mappe est complètement préchargée. |
Status.FULL_PRELOAD_NEEDED | eXtreme Scale efface la mappe et appelle de manière normale la méthode de préchargement. |
Status.PARTIAL_PRELOAD_NEEDED | eXtreme Scale laisse la mappe comme elle est et appelle la méthode de préchargement. Cette stratégie permet au chargeur de l'application de continuer le préchargement à partir du point où il en était resté. |
A l'évidence, lorsqu'un fragment primaire précharge la mappe, il doit laisser un état dans une mappe du groupe de mappes à répliquer pour que la réplique détermine l'état à retourner. Vous pouvez utiliser une mappe supplémentaire appelée, par exemple, RecoveryMap. Cette mappe doit faire partie du groupe de mappes à précharger pour que la mappe soit répliquée de manière cohérente avec les données à précharger. Nous allons suggérer une implémentation.
Lorsque le préchargement valide chaque bloc d'enregistrements, dans le cadre de cette transaction, le processus actualise également un compteur ou une valeur dans la RecoveryMap. Les données préchargées et celles de la RecoveryMap sont répliquées de manière atomique vers les fragments réplique. Lorsque le fragment réplique passe au statut de fragment primaire, il est à présent en mesure de vérifier la RecoveryMap pour voir ce qui s'est passé.
La RecoveryMap peut contenir une seule entrée avec la clé d'état. Si aucun objet n'existe pour cette clé, vous avez besoin d'une méthode complète preload (checkPreloadStatus returns FULL_PRELOAD_NEEDED). Si un objet existe pour cette clé d'état et que la valeur est COMPLETE, le préchargement prend fin et la méthode checkPreloadStatus retourne PRELOADED_ALREADY. Autrement, l'objet value indique le point de redémarrage du préchargement et la méthode checkPreloadStatus retourne PARTIAL_PRELOAD_NEEDED. Le chargeur peut stocker le point de récupération dans une variable d'instance du chargeur de manière à ce que, lorsque le préchargement est appelé, le chargeur sache d'où partir. La RecoveryMap peut également détenir une entrée par mappe si chacune des mappes est préchargée de manière indépendante.
L'environnement d'exécution d'eXtreme Scale est conçu pour ne pas perdre de données validées en cas de défaillance du fragment primaire. Nous allons voir quels algorithmes sont utilisés à cet effet. Ces algorithmes ne s'appliquent que lorsqu'un groupe de réplication utilise la réplication synchrone. L'usage d'un chargeur n'est pas obligatoire.
Il est possible de configurer l'environnement d'exécution d'eXtreme Scale pour répliquer de manière synchrone vers les fragments réplique toutes les modifications d'un fragment primaire. Lorsqu'il est placé, un fragment réplique synchrone reçoit une copie des données existant dans le fragment primaire. Pendant ce temps, le fragment primaire continue de recevoir des transactions qu'il copie de manière asynchrone vers le fragment de réplique. A ce moment-là, le fragment réplique n'est pas encore considéré comme étant en ligne.
Une fois que le fragment réplique a rattrapé le fragment primaire, il passe en mode homologue et la réplication synchrone peut commencer. Toute transaction validée sur le fragment primaire est envoyée aux fragments réplique synchrones et le fragment primaire attend la réponse de chacun de ces fragments. Une séquence de validation synchrone avec un chargeur dans le fragment primaire ressemble à la procédure suivante :
Avec chargeur | Sans chargeur |
---|---|
Obtention des verrous pour les entrées | Identique |
Vidage des modifications vers le chargeur | "No operation" |
Enregistrement des modifications dans le cache | Identique |
Envoi des modifications aux fragments de réplique et attente d'accusé de réception | Identique |
Validation vers le chargeur via le plug-in TransactionCallback | Appel de validation de plug-in, mais sans effet |
Libération des verrous pour les entrées | Identique |
Vous remarquerez que les modifications sont envoyées aux fragments réplique avant d'être validées vers le chargeur. Pour déterminer lorsque les modifications sont validées dans le fragment réplique, modifiez cette séquence : lors de l'initialisation, initialisez de la manière suivante les listes tx dans le fragment primaire.
CommitedTx = {}, RolledBackTx = {}
Pendant le traitement synchrone des validations, utilisez la séquence suivante :
Avec chargeur | Sans chargeur |
---|---|
Obtention des verrous pour les entrées | Identique |
Vidage des modifications vers le chargeur | "No operation" |
Enregistrement des modifications dans le cache | Identique |
Envoi des modifications avec une transaction validée, annulation de la transaction dans le fragment de réplique et attente d'accusé de réception | Identique |
Effacement de la liste des transactions validées et des transactions annulées | Identique |
Validation vers le chargeur via le plug-in TransactionCallBack | La validation via le plug-in TransactionCallBack est toujours appelée, mais en principe sans effet |
Si la validation réussit, ajout de la transaction aux transactions validées, sinon, ajout aux transactions annulées | "No operation" |
Libération des verrous pour les entrées | Identique |
Pour le traitement des fragments réplique, utilisez la séquence suivante :
Vous remarquerez que, dans le fragment réplique, il ne se passe aucune interaction avec le chargeur tant que le fragment réplique est en mode réplique. C'est au fragment primaire d'envoyer toutes les modifications via le chargeur. La réplique n'envoie pas de modifications. Un effet collatéral de cet algorithme est que le fragment réplique dispose toujours des transactions, mais celles-ci ne sont validées qu'après que la transaction primaire suivante a envoyé le statut de validation de ces transactions. Les transactions sont alors validées ou annulées dans le fragment réplique. Jusque-là, les transactions ne sont pas validées. Il est possible d'ajouter dans le fragment primaire un minuteur qui envoie le résultat de la transaction après un bref délai (quelques secondes). Ce minuteur limite, sans l'éliminer tout à fait, le décalage de ce créneau. Ce décalage n'est un problème qu'en mode de lecture de réplique. Sinon, il n'a aucun impact sur l'application.
Lorsque le fragment primaire échoue, c'est vraisemblablement que quelques transactions ont été validées ou annulées dans ce fragment primaire, mais que le message n'a jamais été transmis au fragment réplique avec ces résultats. Lorsqu'un fragment réplique passe au rang de nouveau fragment primaire, l'une de ses premières actions est de gérer cette situation. Chaque transaction en attente est traitée à nouveau par rapport à l'ensemble de mappes du nouveau fragment primaire. S'il y a un chargeur, chaque transaction est remise à ce dernier. Ces transactions sont appliquées dans un ordre FIFO strict. Si une transaction échoue, elle est ignorée. Si trois transactions A, B et C sont en attente, A peut être validée, B peut être annulée et C peut être aussi validée. Aucune de ces trois transactions n'a d'impact sur les autres. Supposons qu'elles soient indépendantes.
Lorsqu'il se trouve en mode de reprise par basculement, un chargeur pourra vouloir utiliser une logique légèrement différente de celle utilisée en mode normal. L'implémentation de l'interface ReplicaPreloadController permet au chargeur de savoir facilement lorsqu'il est en mode de reprise par basculement. La méthode checkPreloadStatus n'est appelée que lorsque la reprise par basculement est terminée. Par conséquent, si la méthode apply de l'interface Loader est appelée avant la méthode checkPreloadStatus, il y a une transaction de récupération. Après l'appel de la méthode checkPreloadStatus, la reprise par basculement est terminée.
eXtreme Scale, sauf configuration différente, envoie au serveur primaire toutes les demandes de lecture et d'écriture concernant un groupe de réplication donné. Ce serveur primaire doit servir toutes les demandes émanant des clients. Vous voudrez peut-être autoriser l'envoi des demandes de lecture aux répliques du fragment primaire. L'envoi des demandes de lecture aux fragments réplique permet à la charge de ces demandes d'être partagées par plusieurs machines virtuelles Java. Cela dit, il faut savoir que l'utilisation des fragments réplique pour les demandes de lecture peut donner des réponses incohérentes.
Equilibrer la charge entre les fragments réplique s'utilise en général uniquement lorsque les clients mettent en cache des données qui changent en permanence ou lorsque les clients utilisent le verrouillage pessimiste.
Si les données changent en permanence et qu'elles sont ensuite invalidées dans les caches locaux du client, le fragment primaire devrait constater en résultat un taux relativement élevé de demandes get provenant des clients. De même, en mode de verrouillage pessimiste, il n'existe aucun cache local, c'est pourquoi toutes les demandes sont envoyées au fragment primaire.
Si les données sont relativement statiques ou que le mode pessimiste n'est pas utilisé, l'envoi des demandes de lecture au fragment de réplique n'a pas un énorme impact sur les performances. La fréquence des demandes get émanant des clients avec des caches pleins de données n'est pas élevée.
Lors du premier démarrage d'un client, son cache local est vide. Les demandes adressées au cache vide sont transmises au fragment primaire. Au fil du temps, le cache client obtient des données, ce qui fait tomber la charge des demandes. Si un grand nombre de clients démarrent simultanément, la charge peut être importante et la lecture des fragments de réplique peut être un choix approprié.
Avec eXtreme Scale, vous pouvez répliquer une mappe serveur vers un ou plusieurs clients à l'aide de la réplication asynchrone. Un client peut demander une copie locale en lecture seule d'une mappe côté serveur à l'aide de la méthode ClientReplicableMap.enableClientReplication.
void enableClientReplication(Mode mode, int[] partitions, ReplicationMapListener listener) throws ObjectGridException;
Le premier paramètre est le mode de réplication. Il peut s'agir d'une réplication continue ou d'une réplication instantanée. Le deuxième paramètre est une matrice de partitions représentant les partitions à partir desquelles la réplication doit se faire. Si la valeur est nulle ou si la matrice est vide, les données sont répliquées à partir de toutes les partitions. Le dernier paramètre est programme d'écoute permettant de recevoir les événements de réplication du client. Pour plus d'informations, voir les sections sur ClientReplicableMap et ReplicationMapListener dans la documentation relative aux API.
Une fois la réplication activée, le serveur démarre le processus de réplication de la mappe vers le client. A tout moment, le client est en retard de quelques transactions seulement par rapport au serveur.