ADO.NET grain 持久性

Orleans 中的关系存储后端代码建立在通用 ADO.NET 功能的基础之上,因此与数据库供应商无关。 运行时表中介绍了 Orleans 数据存储布局。 按照 Orleans 配置指南中的说明设置连接字符串。

若要使 Orleans 代码能够与给定的关系数据库后端配合运行,需要做好以下准备:

  1. 必须将相应的 ADO.NET 库加载到进程中。 应按常规方式定义此操作,例如,通过应用程序配置中的 DbProviderFactories 元素。
  2. 通过选项中的 Invariant 属性配置 ADO.NET 不变量。
  3. 数据库需要存在并与代码兼容。 这是通过运行特定于供应商的数据库创建脚本来实现的。 有关详细信息,请参阅 ADO.NET 配置

ADO .NET grain 存储提供程序允许将 grain 状态存储在关系数据库中。 目前支持以下数据库:

  • SQL Server
  • MySQL/MariaDB
  • PostgreSQL
  • Oracle

首先安装基础包:

Install-Package Microsoft.Orleans.Persistence.AdoNet

请阅读 ADO.NET 配置一文获取有关配置数据库的信息,包括相应的 ADO.NET 不变量和设置脚本。

下面是如何通过 ISiloHostBuilder 配置 ADO.NET 存储提供程序的示例:

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

实质上,只需设置特定于数据库供应商的连接字符串和用于标识供应商的 Invariant(请参阅 ADO.NET 配置)即可。 还可以选择数据的保存格式,这可以是二进制(默认)、JSON 或 XML。 虽然二进制是最紧凑的格式选项,但它是不透明的,因此你无法读取或处理数据。 JSON 是建议的选项。

可以通过 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; }
}

ADO.NET 持久性能够控制数据的版本,并使用任意应用程序规则和流来定义任意序列化程序(反序列化程序),但目前没有任何方法可将其公开给应用程序代码。

ADO.NET 持久性基本原理

ADO.NET 支持的持久性存储的原则是:

  1. 在数据、数据格式和代码不断演变的同时,保持业务关键数据的安全性和可访问性。
  2. 利用特定于供应商和存储的功能。

在实践中,这意味着需要遵守 ADO.NET 实现目标,并在特定于 ADO.NET 的存储提供程序中添加某种实现逻辑,以允许存储中数据的形状不断演变。

除了一般的存储提供程序功能外,ADO.NET 提供程序还具有以下内置功能:

  1. 在处于往返状态时将存储数据从一种格式更改为另一种格式(例如从 JSON 更改为二进制)。
  2. 以任意方式塑造要保存在存储中或从存储中读取的类型。 这样就允许状态版本不断演变。
  3. 从数据库中流式传输数据。

1.2. 都可以根据任意决策参数(例如 grain ID、grain 类型、有效负载数据)来应用。

在这种情况下,可以选择一种序列化格式(例如简单二进制编码 (SBE))并实现 IStorageDeserializerIStorageSerializer。 已使用此方法生成了内置序列化程序:

实现序列化程序后,需要将它们添加到 AdoNetGrainStorage 中的 StorageSerializationPicker 属性。 这是 IStorageSerializationPicker 的一个实现。 默认情况下,将使用 StorageSerializationPickerRelationalStorageTests 中提供了更改数据存储格式或使用序列化程序的示例。

目前,没有任何方法可将序列化选取器公开给 Orleans 应用程序,因为没有任何方法可以访问框架创建的 AdoNetGrainStorage

设计目标

1. 允许使用任何具有 ADO.NET 提供程序的后端

这包括适用于 .NET 的最广泛使用的可能后端集,这也是本地安装中的一个因素。 ADO.NET 概述中列出了一些提供程序,但有些提供程序并未列出,例如 Teradata

2. 保持适当优化查询和数据库结构的潜力,即使是部署正在运行时

在许多情况下,服务器和数据库由与客户端建立了契约关系的第三方托管。 寻求虚拟化托管环境的情况并不少见,其中的性能会随着不可预见因素(例如近邻干扰或硬件故障)而波动。 可能无法更改和重新部署 Orleans 二进制文件(出于契约性原因),甚至包括应用程序二进制文件,但通常可以调整数据库部署参数。 更改标准组件(例如 Orleans 二进制文件)需要更长时间的过程才能在给定的场合下进行优化。

3.允许使用特定于供应商和版本的功能

供应商在其产品中实现了不同的扩展和功能。 如果这些功能可用,使用这些功能是明智的。 这些功能包括 PostgreSQL 中的本机 UPSERTPipelineDB,以及 SQL Server 中的 PolyBase本机编译表和存储过程

4. 使优化硬件资源成为可能

在设计应用程序时,通常可以预测哪些数据的插入速度需要比其他数据更快,哪些数据更有可能放入较便宜的冷存储(例如,在 SSD 和 HDD 之间拆分数据)。 其他考虑因素包括数据的物理位置(某些数据的存储可能更昂贵(例如 SSD RAID 比 HDD RAID 更昂贵)或更安全),或其他一些决策依据。 与第 3 点相关,某些数据库提供特殊的分区方案,例如 SQL Server 分区表和索引

这些原则适用于整个应用程序生命周期。 考虑到 Orleans 本身的原则之一是高可用性,应该可以在不中断 Orleans 部署的情况下调整存储系统,或者可以根据数据和其他应用程序参数调整查询。 在 Brian Harry 的博客文章中可以看到动态更改的示例:

当表很小时,查询计划是什么基本上无关紧要。 如果表的大小中等,一个差不多的查询计划就可以了。但如果表是巨型的(包含数百万甚至数十亿行),即使查询计划出现微小的变化,也会让你头痛不已。 因此,我们会对敏感查询给出大量的提示。

5. 不要假设组织中使用了哪些工具、库或部署流程

许多组织都熟悉特定的一组数据库工具,例如 DacpacRed Gate。 部署数据库可能需要权限或人员(例如具有 DBA 角色的人员)来执行部署。 通常这意味着,还需要提供目标数据库布局和应用程序生成的查询的草图来估算负载。 可能存在一些流程,因受行业标准的影响,这些流程要求进行基于脚本的部署。 在外部脚本中使用查询和数据库结构可以做到这一点。

6. 使用所需的最小接口功能集来加载 ADO.NET 库和功能

这样既可以加快速度,又可以减少公开给 ADO.NET 库实现差异的接触面。

7. 使设计可分片

如果这样做有意义(例如在关系存储提供程序中),可以使设计随时可分片。 例如,这意味着不使用数据库相关的数据(如 IDENTITY)。 区分行数据的信息应该仅基于实际参数中的数据。

8. 使设计易于测试

理想情况下,创建新后端就像将现有部署脚本之一转换成你要尝试针对的后端的 SQL 方言,将新连接字符串添加到测试(采用默认参数),检查给定的数据库是否已安装,然后对它运行测试一样简单。

9. 考虑到前面的要点,使新后端的移植脚本和修改已部署后端脚本尽可能透明

实现目标

Orleans 框架不知道特定于部署的硬件(哪些硬件可能在活动部署期间发生更改)、部署生命周期内的数据更改,或者仅在特定情况下可用的某些特定于供应商的功能。 因此,数据库和 Orleans 之间的接口应该遵守最小抽象和规则集来达到这些目标,使其能够可靠地防止误用,并在需要时易于测试。 运行时表、群集管理和具体的成员身份协议实现。 此外,SQL Server 实现包含特定于 SQL Server 版本的调整。 数据库与 Orleans 之间的接口协定定义如下:

  1. 一般的思路是通过特定于 Orleans 的查询来读取和写入数据。 Orleans 在读取时会对列名和类型进行操作,在写入时对参数名和类型进行操作。
  2. 实现必须保留输入和输出名称及类型。 Orleans 使用这些参数来按名称和类型读取查询结果。 允许进行特定于供应商和部署的优化,我们鼓励做出这种改进,但前提是维持接口协定。
  3. 跨供应商特定脚本的实现应保留约束名称。 这样可以通过跨具体实现的统一命名来简化故障排除。
  4. 应用程序代码中的版本ETag - 对于 Orleans 而言,这代表唯一的版本。 只要它代表唯一的版本,其实际实现的类型并不重要。 在实现中,Orleans 代码需要一个带符号的 32 位整数。
  5. 为了明确起见并消除歧义,Orleans 要求某些查询返回 TRUE 表示 > 0 的值,或返回 FALSE 表示 = 0 的值。 也就是说,受影响或返回的行数无关紧要。 如果引发错误或引发异常,查询必须确保回滚整个事务,并可以返回 FALSE 或传播异常。
  6. 目前,除一个查询以外的其他所有查询都是单行插入或更新(请注意,可将 UPDATE 查询替换为 INSERT,前提是关联的 SELECT 查询执行了最后一次写入)。

数据库引擎支持数据库中编程。 这类似于加载可执行脚本并调用它来执行数据库操作的思路。 在伪代码中,可将它描绘为:

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

这些原则也包含在数据库脚本中

有关应用自定义脚本的一些思路

  1. 使用 IF ELSE 更改 OrleansQuery 中用于实现 grain 持久性的脚本,以使用默认的 INSERT 保存某种状态,而某些 grain 状态可以使用内存优化表。 需要相应地更改 SELECT 查询。
  2. 使用 1. 中的思路可以利用其他特定于部署或供应商的方面,例如在 SSDHDD 之间拆分数据,将某些数据放入加密的表中,或者,也许可以通过 SQL-Server-to-Hadoop 甚至链接服务器插入统计数据。

可以通过运行 Orleans 测试套件或者直接在数据库中(例如,使用 SQL Server 单元测试项目)来测试已更改的脚本。

有关添加新 ADO.NET 提供程序的准则

  1. 根据上面的实现目标部分添加新的数据库设置脚本。
  2. 将供应商 ADO 不变量名称添加到 AdoNetInvariants,将 ADO.NET 提供程序特定的数据添加到 DbConstantsStore。 这些数据(可能)在某些查询操作中使用。 例如,选择正确的统计信息插入模式(即包含或不包含 FROM DUALUNION ALL)。
  3. Orleans 针对所有系统存储提供全面的测试:成员身份、提醒和统计信息。 通过复制粘贴现有测试类并更改 ADO 不变量名称来为新数据库脚本添加测试。 另外,请从 RelationalStorageForTesting 派生,以便为 ADO 不变量定义测试功能。