NoSQL HBase
Hbase架构
Hmaster: 元数据,schema管理。 region分配,负载平衡,但是不介入直接的数据访问
Region server: 读写请求处理,Region分裂管理。
介绍
我们以一个实际的业务例子来粗浅地看看在面对不同数据规模的互联网业务时,数据体系的一个演变过程。
Hush是一家提供短链接服务的互联网公司。它把客户的长HTTP链接转换成一个短HTTP链接,方便客户的链接在在线,纸质媒体上进行传播。
短短时间内,这家公司获得了初步的成功,有数以千计的用户开始使用它们的服务。这个时候,这家公司的技术架构基本是以LAMP(Linux, Apache, Mysql, Php) 来组建的。 在数据体系上,它使用了下面的ER设计
主要有用户表,记录使用服务的用户。 URL,客户自己的原始URL。SHORTURL,针对URL提供的短链接服务。 CLICK,点击统计。 设计原则上
- 3范式设计
- 使用外键保证引用一致性
- 使用索引保证检索性能
- 如果有必要,业务逻辑可以部分进入存储过程
事务特性上
- 数据更新上支持事务
- RDBMS支持强一致性 (ACID)
- 引用一致性
这个阶段基本上初创公司,微小公司的常见技术架构。在小量用户下,可能是最适合的架构体系。
很快,用户增长到了上万级别。网站的压力出现了。
首先,应用服务器 (这里是Apache) 增加了。前端使用负载均衡技术分摊访问到各个应用服务器上。但是数据库的压力如何解决?你想到了读写分离,主备结构。
- 增加备用数据库。备用数据库承担所有的读请求。
- 主数据库承担所有的写请求 (在Hush这家公司,很幸运,写请求是比较少的。。)
- 通过数据复制,新数据从主库复制到(可能多个)备库。
幸福的烦恼:业务在继续增长,用户已经迈过了十万的门槛。读请求再次成为了瓶颈。备库也无法承担读请求了。是时候引入缓存功能了。。
- 比如Memcached 或者Redis,都是广泛使用的缓存技术
- 读请求压力转移到更适合随机访问的内存系统中去
但是,这是有代价的
- 你不再有严格的一致性保证了。读到的数据可能是旧数据,或者已经被删除掉的数据
- 你必须非常谨慎地选择缓存失效策略,来保证数据库和缓存中的数据一致性。
渐渐的,渐渐的,单个主服务器已经比较难于处理所有的写请求。不过,还可以苟且一下。。
- 加内存,换服务器,换昂贵的数据库专用服务器
- 表连接成为了查询性能的障碍,开始对部分表反范式化,数据冗余。
- 停止使用存储过程~~ 数据库的CPU很昂贵,让它干它必须要干的话!
- 删掉一些辅助查询的索引吧,提升写性能最关键了
- 统计功能改为批次作业,或者移到一个专门的日志处理系统,不要占用业务库性能 (数仓诞生了)
- 引入一些物化视图吧,JOIN没法跑出结果了
- MySQL不行了,要不换成Oracle吧。。
最后,最后,这些都不起作用了。你还有一个大招没放: 分库分表 (Sharding)
- 把数据分布到多个服务器上去
- 简单来说把表水平切开搬到不同的服务器上
- 切分的边界是相对固定的(如用户id区间)
- 如果加入新的服务器,可能需要重新切分,把数据在不同服务器之间重新分摊。
- 这个可一点都不好玩~! 搬迁期间会占用巨量的I/O资源。
那么,更好的解决方案在哪里?
经过了这么长的铺垫后,主角开始登场了,那就是NoSQL 数据库。
- 它们一般都不支持SQL语言
- 数据模型不再以单纯是表结构
- 更大的区别是数据一致性。事务特性要么不支持,要么有限支持。
- 通常来说,可能只能支持有限的场景,不是通用性方案。
这里先插入一段,概述下一致性模型和CAP。
一致性模型是指数据库系统对于数据写入,更新,读取等数据操作时,系统对客户端提供怎样的承诺。
常见的说法有:
强一致性(Strict):数据的任何改变都是原子的。一旦数据改变提交,任何客户端都会看到最新的变化。
弱一致性 (Weak):数据的改变提交后,不保证客户端读到的是最新的值。一般而言,经过一段时间后(不一致性窗口),客户端会读到最新的值。
最终一致性(Eventual):数据改变后,如果没有其他的改变发生,在经过一段时间后,各个客户端最终会读到一致的值。这是弱一致性的特例,强调一致性状态最终会达到。
还有一些变体,如
因果一致性 (Casual): 如果写入数据的客户端A 通知了 客户端B, 那么客户端B可以保证读到最新数据。而没有通知到的客户端C,则可能读不到最新数据。
序列一致性(Sequential):可以分为单调读一致性 (Monotonic read),即如果客户端A已经读到了某个版本的数据,那么系统保证客户端A在此之后不会读到更老版本的数据。还有单调写一致性(Monotonic write),系统保证来自于同一个客户端A的所有写请求是保序执行的。
会话级别一致性(Session):同一个会话内部数据的改变是强一致的。
一致性理论中常见的两个缩写: ACID(酸) 和BASE (碱)
ACID是指数据库事务具有的四个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。ACID中的一致性要求比较强,事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。而BASE对一致性要求较弱,它的三个特征分别是:基本可用(Basically Available), 软状态/柔性事务(Soft-state,即状态可以有一段时间的不同步), 最终一致性(Eventual consistency)
还有大名鼎鼎的CAP。
CAP理论断言,任何基于网络的数据共享系统,最多只能满足数据一致性(Consistency,C)、可用性(Availability ,A)、网络分区(Partition,P)容忍性这三要素中的两个要素。
对于一个分布式数据库而言,P是必须要保障的。因此,大多数情况下,能折衷考量的,都是C和A了。要么让数据库体系的一致性更强,要么让数据库体现的可用性更强。选择了ACID,那么可用性一般较低。选择了BASE,则是对可用性和一致性的平衡和妥协。
NoSQL数据库是多种多样的,如前所述,它们很多是紧贴场景的,而非通用性方案。一般而言,考察一个NoSQL数据库,我们看这么几点:
- 数据模型
- 数据怎么存? Key Value? 半结构化(JSON)?列式?
- 如何访问? SQL? 类SQL?接口?
- Schema是否可变?变化代价大吗?
- 存储模型
- 内存还是持久化?
- 依赖的文件系统?
- 持久化的日志格式和数据文件格式?内存中数据格式?
- 什么样的访问是最高效的?什么样的访问是低效的?
- 一致性模型
- 严格一致性?最终一致性?支持事务?事务粒度?
- 一致性模型对读写速度的影响?对内存中数据格式,数据文件格式的影响?
- 物理模型
- 分布式还是单机?
- 系统是水平扩展还是向上扩展?是否存在单点?
- 读写性能
- 读写性能如何?单机抗多少的QPS? 多少TPS?
- 系统设计是读性能佳,还是写性能更加?
- 辅助索引
- 是否支持辅助索引?你的工作负荷是否需要二级索引?
- 是否存在其他的仿真,替代的辅助索引方案?
- 硬件故障处理
- 系统的组件是如何处理硬件故障的
- 故障发生后系统是否可用
- 系统是否支持热升级
- 压缩
- 压缩算法是否可配置
- 压缩比如何
- 负载平衡
- 系统是否可以自动支持负载均衡
- 原子性的 read-modify-write (即处理写入依赖于读取之前的状态。整个读,修改,写入是原子性操作
- 在单机系统容易实现,分布式系统中不易实现
- 可以防止多线程,无共享设计中的竞争冒险 (即多个客户端竞争对某个变量写入值,因而引起最终值的随机性)
- 可以减少客户端系统的复杂度。
- 锁,等待,死锁。
- 对多客服端并行访问数据的支持
- 是否有锁机制
- 是否是无锁,无等待设计?因此无死锁?
分布式数据库的Schema 设计,一般遵守DDI的原则
- 反范式 (Denormalization)
- 冗余数据,减少数据汇总,表关联的性能问题
- 冗余 (duplication)
- 业务主键 (有业务含义的主键而非代理主键 Intelligent Key)
现在我们来看一下对于本文开始的表结构在Hbase上应该怎么设计才合理
ShortUrl: 存储短链接以及它们的访问统计。注意对于统计信息,时间戳和维度信息合起来做了字段名,然后每个列簇定义了不同的ttl。另外,对于同一个Shortid的统计记录,由于字段名是按时间命名的,所以它们的存储也是临近的,适合做范围扫描。
Url 表: 存储下载的页面,以及提取出的页面信息。 注意,首先列簇定义了压缩。其次页面内容(raw) 属于不经常访问的字段,因此定义到不同的列簇里去。
User-shorturl表: 这是一个为了特定查询设计的查找表(找出指定用户的所有短链接)
User: 保存用户信息
Hbase的一些概念 (和Bigtable 大部分相同,因此部分从略)、
- 空值处理:Hbase 空值不储存 (稀疏)
- Hbase的数据储存大概可以这样理解:
- 逻辑上:
(Table, RowKey, Family, Column, Timestamp) -> Value
- 物理上:
SortedMap<RowKey, List<SortedMap<Column,List<Value, Timestamp>>>>
- Hbase提供严格一致性,行级别事务
- Region
- 扩展性和负载平衡的单位
- rowkey序列相近的行保存在一起
- 可以类比RDBMS的分区概念
- 增大到足够大时系统自动分裂。分裂时从rowkey的中间分开。
- 太小时也可以自动合并
- Region server提供对Region的管理。
- Region server失败后可以快速恢复
- Regions可以移动到不同的服务器上自动进行负载均衡。
- 数据访问接口
- 不支持SQL,提供API。 API是命令式,非声明式(SQL是声明式语言)
- Scan API: 范围扫描 (可限制返回行数和选择字段,以及字段的版本)
- 读-改-写接口: 单行事务
- 计数器: 自增计数器,全局唯一,严格连续,序列化的计数器。实现上利用了单行事务。
- Coprocessors:类似于存储过程,可以把用户代码在服务器端执行。可以实现轻量级批处理,数据汇总等工作
- 数据存储
- 存储格式名为HFile,在文件末端储存数据的索引。文件打开后索引装入到内存。
- 数据访问
- 数据访问时首先对内存中的索引做一次查找,如果存在记录则对硬盘做一次扫描
- 写操作先进日志,称为WAL ( write ahead log)。然后进入内存memstore,最后从memstore进入到Hfile
- 读到的数据是合并memstore和Hfile提供数据出来
- 数据的删除:标记删除,然后垃圾回收(tombstone marker)
- 数据整理(Compaction)
- memstore到Hfile每次都是创建新文件
- 小整理把零碎的小文件定期合并成相对较大的文件 (多路合并算法)
- 大整理把一个Region的所有文件都合并成一整个大文件,同时清理掉删除了的数据,清理掉旧的数据。