读写分离,将访问压力分散到集群中的多个节点,但是没有分散存储压力
数据库集群
读写分离,将访问压力分散到集群中的多个节点,但是没有分散存储压力
一般采用主从集群,写在主,读在从;但是引入了主从复制延迟和分配机制
主从复制延迟:主从数据同步总是有时间间隔;解决方法:
1. 写操作后的读操作指定发给数据库主服务器
2. 读从机失败后再读一次主机
3. 关键业务读写操作全部指向主机,非关键业务采用读写分离
分配机制:将读写操作区分开来,然后访问不同的数据库服务器,
1、程序代码封装,指在代码中抽象一个数据访问层(所以有的文章也称这种方式为“中间层封装”),实现读写操作分离和数据库服务器连接的管理。比如Hibernate,配置不同的数据库连接池,淘宝的 TDDL
2. 中间件封装,指的是独立一套系统出来,实现读写操作分离和数据库服务器连接的管理。比如MySQL Router,奇虎360的Atlas,Atlas 是基于 MySQL Proxy
读写分离,分散了数据库读写操作的压力,但没有分散存储压力,当数据量达到千万甚至上亿条的时候,单台数据库服务器的存储能力会成为系统的瓶颈。
分库分表,既可以分散访问压力,又可以分散存储压力
业务分库,指的是按照业务模块将数据分散到不同的数据库服务器,分散了存储带来的压力,也带来了新的问题
1.join 操作问题
2. 事务问题
3. 成本问题
分表,将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务。单表数据拆分有两种方式:垂直分表和水平分表
垂直分表适合将表中某些不常用且占了大量空间的列拆分出去,引入的复杂性主要体现在表操作的数量要增加
水平分表适合表行数特别大的表,有的公司要求单表行数超过 5000 万就必须进行分表,这个数字可以作为参考,但并不是绝对标准,关键还是要看表的访问性能,需要引入路由
范围路由:选取有序的数据列(例如,整形、时间戳等)作为路由的条件,不同分段分散到不同的数据库表中。
Hash 路由:选取某个列(或者某几个列组合也可以)的值进行 Hash 运算,然后根据 Hash 结果分散到不同的数据库表中。
配置路由:配置路由就是路由表,用一张独立的表来记录路由信息
分库分表具体的实现方式也是“程序代码封装”和“中间件封装”,但实现会更复杂。读写分离实现时只要识别 SQL 操作是读操作还是写操作,通过简单的判断 SELECT、UPDATE、INSERT、DELETE 几个关键字就可以做到,而分库分表的实现除了要判断操作类型外,还要判断 SQL 中具体需要操作的表、操作函数(例如 count 函数)、order by、group by 操作等,然后再根据不同的操作进行不同的处理。例如 order by 操作,需要先从多个库查询到各个库的数据,然后再重新 order by 才能得到最终的结果。
另外可以先做以下处理后再进行分库分表设计
1.做硬件优化,例如从机械硬盘改成使用固态硬盘,当然固态硬盘不适合服务器使用,只是举个例子2.先做数据库服务器的调优操作,例如增加索引,oracle有很多的参数调整;3.引入缓存技术,例如Redis,减少数据库压力4.程序与数据库表优化,重构,例如根据业务逻辑对程序逻辑做优化,减少不必要的查询;5.在这些操作都不能大幅度优化性能的情况下,不能满足将来的发展,再考虑分库分表,也要有预估性
NoSQL数据库
关系数据库缺点:
1、关系数据库存储的是行记录,无法存储数据结构
2、关系数据库的 schema 扩展很不方便
3、关系数据库在大数据场景下 I/O 较高
4、关系数据库的全文搜索功能比较弱
K-V 存储(redis)
Redis 的 Value 是具体的数据结构,包括 string、hash、list、set、sorted set、bitmap 和 hyperloglog,所以常常被称为数据结构服务器。
Redis 的缺点主要体现在并不支持完整的 ACID 事务,Redis 虽然提供事务功能,但 Redis 的事务和关系数据库的事务不可同日而语,Redis 的事务只能保证隔离性和一致性(I 和 C),无法保证原子性和持久性(A 和 D)。
文档数据库(mongdb)
为了解决关系数据库 schema 带来的问题,文档数据库应运而生。文档数据库最大的特点就是 no-schema,可以存储和读取任意的数据
1. 新增字段简单
2. 历史数据不会出错
3. 可以很容易存储复杂数据
它最主要的代价就是不支持事务,另外一个缺点就是无法实现关系数据库的 join 操作。
列式数据库(hbase)
列式数据库就是按照列来存储数据的数据库,与之对应的传统关系数据库被称为“行式数据库”,因为关系数据库是按照行来存储数据的。
1、业务同时读取多个列时效率高,因为这些列都是按行存储在一起的,一次磁盘操作就能够把一行数据中的各个列都读取到内存中。
2、能够一次性完成对一行中的多个列的写操作,保证了针对行数据写操作的原子性和一致性;否则如果采用列存储,可能会出现某次写操作,有的列成功了,有的列失败了,导致数据不一致。
一般将列式存储应用在离线的大数据分析和统计场景中,因为这种场景主要是针对部分列单列进行操作,且数据写入后就无须再更新删除。
全文搜索引擎(elasticsearch)
传统的关系型数据库通过索引来达到快速查询的目的,但是在全文搜索的业务场景下,索引也无能为力,主要体现在:
1、全文搜索的条件可以随意排列组合,如果通过索引来满足,则索引的数量会非常多。
2、全文搜索的模糊匹配方式,索引无法满足,只能用 like 查询,而 like 查询是整表扫描,效率非常低。
全文搜索引擎的技术原理被称为“倒排索引”(Inverted index),也常被称为反向索引、置入档案或反向档案,是一种索引方法,其基本原理是建立单词到文档的索引。
全文搜索引擎的索引对象是单词和文档,而关系数据库的索引对象是键和行,两者的术语差异很大,不能简单地等同起来。因此,为了让全文搜索引擎支持关系型数据的全文搜索,需要做一些转换操作,即将关系型数据转换为文档数据。
高性能缓存架构
虽然我们可以通过各种手段来提升存储系统的性能,但在某些复杂的业务场景下,单纯依靠存储系统的性能提升不够的,典型的场景有:
a、需要经过复杂运算后得出的数据,存储系统无能为力
b、读多写少的数据,存储系统有心无力
缓存就是为了弥补存储系统在这些复杂业务场景下的不足,其基本原理是将可能重复使用的数据放到内存中,一次生成、多次使用,避免每次使用都去访问存储系统
缓存架构设计要点:
缓存穿透
缓存穿透是指缓存没有发挥作用,业务系统虽然去缓存查询数据,但缓存中没有数据,业务系统需要再次去存储系统查询数据。通常情况下有两种情况:
1. 存储数据不存在。这种情况的解决办法比较简单,如果查询存储系统的数据没有找到,则直接设置一个默认值(可以是空值,也可以是具体的值)存到缓存中,这样第二次读取缓存时就会获取到默认值,而不会继续访问存储系统。
2. 缓存数据生成耗费大量时间或者资源。业务上最简单的就是每次点击分页的时候按分页计算和生成缓存。通常情况下这样实现是基本满足要求的,但是如果被竞争对手用爬虫来遍历的时候,系统性能就可能出现问题。没有太好的解决方案,主要还是监控处理了。
缓存雪崩
缓存雪崩是指当缓存失效(过期)后引起系统性能急剧下降的情况。当缓存过期被清除后,业务系统需要重新生成缓存,因此需要再次访问存储系统,再次进行运算,这个处理步骤耗时几十毫秒甚至上百毫秒。高并发时,都去访问存储系统,压力太大,拖慢整个系统。
常见解决方法有两种:
1. 更新锁。对缓存更新操作进行加锁保护,保证只有一个线程能够进行缓存更新,未能获取更新锁的线程要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。分布式集群的业务系统要实现更新锁机制,需要用到分布式锁
2. 后台更新。由后台线程来更新缓存,而不是由业务线程来更新缓存,缓存本身的有效期设置为永久,后台线程定时更新缓存。
后台线程除了定时更新缓存,还要频繁地去读取缓存(例如,1 秒或者 100 毫秒读取一次),如果发现缓存被“踢了”就立刻更新缓存,这种方式实现简单,但读取时间间隔不能设置太长或者业务线程发现缓存失效后,通过消息队列发送一条消息通知后台线程更新缓存。
缓存热点
虽然缓存系统本身的性能比较高,但对于一些特别热点的数据,如果大部分甚至所有的业务请求都命中同一份缓存数据,则这份数据所在的缓存服务器的压力也很大。缓存热点的解决方案就是复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力。
缓存副本设计有一个细节需要注意,就是不同的缓存副本不要设置统一的过期时间,否则就会出现所有缓存副本同时生成同时失效的情况,从而引发缓存雪崩效应。正确的做法是设定一个过期时间范围,不同的缓存副本的过期时间是指定范围内的随机值。
单服务器高性能模式
高并发需要根据两个条件划分:连接数量,请求数量。
1. 海量连接(成千上万)海量请求:例如抢购,双十一等
2. 常量连接(几十上百)海量请求:例如中间件
3. 海量连接常量请求:例如门户网站
4. 常量连接常量请求:例如内部运营系统,管理系统
性能架构设计主要集中在两方面:
1、尽量提升单服务器的性能,将单服务器的性能发挥到极致。
2、如果单服务器无法支撑性能,设计服务器集群方案。
架构设计决定了系统性能的上限,实现细节决定了系统性能的下限
单服务器高性能的关键之一就是服务器采取的并发模型,并发模型有如下两个关键设计点:
1、服务器如何管理连接。
2、服务器如何处理请求。
以上两个设计点最终都和操作系统的 I/O 模型及进程模型相关。
I/O 模型:阻塞、非阻塞、同步、异步。
进程模型:单进程、多进程、多线程。
PPC
Process Per Connection 的缩写,其含义是指每次有新的连接就新建一个进程去专门处理这个连接的请求,这是传统的 UNIX 网络服务器所采用的模型。比较适合服务器的连接数没那么多的情况,例如数据库服务器。缺点:父进程fork子进程 代价高;父子进程通信复杂;支持的并发连接数量有限。
prefork 就是提前创建进程(pre-fork)。系统在启动的时候就预先创建好进程,然后才开始接受用户的请求,当有新的连接进来的时候,就可以省去 fork 进程的操作,让用户访问更快、体验更好。还是存在父子进程通信复杂、支持的并发连接数量有限的问题
TPC
TPC 是 Thread Per Connection 的缩写,其含义是指每次有新的连接就新建一个线程去专门处理这个连接的请求。引入了新问题
创建线程虽然比创建进程代价低,但并不是没有代价,高并发时(例如每秒上万连接)还是有性能问题。无须进程间通信,但是线程间的互斥和共享又引入了复杂度,可能一不小心就导致了死锁问题。多线程会出现互相影响的情况,某个线程出现异常时,可能导致整个进程退出(例如内存越界)。prethread 模式会预先创建线程,然后才开始接受用户的请求,当有新的连接进来的时候,就可以省去创建线程的操作,让用户感觉更快、体验更好。
Reactor
Reactor 模式的核心组成部分包括 Reactor 和处理资源池(进程池或线程池),其中 Reactor 负责监听和分配事件,处理资源池负责处理事件。Reactor 模式的具体实现方案灵活多变,主要体现在:
1、Reactor 的数量可以变化:可以是一个 Reactor,也可以是多个 Reactor。
2、资源池的数量可以变化:以进程为例,可以是单个进程,也可以是多个进程(线程类似)。
最终 Reactor 模式有这三种典型的实现方案:单 Reactor 单进程 / 线程。单 Reactor 多线程。多 Reactor 多进程 / 线程。
单 Reactor 单进程 / 线程整个流程:Reactor 对象通过 select 监控连接事件,收到事件后通过 dispatch 进行分发。如果是连接建立的事件,则由 Acceptor 处理,Acceptor 通过 accept 接受连接,并创建一个 Handler 来处理连接后续的各种事件。如果不是连接建立事件,则 Reactor 会调用连接对应的 Handler(第 2 步中创建的 Handler)来进行响应。Handler 会完成 read-> 业务处理 ->send 的完整业务流程。
单 Reactor 单进程的方案在实践中应用场景不多,只适用于业务处理非常快速的场景,目前比较著名的开源软件中使用单 Reactor 单进程的是 Redis。
单 Reactor 多线程:主线程中,Reactor 对象通过 select 监控连接事件,收到事件后通过 dispatch 进行分发。如果是连接建立的事件,则由 Acceptor 处理,Acceptor 通过 accept 接受连接,并创建一个 Handler 来处理连接后续的各种事件。如果不是连接建立事件,则 Reactor 会调用连接对应的 Handler(第 2 步中创建的 Handler)来进行响应。Handler 只负责响应事件,不进行业务处理;Handler 通过 read 读取到数据后,会发给 Processor 进行业务处理。Processor 会在独立的子线程中完成真正的业务处理,然后将响应结果发给主进程的 Handler 处理;Handler 收到响应后通过 send 将响应结果返回给 client。
多 Reactor 多进程 / 线程:父进程中 mainReactor 对象通过 select 监控连接建立事件,收到事件后通过 Acceptor 接收,将新的连接分配给某个子进程。子进程的 subReactor 将 mainReactor 分配的连接加入连接队列进行监听,并创建一个 Handler 用于处理连接的各种事件。当有新的事件发生时,subReactor 会调用连接对应的 Handler(即第 2 步中创建的 Handler)来进行响应。Handler 完成 read→业务处理→send 的完整业务流程。目前著名的开源系统 Nginx 采用的是多 Reactor 多进程,采用多 Reactor 多线程的实现有 Memcache 和 Netty。
Proactor
可以理解为“来了事件我来处理,处理完了我通知你”。这里的“我”就是操作系统内核,“事件”就是有新连接、有数据可读、有数据可写的这些 I/O 事件,“你”就是我们的程序代码。
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服务
硬件负载均衡
硬件负载均衡是通过单独的硬件设备来实现负载均衡功能,这类设备和路由器、交换机类似,可以理解为一个用于负载均衡的基础网络设备。目前业界典型的硬件负载均衡设备有两款:F5 和 A10。优点:功能强大;性能强大;稳定性高;支持安全防护。缺点:价格昂贵;扩展能力差。
软件负载均衡
软件负载均衡通过负载均衡软件来实现负载均衡功能,常见的有 Nginx 和 LVS,其中 Nginx 是软件的 7 层负载均衡,LVS 是 Linux 内核的 4 层负载均衡。4 层和 7 层的区别就在于协议和灵活性,Nginx 支持 HTTP、E-mail 协议;而 LVS 是 4 层负载均衡,和协议无关,几乎所有应用都可以做,例如,聊天、数据库等。性能比不上硬件
优点:简单;便宜;灵活、
缺点:性能一般;功能没有硬件那么强大;一般不具备防火墙和防DDoS功能。
典型架构
3种负载均衡组合使用;组合的基本原则为:DNS 负载均衡用于实现地理级别的负载均衡;硬件负载均衡用于实现集群级别的负载均衡;软件负载均衡用于实现机器级别的负载均衡。可参考下面示例
日活千万的论坛举例
1、首先,流量评估。
1000万DAU,换算成秒级,平均约等于116。
考虑每个用户操作次数,假定10,换算成平均QPS=1160。
考虑峰值是均值倍数,假定10,换算成峰值QPS=11600。
考虑静态资源、图片资源、服务拆分等,流量放大效应,假定10,QPS*10=116000。
2、其次,容量规划。
考虑高可用、异地多活,QPS*2=232000。
考虑未来半年增长,QPS*1.5=348000。
3、最后,方案设计。
三级导流。
第一级,DNS,确定机房,以目前量级,可以不考虑。
第二级,确定集群,扩展优先,则选Haproxy/LVS,稳定优先则选F5。
第三级,Nginx+KeepAlived,确定实例。
算法
负载均衡算法数量较多,而且可以根据一些业务特性进行定制开发,抛开细节上的差异,根据算法期望达到的目的,大体上可以分为下面几类:任务平分类;负载均衡类;性能最优类;Hash类。
轮询
负载均衡系统收到请求后,按照顺序轮流分配到服务器上。只要服务器在运行,运行状态是不关注的。总而言之,“简单”是轮询算法的优点,也是它的缺点。
加权轮询
负载均衡系统根据服务器权重进行任务分配,这里的权重一般是根据硬件配置进行静态配置的,采用动态的方式计算会更加契合业务,但复杂度也会更高。加权轮询是轮询的一种特殊形式,其主要目的就是为了解决不同服务器处理能力有差异的问题。
负载最低优先
负载均衡系统将任务分配给当前负载最低的服务器,这里的负载根据不同的任务类型和业务场景,可以用不同的指标来衡量。负载最低优先的算法解决了轮询算法中无法感知服务器状态的问题,由此带来的代价是复杂度要增加很多。负载最低优先算法如果本身没有设计好,或者不适合业务的运行特点,算法本身就可能成为性能的瓶颈,或者引发很多莫名其妙的问题。所以负载最低优先算法虽然效果看起来很美好,但实际上真正应用的场景反而没有轮询(包括加权轮询)那么多。
性能最优
负载最低优先类算法是站在服务器的角度来进行分配的,而性能最优优先类算法则是站在客户端的角度来进行分配的,优先将任务分配给处理速度最快的服务器,通过这种方式达到最快响应客户端的目的。和负载最低优先类算法类似,性能最优优先类算法本质上也是感知了服务器的状态,只是通过响应时间这个外部标准来衡量服务器状态而已
Hash类
负载均衡系统根据任务中的某些关键信息进行 Hash 运算,将相同 Hash 值的请求分配到同一台服务器上,这样做的目的主要是为了满足特定的业务需求。常用的有源地址IPhash,ID hash等