架构实战营 1 期第四模块作业
作业部分
设计千万级学生管理系统的考试试卷存储方案。
基于模块 4 第 6 课的估算结果和 Redis sentinel 的初步方案设计,完善考试试卷存储方案,具体包括:
完善 Redis 的数据结构设计,明确具体使用哪种 Redis 数据结构。
设计具体的读写流程(可以文字描述也可以序列图描述,序列图要有文字辅助说明)。
对照模块 4 第 6 课的性能估算结果,计算 Redis sentinel 集群的服务器数量和性能。
数据结构设计
如果把每场考试看做是互相独立的,那么一场考试只有一份试卷,是典型的“键——单值”模式,所以采用 String 数据结构;
如果把每场考试按照所属学校归类,那么学校和考试是“键——多值”模式。此时可以采用 Hash 数据结构;
不论采用 String 还是 Hash,键里都要包含学校 ID:
当使用 String 类型时,考试的键为学校 ID+考试 ID
当使用 Hash 类型时,考试的键为学校 ID,然后使用考试 ID 作为 Hash 数据结构本身的键来定位一场考试
String 和 Hash 的优缺点如下:
在空间上,当需要存储的有效载荷较小时,String 比较耗费内存,Hash 比较节省内存
虽然在校生有 1000 万,但考试试卷尤其是每一天的考试试卷远没有这么多。Hash 数据结构节省的空间主要体现在只需要在全局哈希表中只需要一个 dictEntry 的结构体(每个结构体的大小为 24 字节,内存分配经过地址对齐后为 32 字节)。考虑到考试没有那么多,所以从空间上考量,两者皆可。
一台 Redis 实例的内存不能太大,否则会增加 RDB 文件生成、传输和重新加载的开销,也会增加从库和主库进行全量复制时主库的压力。
在时间上,查询的效率都是 O(1);
结论
使用 String 或者 Hash 数据结构都可以。
读写流程
考试的时候,学生下载试卷都是只读的过程,不需要更新,所以可以采用如下的流程:
试卷在考试前一定时间内(比如两周)由老师上传到系统,保存在 MySQL 集群中。
系统按照考试时间,在考试开始之前一定的时间内自动把试卷预加载进 Redis 哨兵集群的主节点;
主节点通过主从同步将试卷信息同步到从节点。
试卷的有效期设置为半小时,半小时后,开始自动加载下一轮考试的试卷
考试开始后,客户端可以选择从任何一个节点拉取试卷。
计算 Redis sentinel 集群的服务器数量和性能
空间
假设每场考试试卷的大小是 200K 字节,每台 Redis 实例的内存大小为 4G,则整个 sentinel 集群在某个场次可以存储 4 * 1024 * 1024 / 200 即超过 20000 张试卷,满足要求。
读写性能
在考试场景的“用户行为建模和性能估算”阶段,估算的请求试卷的 QPS 为 5 万/秒,Redis 单机的 TPS 为 5~10 万,这里按 5 万来算的话,对于 Redis sentinel 集群来说,至少需要 3 个哨兵实例,为了高可用,每个哨兵实例位于一台单独的机器上;考虑到简单部署,每台机器上也部署一个 Redis Server,这样整个集群至少可以提供 15 万的 TPS,满足需求,
笔记部分
数据库存储架构
(番外)MySQL 复制架构
MySQL 复制原理
MySQL 的复制都是基于 binlog 进行的,那 binlog 是何时写入完成的呢?见下图,是“时刻 B”完成的。
上图中的
commit
是COMMIT
语句即事务提交过程中的一个小步骤,当这个步骤执行完后,事务提交就结束了。MySQL 复制基本步骤
创建复制所需的账号和权限;
从 Master 服务器拷贝一份数据,可以使用逻辑备份工具 mysqldump、mysqlpump 或物理备份工具 Clone Plugin;
通过命令
CHANGE MASTER TO
搭建复制关系通过命令
SHOW SLAVES STATUS
观察复制状态。配置(用于保证 crash safe,即无论 Master 还是 Slave 宕机,当它们恢复后,连上主机后,主从数据依然一致)
gtid_mode = on enforce_gtid_consistency = 1 binlog_gtid_simple_recovery = 1 relay_log_recovery = ON master_info_repository = TABLE relay_log_info_repository = TABLE
MySQL 复制类型
异步复制
半同步复制。并不是 MySQL 内置功能,需要安装半同步插件,设置 N 个 Slave 接收 binlog 成功
plugin-load="rpl_semi_sync_master=semisync_master.so;rpl_semi_sync_slave=semisync_slave.so" rpl-semi-sync-master-enabled = 1 #启用半同步Master插件 rpl-semi-sync-slave-enabled = 1 #启用半同步Slave插件 rpl-semi-sync-master_wait_no_slqve = 1 #提交的事务必须至少有一个Slave接收到二进制日志
有损半同步复制
下图的
WAIT ACK
点应该是“MySQL 两阶段提交过程”图完成后的那个时间点,此时主节点的事物已经提交了。
无损半同步复制
在半同步复制的配置基础上,需要额外做如下的配置项
rpl_semi_sync_master_wait_point = AFTER_SYNC
从下图可以看出,是先等待从节点响应后,在提交,从而保证从节点接收到了最新的日志,下图的
WAIT ACK
点应该对应“MySQL 两阶段提交过程”图中的“时刻 B”。
多源复制
延迟复制:允许 Slave 延迟回放接收到的 binlog,避免主服务器上的误操作马上同步到从服务器,导致数据完全丢失.。本质上说拥有了一份 N 小时以前的快照。
数据库读写分离架构
如何判断要读写分离
业务量持续增长。一般情况下读请求增长的速度会远远超过写请求增长的速度。
上主从架构之前应该先优化现有系统(索引、缓存、冷热数据分离)
复杂度分析
复制延迟
会导致数据主从节点数据不一致:
如果先写主节点,然后在从节点上读取不到数据(还未同步),则违背读自己写一致性;如果是另一用户去从节点读取,也会导致数据不一致,这个对应哪个一致性还未知,可能是广义的写后读一致性。在单机上,这个可用读已提交来达成;。
如果这个“另一用户”发起了写请求,因为只有主节点可以写,所以不会违背先读后写一致性(即两个用户之间的写入要保证先后顺序);同时,这两次写入在同步到从节点的时候也需要保证这个先后顺序不变,即保证因果关系的一致性,如果做到这一点,就说做到了前缀一致性。
如果第一次读取从节点 A 成功,第二次读从节点 B 失败(从节点之间数据不一致),则违背“单调读一致性”(即一旦一个用户读取到某个值,不会读到比这个更旧的值)
这些一致性都是从客户端的角度来说的,解决办法就是将客户端与数据库节点的对应关系固定下来:
直接绑定——读写绑定。写操作后的读操作指定发送给数据库主服务器。缺点:业务侵入比较大。
出错后再绑定——二次读取。读从机失败后再读一次主机。缺点:如果二次读取较多,增加主机压力。
按需绑定——业务分级。关键业务全部使用主机,非关键业务采用读写分离。缺点:无法约束编码人员。
对于 MySQL 来说,数据复制是基于 binlog。binlog 日志简单易懂,但事务不能太大,否则会导致二进制日志非常大,一个大事务的提交会非常慢,同时导致主从复制延迟变大。
一定要对大事务特别对待:
设计时,把 DELETE 删除操作转化为 DROP TABLE/PARTITION 操作:把流水和日志类的表按时间分表或者分区,便于 DROP。
业务设计时,把大事务拆成小事务。
如果没有分区或者分表,对于 DELETE 操作可以如下拆分成小事务:
3. 配置参数优化
要彻底避免 MySQL 主从复制延迟,数据库版本至少要升级到 5.7,因为之前的 MySQL 版本从机回放 binlog 都是单线程的(5.6 是基于库级别的单线程)。从 MySQL5.7 版本开始,MySQL 支持了从机多线程回放 binlog 的方式,称为"并行复制(Multi-Thread Slave, MTS)"。
从机并行复制有两种模式:
COMMIT ORDER:主机怎么并行,从机就怎么并行。
WRITESET:基于每个事务,只要事务更新的记录不冲突,就可以并行。即使主机是单线程,只要插入的记录没有冲突(比如唯一索引冲突),从机依然可以多线程回放,此时主从几乎没有延迟,配置如下:
4. 主从复制延迟监控
Seconds_Behind_Master
通过命令
SHOW SLAVE STATUS
,其中的 Seconds_Behind_Master 可以查看复制延迟。但这个参数并不准确。
心跳表
在主机上引入一张心跳表,主机上写入的时间会被复制到从机:
建表语句如下:
REPLACE 语句用于定期更新当前时间,并存入到表 heartbeat(正常运行情况下只有一条记录)。定期执行 REPLACE 语句可以使用定期脚本调度程序,也可以使用 MySQL 自带的时间调度器(event scheduler):
需要分离读写请求到不同的节点
程序代码封装模式(程序代码已经是多份,自带高性能高可用)
中间件封装模式(中间件要高性能高可用)
数据库分库分表
主要目的是解决主节点写入性能瓶颈和存储容量瓶颈。
数据分库
Join 问题
原本在同一个数据库中的表分散到不同数据库中,导致无法使用 SQL 的 Join 查询。解决办法如下:
小表冗余。将一些小表在每个库中冗余一份,例如字典表。
代码 Join。在代码里实现 Join 功能
字段冗余,例如订单表直接记录商品类型。
事务问题
表分散到不同的数据库中,无法通过事务统一修改,只能使用分布式事务。
数据分表
垂直拆分:即案列拆分,优化单机处理性能,常见于 2B 领域宽表的拆分。可能原理:每行变短后,每个磁盘页可以存储更多的数据,从而减少磁盘 IO。
水平拆分:按行拆分,提升的是系统的处理性能,常见于 2C 领域大表的拆分。
多大的表需要拆分?
从磁盘 IO 的角度进行考虑:B+Tree 的层数:3 层大约是 2000 万条数据;
从内存 buffer 的角度进行考虑:Innodb buffer pool:2000 万条数据,每条数据 100 字节,单表就 2G 了;
综合考虑:数据量持续增长的表需要拆分。
水平分表复杂度
水平分表后由于数据分散在多张表中,会引入如下问题:
路由问题
如何计算 Count
如何执行与其它表 Join
如何执行 Order by
建议直接使用数据库中间件比如 Sharding-JDBC 来解决。
水平分表能够通过加服务器来不断提升性能么?
不能。
数据库本身的性能会提高,但是聚合操作点是无法靠增加数据库数量来提升性能的。
此外,聚合点比如 Sharding-JDBC 等 SDK 与数据库服务器的连接数会非常多,制约着服务器的数量。
水平分表伸缩瓶颈
每个应用都需要连接所有的分片,当应用数量增多后,数据库连接会逐渐成为瓶颈。以 MySQL 为例,默认 100 连接,实测 50~100 连接性能最高,超过 200 后会显著下降。
单个 Sharding-JDBC 的聚合操作会有性能瓶颈。
(番外)关于 InnoDB Buffer Pool
Buffer Pool 有加速更新的作用,可以减少随机写盘。
在事务 commit 的时候,根据 WAL 机制,要顺序写 redolog。同时,在事务更新的过程中,日志要写多次,比如一个事务中包含多条 insert 语句,插入数据过程中生成的日志都要先存起来,但有不能在还没有 commit 的时候就直接写到 redo log 文件里(因为 redolog 中有 commit 标志就表示事务提交成功了)。所以 redo 日志要先存到 redo log buffer(buffer pool 的一部分)中,在执行 commit 语句时才真正写 redolog 文件。
更新数据的时候,数据是在内存页中更新的(即脏页,脏页由系统定期刷盘),而内存页是由 buffer pool 管理的。
change buffer
加速普通索引记录的更新。
进一步的,当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中,在不影响数据一致性的前提下,InnoDB 会将这些更新操作缓存在change buffer
中,这样就不需要从磁盘中读入这个数据页了。如果有查询需要访问这个数据页,才会将数据页从磁盘读入内存,然后执行change buffer
中与这个页有关的操作。
将change buffer
中的操作应用到原始数据页,得到最新结果的过程称为merge
。
什么条件下可以使用 change buffer?
对于唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束,所以必须将数据页读入内存才能判断。既然已经读入内存了,就直接更新内存吧,所以此时不会使用 change buffer。
所以,只有普通索引可以使用。将更新记录直接记录在 change buffer,不用读盘,语句直接执行结束。
唯一索引会更多的读取磁盘,所以有大量插入的数据,如果有唯一索引的话,会引起性能下降。
设置参数
change buffer 用的是 buffer pool 里的内存,其大小可以通过参数
innodb_change_buffer_max_size
来动态设置。比如当为 50 的时候,标识 change buffer 的大小最多只能占用 buffer pool 的 50%。
change buffer 的使用场景
在一个数据页做 merge 之前,change buffer 记录的变更越多(也就是这个页面上要更新的次数越多),收益就越大。所以,对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时 change buffer 的使用效果最好。这种业务模型就是常见的账单类、日之类系统。反之,写入之后马上查询的模式会立即出发 merge 的过程,反而会起到副作用。
change buffer 和 redolog
这两个组件都用于更新数据,区别是:
change buffer 是正常情况下来更新数据。其记录的是写操作的相关信息,如果数据页被读入内存了,则可以直接应用。依靠减少随机读磁盘 IO 的消耗来提高性能。
redolog 是崩溃恢复时用于更新数据。由于 redo log 并没有记录数据页的完整数据,所以它并没有能力自己去更新磁盘数据页。在恢复的过程中,InnoDB 如果判断到一个数据页可能丢失了更新,就会先将它读到内存中,然后让 redo log 更新内存。依靠将随机写改为顺序写提升性能。
上面两个组件都会生成脏页,脏页的刷盘由系统进行。
更新的过程如下:
如果数据页(比如 Page 1)在内存中,则直接更新内存。
如果数据页(比如 Page 2)不在内存中,就在内存的 change buffer 区域,记录下类似于“我要往 Page2 插入一行”这个信息。
将上述两个动作记入 redo log 中。
更新之后的读请求过程如下:
读 Page 1 的时候,直接从内存返回。虽然磁盘上的数据还是旧的,但直接从内存返回结果是正确的。
读 Page 2 的时候,需要把它从磁盘读入内存中,然后应用 change buffer 里面的操作日志,生成一个正确的版本并返回。
Buffer Pool 加速查询——数据页刷新。由于有 WAL 机制,当事务提交的时候,磁盘上的数据页是旧的,如果这时候马上有一个查询来读取这个数据页,是不需要把 redolog 应用到(磁盘)上的数据页然后查询磁盘数据页的,只需要直接读内存页就可以了。
Buffer Pool 对查询的加速效率,依赖于一个重要的指标,内存命中率。可以在
show engine innodb status
结果中查看一个系统当前的命中率Buffer pool hit rate
,如果要保证响应时间的话,内存命中率要在 99%以上。当 Buffer Pool 满了,会按照 LRU 算法淘汰旧数据页。如果此时对一个历史表做全表扫描,就会把 Buffer Pool 里的数据全部淘汰,导致 Buffer Pool 的命中率急剧下降、磁盘压力增加,SQL 语句相应变慢。所以 InnoDB 在实现上,按照 5:3 的比例把整个 LRU 链表分成了 young 区和 old 区,类似于 JAVA 的 GC 算法,但是名称含义和 GC 的内存分区是相反的,InnoDB LRU 的 old 区相当于 GC 的 young 区,从磁盘上新读入的内容位于这个区,在下一次访问的时候,只有当它在 LRU 中存在的时间超过了
innodb_old_blocks_time
秒(默认为 1 秒),才会被移动到整个 LRU 的头部。
Buffer Pool 加速查询——Multi-Range Read 优化(MRR)
“回表”:InnoDB 在普通索引 a 上查到主键 id 的值后,在根据一个个主键 id 的值到主键索引上去查整行数据的过程。回表是一行行的查数据而不是批量地查询。
上图中,如果随着 a 的值递增顺序查询的话,id 的值就变成随机的,性能相对较差。因为大多数的数据都是按照主键递增顺序插入得到的,所以可以认为如果按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能。
MRR 优化思路是将根据索引 a 得到的主键 id 放入到read_rnd_buffer
中并进行排序后再查询主键索引。read_rnd_buffer
的大小由 read_rnd_buffer_size 参数控制,此外想要稳定的使用 MRR 优化的话,需要设置
如果 MRR 生效,使用 explain 查看执行计划是,在Extra
字段会显示Using MRR
。
MRR 能够提升性能的核心在于,查询语句在索引 a 上做的是一个范围查询(多值查询),可以得到足够多的主键 id。这样通过排序以后,再去主键索引查数据,才能体现出顺序性的优势。
InnoDB buffer pool 相关参数
InnoDB buffer pool 的大小由参数 innodb_buffer_pool_size 确定的,一般建议设置成可用物理内存的 60% ~80%。
数据库分布式事务
无笔记。
思考题
为什么数据库系统自己不实现分库分表的功能,而 Redis、MongoDB 等却都提供 sharding 功能?
Redis、MongoDB 等之所以提供 sharding 功能,是因为它们放弃了事务。而数据库不同,如果数据库系统要自己实现分库分表功能,那么无可避免的就要解决分布式事务,分布式事务是由多个单机事务组成的,每个实现单机事务的节点,本身就可以看做一个传统意义上的数据库。
分布式事务本身就有点类似于递归的概念,是一个分布式系统,由数据库系统本身实现分库分表(即实现分布式事物)的说法本身就是一个自相矛盾的概念。
对于原生的分布式数据库,计算与存储分离,底层存储基于分布式键值系统,存储层面完全放弃了数据库事物能力。
存储架构模式 - 复制架构
复制架构主要解决高可用问题。
存储类问题处理框架图
这里不止是关注数据库系统,缓存等系统也包括在内。但是可以以 MySQL 为例来理解和记忆。
存储类问题高可用处理框架主要解决故障和灾难场景下的可用和可恢复两个目标。
故障时的目的是可用和可恢复,主要手段是复制架构。
灾难(比如整个城市发生地震)
如果此时要保证可用,就需要做多活,可以把请求切换到另外一个城市去。
如果此时只要求可恢复,那备份的手段就够了。
直接上多活架构就好了,还要复制架构干啥呢?
多活的成本和技术复杂度都非常高
即使有了多活,在某个数据中心发生故障时,仍然需要复制架构保证这个数据中心是可用和可恢复的,不可能随随便便就切换数据中心,这样风险非常大。
高可用关键指标
RPO, Recovery Point Objective,恢复点目标,指“最大可接受的数据损失”,因为数据备份和复制都有时间限制,不可能做到绝对实时。
RTO, Recovery Time Objective, 恢复时间目标,指“最大可接受系统恢复所需时间”,因为定位、处理、恢复需要时间。
WRT, Work Recovery Time, 工作恢复时间, 指“系统恢复后,业务恢复所需时间”,因为要进行各种业务检查、校验、恢复。
MTD, Maximum Tolerable Downtime, 最大可容忍宕机时间,等于 RTO + WRT。
注意:高可用计算不涉及 RPO,因为它没有状态。
主备和主从架构
主备复制
主备架构的灾备部署:即多个备机位于不同个机房(机房可以位于一个城市,也可以位于不同城市)。
主备切换:应用于内部系统或者管理系统
主从复制
主从架构的灾备部署:类似于主备架构的灾备部署,但是只有和主机位于同一个 IDC 的服务会被设置为从机,位于不同 IDC 的服务器充当备机,不提供服务,这样做也是为了避免夸 IDC 的主从延迟。
主从切换,类似于主备切换
主备级联复制(Redis 和 MySQL 支持此模式)。主动架构一般不会做主从级联复制,因为这样会导致级联从机延迟太大。
集群选举架构
最佳实践是基于 ZooKeeper 实现双机切换或者集群选举,这样可以大大降低复杂度:
ZooKeeper 自己实现了高可用
基于 ZooKeeper,切换或选举过程实现比较简单
ZooKeeper 可以有多用途
(番外)MySQL 复制架构
参见本文笔记第一节“数据库存储架构”——“(番外)MySQL 复制架构”。
思考题
为什么 MongoDB 要改掉 Bully 而 Elasticsearch 可以用 Bully ?
因为 Bully 算法在选举主节点的时候,会选取节点 ID 最大的节点作为主节点。MongoDB 的分布式选举中,采用节点的最后操作时间戳来表示 ID,时间戳最新的节点其 ID 是最大的,也就是说选举时间戳最新的、活着的节点为主节点。
Bully 算法的特点是简单(谁活着且谁的节点 ID 最大谁就是主节点,其它节点必须无条件服从)、选举速度快、算法简单。它的缺点是每个节点需要有全局的节点信息,因此额外信息存储较多;其次,任意一个比当前主节点 ID 大的新节点或节点故障恢复加入集群的时候,会出发重新选主。
这些特点对于 MongoDB 这种文档数据库来说是不可接受的,对 Elasticsearch 来说却没有什么影响。
分片架构和分区架构
分片架构
分片架构与主从复制架构不同:
主从复制架构
只有主机承担写请求,写性能会存在瓶颈;
每台机器保存全量数据,存储存在瓶颈
分片架构:通过叠加更多服务器来提升写性能和存储性能。
设计的核心点
有两个:
分片规则:按照什么规则对数据分片
分片在不同系统中有各自的别名,Spanner 和 YugabyteDB 中被称为 Tablet,在 HBase 和 TiDB 中被称为 Region,在 CockraochDB 中被称为 Range。无论叫什么,概念都是一样的,分片是一种水平切分数据表的方式,它是数据记录的集合,也是数据表的组成单位。
核心原则:让数据均匀分布、避免热点;选取基数(Cardinality,被选的数据维度取值范围)
分片用的数据:
按主键分片:适合主业务数据,比如用户 ID,订单 ID。使用哈希策略。
按时间分片:适合流水性业务。使用范围策略
分片规则
哈希(Hash)分片:分布均匀,但是不支持范围查询;扩容服务器麻烦,需要做数据迁移。即哈希分片不支持动态调度。
范围(Range)分片:分布可能不均匀,但是支持范围查询;方便扩容服务器,无需迁移历史数据。
从某种意义上来说,Range 是更好的分片策略,就是因为 Range 分片有条件做到更好的动态调度,只有动态了,才能自适应各种业务场景下的数据变化,平衡存储、访问压力、分布式事务和访问链路延时等多方面的诉求。
调度机制
静态:静态意味着分片在节点上的分布基本是固定的,即使移动也需要人工的介入;
动态:动态则是指通过调度管理器基于算法在各节点之间自动地移动分片。
动态分片是原生分布式数据库(相对于从传统单点数据库改造而来的分布式数据库而言的)采用 Range 分片时具备的能力。动态分片有两个好处:
减少分布式事物。比如,Spanner 在 Tablet,也就是 Range 分片,之下增加了目录(Directory),作为数据调度的最小单位,它的调度范围是可以跨 Tablet 的。通过调度 Directory 可以将频繁参与同样事务的数据,转移到同一个 Tablet 下,从而将分布式事务转换为本地事务。由此可以看出,分片单位与调度单位可以分开。
缩短服务延时。比如,Spanner 可以将 Directory 调度到靠近用户的数据中心,缩短数据传输时间。
分片可以自动完成分裂与合并:当单个分片的数据量超过设定值时,分片可以一分为二,这样就可以保证每个分片的数据量较为均衡。多个数据量较少的分片,会在一定的周期内被合并为一个分片。
可以根据访问压力调度分片。
数据均匀分布并不意味着读写均匀分布!
路由规则:业务服务器怎么找到数据
和调度机制非常像。但我认为两者并不完全等同,调度机制更多的是分布式数据库内部实现的,而路由规则是从业务逻辑出发的。
静态路由:使用配置文件来实现,实现简单,但不灵活,无法动态扩容和平衡数据库分表用这种形式。数据库分表一般采用这种形式。
动态路由,有两种实现方式
配置中心:有一个地方集中管理数据和分片服务器之间的对应关系。比如 MongoDB 使用 Config Servers 做配置中心
HDFS 使用 NameNode 做配置中心
配置中心的架构可以支持超大规模集群,节点数量可达成百上千。
但是架构复杂,独立的配置中心节点需要做到高可用,如 MongoDB 使用 Replica Set,HDFS 使用 ZooKeeper。
动态转发:分片服务器之间互相知道数据的分布情况,比如 Redis Cluster。如果某个分片收到请求,数据不在,可以转发请求到对应的分片。实现复杂,支持动态扩容和平衡。
具体的有两种形式:
客户端重定向,比如 Redis Cluster
服务端转发请求
动态转发的形式要求所有的节点都保存路由信息,且互相之间的网络互通,这就限制了集群的规模。但是架构相对简单,一般通过 gossip 协议来更新分片信息。
分片架构高可用方案
独立备份
节点级的高可用,即最小高可用元是由主节点和多个备节点组成,这些节点在 TDSQL 中被称作 Set,Set 的主备节点间复制,多采用半同步复制。
例子:MongoDB,Redis,MySQL 等。
互相备份
分片级高可用,最小高可用单元是分片,在有些分布式数据库中被称作复制组(Group),复制组由一个主副本和若干个副本组成,通过 Raft 或 Paxos 等共识算法完成数据同步。这些副本可以位于不同的节点。
例子:HDFS、Elasticsearch、Kafka 等。
从架构设计角度看,Group 比 Set 更具优势:
Group 的高可用单元更小,出故障时影响的范围更小,系统整体的可靠性就更高;
Group 的主副本可以在所有节点上运行,资源可以得到最大化使用。而 Set 模式下,大部分备节点是不提供有效服务的,资源使用率不高。
分区架构
分片架构无法应对城市级别的故障,这是引入分区架构,通过冗余 IDC 来避免城市级别的故障,同时还可以提供就近访问。
分片架构无法夸城市部署,会导致分片架构的性能急剧下降,所以无法远距离部署。
分区架构全局路由
DNS
GSLB
分区架构备份策略
集中式:所有分区都在同一个独立的备份中心做备份。使用最多的一种策略,因为复杂度低,扩展起来比较容易,成本又适中。
互备式:类似于分片架构的互备式架构
独立式:每个分区都有自己独立的备份中心
思考题
既然数据集群就可以做到不同节点之间复制数据,为何不搭建一个远距离分布的集群来应对地理位置级别的故障呢?
应该是基于性能考虑吧,距离太远,会导致性能下降太严重。
如何设计存储架构
总体思路
估算性能需求 -> 选择存储系统 <-> 设计存储方案。(“<->”表示迭代)
性能需求:存储量和读写性能
选择存储系统:根据技术储备,方案优缺点,选择合适的存储系统
根据选择的存储系统,设计存储方案;如果发现不行,回到上一步,重新选择一个存储系统
估算业务所需存储性能
模型:
用户量预估 -> 用户行为建模 -> 性能需求计算。
上述步骤,
在 2B 领域是一个澄清的过程——与客户或者解决方案架构师进行沟通后明确下来,因为没有比甲方或者业务方更熟悉业务的人了;
解决方案架构师更注重于业务分析,系统架构师更偏向于系统设计。
在 2C 领域是一个决策的过程,需要与产品人员、运营人员以及老板沟通后,决定一个模型。
用户量预估的方法有三个(注意:用户不一定是人,可以是设备(IoT 平台),可以是公司(云平台)):
规划的用户量,可以根据成本、预算、目标等确定。比如某个新业务预算投入 2000 万拉新,那出一单个新用户的成本就可以知道总的用户数;
推算的用户量,即基于已有的相关数据进行推算。比如业务场景有明确的目标用户,比如面向广州在校大学生的业务;
和标杆对比,得出自己的用户量。这个标杆可以是竞争对手,也可以是自己的同类业务。
用户行为建模即用户行为的模式,比如某种行为的用户数量或者频率,行为随时间段的分布,典型的行为有哪些,等等。比如,“预计每个月使用钱包付款码的用户有 100 万,付款笔数达到 500 万笔”等等。
存储性能需求计算包括数据量,读写性能(QPS/TPS),以及为未来预留增长的空间。有如下技巧:
根据数据的特点进行估算,比如根据数据的冷热将历史数据和当前数据分开;
TPS/QPS 等读写性能单位是秒,不要看总体的数据。还要注意区分“平均值”和“峰值”;
预留增长空间要合理(估算量的 1.5 倍或者两倍),如果可以线性伸缩则是最好的。
如何选择存储架构
如何选择合适的存储系统
首先从技术本质出发,选择适合应用场景和系统本质的系统.
接着在适合的多个系统中选择自己最熟悉的的系统。
最后综合考虑成本、可维护性、成熟度等质量属性。
技术本质 V.S. 技术细节
技术本质决定了系统适合的应用场景,而技术细节不会;但是技术细节会在方案落地的时候影响方案设计。在做备选方案的时候,不需要对技术细节有十分深入的了解。
如何设计存储方案
设计数据结构,例如设计表结构,选择 Redis 的数据结构;
验证读写场景。将数据结构放到具体的场景进行验证,设计读写执行的具体过程(Rule);
评估读写性能;
如果需要的话,进行迭代。
思考题
为什么存储系统如此多?
因为存储系统是系统中最复杂的,一个系统在设计是无法做到绵绵具体;而具体到业务场景,总是各有特点的,也不会对所有指标都有很高的要求。所以存储系统在设计的时候,针对某个点方面进行加强是最明智的。这就导致了存储系统如此众多。
常见存储系统剖析
如何学习存储系统
步骤
理解技术本质:比如知道 Redis 是 KV 存储系统,而 HBase 是 sorted map;
明确部署架构:比如知道 Redis 有三种部署架构(单点、哨兵、集群);
研究数据模型:比如 Redis 的多种数据结构;
模拟业务场景。
如果官方文档比较完善,上述内容都可以找到。
思考题
ClickHouse 做数据分析和 Hadoop 做数据分析有什么优点?
ClickHouse 兼容 SQL,如果现有的分析系统是基于关系数据库的话,会容易的多。
千万学生管理系统存储架构设计
在估算存储性能需求的时候,要分场景进行,比如学生管理系统存储架构在估算时分为如下几个场景:
登录注册
文件上传、下载
选课
考试
思考题
如果考虑分区架构,整体存储架构如何设计?
评论