本章将展示如何构建一个基于MySQL的应用,并且当规模变得越来越庞大时,还能保证快速、高效并且经济。
MySQL经常被批评很难进行扩展,有些情况下这种看法是正确的,但如果选择正确的架构并很好地实现,就能够非常好地扩展MySQL。但是扩展性并不是一个很好理解的主题,所以我们先来理清一些容易混淆的地方。
容量和可扩展性并不依赖于性能。以高速公路上的汽车来类比的话:
在这个类比中,可扩展性依赖于多个条件:换道设计得是否合理、路上有多少车抛锚或者发生事故,汽车行驶速度是否不同或者是否频繁变换车道—但一般来说和汽车的引擎是否强大无关。这并不是说性能不重要,性能确实重要,只是需要指出,即使系统性能不是很高也可以具备可扩展性。
从较髙层次看,可扩展性就是能够通过增加资源来提升容量的能力。
即使MySQL架构是可扩展的,但应用本身也可能无法扩展,如果很难增加容量,不管原因是什么,应用都是不可扩展的。之前我们从吞吐量方面来定义容量,但同样也需要从较高的层次来看待容量问题。从有利的角度来看,容量可以简单地认为是处理负载的能力,从不同的角度来考虑负载很有帮助。
数据量
用户量
用户活跃度
不是所有的用户活跃度都相同,并且用户活跃度也不总是不变的。如果用户突然变得活跃,例如由于增加了一个吸引人的新特性,那么负载可能会明显提升。用户活跃度不仅仅指页面浏览数,即使同样的页面浏览数,如果网站的某个需要执行大量工作的部分变得流行,也可能导致更多的工作。另外,某些用户也会比其他用户更活跃:他们可能比一般人有更多的朋友、消息和照片。
有必要探讨一下可扩展性在数学上的定义了,这有助于在更髙层次的概念上清晰地理解可扩展性。如果没有这样的基础,就可能无法理解或精确地表达可扩展性。不过不用担心,这里不会涉及髙等数学,即使不是数学天才,也能够很直观地理解它。
关键是之前我们使用的短语:“划算的等同提升”。另一种说法是,可扩展性是当增加资源以处理负载和增加容量时系统能够获得的投资产出率(ROI)。假设有一个只有一台服务器的系统,并且能够测量它的最大容量,如图11-1所示。
假设现在我们增加一台服务器,系统的能力加倍,如图11-2所示。
这就是线性扩展。我们增加了一倍的服务器,结果增加了一倍的容量。大部分系统并不是线性扩展的,而是如图11-3所示的扩展方式。
大部分系统都只能以比线性扩展略低的扩展系数进行扩展。越髙的扩展系数会导致越大的线性偏差。事实上,多数系统最终会达到一个最大吞吐量临界点,超过这个点后增加投入反而会带来负回报——继续增加更多工作负载,实际上会降低系统的吞吐量。
这怎么可能呢?这些年产生了许多可扩展性模型,它们有着不同程度的良好表现和实用性。我们这里所讲的可扩展性模型是基于某些能够影响系统扩展的内在机制。这就是NeilJ.Gunther博士提出的通用可扩展性定律。Gunther博士将这些详尽地写到了他的书中,包括CuerrillaCapacityPlanning(Springer)。这里我们不会深入到背后的数学理论中,如果你对此感兴趣,他撰写的书籍以及由他的公司PerformanceDynamics提供的训练课程可能是比较好的资源。
增加第二个因素-内部节点间或者进程间的通信-到Amdah1定律就得出了USL。这种通信的代价取决于通信信道的数量,而信道的数量将按照系统内工作者数量的二次方增长。因此最终开销比带来的收益增长得更快,这是产生扩展性倒退的原因。图11-4阐明了目前讨论到的三个概念:线性扩展、Amdahl扩展,以及USL扩展。大多数真实系统看起来更像USL曲线。
USL可以应用于硬件和软件领域。对于硬件,横轴表示硬件的数量,例如服务器数量或CPU数量。每个硬件的工作量、数据大小以及査询的复杂度必须保持为常量。对于软件,横轴表示并发度,例如用户数或线程数。每个并发的工作量必须保持为常量。
有一点很重要,USL并不能完美地描述真实系统,它只是一个简化模型。但这是一个很好的框架,可用于理解为什么系统增长无法带来等同的收益。它也掲示了一个构建高可扩展性系统的重要原则:在系统内尽量避免串行化和交互。
可以衡量一个系统并使用回归来确定串行和交互的量。你可以将它作为容量规划和性能预测评估的最优上限值。也可以检査系统是怎么偏离USL模型的,将其作为最差下限值以指出系统的哪一部分没有表现出它应有的性能。这两种情况下,USL给出了一个讨论可扩展性的参考。如果没有USL,那即使盯着系统看也无法知道期望的结果是什么。如果想深入了解这个主题,最好去看一下对应的书籍。Gunther博士已经写得很清楚,因此我们不会再深入讨论下去。
另外一个理解可扩展性问题的框架是约束理论,它解释了如何通过减少依赖事件和统计变化来改进系统的吞吐量和性能。这在EliyahuM.Goldratt所撰写的了TheGoal(NorthRiver)—书中有描述,其中有一个关于管理制造业设备的延伸的比喻。尽管这看起来和数据库服务器没有什么关联,但其中包含的法则和排队理论以及其他运筹学方面是一样的。
如果将应用所有的数据简单地放到单个MySQL服务器实例上,则无法很好地扩展,迟早会碰到性能瓶颈。对于许多类型的应用,传统的解决方法是购买更多强悍的机器,也就是常说的“垂直扩展”或者“向上扩展”。另外一个与之相反的方法是将任务分配到多台计算机上,这通常被称为“水平扩展”或者“向外扩展”。我们将讨论如何联合使用向上扩展和向外扩展的解决方案,以及如何使用集群方案来进行扩展。最后,大部分应用还会有一些很少或者从不需要的数据,这些数据可以被清理或归档。我们将这个方案称为“向内扩展”,这么取名是为了和其他策略相匹配。
人们通常只有在无法满足增加的负载时才会考虑到可扩展性,具体表现为工作负载从cpu密集型变成I/O密集型,并发査询的竞争,以及不断增大的延迟。主要原因是査询的复杂度增加或者内存中驻留着一部分不再使用的数据或者索引。你可能看到一部分类型的査询发生改变,例如大的査询或者复杂査询常常比那些小的査询更影响系统。
如果是可扩展的应用,则可以简单地增加更多的服务器来分担负载,这样就没有性能问题了。但如果不是可扩展的,你会发现自己将遭遇到无穷无尽的问题。可以通过规划可扩展性来避免这个问题。
规划可扩展性最困难的部分是估算需要承担的负载到底有多少。这个值不一定非常精确,但必须在一定的数量级范围内。如果估计过髙,会浪费开发资源。但如果低估了,则难以应付可能的负载。
以下问题可以帮助规划可扩展性:
优化性能
很多时候可以通过一个简单的改动来获得明显的性能提升,例如为表建立正确的索引或从MyISAM切换到InnoDB存储引擎。如果遇到了性能限制,可以打开查询日志进行分析。
在修复了大多数主要的问题后,会到达一个收益递减点,这时候提升性能会变得越来越困难。每个新的优化都可能耗费更多的精力但只有很小的提升,并会使应用更加复杂。
购买性能更强的硬件
升级或增加服务器在某些场景下行之有效,特别是对处于软件生命周期早期的应用,
向上扩展(有时也称为垂直扩展)意味着购买更多性能强悍的硬件,对很多应用来说这是唯一需要做的事情。这种策略有很多好处。例如,单台服务器比多台服务器更加容易维护和开发,能显著节约开销。在单台服务器上备份和恢复应用同样很简单,因为无须关心一致性或者哪个数据集是权威的。当然,还有一些别的原因。从复杂性的成本来说,向上扩展比向外扩展更简单。
向上扩展的空间其实也很大。拥有0.5TB内存、32核(或者更多)CPU以及更强悍I/O性能的(例如PCIe卡的flash存储)商用服务器现在很容易获得。优秀的应用和数据库设计,以及很好的性能优化技能,可以帮助你在这样的服务器上建立一个MySQL大型应用。
在现代硬件上MySQL能扩展到多大的规模呢?尽管可以在非常强大的服务器上运行,但和大多数数据库服务器一样,在增加硬件资源的情况下MySQL也无法很好地扩展(非常奇怪!)。为了更好地在大型服务器上运行MySQL,一定要尽量选择最新的版本。由于内部可扩展性问题,MySQL5.0和5.1在大型硬件里的表现并不理想。建议使用MySQL5.5或者更新的版本,或者PerconaServer5.1及后续版本。即便如此,当前合理的“收益递减点”的机器配置大约是256GBRAM,32核CPU以及一个PCIeflash驱动器。如果继续提升硬件的配置,MySQL的性能虽然还能有所提升,但性价比就会降低,实际上,在更强大的系统上,也可以通过运行多个小的MySQL实例来替代单个大实例,这样可以获得更好的性能。当然,机器配置的变化速度非常快,这个建议也许很快就会过时了。
最后,向上扩展不是无限制的,即使最强大的计算机也有限制。单服务器应用通常会首先达到读限制,特别是执行复杂的读査询时。类似这样的査询在MySQL内部是单线程的,因此只能使用一个CPU,这种情况下花钱也无法提升多少性能。即使购买最快的CPU也仅仅会是商用CPU的几倍速度。增加更多的CPU或CPU核数并不能使慢査询执行得更快。当数据变得庞大以至于无法有效缓存时,内存也会成为瓶颈,这通常表现为很髙的磁盘使用率,而磁盘是现代计算机中最慢的部分。
无法使用向上扩展最明显的场景是云计算。在大多数公有云中都无法获得性能非常强的服务器,如果应用肯定会变得非常庞大,就不能选择向上扩展的方式。
因此,我们建议,如果系统确实有可能碰到可扩展性的天花板,并且会导致严重的业务问题,那就不要无限制地做向上扩展的规划。如果你知道应用会变得很庞大,在实现另外一种解决方案前,短期内购买更优的服务器是可以的。但是最终还是需要向外扩展,这也是下一节要讲述的主题。
可以把向外扩展(有时也称为横向扩展或者水平扩展)策略划分为三个部分:复制、拆分,以及数据分片(sharding)。
最简单也最常见的向外扩展的方法是通过复制将数据分发到多个服务器上,然后将备库用于读查询。这种技术对于以读为主的应用很有效。它也有一些缺点,例如重复缓存,但如果数据规模有限这就不是问题。关于这些问题我们在前一章已经讨论得足够多,后面会继续提到。
另外一个比较常见的向外扩展方法是将工作负载分布到多个“节点”。具体如何分布工作负载是一个复杂的话题。许多大型的MySQL应用不能自动分布负载,就算有也没有做到完全的自动化。本节会讨论一些可能的分布负载的方案,并探讨它们的优点和缺点。
在MySQL架构中,一个节点(node)就是一个功能部件。如果没有规划冗余和高可用性,那么一个节点可能就是一台服务器。如果设计的是能够故障转移的冗余系统,那么一个节点通常可能是下面的某一种:
大多数情况下,一个节点内的所有服务器应该拥有相同的数据。我们倾向于把主一主复制架构作为两台服务器的主动一被动节点。
1.按功能拆分
按功能拆分,或者说按职责拆分,意味着不同的节点执行不同的任务。我们之前已经提到了一些类似的实现,在前一章我们描述了如何为OLTP和OLAP工作负载设计不同的服务器。按功能拆分采取的策略比这些更进一步,将独立的服务器或节点分配给不同的应用,这样每个节点只包含它的特定应用所需要的数据。
如果应用很大,每个功能区域还可以拥有其专用的Web服务器,但没有专用的数据库服务器这么常见。
另一个可能的按功能划分方法是对单个服务器的数据进行划分,并确保划分的表集合之间不会执行关联操作。当必须执行关联操作时,如果对性能要求不髙,可以在应用中做关联。虽然有一些变通的方法,但它们有一个共同点,就是每种类型的数据只能在单个节点上找到。这并不是一种通用的分布数据方法,因为很难做到高效,并且相比其他方案没有任何优势。
归根结底,还是不能通过功能划分来无限地进行扩展,因为如果一个功能区域被捆绑到单个MySQL节点,就只能进行垂直扩展。其中的一个应用或者功能区域最终增长到非常庞大时,都会迫使你去寻求一个不同的策略。如果进行了太多的功能划分,以后就很难采用更具扩展性的设计了。
2.数据分片
在目前用于扩展大型MySQL应用的方案中,数据分片是最通用且最成功的方法。它把数据分割成一小片,或者说一块,然后存储到不同的节点中。
大型应用可能有多个逻辑数据集,并且处理方式也可以各不相同。可以将它们存储到不同的服务器组上,但这并不是必需的。还可以以多种方式对数据进行分片,这取决于如何使用它们。下文会举例说明。
分片技术和大多数应用的最初设计有着显著的差异,并且很难将应用从单一数据存储转换为分片架构。如果在应用设计初期就已经预计到分片,那实现起来就容易得多。
如果事先知道应用会扩大到很大的规模,并且清楚按功能划分的局限性,就可以跳过中间步骤,直接从单个节点升级为分片数据存储。事实上,这种前瞻性可以帮你避免由于粗糙的分片方案带来的挑战。
分片数据存储看起来像是优雅的解决方案,但很难实现。那为什么要选择这个架构呢?答案很简单:如果想扩展写容量,就必须切分数据。如果只有单台主库,那么不管有多少备库,写容量都是无法扩展的。对于上述缺点而言,数据分片是我们首选的解决方案。
讨论:分片?还是不分片?
简单的说,对单台服务器而言,数据大小或写负载变得太大时,分片将是不可避免的。如果不分片,而是尽可能地优化应用,系统能扩展到什么程度呢?答案可能会让你很惊讶。有些非常受欢迎的应用,你可能以为从一开始就分片了,但实际上直到已经值数十亿美元并且流量极其巨大也还没有采用分片的设计。分片不是城里唯一的游戏,在没有必要的情况下采用分片的架构来构建应用会步履维艰。
3.选择分区键(partitioningkey)
数据分片最大的挑战是査找和获取数据:如何査找数据取决于如何进行分片。有很多方法,其中有一些方法会比另外一些更好。
我们的目标是对那些最重要并且频繁査询的数据减少分片(记住,可扩展性法则的其中一条就是要避免不同节点间的交互)。这其中最重要的是如何为数据选择一个或多个分区键。分区键决定了每一行分配到哪一个分片中。如果知道一个对象的分区键,就可以回答如下两个问题:
跨多个分片的査询比单个分片上的査询性能要差,但只要不涉及太多的分片,也不会太糟糕。最糟糕的情况是不知道需要的数据存储在哪里,这时候就需要扫描所有分片。
一个好的分区键常常是数据库中一个非常重要的实体的主键。这些键值决定了分片单元。例如,如果通过用户ID或客户端ID来分割数据,分片单元就是用户或者客户端。
某些数据模型比其他的更容易进行分片,具体取决于实体一关系图中的关联性程度。图11-8的左边展示了一个易于分片的数据模型,右边的那个则很难分片。
左边的数据模型比较容易分片,因为与之相连的子图中大多数节点只有一个连接,很容易切断子图之间的联系。右边的数据模型则很难分片,因为它没有类似的子图。幸好大多数数据模型更像左边的图。
4.多个分区键
复杂的数据模型会使数据分片更加困难。许多应用拥有多个分区键,特别是存在两个或更多个“维度”的时候。换句话说,应用需要从不同的角度看到有效且连贯的数据视图。这意味着某些数据在系统内至少需要存储两份。
5.跨分片查询
大多数分片应用多少都有一些査询需要对多个分片的数据进行聚合或关联操作。例如,一个读书俱乐部网站要显示最受欢迎或最活跃的用户,就必须访问每一个分片。如何让这类査询很好地执行,是实现数据分片的架构中最困难的部分。虽然从应用的角度来看,这是一条査询,但实际上需要拆分成多条并行执行的査询,每个分片上执行一条。一个设计良好的数据库抽象层能够减轻这个问题,但类似的査询仍然会比分片内査询要慢并且更加昂贵,所以通常会更加依赖缓存。
一些语言,如PHP,对并行执行多条査询的支持不够好。普遍的做法是使用C或Java编写一个辅助应用来执行査询并聚合结果集。PHP应用只需要査询该辅助应用即可,例如Web服务或者类似Gearman的工作者服务。
跨分片査询也可以借助汇总表来执行。可以遍历所有分片来生成汇总表并将结果在每个分片上冗余存储。如果在每个分片上存储重复数据太过浪费,也可以把汇总表放到另外一个数据存储中,这样就只需要存储一份了。
未分片的数据通常存储在全局节点中,可以使用缓存来分担负载。
如果数据的均衡分布非常重要,或者没有很好的分区键,一些应用会采用随机分片的方式。分布式检索应用就是个很好的例子。这种场景下,跨分片査询和聚合査询非常常见。
跨分片査询并不是数据分片面临的唯一难题。维护数据一致性同样困难。外键无法在分片间工作,因此需要由应用来检査参照一致性,或者只在分片内使用外键,因为分片内的内部一致性可能是最重要的。还可以使用XA事务,但由于开销太大,现实中使用很少。
6.分配数据、分片和节点
分片和节点不一定是一对一的关系,应该尽可能地让分片的大小比节点容量小很多,这样就可以在单个节点上存储多个分片。
小一点的分片也便于转移。这有助于重新分配容量,平衡各个节点的分片。转移分片的效率一般都不高。通常需要先将受影响的分片设置为只读模式(这也是需要在应用中构建的特性),提取数据,然后转移到另外一个节点。这包括使用mysqldump获取数据然后使用mysql命令将其重新导入。如果使用的是PerconaServer,可以通过XtraBackup在服务器间转移文件,这比转储和重新载入要高效得多。
除了在节点间移动分片,你可能还需要考虑在分片间移动数据,并尽量不中断整个应用提供服务。如果分片太大,就很难通过移动整个分片来平衡容量,这时候可能需要将一部分数据(例如一个用户)转移到其他分片。分片间转移数据比转移分片要更复杂,应该尽量避免这么做。这也是我们建议设置分片大小尽量易于管理的原因之一。
分片的相对大小取决于应用的需求。简单的说,我们说的“易于管理的大小”是指保持表足够小,以便能在5或10分钟内提供日常的维护工作,例如ALTERTABLE、CHECKTABLE或者OPTIMIZETABLE。
如果将分片设置得太小,会产生太多的表,这可能引发文件系统或MySQL内部结构的问题。另外太小的分片还会导致跨分片查询增多。
7.在节点上部署分片
需要确定如何在节点上部署数据分片。以下是一些常用的办法:
如果在表名中包含了分片号,就需要在査询模板里插入分片号。常用的方法是在査询中使用特殊的“神奇的”占位符,例如sprintf()这样的格式化函数中的%s,或者使用变量做字符串插值。以下是在PHP中创建查询模板的方法:
$sql="SELECTbook_id,book_titleFROMbookclub_%d.comments_%d...";$res=mysql_query(sprintf($sql,$shardno,$shardno),$conn);也可以就使用字符串插值的方法:
$sql="SELECTbook_id,book_titleFROMbookclub_$shardno.comments_$shardno...";$res=mysql_query($sql,$conn);这在新应用中很容易实现,但对于已有的应用则有点困难。构建新应用时,查询模板并不是问题,我们倾向于使用每个分片一个数据库的方式,并把分片号写到数据库名和表名中。这会增加例如ALTERTABLE这类操作的复杂度,但也有如下一些优点:
为已有的应用增加分片支持的结果往往是一个节点对应一个分片。这种简化的设计可以减少对应用查询的修改。分片对应用而言通常是一种颠覆性的改变,所以应尽可能简化它。如果在分片后,每个节点看起来就像是整个应用数据的缩略图,就无须去改变大多数査询或担心查询是否传递到期望的节点。
8.固定分配
将数据分配到分片中有两种主要的方法:固定分配和动态分配。两种方法都需要一个分区函数,使用行的分区键值作为输入,返回存储该行的分片。
固定分配使用的分区函数仅仅依赖于分区键的值。哈希函数和取模运算就是很好的例子。这些函数按照每个分区键的值将数据分散到一定数量的“桶”中。
假设有100个桶,你希望弄清楚用户111该放到哪个桶里。如果使用的是对数字求模的方式,答案很简单:111对100取模的值为11,所以应该将其放到第11个分片中。
而如果使用CRC320函数来做哈希,答案是81。
mysql>SELECTCRC32(111)%100;+------------------+|CRC32(111)%100|+------------------+|81|+------------------+固定分配的主要优点是简单,开销低,甚至可以在应用中直接硬编码。
但固定分配也有如下缺点:
正是由于这些限制,我们倾向于为新应用选择动态分配的方式。但如果是为已有的应用做分片,使用固定分配策略可能会更容易些,因为它更简单。也就是说,大多数使用固定分配的应用最后迟早要使用动态分配策略。
9.动态分配
另外一个选择是使用动态分配,将每个数据单元映射到一个分片。假设一个有两列的表,包括用户ID和分片ID。
CREATETABLEuser_to_shard(user_idINTNOTNULL,shard_idINTNOTNULL,PRIMARYKEY(user_id));这个表本身就是分区函数。给定分区键(用户ID)的值就可以获得分片号。如果该行不存在,就从目标分片中找到并将其加入到表中。也可以推迟更新——这就是动态分配的含义。
动态分配增加了分区函数的开销,因为需要额外调用一次外部资源,例如目录服务器(存储映射关系的数据存储节点)。出于效率方面的考虑,这种架构常常需要更多的分层。例如,可以使用一个分布式缓存系统将目录服务器的数据加载到内存中,因为这些数据平时改动很小。或者更普遍地,你可以直接向USERS表中增加一个shard_id列用于存储分片号。
动态分配的最大好处是可以对数据存储位置做细粒度的控制。这使得均衡分配数据到分片更加容易,并可提供适应未知改变的灵活性。
动态映射可以在简单的键一分片(key-to-shard)映射的基础上建立多层次的分片策略。例如,可以建立一个双重映射,将每个分片单元指定到一个分组中(例如,读书俱乐部的用户组),然后尽可能将这些组保持在同一个分片中。这样可以利用分片亲和性,避免跨分片査询。
如果使用动态分配策略,可以生成不均衡的分片。如果服务器能力不相同,或者希望将其中一些分片用于特定目的(例如归档数据),这可能会有用。如果能够做到随时重新平衡分片,也可以为分片和节点间维持一一对应的映射关系,这不会浪费容量。也有些人喜欢简单的每个节点一个分片的方式。(但是请记住,保持分片尽可能小是有好处的。)
动态分配以及灵活地利用分片亲和性有助于减轻规模扩大而带来的跨分片査询问题。假设一个跨分片査询涉及四个节点,当使用固定分配时,任何给定的査询可能需要访问所有分片,但动态分配策略则可能只需要在其中的三个节点上运行同样的査询。这看起来没什么大区别,但考虑一下当数据存储增加到400个分片时会发生什么?固定分配策略需要访问400个分片,而动态分配方式依然只需要访问3个。
动态分配可以让分片策略根据需要变得很复杂。固定分配则没有这么多选择。
10.混合动态分配和固定分配
可以混合使用固定分配和动态分配。这种方法通常很有用,有时候甚至必须要混合使用。目录映射不太大时,动态分配可以很好胜任。但如果分片单元太多,效果就会变差。
以一个存储网站链接的系统为例。这样一个站点需要存储数百亿的行,所使用的分区键是源地址和目的地址URL的组合。(这两个URL的任意一个都可能有好几亿的链接,因此,单独一个URL并不适合做分区键)。但是在映射表中存储所有的源地址和目的地址URL组合并不合理,因为数据量太大了,每个URL都需要很多存储空间。
一个解决方案是将URL相连并将其哈希到固定数目的桶中,然后把桶动态地映射到分片上。如果桶的数目足够大——例如100万个——你就能把大多数数据分配到每个分片上,获得动态分配的大部分好处,而无须使用庞大的映射表。
11.显式分配
第三种分配策略是在应用插入新的数据行时,显式地选择目标分片。这种策略在已有的数据上很难做到。所以在为应用增加分片时很少使用。但在某些情况下还是有用的。
这个方法是把数据分片号编码到ID中,这和之前提到的避免主一主复制主键冲突策略比较相似。(详情请参阅“在主一主复制结构中写入两台主库”。)
例如,假设应用要创建一个用户3,将其分配到第11个分片中,并使用BIGINT列的高八位来保存分片号。这样最终的ID就是(1<<56)+3,即792633534417207299。应用可以很方便地从中抽取出用户ID和分片号,如下例所示。
我们讨论了混合分配方式,因为在某些场景下它是有用的。但正常情况下我们并不推荐这样用。我们倾向于尽可能使用动态分配,避免显式分配。
12.重新均衡分片数据
在分片间移动数据的好处很明显。例如,当需要升级硬件时,可以将用户数据从旧分片转移到新分片上,而无须暂停整个分片的服务或将其设置为只读。
然而,我们也应该尽量避免重新均衡分片数据,因为这可能会影响用户使用。在分片间转移数据也使得为应用增加新特性更加困难,因为新特性可能还需要包含针对重新均衡脚本的升级。如果分片足够小,就无须这么做;也可以经常移动整个分片来重新均衡负载,这比移动分片中的部分数据要容易得多(并且以每行数据开销来衡量的话,更有效率)。
一个较好的策略是使用动态分片策略,并将新数据随机分配到分片中。当一个分片快满时,可以设置一个标志位,告诉应用不要再往这里放数据了。如果未来需要向分片中放入更多数据,可以直接把标记位清除。
假设安装了一个新的MySQL节点,上面有100个分片。先将它们的标记设置为1,这样应用就知道它们正准备接受新数据。一旦它们的数据足够多时(例如,每个分片10000个用户),就把标记位设置为0。之后,如果节点因为大量废弃账号导致负载不足,可以重新打开一些分片向其中增加新用户。
如果升级应用并且增加的新特性会导致每个分片的査询负载升高,或者只是算错了负载,可以把一些分片移到新节点来减轻负载。缺点是操作期间整个分片会变成只读或者处于离线状态。这需要根据实际情况来看是否能接受。
另外一种使用得较多的策略是为每个分片设置两台备库,每个备库都有该分片的完整数据。然后每个备库负责其中一半的数据,并完全停止在主库上査询。这样每个备库都会有一半它不会用到的数据;我们可以使用一些工具,例如PerconaToolkit的pt-archiver在后台运行,移除那些不再需要的数据。这种办法很简单并且几乎不需要停机。
13.生成全局唯一ID
当希望把一个现有系统转换为分片数据存储时,经常会需要在多台机器上生成全局唯一ID。单一数据存储时通常可以使用AUTO_INCREMENT列来获取唯一ID。但涉及多台服务器时就不凑效了。以下几种方法可以解决这个问题:
使用auto_increment_increment和auto_increment_offset
这两个服务器变量可以让MySQL以期望的值和偏移量来增加AUT0_INCREMENT列的值。举一个最简单的场景,只有两台服务器,可以配置这两台服务器自增幅度为2,其中一台的偏移量设置为1,另外一台为2(两个都不可以设置为0)。这样一台服务器总是包含偶数,另外一台则总是包含奇数。这种设置可以配置到服务器的每一个表里。
这种方法简单,并且不依赖于某个节点,因此是生成唯一ID的比较普遍的方法。但这需要非常仔细地配置服务器。很容易因为配置错误生成重复数字,特别是当增加服务器需要改变其角色,或进行灾难恢复时。
全局节点中创建表
在一个全局数据库节点中创建一个包含AUTO_INCREMENT列的表,应用可以通过这个表来生成唯一数字。
使用memcached
在memcached的API中有一个incr()函数,可以自动增长一个数字并返回结果。另外也可以使用Redis。
批量分配数字
应用可以从一个全局节点中请求一批数字,用完后再申请。
使用复合值
可以使用一个复合值来做唯一ID,例如分片号和自增数的组合。具体参阅之前的章节。
使用GUID值
可以使用UUID()函数来生成全局唯一值。注意,尽管这个函数在基于语句的复制时不能正确复制,但可以先获得这个值,再存放到应用的内存中,然后作为数字在查询中使用。GUID的值很大并且不连续,因此不适合做InnoDB表的主键。具体参考“和InnoDB主键一致地插入行”。在5.1及更新的版本中还有一个函数UUID_SHORT(),能够生成连续的值,并使用64位代替了之前的128位。
如果使用全局分配器来产生唯一ID,要注意避免单点争用成为应用的性能瓶颈。
虽然memcached方法执行速度快(每秒数万个值),但不具备持久性。每次重启memcached服务都需要重新初始化缓存里的值。由于需要首先找到所有分片中的最大值,因此这一过程非常缓慢并且难以实现原子性。
14.分片工具
在设计数据分片应用时,首先要做的事情是编写能够査询多个数据源的代码。
如果没有任何抽象层,直接让应用访问多个数据源,那绝对是一个很差的设计,因为这会增加大量的编码复杂性。最好的办法是将数据源隐藏在抽象层中。这个抽象层主要完成以下任务:
你可能不需要从头开始构建分片结构。有一些工具和系统可以提供一些必要的功能或专门设计用来实现分片架构。
Sphinx是一个全文检索引擎,虽然不是分片数据存储和检索系统,但对于一些跨分片数据存储的査询依然有用。Sphinx可以并行査询远程系统并聚合结果集。
一个分片较多的架构可能会更有效地利用硬件。我们的研究和经验表明MySQL并不能完全发挥现代硬件的性能。当扩展到超过24个CPU核心时,MySQL的性能开始趋于平缓,不再上升。当内存超过128GB时也同样如此,MySQL甚至不能完全发挥诸如Virident或Fusion-io卡这样的髙端PCIeflash设备的I/O性能。
不要在一台性能强悍的服务器上只运行一个服务器实例,我们还有别的选择。你可以让数据分片足够小,以使每台机器上都能放置多个分片(这也是我们一直提倡的),每台服务器上运行多个实例,然后划分服务器的硬件资源,将其分配给每个实例。
这样做尽管比较烦琐,但确实有效。这是一种向上扩展和向外扩展的组合方案。也可以用其他方法来实现——不一定需要分片——但分片对于在大型服务器上的联合扩展具有天然的适应性。
一些人倾向于通过虚拟化技术来实现合并扩展,这有它的好处。但虚拟化技术本身有很大的性能损耗。具体损耗多少取决于具体的技术,但通常都比较明显,尤其是I/O非常快的时候损耗会非常惊人。另一种选择是运行多个MySQL实例,每个实例监听不同的网络端口,或绑定到不同的IP地址。
我们已经在一台性能强焊的硬件上获得了10倍或15倍的合并系数。你需要平衡管理复杂度代价和更优性能的收益,以决定哪种方法是最优的。
这时候网络可能会成为瓶颈——这个问题大多数MySQL用户都不会遇到。可以通过使用多块网卡并进行绑定来解决这个问题。但Linux内核可能会不理想,这取决于内核版本,因为老的内核对每个绑定设备的网络中断只能使用一个CPU。因此不要把太多的连线绑定到很少的虚拟设备上,否则会遇到内核层的网络瓶颈。新的内核在这一方面会有所改善,所以需要检査你的系统版本,以确定该怎么做。
另一个方法是将每个MySQL实例绑定到特定的CPU核心上。这有两点好处:第一,由于MySQL内部的可扩展性限制,当核心数较少时,能够在每个核心上获得更好的性能;第二,当实例在多个核心上运行线程时,由于需要在多核心上同步共享数据,因而会有一些额外的开销。这可以避免硬件本身的可扩展性限制。限制MySQL到少数几个核心能够帮助减少CPU核心之间的交互。注意到反复出现的问题了没?将进程绑定到具有相同物理套接字的核心上可以获得最优的效果。
理想的扩展方案是单一逻辑数据库能够存储尽可能多的数据,处理尽可能多的査询,并如期望的那样增长。许多人的第一想法就是建立一个“集群”或者“网格”来无缝处理这些事情,这样应用就无须去做太多工作,也不需要知道数据到底存在哪台服务器上。随着云计算的流行,自动扩展——根据负载或数据大小变化动态地在集群中增加/移除服务器——变得越来越有趣。
Mysql4.x的时候,出现了许多被称为NoSQL的技术。许多NoSQL的支持者发表了一些奇怪且未经证实的观点,例如“关系模型无法进行扩展”,或者“SQL无法扩展”。随着新概念的出现,也出现了一些新的术语。最近谁没有听说过最终一致性、BASE、矢量时钟,或者CAP理论呢?
所有的旧事物都变成新的了吗?是,但也不是。许多关系型数据库集群的高性能设计正在被构建到系统的更低层,在NoSQL数据库中,特别是使用键一值存储时,这一点很明显。例如NDBCluster并不是一个SQL数据库,它是一个可扩展的数据库,使用其原生API来控制,通常是使用NoSQL,但也可以通过在前端使用MySQL存储引擎来支持SQL。它是一个完全分布式、非共享高性能、自动分片并且不存在单点故障的事务型数据库服务器。最近几年正变得更强大、更复杂,用途也更广泛。同时,NoSQL数据库也逐渐看起来越来辑像关系型数据库。有些甚至还开发了类SQL查询语言。未来典型的集群数据库可能更像是SQL和NoSQL的混合体,有多种存取机制来满足不同的使用需求。所以,我们在从NoSQL中汲取优点,但SQL仍然会保留在集群数据库中。
在开始前,需要指出,可扩展性、高可用性、事务性等是数据库系统的不同特性。许多人会感到困惑并将这些当作是相同的东西,但事实上不是。本章我们主要集中讨论可扩展性。但事实上,可扩展的数据库并不一定非常优秀,除非它能保证髙性能,谁愿意牺牲高可用性来进行扩展呢?这些特性的组合堪称数据库的必杀技,但这很难实现。当然这不是本章要讨论的内容。
1.MySQLCluster(NDBCluster)
MySQLCluster是两项技术的结合:NDB数据库,以及作为SQL前端的MySQL存储引擎。NDB是一个分布式、具备容错性、非共享的数据库,提供同步复制以及节点间的数据自动分片。NDBCluset存储引擎将SQL转换为NDBAPI调用,但遇到NDB不支持的操作时,就会在MySQL服务器上执行(NDB是一个键一值数据存储,无法执行类似联接或聚合的复杂操作)。
NDB是一个非常复杂的数据库,和MySQL几乎完全不同。在使用NDB时甚至可以不需要MySQL:你可以把它作为一个独立的键一值数据库服务器。它的亮点包括非常髙的写入和按键査询吞吐量。NDB可以基于键的哈希自动决定哪个节点应该存储给定的数据。当通过MySQL来控制NDB时,行的主键就是键,其他的列是值。
因为它基于一些新的技术,并且集群具有容错性和分布式特性,所以管理NDB需要非常专业和特殊的技能。有许多动态变化的部分,还有类似升级集群或增加节点的操作必须正确执行以防止意外的问题。NDB是一项开源技术,但也可以从Oracle购买商业支持。商业支持中包括能够获得专门的集群管理产品ClusterManager,可以自动执行一些枯燥且棘手的任务。
MySQLCluster正在迅速地增加越来越多的特性和功能。例如在最近的版本中,它开始支持更多类型的集群变更而无须停机操作,并且能够在数据存储的节点上执行一些特定类型的查询,以减少数据传递给MySQL层并在其中执行査询的必要性。(这个特性已由关联下推(push-downjoin)更名为自适应查询本地化(adaptivequerylocalization)。)
NDB需要一个快速且可靠的网络来连接节点。为了获得最好的性能,最好使用特定的高速连接设备。由于大多数情况下需要内存操作,因此服务器间需要大量的内存。
那么它有什么缺点呢?复杂査询现在支持得还不是很好,例如那些有很多关联和聚合的査询。所以不要指望用它来做数据仓库。NDB是一个事务型系统,但不支持MVCC,所以读操作也需要加锁,也不做任何的死锁检测。如果发生死锁,NDB就以超时返回的方式来解决。还有很多你应该知道的要点和警告,可以专门写一本书了。(有一些关于MySQLCluster的书,但大多数都过时了,最好的办法是阅读手册。)
2.Clustrix
我们的实验评估和性能测试表明,Clustrix能够提供髙性能和可扩展性。Clustrix看起来是一项比较有前景的技术,我们将继续观察和评估。
3.ScaleBase
ScaleBase是一个软件代理,处于应用和多个后端MySQL服务器之间。它会把发起的査询进行分裂,并将其分发到后端服务器并发执行,然后汇集结果返回给应用。
4.GenieDB
GenieDB最开始用于地理上分布部署的NoSQL文档存储。现在它也有一个SQL层,可以通过MySQL存储引擎进行控制。它包含了很多技术,包括本地内存缓存、消息层,以及持久化磁盘数据存储。将这些技术汇集在一起,就可以使用松散的最终一致性,让应用在本地快速执行査询,或是通过分布式集群(会增加网络延迟)来保证最新的数据视图。
通过存储引擎实现的MySQL兼容层不能提供100%的MySQL特性,但对于支持类似Joomla!、WordPress,以及Drupal这样的应用已经够用了。MySQL存储引擎的用处主要是使GenieDB能够结合存储引擎获得对ACID的支持,例如InnoDB。GenieDB本身并不是ACID数据库。
我们还没用应用过GenieDB,也没有看到任何生产环境部署。
5.Akiban
在设计归档和清理策略时需要考虑到如下几点。
对应用的影响
一个设计良好的归档系统能够在不影响事务处理的情况下,从一个髙负载的OLTP服务器上移除数据。这里的关键是能髙效地找到要删除的行,然后一小块一小块地移除。通常需要平衡一次归档的行数和事务的大小,以找到一个锁竞争和事务负载量的平衡。还需要设计归档任务在必要的时候让步于事务处理。
要归档的行
当知道某些数据不再使用后,就可以立刻清理或归档它们。也可以设计应用去归档那些几乎不怎么使用的数据。可以把归档的数据置于核心表附近,通过视图来访问,或完全转移到别的服务器上。
维护数据一致性
当数据间存在联系时,会导致归档和清理工作更加复杂。一个设计良好的归档任务能够保证数据的逻辑一致性,或至少在应用需要时能够保证一致,而无须在大量事务中包含多个表。
避免数据丢失
如果是在服务器间归档,归档期间可能就无法做分布式事务处理,也有可能将数据归档到MyISAM或其他非事务型的存储引擎中。因此,为了避免数据丢失,在从源表中删除时,要保证已经在目标机器上保存。将归档数据单独写到一个文件里也是个好主意。可以将归档任务设计为能够随时关闭或重启,并且不会引起不一致或索引冲突之类的错误。
解除归档(unarchiving)
保持活跃数据独立
即使并不真的把老数据转移到别的服务器,许多应用也能受益于活跃数据和非活跃数据的隔离。这有助于髙效利用缓存,并为活跃和不活跃的数据使用不同的硬件或应用架构。下面列举了几种做法:
将表划分为几个部分
分表是一种比较明智的办法,特别是整张表无法完全加载到内存时。例如,可以把users表划分为active_users和inactive_users表。你可能认为这并不需要,因为数据库本身只缓存“热”数据,但事实上这取决于存储引擎。如果用的是InnoDB,每次缓存一页,而一页能存储100个用户,但只有10%是活跃的,那么这时候InnoDB可能认为所有的页都是“热”的——因此每个“热”页的90%将被浪费掉。将其拆成两个表可以明显改善内存利用率。
MySQL分区
MySQL5.1本身提供了对表进行分区的功能,能够帮助把最近的数据留在内存中。
我们也看到过这样一种设计,在两个节点的分片上存储用户数据。新数据总是进入“活跃”节点,该节点使用更大的内存和快速硬盘,另外一个节点存储旧数据,使用非常大(但比较慢)的硬盘。应用假设不太会需要旧数据。对于很多应用而言这是合理的假设,依靠10%的最新数据能够满足90%或更多的请求。.
可以通过动态分片来轻松实现这种策略。例如,分片目录表可能定义如下:
CREATETABLEusers(user_idintunsignednotnull,shard_newintunsignednotnull,shard_archiveintunsignednotnull,archive_timestamptimestamp,PRIMARYKEY(user_id));通过一个归档脚本将旧数据从活跃节点转移到归档节点,当移动用户数据到归档节点时,更新archive_timestamp列的值。shardnew和shardarchive列记录存储数据的分片号。
负载均衡的基本思路很简单:在一个服务器集群中尽可能地平均负载量。通常的做法是在服务器前端设置一个负载均衡器(一般是专门的硬件设备)。然后负载均衡器将请求的连接路由到最空闲的可用服务器。图11-9显示了一个典型的大型网站负载均衡设置,其中一个负载均衡器用于HTTP流量,另一个用于MySQL访问。
负载均衡有五个常见目的。
可扩展性
负载均衡对某些扩展策略有所帮助,例如读写分离时从备库读数据。
高效性
负载均衡有助于更有效地使用资源,因为它能够控制请求被路由到何处。如果服务器处理能力各不相同,这就尤为重要:你可以把更多的工作分配给性能更好的机器。
可用性
一个灵活的负载均衡解决方案能够使用时刻保持可用的服务器。
透明性
客户端无须知道是否存在负载均衡设置,也不需要关心在负载均衡器的背后有多少机器,它们的名字是什么。负载均衡器给客户端看到的只是一个虚拟的服务器。
一致性
负载均衡有许多微妙之处,举个例子,其中一个挑战就是管理读/写策略。有些负载均衡技术本身能够实现这一点,但其他的则需要应用自己知道哪些节点是可读的或可写的。
在决定如何实现负载均衡时,应该考虑到这些因素。有许多负载均衡解决方案可以使用,从诸如Wackamole这样基于端点的(peer-based)实现,到DNS、LVS、硬件负载均衡器、TCP代理、MySQLProxy,以及在应用中管理负载均衡。
在我们的客户中,最普遍的策略是使用硬件负载均衡器,大多是使用HAProxy,它看起来很流行并且工作得很好。还有一些人使用TCP代理,例如Pen。但MySQLProxy用得并不多。
有些人认为负载均衡就是配置在应用和MySQL服务器之间的东西。但这并不是唯一的负载均衡方法。你可以在保持应用和MySQL连接的情况下使用负载均衡。事实上,集中化的负载均衡系统只有在存在一个对等置换的服务器池时才能很好工作。如果应用需要做一些决策,例如在备库上执行读操作是否安全,就需要直接连接到服务器。
除了可能出现的一些特定逻辑,应用为负载均衡做决策是非常髙效的。例如,如果有两个完全相同的备库,你可以使用其中的一个来处理特定分片的数据査询,另一个处理其他的査询。这样能够有效利用备库的内存,因为每个备库只会缓存一部分数据。如果其中一个备库失效,另外一个备库拥有所有的数据,仍然能提供服务。
接下来的小节将讨论一些应用直连的常见方法,以及在评估每一个选项时的注意点。
1.复制上的读/写分离
MySQL复制产生了多个数据副本,你可以选择在备库还是主库上执行查询。由于备库复制是异步的,因此主要的难点是如何处理备库上的脏数据。应该将备库用作只读的,而主库可以同时处理读和写査询。
通常需要修改应用以适应这种分离需求。然后应用就可以使用主库来进行写操作,并将读操作分配到主库和备库上;如果不太关心数据是否是脏的,可以使用备库,而对需要即时数据的请求使用主库。我们将这称为读/写分离。
如果使用的是主动一被动模式的主一主复制对,同样也要考虑这个问题。使用这种配置时,只有主动服务器接受写操作。如果能够接受读到脏数据,可以将读分配给被动服务器。
比较常见的读/写分离方法如下:
基于查询分离
最简单的分离方法是将所有不能容忍脏数据的读和写査询分配到主动或主库服务器上。其他的读査询分配到备库或被动服务器上。该策略很容易实现,但事实上无法有效地使用备库,因为只有很少的査询能容忍脏数据。
基于脏数据分离
这是对基于查询分离方法的小改进。需要做一些额外的工作,让应用检査复制延迟,以确定备库数据是否太旧。许多报表类应用都使用这个策略:只要晚上加载的数据复制到备库即可,它们并不关心是不是100%跟上了主库。
基于会话分离
如果有足够的想象力,可以把基于会话的分离方法和复制延迟监控结合起来。如果用户在10秒前更新了数据,而所有备库延迟在5秒内,就可以安全地从备库中读取数据。但为整个会话选择同一个备库是一个很好的主意,否则用户可能会奇怪有些备库的更新速度比其他服务器要慢。
基于版本分离
基于全局版本/会话分离
这个办法是基于版本分离和基于会话分离的变种。当应用执行写操作时,在提交事务后,执行一次SHOWMASTERSTATUS操作。然后在缓存中存储主库日志坐标,作为被修改对象以及/或者会话的版本号。当应用连接到备库时,执行SHOWSLAVESTATUS并将备库上的坐标和缓存中的版本号相对比。如果备库相比记录点更新,就可以安全地读取备库数据。
大多数读/写分离解决方案都需要监控复制延迟来决策读査询的分配,不管是通过复制或负载均衡器,或是一个中间系统。如果这么做,需要注意通过SHOWSLAVESTATUS得到的Seconds_behind_master列的值并不能准确地用于监控延迟。PerconaToolkit中的pt-heartbeat工具能够帮助监控延迟,并维护元数据,例如二进制日志位置,这可以减轻之前我们讨论的一些策略存在的问题。
如果不在乎用昂贵的硬件来承载压力,也就可以不使用复制来扩展读操作,这样当然更简单。这可以避免在主备上分离读的复杂性。有些人认为这很有意义;也有人认为会浪费硬件。这种分歧是由于不同的目的引起的:你是只需要可扩展性,还是要同时具有可扩展性和高利用率?如果需要髙利用率,那么备库除了保存数据副本外还需要承担其他任务,就不得不处理这些额外的复杂度。
2.修改应用的配置
还有一个分发负载的方法是重新配置应用。例如,你可以配置多个机器来分担生成大报表操作的负载。每台机器可以配置成连接到不同的MySQL备库,并为第N个用户或网站生成报表。
这样的系统很容易实现,但如果需要修改一些代码一包括配置文件修改——会变得脆弱且难以处理。硬编码有着固有的限制,需要在每台服务器上修改硬编码,或者在一个中心服务器上修改,然后通过文件副本或代码控制更新命令“发布”到其他服务器上。如果将配置存储在服务器或缓存中,就可以避免这些麻烦。
3.修改DNS名
这是一个比较粗糙的负载均衡技术,但对于一些简单的应用,为不同的目的创建DNS还是很实用的。你可以为不同的服务器指定一个合适的名字。最简单的方法是只读服务器拥有一个DNS名,而给负责写操作的服务器起另外一个DNS名。如果备库能够跟上主库,那就把只读DNS名指定给备库,当出现延迟时,再将该DNS名指定给主库。
这种DNS技术非常容易实现,但也有很多缺点。最大的问题是无法完全控制DNS。
除非应用非常简单,否则依赖于不受控制的系统会非常危险。你可以通过修改/etc/hosts文件而非DNS来改善对系统的控制。当发布一个对该文件的更新时,会知道该变更已经生效。这比等待缓存的DNS失效要好得多。但这仍然不是理想的办法。
我们通常建议人们构建一个完全不依赖DNS的应用。即使应用很简单也适用,因为你无法预知应用会增长到多大规模。
4.转移IP地址
一些负载均衡解决方案依赖于在服务器间转移虚拟地址,一般能够很好地工作。这听起来和修改DNS很像,但完全是两码事。服务器不会根据DNS名去监听网络流量,而是根据指定的IP地址去监听流量,所以转移IP地址允许DNS名保持不变。你可以通过ARP命令强制使IP地址的更改快速而且原子性地通知到网络上。
我们看过的使用最普遍的技术是Pacemaker,这是Linux-HA项目的Heartbeat工具的继承者。你可以使用单个IP地址,为其分配一个角色,例如read-only,当需要在机器间转移IP地址时,它能够感知到。其他类似的工具包括LVS和Wackamole。
一个比较方便的技术是为每个物理服务器分配一个固定的IP地址。该IP地址固定在服务器上,不再改变。然后可以为每个逻辑上的“服务”使用一个虚拟IP地址。它们能够很方便地在服务器间转移,这使得转移服务和应用实例无须再重新配置应用,因此更加容易。即使不怎么经常转移IP地址,这也是一个很好的特性。
迄今为止,我们所讨论的方案都假定应用跟MySQL服务器是直接相连的。但是许多负载均衡解决方案都会引入一个中间件,作为网络通信的代理。它一边接受所有的通信请求,另一边将这些请求派发到指定的服务器上,然后把执行结果发送回请求的机器上。中间件可以是硬件设备或是软件。图11-10描述了这种架构。这种解决方案通常能工作得很好,当然除非为负载均衡器本身增加冗余,这样才能避免单点故障引起的整个系统瘫痪。从开源软件,如HAProxy,到许多广为人知的商业系统,有许多负载均衡器得到了成功的应用。
1.负载均衡器
在市场上有许多负载均衡硬件和软件,但很少有专门为MySQL服务器设计的。Web服务器通常更需要负载均衡,因此许多多用途的负载均衡设备都会支持HTTP,而对其他用途则只有一些很少的基本特性。MySQL连接都只是正常的TCP/IP连接,所以可以在MySQL上使用多用途负载均衡器。但由于缺少MySQL专有的特性,因此会多一些限制。
2.负载均衡算法
有许多算法用来决定哪个服务器接受下一个连接。每个厂商都有各自不同的算法,下面这个清单列出了一些可用的方法:
随机
负载均衡器随机地从可用的服务器池中选择一个服务器来处理请求。
轮询
负载均衡器以循环顺序发送请求到服务器,例如:A,B,C,A,B,C。
最少连接数
下一个连接请求分配给拥有最少活跃连接的服务器。
最快响应
能够最快处理请求的服务器接受下一个连接。当服务器池里同时存在快速和慢速服务器时,这很有效。即使同样的査询在不同的场景下运行也会有不同的表现,例如当査询结果已经缓存在査询缓存中,或者服务器缓存;中已经包含了所需要的数据时。
哈希
负载均衡器通过连接的源IP地址进行哈希,将其映射到池中的同一个服务器上。每次从同一个IP地址发起请求,负载均衡器都会将请求发送给同样的服务器。只有当池中服务器数目改变时这种绑定才会发生变化。
权重
负载均衡器能够结合使用上述几种算法。例如,你可能拥有单CPU和双CPU的机器。双CPU机器有接近两倍的性能,所以可以让负载均衡器分派两倍的请求给双CPU机器。
哪种算法最优取决于具体的工作负载。例如最少连接算法,如果有新机器加入,可能会有大量连接涌入该服务器,而这时候它的缓存还没有包含热数据。
你需要通过测试来为你的工作负载找到最好的性能。除了正常的日常运转,还需要考虑极端情况。在比较极端的情况下——例如负载升髙,修改模式,或者多台服务器下线——至少要避免系统出现重大错误。
我们这里只描述了即时处理请求的算法,无须对连接请求排队。但有时候使用排队算法可能更有效。例如,一个算法可能只维护给定的数据库服务器并发数目,同一时刻只允许不超过N个活跃事务。如果有太多的活跃事务,就将新的请求放到一个队列里,然后让可用服务器列表的第一个来处理它。有些连接池也支持队列算法。
3.在服务器池中增加/移除服务器
在配置连接池中的服务器时,要保证有足够多未使用的容量,以备在撤下服务器做维护时使用,或者当服务器失效时可以派上用场。每台服务器上都应该保留高于“足够”的容量。
要确保配置的限制值足够高,即使从池中撒出一些服务器也能够工作。举个例子,如果你发现每个MySQL服务器一般有100个连接,应该设置池中每个服务器的max_connections值为200。这样就算一半的服务器失效,服务器池整体也能处理同样数量的请求。
最常见的复制拓扑结构就是一个主库加多个备库。我们很难绕开这个架构。许多应用都假设只有一个目标机器用于所有的写操作,或者所有的数据都可以从单个服务器上获得。尽管这个架构不太具有很好的可扩展性,但可以通过一些办法结合负载均衡来获得很好的效果。本小节将讲述其中的一些技术。
功能分区
正如之前讨论的,对于特定的目的可以通过配置备库或一组备库来极大地扩展容量。一些比较常见的功能包括报表、分析、数据仓库,以及全文检索。
过滤和数据分区
可以使用复制过滤技术在相似的备库上对数据进行分区。只要数据在主库上已经被隔离到不同的数据库或表中,这种方法就可以奏效。不幸的是,没有内建的办法在行级别上进行复制过滤。你需要使用一些独创性的技术来实现这一点,例如使用触发器和一组不同的表。
将部分写操作转移到备库
主库并不总是需要处理写操作中的所有工作。你可以分解写査询,并在备库上执行其中的一部分,从而显著减少主库的工作量。<参考复制MySQL主从复制章节>
保证备库跟上主库
同步写操作
正确地扩展MySQL并没有看起来那么美好。从第一天就建立下一个Facebook架构,这并不是正确的方式。最好的策略是实现应用所明确需要的,并为可能的快速增长做好预先规划,成功的规划是可以为任何必要的措施筹集资金以满足需求。
为可扩展性制定一个数学意义上的定义是很有意义的,就像为性能制定了一个精确概念一样。USL能够提供一个有帮助的框架。如果知道系统无法做到线性扩展是因为诸如序列化或交互操作的开销,将可以帮助你避免将这些问题带入到应用中。同时,许多可扩展性问题并不是可以从数学上定义的;可能是由于组织内部的问题,例如缺少团队协作或其他不适当的问题。NeilJ.Gunther博士所写的GuerrillaCapcaityPlanning以及EliyahuM.Goldratt写的TheGoal可以帮助有兴趣的读者了解为什么系统无法扩展。