Sharding Pattern

将数据存储为一组水平的数据分区。这种模式可以在存储和访问大量的数据的时候提高可扩展性。

场景和问题

由单个服务器托管的数据存储可能受到下列限制:

通过添加更多的磁盘容量、处理能力、内存和网络连接来垂直缩放可能会推迟这些限制的影响,但它只是一个临时的解决方案。一个商业云应用程序能够支持大量的用户和大量的数据必须能够无限规模地扩展,所以垂直缩放不一定是最好的解决方案。

解决方案

将数据仓库进行分隔来变成多个水平的分区。每个分区(shard)都有相同的结构,但有其自身不同的数据子集。碎片本身就是一个数据仓库(可以包含许多不同种类的实体的数据),在服务器上作为存储节点运行。

这种模式提供以下好处:

当将数据存储进行分隔成分区时,需靠考虑将哪些数据应该放在哪个分区。一个分区通常需要检索一个或者多个一定范围内的数据属性,就由这些属性构成Shard的Key(有时称为分区Key)。Shard的Key应该是静态的。它不应该基于可能改变的数据。

分区在物理存储上组织数据。当一个应用程序存储和检索数据的时候,逻辑的分片指向应用到相应的分区。其实分区逻辑是作为应用数据访问代码的一部分来实现的,当然,如果数据存储系统支持透明的sharding的话,也可以由数据存储系统来实现。

在sharding逻辑中抽象数据的物理位置可以对那个分区包含哪些数据提供一种high-level的控制,同时,使数据在分区之间迁移的时候不需要重新执行应用的业务逻辑(比如分区数据不平衡的时候,进行的重新分布)。当然,其代价是在确定每个数据项的位置时所需的额外数据访问开销。

为了确保最佳的性能和可扩展性,根据应用所执行查询的方式来进行选择合适的分割数据的方案是非常重要的。在许多情况下,是不太可能令分区方案将完全匹配的每个查询的要求。例如,在多租户系统中,应用程序可能需要使用租户ID来检索租户数据,但也可能需要根据其他属性(如租户的名称或位置)来查找这些数据。为了处理这类情况,就需要设计一种合适的Shard key来满足最常执行的请求(最关键的,对性能要求高CRUD操作)。

如果查询定期检索数据结合使用的属性值,可以通过定义组合属性为shard key。另外,使用Index-Table模式可以根据没有被Shard Key覆盖的数据进行快速查找。

Sharding策略

通常在选择shard key和决定如何将数据存入分区使用三种策略。需要提一点的是,分区和对应的服务器是不必一一对应的,服务器可以host多个分区。策略如下:

下表列出了三种策略的优势以及需要考虑的问题:

策略 优势 需要考虑的信息
查找策略 对于分区的配置和使用控制粒度更细。
使用虚拟分区可以减少重新均衡数据所带来的一些问题。因为新的物理分区可以添加到负载中去。虚拟分区和物理分区之间的映射可以在不影响应用代码的情况下进行修改,完全不会影响到应用通过shard key对数据的存储和获取
定位数据所在的分区会带来额外的计算负载
范围策略 易于实现,并且在范围查询的时候性能很好,因为所获取的数据通常都来自于同一个分区,只一次IO就可以获取全部的数据
易于管理数据。例如,如果在同一区域的用户都在同一个分区,更新可以安排每个时区基于本地负载和需求自由定制。
无法针对分区之间的负载差异进行优化。
平衡分区很困难,并且在大量请求都是基于临近行为的时候,很有可能无法解决负载不均衡的问题。
哈希策略 可以更好的平均分配数据,均衡负载。请求路由可以直接通过哈希函数来实现,无需维护请求到分区的映射。 计算哈希值会带来额外的计算负载。
重新平衡分区十分困难。

最常见的Sharding的方式,就是使用前文所描述的一些方法,但是开发者也需要同时考虑应用的业务需求以及数据的使用模式。还是以租房应用为例:

扩展和数据移动操作

每种Sharding策略在管理向内扩展,向外扩展,数据移动,状态保持等方面,都有着不同的能力级别。

查找策略允许扩展和数据移动这些操作操作在用户级别进行,无论是在线或离线的进行。查找策略的技术就在于阻断某些用户的行为(非高峰时段),将数据迁移到新的虚拟分区或者物理分区,修改映射,刷新任何持有数据的缓存,然后恢复之前阻断的用户行为。通常来说,这类操作是可以集中进行管理。查找策略需要高度可缓存的并且友好复制分发的状态。

范围策略对扩展和数据迁移操作会带来一定的局限性,通常必须时进行数据存储的一部分或全部下线,因为数据分区必须要继续分割,或者重新合并。如果最活跃的数据是相邻Shard Key或数据的标识符在相同的范围内,那么通过迁移数据来重新平衡分区负载可能无法解决负载不均问题。范围策略同样需要维护一些状态来将一定范围的数据映射到物理分区。

哈希策略会令扩展和数据迁移的操作变得更加复杂,因为分区的Key都是基于Shard Key的哈希值。所有新的分区都需要根据哈希值或者是实现映射的函数来计算判定。但是,哈希策略对于状态的保持是没有要求的。

问题和顾虑

在使用Sharding的时候,需要进行如下的考虑:

何时使用该模式

Sharding模式主要的重点是在于提高性能和系统的可扩展性,而且它同时也因为数据的独立分区提高了可用性。一个分区中的故障并不会影响应用程序访问在其他分区中保存的数据,并且运维人员可以对一个或多个分区执行维护或恢复,而不会令应用完全不可用。想了解更多相关信息,可以参考Data Partitioning Guidance

举例

下面的例子使用一些SQL Server数据库作为分区。每个数据库持有应用使用数据的一部分。应用从多个分区获取数据。其中GetShards()方法返回所有数据所处于的分区。GetShards()方法返回了ShardInfomation对象的列表,其中ShardInfomation类包含了一个Shard的标识符,也包含了一个数据库连接字符串由应用连接到对应的分区。

private IEnumerable<ShardInformation> GetShards()
{
    // This retrieves the connection information from a shard store
    // (commonly a root database).
    return new[]
    {
        new ShardInformation
        {
            Id = 1,
            ConnectionString = ...
        },
        new ShardInformation
        {
            Id = 2,
            ConnectionString = ...
        }
    };
}

下面的代码展示了应用如何使用ShardInfomation对象列表来并行查询数据的。其中具体请求的代码没有展示,但是在例子中,请求客户的名字的信息,如果分区中包括客户信息,就会返回。然后结果聚合到ConcurrentBag集合中,等待应用处理。

// Retrieve the shards as a ShardInformation[] instance.
var shards = GetShards();
var results = new ConcurrentBag<string>();
// Execute the query against each shard in the shard list.
// This list would typically be retrieved from configuration
// or from a root/master shard store.
Parallel.ForEach(shards, shard =>
{
    // NOTE: Transient fault handling is not included,
    // but should be incorporated when used in a real world application.
    using (var con = new SqlConnection(shard.ConnectionString))
    {
        con.Open();
        var cmd = new SqlCommand("SELECT ... FROM ...", con);
        Trace.TraceInformation("Executing command against shard: {0}", shard.Id);
        var reader = cmd.ExecuteReader();
        // Read the results in to a thread-safe data structure.
        while (reader.Read())
        {
            results.Add(reader.GetString(0));
        }
    }
});
Trace.TraceInformation("Fanout query complete - Record Count: {0}",
results.Count);

相关的其他模式

在考虑使用Sharding模式的时候,可以参考以下文章: