Share via


Persistencia de granos de ADO.NET

El código de back-end de almacenamiento relacional de Orleans se basa en la funcionalidad genérica de ADO.NET y, por tanto, es independiente del proveedor de bases de datos. El diseño del almacenamiento de datos de Orleans ya se ha explicado en tablas de runtime. La configuración de las cadenas de conexión se realiza tal como se explica en la Guía de configuración de Orleans.

Para que el código de Orleans funcione con un back-end de base de datos relacional determinado, se requiere lo siguiente:

  1. Es necesario cargar en el proceso la biblioteca de ADO.NET adecuada. Debe definirse como de costumbre, por ejemplo, mediante el elemento DbProviderFactories en la configuración de la aplicación.
  2. Configure la invariante de ADO.NET mediante la propiedad Invariant en las opciones.
  3. La base de datos debe existir y ser compatible con el código. Para ello, ejecute un script de creación de base de datos específico del proveedor. Para obtener más información, consulte Configuración de ADO.NET.

El proveedor de almacenamiento de granos de ADO.NET permite almacenar el estado del grano en bases de datos relacionales. Actualmente, se admiten las bases de datos siguientes:

  • SQL Server
  • MySQL/MariaDB
  • PostgreSQL
  • Oracle

En primer lugar, instale el paquete base:

Install-Package Microsoft.Orleans.Persistence.AdoNet

Lea el artículo Configuración de ADO.NET para obtener información sobre cómo configurar la base de datos, incluida la invariante de ADO.NET correspondiente y los scripts de configuración.

A continuación se muestra un ejemplo de cómo configurar un proveedor de almacenamiento de ADO.NET mediante ISiloHostBuilder:

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

Básicamente, solo tiene que establecer la cadena de conexión específica del proveedor de bases de datos y un objeto Invariant (consulte Configuración de ADO.NET) que identifique al proveedor. También puede elegir el formato en el que se guardan los datos, que pueden ser binarios (de forma predeterminada), JSON o XML. Aunque la opción de datos binarios es más compacta, resulta opaca y no podrá leer ni trabajar con los datos. JSON es la opción recomendada.

Puede establecer las siguientes propiedades mediante 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; }
}

La persistencia de ADO.NET ofrece la funcionalidad de controlar la versión de los datos y definir serializadores (o deserializadores) arbitrarios con transmisión y reglas arbitrarias de aplicación, pero actualmente no hay ningún método para exponerla al código de la aplicación.

Lógica de la persistencia de ADO.NET

Los principios del almacenamiento de persistencia respaldado por ADO.NET son los siguientes:

  1. Garantizar la seguridad y la accesibilidad de los datos críticos para la empresa mientras los datos, el formato de estos y el código evolucionan.
  2. Aprovechar las ventajas que ofrece la funcionalidad específica del proveedor y del almacenamiento.

En la práctica, esto significa atenerse a los objetivos de implementación de ADO.NET, así como a la lógica de implementación agregada en proveedores de almacenamiento específicos de ADO.NET que permiten la evolución de la forma de los datos en el almacenamiento.

Además de las funcionalidades habituales del proveedor de almacenamiento, el proveedor de ADO.NET tiene capacidad integrada para:

  1. Cambiar los datos de almacenamiento de un formato a otro (por ejemplo, de JSON a binario) al realizar el recorrido de ida y vuelta del estado.
  2. Dar forma al tipo que se va a guardar o leer desde el almacenamiento de maneras arbitrarias. Esto permite que la versión del estado evolucione.
  3. Transmitir datos fuera de la base de datos.

Tanto el punto 1. como el punto 2. se pueden aplicar en función de parámetros de decisión arbitrarios, como el identificador de grano, el tipo de grano y los datos de carga.

Esto es así para que pueda elegir un formato de serialización, por ejemplo, Simple Binary Encoding (SBE), e implementa IStorageDeserializer y IStorageSerializer. Los serializadores integrados se han creado con este método:

Cuando se han implementado los serializadores, deben agregarse a la propiedad StorageSerializationPicker en AdoNetGrainStorage. Esta es una implementación de IStorageSerializationPicker. De forma predeterminada, se usará StorageSerializationPicker. En RelationalStorageTests puede ver un ejemplo de cómo cambiar el formato de almacenamiento de datos o usar serializadores.

Actualmente, no hay ningún método para exponer el selector de serialización a la aplicación de Orleans, ya que no existe ningún método para acceder al objeto AdoNetGrainStorage creado por el marco.

Objetivos del diseño

1. Permitir el uso de cualquier back-end que tenga un proveedor de ADO.NET

Esto debe abarcar el conjunto más amplio posible de back-ends disponibles para .NET, que es un factor determinante en las instalaciones locales. En Información general sobre ADO.NET aparecen algunos proveedores, pero no todos, como Teradata.

2. Mantener el potencial de optimizar las consultas y la estructura de la base de datos según corresponda, incluso mientras se ejecuta una implementación

En muchos casos, los servidores y las bases de datos se hospedan en un tercero con el que el cliente tiene una relación contractual. No es infrecuente encontrar un entorno de hospedaje virtualizado en el que el rendimiento fluctúa debido a factores imprevistos, como vecinos ruidosos o hardware defectuoso. Tal vez no se pueda modificar y reimplementar los archivos binarios de Orleans (por motivos contractuales) o incluso los archivos binarios de la aplicación, pero normalmente es posible ajustar los parámetros de implementación de la base de datos. Para alterar componentes estándar, como archivos binarios de Orleans, se requiere un procedimiento más largo para la optimización en una situación determinada.

3. Permitir usar capacidades específicas del proveedor y la versión

Los proveedores han implementado extensiones y características diferentes en sus productos. Es razonable usar dichas características cuando están disponibles. Se trata de características como UPSERT nativo o PipelineDB en PostgreSQL, y PolyBase o tablas compiladas de forma nativa y procedimientos almacenados en SQL Server.

4. Posibilitar la optimización de los recursos de hardware

Cuando se diseña una aplicación, a menudo es posible prever qué datos deben insertarse más rápido que otros y qué datos se colocarán más probablemente en el almacenamiento en frío, que es más barato (por ejemplo, dividir datos entre SSD y HDD). Entre otras consideraciones adicionales se incluyen la ubicación física de los datos (algunos datos podrían ser más caros, como SSD RAID frente a HDD RAID, o estar más protegidos) y otras decisiones. En relación con el punto 3, algunas bases de datos ofrecen esquemas de partición especiales, como las tablas e índices con particiones de SQL Server.

Estos principios se aplican durante todo el ciclo de vida de la aplicación. Teniendo en cuenta que uno de los principios de Orleans es la alta disponibilidad, debería ser posible ajustar el sistema de almacenamiento sin interrupciones a la implementación de Orleans, o ajustar las consultas en función de los datos y otros parámetros de la aplicación. En esta entrada de blog de Brian Harry puede verse un ejemplo de los cambios dinámicos:

Cuando una tabla es pequeña, prácticamente no importa cuál es el plan de consulta. Cuando es mediana, está bien contar con un plan de consulta correcto. Pero cuando es muy grande (millones de filas, incluso miles de millones), tan solo una ligera variación del plan de consulta puede ser fatídica. Por este motivo, incluimos muchas sugerencias en las consultas confidenciales.

5. No dar por supuesto qué herramientas, bibliotecas o procesos de implementación se usan en las organizaciones

Muchas organizaciones están familiarizadas con un conjunto determinado de herramientas de base de datos, como Dacpac o Red Gate. Puede darse que la implementación de una base de datos requiera un permiso o una persona para hacerlo, como alguien con el rol de DBA. Normalmente, esto también conlleva tener el diseño de la base de datos de destino y una idea aproximada de las consultas que generará la aplicación para usarlas al estimar la carga. Podría haber procesos, quizás influenciados por los estándares del sector, que exigen la implementación basada en scripts. Esto resulta posible si se tienen las consultas y las estructuras de base de datos en un script externo.

6. Usar el conjunto mínimo de funcionalidades de interfaz necesarias para cargar las bibliotecas y funcionalidades de ADO.NET

Esto es rápido y expone una superficie menor a las discrepancias de implementación de la biblioteca de ADO.NET.

7. Hacer que el diseño sea particionable

Cuando tenga sentido (por ejemplo, en un proveedor de almacenamiento relacional), haga que el diseño sea fácilmente particionable. Por ejemplo, esto significa que no se usan datos dependientes de la base de datos (por ejemplo, IDENTITY). La información que distingue datos de fila debe basarse únicamente en datos de parámetros reales.

8. Hacer que el diseño sea fácil de probar

La creación de un back-end debería ser idealmente tan fácil como traducir uno de los scripts de implementación existentes al dialecto SQL del back-end que se quiere establecer como destino, agregar una nueva cadena de conexión a las pruebas (suponiendo que se usan parámetros predeterminados), comprobar si hay instalada una base de datos determinada y ejecutar las pruebas en ella.

9. Teniendo en cuenta los puntos anteriores, hacer que los procesos de migración de los scripts de back-ends nuevos y modificación de los scripts de back-ends ya implementados sean lo más transparentes posible

Consecución de los objetivos

El marco de Orleans desconoce cuál es el hardware específico de la implementación (qué hardware puede cambiar durante la implementación activa), qué datos cambian durante el ciclo de vida de la implementación o qué características específicas del proveedor solo se pueden usar en determinadas situaciones. Por este motivo, la interfaz entre la base de datos y Orleans debe cumplir un conjunto mínimo de abstracciones y reglas para conseguir estos objetivos, proteger contra el uso indebido y facilitar las pruebas, si son necesarias. Tablas de runtime, administración de clústeres e implementación concreta del protocolo de pertenencia. Además, la implementación de SQL Server contiene ajustes específicos de la edición de SQL Server. El contrato de interfaz entre la base de datos y Orleans se define de la manera siguiente:

  1. La idea general es que los datos se leen y escriben mediante consultas específicas de Orleans. Orleans funciona en los nombres y tipos de columna al leer, y en los nombres y tipos de parámetro al escribir.
  2. Las implementaciones deben conservar los nombres y tipos de entrada y salida. Orleans usa estos parámetros para leer los resultados de la consulta por nombre y tipo. Se permite el ajuste específico de la implementación y del proveedor y las contribuciones son bienvenidas, siempre y cuando se cumpla el contrato de interfaz.
  3. La implementación en scripts específicos del proveedor debe conservar los nombres de las restricciones. Esto simplifica la solución de problemas, ya que uniformiza la nomenclatura en implementaciones concretas.
  4. En Orleans, Version (o ETag en el código de la aplicación) representa una versión única. El tipo de su implementación real no es importante, siempre y cuando represente una versión única. En la implementación, el código de Orleans espera un entero de 32 bits con signo.
  5. Para ser explícito y eliminar la ambigüedad, Orleans espera que algunas consultas devuelvan el valor TRUE as > 0 o FALSE as = 0. Es decir, el número de filas afectadas o devueltas no importa. Si se produce un error o una excepción, la consulta debe garantizar que se revierta toda la transacción, y puede devolver FALSE o propagar la excepción.
  6. Actualmente, todas las consultas menos una son actualizaciones o inserciones de una sola fila (tenga en cuenta que las consultas UPDATE se podrían reemplazar por INSERT, siempre y cuando las consultas SELECT asociadas realizasen la última escritura).

Los motores de base de datos admiten la programación en la base de datos. Esto es similar a la idea de cargar un script ejecutable e invocarlo para ejecutar operaciones de base de datos. En el seudocódigo, podría representarse de la manera siguiente:

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

Estos principios también se incluyen en los scripts de base de datos.

Algunas ideas sobre la aplicación de scripts personalizados

  1. Modifique los scripts de OrleansQuery para la persistencia de granos con IF ELSE para que parte del estado se guarde con el valor predeterminado INSERT, mientras que algunos estados de grano podrían usar tablas optimizadas para memoria. Las consultas SELECT deben modificarse en consecuencia.
  2. La idea del punto 1. se puede usar para aprovechar otros aspectos específicos de la implementación o del proveedor, como dividir datos entre SSD o HDD, colocar algunos datos en tablas cifradas o insertar datos de estadísticas mediante SQL Server en Hadoop, o incluso servidores vinculados.

Los scripts modificados se pueden probar mediante la ejecución del conjunto de pruebas de Orleans o directamente en la base de datos mediante, por ejemplo, el proyecto de prueba unitaria de SQL Server.

Directrices para agregar nuevos proveedores de ADO.NET

  1. Agregue un nuevo script de configuración de base de datos como se indica anteriormente en la sección Consecución de los objetivos.
  2. Agregue el nombre de la invariante de ADO del proveedor a AdoNetInvariants y los datos específicos del proveedor de ADO.NET a DbConstantsStore. Se usan potencialmente en algunas operaciones de consulta. Por ejemplo, para seleccionar el modo correcto de inserción de estadísticas (es decir, UNION ALL con o sin FROM DUAL).
  3. Orleans tiene pruebas completas para todos los almacenes del sistema: pertenencia, recordatorios y estadísticas. Para agregar pruebas para el nuevo script de base de datos, se copian y se pegan las clases de prueba existentes y se cambia el nombre de la invariante de ADO. Además, derive de RelationalStorageForTesting para definir la funcionalidad de prueba para la invariante de ADO.