Partager via


Persistance des grains ADO.NET

Le code back-end de stockage relationnel dans Orleans repose sur une fonctionnalité ADO.NET générique et ne dépend donc pas du fournisseur de base de données. La disposition du stockage de données Orleans a déjà été expliquée dans les tables de runtime. La configuration des chaînes de connexion est effectuée comme indiqué dans le Guide de configuration d’Orleans.

Pour que le code Orleans fonctionne avec un back-end de base de données relationnelle déterminé, les conditions suivantes doivent être réunies :

  1. La bibliothèque ADO.NET appropriée doit être chargée dans le processus. Cela se définit comme d’habitude, c’est-à-dire via l’élément DbProviderFactories dans la configuration de l’application.
  2. L’invariant ADO.NET doit être configuré via la propriété Invariant dans les options.
  3. La base de données doit exister et être compatible avec le code. Pour ce faire, il convient d’exécuter un script de création de base de données propre au fournisseur. Pour plus d’informations, consultez Configuration d’ADO.NET.

Le fournisseur de stockage de grains ADO .NET vous permet de stocker l’état des grains dans les bases de données relationnelles. Pour l’heure, les bases de données prises en charge sont les suivantes :

  • SQL Server
  • MySQL/MariaDB
  • PostgreSQL
  • Oracle

Pour commencer, installez le package de base :

Install-Package Microsoft.Orleans.Persistence.AdoNet

Lisez l’article Configuration d’ADO.NET pour savoir comment configurer votre base de données, notamment l’invariant ADO.NET et les scripts d’installation correspondants.

Voici un exemple de configuration d’un fournisseur de stockage ADO.NET via ISiloHostBuilder :

var siloHostBuilder = new HostBuilder()
    .UseOrleans(c =>
    {
        c.AddAdoNetGrainStorage("OrleansStorage", options =>
        {
            options.Invariant = "<Invariant>";
            options.ConnectionString = "<ConnectionString>";
            options.UseJsonFormat = true;
        });
    });

Dans l’absolu, vous avez seulement besoin de définir la chaîne de connexion propre au fournisseur de base de données et d’un Invariant (voir Configuration d’ADO.NET) qui identifie le fournisseur. Vous pouvez aussi choisir le format dans lequel les données sont enregistrées, à savoir binaire (par défaut), JSON ou XML. Même si le format binaire est le plus compact, il est aussi le plus opaque : vous ne pourrez ni lire ni utiliser les données. JSON est l'option recommandée.

Vous pouvez définir les propriétés suivantes via AdoNetGrainStorageOptions :

/// <summary>
/// Options for AdoNetGrainStorage
/// </summary>
public class AdoNetGrainStorageOptions
{
    /// <summary>
    /// Define the property of the connection string
    /// for AdoNet storage.
    /// </summary>
    [Redact]
    public string ConnectionString { get; set; }

    /// <summary>
    /// Set the stage of the silo lifecycle where storage should
    /// be initialized.  Storage must be initialized prior to use.
    /// </summary>
    public int InitStage { get; set; } = DEFAULT_INIT_STAGE;
    /// <summary>
    /// Default init stage in silo lifecycle.
    /// </summary>
    public const int DEFAULT_INIT_STAGE =
        ServiceLifecycleStage.ApplicationServices;

    /// <summary>
    /// The default ADO.NET invariant will be used for
    /// storage if none is given.
    /// </summary>
    public const string DEFAULT_ADONET_INVARIANT =
        AdoNetInvariants.InvariantNameSqlServer;

    /// <summary>
    /// Define the invariant name for storage.
    /// </summary>
    public string Invariant { get; set; } =
        DEFAULT_ADONET_INVARIANT;

    /// <summary>
    /// Determine whether the storage string payload should be formatted in JSON.
    /// <remarks>If neither <see cref="UseJsonFormat"/> nor <see cref="UseXmlFormat"/> is set to true, then BinaryFormatSerializer will be configured to format the storage string payload.</remarks>
    /// </summary>
    public bool UseJsonFormat { get; set; }
    public bool UseFullAssemblyNames { get; set; }
    public bool IndentJson { get; set; }
    public TypeNameHandling? TypeNameHandling { get; set; }

    public Action<JsonSerializerSettings> ConfigureJsonSerializerSettings { get; set; }

    /// <summary>
    /// Determine whether storage string payload should be formatted in Xml.
    /// <remarks>If neither <see cref="UseJsonFormat"/> nor <see cref="UseXmlFormat"/> is set to true, then BinaryFormatSerializer will be configured to format storage string payload.</remarks>
    /// </summary>
    public bool UseXmlFormat { get; set; }
}

Si la persistance ADO.NET dispose de la fonctionnalité permettant de versionner les données et de définir des (dé)sérialiseurs arbitraires avec des règles d’application arbitraires et le streaming, pour l’heure, il n’existe aucune méthode pour l’exposer au code d’application.

Logique de la persistance ADO.NET

Les principes du stockage persistant soutenus par ADO.NET sont les suivants :

  1. Préserver la sécurité et l’accessibilité des données critiques pour l’entreprise alors que les données, le format des données et le code évoluent.
  2. Tirer parti des fonctionnalités propres au fournisseur et au stockage.

Dans la pratique, cela consiste à adhérer aux objectifs d’implémentation d’ADO.NET et à une certaine logique d’implémentation ajoutée au niveau des fournisseurs de stockage propres à ADO.NET qui autorisent une évolution de la forme des données dans le stockage.

Outre les fonctionnalités habituelles du fournisseur de stockage, le fournisseur ADO.NET propose des capacités intégrées permettant de :

  1. Faire passer les données de stockage d’un format à un autre (par exemple, de JSON au format binaire) lorsque l’état fait l’objet d’allers-retours.
  2. Former le type à enregistrer ou à lire à partir du stockage de manières arbitraires. Cela permet à la version de l’état d’évoluer.
  3. Transmettre en continu les données hors de la base de données.

Les points 1. et 2. peuvent tous deux être appliqués selon des paramètres de décision arbitraire, tels que l’ID de grain, le type de grain, les données de charge utile.

Cela est le cas afin que vous puissiez choisir un format de sérialisation, par exemple, l’encodage binaire simple (SBE) et implémenter IStorageDeserializer et IStorageSerializer. Les sérialiseurs intégrés ont été créés en employant cette méthode :

Une fois implémentés, les sérialiseurs doivent être ajoutés à la propriété StorageSerializationPicker dans AdoNetGrainStorage. Voici une implémentation de IStorageSerializationPicker. StorageSerializationPicker est utilisé par défaut. Vous pouvez voir un exemple de changement du format de stockage de données ou d’utilisation de sérialiseurs dans RelationalStorageTests.

Il n’existe actuellement pas de méthode pour exposer le sélecteur de sérialisation à l’application Orleans, car aucune méthode ne permet d’accéder au AdoNetGrainStorage créé par l’infrastructure.

Objectifs de la conception

1. Autoriser l’utilisation de n’importe quel back-end disposant d’un fournisseur ADO.NET

Cela doit porter sur l’ensemble le plus large possible de back-ends disponibles pour .NET, ce qui est un élément à prendre en considération dans les installations locales. Certains fournisseurs sont listés dans Vue d’ensemble d’ADO.NET, mais tous ne sont pas cités, comme Teradata.

2. Laisser la possibilité d’ajuster les requêtes et la structure de base de données selon les besoins, même en cours de déploiement

Dans de nombreux cas, les serveurs et les bases de données sont hébergés par un tiers dans le cadre d’une relation contractuelle avec le client. Il n’est pas rare de trouver un environnement d’hébergement virtualisé dont les performances fluctuent en raison de facteurs imprévus, tels que la présence de voisins bruyants ou un matériel défectueux. S’il n’est pas toujours possible de modifier et de redéployer des fichiers binaires Orleans (pour des raisons contractuelles) ou même des fichiers binaires d’application, il est généralement possible d’ajuster les paramètres de déploiement de base de données. La modification des composants standard, comme les fichiers binaires Orleans, passe par une procédure d’optimisation plus longue dans une situation donnée.

3. Vous permettre d’utiliser des capacités propres au fournisseur et à la version

Les fournisseurs ont implémenté différentes extensions et fonctionnalités dans leurs produits. Il est judicieux d’utiliser ces fonctionnalités lorsqu’elles sont disponibles. Il peut s’agir de fonctionnalités telles que UPSERT natif ou PipelineDB dans PostgreSQL et PolyBase ou des tables et procédures stockées compilées en mode natif dans SQL Server.

4. Offrir la possibilité d’optimiser les ressources matérielles

Pendant la phase de conception d’une application, il est souvent possible d’anticiper les données qui devront être insérées plus rapidement que d’autres, ainsi que celles données qui sont susceptibles d’être placées dans un stockage froid, ce qui est plus économique (par exemple, répartition des données entre SSD et HDD). Les autres éléments à prendre en considération sont notamment l’emplacement physique des données (certaines données peuvent s’avérer plus coûteuses (par exemple, un RAID de SSD, à savoir un RAID de HDD) ou plus sécurisées) ou une autre base de décision. En lien avec le point 3, certaines bases de données offrent des schémas de partitionnement spéciaux, ce qui est le cas des tables et index partitionnés de SQL Server.

Ces principes s’appliquent tout au long du cycle de vie de l’application. Sachant que l’un des principes mêmes d’Orleans est la haute disponibilité, il devrait être possible d’ajuster le système de stockage sans interrompre le déploiement d’Orleans, ou encore d’ajuster les requêtes en fonction des données et d’autres paramètres d’application. Un exemple de changements dynamiques peut être consulté dans le billet de blog de Brian Harry :

Quand une table est de petite taille, le plan de requête n’a pratiquement aucune importance. Quand elle est de taille moyenne, un plan de requête correct suffit, mais quand elle est volumineuse (des millions voire des milliards de lignes), la moindre variation dans le plan de requête peut vous être fatale. Pour cette raison, nous conseillons fortement nos requêtes sensibles.

5. Pas de suppositions quant aux outils, bibliothèques ou processus de déploiement utilisés dans les organisations

Nombreuses sont les organisations à être habituées à un certain jeu d’outils de base de données, Dacpac ou Red Gate étant des exemples. Pour déployer une base de données, une autorisation ou une personne dotée d’un rôle d’administrateur de base de données peuvent s’avérer nécessaires. En règle générale, cela implique aussi de disposer de la structure de la base de données cible et d’une ébauche approximative des requêtes que l’application produira pour estimer la charge. Il peut exister des processus, peut-être sous l’influence des standards du secteur, qui imposent un déploiement à base de scripts. Cela peut se faire en intégrant les requêtes et les structures de base de données dans un script externe.

6. Utiliser le minimum de fonctionnalités d’interface nécessaires au chargement des bibliothèques et des fonctionnalités ADO.NET

Cela est plus rapide et la surface d’exposition aux différences d’implémentation des bibliothèques ADO.NET est moindre.

7. Faire en sorte que la conception soit partitionnable

Quand cela se justifie, par exemple dans un fournisseur de stockage relationnel, faites en sorte que la conception soit facilement partitionnable. Par exemple, cela implique d’utiliser des données non dépendantes de la base de données (par exemple, IDENTITY). Les informations qui distinguent les données de ligne doivent reposer uniquement sur les données résultant des paramètres réels.

8. Faire sorte que la conception puisse être facilement testée

Idéalement, créer un back-end devrait être aussi simple que de traduire l’un des scripts de déploiement existants dans le dialecte SQL du back-end que vous essayez de cibler, avec l’ajout d’une nouvelle chaîne de connexion aux tests (avec les paramètres par défaut), la vérification consistant à déterminer si une base de données donnée est installée, puis l’exécution des tests sur celle-ci.

9. En tenant compte des points précédents, faire en sorte que le portage de scripts pour les nouveaux back-ends et la modification des scripts de back-end déjà déployés soient aussi transparents que possible

Réalisation des objectifs

L’infrastructure Orleans ne sait rien du matériel propre au déploiement (qui peut changer pendant le déploiement actif), du changement de données pendant le cycle de vie du déploiement ou de certaines fonctionnalités propres au fournisseur qui ne peuvent être utilisées que dans certains cas. C’est pour cette raison que l’interface entre la base de données et Orleans doit respecter l’ensemble minimal d’abstractions et de règles pour atteindre ces objectifs, le protéger contre une utilisation abusive et en faciliter les tests si nécessaire. Tables de runtime, gestion de cluster et implémentation concrète du protocole d’appartenance. Par ailleurs, l’implémentation de SQL Server propose un réglage propre à l’édition de SQL Server. Le contrat d’interface entre la base de données et Orleans est défini comme suit :

  1. L’idée générale est que les données sont lues et écrites via des requêtes propres à Orleans. Orleans opère sur les noms et les types de colonnes pendant la lecture, et sur les noms et les types de paramètres pendant l’écriture.
  2. Les implémentations doivent préserver les noms et les types d’entrée et de sortie. Orleans utilise ces paramètres pour lire les résultats des requêtes par nom et par type. Le réglage propre au fournisseur et au déploiement est autorisé, et les contributions sont encouragées dans la mesure où le contrat d’interface est préservé.
  3. L’implémentation entre les scripts propres au fournisseur doit préserver les noms de contraintes. Cela simplifie la résolution des problèmes, en raison du nommage uniforme entre les implémentations concrètes.
  4. Version (ou ETag dans le code d’application) pour Orleans, cela représente une version unique. Le type de son implémentation réelle n’est pas important pourvu qu’il représente une version unique. Dans l’implémentation, le code Orleans attend un entier 32 bits signé.
  5. Pour être explicite et ôter toute ambiguïté, Orleans s’attend à ce que certaines requêtes retournent la valeur TRUE as >0 ou la valeur FALSE as = 0. Autrement dit, le nombre de lignes affectées ou retournées n’a pas d’importance. Si une erreur est déclenchée ou si une exception est levée, la requête doit vérifier que la transaction est restaurée en intégralité et peut retourner FALSE ou propager l’exception.
  6. Pour l’heure, toutes les requêtes sauf une sont des insertions ou des mises à jour au niveau d’une même ligne (à noter qu’il est possible de remplacer les requêtes UPDATE par INSERT, à condition que les requêtes SELECT associées ont effectué la dernière écriture).

Les moteurs de base de données prennent en charge la programmation dans la base de données. Cela revient à charger un script exécutable et à l’appeler pour exécuter des opérations de base de données. Voici comment cela pourrait être illustré dans un pseudocode :

const int Param1 = 1;
const DateTime Param2 = DateTime.UtcNow;
const string queryFromOrleansQueryTableWithSomeKey =
    "SELECT column1, column2 "+
    "FROM <some Orleans table> " +
    "WHERE column1 = @param1 " +
    "AND column2 = @param2;";
TExpected queryResult =
    SpecificQuery12InOrleans<TExpected>(query, Param1, Param2);

Ces principes s’appliquent aussi aux scripts de base de données.

Quelques idées concernant l’application de scripts personnalisés

  1. Modifiez les scripts dans OrleansQuery pour la persistance des grains avec IF ELSE de façon à enregistrer certains états avec le INSERT par défaut, tandis que certains états de grain pourront utiliser des tables à mémoire optimisée. Les requêtes SELECT doivent être modifiées en conséquence.
  2. L’idée exposée au point 1. peut être mise à profit pour tirer parti d’un autre déploiement ou de certains aspects propres au fournisseur, comme répartir des données entre SSD ou HDD, placer certaines données dans des tables chiffrées, ou peut-être insérer des données statistiques via la liaison SQL Server-Hadoop ou même des serveurs liés.

Les scripts modifiés peuvent être testés en exécutant la suite de tests Orleans, ou directement dans la base de données en utilisant, par exemple, le projet de test unitaire SQL Server.

Recommandations pour ajouter des nouveaux fournisseurs ADO.NET

  1. Ajoutez un nouveau script d’installation de base de données en tenant compte des indications de la section Réalisation des objectifs ci-dessus.
  2. Ajoutez le nom d’invariant ADO du fournisseur à AdoNetInvariants et les données propres au fournisseur ADO.NET à DbConstantsStore. Celles-ci pourront être utilisées dans certaines opérations de requête, par exemple pour sélectionner le mode d’insertion de statistiques approprié (c’est-à-dire, UNION ALL avec ou sans FROM DUAL).
  3. Orleans propose des tests complets pour tous les magasins système : appartenance, rappels et statistiques. L’ajout des tests pour le nouveau script de base de données consiste à copier et coller les classes de test existantes et à changer le nom d’invariant ADO. Par ailleurs, une dérivation de RelationalStorageForTesting est nécessaire afin de définir la fonctionnalité de test pour l’invariant ADO.