从零开始学架构系列文章:
-
从零开始学架构——概念和基础
-
从零开始学架构——万字长文揭秘高性能架构
-
从零开始学架构——高可用架构
-
从零开始学架构——可扩展架构
高性能存储
关系数据库
互联网业务兴起之后,海量用户加上海量数据的特点,单个数据库服务器己经难以满足业务需要,必须考虑数据库集群的方式来提升性能。
高性能数据库集群有两种实现方式:
第一种方式是“读写分离”,其本质是将访问压力分散到集群中的多个节点,但是没有分散存储压力;
第二种方式是“分库分表飞既可以分散访问压力,又可以分散存储压力。接下来我们将详细介绍这些方案。
读写分离
读写分离的基本原理是将数据库读写操作分散到不同的节点上,其基本架构如下图所示。
读写分离的基本实现如下:
-
数据库服务器搭建主从集群, 一主一从、一主多从都可以。
-
数据库主机负责读写操作,从机只负责读操作。
-
数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
-
业务服务器将写操作发给数据库主机,将读操作发给数据库从机。
读写分离的实现逻辑并不复杂,但在实际应用过程中需要应对复制延迟带来的复杂性。
以用户注册登录为例,用户刚注册时将数据写入了主库,用户登录时读的数据是从库的数据,如果主库的数据没有及时的同步到从库,就会造成用户不存在的bad case。
解决主从复制延迟有几种常见的方法:
-
写操作后的读操作指定发给数据库主服务器。
-
读从机失败后再读一次主机。
-
关键业务读写操作全部指向主机,非关键业务采用读写分离。
分库分表
读写分离分散了数据库读写操作的压力, 但没有分散存储压力, 当数据量达到千万甚至上亿条的时候, 单台数据库服务器的存储能力会成为系统的瓶颈, 主要体现在以下几个方面:
-
数据量太大,读写的性能会下降,即使有索引,索引也会变得很大,性能同样会下降。
-
数据文件会变得很大, 数据库备份和恢复需要耗费很长时间。
-
数据文件越大, 极端情况下丢失数据的风险越高。
分散存储压力的方法有两种:
-
一是 “分库”
-
二是 “分表”
业务分库
业务分库指的是按照业务模块将数据分散到不同的数据库服务器。例如,一个简单的电商网站,包括用户、商品、订单三个业务模块,我们可以将用户数据、商品数据、订单数据分开放到三台不同的数据库服务器上,而不是将所有数据都放在一台数据库服务器上。
虽然业务分库能够分散存储和访问压力,但同时也带来了新的问题:
-
业务分库后,原本在同一个数据库中的表分散到不同数据库中,导致无法使用SQL 的join查询;
-
原本在同一个数据库中不同的表可以在同一个事务中修改,业务分库后,表分散到不同的数据库中,无法通过事务统一修改。
-
业务分库同时也带来了成本的代价,本来1 台服务器搞定的事情,现在要3 台,如果考虑备份,那就是2 台变成了6 台。
分表
随着业务继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈,此时就需要对单表数据进行拆分。
单表数据拆分有两种方式: 垂直分表和水平分表。示意图如下。
垂直分表
垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。例如,前面示意图中的nickname 和description 字段,垂直分表引入的复杂性主要体现在表操作的数量要增加。例如,原来只要一次查询就可以获取name 、age 、sex 、nickname 、description,现在需要两次查询, 一次查询获取name 、age 、sex ,另外一次查询获取nickname 、description 。
水平分表
水平分表适合表行数特别大的表,将行数多的一个表拆分成行数较少的多个表,水平分表相比垂直分表,会引入更多的复杂性,主要表现在以下方面:
【路由】
水平分表后,某条数据具体属于哪个切分后的子表,需要增加路由算法进行计算,这个算法会引入一定的复杂性。常见的路由算法有如下几种:
- 范围路由:选取有序的数据列(例如,整型、时间戳等〉作为路由的条件,不同分段分散到不同的数据库表中,
范围路由设计的复杂点主要体现在分段大小的选取上,分段太小会导致切分后子表数量过多, 增加维护复杂度;分段太大可能会导致单表依然存在性能问题, 一般建议分段大小在100万至2000 万之间, 具体需要根据业务选取合适的分段大小。
优点 | 缺点 |
---|---|
可以随着数据的增加平滑地扩充新的表。例如,现在的用户是100 万,如果增加到1000 万,只需要增加新的表就可以了,原有的数据不需要动。 | 范围路由的一个比较隐含的缺点是分布不均匀,假如按照1000 万来进行分表,有可能某个分段实际存储的数据只有1000 条, 而另外一个分段实际存储的数据有900 万条。 |
- Hash 路由:选取某个列(或者某几个列组合也可以〉的值进行Hash 运算,然后根据Hash 结果分散到不同的数据库表中。Hash 路由设计的复杂点主要体现在初始表数量的选取上,表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题。而用了Hash 路由后,增加字表数量是非常麻烦的, 所有数据都要重分布。
优点 | 缺点 |
---|---|
表分布比较均匀 | 扩充新的表很麻烦,所有数据都要重分布。 |
- 配置路由:配置路由就是用一张独立的表来记录路由信息。以用户ID 为例,我们新增一张user_router 表,这个表包含user_id 和table_id 两列,根据user_id 就可以查询对应的table_id 。
优点 | 缺点 |
---|---|
配置路由设计简单,使用起来非常灵活,尤其是在扩充表的时候,只需要迁移指定的数据,然后修改路由表就可以了。 | 配置路由的缺点就是必须多查询一次,会影响整体性能; 而且路由表本身如果太大(例如,几亿条数据),性能同样可能成为瓶颈。 |
【join操作】
水平分表后,数据分散在多个表中,如果需要与其他表进行join查询,需要在业务代码或数据库中间件中进行多次join查询,然后将结果合井。
【count() 操作】
水平分表后,虽然物理上数据分散到多个表中,但某些业务逻辑上还是会将这些表当作一个表来处理。例如,获取记录总数用于分页或展示, 水平分表前用一个count()就能完成的操作,在分表后就没那么简单了。常见的处理方式有如下两种:
-
count()相加:对每个表进行count()操作,然后将结果相加。
-
记录数表:新建一张表,包含table_name 、row_count 两个字段,每次插入或删除子表数据成功后,都更新“记录数表”。
【order by 操作】
水平分表后, 数据分散到多个子表中,排序操作无法在数据库中完成,只能由业务代码或数据库中间件分别查询每个子表中的数据,然后汇总进行排序。
NoSQL
关系数据库具有强大的SQL 功能和ACID 的属性,使得关系数据库广泛应用于各式各样的系统中,但这井不意味着关系数据库是完美的,关系数据库存在如下缺点:
-
关系数据库存储的是行记录,无法存储数据结构;
-
关系数据库的schema 扩展很不方便;
-
关系数据库在大数据场景下I/O 较高;
-
关系数据库的全文搜索功能比较弱。
针对上述问题,分别诞生了不同的NoSQL 解决方案,这些方案与关系数据库相比,在某些应用场景下表现更好。但世上没有免费的午餐, NoSQL 方案带来的优势,本质上是牺牲ACID特性中的某个或某几个特性, 因此我们不能盲目地迷信NoSQL 是银弹, 而应该将 NoSQL 作为 SQL 的一个有力补充, NoSQL !=No SQL ,而是NoSQL = Not Only SQL 。
常见的NoSQL方案有如下4 类:
-
K-V 存储:解决关系数据库无法存储数据结构的问题,以Redis 为代表。
-
文档数据库:解决关系数据库强schema约束的问题,以MongoDB 为代表。
-
列式数据库: 解决关系数据库大数据场景下的I/O 问题,以HBase 为代表。
-
全文搜索引擎:解决关系数据库的全文搜索性能问题,以Elasticsearch 为代表。
缓存
缓存作为一种广泛应用的高性能解决方案,几乎存在于所有对性能要求较高的系统中。缓存给系统带来更高的性能同时也会引入更多的复杂性:
缓存穿透
当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。
应对缓存穿透的方案,常见的方案有三种。
-
第一种方案,非法请求的限制;
-
第二种方案,缓存空值或者默认值;
-
第三种方案,使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在;
缓存击穿
如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。
应对缓存击穿可以采取前面说到两种方案:
-
互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
-
不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;
缓存雪崩
当大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。
发生缓存雪崩的原因主要有两个:
-
大量数据同时过期;
-
Redis(缓存组件)故障。
针对这两个问题有不同的解决办法:
-
大量数据同时过期:
-
缓存组件宕机:
缓存热点
虽然缓存系统本身的性能比较高,但对于一些特别热点的数据,如果大部分甚至所有的业务请求都命中同一份缓存数据,则这份数据所在的缓存服务器的压力也很大。例如,某明星微博发布“我们 ”来宣告恋爱了,短时间内上千万的用户都会来围观。
缓存热点的解决方案就是复制多份缓存, 将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力。以新浪微博为例,对于粉丝数超过100 万的明星,每条微博都可以生成100 份缓存,缓存的数据是一样的,通过在缓存的key 里面加上编号进行区分,每次读缓存时都随机读取其中某份缓存。
高性能架构设计主要集中在两方面:
-
尽量提升单服务器的性能,将单服务器的性能发挥到极致。
-
如果单服务器无法支撑性能,设计服务器集群方案。
除了以上两点,最终系统能否实现高性能,还和具体的实现及编码相关。但架构设计是高性能的基础,如果架构设计没有做到高性能,则后面的具体实现和编码能提升的空间是有限的。形象地说,架构设计决定了系统性能的上限,实现细节决定了系统性能的下限。
单服务器高性能
单服务器高性能的关键之一就是服务器采取的网络编程模型,网络编程模型有如下两个关键设计点:
-
服务器如何管理连接。
-
服务器如何处理请求。
以上两个设计点最终都和操作系统的I/O模型及进程模型相关。
-
I/O 模型:阻塞、非阻塞、同步、异步。
-
进程模型:单进程、多进程、多线程。
不同的I/O模型和进程模型的组合,性能有很大差异,下面我们分别介绍:
PPC
PPC 是Process per Connection 的缩写,其含义是指每次有新的连接就新建一个进程去专门处理这个连接的请求,这是传统的UNIX 网络服务器所采用的模型。基本的流程图如下。
主要流程如下:
-
父进程接受连接(图中accept ) 。
-
父进程“ fork ”子进程(图中fork )。
-
子进程处理连接的读写请求(图中子进程read 、业务处理、write )。
-
子进程关闭连接(图中子进程中的close ) 。
优点 | 缺点 |
---|---|
PPC 模式实现简单,比较适合服务器的连接数没那么多的情况。 | fork 代价高:站在操作系统的角度,创建一个进程的代价是很高的,需要分配很多内核资源,需要将内存映像从父进程复制到子进程。父子进程通信复杂: 父进程“ fork ” 子进程时,文件描述符可以通过内存映像复制从父进程传到子进程,但“ fork ” 完成后,父子进程通信就比较麻烦了,需要采用IPC ( Interprocess Communication )之类的进程通信方案。进程数量增大后对操作系统压力较大:如果每个连接存活时间比较长,而且新的连接又源源不断的进来,则进程数量会越来越多,操作系统进程调度和切换的频率也越来越高,系统的压力也会越来越大。 |
PreFork
在 PPC 模式中,只有在有连接进来时才会创建新的进程来处理连接请求。由于创建进程的开销比较大,用户在访问时可能会感觉速度比较慢。prefork 模式就是为了解决这个问题而出现的。
顾名思义,prefork 就是提前创建进程。系统在启动时就预先创建好进程,然后再开始接受用户的请求。当有新的连接进来时,就不需要再创建新的进程,从而加快用户的访问速度,提高用户体验。prefork 的基本示意图如下所示。
PreFork只是解决了PPC模式下 fork 进程比较慢的问题,其余问题并未解决:
优点 | 缺点 |
---|---|
PPC 模式实现简单,比较适合服务器的连接数没那么多的情况。 | 父子进程通信复杂: 父进程“ fork ” 子进程时,文件描述符可以通过内存映像复制从父进程传到子进程,但“ fork ” 完成后,父子进程通信就比较麻烦了,需要采用IPC ( Interprocess Communication )之类的进程通信方案。进程数量增大后对操作系统压力较大:如果每个连接存活时间比较长,而且新的连接又源源不断的进来,则进程数量会越来越多,操作系统进程调度和切换的频率也越来越高,系统的压力也会越来越大。 |
TPC
TPC 是Thread per Connection 的缩写,其含义是指每次有新的连接就新建一个线程去专门处理这个连接的请求。
TPC 的基本流程如下:
-
父进程接受连接(图中accept ) 。
-
父进程创建子线程(图中pthread )。
-
子线程处理连接的读写请求(图中子线程read 、业务处理、write )。
-
子线程关闭连接(图中子线程中的close ) 。
与进程相比, 线程更轻量级,创建线程的消耗比进程要少得多;同时多线程是共享进程内存空间的, 线程通信相比进程通信更简单。因此, TPC 实际上是解决或弱化了PPC 的问题1 ( fork 代价高) 和问题2 (父子进程通信复杂)。但是也引入了新的问题:
优点 | 缺点 |
---|---|
PPC 模式实现简单,比较适合服务器的连接数没那么多的情况。 | 并发安全:线程间的互斥和共享又引入了复杂度,可能一不小心就导致了死锁问题;多线程会出现互相影响的情况,某个线程出现异常时,可能导致整个进程退出(例如内存越界〉。TPC 还是存在CPU 线程调度和切换代价的问题。 |
prethread
和prefork 类似, prethread 模式会预先创建线程,然后才开始接受用户的请求,当有新的连接进来的时候,就可以省去创建线程的操作,让用户感觉更快、体验更好。但是TPC模式下的问题还会存在,并未得到解决,这里就不做过多赘述。
Reactor
Reactor 模式也叫Dispatcher 模式(很多开源的系统里面会看到这个名称的类,其实就是实现Reactor 模式的〉,更加贴近模式本身的含义,即I/O 多路复用统一监昕事件,收到事件后分配( Dispatch )给某个进程。
Reactor 模式的核心组成部分包括Reactor 和处理资源池(进程池或线程池),其中Reactor负责监听和分配事件,处理资源池负责处理事件。
根据Reactor个数和处理资源池线程(进程)个数的不同,有下面常见的应用场景:
单Reactor单进程/线程
单Reactor 单进程/线程的方案示意图如下(以进程为例)。
方案详细说明如下:
-
Reactor 对象通过select 监控连接事件,收到事件后通过dispatch 进行分发。
-
如果是连接建立的事件,则由Acceptor 处理, Acceptor 通过accept 接受连接,并创建一个Handler 来处理连接后续的各种事件。
-
如果不是连接建立事件,则Reactor 会调用连接对应的Handler(第2 步中创建的Handler )来进行响应。
-
Handler 会完成read->业务处理->send 的完整业务流程。
优点 | 缺点 |
---|---|
单Reactor 单进程的模式优点就是很简单,没有进程间通信,没有进程竞争,全部都在同一个进程内完成。 | 只有一个进程,无法发挥多核CPU 的性能;只能采取部署多个系统来利用多核CPU,但这样会带来运维复杂度,本来只要维护一个系统,用这种方式需要在一台机器上维护多套系统。Handler 在处理某个连接上的业务时, 整个进程无法处理其他连接的事件, 很容易导致性能瓶颈。 |
单Reactor多线程
为了避免单Reactor 单进程/线程方案的缺点,引入多进程/多线程是显而易见的,这就产生了第二个方案: 单Reactor 多钱程。
方案详细说明如下:
-
主线程中, Reactor 对象通过select 监控连接事件,收到事件后通过dispatch 进行分发。
-
如果是连接建立的事件,则由Acceptor 处理, Acceptor 通过accept 接受连接,并创建一个Handler 来处理连接后续的各种事件。
-
如果不是连接建立事件,则Reactor 会调用连接对应的Handler(第2 步中创建的Handler)来进行响应。
-
Handler 只负责响应事件,不进行业务处理; Handler 通过read 读取到数据后, 会发给Processor 进行业务处理。
-
Processor 会在独立的子钱程中完成真正的业务处理,然后将响应结果发给主进程的Handler 处理; Handler 收到响应后通过send 将响应结果返回给client 。
优点 | 缺点 |
---|---|
单Reactor 多线程方案能够充分利用多核多CPU 的处理能力 | 多线程数据共享和访问比较复杂。Reactor 承担所有事件的监昕和响应,只在主线程中运行,瞬间高并发时会成为性能瓶颈。 |
多Reactor 多进程/线程
为了解决单Reactor 多线程的问题,最直观的方法就是将单Reactor 改为多Reactor ,这就产生了第三个方案: 多Reactor 多进程/线程。
方案详细说明如下:
-
父进程中main Reactor 对象通过select 监控连接建立事件,收到事件后通过Acceptor接收,将新的连接分配给某个子进程。
-
子进程的sub Reactor 将main Reactor 分配的连接加入连接队列进行监听,并创建一个Handler 用于处理连接的各种事件。
-
当有新的事件发生时, sub Reactor会调用连接对应的Handler(即第2 步中创建的Handler)来进行响应。
-
Handler 完成read→业务处理→ send 的完整业务流程。
多Reactor 多进程/线程的方案看起来比单Reactor 多线程要复杂,但实际实现时反而更加简单,主要原因如下:
-
父进程和子进程的职责非常明确,父进程只负责接收新连接,子进程负责完成后续的业务处理。
-
父进程和子进程的交互很简单,父进程只需要把新连接传给子进程,子进程无须返回数据。
-
子进程之间是互相独立的,无须同步共享之类的处理( 这里仅限于网络模型相关的select 、read 、send 等无须同步共享,“业务处理”还是有可能需要同步共享的) 。
Proactor
Reactor 是非阻塞同步网络模型,因为真正的read 和send 操作都需要用户进程同步操作。这里的“同步”指用户进程在执行read 和send 这类I/O 操作的时候是同步的,如果把I/O 操作改为异步就能够进一步提升性能, 这就是异步网络模型Proactor 。
-
Proactor Initiator 负责创建Proactor 和 Handler ,并将Proactor 和Handler 都通过Asynchronous Operation Processor 注册到内核。
-
Asynchronous Operation Processor 负责处理注册请求, 并完成I/O操作。
-
Asynchronous Operation Processor 完成I/O 操作后通知Proactor 。
-
Proactor 根据不同的事件类型回调不同的Handler 进行业务处理。
-
Handler 完成业务处理, Handler 也可以注册新的Handler 到内核进程。
集群高性能
高性能集群的本质很简单,通过增加更多的服务器来提升系统整体的计算能力。计算本身存在一个特点:同样的输入数据和逻辑,无论在哪台服务器上执行,都应该得到相同的输出。因此高性能集群设计的复杂度主要体现在任务分配这部分,需要设计合理的任务分配策略,将计算任务分配到多台服务器上执行。
负载均衡分类
常见的负载均衡系统包括3 种: DNS 负载均衡、硬件负载均衡和软件负载均衡。
DNS 负载均衡
DNS 是最简单也是最常见的负载均衡方式,DNS 负载均衡的本质是DNS 解析同一个域名可以返回不同的IP 地址,一般用来实现地理级别的均衡。例如,同样是www.baidu.com,北方用户解析后获取的地址是61.135 .165.224 (这是北京机房的IP ),南方用户解析后获取的地址是14 .2 15.177.38 (这是深圳机房的IP ) 。
优点 | 缺点 |
---|---|
简单、成本低: 负载均衡工作交给DNS 服务器处理,无须自己开发或维护负载均衡设备。就近访问,提升访问速度: DNS 解析时可以根据请求来源IP ,解析成距离用户最近的服务器地址,可以加快访问速度, 改善性能。 | 更新不及时: DNS 缓存的时间比较长,修改DNS 配置后,由于缓存的原因,还是有很多用户会继续访问修改前的IP ,这样的访问会失败,达不到负载均衡的目的, 并且也影响用户正常使用业务。扩展性差: DNS 负载均衡的控制权在域名商那里, 无法根据业务特点针对其做更多的定制化功能和扩展特性。分配策略比较简单: DNS 负载均衡支持的算法少:不能区分服务器的差异(不能根据系统与服务的状态来判断负载);也无法感知后端服务器的状态。 |
硬件负载均衡
目前业界典型的硬件负载均衡设备有两款: F5和A10。这类设备和路由器交换机类似,可以理解为一个用于负载均衡的基础网络设备。
优点 | 缺点 |
---|---|
功能强大: 全面支持各层级的负载均衡,支持全面的负载均衡算法,支持全局负载均衡。性能强大: 对比一下,软件负载均衡支持到10 万级井发己经很厉害了,硬件负载均衡可以支持100 万以上的并发。稳定性高: 商用硬件负载均衡,经过了良好的严格测试,经过大规模使用, 在稳定性方面高。支持安全防护: 硬件均衡设备除具备负载均衡功能外,还具备防火墙、防DDOS 攻击等安全功能。 | 价格昂贵: 最普通的一台FS 就是一台“马6 气好一点的就是“宝马、Q7 ”了;扩展能力差:硬件设备,可以根据业务进行配置, 但无法进行扩展和定制。 |
软件负载均衡
软件负载均衡通过负载均衡软件来实现负载均衡功能, 常见的有Nginx 和LVS , 其中Nginx是软件的7 层负载均衡,LVS 是Linux 内核的4 层负载均衡。4 层和7 层的区别就在于协议和灵活性。Nginx 支持HTTP 、E-mail 协议, 而LVS 是4 层负载均衡,和协议无关,几乎所有应用都可以做,例如,聊天、数据库等。
优点 | 缺点 |
---|---|
简单:无论部署,还是维护都比较简单。 | 性能一般: 一个Nginx 大约能支撑5 万并发。 |
便宜:只要买个Linux 服务器,装上软件即可。 | 功能没有硬件负载均衡那么强大。 |
灵活: 4 层和7 层负载均衡可以根据业务进行选择;也可以根绝业务进行比较方便的扩展,例如,可以通过Nginx 的插件来实现业务的定制化功能。 | 一般不具备防火墙和防DDOS 攻击等安全功能。" |
负载均衡架构
前面我们介绍了3 种常见的负载均衡机制: DNS 负载均衡、硬件负载均衡、软件负载均衡,每种方式都有一些优缺点,但并不意味着在实际应用中只能基于它们的优缺点进行非此即彼的选择,反而是基于它们的优缺点进行组合使用。具体来说,组合的基本原则为:
-
DNS 负载均衡用于实现地理级别的负载均衡;
-
硬件负载均衡用于实现集群级别的负载均衡;
-
软件负载均衡用于实现机器级别的负载均衡。
整个系统的负载均衡分为三层。
-
地理级别负载均衡: www.xxx .com 部署在北京、广州、上海三个机房,当用户访问时,DNS 会根据用户的地理位置来决定返回哪个机房的IP ,图中返回了广州机房的IP 地址,这样用户就访问到广州机房了。
-
集群级别负载均衡:广州机房的负载均衡用的是FS 设备, FS 收到用户请求后,进行集群级别的负载均衡,将用户请求发给3 个本地集群中的一个,我们假设FS 将用户请求发给了“广州集群2 ”。
-
机器级别的负载均衡:广州|集群2 的负载均衡用的是Nginx, Nginx 收到用户请求后,将用户请求发送给集群里面的某台服务器,服务器处理用户的业务请求井返回业务响应。
负载均衡的算法
负载均衡算法数量较多,而且可以根据-些业务特性进行定制开发,抛开细节上的差异,根据算法期望达到的目的,大体上可以分为如下几类。
-
任务平分类: 负载均衡系统将收到的任务平均分配给服务器进行处理,这里的“平均”可以是绝对数量的平均,也可以是比例或权重上的平均。
-
负载均衡类: 负载均衡系统根据服务器的负载来进行分配,这里的负载井不一定是通常意义上我们说的接数、I/O使用率、网卡吞吐量等来衡量系统的压力。
-
性能最优类: 负载均衡系统根据服务器的响应时间来进行任务分配, 优先将新任务分配给响应最快的服务器。
-
Hash 类:负载均衡系统根据任务中的某些关键信息进行Hash运算,将相同Hash值的请求分配到同一台服务器上。常见的有源地址Hash 、目标地址hash 、session_id hash、user_id hash 等。
轮询
将请求按顺序轮流地分配到每个节点上,不关心每个节点实际的连接数和当前的系统负载。
优点 | 缺点 |
---|---|
简单高效,易于水平扩展,每个节点满足字面意义上的均衡; | 没有考虑机器的性能问题,根据木桶最短木板理论,集群性能瓶颈更多的会受性能差的服务器影响。 |
加权轮询
不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。
优点 | 缺点 |
---|---|
可以将不同机器的性能问题纳入到考量范围,集群性能最大化。 | 生产环境复杂多变,服务器抗压能力也无法精确估算,静态算法导致无法实时动态调整节点权重,只能粗糙优化。 |
最小连接数法
根据每个节点当前的连接情况,动态地选取其中当前积压连接数最少的一个节点处理当前请求,尽可能地提高后端服务的利用效率,将请求合理地分流到每一台服务器。俗称闲的人不能闲着,大家一起动起来。
优点 | 缺点 |
---|---|
动态,根据节点状况实时变化; | 提高了复杂度,每次连接断开需要进行计数; |
最快响应速度法
根据请求的响应时间,来动态调整每个节点的权重,将响应速度快的服务节点分配更多的请求,响应速度慢的服务节点分配更少的请求,俗称能者多劳,扶贫救弱。
优点 | 缺点 |
---|---|
动态,实时变化,控制的粒度更细,跟灵敏; | 复杂度更高,每次需要计算请求的响应速度; |
Hash 类
以源地址哈希为例,源地址哈希的思想是根据客户端的IP地址,通过哈希函数计算得到一个数值,用该数值对服务器节点数进行取模,得到的结果便是要访问节点序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会落到到同一台服务器进行访问。
优点 | 缺点 |
---|---|
相同的IP每次落在同一个节点,可以人为干预客户端请求方向,例如灰度发布; | 如果某个节点出现故障,会导致这个节点上的客户端无法使用,无法保证高可用。 |
参考
https://book.douban.com/subject/30335935/
https://www.51cto.com/article/596022.html
https://xiaolincoding.com/redis/cluster/cache_problem.html