快速成长期应用架构实践 (7):后端系统扩展

勿忘初心2018-11-15 10:00

欢迎访问网易云社区,了解更多网易技术产品运营经验。


4.2.4 后端系统扩展


后端系统,一般指用户接入端(Web 系统、长连接服务器)和各种中间件之后的后台系 统。在这一阶段,最重要的后端系统就是两种,缓存服务和数据库服务。下面,我们分别以 Redis 缓存服务和 MySQL 数据库为例,来介绍后端系统水平扩展的技术和核心技术点。


1. Redis 水平扩展


Redis 去年发布了 3.0 版本,官方支持了 Redis cluster 即集群模式。至此结束了 Redis 没有官方集群的时代,在官方集群方案以前应用最广泛的就属 Twitter 发布的 Twemproxy (https://github.com/twitter/twemproxy),国内的有豌豆荚开发的 Codis(https://github. com/wandoulabs/codis)。


下面我们介绍一下 Twemproxy 和 Redis Cluster 两种集群水平扩展。


Twemporxy+Sentinel 方案


Twemproxy,也叫 nutcraker,是 Twitter 开源的一个 Redis 和 Memcache 快速/轻量级代 理服务器。


Twemproxy 内部实现多种 hash 算法,自动分片到后端多个 Redis 实例上,Twemproxy 支持失败节点自动删除,它会检测与每个节点的连接是否健康。为了避免单点故障,可 以平行部署多个代理节点(一致性 hash 算法保证 key 分片正确),Client 可以自动选择 一个。
有了这些特性,再结合负载均衡和 Sentinel 就可以架构出 Redis 集群,如图 4-5 所示。


图 4-5 基于 Twemporxy 的 Redis 水平扩展


负载均衡:实现 Twemproxy 的负载均衡,提高 proxy 的可用性和可扩张能力,使 Twemproxy 的扩容对应用透明。
Twemproxy 集群:多个 Twemproxy 平行部署(配置相同),同时接受客户端的请 求并转发请求给后端的 Redis。
Redis Master-Slave 主从组:Redis Master 存储实际的数据,并处理 Twemproxy 转 发的数据读写请求。数据按照 hash 算法分布在多个 Redis 实例上。Redis Slave 复 制 master 的数据,作为数据备份。在 Master 失效的时候,由 Sentinel 把 Slave 提 升为 Master。
Sentinel 集群:检测 Master 主从存活状态,当 Redis Master 失效的时候,把 Slave 提升为新 Master。


水平扩展实现就可以将一对主从实例加入 Sentinel 中,并通知 Twemporxy 更新配置加 入新节点,将部分 key 通过一致性 hash 算法分布到新节点上。


Twemproxy 方案缺点如下。

加入新节点后,部分数据被动迁移,而 Twemproxy 并没有提供相应的数据迁移能 力,这样会造成部分数据丢失。


LB(负载均衡)+ Twemproxy + Redis 3 层架构,链路长,另外加上使用 Sentinel 集群保障高可用,整个集群很复杂,难以管理。


Redis Cluster 方案


Redis Cluster是Redis官方推出的集群解决方案,其设计的重要目标就是方便水平扩展, 在 1000 个节点的时候仍能表现良好,并且可线性扩展。


Redis Cluster 和传统的集群方案不一样,在设计的时候,就考虑到了去中心化、去中 间件,也就是说,集群中的每个节点都是平等的关系,每个节点都保存各自的数据和整个 集群的状态。


数据的分配也没有使用传统的一致性哈希算法,取而代之的是一种叫做哈希槽(hash slot)的方式。Redis Cluster 默认分配了 16384 个 slot,当我们 set 一个 key 时,会用 CRC16 算法来取模得到所属的 slot,然后将这个 key 分到哈希槽区间的节点上,具体算法是 CRC16(key) % 16384。举个例子,假设当前集群有 3 个节点,那么:


节点 r1 包含 0 到 5500 号哈希槽。

节点 r2 包含 5501 到 11000 号哈希槽。

节点 r3 包含 11001 到 16384 号哈希槽。


集群拓扑结构如图 4-3 所示,此处不再重复给出。


Redis Cluster 水平扩展很容易操作,新节点加入集群中,通过 redis-trib 管理工具将其 他节点的 slot 迁移部分到新节点上面,迁移过程并不影响客户端使用,如图 4-6 所示。 

图 4-6 Redis Cluster 水平扩展

为了保证数据的高可用性,Redis Cluster 加入了主从模式,一个主节点对应一个或多 个从节点,主节点提供数据存取,从节点则是从主节点实时备份数据,当这个主节点瘫痪 后,通过选举算法在从节点中选取新主节点,从而保证集群不会瘫痪。


Redis Cluster 其他具体细节可以参考官方文档,这里不再详细介绍。Redis Cluster 方案 缺点如下。


客户端实现复杂,管理所有节点连接,节点失效或变化需要将请求转移到新节点。

没有中心管理节点,节点故障通过 gossip 协议传递,有一定时延。


2. 数据库水平扩展


单机数据库的性能由于物理硬件的限制会达到瓶颈,随着业务数据量和请求访问量的 不断增长,产品方除了需要不断购买成本难以控制的高规格服务器,还要面临不断迭代的 在线数据迁移。在这种情况下,无论是海量的结构化数据还是快速成长的业务规模,都迫 切需要一种水平扩展的方法将存储成本分摊到成本可控的商用服务器上。同时,也希望通 过线性扩容降低全量数据迁移对线上服务带来的影响,分库分表方案便应运而生。


分库分表的原理是将数据按照一定的分区规则 Sharding 到不同的关系型数据库中,应 用再通过中间件的方式访问各个 Shard 中的数据。分库分表的中间件,隐藏了数据 Sharding 和路由访问的各项细节,使应用在大多数场景下可以像单机数据库一样,使用分库分表后 的分布式数据库。


分布式数据库


网易早在 2006 年就开始了分布式数据库(DDB)的研究工作,经过 10 年的发展和演 变,DDB 的产品形态已全面趋于成熟,功能和性能得到了众多产品的充分验证。 

图 4-7 是 DDB 的完整架构,由 cloudadmin、LVS、DDB Proxy、SysDB 及数据节点 组成。


cloudadmin:负责 DDB 的一键部署、备份管理、监控报警及版本管理等功能。 

LVS:负责将用户请求均匀分布到多个 DDB Proxy 上。 

DDB Proxy:对外提供 MySQL 协议访问,实现 SQL 语法解析、分布式执行计划 生成、下发 SQL 语句到后端数据库节点,汇总合并数据库节点执行结果。 

SysDB:DDB 元数据存储数据库,也基于 RDS 实现高可用。

RDS:底层数据节点,一个 RDS 存储多个数据分片。 

图 4-7 网易 DDB 架构


分布式执行计划


分布式执行计划定义了 SQL 在分库分表环境中各个数据库节点上执行的方法、顺序和 合并规则,是 DDB 实现中最为复杂的一环。如 SQL:select * from user order by id limit 10 offset 10。


这个 SQL 要查询 ID 排名在 10~20 之间的 user 信息,这里涉及全局 ID 排序和全局 LIMIT OFFSET 两个合并操作。对全局 ID 排序,DDB 的做法是将 ID 排序下发给各个数据 库节点,在 DBI 层再进行一层归并排序,这样可以充分利用数据库节点的计算资源,同时 将中间件层的排序复杂度降到最低,例如一些需要用到临时文件的排序场景,如果在中间 件做全排序会导致极大的开销。


对全局 LIMIT OFFSET,DDB 的做法是将 OFFSET 累加到 LIMIT 中下发,因为单个数 据节点中的 OFFSET 没有意义,且会造成错误的数据偏移,只有在中间件层的全局 OFFSET 才能保证 OFFSET 的准确性。


所以最后下发给各个 DBN 的 SQL 变为:select * from user order by id limit 20。又如SQL:select avg(age) from UserTet group by name 可以通过 EXPLAIN 语法得到 SQL 的执行 计划,如图 4-8 所示。 

图 4-8 分布式执行计划 

上述 SQL 包含 GROUP BY 分组和 AVG 聚合两种合并操作,与全局 ORDER BY 类似, GROUP BY 也可以下发给数据节点、中间件层做一个归并去重,但是前提要将 GROUP BY 的字段同时作为 ORDER BY 字段下发,因为归并的前提是排序。对 AVG 聚合,不能直接 下发,因为得到所有数据节点各自的平均值,不能求出全局平均值,需要在 DBI 层把 AVG 转化为 SUM 和 COUNT 再下发,在结果集合并时再求平均。


DDB 执行计划的代价取决于 DBI 中的排序、过滤和连接,在大部分场景下,排序可以 将 ORDER BY 下发简化为一次性归并排序,这种情况下代价较小,但是对 GROUP BY 和 ORDER BY 同时存在的场景,需要优先下发 GROUP BY 字段的排序,以达到归并分组的 目的,这种情况下,就需要将所有元素做一次全排序,除非 GROUP BY 和 ORDER BY 字 段相同。


DDB 的连接运算有两种实现,第一种是将连接直接下发,若连接的两张表数据分布完 全相同,并且在分区字段上连接,则满足连接直接下发的条件,因为在不同数据节点的分 区字段必然没有相同值,不会出现跨库连接的问题。第二种是在不满足连接下发条件时,会在 DBI 内部执行 Nest Loop 算法,驱动表的顺序与 FROM 表排列次序一致,此时若出现 ORDER BY 表次序与表排列次序不一致,则不满足 ORDER BY 下发条件,也需要在 DBI 内做一次全排序。


分库分表的执行计划代价相比单机数据库而言,更加难以掌控,即便是相同的 SQL 模 式,在不同的数据分布和分区字段使用方式上,也存在很大的性能差距,DDB 的使用要求 开发者和 DBA 对执行计划的原理具有一定认识。


分库分表在分区字段的使用上很有讲究,一般建议应用中 80%以上的 SQL 查询通过分 区字段过滤,使 SQL 可以单库执行。对于那些没有走分区字段的查询,需要在所有数据节 点中并行下发,这对线程和 CPU 资源是一种极大的消耗,伴随着数据节点的扩展,这种消 耗会越来越剧烈。另外,基于分区字段跨库不重合的原理,在分区字段上的分组、聚合、 DISTINCT、连接等操作,都可以直接下发,这样对中间件的代价往往最小。


分布式事务 

分布式事务是个历久弥新的话题,分库分表、分布式事务的目的是保障分库数据一致 性,而跨库事务会遇到各种不可控制的问题,如个别节点永久性宕机,像单机事务一样的 ACID 是无法奢望的。另外,业界著名的 CAP 理论也告诉我们,对分布式系统,需要将数 据一致性和系统可用性、分区容忍性放在天平上一起考虑。


两阶段提交协议(简称 2PC)是实现分布式事务较为经典的方案,适用于中间件这种 数据节点无耦合的场景。2PC 的核心原理是通过提交分阶段和记日志的方式,记录下事务 提交所处的阶段状态,在组件宕机重启后,可通过日志恢复事务提交的阶段状态,并在这 个状态节点重试,如Coordinator重启后,通过日志可以确定提交处于Prepare还是PrepareAll 状态,若是前者,说明有节点可能没有 Prepare 成功,或所有节点 Prepare 成功但还没有下 发 Commit,状态恢复后给所有节点下发 RollBack;若是 PrepareAll 状态,需要给所有节点 下发 Commit,数据库节点需要保证 Commit 幂等。


2PC 整个过程如图 4-9 所示,更详细的过程与问题说明请参考第 5 章的内容,本节仅 介绍 DDB 的两阶段分布式事务实现。

图 4-9 两阶段提交协议


在网易 DDB 中,DBI 和 Proxy 组件都作为 Coordinator 存在,2PC 实现时,记录 Prepare 和 PrepareAll 的日志必须 sync,以保障重启后恢复状态正确,而 Coordinator 最后的 Commit 日志主要作用是回收之前日志,可异步执行。


由于 2PC 要求 Coordinator 记日志,事务吞吐率受到磁盘 I/O 性能的约束,为此 DDB 实现了 GROUP I/O 优化,可极大程度提升 2PC 的吞吐率。2PC 本质上说是一种阻塞式协议, 两阶段提交过程需要大量线程资源,因此 CPU 和磁盘都有额外消耗,与单机事务相比,2PC 在响应时间和吞吐率上相差很多,从 CAP 角度出发,可以认为 2PC 在一定程度上成全了 C, 牺牲了 A。


另外,目前 MySQL 最流行的 5.5 和 5.6 版本中,XA 事务日志无法复制到从节点,这 意味着主库一旦宕机,切换到从库后,XA 的状态会丢失,可能造成数据不一致,MySQL 5.7版本在这方面已经有所改善。


虽然 2PC 有诸多不足,我们依然认为它在 DDB 中有实现价值,DDB 作为中间件,其 迭代周期要比数据库这种底层服务频繁,若没有 2PC,一次更新或重启就可能造成应用数 据不一致。从应用角度看,分布式事务的现实场景常常无法规避,在有能力给出其他解决 方案前,2PC 也是一个不错的选择。


对购物转账等电商和金融业务,中间件层的 2PC 最大问题在于业务不可见,一旦出现不 可抗力或意想不到的一致性破坏,如数据节点永久性宕机,业务难以根据 2PC 的日志进行补 偿。金融场景下,数据一致性是命根,业务需要对数据有百分之百的掌控力,建议使用 TCC 这类分布式事务模型,或基于消息队列的柔性事务框架,请参考第 5 章,这两种方案都在业 务层实现,业务开发者具有足够掌控力,可以结合 SOA 框架来架构。原理上说,这两种方 案都是大事务拆小事务,小事务变本地事务,最后通过幂等的 Retry 来保障最终一致性。


弹性扩容 

分库分表数据库中,在线数据迁移也是核心需求,会用在以下两种场景中。


数据节点弹性扩容:随着应用规模不断增长,DDB 现有的分库可能有一天不足以 支撑更多数据,要求 DDB 的数据节点具有在线弹性扩容的能力,而新节点加入集 群后,按照不同的 Sharding 策略,可能需要将原有一些数据迁入新节点,如 HASH 分区,也有可能不需要在线数据迁移,如一些场景下的 Range 分区。无论如何, 具备在线数据迁移是 DDB 支持弹性扩容的前提。


数据重分布:开发者在使用 DDB 过程中,有时会陷入困局,比如一些表的分区字 段一开始没考虑清楚,在业务初具规模后才明确应该选择其他字段。又如一些表 一开始认为数据量很小,只要单节点分布即可,而随着业务变化,需要转变为多 节点Sharding。这两种场景都体现了开发者对DDB在线数据迁移功能的潜在需求。

无论是弹性扩容,还是表重分布,都可当作 DDB 以表或库为单位的一次完整在线数据 迁移。该过程分为全量迁移和增量迁移两个阶段,全量迁移是将原库或原表中需要迁移的 数据 DUMP 出来,并使用工具按照分区策略导入到新库新表中。增量迁移是要将全量迁移 过程中产生的增量数据更新按照分区策略应用到新库新表。


全量迁移的方案相对简单,使用DDB自带工具按照特定分区策略DUMP和Load即可。 对增量迁移,DDB 实现了一套独立的迁移工具 Hamal 来订阅各个数据节点的增量更新,Hamal 内部又依赖 DBI 模块将增量更新应用到新库新表,如图 4-10 所示。 

图 4-10 DDB 增量迁移工具 Hamal

Hamal 作为独立服务,与 Proxy 一样由 DDB 统一配置和管理,每个 Hamal 进程负责一 个数据节点的增量迁移,启动时模拟 Slave 向原库拉取 Binlog 存储本地,之后实时通过 DBI 模块应用到新库新表,除了基本的迁移功能外,Hamal 具备以下两个特性。


并行复制:Hamal 的并行复制组件,通过在增量事件之间建立有向无环图,实时 判断哪些事件可以并行执行,Hamal 的并行复制与 MySQL 的并行复制相比快 10 倍以上。


断点续传:Hamal 的增量应用具有幂等性,在网络中断或进程重启之后可以断点 续传。


全局表 

考虑一种场景:City 表记录了国内所有城市信息,应用中有很多业务表需要与 City 做 联表查询,如按照城市分组统计一些业务信息。假设 City 的主键和分区键都是 CityId,若 连接操作发生在中间件层,代价较高,为了将连接操作下发数据节点,需要让连接的业务 表同样按照 CityId 分区,而大多数业务表往往不能满足这个条件。


连接直接下发需要满足两个条件,数据分布相同和分区键上连接,除此之外,其实还 有一种解法,可以把 City 表冗余到所有数据节点中,这样各个数据节点本地连接的集合便 是所求结果。DDB 将这种类型的表称之为全局表。


全局表的特点是更新极少,通过 2PC 保障各个节点冗余表的一致性。可以通过在建表语 句添加相关 Hint 指定全局表类型,在应用使用 DDB 过程中,全局表的概念对应用不可见。


文章节选自《云原生应用架构实践》 网易云基础服务架构团队 著 


网易云计算基础服务深度整合了 IaaSPaaS 及容器技术,提供弹性计算、DevOps 工具链及微服务基础设施等服务,帮助企业解决 IT、架构及运维等问题,使企业更聚焦于业务,是新一代的云计算平台。点击可免费试用