应用程序日益优化,功能增多,用户活跃度提高,每天产生的数据也在持续增长。然而,数据库的问题已经拖慢了应用的其他部分。在这篇文章中,我们将探讨数据库分片这一可能的解决方案,理解它是什么,如何运作,以及在什么情况下使用它是最佳选择。
在探讨数据库分片之前,我们需要了解为什么我们需要对数据存储进行分片,以及在我们决定进行分片之前有哪些可行的选择。
当数据表达到一定的规模时,人们常常寄希望于分片技术,认为它能解决所有的扩展问题。然而,我曾经遇到过一个拥有数十亿行的表的情况,当时并没有找到明显的理由进行分片。因为我们的使用模式非常适合于单个表,而且也没有遇到需要分片的强烈需求(除了管理如此庞大的表,这在某些情况下是一个充分的原因)。
什么是数据库分片?
简而言之,分片是一种将数据分布到多台机器上的技术。当单台机器无法处理预期的工作负载时,分片就变得非常实用。
分片是一种通过将数据分布到多台机器上实现横向扩展的例子,而纵向扩展则是通过获取更大规模的机器来支持新的工作负载的一个示例。
工程师们常常倾向于以最复杂的方式解决问题,但保持早期的简单性可以使后续更具挑战性的工作变得容易得多。所以,如果你的问题可以通过获得更多资源丰富的机器来解决,那么这很可能就是正确的解决方案。
现在,既然我们已经讨论了潜在的服务器架构,接下来让我们来谈谈数据布局。
您可以选择几种方式来对数据进行分区,并将特定的表移动到其数据库中。这与微服务架构中的情况非常相似,其中应用程序的特定方面拥有其独立的数据库服务器。应用程序知道在哪里查找每个数据库。或者,您也可以选择将同一表中的行存储在多个数据库节点上,这就涉及到了分片键的概念;我们稍后将对此进行更深入的探讨。
像Cassandra这样的更现代的数据库将其从应用程序逻辑中抽象出来,并在数据库级别进行维护。
在分片之前,我有什么选择?
像任何分布式架构一样,数据库分片也需要付出一定的代价。分片的设置过程可能既耗时又复杂,同时保持每个分片上的数据实时更新并确保请求被正确发送到相应的分片也是一项不小的任务。因此,在您决定进行分片之前,您可能希望考虑其他可行的选项是否更适合您的需求。
选项1:什么都不做
有人曾向我咨询是否应该使用分片,但并没有提到是否存在明显的性能瓶颈或硬件无法应对工作负载等限制因素。如果没有什么不妥,那么就没有必要大动干戈去进行修正。
选项2:垂直扩展。
之前我们提到过,只需购买具有更多资源的机器,增加额外的RAM,为计算量大的工作负载增加更多的CPU核心,并增加额外的存储空间。这些都是不需要重新设计应用程序和数据库架构的选项。其他最终的限制因素,例如带宽(网络或系统内部),也可能迫使您进行分片。
选项3:复制。
如果您使用数据的主要目的是进行读取操作,那么复制可以提高数据的可用性和读取速度。这可以帮助您避免数据库分片的一些复杂性。通过增加数据库的副本数量,可以改善读取性能。当然,这里假设您已经使用了缓存。这可以通过负载均衡或根据副本的位置来路由查询来完成。然而,复制使得处理沉重的写入工作负载变得更加困难,因为每个写入操作必须复制到每个节点。这可以根据数据存储方式的不同而有所差异,其中一些是异步进行的,而另一些可能会延迟初始写入以确保其已复制。
WAL(Write-Ahead Logging,预写式日志)是磁盘上的一个附加结构,只能进行添加操作。在将更改写入数据库之前,这些更改会首先被写入到日志中,这个日志必须被存储在持久性存储介质上。WAL被用于从崩溃和丢失的事务中进行恢复。此外,这种日志还被用于支持某些数据库(例如PostgreSQL和MySQL)的复制功能。
选项4:专用数据库。
性能不佳往往是由于数据库的设计无法很好地应对其服务的工作负载。例如,将搜索数据存储在关系数据存储中可能并不合理,将这类数据移至Elasticsearch会更有效。将blob数据移至像S3这样的对象存储中会带来显著的效益,而不是将其存储在关系存储中。将特定功能外包可能比尝试对整个数据库进行分片更有意义。
如果应用程序需要管理大量数据、进行大量的读取和写入操作,或者需要保证始终可用,分片可能是最佳的解决方案。接下来我们将详细探讨分片的利弊。
如果必须分片
分片技术可以为您提供几乎无限的可扩展性,包括增强系统吞吐量、存储容量以及可用性。随着许多更小、更易于管理的系统的出现,每个分片系统都可以独立地扩展和缩减,并拥有各自的副本。然而,这种优势的代价是增加了运营复杂性、应用程序开销以及基础设施成本,以支持这种新的设计架构。
它是如何工作的?
在分片数据库之前,我们需要回答几个重要的问题。您的计划将取决于您如何回答这些问题。
- 我们如何将数据分配到各个分片上?如果数据没有均匀分配,是否存在潜在热点?
- 我们运行哪些查询,表之间如何交互?
- 数据将如何增长?稍后需要如何重新分配数据?
术语hotspot意味着一个节点的负载超过了特定资源(内存、io等)的阈值。有一个有趣的例子,称为“Bieber Bug”。
在进一步探讨之前,理解以下术语非常重要。
Shard Key是 primary key的一部分,它告诉数据应该如何分布。使用分片键,您可以通过将操作路由到正确的数据库来快速查找和更改数据。
同一节点包含具有相同分片键的条目。共享相同分片键的一组数据称为logical shard。一个数据库节点包含多个逻辑分片,也称为 physical shard。
最关键的假设也是将来最难改变的假设。逻辑分片只能跨越一个节点,因为它是一个原子存储单元。在分片对单个节点来说太大的情况下,数据库集群实际上没有足够的空间。
基于键的分片(Key Based Sharding)
使用算法进行分片的数据库利用哈希函数来确定数据的存储位置。这使得我们可以通过特定的分片键来定位正确的物理分片,以便请求所需的数据。
数据的分布仅通过哈希函数来实现,而无需考虑有效负载的大小或空间使用情况。使用哈希函数的好处在于,即使在没有合适的分区键的情况下,也可以实现更均匀的数据分布。而如果您选择了合适的分区键,则可以计算出正确的存储位置。
如果您对确定性哈希函数感到好奇,请查看我关于Redis的文章。
这种分片策略的缺点是重新分片数据可能很困难,并且在可用时维护一致性更加困难。
基于范围的分片(Range Based Sharding)
在基于范围的分片中,根据某个值的范围将数据划分为块。
确保查找表的一致性并正确选择存储数据的范围至关重要。在为这种分片类型选择分片键时,应选择具有高基数的键,即可能值数量较多的键。例如,“北”、“南”、“东”和“西”等只有4个可能值的键的基数较低。理想情况下,您还希望在良好分布的前提下拥有较高的基数。
如果所有内容都集中在可能值的50%以内,那么某些分片就会开始出现热点。为了进行这样的实验,只需要几行代码就能轻松地使用准确数据进行模拟。首先,选择键和范围,并检查潜在的分布情况。
基于关系的分片(Relationship Based Sharding)
这种共享机制将相关数据存储在单个物理分片上。例如,相关数据通常分布在关系数据库的多个表中。
例如,对于像Instagram这样的应用程序,用户和所有相关数据会被分片到相同的物理节点上,包括帖子和评论等数据。通过将相关实体放在同一分区中,您可以更好地利用单个分区的好处。因此,可以在整个物理分片中维护更强的一致性,并减少跨物理分片的查询。
跨分片事务(Cross Shard Transactions)
最后,我想对跨多个分片执行的事务可能带来的复杂性进行一些总结。无论您在前期计划得多么周全,一个寿命足够长的服务或应用程序最终都可能会遇到一些跨分片的事务。
这基本上意味着您需要ACID兼容数据库提供的交易保证,但数据库不在分片上确保这种兼容性,因为您操作的数据超出了启动事务的范围。
这通常称为全局事务,其中多个子事务需要协调并成功完成。一般来说,事务开放的时间越长,可能发生的争用和潜在故障就越多。因此,对于跨多个分片执行的事务,我们需要特别注意和考虑其可能带来的复杂性。
二阶段提交(Two-phase commit)
两阶段提交在理论上看似简单,但在实际操作中却具有挑战性。
- 领导者会先写入一个持久的交易记录,以指示涉及跨分片的交易。
- 参与者也会写入一个持久的记录,表明他们愿意提交并通知领导者。
- 领导者在收到所有响应后,会通过更新持久的交易记录来提交事务(如果未收到响应,可能会中止事务)。
- 参与者在领导者宣布提交决定后,可以显示新状态(如果领导者中止事务,则删除预提交状态)。
协议路径中的读写放大是一个主要问题。写放大发生是因为必须写入事务记录并持久地提交,这要求每个参与者至少进行一次写入。过多的写入可能导致锁竞争和应用不稳定。此外,数据库还必须过滤每个读取以确保不会看到任何依赖于挂起跨分片事务的状态,这会影响系统中的所有读取,即使是非事务性的读取。
总结
在之前的讨论中,我们探讨了分片的概念,以及何时应该使用它和如何设置它。对于需要处理大量数据的应用程序,分片是一种很好的解决方案,它可以随时进行大量的读取和写入操作。然而,分片也会使操作变得更加复杂。在开始实施分片之前,您应该仔细考虑这些好处是否值得付出相应的代价,或者是否有更简单的解决方案可供选择。