架构学习-高性能架构模式

高性能架构模式,本文会总结业界相对比较成熟的各种架构模式,大部分情况下,我们会基于这些已有的成熟模式,结合业务和团队的具体情况来进行一定的优化或调整。

很多情况下高性能的设计最核心的部分就是关系数据库的设计。单个数据库在当前情况下是很难满足业务需求的了,必须考虑数据库集群的方式来提升性能。高性能架构,其关键点就在于数据库层如何实现高性能,有很多种方式,先来介绍读写分离原理。

1. 高性能数据库集群 - 读写分离

读写分离的基本原理就是将数据库读写操作分散到不同的节点上。
fig1.png

1.1 基本实现 - 主从集群

  • 数据库服务器搭建主从集群,一主一从到一主多从皆可。
  • 数据库主机负责读写操作,从机只负责读操作
  • 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据
  • 业务服务器将写操作发给数据库主机,将读操作发给数据库从机

注意这里实现的主从集群,而不是主备集群,主从,从属的还是要接收请求的,主备中的备是完全的备用目的,基本不会有流量。

1.2 主从复制延迟

以 MySQL 为例,主从复制延迟可能达到 1 秒,如果有大量数据同步,延迟 1 分钟也是有可能的。主从复制延迟会带来一个问题:如果业务服务器将数据写入到数据库主服务器后立刻(1 秒内)进行读取,此时读操作访问的是从机,主机还没有将数据复制过来,到从机读取数据是读不到最新数据的,业务上就可能出现问题。例如,用户刚注册完后立刻登录,业务服务器会提示他“你还没有注册”,而用户明明刚才已经注册成功了。

解决复制延迟的常见方法:

  • 写操作以后的读操作指定发给数据库主服务器
    • 和业务强绑定,容易发生bug
  • 读从机失败之后再读一次主机
    • 二次读取和业务无绑定,只需要对底层数据库访问的API进行封装即可,实现代价小
    • 不足之处是如果有大量二次读取,那么主机压力会很大,可能会导致崩溃
    • 关键业务读写操作全部指向主机,非关键业务采用读写分离

1.3 分配机制 - 如何区分读写操作,访问不同的数据库服务器

1.3.1 程序代码封装

指在代码中抽象出一个数据访问层,实现读写分离的操作和数据库服务器的连接管理。例如基于Hibernate进行简单封装

fig2.png

  • 实现简单,可以根据业务做较多的定制化功能
  • 每个编程语言都要自己实现一次,没法通用,如果一个业务包含多个编程语言的多个子系统,那么重复开发工作量比较大
  • 故障情况下,如果主从切换,那么所有系统都很可能需要修改配置并重启

1.3.2 中间件封装

指独立出一套系统来,实现读写操作分离和数据库服务器连接的管理。中间件对业务服务器提供SQL兼容的协议,业务服务器无须自己进行读写分离。对于业务服务器来说,访问中间件

fig3.png

  • 能够支持多种编程语言,因为中间件对业务服务器提供的是标准SQL接口
  • 中间件要支持完整的SQL语法和数据库服务器的协议,实现复杂,bug会比较多,需要较长时间才可以稳定下来
  • 中间件自己不执行读写,但是所有数据库的操作请求都要经过中间件,所以对于性能有很高的要求
  • 数据库主从切换对业务服务器无感知,数据库中间件可以探测数据库服务器的主从状态

现在市面上有的中间件,MySQL Router, Atlas

2. 分库分表

读写分离可以分散数据库读写操作的压力,但没有分散存储压力,当数据量达到千万级别的时候,单台数据库服务器的存储能力会成为系统的瓶颈, 这体现在:

  • 数据量过大,读写性能会下降;即使有索引,索引也会变大,性能同样会下降
  • 数据文件会变得很大,数据库北非和恢复需要耗费很长时间
  • 数据文件越大,极端情况下丢失数据的风险就越高

基于上述原因,单个数据库服务器存储的数据量不能太大,需要控制在一定的范围内。将存储分散到多台数据库服务器上。

2.1 业务分库

业务分库指的是按照业务模块将数据分散到不同的数据库服务器。

譬如一个电商网站,将用户数据,商品数据,订单数据分开放到三台不同的数据库服务器上。虽然业务分库能够分散存储和访问的压力,但是也带来了新的问题。

2.1.1 join操作问题

原先在一个表里的数据现在分散到了多个表当中,这就导致无法使用SQL的join来进行查询了。

2.1.2 事务问题

原本在同一个数据库中不同的表可以在同一个事务中修改,业务分库以后,表分散到了不同的数据库当中,无法通过事务统一修改。 尽管有一个分布式事务的解决方案,但性能太低,与高性能的存储的目标是相违背的。

2.1.3 成本问题

一台变多台

2.2 业务分表

将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,但如果业务继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。例如,淘宝的几亿用户数据,如果全部存放在一台数据库服务器的一张表中,肯定是无法满足性能要求的,此时就需要对单表数据进行拆分。

单表数据的拆分有两种方式: 垂直分表和水平分表

2.2.1 垂直分表

原先比如是id, name, age, sex, nickname, 垂直分表以后可以变成

  • id, name, age
  • id, sex, nickname

也会带来复杂性,即表操作的数量要增加,原来的一次查询现在要变成多次查询了

2.2.2 水平分表

用2.2.1的例子来说明的话,会包含一样的attributes,只是总共行数变le

水平分表以后,某条数据具体属于哪个切分以后的子表,需要依靠路由算法来进行计算

  • 范围路由
    • 使用用户ID等分段
    • 好处
      • 随数据增加平滑的扩充新表
    • 坏处
      • 分布不均
  • Hash路由
    • 选取某个列的值进行Hash运算,然后根据Hash结果分散到不同的数据表当中
    • 复杂点
      • 初始表的数量选取,太多维护麻烦,太少可能会让单表性能存在问题
      • 用了Hash以后重Hash(增加表数量)会非常麻烦
    • 好处 分布均匀
  • 配置路由
    • 路由表,用独立的表来记录路由信息
    • 缺点就是会多查询一次,先看了一眼路由表嘛

3. 高性能NoSQL

关系型数据库存在问题:

  • 关系数据库存储的是行记录,无法存储数据结构
  • 关系数据库的schema扩展不方便
    • schema是强约束的,操作不存在的列会报错,业务变化时扩充列也会比较麻烦,需要执行DDL (data definition language create, alter, drop等) 语句修改,而且修改时可能会长时间锁表
  • 关系数据库在大数据场景下I/O较高
    • 对于每一行数据量都很大的表做统计之类的运算的时候I/O会很高,因为即使只针对其中某一列进行运算,关系数据库也会将整行数据从存储设备读入内存。
  • 关系数据库的全文搜索功能比较弱

针对上面的问题,产生了不同的NoSQL解决方案,在某一方面会有更好的表现。此外,NoSQL的方案带来的优势,本质上是牺牲ACID中的某个或者某几个特性,因此我们不能盲目地迷信NoSQL是银弹,应该将NoSQl作为SQL的一个有力补充。

常见NoSQL分类:

  • K-V 存储: 解决SQL无法存储数据结构的问题
  • 文档数据库:解决SQL强schema约束的问题
  • 列式数据库:解决SQL大数据场景下的I/O问题,以HBase为代表
  • 全文搜索引擎:解决关系数据库的全文搜索性能问题, ElasticSearch

3.1 Key - Value存储

Redis 是 K-V 存储的典型代表,它是一款开源(基于 BSD 许可)的高性能 K-V 缓存和存储系统。Redis 的 Value 是具体的数据结构,包括 string、hash、list、set、sorted set、bitmap 和 hyperloglog,所以常常被称为数据结构服务器。

更灵活的对数据的操作:

  • LPOP key 从队列的左边出队一个元素。
  • LINDEX key index 获取一个元素,通过其索引列表。
  • LLEN key 获得队列(List)的长度。

Redis 的缺点主要体现在并不支持完整的 ACID 事务,Redis 虽然提供事务功能,但 Redis 的事务和关系数据库的事务不可同日而语,Redis 的事务只能保证隔离性和一致性(I 和 C),无法保证原子性和持久性(A 和 D)。

3.2 文档数据库

最大特点是no-schema,可以存储和读取任意的数据。带来优势:

  • 新增字段简单
  • 历史数据不会出错
  • 可以很容易存储复杂数据

缺陷

  • 事务
  • 没有join操作

3.3 列式数据库

按照列来存储数据,典型场景海量数据统计,只需要其中的一两列的数据即可。I/O相对低一些。

列式存储有更高的存储压缩比,因为单个列的数据相似度一般来说比行更高,能够达到更高的压缩率。

一般将其用在离线的大数据分析和统计的场景当中,因为这种场景经常是针对部分单列来进行操作的,数据写入以后无须更新删除。

3.4 全文搜索引擎

  • 全文搜索的条件可以随意排列组合,如果通过索引来满足,则索引的数量会非常多
  • 全文搜索的模糊匹配方式,索引无法满足,只能用like,而like是整表扫描,很慢

3.4.1 基本原理

  • 倒排索引/ 反向索引
    • 建立单词到文档的索引
    • 即用关键词来查,显示出出现的地方

3.4.2 使用方式

将数据库里面的内容转成JSON格式,然后输入全文搜索引擎进行搜索。 ES能够以实时化的方式,存储和检索复杂的数据结构,并令每个字段都默认可以被索引。

4. 高性能缓存结构

存储系统的能力有的时候并不够用,比如

  • 需要经过复杂运算得出的数据 – 比如实时在线人数
  • 读多写少的数据

缓存就是为了弥补存储系统在这些复杂业务场景下的不足,其基本原理就是将可能重复使用的数据放到内存当中,一次生成,多次使用,避免每次使用都去访问存储系统。

缓存能够带来性能的大幅提升,以memcache为例,单台就可以达到TPS50000以上,基本架构就是第一次从数据库拿数据,第二次及以后就可以从memcached中来取得数据了。

4.1 缓存穿透

指缓存没有发挥作用,虽然去查询了缓存数据,但是不在那里面,业务系统就需要再次去存储系统查询数据,这种情况的出现通常是因为:

  • 存储数据不存在

黑客攻击,故意大量访问某些读取不存在数据的业务。解决方案就是设置一个默认值,放到缓存里面,这样第二次读取缓存的时候就会获取默认值,而不会继续访问存储系统。

  • 缓存数据生成耗费大量的时间或者资源

第二种情况是存储系统中存在数据,但生成缓存数据需要耗费较长时间或者耗费大量资源。如果刚好在业务访问的时候缓存失效了,那么也会出现缓存没有发挥作用,访问压力全部集中在存储系统上的情况。

典型的就是电商的商品分页,假设我们在某个电商平台上选择“手机”这个类别查看,由于数据巨大,不能把所有数据都缓存起来,只能按照分页来进行缓存,由于难以预测用户到底会访问哪些分页,因此业务上最简单的就是每次点击分页的时候按分页计算和生成缓存。通常情况下这样实现是基本满足要求的,但是如果被竞争对手用爬虫来遍历的时候,系统性能就可能出现问题。

4.2 缓存雪崩

是指当缓存失效(过期)后引起系统性能急剧下降的情况。当缓存过期被清除后,业务系统需要重新生成缓存,因此需要再次访问存储系统,再次进行运算,这个处理步骤耗时几十毫秒甚至上百毫秒。而对于一个高并发的业务系统来说,几百毫秒内可能会接到几百上千个请求。由于旧的缓存已经被清除,新的缓存还未生成,并且处理这些请求的线程都不知道另外有一个线程正在生成缓存,因此所有的请求都会去重新生成缓存,都会去访问存储系统,从而对存储系统造成巨大的性能压力。这些压力又会拖慢整个系统,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。

为了解决,通常可以采用两种方案: 更新锁机制和后台更新机制。

4.2.1 更新锁

对缓存更新操作进行加锁保护,保证只有一个线程能够进行缓存更新,未能获取更新锁的线程要么等待锁释放以后重新读取缓存,要么就返回空值或者默认值。

对于采用分布式集群的业务系统,由于存在几十上百台服务器,即使单台服务器只有一个线程更新缓存,但几十上百台服务器一起算下来也会有几十上百个线程同时来更新缓存,同样存在雪崩的问题。因此分布式集群的业务系统要实现更新锁机制,需要用到分布式锁,如 ZooKeeper。

4.2.2 后台更新

由后台线程来更新缓存,而不是业务线程。缓存本身的有效期设置为永久,后台线程定时更新缓存。

后台定时机制需要考虑一种特殊的场景,当缓存系统内存不够时,会“踢掉”一些缓存数据,从缓存被“踢掉”到下一次定时更新缓存的这段时间内,业务线程读取缓存返回空值,而业务线程本身又不会去更新缓存,因此业务上看到的现象就是数据丢了。解决的方式有两种:

  • 后台线程除了定时更新缓存,还要频繁地去读取缓存(例如,1 秒或者 100 毫秒读取一次),如果发现缓存被“踢了”就立刻更新缓存,这种方式实现简单,但读取时间间隔不能设置太长,因为如果缓存被踢了,缓存读取间隔时间又太长,这段时间内业务访问都拿不到真正的数据而是一个空的缓存值,用户体验一般。
  • 业务线程发现缓存失效后,通过消息队列发送一条消息通知后台线程更新缓存。可能会出现多个业务线程都发送了缓存更新消息,但其实对后台线程没有影响,后台线程收到消息后更新缓存前可以判断缓存是否存在,存在就不执行更新操作。这种方式实现依赖消息队列,复杂度会高一些,但缓存更新更及时,用户体验更好。

4.3 缓存热点

复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器的压力。

注意不同的缓存副本不要设置统一的过期时间,否则会出现所有缓存副本同时生成同时失效的情况,从而引发缓存雪崩效应。正确的做法是设定一个过期的时间范围,不同的缓存副本的过期时间是指定范围内的随机值。

5. 单服务器高性能模式 - 并发模型

  • 影响高性能效果的因素
    • 磁盘
    • 操作系统
    • CPU
    • 内存
    • 缓存
    • 网络
    • 编程语言
    • 架构

高性能架构设计主要集中在两个方面:

  • 尽量提升单服务器的性能,将单服务器的性能发挥到极致
  • 如果单服务器无法支撑性能,设计服务器集群方案
  • 关键设计点

    • 服务器如何管理连接
    • 服务器如何处理请求
  • 常见分类

    • 海量连接海量请求:双十一
    • 常量连接海量请求:中间件
    • 海量连接常量请求:门户网站
    • 常量连接常量请求:内部运营系统,管理系统
      这两个设计点都和操作系统的I/O模型以及进程模型相关。
  • BIO: 一个线程处理一个请求

  • NIO:利用多路复用技术,通过少量的线程处理大量的请求

  • I/O 模型

    • 阻塞
    • 非阻塞
    • 同步
    • 异步
  • 进程模型

    • 单进程
    • 多进程
    • 多线程

5.1 PPC - Process Per Connection

每次有新的连接就新建一个进程去专门处理这个连接的请求,这也是传统的UNIX网络服务器锁采用的模型。

fig4.png

  • 父进程接受连接
  • 父进程fork子进程
  • 子进程处理连接的读写请求
  • 子进程关闭连接

PPC 模式实现简单,比较适合服务器的连接数没那么多的情况,例如数据库服务器。对于普通的业务服务器,在互联网兴起之前,由于服务器的访问量和并发量并没有那么大,这种模式其实运作得也挺好,世界上第一个 web 服务器 CERN httpd 就采用了这种模式。但随着互联网星期,服务器的并发量和访问量都有了很大的提高,这种方法就涌现出了不少弊端

  • fork代价比较高,要分配很多内核资源,需要将内存映像从父进程复制到子进程。
  • 父子进程通信复杂,父进程fork子进程时,文件描述符可以通过内存映像复制从父进程传到子进程,但fork完成之后,父子进程通信就比较麻烦了,需要采用IPC(Interprocess Communication)之类的进程通信方案。
  • 支持的并发连接数量有限,如果每个连接存活时间比较长,而且新的连接又源源不断的来,则进程数量会越来越多,操作系统进程调度和切换的频率也会越来越高,系统的压力也会越来越大。一般来说,PPC方案最大的并发连接数就几百的样子。

5.2 prefork 提前创建进程

在启动的时候就预先创建好进程,然后开始接受用户的请求。当有新的连接进来的时候,就可以省去fork进程的操作,让用户访问更快,体验更好。

fig5.jpg

实现关键点在多个子进程都accept同一个socket,当有新的连接接入时,操作系统保证只有一个进程能最后accept成功。

prefork还是和PPC一样,存在父子进程通信复杂,支持的并发连接数量有限的问题。

5.3 TPC - Thread Per Connection

每次有新的连接,就建立一个新的线程去专门处理这个连接的请求。与进程相比,线程更轻量级,创建线程的消耗比进程要少得多;同时因为多线程共享进程内存空间,就可以简化线程之间通信的复杂程度。

fig6.png

  • 父进程接受连接
  • 父进程创建子线程
  • 子线程处理连接的读写请求
  • 子线程关闭连接

和PPC相比,主进程不用close了,原因是子线程是共享主进程的进程空间的,连接的文件描述符并没有被复制,因此只需要一次close即可。

TPC引入了新的问题:

  • 创建线程依然有耗损,性能问题
  • 线程间的互斥和共享的复杂度
  • 多线程会互相影响,某个线程出现异常的时候,可能导致整个进程的退出

在并发几百的情况下,还是会更多采用PPC的方案,因为无死锁的风险,也不会有多进程之间的相互影响,稳定性更高。

5.4 prethread

预先创建线程,然后才开始接受用户的请求,当有新的连接进来的时候,就可以省去创建线程的操作,让用户感觉更快。常用的实现方式:

  • 主进程accept,然后将连接交给某个线程处理
  • 子线程都尝试去accept,最终只有一个线程accept成功

fig7.jpg

5.5 Reactor

PPC模式的问题是每个连接都要创建进程,连接结束进程就被销毁了。为了解决这个问题,瞄准资源复用,即不再单独为每个连接创建进程,而是创建一个进程池,将连接分配给进程,一个进程可以处理多个连接的业务。

一个进程处理多个连接的业务

引入资源池的处理方式后,会引出一个新的问题:进程如何才能高效地处理多个连接的业务?当一个连接一个进程时,进程可以采用“read -> 业务处理 -> write”的处理流程,如果当前连接没有数据可以读,则进程就阻塞在 read 操作上。这种阻塞的方式在一个连接一个进程的场景下没有问题,但如果一个进程处理多个连接,进程阻塞在某个连接的 read 操作上,此时即使其他连接有数据可读,进程也无法去处理,很显然这样是无法做到高性能的。

为了解决这个问题,可以将read操作改为非阻塞的,然后进程不断轮询多个连接。但是这样做,不好的地方在于轮询耗费CPU资源;其次,如果一个进程处理成千上万的连接,轮询的效率是很低的。

更好的解决办法,只有当连接上有数据的时候进程才去处理,这就是I/O多路复用的技术的来源

  • 当多条连接共用一个阻塞对象后,进程只需要在一个阻塞对象上等待,而无须再轮询所有连接,常见的连接方式有select\epoll\kqueue等。
  • 当某条连接有新的数据可以处理时,操作系统会通知进程,进程从阻塞状态返回,开始进行业务处理

I/O多路复用结合线程池,就是我们说的Reactor了,即事件反应的意思

Reactor会根据事件类型来调用相应的代码进行处理,也成为dispatcher模式,指的是I/O多路复用统一监听事件,收到事件后分配给某个进程的过程。

  • Reactor模式的benefits
    • reactor数量可以变化
    • 资源池的数量可以变化: 以进程为例,可以是单进程的,也可以是多个进程的
  • 常见的使用方式
    • 单 Reactor 单进程 / 线程。
    • 单 Reactor 多线程。
    • 多 Reactor 多进程 / 线程。

上述方案选择进程还是线程,更多和平台以及编程语言相关。例如Java一般使用线程 - Netty, Nginx选择进程

5.5.1 单Reactor 单进程/ 线程

fig8.png

  • select, accept, read, send是标准的网络编程API
  • dispatch和业务处理是需要完成的操作
  • Reactor对象通过select监控连接时间,收到事件以后通过dispatch来进行转发
  • 如果是连接建立事件,就交给Acceptor,来接受连接,并创建一个Handler来处理接下来的各种事件
  • 如果不是连接事件,就会用上面已经建好的handler来处理请求,做出响应
  • Handler会完成read - 业务处理 -send的完整业务流程
  • Benefits
    • 无进程间通信
    • 无进程竞争
  • Weakness
    • 只有一个进程,无法发挥出多核CPU的性能
    • handler在处理某个连接上的业务时,整个进程就无法处理

只适用于业务处理非常快速的场景,目前比较著名的使用这个的开源软件是Redis

5.5.2 单Reactor 多线程

fig9.png

  • 主线程当中,Reactor对象通过select监控连接时间,收到事件后通过dispatch进行分发
  • blabla… similar to above
  • Handler只负责响应事件,不进行业务处理;Handler通过read读取到数据后,会发给processor进行业务处理
  • Processor会在独立的子线程当中完成真正的业务处理,然后将响应结果发给主进程的Handler处理;Handler收到响应以后通过send将响应结果返回给client
  • Benefits
    • 能够充分利用多核多CPU的处理能力
  • Weakness
    • 多线程数据共享和访问比较复杂
    • reactor承担所有事件的监听和响应,只在主线程中进行,瞬间高并发会成为性能瓶颈

5.5.3 多Reactor 多进程/ 线程

fig10.png

  • 父进程中mainReactor对象通过select监控连接建立事件,收到事件后通过Acceptor接收,将新的连接分配给某个子进程。
  • 子进程的subReactor将mainReactor分配的连接加入连接队列进行监听,并创建一个Handler用于处理连接的各种事件
  • 当有新的事件发生时,subReactor会调用连接对应的handler来进行响应
  • handler完成read - 业务处理 -send的流程
  • Benefits
    • 父进程和子进程的职责非常明确,父进程只负责接收新连接,子进程负责完成后续的业务处理。
    • 父进程和子进程的交互很简单,父进程只需要把新连接传给子进程,子进程无须返回数据。
    • 子进程之间是互相独立的,无须同步共享之类的处理(这里仅限于网络模型相关的 select、read、send 等无须同步共享,“业务处理”还是有可能需要同步共享的)。

5.6 Proactor

Reactor是非阻塞同步网络模型,因为真正的read和send操作都需要用户进程同步操作,proactor将其异步化,

fig11.png

  • 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 到内核进程。

5.7 同步阻塞IO vs 同步非阻塞IO vs 异步非阻塞IO

- 等待数据准备好的阶段(读到内核缓存) 将数据从内核读到用户空间
同步阻塞IO 阻塞 阻塞
同步非阻塞IO 非阻塞 阻塞
异步非阻塞IO 非阻塞 非阻塞

6. 高性能负载均衡 - 分类及架构 - 高性能集群

高性能集群的本质: 通过增加更多的服务器来提升系统整体的计算能力。

由于计算本身的特点,即同样的输入数据和逻辑,无论在哪台服务器上执行,都应该得到相同的输出。因此高性能集群设计的复杂度主要体现在任务分配这部分,需要设计合理的任务分配策略,将计算任务分配到多台服务器上来执行。

即复杂性主要体现在需要增加一个任务分配器,以及为任务选择一个合适的任务分配算法。

  • 任务分配算法的考虑因素
    • 计算单元的负载均衡
    • 基于负载考虑
    • 基于性能(吞吐量、响应时间)考虑
    • 基于业务考虑

6.1 负载均衡分类

6.1.1 DNS负载均衡

用于实现地理级别的均衡,其原理是DNS解析同一个域名,可以返回不同的IP地址。
fig12.jpg

  • Benefits
    • 简单,成本低
    • 负载均衡工作交给DNS服务器来处理,无须自己开发或者维护负载均衡设备
    • 就近访问,提升访问速度
  • weakness
    • 更新不及时,DNS缓存时间比较长,更新缓存以后,还有很多用户会访问修改前的IP,这样的访问会失败的
    • 扩展性差,DNS负载均衡控制权在域名商那里,无法根据业务特点针对其做更多的定制化功能和特性的拓展
    • 分配策略比较简单: DNS负载均衡的支持算法少,不能区分服务器的差异(不能根据系统与服务的状态来判断负载),也无法感知后端服务器的状态。

6.1.2 硬件负载均衡

指通过单独的硬件设备来实现负载均衡的功能,类似路由器交换机,可以理解为一个用于负载均衡的基础网络设备。

  • benefits

    • 功能强大,支持各层级负载均衡
    • 性能强大,100万以上的并发
    • 稳定性高
    • 支持安全防护 DDos
  • weakness

    • 价格昂贵
    • 扩展能力差

6.1.3 软件负载均衡

通过负载均衡软件来实现负载均衡的功能,常见的有Nginx - 软件的7层负载均衡和LVS - Linux内核的4层负载均衡。

Nginx支持HTTP,Email协议; 而LVS是4层负载均衡,和协议无关,几乎所有的应用都可以做。

软硬件最主要的区别在于性能,硬件负载均衡性能要远远高于软件的复杂均衡性能,但是软件的会便宜很多。

fig13.jpg

  • benefits
    • 部署维护简单
    • 便宜
    • 灵活,可扩展

6.1.4 负载均衡的典型架构

是结合起来一起用的,DNS负载均衡用于实现地理级别的负载均衡,硬件负载均衡用于实现集群级别的负载均衡,软件负载均衡用于实现机器级别的负载均衡。

整个系统的负载均衡分为三层:

  • 地理级别负载均衡
  • 集群级别负载均衡 用硬件设备来做平均
  • 机器级别的负载均衡 用nginx,收到用户的请求之后,将用户的请求发送给集群里面的某台服务器,服务器处理用户的业务请求并返回业务响应

6.2 高性能负载均衡 - 算法

6.2.1 分类

  • 任务平分类:负载均衡系统将收到的任务平均分配给服务器进行处理,这里的“平均”可以是绝对数量的平均,也可以是比例或者权重上的平均。
  • 负载均衡类:负载均衡系统根据服务器的负载来进行分配,这里的负载并不一定是通常意义上我们说的“CPU 负载”,而是系统当前的压力,可以用 CPU 负载来衡量,也可以用连接数、I/O 使用率、网卡吞吐量等来衡量系统的压力。
  • 性能最优类:负载均衡系统根据服务器的响应时间来进行任务分配,优先将新任务分配给响应最快的服务器。
  • Hash 类:负载均衡系统根据任务中的某些关键信息进行 Hash 运算,将相同 Hash 值的请求分配到同一台服务器上。常见的有源地址 Hash、目标地址 Hash、session id hash、用户 ID Hash 等。

6.2.2 轮询

负载均衡系统收到请求后,按照顺序轮流分配到服务器上。

需要注意的是负载均衡系统无须关注“服务器本身状态”,这里的关键词是“本身”。也就是说,只要服务器在运行,运行状态是不关注的。但如果服务器直接宕机了,或者服务器和负载均衡系统断连了,这时负载均衡系统是能够感知的,也需要做出相应的处理。例如,将服务器从可分配服务器列表中删除,否则就会出现服务器都宕机了,任务还不断地分配给它,这明显是不合理的。

6.2.3 加权轮询

负载均衡系统根据服务器权重进行任务分配,这里的权重一般是根据硬件配置进行静态配置的,采用动态的方式计算会更加契合业务,但复杂度也会更高。加权轮询主要为了解决不同服务器的处理能力有差异的问题。

6.2.4 负载最低优先

从服务器的角度出发来看如何进行负载分配

负载均衡系统将任务分配给当前负载最低的服务器,这里的负载根据不同的任务类型和业务场景,可以用不同的指标来衡量。例如:

  • LVS 这种 4 层网络负载均衡设备,可以以“连接数”来判断服务器的状态,服务器连接数越大,表明服务器压力越大。
  • Nginx 这种 7 层网络负载系统,可以以“HTTP 请求数”来判断服务器状态(Nginx 内置的负载均衡算法不支持这种方式,需要进行扩展)。
  • 如果我们自己开发负载均衡系统,可以根据业务特点来选择指标衡量系统压力。如果是 CPU 密集型,可以以“CPU 负载”来衡量系统压力;如果是 I/O 密集型,可以以“I/O 负载”来衡量系统压力。

负载最低优先算法解决了轮询算法中无法感知服务器状态的问题,但复杂度会增加很多。

  • 最少连接数优先的算法要求负载均衡系统统计每个服务器当前建立的连接,其应用场景仅限于负载均衡接收的任何连接请求都会转发给服务器进行处理,否则如果负载均衡系统和服务器之间是固定的连接池方式,就不适合采取这种算法。例如,LVS 可以采取这种算法进行负载均衡,而一个通过连接池的方式连接 MySQL 集群的负载均衡系统就不适合采取这种算法进行负载均衡。
  • CPU 负载最低优先的算法要求负载均衡系统以某种方式收集每个服务器的 CPU 负载,而且要确定是以 1 分钟的负载为标准,还是以 15 分钟的负载为标准,不存在 1 分钟肯定比 15 分钟要好或者差。不同业务最优的时间间隔是不一样的,时间间隔太短容易造成频繁波动,时间间隔太长又可能造成峰值来临时响应缓慢。

6.2.5 性能最优类

从客户端的角度,和负载最低优先类算法类似,性能最优优先类算法本质上也是感知了服务器的状态,只是通过响应时间这个外部标准来衡量服务器状态而已。因此性能最优优先类算法存在的问题和负载最低优先类算法类似,复杂度都很高,主要体现在:

  • 负载均衡系统需要收集和分析每个服务器每个任务的响应时间,在大量任务处理的场景下,这种收集和统计本身也会消耗较多的性能。
  • 为了减少这种统计上的消耗,可以采取采样的方式来统计,即不统计所有任务的响应时间,而是抽样统计部分任务的响应时间来估算整体任务的响应时间。采样统计虽然能够减少性能消耗,但使得复杂度进一步上升,因为要确定合适的采样率
  • 采样周期,要10s性能最优还是1min性能最优

6.2.6 Hash类

负载均衡系统根据任务中的某些关键信息进行 Hash 运算,将相同 Hash 值的请求分配到同一台服务器上,这样做的目的主要是为了满足特定的业务需求。

  • 源地址Hash
    • 将来源于同一个源 IP 地址的任务分配给同一个服务器进行处理,适合于存在事务、会话的业务。例如,当我们通过浏览器登录网上银行时,会生成一个会话信息,这个会话是临时的,关闭浏览器后就失效。网上银行后台无须持久化会话信息,只需要在某台服务器上临时保存这个会话就可以了,但需要保证用户在会话存在期间,每次都能访问到同一个服务器,这种业务场景就可以用源地址 Hash 来实现。
  • ID Hash
    • 将某个 ID 标识的业务分配到同一个服务器中进行处理,这里的 ID 一般是临时性数据的 ID(如 session id)。例如,上述的网上银行登录的例子,用 session id hash 同样可以实现同一个会话期间,用户每次都是访问到同一台服务器的目的。

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 stone2paul@gmail.com

文章标题:架构学习-高性能架构模式

文章字数:10k

本文作者:Leilei Chen

发布时间:2020-02-04, 12:25:06

最后更新:2020-02-04, 12:27:50

原始链接:https://www.llchen60.com/%E6%9E%B6%E6%9E%84%E5%AD%A6%E4%B9%A0-%E9%AB%98%E6%80%A7%E8%83%BD%E6%9E%B6%E6%9E%84%E6%A8%A1%E5%BC%8F/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏