同样是笔记摘录自---极客时间 李运华 《从0开始学架构》。这部分本人接触不多,也不太懂,只是大概了解了个概念。
一、高性能数据库集群
1、读写分离 (适用于普通的数据查询远多于数据变动的情况)
1.1 读写分离的基本实现是:
- 数据库服务器搭建主从集群,一主一从、一主多从都可以。
- 数据库主机负责读写操作,从机只负责读操作。
- 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
- 业务服务器将写操作发给数据库主机,将读操作发给数据库从机。
1.2 两个细节点将引入设计复杂度:主从复制延迟和分配机制。
1)解决主从复制延迟有几种常见的方法:
- 读从机失败后再读一次主机。
- 写操作后的读操作指定发给数据库主服务器。
- 关键业务读写操作全部指向主机,非关键业务采用读写分离。
将读写操作区分开来,然后访问不同的数据库服务器,一般有两种方式:程序代码封装和中间件封装。
2)程序代码封装
指在代码中抽象一个数据访问层(所以有的文章也称这种方式为“中间层封装”),实现读写操作分离和数据库服务器连接的管理。例如,基于 Hibernate 进行简单封装,就可以实现读写分离。
3)中间件封装
指的是独立一套系统出来,实现读写操作分离和数据库服务器连接的管理。事实上在业务服务器看来,中间件就是一个数据库服务器。由于数据库中间件的复杂度要比程序代码封装高出一个数量级,一般情况下建议采用程序语言封装的方式,或者使用成熟的开源数据库中间件。
2、分库分表(单个数据库服务器存储的数据量大(上亿条),一般初期都用不到)
常见的分散存储的方法“分库分表”,其中包括“分库”和“分表”两大类。
2.1 业务分库指的是按照业务模块将数据分散到不同的数据库服务器。
业务分库能够分散存储和访问压力,但同时也带来了新的问题,业务分库后,表之间的 join 查询、数据库事务无法简单实现了。1)业务分库后,原本在同一个数据库中的表分散到不同数据库中,导致无法使用 SQL 的 join 查询。
2)原本在同一个数据库中不同的表可以在同一个事务中修改,业务分库后,表分散到不同的数据库中,无法通过事务统一修改。虽然数据库厂商提供了一些分布式事务的解决方案(例如,MySQL 的 XA),但性能实在太低,与高性能存储的目标是相违背的。
2.2 更进一步,规模进一步增大,单个表的存储都太大,需要分表。(这个估计只有几家头部互联网公司一级的业务会触及)
单表数据拆分有两种方式:垂直分表和水平分表。
1. 垂直分表
垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。垂直分表引入的复杂性主要体现在表操作的数量要增加。
2.水平分表
水平分表适合表行数特别大的表,相比垂直分表,会引入更多的复杂性:
- 路由 水平分表后,某条数据具体属于哪个切分后的子表,需要增加路由算法进行计算,这个算法会引入一定的复杂性。
- join 操作 需要重复多次
- count() 操作 分别操作
- order by 操作 需要汇总排序
和数据库读写分离类似,分库分表具体的实现方式也是“程序代码封装”和“中间件封装”,但实现会更复杂。读写分离实现时只要识别 SQL 操作是读操作还是写操作,通过简单的判断 SELECT、UPDATE、INSERT、DELETE 几个关键字就可以做到,而分库分表的实现除了要判断操作类型外,还要判断 SQL 中具体需要操作的表、操作函数(例如 count 函数)、order by、group by 操作等,然后再根据不同的操作进行不同的处理。例如 order by 操作,需要先从多个库查询到各个库的数据,然后再重新 order by 才能得到最终的结果。
二、高性能NOSQL
1、常见的 NoSQL 方案
- K-V 存储:解决关系数据库无法存储数据结构的问题,以 Redis 为代表。
- 文档数据库:解决关系数据库强 schema 约束的问题,以 MongoDB 为代表。
- 列式数据库:解决关系数据库大数据场景下的 I/O 问题,以 HBase 为代表。
- 全文搜索引擎:解决关系数据库的全文搜索性能问题,以 Elasticsearch 为代表。
2、K-V 存储
优势: 解决关系数据库无法存储数据结构的问题
劣势:Redis 的缺点主要体现在并不支持完整的 ACID 事务,Redis 虽然提供事务功能,但 Redis 的事务和关系数据库的事务不可同日而语,Redis 的事务只能保证隔离性和一致性(I 和 C),无法保证原子性和持久性(A 和 D)
3、文档数据库
目前绝大部分文档数据库存储的数据格式是 JSON(或者 BSON)。
优势:
1. 新增字段简单 业务上增加新的字段,无须再像关系数据库一样要先执行 DDL 语句修改表结构,程序代码直接读写即可。
2. 历史数据不会出错 对于历史数据,即使没有新增的字段,也不会导致错误,只会返回空值,此时代码进行兼容处理即可。
3. 可以很容易存储复杂数据。
劣势:1、不支持事务。2、无法实现关系数据库的 join 操作。
4、列式数据库
优势: 特定场景下,a)节省 I/O;b)具备更高的存储压缩比,能够节省更多的存储空间。
劣势:频繁地更新多个列情况下,性能会很低。
适用场景:一般将列式存储应用在离线的大数据分析和统计场景中,因为这种场景主要是针对部分列单列进行操作,且数据写入后就无须再更新删除。原理就是大数据中的稀疏矩阵,和数据的低价值密度。
5、全文搜索引擎
全文搜索引擎能够基于 JSON 文档建立全文索引,然后快速进行全文搜索
三、高性能缓存架构
-
缓存就是为了弥补存储系统在这些复杂业务场景下的不足,其基本原理是将可能重复使用的数据放到内存中,一次生成、多次使用,避免每次使用都去访问存储系统。缓存能够带来性能的大幅提升。
-
缓存的架构设计要点:
-
缓存穿透
缓存穿透是指缓存没有发挥作用,业务系统虽然去缓存查询数据,但缓存中没有数据,业务系统需要再次去存储系统查询数据。通常情况下有两种情况:- 存储数据不存在
如果查询存储系统的数据没有找到,则直接设置一个默认值(可以是空值,也可以是具体的值)存到缓存中,这样第二次读取缓存时就会获取到默认值,而不会继续访问存储系统。 - 缓存数据生成耗费大量时间或者资源
存储系统中存在数据,但生成缓存数据需要耗费较长时间或者耗费大量资源。如果刚好在业务访问的时候缓存失效了,那么也会出现缓存没有发挥作用,访问压力全部集中在存储系统上的情况。
- 存储数据不存在
-
缓存雪崩
缓存雪崩是指当缓存失效(过期)后引起系统性能急剧下降的情况。缓存雪崩的常见解决方法有两种:更新锁机制和后台更新机制。- 更新锁机制 对缓存更新操作进行加锁保护,保证只有一个线程能够进行缓存更新,未能获取更新锁的线程要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。分布式集群的业务系统要实现更新锁机制,需要用到分布式锁,如 ZooKeeper。
- 后台更新机制 由后台线程来更新缓存,而不是由业务线程来更新缓存,缓存本身的有效期设置为永久,后台线程定时更新缓存。
-
缓存热点
- 缓存热点的解决方案就是复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力。
- 不同的缓存副本不要设置统一的过期时间,否则就会出现所有缓存副本同时生成同时失效的情况,从而引发缓存雪崩效应。
-
-
由于缓存的各种访问策略和存储的访问策略是相关的,因此上面的各种缓存设计方案通常情况下都是集成在存储访问方案中,可以采用“程序代码实现”的中间层方式,也可以采用独立的中间件来实现。
理解与思考
-
缓存很有用。软件系统中随处可见各种缓存。硬件,操作系统,数据库,web系统中都能看到缓存的应用。
-
制定缓存和失效的策略,是个技术活,也是用好缓存系统的关键。稍有不慎就自废武功。
四、单服务器高性能模式
1、PPC 每次有新连接就新开一个进程。
缺点:
- fork 代价高 (提前创建进程池)
- 父子进程通信复杂
- 支持的并发连接数量有限
2、TPC 每次有新连接就新开一个线程。
与PPC相比,线程更轻量级,创建线程的消耗比进程要少得多;同时多线程是共享进程内存空间的,线程通信相比进程通信更简单
缺点:
- 创建线程虽然比创建进程代价低,但并不是没有代价,高并发时(例如每秒上万连接)还是有性能问题。
- 无须进程间通信,但是线程间的互斥和共享又引入了复杂度,可能一不小心就导致了死锁问题。
- 多线程会出现互相影响的情况,某个线程出现异常时,可能导致整个进程退出(例如内存越界)。
3、Reactor:I/O 多路复用结合线程池,完美地解决了 PPC 和 TPC 的问题,中文是“反应堆”。
Reactor 模式的核心组成部分包括 Reactor 和处理资源池(进程池或线程池),其中 Reactor 负责监听和分配事件,处理资源池负责处理事件。初看 Reactor 的实现是比较简单的,但实际上结合不同的业务场景,Reactor 模式的具体实现方案灵活多变,主要体现在:
Reactor 的数量可以变化:可以是一个 Reactor,也可以是多个 Reactor。
资源池的数量可以变化:以进程为例,可以是单个进程,也可以是多个进程(线程类似)。
最终 Reactor 模式有这三种典型的实现方案:
单 Reactor 单进程 / 线程
单 Reactor 多线程
多 Reactor 多进程 / 线程
以上方案具体选择进程还是线程,更多地是和编程语言及平台相关。例如,Java 语言一般使用线程(例如,Netty),C 语言使用进程和线程都可以。例如,Nginx 使用进程,Memcache 使用线程。
4、Proactor
“前摄器”,与其类似的单词是 proactive,含义为“主动的”。Reactor 可以理解为“来了事件我通知你,你来处理”,而 Proactor 可以理解为“来了事件我来处理,处理完了我通知你”。这里的“我”就是操作系统内核,“事件”就是有新连接、有数据可读、有数据可写的这些 I/O 事件,“你”就是我们的程序代码。
五、高性能负载均衡
高性能集群的复杂性主要体现在需要增加一个任务分配器,以及为任务选择一个合适的任务分配算法。对于任务分配器,现在更流行的通用叫法是“负载均衡器”。
1、负载均衡分类
- DNS 负载均衡 一般用来实现地理级别的均衡。
- 硬件负载均衡 性能最好,价格最高。
- 软件负载均衡 通过负载均衡软件来实现负载均衡功能,常见的有 Nginx 和 LVS,其中 Nginx 是软件的 7 层负载均衡,LVS 是 Linux 内核的 4 层负载均衡。4 层和 7 层的区别就在于协议和灵活性,Nginx 支持 HTTP、E-mail 协议;而 LVS 是 4 层负载均衡,和协议无关,几乎所有应用都可以做,例如,聊天、数据库等。
2、算法
负载均衡算法数量较多,而且可以根据一些业务特性进行定制开发,抛开细节上的差异,根据算法期望达到的目的,大体上可以分为下面几类。
任务平分类:负载均衡系统将收到的任务平均分配给服务器进行处理,这里的“平均”可以是绝对数量的平均,也可以是比例或者权重上的平均。
负载均衡类:负载均衡系统根据服务器的负载来进行分配,这里的负载并不一定是通常意义上我们说的“CPU 负载”,而是系统当前的压力,可以用 CPU 负载来衡量,也可以用连接数、I/O 使用率、网卡吞吐量等来衡量系统的压力。
性能最优类:负载均衡系统根据服务器的响应时间来进行任务分配,优先将新任务分配给响应最快的服务器。
Hash 类:负载均衡系统根据任务中的某些关键信息进行 Hash 运算,将相同 Hash 值的请求分配到同一台服务器上。常见的有源地址 Hash、目标地址 Hash、session id hash、用户 ID Hash 等。