相对于缓存服务器集群,数据存储服务器集群对数据的持久性和可用性提出了更高的要求。因为缓存部分丢失不会影响业务,数据存储服务器则必须保证数据的可靠存储。所以不能简单地使用一致性 Hash。
具体来说,数据存储集群伸缩性又分为两种:
- 关系数据库集群伸缩性
- NoSQL 数据库集群伸缩性
关系数据库集群的伸缩性设计
主流的关系型数据库都支持数据复制功能,我们可以利用这个功能对数据库进行简单伸缩。使用数据复制的 MySQL 集群伸缩性方案。
在这种架构中,数据库写操作都在主服务器上,由主服务器将数据同步到集群中其他从服务器,数据读操作及数据分析等离线操作在从服务器上进行。
除了主从读写分离,还可以使用数据分库,即把不同的业务数据表部署在不同的数据库集群上。使用这种方式的不足是:跨库的表不能进行关联查询(join)。
在实际应用中,还会对一些数据量很大的单表进行分片,即把一张表拆开,分别存储在多个数据库中。
目前比较成熟的支持数据分片的分布式关系数据库有开源的 Amoeba 和 Cobar。它们有相似的架构,这里以 Cobar 为例:
Cobar 是分布式关系数据库的访问代理,部署于应用服务器和数据库服务器之间,也可以非独立部署(以 lib 的方式与应用部署在一起)。应用通过 JDBC 访问 Cobar 集群,Cobar 服务器依据 SQL 和分库规则来分解 SQL,分发到 MySQL 集群中的不同数据库实例上执行(每个实例都部署为主从结构,保证数据高可用)。
前端通信模块接收应用发送过来的 SQL 请求(select * from users where userid in (12, 22, 23))后交与 SQL 解析模块处理,SQL 解析模块解析获得 SQL 中的路由规则查询条件(userid in (12, 22, 23))再转交到 SQL 路由模块。SQL 路由模块根据路由配置的规则(userid 为偶数路由至数据库 A,为奇数路由至数据库 B)把 SQL 语句分解为多条 SQL(select * from users where userid in (12, 22);select * from users where userid in (23);)转交给 SQL 执行代理模块,发送至数据库 A 和数据库 B 分别执行。
两个数据库把执行的结果返回给 SQL 执行代理模块,再通过结果合并,把两个结果集合并为一个结果集,最终返回给应用。
Cobar 如何做集群的伸缩
Cobar 服务器可以看作是无状态的应用服务器,所以可以直接使用负载均衡手段实现集群伸缩。而 MySQL 服务器中存储着数据,所以要做数据迁移(把集群中原有的服务器中的数据迁移到新的服务器),才能保证扩容后数据一致负载均衡。
具体迁移哪些数据可以利用一致性 Hash 算法(即路由模块使用一致性 Hash 算法进行路由),尽量使要迁移的数据最少。因为迁移数据需要遍历数据库中的每一条记录(的索引),并重新进行路由计算确定是否需要迁移,所以这会对数据库造成一定的压力。而且还要解决迁移过程中的数据一致性、可访问性、迁移过程中服务器宕机时的可用性等问题。
实践中,Cobar 服务器利用 MySQL 的数据同步功能进行数据迁移。迁移是以 Schema 为单位。在 Cobar 集群初始化时,为每一个 MySQL 实例创建多个 Schema。Schema 的个数依据业务远景的集群规模来估算,如果未来集群的最大规模为 1000 台数据库服务器,那么总的初始 Schema 数>= 1000。在扩容的时候,从每个服务器中迁移部分 Schema 到新服务器。因为迁移是以 Schema 为单位,所以迁移过程可以使用 MySQL 同步机制:
同步完成后,即新服务器中的 Schema 数据和原服务器中的 Schema 数据一致后,修改 Cobar 服务器的路由配置,把这些 Schema 的 IP 地址修改为新服务器的 IP 地址,最后删除原服务器中被迁移的 Schema,完成 MySQL 集群扩容。
在整个分布式关系数据库的访问请求过程中,Cobar 服务器处理所消耗的时间很少,时间主要还是花费在 MySQL 数据库服务器上。所以应用通过 Cobar 访问分布式关系数据库的性能与直接访问关系数据库是相当的,因此可以满足网站在线业务的实时处理需求。事实上由于 Cobar 代替应用程序连接数据库,数据库只需要维护更少的连接,减少不必要的资源消耗(其实是代理层 Cobar 限制了数据库的并发数,比如代理只有 8 线程,同时的连接数就只有 8 个,用完之后可以不释放。如果不限制线程并发的数量,则 CPU 的资源很快就被耗尽,每个线程执行任务也会相当缓慢,因为 CPU 要把时间片分配给不同的线程对象,而且上下文切换也要耗时,最终造成系统运行效率大幅降低)。
但 Cobar 路由后只能在单一数据库实例上处理查询请求,因此无法执行跨库关联操作,当然更不能进行跨库事务处理。这是分布式数据库的通病。
相比关系数据库本身功能的强大,目前各类分布式关系数据库解决方案都显得简陋,限制了关系数据库某些功能的使用。但因为不断增长的海量数据存储压力,又不得不利用分布式关系数据库的集群伸缩能力,这时就必须从业务上回避分布式关系数据库的各种缺点:避免事务或利用事务补偿机制代替数据库事务;分解数据访问逻辑避免 JOIN 操作等。
NoSQL 数据库集群的伸缩性设计
NoSQL 指的是非关系的、分布式数据库设计模式。一般而言,NoSQL 数据库产品都放弃了关系数据库的两大重要基础:以关系代数为基础的结构化查询语句(SQL)和事务一致性保证(ACID)。而强化其他一些大型网站更关注的特性:高可用性和可伸缩性。
目前应用最广泛的 NoSQL 产品是 Apache HBase。
HBase 的伸缩性主要依赖其可分裂的 HRegion 和可伸缩的分布式文件系统 HDFS 实现。
HBase 中,数据以 HRegion 为单位进行管理,也就是说应用程序如果想要访问一个数据,必须先找到 HRegion,然后将数据读写操作提交给 HRegion,由 HRegion 完成存储层面的数据操作。每个 HRegion 存储一段 KEY 值区间 [key1,key2) 的数据。HRegionServer 是物理服务器,每个 HRegionServer 上可以启动多个 HRegion 实例。当一个 HRegion 写入的数据超过配置的阈值时,HRegion 就会分裂为两个 HRegion,并将 HRegion 在整个集群中进行迁移,以使 HRegionServer 达到负载均衡。
所有的 HRegion 信息(存储的 Key 值区间、所在的 HRegionServer 地址、访问端口号等)都记录在 HMaster 服务器上。为了保证高可用,HBase 会启动多个 HMaster,并通过 ZooKeeper (一个支持分布式一致性的数据管理服务)选举出一个主服务器。应用会通过 ZooKeeper 获得主 HMaster 的地址,输入 Key 值获得这个 Key 所在的 HRegionServer 地址,然后请求 HRegionServer 上的 HRegion,获得需要的数据。
写入过程也类似,需要先得到 HRegion 才能继续操作。HRegion 会把数据存储为多个 HFile 格式的文件,这些文件使用 HDFS 分布式文件系统进行存储,保证在整个集群内分布并高可用。当一个 HRegion 中数据量太多时,HRegion(连同 HFile)会分裂长两个 HRegion,并根据集群中服务器负载进行迁移,如果集群中加入了新的服务器,也就是说有了新的 HRegionServer,由于其负载较低,也会把 HRegion 迁移过去并记录到 HMaster 中,从而实现 HBase 的线性伸缩。