架构设计 2-高性能架构
扫描左侧二维码 关注公众号 回复 “架构设计” 获取架构设计笔记完整思维导图
高性能关系型数据库
读写分离
读写分离的基本原理是将数据库读写操作分散到不同的节点上。
设计复杂度
- 主从复制延迟
- 写操作后的读操作指定发给数据库主服务器
- 读从机失败后再读一次主机
- 关键业务读写操作全部指向主机,非关键业务采用读写分离
- 分配机制
- 程序代码封装 如 TDDL
- 中间件封装 如 MySQL Router
“读写分离”,读写分离分散了数据库读写操作的压力,但没有分散存储压力。
分库分表
单库数据量大的风险
- 数据量太大,读写的性能会下降,即使有索引,索引也会变得很大,性能同样会下降。
- 数据文件会变得很大,数据库备份和恢复需要耗费很长时间。
- 数据文件越大,极端情况下丢失数据的风险越高
常见的分散存储的方法“分库分表”,其中包括“分库”和“分表”两大类。
分库
业务分库指的是按照业务模块将数据分散到不同的数据库服务器。问题:
- .join 操作问题
- 事务问题
- 成本问题
优点:分散存储和访问压力
分表
将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,但如果业务继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。如果几亿用户数据,全部存放在一台数据库服务器的一张表中,肯定是无法满足性能要求的,此时就需要对单表数据进行拆分。
单表数据拆分有两种方式:垂直分表和水平分表。
垂直分表
- 垂直分表适合将表中某些不常用且占了大量空间的列拆分出去
- 垂直分表引入的复杂性主要体现在表操作的数量要增加。
水平分表
- 路由
- 范围路由
- Hash路由
- 配置路由
- join 操作
- count() 操作
- count() 相加
- 记录数表
- order by 操作
- 路由
高性能NoSQL数据库
关系数据库缺点
- 关系数据库存储的是行记录,无法存储数据结构
- 关系数据库的 schema 扩展很不方便
- 关系数据库在大数据场景下 I/O 较高
- 关系数据库的全文搜索功能比较弱
常见的 NoSQL 方案
- K-V 存储:解决关系数据库无法存储数据结构的问题,以 Redis 为代表
- 文档数据库:解决关系数据库强 schema 约束的问题,以 MongoDB 为代表
- 列式数据库:解决关系数据库大数据场景下的 I/O 问题,以 HBase 为代表
- 全文搜索引擎:解决关系数据库的全文搜索性能问题,以 Elasticsearch 为代表
高性能缓存架构
单纯依靠存储系统的性能提升不够的,典型的场景
- 需要经过复杂运算后得出的数据,存储系统无能为力
- 读多写少的数据,存储系统有心无力
缓存的架构设计要点
缓存穿透
缓存穿透是指缓存没有发挥作用,业务系统虽然去缓存查询数据,但缓存中没有数据,业务系统需要再次去存储系统查询数据。两种情况:
- 存储数据不存在,如果查询存储系统的数据没有找到,则直接设置一个默认值存到缓存中。
- 缓存数据生成耗费大量时间或者资源。
缓存雪崩
缓存雪崩是指当缓存失效(过期)后引起系统性能急剧下降的情况。常见解决方法:
更新锁机制
- 对缓存更新操作进行加锁保护,保证只有一个线程能够进行缓存更新,未能获取更新锁的线程要么等待锁释放后重新读取缓存,要么就返回空值或者默认值
- 分布式集群的业务系统要实现更新锁机制,需要用到分布式锁
后台更新机制
- 由后台线程来更新缓存,而不是由业务线程来更新缓存,缓存本身的有效期设置为永久,后台线程定时更新缓存
- 当缓存系统内存不够时,会“踢掉”一些缓存数据,从缓存被“踢掉”到下一次定时更新缓存的这段时间内,业务线程读取缓存返回空值,而业务线程本身又不会去更新缓存,因此业务上看到的现象就是数据丢了
- 后台线程除了定时更新缓存,还要频繁地去读取缓存(例如,1 秒或者 100 毫秒读取一次),如果发现缓存被“踢了”就立刻更新缓存
- 业务线程发现缓存失效后,通过消息队列发送一条消息通知后台线程更新缓存。
- 优点
- 后台更新既适应单机多线程的场景,也适合分布式集群的场景,相比更新锁机制要简单一些。
- 后台更新机制还适合业务刚上线的时候进行缓存预热。缓存预热指系统上线后,将相关的缓存数据直接加载到缓存系统,而不是等待用户访问才来触发缓存加载。
缓存热点
- 对于一些特别热点的数据,如果大部分甚至所有的业务请求都命中同一份缓存数据,则这份数据所在的缓存服务器的压力也很大
- 缓存热点的解决方案就是复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力
- 缓存副本设计有一个细节需要注意,就是不同的缓存副本不要设置统一的过期时间,否则就会出现所有缓存副本同时生成同时失效的情况,从而引发缓存雪崩效应。正确的做法是设定一个过期时间范围,不同的缓存副本的过期时间是指定范围内的随机值
实现方式
- 程序代码实现”的中间层方式
- 独立的中间件
单机服务器高性能模式
高性能架构设计主要集中在两方面
- 尽量提升单服务器的性能,将单服务器的性能发挥到极致
- 如果单服务器无法支撑性能,设计服务器集群方案
单服务器高性能的关键,服务器采取的并发模型:
- 服务器如何管理连接
- 服务器如何处理请求
PPC和TPC
PPC
Process Per Connection 的缩写,其含义是指每次有新的连接就新建一个进程去专门处理这个连接的请求,这是传统的 UNIX 网络服务器所采用的模型。PPC 模式实现简单,比较适合服务器的连接数没那么多的情况,例如数据库服务器。
弊端:
- fork 代价高:站在操作系统的角度,创建一个进程的代价是很高的,需要分配很多内核资源,需要将内存映像从父进程复制到子进程
- 父子进程通信复杂:父进程“fork”子进程时,文件描述符可以通过内存映像复制从父进程传到子进程,但“fork”完成后,父子进程通信就比较麻烦了,需要采用 IPC(Interprocess Communication)之类的进程通信方案
- 支持的并发连接数量有限:如果每个连接存活时间比较长,而且新的连接又源源不断的进来,则进程数量会越来越多,操作系统进程调度和切换的频率也越来越高,系统的压力也会越来越大。因此,一般情况下,PPC 方案能处理的并发连接数量最大也就几百
prefork
- prefork 就是提前创建进程(pre-fork)
TPC
Thread Per Connection 的缩写,其含义是指每次有新的连接就新建一个线程去专门处理这个连接的请求。TPC 虽然解决了 fork 代价高和进程通信复杂的问题。
弊端
- 创建线程虽然比创建进程代价低,但并不是没有代价,高并发时(例如每秒上万连接)还是有性能问题。
- 无须进程间通信,但是线程间的互斥和共享又引入了复杂度,可能一不小心就导致了死锁问题。
- 多线程会出现互相影响的情况,某个线程出现异常时,可能导致整个进程退出(例如内存越界)
- TPC 还是存在 CPU 线程调度和切换代价的问题
prethread
- prethread 模式会预先创建线程
TPC 方案本质上和 PPC 方案基本类似,在并发几百连接的场景下,反而更多地是采用 PPC 的方案,因为 PPC 方案不会有死锁的风险,也不会多进程互相影响,稳定性更高。
Reactor和Proactor
I/O 多路复用技术归纳起来有两个关键实现点
- 当多条连接共用一个阻塞对象后,进程只需要在一个阻塞对象上等待,而无须再轮询所有连接,常见的实现方式有 select、epoll、kqueue 等
- 当某条连接有新的数据可以处理时,操作系统会通知进程,进程从阻塞状态返回,开始进行业务处理。
Reactor
I/O 多路复用结合线程池
- 来了一个事件我就有相应的反应”,这里的“我”就是 Reactor,具体的反应就是我们写的代码,Reactor 会根据事件类型来调用相应的代码进行处理
- Reactor 模式也叫 Dispatcher 模式,更加贴近模式本身的含义,即 I/O 多路复用统一监听事件,收到事件后分配(Dispatch)给某个进程。
Reactor 是非阻塞同步网络模型,因为真正的 read 和 send 操作都需要用户进程同步操作。这里的“同步”指用户进程在执行 read 和 send 这类 I/O 操作的时候是同步的。
Reactor 核心:
- Reactor :负责监听和分配事件
- 处理资源池:负责处理事件
Reactor 模式
- 单 Reactor 单进程 / 线程
- 优点
- 单 Reactor 单进程的模式优点就是很简单,没有进程间通信,没有进程竞争,全部都在同一个进程内完成
- 缺点
- 只有一个进程,无法发挥多核 CPU 的性能;只能采取部署多个系统来利用多核 CPU,但这样会带来运维复杂度,本来只要维护一个系统,用这种方式需要在一台机器上维护多套系统。
- Handler 在处理某个连接上的业务时,整个进程无法处理其他连接的事件,很容易导致性能瓶颈
- 使用场景
- 单 Reactor 单进程的方案在实践中应用场景不多,只适用于业务处理非常快速的场景
- 目前比较著名的开源软件中使用单 Reactor 单进程的是 Redis
- 优点
- 单 Reactor 多线程
- 优点
- 单 Reator 多线程方案能够充分利用多核多 CPU 的处理能力
- 缺点
- 多线程数据共享和访问比较复杂
- Reactor 承担所有事件的监听和响应,只在主线程中运行,瞬间高并发时会成为性能瓶颈。
- 优点
- 多 Reactor 多进程 / 线程
- 优点
- 父进程和子进程的职责非常明确,父进程只负责接收新连接,子进程负责完成后续的业务处理
- 父进程和子进程的交互很简单,父进程只需要把新连接传给子进程,子进程无须返回数据。
- 子进程之间是互相独立的,无须同步共享之类的处理(这里仅限于网络模型相关的 select、read、send 等无须同步共享,“业务处理”还是有可能需要同步共享的)。
- 使用场景
- Nginx 采用的是多 Reactor 多进程
- Memcache 和 Netty采用多 Reactor 多线程
- 优点
Proactor
- 理论上 Proactor 比 Reactor 效率要高一些,异步 I/O 能够充分利用 DMA 特性,让 I/O 操作与计算重叠,但要实现真正的异步 I/O,操作系统需要做大量的工作
- Reactor 可以理解为“来了事件我通知你,你来处理”,而 Proactor 可以理解为“来了事件我来处理,处理完了我通知你”。这里的“我”就是操作系统内核,“事件”就是有新连接、有数据可读、有数据可写的这些 I/O 事件,“你”就是我们的程序代码。
负载均衡
高性能集群的复杂性主要体现在需要增加一个任务分配器,以及为任务选择一个合适的任务分配算法。
不同的任务分配算法目标是不一样的,有的基于负载考虑,有的基于性能(吞吐量、响应时间)考虑,有的基于业务考虑。
分类
常见的负载均衡系统包括 3 种:DNS 负载均衡、硬件负载均衡和软件负载均衡。
DNS 负载均衡
DNS 是最简单也是最常见的负载均衡方式,一般用来实现地理级别的均衡。
优点
- 简单、成本低:负载均衡工作交给 DNS 服务器处理,无须自己开发或者维护负载均衡设备。
- 就近访问,提升访问速度:DNS 解析时可以根据请求来源 IP,解析成距离用户最近的服务器地址,可以加快访问速度,改善性能。
缺点
- 更新不及时:DNS 缓存的时间比较长,修改 DNS 配置后,由于缓存的原因,还是有很多用户会继续访问修改前的 IP,这样的访问会失败,达不到负载均衡的目的,并且也影响用户正常使用业务。
- 扩展性差:DNS 负载均衡的控制权在域名商那里,无法根据业务特点针对其做更多的定制化功能和扩展特性。
- 分配策略比较简单:DNS 负载均衡支持的算法少;不能区分服务器的差异(不能根据系统与服务的状态来判断负载);也无法感知后端服务器的状态。
硬件负载均衡
硬件负载均衡是通过单独的硬件设备来实现负载均衡功能,这类设备和路由器、交换机类似,可以理解为一个用于负载均衡的基础网络设备。主要有 F5 & A10 等。
优点:
- 功能强大:全面支持各层级的负载均衡,支持全面的负载均衡算法,支持全局负载均衡。
- 性能强大:对比一下,软件负载均衡支持到 10 万级并发已经很厉害了,硬件负载均衡可以支持 100 万以上的并发。
- 稳定性高:商用硬件负载均衡,经过了良好的严格测试,经过大规模使用,稳定性高。
- 支持安全防护:硬件均衡设备除具备负载均衡功能外,还具备防火墙、防 DDoS 攻击等安全功能。
缺点
- 价格昂贵
- 扩展能力差
软件负载均衡
软件负载均衡通过负载均衡软件来实现负载均衡功能,常见的有 Nginx 和 LVS,其中 Nginx 是软件的 7 层负载均衡,LVS 是 Linux 内核的 4 层负载均衡。4 层和 7 层的区别就在于协议和灵活性。
优点
- 简单:无论是部署还是维护都比较简单。
- 便宜:只要买个 Linux 服务器,装上软件即可。
- 灵活:4 层和 7 层负载均衡可以根据业务进行选择;也可以根据业务进行比较方便的扩展,例如,可以通过 Nginx 的插件来实现业务的定制化功能。
缺点
- 性能一般:一个 Nginx 大约能支撑 5 万并发
- 功能没有硬件负载均衡那么强大。
- 一般不具备防火墙和防 DDoS 攻击等安全功能。
原则
- DNS 负载均衡用于实现地理级别的负载均衡;
- 硬件负载均衡用于实现集群级别的负载均衡;
- 软件负载均衡用于实现机器级别的负载均衡。
算法
任务平均类
负载均衡系统将收到的任务平均分配给服务器进行处理,这里的“平均”可以是绝对数量的平均,也可以是比例或者权重上的平均。
轮询
- 只要服务器在运行,运行状态是不关注的
- 简单”是轮询算法的优点,也是它的缺点
加权轮询
- 解决不同服务器处理能力有差异的问题
负载均衡类
负载均衡系统根据服务器的负载来进行分配,这里的负载并不一定是通常意义上我们说的“CPU 负载”,而是系统当前的压力,可以用 CPU 负载来衡量,也可以用连接数、I/O 使用率、网卡吞吐量等来衡量系统的压力。负载最低优先:
- LVS 这种 4 层网络负载均衡设备,可以以“连接数”来判断服务器的状态,服务器连接数越大,表明服务器压力越大。
- Nginx 这种 7 层网络负载系统,可以以“HTTP 请求数”来判断服务器状态
- 如果我们自己开发负载均衡系统,可以根据业务特点来选择指标衡量系统压力。如果是 CPU 密集型,可以以“CPU 负载”来衡量系统压力;如果是 I/O 密集型,可以以“I/O 负载”来衡量系统压力。
优点
- 负载最低优先的算法解决了轮询算法中无法感知服务器状态的问题
缺点
- 最少连接数优先的算法要求负载均衡系统统计每个服务器当前建立的连接,其应用场景仅限于负载均衡接收的任何连接请求都会转发给服务器进行处理,否则如果负载均衡系统和服务器之间是固定的连接池方式,就不适合采取这种算法
- CPU 负载最低优先的算法要求负载均衡系统以某种方式收集每个服务器的 CPU 负载,而且要确定是以 1 分钟的负载为标准,还是以 15 分钟的负载为标准,不存在 1 分钟肯定比 15 分钟要好或者差。不同业务最优的时间间隔是不一样的,时间间隔太短容易造成频繁波动,时间间隔太长又可能造成峰值来临时响应缓慢。
性能最优类
负载最低优先类算法是站在服务器的角度来进行分配的,而性能最优优先类算法则是站在客户端的角度来进行分配的,优先将任务分配给处理速度最快的服务器,通过这种方式达到最快响应客户端的目的。
缺点
- 负载均衡系统需要收集和分析每个服务器每个任务的响应时间,在大量任务处理的场景下,这种收集和统计本身也会消耗较多的性能
- 为了减少这种统计上的消耗,可以采取采样的方式来统计,需要合适的采样率
- 无论是全部统计还是采样统计,都需要选择合适的周期
Hash 类
- 源地址 Hash
- ID Hash