本篇第一部分为我阅读《从零开始学架构》的读书笔记。如觉得有帮助,请购买正版图书学习。
第二部分为平时看到的有关架构的一些资料整理。
高性能存储
关系型数据库
数据库范式
- 每个属性都不可再分。即每个列不可再分。1NF 是所有关系型数据库的最基本要求。
- 表中的每列都和主键相关。一个表中只能保存一种数据,不可以把多种数据保存在同一张数据库表中。
- 每一列数据都和主键直接相关,而不能间接相关。
读写分离
- 主从模式(一主多从,多主多从),数据写如主库,从库复制主库数据,分担读压力。
- 问题
- 延迟:主从复制需要时间,可能造成延迟。解决方法:
- 写操作后的读,发送给主库
- 从库读取不到时,读取主库(二次读取)
- 关键业务(延迟要求低业务)全部从主库读取,非关键业务从库读取
- 数据还是单机存储,当数据越来越多时,读写性能下降,备份话的时间更长,极端情况下丢失风险越大
- MySQL 是通过 binglog(保持在磁盘上的二进制日志)实现的,主库异步的将 binglog 传给从库,从库收到后解析成 SQL 在自己执行
业务分库
- 按照业务模块将数据分散到不同的数据库
- 建议:如果没有性能上的瓶颈就暂时不做分库;如果做一次做到位
- 问题:
- join:原本可以通过 join 得到的结果,需要更复杂的实现(先查询 id,再根据 id 查询内容)
- 事务:不同数据库中的表,可能在同一个操作中被修改(MySQL 分布式事务解决方案:XA)
- 成本:需要更多机器支持分库,因此是否分库需要正确评估
分表
- 垂直分表:将原本一张表的内容写入多张表中,加快单表读写性能,读取完整数据可能更复杂
- 水平分表:将一张表(千万级别)的数据存储于多张表中,问题:
- 路由:水平分表后,需要确定一条数据属于哪个子表,需增加路由算法。常见的路由有:范围,hash,配置
- join:需要在业务代码或数据库中间件中进行多次 join 查询,然后合并结果
- count:多次 count,或使用计数表(可能造成数据不一致,增加写压力)
- group by:需要在业务代码或数据库中间件中进行排序
- 无分区键字段查询:维护额外的分区键与查询字段的关系表
唯一主键生成
- Snowflake:
- 特点:调递增,包含机器 ID、序列号、时间戳。
- 使用:可以嵌入代码中,也可以部署单独的 ID 服务器。
- 缺点:依赖系统的时间戳,如果时间不准会产生重复 ID;并发不高时,可能造成 ID 分布不均,可以用秒做结束,或者随机一个开始序号
NoSQL
K-V 存储(Redis 为例)
支持string, hash, list, set, zset, map
缺点:不完全支持 ACID
- 原子性:事务支持不完善
- 一致性:事务支持不完善
- 独立性:单线程,事务过大会阻塞
- 持久性:RDB 和 AOF
一致性 HASH 算法
Redis 集群部署时考虑容错性和可用性,采用一致性 Hash 算法计算 key 的存储节点。一致性 Hash 算法将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数 H 的值空间为 0-2^32-1。使用一致性 Hash 算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题。为了解决这种数据倾斜问题,一致性 Hash 算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。
文档数据库
为了解决 schema 带来的问题,最大的特点是灵活无 schema,大部分使用 json 存储数据。适合与电商游戏等场景。可作为关系型数据库的补充。
优势:
- 新增字段简单
- 历史数据不会出错
- 可以很容易存储复杂数据
缺点:
列式数据库
一般将列式存储应用在离线的大数据分析和统计场景中,因为这种场景通常在几个列中,且不需更新删除数据。
优势:
- 在只需要几个值的情况下,可直接读出,节省 IO
- 压缩比例高
缺点:
- 当同时需要多个列时,效率较低。因为要更新需要将存储的数据解压后更新,再压缩,最后写入磁盘
全文搜索引擎
传统关系型数据库可能无法满足全文搜索的需求场景,因为全文搜索的条件通常是任意组合的,如果建索引需要建很多;like 是整表扫描,效率非常底
全文搜索通常使用倒排索引实现,原理是建立单词到文档的索引。
缓存
问题:
- 缓存穿透:指缓存没有发生作用,虽然去查询了缓存,但缓存中没有数据,再次访问存储系统
- 原因:1)数据不存在;2)生产缓存需要大量的运算
- 解决:解决 1)没有查询到的值设置默认值;解决 2)监控
- 缓存雪崩:当缓存失效后,引起的系统大面积的性能下降
- 解决:更新锁,对缓存更新进行加锁保护;后台更新,由特定的后台线程完成缓存定时更新操作,此时也可能由于缓存占满导致缓存失效,此时可使用定时读取或消息队列通知的形式通知后台线程更新缓存
- 缓存热点:当大多数查询同时命中缓存时可能造成缓存服务器压力较大
CDN 缓存静态资源
CDN 就是将静态的资源分发到位于多个地理位置机房中的服务器上,因此它能很好地解决数据就近访问的问题,也就加快了静态资源的访问速度。
以 www.baidu.com 为例给你简单的域名解析过程:
- 域名解析请求先会检查本机的 hosts 文件,查看是否有 www.baidu.com 对应的 IP;
- 如果没有的话,就请求 Local DNS 是否有域名解析结果的缓存,如果有就返回标识是从非权威 DNS 返回的结果;
- 如果没有就开始 DNS 的迭代查询。先请求根 DNS,根 DNS 返回顶级 DNS(.com)的地址;再请求.com 顶级 DNS 得到 baidu.com 的域名服务器地址;再从 baidu.com 的域名服务器中查询到 www.baidu.com 对应的 IP 地址,返回这个 IP 地址的同时标记这个结果是来自于权威 DNS 的结果,同时写入 Local DNS 的解析结果缓存,这样下一次的解析同一个域名就不需要做 DNS 的迭代查询了。
消息队列
作用
- 削峰填谷,将峰值流量缓存在消息队列中,如秒杀场景
- 异步处理,处理非主要流程,如消费后发放优惠卷
- 解耦,不直接依赖接口,依赖消息队列降低系统间的耦合型
消息丢失
- 生产者发送消息时网络错误,以为发送给了 MQ,实际 MQ 没有接收到。这种情况设置正确的消息重传次数和失败处理,另外也可以设置消息同步到多个副本才算成功。
- 设置副本数大于 1
- 设置活跃 follower 大于 1
- 设置 leader 需要把消息同步到最小活跃同步 follower 才算写入成功
- 重试次数和处理设置
- MQ 仅接收到消息,还未来得及写入磁盘保存;只写入到 leader 没有同步到 follower 时,leader 挂了
- 设置副本数大于 1
- 设置活跃 follower 大于 1
- 设置 leader 需要把消息同步到最小活跃同步 follower 才算写入成功
- 重试次数和处理设置
- 消费者已经提交了确认,但消费途中没有处理完就 down 了 –> 关闭自动提交确认,手动保证处理完成后提交
消息重复消费
避免重复消费,需要保证消息的生产、消费过程的幂等性:
- 生产幂等性有多种 MQ 都支持,MQ 做法是给每一个生产者一个唯一的 ID,并且为生产的每一条消息赋予一个唯一 ID,消息队列的服务端会存储 < 生产者 ID,最后一条消息 ID> 的映射。当某一个生产者产生新的消息时,消息队列服务端会比对消息 ID 是否与存储的最后一条 ID 一致,如果一致就认为是重复的消息,服务端会自动丢弃
- 消费者通过鉴定唯一 ID 是否已经消费过保证。可以用数据库保证,需要整个处理过程在同一个事务中。也可以用乐观锁保证,比如给数据添加版本号。
消息延迟
- 监控:
- 使用消息队列提供的工具,通过监控消息的堆积来完成,如
kafka-consumer-groups.sh
、JMX
- 生成监控消息的方式来监控消息的延迟情况,具体是生成一个时间戳消息,消费者消费到此消息时判断时间是否大于阈值,发送报警
- 减少的方法:
- 优化消费性能
- 增加消费者数量(Kafka 消费者的数量和分区数一样,需要同时增加分区数和消费者数)
Kafka 设计优点
- Kafka 使用硬盘存储,但做了很多优化的设计使存储性能很高
- 顺序存储,所有的消息在文件后面追加
- 通过维护一个 offset 来实现顺序访问
- IO 采用 0 拷贝技术,减少了从内核空间读取到用户缓存,再从用户缓存输出到网络流的时间。直接从页缓存写入网络流
- 网络带宽上的设计考虑,会对消息做压缩,减少带宽消耗。也可以设置消息批量发送,减少网络请求次数
- 分布式存储设计,有备份和主分区,保证消息不丢失。用户通过 offset 也能从宕机事故中快速恢复