当前位置:主页 > 查看内容

面向应用的反范式化建模

发布时间:2021-05-17 00:00| 位朋友查看

简介:作者 天穆 ? 一、基础 数据分布 一 扩展性 scale up scale out 分布式系统里常见的扩展性问题有两种 scale up 和 scale out。拿数据存储举例 如果一块盘存不下 换一个更大的盘 从1t到4t到8t到更大的硬盘 但是这种方式很容易触及到系统的容量上限。 因为不可……

作者 天穆



?

一、基础 数据分布


一 扩展性 scale up scale out


分布式系统里常见的扩展性问题有两种 scale up 和 scale out。拿数据存储举例 如果一块盘存不下 换一个更大的盘 从1t到4t到8t到更大的硬盘 但是这种方式很容易触及到系统的容量上限。


因为不可能把一块盘做的非常大 所以现在业界最常用的扩展性的方式是通过scale out方式 一块硬盘不够 用更多的盘 当一台机器盘的数量达到上限 用更多的机器组成集群 如果集群不够了 用更多的集群组成联邦。再往前一步可以用一个机房 用更多机房 甚至全球分布 扩展整个系统的容量。


1.png


二 基本问题 数据分布策略


做scale out的时候必然会面临一个问题 当存储数据的节点或盘变多了以后 必须要解决数据怎么在硬盘和机器上分布问题 也就是数据分布策略。


理解数据分布策略 可以从读、写两个方面开始 比如写的时候一个请求或者要写一行数据 要写到哪个机器上、写到哪个盘上 从读的角度来讲 一个请求读取数据 不可能访问整个集群的所有盘或者所有机器 这样读取这行数据太慢了 所以必须有很好的算法或者是分布策略 能够让读、写的请求能够一次到达目标。最多可以有多一条的方式 但是最终期待的是一条就能获取。


讨论具体的分布策略之前 要明白设计分布策略的目标 第一是 负载均衡 第二是 线性扩展。


负载均衡 是希望在写的时候 能够均匀的写到每一台机器上、每一块盘上 整体是均匀的 不会某些盘或者某些机器成为写热点 也不会因为某些机器写的太多 水位很高 其他机器都空着。从读的角度来讲也一样 希望读能够很均匀的分布在整个集群上 不会导致热点和倾斜。


达到负载均衡以后 还要有线性扩展 比如集群扩缩容、盘坏了要下盘、还要加新的盘上来 这个时候希望无论机器怎么扩容 盘怎么增减 整个系统仍然处于负载均衡的状态。只要保证这一点 当系统盘增加了或者机器增加的时候 整个系统仍然能够处于线性扩容的关系 机器多了能够存的数据就多了 能承载的吞吐也变多了 是整个数据分布的目标。


总结

负载均衡

写 均匀的写到集群的每一台机器上 每一块盘上 读 均匀的从集群的每台机器上、每块盘上读数据。


线性扩展

机器扩缩容 磁盘上下线 系统始终/最终处于负载均衡状态 系统容量、吞吐与系统资源成正比(线性关系)。


2.png


三 两种分布策略


目前业界有两种比较典型的分布策略 一种是顺序分布 一种是Hash分布。


顺序分布 根据用户定义组件 让数据从最小的主键开始 依次往后排 如图例所示 user_id和ts是联合的主键 先按user_id 1、2、3、4、5排序 排完之后 再按ts进行排序。顺序分布是把整个表做拆分 例如 把user_id 等于1的分到一个Region里面 user_idt等于2、3的分到一个Region里面。


3.png


Hash公布 需要有一个Hash算法 选一个分区键 经过算法得到所在机器的名字。常见的一种算法就是取模“分区 user_id % 机器数” 可以拿user_id模上这个机器数 得到所在的机器。如图例所示 假设有3台机器 “%3 0”在第一台机器上 “%3 1”在第二台机器上 以此类推 是一种基于规则的分布。


4.png


1 顺序分布 目前比较典型的产品有hbase tidb。


顺序分布的缺点

第一 是比较依赖主键的值 如果user_id分布不均匀 因为通常user_id是顺序分配的 比如有1、2、3、4、5、6、7、8、9、10 user_id更大的时候 热度会比较高 user_id小的时候 热度会比较低。会产生一种问题 越往表的尾部越热 头部的可能就会冷一点 会产生数据倾斜以及访问倾斜 需要通过额外设计或人工介入调整。第二 相同前缀的数据也可能会分开 比如上图所示的“user_id 3” 的数据 可能会被分到两个Region上 当访问等于3的所有数据时 必然会涉及到两次Region。第三 因为有强大的干预能力 需要很复杂的路由表机制。


顺序分布优点

第一 一个Region包含哪些数据 通过路由表决定 比如HBase的meta表 tidb里面是PD。Region可以灵活分布 比如让user_id 1的数据 在Region里面拆分 也可以把user_id 1、2、3合并。第二 Region在哪台机器可以人工指定 比如可以让Region 1单独分一台机器 Region 1、3共享另外一台机器 在生产上 尤其是在有数据热点场景下 有人工介入干预能力。


5.png


2 Hash分布 是基于规则的分布 选取分区键 user_id根据分布算法或者Hash算法 得到所在的机器。比较典型的代表产品有cassandra、dynamodb。跟传统关系数据库里面的分库分表非常类似 因为没有外部依赖 所以比较简单。


缺点

第一 是在做扩缩容的时候 需要对很多的数据进行搬迁 所以需要一致性hash方案

第二 是分区无法灵活调整 因为是基于规则的 当数据基于分区键算好分区之后 所在的机器就确定了 不能灵活调整。

第三 有数据倾斜问题 比如有超大分区 比如user_id 1是个超大的用户 记录非常多 会产生热点的问题 user_id 1的所有的数据强制分布在某一台机器上 数据特别多的话 这台机器很快会达到上限。


6.png


四 Hash分布 分区键的选择


如图所示 基于直觉的方式是选user_id作为分区键 为什么不能用把TS也放进去


7.png


假设把TS放进去 user_id和TS一起算Hash 势必会产生一种情况 就是user_id 3的数据 可能分布在整个集群的不同位置 做查询的时候where user_id 3 等于3的所有数据会面临查很多分区。而且 user_id 3下面的TS 没法知道有多少 是一个不可预测的值 这时涉及到跨分区的查询 这种查询会退化成全面表扫描 是不能接受的。


选择分区键要结合查询的场景 选择合适的分区键 尽量避免或者一定要避免跨分区的查询。比如where user_id 3 这种是没办法直接高效的定位查询 一定要扫全表 但是where user_id in (3, 6, 9, ...)这种 是可以拆分成多个请求逐个查询 因为是可枚举的。




二、Cassandra的数据模型 一 Partition Key Clustering key


Cassandra数据模型里ts叫聚类键 user_id叫分区键 分区键和聚类键加一起 构成表的主键 主键要求唯一性。比如下图所示的表里面 user_id和TS放到一起一定要全局唯一 如果400有两个 就是冲突的数据。


对于分区键和聚类键 可以有很多个 可以很多个Key作为分区键 也可以有很多Key作为聚类键。除了主键之外 Cassandra里面还有非主键 或者叫属性列或者叫数据列 比如location存具体数据 不参与数据排序。


8.png


二 联合主键与前缀匹配


key比较多的场景称为联合主键或 联合主键如何排序以及查询 如图例所示的场景 分区键是city 有两个聚类键 一个是last_name 一个是first_name。因为分区间键不参与排序 当我们做Hash分布的时候 分区键在整个表里面随机分布 但是在某一个特定的分区键下面 clustering key是顺序分布的。图例中是按last_name前缀排序 p排在前面 w排在后面 在last_name相同的时候 再排下一个列 potter相同的时候 Harry排在前面 James排在后面 是这种排序规则。


9.png


因为是这样排的 所以在查的时候 要从左到右依次去查 有以下几种情况


1.where city hangzhou and last_name Potter 前缀扫描

2. where city hangzhou and last_name Potter and first_name James 单行读


这两种可以很高效的完成 因为查询的扫描范围和结果集一样大 有一些场景不能很好的支持 如

3. where city hangzhou and first_name Harry 跳过了last_name列 直接查first_name 这种查询first_name不能够用于圈定扫描范围 会变成一个filter 直接对每一行数据过滤 查询的扫描范围是city hangzhou 的所有数据 为每一行数据基于first_name Harry 做过滤 假设 hangzhou 是一个很大的Partition Key 数据量很多 这个查询会非常低效。

4. where city hangzhou 当city hangzhou 的时候 就是一个跨分区键的查询 也不能被支持。

5.where city hangzhou and last_name P and first_name James first_name进入filter。

6.where city hangzhou and last_name P and first_name Ron


这两个查询从表上看 James排在前面 Ron排在后面 但是事实上last_name是范围查询 first_name字段变成filter来扫 而不是用来缩小查询范围 所以说5和6两个语句的扫描范围一样。


10.png


三 逻辑分区 一组具有相同前缀的行


一个Partition Key的值代表一个分区 但本质上来讲 并不是物理上的分区 比如一块盘、一个机器 有物理的实体跟其对应 但是分区不会有一个文件或者实体跟其对应 分区是一种逻辑概念。


在这里面把分区定义成是一组具有相同前缀的行 前缀是Partition Key 如下图所示 Partition Key等于杭州 杭州这两行数据就是一个分区 等于上海的就是另外一个分区 这种就是叫逻辑分区。在物理上没有一个有力度的实体跟它对应 所以它的数量可以无穷大 这里的city是一个字符串 可以有无穷多的数据组合 city分区键可以无穷无尽的分区。


11.png


分区键 值域可能非常大(比如long) 分区键的每一个值 都代表了一个 分区 分区 的数量可能会非常大 分区 的本质 一组具有相同前缀的行 前缀 即分区键的值 所有的分区都是 逻辑分区 线性扩展。


线性扩展 是指分区根据一致性Hash算法划分到某一个机器上 一台机器可以服务很多分区 机器数量增加之后 能够承载的分区数量也会相应的增加 能够获得线性扩展能力。除非产生了一些巨大的分区 这些分区把一些机器占满了 这种情况下线性扩展能力是受限的。




三、范式与反范式设计
一 范式化与反范式化


范式化 是传统关系型数据库要求的概念 数据库刚出现的时候 盘都比较贵 存储空间都比较贵 数据库的表设计必须要满足降低数据冗余度的原则 需要范式化的设计 减少数据冗余度。


另外需要增加数据的一致性校验 比如有很多表 一些表来存买家 一些表存卖家 一些表来存订单 通过主键和外键之间的关系进行关联 通过外建描述数据的完整性 也是范式化设计的一部分。这种通常是用于关系数据库的设计 而且能够很好的解决复杂业务的设计 通过一整套的方法论 业务模型进行抽象。


在NoSQL系统里面 强调反范式化的设计 通过增加冗余度换取更好的性能。带来的一个问题就是数据冗余 存储空间开销上升 但是现在存储越来越便宜了 成本并没有上升很多。


范式化(Normalization)

目的

? 降低数据冗余度

? 增加数据的完整性(如外键)。

? 通常用于关系型数据库的设计


反范式化(Denormalization)

增加冗余度 用空间换时间 数据在多个地方都有 存在一致性问题。


二 示例


下图所示 是一个部门和部门下的雇员之间的表设计 比如有个department表 存 depId和名字 还有一个user的表 来存每一个人和userId,要描述一个部门还有哪些人的时候 需要把这两张表关联起来。记录表的depId和userId之间的关系 当查一个部门有哪些人的时候 要先扫这个部门的人员表 得到这个部门的userId信息 比如查depId 2 得到的userId是1和2 这时转user表拿到1和2两个ID的用户名 同时拿depId 2的depName 才能获取depName是Math 一次查询 需要有三张表 这是范式化设计。


12.png


反范式化设计 就用一张表来代替。如下图所示 depName和userName直接存在一起 查询一次搞定。缺点是名字重复存在 depName内容也重复存在 数据冗余度增加。另外 当修改名字的时候 要改很多地方。


13.png


三 反范式化优缺点


优点

多个表的数据统一到一张表里 JOIN不是必须的(大部分NoSQL也不支持join) 查询更高效 采用宽表设计 从业务设计来讲业务更简洁 查询更简洁 整个业务模式会更清晰 SQL会更简单 维护性会更好 当业务出现问题的时候 调查问题的效率得到相应的提升。


缺点

冗余存储 空间开销增加。但是因为现在存储变便宜了 所以说成本没有增加。数据冗余之后 带来的一致性的问题 比如只有一张表 Math存了两次 但是假设当有很多张表的时候 都有Math字段 会面临在多张表之间处理一致性问题。


四 原则


反范式化设计的基本原则是

根据读写模式来设计表 设计主键 使用分区键来规划数据分布 一次查询需要的数据 尽可能在一个分区里 使用聚类键来保证数据在分区内的唯一性 并控制结果集中的数据的排序(ASC/DESC) 设计好主键以后 使用非主键列来记录额外信息。这个时候非主键包含了很多业务字段 比如订单存储 希望其包含订单金额、订单ID、买家名字、卖家信息、商品信息等 是一张大宽表 可以通过一次或者是少量的查询 得到需要的所有数据 避免join 提升整个系统的查询性能。反范式化设计 将原本需要通过join得到的数据 都包含进来。



四、典型场景分析


一 典型场景一 物流详情


场景描述

电商物流订单 每个订单会经历多轮中转最后达到用户手中。每一次中转会产生一个事件 比如已揽收、装车、到达xx中转站、派送中、已签收。需要记录全网所有物流订单的状态变化 为用户提供订变更记录的查询能力。订单数据量极大 可能有上百亿 体量不能影响读写性能。


场景抽象

写 记录一个订单的一次状态变更。读 读取一个订单最近N条记录 读取一个订单的全部记录。


如下图所示 表中有两列主键 orderId指订单的ID 是分区键 gmtCreated指事件产生的时间 是聚类键 非主键列detail指的是一次事件的信息 比如已揽收或到达的状态 是数据列。


14.png


1 物流详情 高表设计


高表 设计

行不断增加 一行描述一个订单的一个事件。一个订单的所有数据 由连续的一组行来描述(一个逻辑分区)。查一个订单的所有数据时 事实是查一组具有相同前缀的行 就是查一个分区的数据。


优缺点

单个分区键下的key数量可以很多 过多的数据将导致宽分区的产生 应避免 无论数据量多大 单次next()的RT可控 流式ResultSet。


高表设计可以避免很大的行产生 因为所有的变化都产生在行里面 不是产生在列里面。可以很好的解决orderId的问题 如果某一个订单数据量特别多的时候 会产生宽分区 需要避免。常规做法是增加维度 拆开分到不同的分区里面。


高表设计无论数据量多大 单次读下一行数据的时间不变 有流式ResultSet能力 一次加载一部分数据。


2 物流详情 宽表设计(不推荐)


宽表设计

用一行来描述一个订单的所有事件 每一列是一个事件 用事件的发生事件作列名 也可将所有事件encode到一个列里。


15.png


宽表设计 用一行来描述一个订单的所有事件 每一次事件通过一列来描述。


如上图所示 把时间作为列名 每一个列记录了一个订单的某一次事件。也可以把后面的列合到一起 变成一个列。


优缺点

单行读 读一个订单的所有的数据时 只做单行读 业务会更简单。无法预知列名 列数量 每一行的列都可能不一样 强依赖schema-free能力 只能读所有数据 不容易实现topN读取 超大行风险 个别行的列特别多 会影响性能。


所以在物流详情场景下 不推荐宽表设计 建议用高表设计。


二 典型场景二 时序类---监控系统


如下图所示 是CPU监控 对整个集群的多台机器做 CPU指标的监控 CPU指标有user、system、idle等不同类型 还有很多主机如host 一台机器的某一个CPU user指标有很多点位 比如这里面192.168.1.1机器在CPU type里面产生了两个点 一个是30 一个是40 这个是时间线。


16.png


这个表里面列出来的是监控系统里面需要的数据 在这个数据场景下 怎么选择分区键、聚类键以及监控数据的存储 可以有以下几种选择


分区键怎么选

metric metric host metric host type metric type host。


监控数据怎么存

一行一个点 一行存所有点 一行存有限个点 如1分钟/1小时内产生的点。


1 分区键 只使用metric


只使用metric作为分区键 意味着分区只有 CPU 如果加入网络、磁盘 每一个metric是一个分区 意味着所有被监控的对象 所有机器的所有数据 都在一个分区下面 很容易触发单分区限制。


17.png


因为有变量和不变量的问题 CPU指标本身是不变量 即使未来新增指标 通常也是低频事件。但被监控的机器是变量 会不断的增加 可能数量巨大(比如物流订单的数量)


单分区限制 所有机器的指标都聚集在一个分区里 被监控的机器可能无限增长 但单机的承受能力不会线性增长。业务侧 识别变量和不变量cpu指标本身是不变量 即使未来新增指标 通常也是低频事件 被监控的机器是变量 会不断的增加 可能数量巨大(比如物流订单的数量)。


2 分区键 metric host


metric host策略可以很好的控制了单分区的数据量 不会出现宽分区。因为除了 host以外 没有其他维度大幅度的变化量 如下图所示 type和TS都不会太大变化 TS本质上是作为host下面的一个子集存在 比如在做一个查询的时候 要查某一台机器的某一段时间范围的 CPU指标 肯定希望这台机器的数据都排在一起 用一个查询搞定 所以TS不能够放在分区键里面。


18.png


这个设计的缺点是 并发读写同一个机器的cpu指标 请求都路由给同一台机器 不利于并发。


优缺点总结

很好的控制了单分区的数据量 不会出现宽分区 单机的所有类别的cpu指标都在一个分区里 并发读写同一个机器的cpu指标 请求都路由给同一台机器 不利于并发。


3 分区键 metric host type


metric host type策略 把指标类别也加到分区键里面 可以很好的适配并发查询模式 提高整个集群的吞吐。因为 metric host type整体作为分区键 只有三个全相等的时候 才会分在一个分区里面。


19.png


另外一种方式是host和type交换位置 其对采用一致性hash的cassandra来说 没有区别。但是对于顺序分布来讲 可能会有一点区别 因为改变了key的值域范围 可能导致值变少了 这个时候会产生聚合效应 可能导致一些潜在的问题。总结


同一台机器的不同cpu类别的指标在不同的分区里 很好的支持并发访问 host和type的顺序对采用一致性hash的cassandra来说 没有区别。


4 优化 type合并至metric中


metric host type策略还有另一个优化 type合并至metric中 如下图所示 是时序数据库建模时的特点 type在时序数据库里面叫 tag 标签的意思 可以有很多标签 比如IP是一种标签 所在机房可能也是一个标签 甚至可以有业务的标签 CPU的 type也是一种标签 这种场景下 可以把标签合到 metric中。


20.png


因为type是可枚举的 只有几种 不会增加也不会减少。合并过去之后 减少了列 存储的列变少以后 可以提高性能 减少开销 仅数据量较大时 体量小的时候看不出来。


还有一种场景 储存的是进程ID 这种时候没办法合并 比如监控每一个进程的网络流量 这个时候进程ID没法合到 Metric里面 因为进程ID不可预估 而且不可枚举。


21.png


5 数据点位的存储 宽表 or 高表


数据点位的存储指的是每个时间点metric的值 依然可以有高表和宽表的设计。如下图所示 高表设计 把TS放到Clustering key里面 一行存储一个数据点。高表设计因为一行只有一列 容易扩展多值监控 对于像经纬度 一个点位有两个值 在高表设计里面很容易扩展。


22.png


宽表设计 一行存储这个某个机器的某个指标的所有点 这种宽表设计还是会存在单行上线的问题 列多了以后会有性能问题。


融合设计 一行记录有限个点 比如存1分钟采集的所有的点或者1小时的点 结合高表和宽表的设计 粒度选择合适的时候 得到最优的性能 而且能够配合整个系统内部机制 如cache bloomfilter等。


当然这些例子 在时序场景下面比较简单 能够解决简单业务的时序设计问题 对于业界的实际数据库来讲 但生产使用时有很多考量因素。


总结

高表设计 一行存储一个数据点 如上表所示 容易扩展多值监控 如经纬度 宽表设计 一行存储这个某个机器的某个指标的所有点 单行上限 融合设计 一行记录有限个点 如1分钟 1小时内采集到的所有点 粒度选择 由指标的采集频率决定 以控制单行的列数 适当的控制行数 可以配合一些内部优化机制 如cache bloomfilter等


注意 时序建模的原理很简单 但生产使用时有很多考量因素 各TSDB都有不同的侧重点。应根据业务实际需要选择合适的模型 没有银弹


三 常见误区


1 常见误区一 分页查询


常见的分页查询误区 从Mexico[MOU23] 过来的用户很容易遇到一个问题拆请求 如下图所示 做一个大表的扫描 user id 3的数据可能非常多 为了避免一次返回太多的数据 需要对请求进行拆分。


23.png


比如按TS进行分页 先扫500的 再扫下500 再扫下500 一次一次扫。在 MySQL里面这样做是合理的 因为RPC一次返回所有记录。但是在Cassandra里面没必要 因为Cassandra用的是一种流式ResultSet方式 在系统设计层面 已经考虑到了不断往下next的情况 已经做了请求拆分。比如第一次next的时候 会新加载500行的数据 等到这500行数据消化完了 再下一次next 会加载下500行数据 如此往复 直到所有结果集返回。


总结

流式ResultSet

为了避免单次RPC返回过多数据导致RT过高 CQL driver会自动对请求进行拆分 第一次next()调用会从服务端load N行数据 之后的N-1次next()只从内存消费数据 下一次next()会再加载N行数据到客户端 如此往复 直到所有结果集返回。

参见 https://docs.datastax.com/en/developer/java-driver/3.2/manual/paging/


结论 不要为了拆分大请求而进行分页。


2 常见误区二 修改主键

场景1 修改主键的schema 在MySQL里面可以 但在Cassandra里面不允许 只能重新建表。场景2 修改主键的值 本身就是错误的说法。考虑java的map的key key能修改吗 修改key的逻辑就是删除老key 写入新key。从数据库角度来讲 没有修改主建操作 只有删除、添加这两种操作 非主键可以修改。

本文转自网络,原文链接:https://developer.aliyun.com/article/784103
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文


随机推荐