我们在谈到系统的时候,总会和性能挂钩,自然而然的会去想系统必须得性能好,设计系统的时候考虑最多的质量属性也是性能。高性能是每个程序员的追求,无论我们是做一个系统,还是写一行代码,都希望能够达到高性能的效果。
高性能架构设计主要集中在两方面:
(1 )尽量提升单服务器的性能,将单服务器的性能发挥到极致。
(2 )如果单服务器无法支撑性能,设计服务器集群方案。
我们该从哪些方面去考虑系统的性能呢?高性能从一个系统角度可以分为存储高性能和计算高性能。
一、存储高性能
我们先看一下存储高性能,从关系型数据库,NoSql数据库,缓存三块来提高存储的性能。关系型数据库可以通过读写分离和分库分表的方式提升存储性能;如果因为关系型数据库本身的限制,无法满足业务需求,可以选择适合的NoSql类型数据库;针对高并发的读操作时,可以考虑使用缓存的方案提升系统的性能。
1.关系型数据库
单台数据库服务器的性能优化就是硬件扩容和硬件升级,升级内存,升级硬盘,升级cpu,或者直接换小型机,大型机等。但这种方式的升级成本昂贵,而且性能的提升也容易达到瓶颈。随着互联网业务的发展,越来越多的数据,单个数据库服务器已经难以满足业务需要,如果继续使用关系型数据库,就必须考虑数据库集群的方式来提升性能。
数据库集群的方案一般有两种:读写分离和分库分表。
1)读写分离
将数据库的读写操作分散到不同的节点上。
数据库服务器搭建主从集群,一主一从或一主多从。
数据库集群的主机负责读写操作,从机只负责读操作。
主机通过数据库自带的复制技术,将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
业务服务器将写操作发给数据库主机,将读操作发送给数据库从机。
主从和主备的区别:备机仅仅提供备份功能,不提供访问功能,从机是可以提供读数据的功能的。
读写分离的方案,在实现上不复杂,但在实际使用过程中会存在复制延迟,MySql中这种延迟可能有1秒,如果数据量大的时候,甚至有可能会达到1分钟。根据业务的需要,考虑是否需要解决这种复制延迟。
复制延迟的常见解决方法:
写操作后的读操作指定发给数据库主服务器,但这种方案业务耦合性较大。
读从机失败后,再次读取主机。也就是常说的“二次读取”,但这种方案会增加主机的读压力
关键业务读写操作全部指向主机,非关键业务采用读写分离。
2)分库分表
读写分离的方案,主要是针对业务逻辑上,读压力远大于写压力的时候。但对于那些写压力也很大业务,使用读写分离的方法提升的性能十分有限。
业务分库:按照业务模块将数据分散到不同的数据库服务器。例如,一个简单的电商网站,包括用户、商品、订单三个业务模块,我们可以将用户数据、商品数据、订单数据分开放到三台不同的数据库服务器上。
分库虽然可以分散读写,但也带来新的问题:
join操作问题,本来可以直接使用join实现的表连接,现在分在两个库,无法使用join,只能一张表一张表进行查询操作。
事务问题,原本可以使用数据库的ACID来保证多表操作的事务,现在分在两个库中,事务的保证只能通过业务自己实现,或者使用第三方的分布式事务管理工具。
分库会带来成本的上升,原来一台服务器,需要使用三台服务器,在业务的初始状况下,不用急着分库,单台数据库服务器的性能其实也没有想象的那么弱,一般来说,单台数据库服务器能够支撑10 万用户量量级的业务,初创业务从0 发展到10 万级用户,并不是想象得那么快。
分表:单表数据拆分有两种方式: 垂直分表和水平分表。
新的表即使在同一个数据库服务器中,也可能带来可观的性能提升。如果单表拆分为多表后,单台服务器依然无法满足性能要求,那就不得不再次进行业务分库的设计了。
垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。带来的问题就是,原来只需要查询一次的,现在需要查询两次。
水平分表适合表行数特别大的表,如果单表行数超过5000 万就必须进行分表,这个数字可以作为参考,但并不是绝对标准,关键还是要看表的访问性能。
水平分表带来的复杂度:
路由,查询某条数据时,具体属于哪个切分后的子表,需要增加路由算法进行计算。常见的路由算法:范围路由,hash路由,配置路由。
join操作,别的表与该表进行join操作时,需要进行多次Join操作。
count操作,数据分布在不同的表中,如果想知道总数,就要对多个表的count求和
读写分离和分库分表的实现:
i.代码集成:在应用中集成jar方式。开源软件淘宝的TDDL,提供了通用的数据访问层
ii.中间件:独立的一套系统,应用访问中间件。MySQL 官方推荐MySQL Router,360 公司也开源了自己的数据库中间件Atlas。
2.NoSql
关系型数据库存在的问题:
a.只能存储行记录,无法存储数据结构。
b.表结构schema的扩展不方便。
c.大数据量下的I/O会很高,只是针对某一列进行计算,也会读取整行数据。
d.全文搜索比较弱,只能使用like 进行整表扫描匹配,性能非常低。
常见的NoSQL 方案有如下4 类:
• K-V 存储:解决关系数据库无法存储数据结构的问题,以Redis 为代表。
• 文档数据库:解决关系数据库强schema 约束的问题,以MongoDB 为代表。
• 列式数据库: 解决关系数据库大数据场景下的I/O 问题,以HBase 为代表。
• 全文搜索引擎:解决关系数据库的全文搜索性能问题,以Elasticsearch 为代表。
针对不同的使用场景使用不同的存储系统方案,在关系数据库无法满足的时候,可以使用特定NoSql数据库来满足业务对性能的需求。
3.缓存
在某些复杂的业务场景下,单纯依靠存储系统的性能提升不够的:
需要经过复杂运算后得出的数据,存储系统无能为力。
读多写少的数据,存储系统有心无力。
缓存的基本原理就是将可能重复使用的数据放到内存中,一次生成,多次使用, 避免每次使用都去访问存储系统。
以Memcache 为例,单台Memcache 服务器简单的key-value查询能够达到5 万以上的TPS。
缓存需要考虑的问题:
缓存穿透:缓存没有发挥作用,业务系统需要再次去存储系统中查询数据。
• 存储数据不存在,解决方法:如果查询存储系统的数据没有找到, 则直接设置一个默认值( 可以是空值,也可以是具体的值)并存到缓存中, 这样第二次读取缓存时就会获取默认值,而不会继续访问存储系统。
• 缓存数据生成耗费大量时间或资源,例如分页计算,缓存的页面数据,一般只缓存前多少页。
缓存雪崩:指当缓存失效(过期)后引起系统性能急剧下降的情况。
对于一个高并发的业务系统来说,几百毫秒内可能会接到几百上千个请求。由于旧的缓存己经被清除,新的缓存还未生成,并且处理这些请求的线程都不知道另外有一个线程正在生成缓存,因此所有的请求都会去重新生成缓存,都会去访问存储系统,从而对存储系统造成巨大的性能压力。
解决方案:(1)更新锁,对缓存更新操作进行加锁保护,保证只有一个线程能够进行缓存更新。(2)后台更新,由后台线程来更新缓存,而不是由业务线程来更新缓存。后台线程定时读取更新或者使用消息队列由业务线程通知。
缓存热点:对于一些特别热点的数据,如果大部分甚至所有的业务请求都命中同一份缓存数据,则这份数据所在的缓存服务器的压力也很大。例如:明星微博宣告某一事件。
解决方案就是复制多份缓存, 将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力。
二、计算高性能
1.单服务器高性能——网络编程模型
这里关于网络编程模型涉及到两个关键点:I/O 模型和进程模型
I/0 模型:阻塞、非阻塞、同步、异步。
进程模型:单进程、多进程、多线程。
同步异步:是消息的通信机制,涉及到IO通知机制;所谓同步,就是发起调用后,被调用者处理消息,必须等处理完才直接返回结果,调用者主动等待结果;所谓异步,就是发起调用后,被调用者直接返回,但是并没有返回结果,等处理完消息后,通过状态、通知或者回调函数来通知调用者,调用者被动接收结果。
阻塞非阻塞:程序等待调用结果时的状态,涉及到CPU线程调度;所谓阻塞,就是调用结果返回之前,该执行线程会被挂起,不释放CPU执行权,线程不能做其它事情,只能等待,只有等到调用结果返回了,才能接着往下执行;所谓非阻塞,就是在没有获取调用结果时,不是一直等待,线程可以往下执行,如果是同步的,通过轮询的方式检查有没有调用结果返回,如果是异步的,会通知回调。
请求立即返回就是非阻塞,不立即返回就是阻塞。
同步阻塞,线程一直在等待结果返回;同步非阻塞,线程在轮询获取结果;异步非阻塞,线程继续往下执行,结果会由通知回调。
PPC,prefork,TPC,prethread模式,都是请求进来后交给新的进程/线程处理。但创建进程或线程都是有代价的,高并发时还是有性能问题。
Reactor模式
I/O多路复用技术,只有当连接上有数据的时候进程才去处理。
I/O 多路复用技术归纳起来有如下两个关键实现点:
1.当多条连接共用一个阻塞对象后,进程只需要在一个阻塞对象上等待 ,而无须再轮询所有连接。
2.当某条连接有新的数据可以处理时,操作系统会通知进程,进程从阻塞状态返回,开始进行业务处理。
Reactor的核心组成部分有两个:Reactor,负责监听和分配事件;处理资源池负责处理事件。
Reactor示意图大致流程说明:
(1)Reactor对象通过select监控连接事件,收到事件后通过dispatch进行分发。
(2)如果是连接事件,则由Acceptor处理,Acceptor通过accept接收连接,并创建一个handler来处理连接后续的事件。
(3)如果不是连接建立事件,则Reactor会调用连接对应的Handler(第2步创建的)来进行响应。
(4)Handler会完成read->业务处理->send的完整业务流程。
单Reactor单进程/线程模式:Reactor的监听处理和业务处理都由一个进程/或线程处理,优点没有进程间通信,没有线程竞争,缺点无法发挥CPU多核的性能,业务处理容易导致性能瓶颈。redis采用该模式。
单Reactor多线程模式:Reactor承担所有事件监听和数据的读写操作由一个线程处理,业务处理由其他所有线程处理。多线程数据共享和访问比较复杂。
多Reactor多进程/线程模式:mainReactor线程只负责监听连接事件,收到连接后分配给subReactor线程处理,由subReactor监听其他事件,读写和业务处理操作。nginx是多Reactor多进程,netty,memcache是多Reator多线程。
Reactor是同步非阻塞,这里的同步指用户进程在执行read和send这类I/O操作的时候是同步的。
Preactor模型,异步非阻塞模型,让异步I/O操作与计算重叠,把I/O操作由操作系统处理,应用进程无需进行I/O读写操作,但是在Linux下实现高并发网络编程时都是以Reactor模式为主。
2.集群服务器高性能——负载均衡方案
单服务器无论如何优化,无论采用多好的硬件,总会有一个性能天花板,高性能集群的本质很简单,通过增加更多的服务器来提升系统整体的计算能力。
常见的负载均衡系统包括 3 种:DNS 负载均衡、硬件负载均衡和软件负载均衡。
DNS 负载均衡的本质是 DNS 解析同 一个 域名可以返回不同的 IP 地址。
软件负载均衡通过负载均衡软件来实现负载均衡功能 , 常见的有 Nginx 和 LVS 。
Nginx是软件的 7 层负载均衡, LVS 是 Linux 内核的 4 层负载均衡。 4 层和 7 层的区别就在于协议和灵活性。
Nginx 支持 HTTP 、 E-mail 协议。
LVS 是4层负载均衡,和协议无关 ,几乎所有应用都可以做,例如,聊天、数据库等。
一般的 Linux 服务器上装一个 Nginx 大概能到 5 万/每秒 : LVS 的性能是 十万级,
负载均衡算法
轮询:收到请求后,按照顺序轮流分配到服务器上 。
加权轮询:根据服务器权重进行任务分配,这里的权重一般是根据硬件配置进行静态配 置的,采用动态的方式计算会更加契合业务,但复杂度也会更高。
负载最低优先:将任务分配给当前负载最低的服务器,这里的负载根据不同的任务类型和业 务场景,可以用不同的指标来衡量。例如,以“连接数”来判断服务器的状态,以“ HTTP 请求数”来判断服务器状态等。
性能最优类:优先将任务分配给处理速度最快的服务器。通过响应时间这个外部标准来衡量服务器状态,负载均衡系统需要收集和分析每个服务器每个任务的响应时间。减少这种统计上的消耗,可以采取采样的方式来统计。
Hash 类:根据任务中的某些关键信息进行 Hash 运算,将相同 Hash 值的请求分配到同 一台服务器上。
网友评论