本文大致会从以下几个方面入手, 谈谈笔者对数据存储方案选型的看法:
从 MySQL 到 HBase 集群化方案的演化
MySQL 与 HBase 的性能取舍
不同方案的优化思路
总结
一, 集群化方案
1,MySQL 应用的演化
MySQL 与 HBase 说到最核心的点, 是一种数据存储方案. 方案本身没有对错, 没有好坏, 只有合适与否. 相信多数公司都与 MySQL 有着不解之缘, 部分学校的课程甚至直接以 SQL 语言作为数据库讲解. 我想借自身经历, 先来谈谈 MySQL 应用的演化.
只有 MySQL
笔者之前曾在一家 O2O 创业公司工作, 公司所有数据都存储在同一个 MySQL 里, 而且没有任何主备方案. 相信这是很多初创公司会用到的一个典型解决办法, 当时这台 MySQL 为用户, 订单, 物流服务, 同时也为线下分析服务.
单实例的问题:
一旦 MySQL 挂了, 服务全部停止;
一旦 MySQL 的磁盘坏了, 公司的所有服务都没有了 (一般会定时备份数据文件).
主从方案
随着业务增加, 单个 DB 是无法承载这么多请求的. 于是就有了主从复制, 读写分离的解决方案.
master 只负责写请, slave 同步 master 用来服务读请求:
为了扩展读能力可以增加多个 slave;
允许 slave 同步有一定的延迟;
一致性要求严格的, 可以指定读主库.
主从功能的问题:
需要增加管理 Proxy 层, 分配写请求, 读请求;
节点故障: 其它节点应该快速接管故障节点的功能.
垂直拆分
业务继续增长, master 甚至无法承载所有的写请求, 数据库需要按业务拆分.
垂直拆分的问题:
线下分析, 需要在业务代码里 join 各个表. 因为拆成多个库, 已经无法 join 了.
不容易做数据库的事务性, 用户余额减少与下单成功的情况下无法使用 MySQL 的事务功能.
水平拆分
业务继续增长, 订单表有大量的并发写入, 而且已经有了几千万行数据.
单个库无法承载大量的并发写入;
上千万行的大表, 数据写入可能需要调整一棵巨大的 B + 树;
上千万行, B + 树过深, 读写需要更多的磁盘 IO;
很多老数据访问较少, B + 树上层缓存的部分信息无用;
......
参考: 大众点评订单系统分库分表实践
https://zhuanlan.zhihu.com/p/24036067
水平分库 / 分表带来的问题:
维护 map 方案;
辅助索引只能局部有效;
由于分库, 无法使用 join 等函数; 由于分表 count,order,group 等聚合函数也无法做了;
扩容: 需要再次水平拆分的: 修改 map, 迁移数据......
2,MySQL 的问题
MySQL 的主要瓶颈, 单机单进程. CPU 有限, 内存 / 磁盘功能, 连接数有限, 网卡吞吐有限......
集群的限制点:
关系型数据库, 纵向的外键相互 join;
范式参考链接: https://zhuanlan.zhihu.com/p/20028672
数据库事务性, 基于单机的锁机制, 无法扩展到集群中使用;
全局有序列性基于 B + 树, 数据有序聚合存储, 集群化后无法保证;
数据本地存储, 扩容需要迁移数据.
集群的方案:
放弃部分功能, 辅助索引检索, join, 全局事务性, 聚合函数等;
水平拆分: 存储 KV 化, 用机械的 map 思路实现集群;
扩容方案: 手动导数据, 开发数据迁移脚本;
事务性: 两阶段事务, paxos, 单库事务......
备份容灾: 从节点同步主节点, 但有一定的数据延迟;
服务稳定性: 主节点挂了, Proxy 会将从节点升级为主节点; 从节点挂了会被其它从节点替换.
3,HBase 集群化解决方案
水平拆分:
region: 拆分后的子表;
Region Server: 管理这些数据的 server, 相当于一个 MySQL 实例;
.META. 表存储拆分信息 map<row, server>.
单个 region 过大, RegionServer 会将 region 均分为两个 (自动, 手工). 然后更新. META. 表.
扩容方案:
RegionServer 向 HMaster 汇报状态. HMaster 为 RegionServer 负载均衡, 调整其负责的 region .
增 / 删 RegionServer 后, 会为重新调整 region 的分配方式.
服务稳定性:
RegionServer 只是计算单元, 挂掉后 Hmaster 可以随便再找一个节点代替坏节点服务.
事务性:
HBase 只保证行级事务, 单行数据肯定存在同一台机器 (单机事务很好做).
备份容灾:
数据使用 HDFS 存储, 多复本, 任何一个复本挂掉都不影响功能;
RegionServer 只是计算单元, 挂掉后不影响服务.
二, 性能取舍
1, 数据请求流程
HBase:
Client 会通过 Zookeeper 定位到 .META. 表;
根据 .META. 查找需要服务的 RegionServer, 连接 RegionServer 进行读写;
Client 会缓存 .META. 表信息, 下次可以直接连到 RegionServer .
MySQL:
Client 通过 Proxy, 查找需要连接的 MySQL 实例, 连接并进行读写.
Rquest 的路由流程, MySQL 与 HBase 基本一致, 那么 RegionServer 与 MySQL 的性能差异如何呢?
2,Hbase 写得快
新增
为什么 MySQL 建议自增主键?(MySQL 随机插入的代价)
主键索引是有序的 B + 树结构, 新增条目的 ID 肯定是最大的, 新增给 B + 结构带来的调整最小;
主键索引是聚簇的: 新增条目, ID 是最大的. 其 data 追加在上一次插入的后面, 磁盘更容易顺序写.
辅助索引, 插入基本是随机的:
插入条目, 可能会引起 B + 树结构很大的调整.
HBase 可以随机插入:
HBase 的所有插入只是写入内存 memstore, 只保证内存数据的有序即可 (很快, 很容易);
为防止数据丢失写入 memstore 前, 先写入 wal(可以关闭, 速度更快);
HBase 没有辅助索引需要维护;
memstore 写满了, 申请一块新的内存, 旧的 memstore 被后台线程刷盘, 存入 HFile.
修改
MySQL 数据变化引起存储变动:
数据块大小变化: 磁盘空间不足, 可能需要调整磁盘存储结构, 引起大量的磁盘随机读写;
辅助索引发生变化: 可能需要重新调整辅助索引 B + 树.
HBase 直接将变化写入到 memstore, 没有其它开销.
删除
MySQL 数据删除:
直接操作 B + 树的节点, 肯定需要刷新磁盘;
如果引起树结构变化, 甚至可能需要多次刷新磁盘.
HBase 只是在 memstore 记录删除标记, 没有其它开销.
3, 结论
HBase 写入内存 + 后台刷盘 (最多是 WAL, 磁盘顺序写);MySQL 需要维护 B + 树, 大量的磁盘随机读写.
MySQL 要求尽量追加写 (自增 ID), 速度较慢; HBase 可以随机插入, 速度很快.
MySQL 读得快
MySQL 数据是本地存储的, HBase 是基于 HDFS, 有可能数据不在本地.
B+ 树天然的全局有序
根据主键查询, 可以快速定位到数据所在磁盘块, 只需要极少的磁盘 IO 即可拿到数据: 通过缓存高层节点, 主健查询只需要一次磁盘 IO 就可拿到数据; MySQL 单表行数一般建议不会超过 2 千万, 千万行以下的大表, B + 树只需 2~3 层即可;
辅助索引, 提供快速定位能力: 辅助索引 B + 树, 可以快速定位到最终所需的主键 ID, 根据主键 ID 可以快速拿到所需信息.
HBase 只有局部信息, 没有辅助索引
查询会优先查找 memstore, 如果没有会查找 Hfile(存储结构类似 B + 树). 如果第一个 Hfile 中没有所需的信息, 则需要去第二, 第三个 Hfile 中查询; 如果查询的数据恰好在 memstore, 第一个 Hfile,HBase 会优于 MySQL; 平均下来, HBase 读性能一般. 减少 Hfile 数据以提速, 小的 HFile 合并成大的 HFile 文件. 这种存储结构叫 LSM 树 (Log-structured merge-tree);
如果需要检索特定的列, 可能需要遍历所有 Hfile, 成本巨高.
MySQL 成也 B+, 败也 B+;HBase 成也 LSM, 败也 LSM.
4, 附录
B+ 树
查询 "值为 25" 的节点, 只需要 2 次定位即可.
LSM 树
查询 "值为 25" 的节点, 只需要 4 次定位即可.
三, 优化思路
1,HBase 优化点 (主要是读)
异步化
后台线程将 memstore 写入 Hfile;
后台线程完成 Hfile 合并;
wal 异步写入 (数据有丢失的风险).
数据就近
blockcache, 缓存常用数据块: 读请求先到 memstore 中查数据, 查不到就到 blockcache 中查, 再查不到就会到磁盘上读, 把最近读的信息放入 blockcache, 基于 LRU 淘汰, 可以减少磁盘读写, 提高性能;
本地化, 如果 Region Server 恰好是 HDFS 的 data node,Hfile 会将其中一个副本放在本地;
就近原则, 如果数据没在本地, Region Server 会取最近的 data node 中数据.
快速检索
基于 bloomfilter 过滤:
正常检索, RegionServer 会遍历所有 Hfile 查询所需数据. 其中, 需要遍历 Hfile 的索引块才能判断 Hfile 中是否有所需数据;
BloomFiler 存储 HFile 的摘要, 可以通过极少磁盘 IO, 快速判断当前 HFile 是否有所需数据:
行缓存: 快速判断 Hflie 是否有所需要的行, 粒度较粗, 存储占用少, 磁盘 IO 少, 数据较快;
列缓存: 快速判断 Hfile 是否有所需的列, 粒度较细, 但存储占用较多.
基于 timestamp 过滤:
HFile 基于日志追加, 合并, 维护了版本信息;
当查询 1 小时内提交的信息时, 可以跳过只包含 1 小时前数据的文件.
HFile 存储结构:
HFile 存储格式
参考链接:
https://link.zhihu.com/?target=https://blog.csdn.net/yangbutao/article/details/8394149
Trailer 存储整个 Hfile 的定位信息;
DataIndex 存储 Data 块的索引信息: Data 存储为一组磁盘块, 存储数据信息; DataIndex 功能类似于 B + 树的非叶子节点; Data 每个磁盘块中的数据按 key 有序, 加载到内存后可以用二分查找定位; Key 按行 + 列族 + 列 + 时间戳生成, 按字典序排序 (最佳查询方式: 最左匹配);
MetaIndex 存储 Meta 的索引信息, Meta 存储一系列元信息; MetaIndex 功能类似于 B + 树的非叶子节点; Meta 存储 bloomfiler 等辅助信息.
2,MySQL 优化点 (主要是写)
查询缓存
将 SQL 执行结果放入缓存.
缓存 B + 高层节点
一千万行的大表, 一般只需要一棵 3 层的 B + 树, 其中索引节点 (非叶子节点) 的大小约 20MB. 完全可以考虑将大部叶子节点缓存, 基于主键查询只需要一次 IO.
减少随机写 -- 缓冲: 延迟写 / 批量写
上节提到, B + 树通过自增主键大量减少随机插入. 由于辅助索引的存在, 插入, 修改, 删除操作, 辅助索引可能引起大量的随机 IO.
插入缓冲: 只是将被插入数据写入 insert buffer; 定期将其 merge 到 B + 树;
修改缓冲: 类似于 insert buffer 的思路.
减少随机读 --MRR
- SELECT * FROM t WHERE key_part1 >= 1000 AND key_part1 < 2000 AND key_part2 = 10000;
- # 普通操作分解:
- key_part1= 1000, key_part2=1000, id = 1
- select * from t where id=1
- key_part1= 1001, key_part2=1000, id = 10
- select * from t where id=10
- ...
- # MRR 操作分解:
- SELECT * FROM t WHERE key_part1 >= 1000 AND key_part1 < 2000 AND key_part2 = 10000;
- key_part1= 1000, key_part2=1000, id = 1
- buffer.append(1)
- key_part1= 1000, key_part2=1000, id = 10
- buffer.append(10)
- ...
- sort(buffer)
- select * from t where id in (buffer)
索引下推
MySQL 的 server 处理完索引后, 会将索引其它部分传给引擎层;
引擎层根据过滤条件过滤掉无用的行, 减少数据量, 进而优化 server 的性能.
3, 集群化数据库的辅助索引
InnoDB 的辅助索引
B + 树全局有序, 叶子节点存储的是主键. 基于辅助索引定位主键, 再用主键定位数据. MySQL 水平切分后, 没办法跨库维持建立全局有序索引:
单实例维护索引, 丧失了全局有序性;
再做一个基于新索引分库方案, 丧失了辅助索引维护的事务性.
HBase 相同问题
仿照 InnoDB 实现辅助索引, 辅助索引可以做成单独的 key, 其 value 是被索引行的 key;
可以做到全局信息的维护, 但没法保证事务性.
4,HBase 异步合并带来的好处
TTL: 基于后台合并, TTL 很容易做;
数据多版本支持: 基于 "追加",HBase 天然的可以支持多版本;
版本数量: 基于后台合并, 可以将太旧版本干掉.
四, 总结
不知道 BigTable 的前辈们是出于什么思路, 本人冒昧揣测一下, 多少应该是受到 SQL 数据库的影响. 个人感觉, 这些或许就是一脉相承的演进, 至少用这种思路学习不显突兀. HBase 不是凭空而来, 也绝对不是解决所有问题的万能灵丹.
最直接的存储思路肯定是 "文件", 当 "文件" 不能满足需求, 就有了数据的组织方式, 进而演进到关系数据库如 MySQL.
MySQL 以其 "单机" 很好地解决了 ACID 问题, 但是, 性能再好的 "单机" 势必演变成 "单点" 瓶颈, 进而, 分布式思路成为必然.
最简单的是扩展读,"无限" 挂 slave; 进而拆分写节点, 多点写入: 垂直拆库, 水平拆库. 一旦选择分布式, 就涉及如何主从一致, 如何发现节点, 如何运维, ACID 的如何保证等问题.
进而就是一系列分布式方案, 而 HBase 就是其中一种解决思路 -- 只读主库保证一致, 水平拆分, zk 等机制保证自动运维, 单行级 ACID. 至于性能方面, 由于存储思路不同, MySQL 与 HBase 分别取舍了不同的读写性能. 继而, 就衍生出了如何针对性进行优化.
以这种思路, HBase 不是凭空出现. 以个人浅显的目光所及, 没有完美的架构, 也没有绝对厉害的设计. 固然 SQL 类数据库有其独领风骚的场景, NoSQL 数据库自然也有纵横驰骋的疆域, 无论是哪种架构, 都有自己鞭长莫及的角落.
所以, 应该说任何一种方案都没有完美, 只有合适. 而所有的合适都是演变而来, 万变不离其宗: 更好的解决问题.
来源: http://stor.51cto.com/art/201806/576466.htm