0. 引言 解决高并发高可用和由此带来的数据一致性问题
解决思路:
- 利用分布式系统的特性,不断分拆,把大系统拆小,降低风险,各个击破;
- 小步快跑,快速迭代,不断重构
1. 架构师分类 1.1 架构分类
- 第一层:基础架构
一般指:云平台、操作系统、网络、存储、数据库和编译器等。现在都是云平台。
- 第二层:中间件与大数据平台
- 中间件架构:
分布式服务中间件、消息中间件、数据库中间件、缓存中间件、监控系统、工作流引擎和规则引擎等。
- 大数据架构:
例如开源的Hadoop生态体系,Hive、Spark、Storm、Flink等。
- 中间件架构:
- 第三层:业务系统架构
- 通用软件系统:
最常用的办公软件、浏览器、播放器等。
- 离线业务系统:
基于大数据的BI分析、数据挖掘、报表与可视化等。
- 大型在线业务系统:
搜索、推荐、IM、电商、游戏、广告、企业ERP或CRM。
- 通用软件系统:
4. 操作系统 4.1 缓冲IO和直接IO 缓冲I/O:C语言提供的库函数,均以f大头;
直接I/O:linux的系统API(底层也是C编写的)
应用程序内存:通常写代码用malloc/free、new/delete等分配出来的内存。
用户缓冲区:C语言的FILE结构体里面的buffer。
内核缓冲区:Linux操作系统的Page Cache。一个Page一般是4K(没说是Byte还是bit)
对于缓冲IO,一次读操作有3次数据拷贝,写操作有反向的3次数据拷贝4.2 内存映射文件与零拷贝 4.2.1 内存映射文件
读:磁盘->内核缓冲区->用户缓冲区->应用程序内存
写:应用程序内存->用户缓冲区->内核缓冲区->磁盘
对于直接IO,一次读操作有2次数据拷贝,写操作有反向的2次数据拷贝
读:磁盘->内核缓冲区->应用程序内存
写:应用程序内存->内核缓冲区->磁盘
所以,所谓的“直接IO”其中直接的意思是指没有用户级的缓冲,但操作系统本身的缓冲还是有的。
内存映射文件在直接IO又取消了一次拷贝,将 ”内核缓冲区->应用程序内存“ 这一步取消。
读:磁盘->内核缓冲区
写:内核缓冲区->磁盘
本质上,是使用一个逻辑地址将应用程序内存直接指向内核缓冲区。
4.2.2 零拷贝
原来的数据发送需要将数据搬运到socket缓冲区,但现在直接在socket缓冲区做一层映射,将内核缓冲区的数据映射到socket缓冲区。
4.3 网络IO模型 4.3.1 实现层面的网路IO模型
一般有四种:
- 同步阻塞IO
- 同步非阻塞IO
- IO多路复用
在linux中,有三种IO复用方法:select、epoll、poll,epoll效率最高。
- 异步IO
读写都由操作系统完成。
Reactor:主动模式。由程序不断轮询OS或框架IO是否就绪。
Proactor:被动模式。read、write操作都由操作系统或框架完成,之后再回调给应用程序。
4.3.3 IO多路复用、select poll epoll和LT与ET
epoll的过程分成三个步骤:
- 事件注册
- 轮询事件是否已就绪
- 事件就绪,执行实际的IO操作——通过accept/read/write操作。
- LT:水平触发,只要这个文件描述符还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作;
- ET:边缘触发,在它检测到有 I/O 事件时,通过 epoll_wait 调用会得到有事件通知的文件描述符,对于每一个被通知的文件描述符,如可读,则必须将该文件描述符一直读到空,让 errno 返回 EAGAIN 为止,否则下次的 epoll_wait 不会返回余下的数据,会丢掉事件。如果ET模式不是非阻塞的,那这个一直读或一直写势必会在最后一次阻塞。
epoll、select、poll详解:https://www.itqiankun.com/article/select-poll-epoll
I/O多路复用(multiplexing)的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作
select、poll 和 epoll 都是 Linux API 提供的 IO 复用方式。
select、poll、epoll之间的区别:
\ | select | poll | epoll |
---|---|---|---|
操作方式 | 遍历 | 遍历 | 回调 |
底层实现 | 数组 | 链表 | 哈希表 |
IO效率 | 每次调用都进行线性遍历,时间复杂度为O(n) | 每次调用都进行线性遍历,时间复杂度为O(n) | 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到rdllist里面。时间复杂度O(1) |
最大连接数 | 1024(x86)或 2048(x64) | 无上限 | 无上限 |
fd拷贝 | 每次调用select,都需要把fd集合从用户态拷贝到内核态 | 每次调用poll,都需要把fd集合从用户态拷贝到内核态 | 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝 |
epoll可以理解为event poll(基于事件的轮询)。
epoll本质上是在内核中建立了一个小型的文件系统,用于管理不同的套接字io,来一个就去epoll注册一下,然后一旦就绪会触发内部的callback函数,用于提示fd已就绪,
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。
epoll详解:epoll原理详解及epoll反应堆模型 - 知乎 (zhihu.com)
同步IO | 同步阻塞IO | |
同步IO | 同步非阻塞IO | |
同步IO | IO多路复用(同步阻塞) | select epoll poll java的nio |
异步IO | 异步IO | Linux aio windows上的IOCP C++上的asio库 |
- 阻塞和非阻塞是从函数调用角度来说的(自己),而同步和异步是从“读写是谁完成的”角度来说的(双方),
非阻塞:函数立即返回,然后让程序执行轮询策略;
同步:读写由应用程序完成;
异步:读写由OS完成,完成之后,回调或者事件通知应用程序。
- IO多路复用都是同步IO,因为读写都是由程序完成的,也是阻塞IO,因为select read write都是阻塞的。
4.3.4 服务器编程的1+N+M模式
一个监听线程,N个IO线程,M个worker线程,一般N=CPU核数,M看需要。
- 监听线程:
负责accept事件的注册和处理。每一个socket连接请求都由它处理,并移交给IO线程。
- IO线程:
负责每个socket连接上read write事件的注册和实际的socket读写,把读到的request放入Request队列,交由worker处理。
负责:1. 实际的socket读写;2. 将request交给Worker线程。
- Worker线程:
纯粹业务线程,没有socket读写操作。
- 提高CPU利用率
- 提高IO吞吐
- 内存共享,需要加锁;
- 过多的上下文切换。
多进程的好处:
- 减少了切换开销
- 各自独立,其它崩了也不影响。
Redis是单进程单线程(单线程处理请求),利用多核的时候开多个redis实例就可以了
多进程不适合用于IO密集型应用,对于IO密集型的应用,提高IO效率的方法:
- 异步IO
- 多线程
- 多协程
CAS:对比与交换,一般是用版本号。
5. 网络 5.1 HTTP1.0 使用keep-alive字段解决TCP连接复用的问题,使用content-length解决何时关闭TCP连接的问题。
keep-alive和content-Length一起使用。
5.2 HTTP1.1 默认是可复用TCP连接的,所以即使没有keep-alive,也能用。
Chunk机制(content-length的替代):
字段:Transfer-Encoding: chunk。
响应的body分成了一块块的,块之间由间隔符,所有块的结尾由特殊tag。
5.2.2 流水线与Head-of-line Blocking问题
流水线(pipeline)虽然能提升速度,但也有不小的副作用,所以一般默认关闭。
流水线的问题在于一个包阻塞,会导致后面的包也没接触。
5.2.3 HTTP/2出现之前的性能提升方法
5.2.4 “一来多回”问题
5.2.5 断点续传
下载时下载到一半连接中断了,客户端可以从上次断的地方继续下载。
5.3 HTTP/2(SDPY) Google的SPDY协议。
5.3.1 对HTTP1.1的兼容
HTTP/2和HTTP1.1并不是处于平级的位置,而是处在HTTP1.1和TCP之间。现在相当于在HTTP1.1和TCP之间多了一个转换层,这个转换层就是SPDY(HTTP/2)。
5.3.2 二进制分帧
为了解决HTTP1.1的队头阻塞问题所设计的核心属性。
二进制分帧:在把这个字符格式的报文给TCP之前转换成二进制,并且分成多个帧(多个数据块)来发送。
5.3.3 头部压缩
5.4 SSL/TLS SSL——Secure Socket Layer。
TLS——Transport Layer Security,传输层安全协议。
SSL/TLS处于TCP的上面,支撑各种应用层协议。
对称加密->双向非对称加密->单向非对称加密->数字证书与证书认证中心->根证书与CA信任链
截止到单向非对称加密,都可能遭受中间人攻击。
SSL的四次握手:
5.5 HTTPS HTTPS = HTTP + SSL/TLS
所以在HTTP和TCP之间,还有HTTP/2和SSL/TLS可以选
增加了SSL的传输过程的阶段:
- TCP连接的建立;
- SSL/TLS四次握手协商出对称加密的密钥
- 基于密钥,在TCP连接上对所有的HTTP Request/Response进行加密和解密
5.6 TCP和UDP TCP解决三个问题:
- 丢包(ACK+超时重传)
- 乱序(滑动窗口)
- 包乱码(检错)
5.7 QUIC Quick UDP Internet Connect,基于UDP的多路并发传输协议。
只要使用TCP,就无法解决队头阻塞问题。
QUIC取代了TCP的部分功能(不丢包),实现了SSL的所有功能,取代了HTTP/2的部分功能(多路复用)
5.7.1 不丢包
冗余(raid5和raid6)
Raid5:
? 每发送5个数据包,就发送一个冗余包,(R = A+B+C+D+E),所以丢了一个包也能恢复(E = R-A-B-C-D),如果每发送10个数据包就发送一个冗余包,那么十个丢1个包就能恢复。
Raid6:
? 在上面的基础上,变成两个冗余块。所以5个包可以丢两个包。
A+B+C+D+E=R1
A-B+C-D+E=R2
5.7.2 更少的RTT
UDP不需要RTT
5.7.3 连接迁移
与TCP(客户端IP+PORT以及服务端IP+port)不同,使用一个64位的标识标记一个连接。
6. 数据库 6.1 范式与反范式 一般要求达到第三范式。
范式 | 描述 | 反例 |
---|---|---|
第一范式 | 每个字段都是原子的,不能再分解 | |
第二范式 | 1. 表必须有主键,主键可以是单个属性或者几个属性的组合 2. 非主属性必须完全依赖,而不能部分依赖主键 |
在好友关系表中,主键是关注人ID+被关注人ID,但该表中还存储了名字、头像等字段, 这些字段只依赖与组合主键中的某个字段,而不完全依赖主键 |
第三范式 | 没有传递依赖:非主属性必须直接依赖主键 |
- 为什么要分
- 什么时候分
- 怎么分
- Join(联表查询)
- 分布式事务
不同的出发点会导致不同的分库方式,应对高并发可以是设置多个从库,业务拆分可以是
- 业务拆分——方便以后扩展,便于团队职责分工
- 应对高并发,区分高并发读还是高并发写。高并发读可以通过增加缓存、从库解决,高并发写要考虑分库分表
- 数据隔离
必须提供全局唯一的主键生成服务
6.2.3 拆分维度的选择
以电商为例,用用户ID去分还是用订单ID去分。
对于拆分之后其它维度的查询,一般有以下几个方法:
- 建立一个映射表
建立订单ID和用户ID之间的映射关系。但是很麻烦,要维护一张很大的表
- 业务双写
同一份数据,两套分库分表。一套用户ID,一套订单ID
- 异步双写
还是两套表,只是业务单写,然后通过binLog,同步到另外一套表上。
- 两个维度统一到一个维度:
把订单ID和用户ID统一成一个维度,这样可以按照用户ID分库,然后按订单ID查询的时候取出用户ID,用用户ID去查询。
分库分表之后,如何进行Join操作:
- 把Join拆成多个单表查询,不让数据库做Join,在软件层面对结果进行拼装;
- 做宽表,重写轻读
对于不得不用Join的情况,可以另外做一个join表,提前把结果Join好,重写轻读,也是空间换时间
- 利用搜索引擎
分布式事务很麻烦。最大的问题在于保证ACID。
A——atomicity,原子性
C——consistency,一致性
I——isolation,隔离性
D——durability,持久性
6.3 B+树 相比K-V结构,B+树可以做到:
- 范围查询
- 前缀匹配模糊查询
- 排序和分页
6.3.2 物理结构
磁盘的最小单位是块,InnoDB一块(也叫page)默认是16KB,每个page的id是32位,所以InnoDB的存储上限是64TB(2^32 * 16KB),
一个Page大概装1000个Key(1个Key大概是16Byter,其中8B的key,8B其它字段),意味这B+树有1000个分叉,装叶子节点的话,一个page大概可以装200条记录,一个三层的B+树大概可以存储16GB数据,另外需16MB的内存存储非叶子节点。
所以一般是装16MB的非叶节点数据在内存中,可以支撑2E条记录的查找。
所以建议按主键的自增顺序插入记录,防止引起page的大量迁移。
6.3.3 非主键索引
新建索引B+树,但是叶子节点记录的是数据的主键,可以随意组合新的索引组合。
6.4 事务的四个隔离级别 6.4.1 事务的四个隔离级别
事务并发导致的几类问题
编号 | 问题 | 描述 |
---|---|---|
1 | 脏读 | 事务A读取了一条记录的值,在做业务逻辑的时候,事务B回滚了该记录,所以A读了一个脏数据 |
2 | 不可重复读 | 在同一个事务里,两次读取同一行记录,但结果不一样,因为另一个事务在对这个数据进行Update操作(没有加锁) |
3 | 幻读 | 在同一个事务里,两次select数据,但结果数目不一样,因为另一个事务在对这个数据进行insert/delete操作(没有加锁) |
4 | 丢失更新 | 两个事务同时修改同一条记录,A的修改被B的修改覆盖了。 |
InnoDB事务隔离级别
级别 | 名称 | 解决问题 |
---|---|---|
1 | RU,Read Uncommited,读未提交 | 什么都没解决,相当于没有 |
2 | RC,Read Commited,读已提交 | 解决脏读 |
3 | RR,可重复读 | 解决了1-3,InnoDB的默认等级 |
4 | Serialization,串行 | 串行化,完全解决1-4 |
6.4.2 悲观锁和乐观锁
几种防丢失更新的方式:
- 利用单条语句的原子性
- 悲观锁
很重的锁
- 乐观锁
CAS机制,保存版本号。
- 分布式锁
解决分库分表状况下的锁问题
死锁四个条件:
- 互斥
- 循环等待
- 不可剥夺
- 请求和保持
C——consistency,一致性,各种约束条件,比如参照完整性、主键不为空等。C通过上层规则约束,相对简单。
I——isolation,隔离性。
D——durability,持久性。D容易,写到磁盘。
A和I很难,牵扯到并发和崩溃恢复的问题。
6.5.1 D的实现——Write-Ahead
内存操作数据 + Write-Ahead Log实现快速操作数据和把数据写入磁盘。
具体到InnoDB中,Write-Ahead Log是Redo Log。在InnoDB中,不光事务修改的数据库表数据是异步刷盘的,连Redo Log的写入本身也是异步的(RedoLog也是先在内存操作,然后再写到磁盘的)。
6.5.2 Redo Log的逻辑与物理结构
RedoLog是一个循环使用的块(新的会覆盖旧的)。
6.5.3 Physiological Logging
6.5.4 IO写入的原子性
跟底层系统息息相关。
最后的解决方法:
- 让硬件支持16KB写入的原子性
- Double write
6.5.6 事务、LSN与Log Block的关系
6.5.7 事务Rollback与崩溃恢复(ARIES算法)
6.6 事务实现的原理之2:Undo Log 6.6.1 Undo Log存在的必要性
6.6.2 Undo Log(MVCC)
读写的并发问题有三种策略
策略 | 解释 |
---|---|
互斥锁 | |
读写锁 | |
CopyOnWrite | 写的时候,把该数据对象拷贝一份,等写完之后,再把数据对象的指针(引用)一次性赋值回去,读的时候读取原始数据。 1. 读和读可以并发 2. 读和写可以并发 3. 写和写理论上可以并发 |
https://zhuanlan.zhihu.com/p/66791480
https://www.php.cn/mysql-tutorials-460111.html
MVCC是“维持一个数据的多个版本,使读写操作没有冲突”的一个抽象概念。换句话说,可能存在一个数据的多个版本
这个概念需要具体功能去实现,这个具体实现就是快照读。(具体实现下面讲)
- MVCC解决并发哪些问题?
- mvcc用来解决读—写冲突的无锁并发控制,就是为事务分配单向增长的时间戳。为每个数据修改保存一个版本,版本与事务时间戳相关联。
- 读操作只读取该事务开始前的数据库快照。
- 解决问题如下:
- 并发读-写时:可以做到读操作不阻塞写操作,同时写操作也不会阻塞读操作。
- 解决脏读、幻读、不可重复读等事务隔离问题,但不能解决上面的写-写 更新丢失问题。
- 因此有了下面提高并发性能的组合拳:
- MVCC + 悲观锁:MVCC解决读写冲突,悲观锁解决写写冲突
- MVCC + 乐观锁:MVCC解决读写冲突,乐观锁解决写写冲突
- MVCC的实现原理
它的实现原理主要是:
- 版本链,
- undo日志 ,
- Read View 来实现的
6.6.3 Undo Log不是Log
6.6.4 Undo Log与Redo Log的关联
6.6.5 各种锁
MVCC解决了快照读和写直接的并发问题,但对于写和写,当前读和写直接的并发,MVCC就无能为力了。
- 共享锁(S锁)和排他锁(X锁);
- 意向锁
- 记录锁
- 间隙锁
- 临键锁
- 插入意向锁
- 自增锁
锁表 | 锁行 | 锁范围 | |
---|---|---|---|
共享S | 表共享锁 | 行共享锁 | |
排他X | 表排他锁 | 行排他锁 | |
意向共享IS | 表意向共享锁 | ||
意向排他IX | 表意向排他锁 | ||
AI | 自增锁 |
- 表(S锁、X锁)、行(S锁、X锁)
- 意向锁(IS锁,IX锁)
意向锁是一种不与行级锁冲突表级锁。它的作用是提前向其它人宣布自己即将要对表中某一行上锁,提升判断效率。所以,一个事务要给某张表的某一行加S锁,就必须先获得整张表的IS锁;要给某一张加X锁,就必须先获得整张表的IX锁。
换句话说,IX IS锁只是声明,与其它锁并不互斥。
- 的
6.7.2 内部XA-Binlog与Redo Log一致性问题
6.7.3 三种主从复制模式
6.7.4 并行复制
7.框架、软件与中间件 8. 高并发问题 8.1 问题分类 高并发读、高并发写、高并发读写
8.1.1 侧重于 高并发读 的系统
- 搜索引擎
- 电商产品
- 电商系统的商品描述和图片
- 广告扣费系统
- 电商的库存系统和秒杀系统
- 支付系统和微信红包
- 微博和朋友圈
- 加缓存
- 并发读
- 重写轻读
- 读写分离
方法有:
- 本地缓存或Memcached/Redis集中式缓存
缓存可能遇到的问题:
- 缓存雪崩
- 缓存穿透
- 大量的热key过期
- Mysql的Master/Slave(主从)
- CDN静态文件加速(动静分离)
方法有:
- 异步RPC
- Google的“冗余请求”
重写轻读 vs 重读轻写
重写轻读,本质就是“空间换时间“。你不是计算起来耗时,延迟高吗,那我可以提前计算,然后存储起来。取的时候,直接去取。
我们通常对Mysql的用法,都是重读轻写,写的时候,简单;查的时候,做复杂的join计算,返回结果。这样做的好处是容易做到数据的强一致性,不会因为字段冗余,造成数据的不一致。但是性能可能就是问题。
而如果采用重写轻读,怎么做呢?你不是要看Feeds吗,那就为每个人准备一个Feeds,或者说收件箱。某个人发了微博之后,把他的微博扩散到所有人的收件箱,这个扩散是异步的,在后台扩散。这样每个人看自己的Feeds的时候,直接去自己的收件箱取就可以了。
方法有:
- 微博Feeds流
- 多表的关联查询:宽表与搜索引擎
本质都是读写分离,即CQRS(Command Query Reponsibility Separation)
CQRS的特征:
- 分别为读和写设计不同的数据结构
- 写通常通过分库分表抵抗写的压力
- 读和写的串联
- 读比写有延迟
- 数据分片
- 任务分片
- 异步化
- 批量
- 串行化+多进程单线程+异步IO
本质:对要处理的数据或请求分成多份并行处理
包括有:
- 数据库的分库分表
- Kafka的partition
- ES的分布式索引
数据分片是对要处理的数据进行分片,任务分片是对处理程序本身进行分片,例如流水线
包括有:
- CPU的指令流水线
- MapReduce
- Tomcat的1+N+M
- 短信验证码注册登录
- 电商的订单系统
- 广告计费系统
- LSM树(写内存+Write-Ahead日志)
- Kafka的pipeline
写一条记录,可以等数量多了,合并写入。
- Kafka的百万QPS写入
- 广告计费系统的合并扣费
- MySQL的小事务合并机制
多进程单线程是为了解决:锁竞争;线程切换开销大,导致线程数无法开很多。
8.4 容量规划 8.4.1 吞吐量、响应时间和并发数
8.4.2 压力测试与容量评估
9. 高可用与稳定性 9.1 多副本 9.2 隔离、熔断、限流和降级
- 隔离
隔离是指系统或资源分隔开,在系统发生故障时能限定传播范围和影响范围,即发生故障后不会出现滚雪球效应,从而把故障的影响限定在一个范围内。
- 数据隔离
- 机器隔离
- 线程池隔离
- 信号量隔离
比线程池隔离要更轻量。当信号量达到阈值,线程获取不到该信号量会丢弃请求,而不是阻塞在那等待信号量。
- 数据隔离
- 限流
- 技术层面的限流
- 限制并发数,比如限制连接池、线程池、Nginx的limit_conn模块
- 限制QPS
- 【架构|软件架构设计-大型网站技术架构于业务架构融合之道——部分知识点总结【未完】】业务层面的限流
比如100件产品的秒杀,有2W人抢购,只需要处理前500个请求就可以了。
- 限流算法
- 漏桶算法(输出恒定,可适合削峰、应付突发流量)
- 令牌算法(输入恒定,限制平均速率)
- 对比
漏桶算法是流出速率保持恒定,令牌算法是流入速率保持恒定。
- 漏桶算法(输出恒定,可适合削峰、应付突发流量)
- 技术层面的限流
- 熔断
一种自我保护,当某个服务已经接近极限的时候,会强制断开,不再提供服务,防止崩溃
- 根据请求失败率做熔断
- 根据请求响应时间做熔断
- 降级
兜底方案,尽可能提供最基础的核心业务,其它业务可以不提供。
推荐阅读
- #夏日挑战赛#数据库学霸笔记,考试/面试快速复习~
- 数据湖常用查询优化技术——《DEEPNOVA开发者社区》
- RadonDB MySQL Kubernetes 2.2.0 发布!
- 数据库|怎样评价国产报表工具和BI软件
- Redis|七天玩转Redis | Day4、Redis持久化机制
- 大数据|云原生边缘计算KubeEdge在智慧停车中的实践
- 剑指微服务分布式|Java旅游管理系统的设计与实现毕业设计
- 数据库|mysql查询结果去重
- Java|Java面试突击系列(五)(Redis集群模式)