现象:某营销活动开始后,出现系统繁忙。
原因总结:
????????在上线前未对流量做好充分的预估,没有预料到可能出现的瞬时流量过高的问题。在发券过程中,涉及到多次的数据库的读写,且是以事务形式进行的,当并发量较大时,导致数据库出现频繁的读写,一是导致数据库连接池耗尽,二是数据库性能下降。总结其几个问题:
??????1)直接操作数据库库存字段。瞬时高并发流量直接落到数据库上,去查询数据库中的活动库存字段,且每次都需要互斥锁避免超卖。导致数据库以及业务接口性能严重下降。
??????2)未做任何限流措施。没有去拦截无效的流量,每次只发几不到三千张券,每个请求都需要去查数据库库存判断是否已经消耗完。
??????3)未采取降级等手段。在活动期间,一些非必要的查询接口没有进行降级,导致活动期间的一些操作直接影响到了主活动。
??????4)硬件配置较差。某个系统在活动开始前横向或者纵向扩展,服务器配置较低,只有2c4G,且未做机器扩容。 ??????
??????5)反作弊服务欠缺。如果我们营销优惠力度比较大,就一定会引起黄牛的注意,比如各大电商的抢茅台活动,每次都会有很多黄牛来抢,他们都很专业,所以,基本上凡是涉及到秒杀的系统或者平台一定都会有一个专门的风控团队,和黄牛斗智斗勇。风控系统本身是很复杂的,他要根据线上数据去做数据模型训练,设计不同的算法制定拦击规则,开发拦截程序。
??现象:交易成功率低,报系统超时,请稍后重试。
现象分析:客户端发起大量请求,导致服务端负载较高,经排查,服务本身承载并发能力较弱,原本涉及其他场景调用负载就高,客户端调用上升过大,导致超出接口本身负载能力,出现了大量TIMEAWAIT套接字,超过25000,接口本身耗时也收到tcp链接的释放和创建导致等待,进而引发超时,最终导致服务宕机。
原因定位:服务端和Nginx都出现了大量的TIME_WAIT状态的TCP连接,可定义其因为频繁进行连接的建立和释放导致TIME_WAIT堆积。由于调用链路经过nginx,因为是nginx未做keepAlive相关配置导致。TIME_WAIT在nginx和服务端都有出现的话,主要是因为:
1)导致 nginx端出现大量TIME_WAIT的情况有两种:
keepalive_requests设置比较小,高并发下超过此值后nginx会强制关闭和客户端保持的keepalive长 连接;(主动关闭连接后导致nginx出现TIME_WAIT)
keepalive设置的比较小(空闲数太小),导致高并发下nginx会频繁出现连接数震荡(超过该值会关 闭连接),不停的关闭、开启和后端server保持的keepalive长连接;
2)导致后端server端出现大量TIME_WAIT的情况:?
nginx没有打开和后端的长连接,即:没有设置proxy_http_version 1.1;和proxy_set_header
Connection “”;从而导致后端server每次关闭连接,高并发下就会出现server端出现大量TIME_WAIT。
????????高并发系统的主要特点就是在短时间内同时有大量用户请求访问,因此需要系统能够快速、稳定地响应这些请求。
????????从高并发的指标来看,必须要做到系统的高性能以及高可用性。所以在做系统架构设计和开发时,也是要以这两个结果为导向,从不同层面去优化系统。
???如下是我画的一个高并发知识点脑图:
????????整个系统的任何一个节点都可能成为性能限制或者瓶颈的因素,包括数据库,缓存,垃圾收集器,GC算法、并发编程方式,负载均衡算法,TCP连接配置,硬件配置,网卡、网络带宽等等。
????????数据库是系统最重要的组成部分,很多情况下,整个系统的性能瓶颈也基本都在数据库层面。单台数据库服务器会因为数据量的飙升以及过高的访问量导致性能急剧下降。因此需要通过一些手段来提升数据库Server的整体性能,提升数据库性能的方案包括读写分离,分区,分库分表等。
???(1)读写分离:一主多从,多主多从。
?????????思想:读请求路由到从库,写请求路由到主库。通过读写分离能够分摊单库的压力,但要注意主从延迟的问题,特别是高并发时的读写。这也取决于主从复制的方式,不同的主从复制策略会兼顾不同的系统指标。通常主从复制包括三种方式:同步复制,半同步复制,异步复制。同步复制影响性能,异步复制可能导致数据丢失。半同步复制介于两者之间。
???????在实际应用中,通常可以借助独立的Proxy来实现SQL的路由。如对于insert,update,delete等语句,自动路由到主库上;对于select语句,自动路由到从库。不过如果select请求对于数据实时性要求较高,为了避免主从延迟的问题,可以显示指定读取主库。实现方式是在select语句中增加标识词。类似: select /*master*/ ?* from .. 。
????
???(2)分库分表:当简单的读写分离依然成为性能瓶颈的时候,就需要考虑对数据库进行分库或者分表。通过分库分表解决了单表单库的压力,真正做到了分而治之,这也是目前业界普遍采用的方式,通常会将分库分表和读写分离相结合使用。
?????使用分库分表需要关注的问题:
??????1、何时才需要做分库分表?
??????2、如何进行分库分表的划分?
??????3、分库分表如何解决分布式全局唯一ID,如何做好分布式事务?
??????4、分库分表如何做聚合查询?
??????5、单库单表拆分成分库分表后,如何做好历史数据的迁移?
??????6、随着业务的发展如何做好数据库的扩缩容?
??????7、使用分库分表有什么约束限制?
本文会就着上面列出的几个问题进行解读。
1、何时才需要做分库分表?
????正常情况下,如PG,索引默认采用B-Tree结构,经过三次IO操作可以实现千万数量级别的数据查询。因此,如果单表的数据量低于1千万,且TPS,QPS都不高的情况下,完全没必要进行分库或者分表,否则带来的负面影响要远远大于所带来的性能提升。分库分表会引入全局唯一ID、跨库事务管理、联查、聚合查询等诸多问题。
如果每天的写入量很大,每天就有几百万,上千万,上亿的数据写入,且访问频繁,如果还想继续使用关系型数据库的话,分库分表是当前的必选方案。在分库分表过程中可参考如下指标去评估分库分表的方案:
2、如何进行分库分表的划分?
????分库和分表是两种不同的做法。
分库,一般都会将数据分散到多个子库中,每个子库还有相同的表名。子库可以在同一台服务器,也可以在不同服务器上,应用可通过某一个或者多个分片键将数据分散存储到不同的子库中。比如有64个库(或者表),可以根据 uid % 64 进行hash取模后,分配到指定的数据表中。
分表,主要侧重点将一个表根据某种规则拆分为多个表。分表拆分后,子表表明发生了变化,如order_01,order_02,order_03等。
分库分表的划分方式有垂直划分和水平划分。
水平划分就是将数据根据某个唯一建,根据某种规则划分到不同的表(或库)中,比如用户id,设备id等等。
垂直划分这个虽然将某个数据表的字段拆分存储,但其本质上并没有解决单表数据过大的情况。
目前采用比较多的方式就是水平划分。比如一个用户已领取用户券的数据表。可以根据uid,进行hash取模。类似Redis Cluster的slot。一般Redis Cluster有16384个slot,key的分配算法是 CRC16(key)& 16383,计算获得0~16383中的某一个slot值。
目前业界通用的分表规则主要有以下几种:
3、分库分表如何解决分布式全局唯一ID,如何做好分布式事务?
????????全局的唯一ID在分库分表中是第一个难题,目前业界有很多的解决办法,包括采用数据库单表的自增主键、UUID、雪花算法、美团的LEAF,百度的UIDGenerator等,不过很多公司都会自己开发一套企业级的分布式全局唯一ID生成服务。
?4、分库分表如何做聚合查询?
????????尽量不要在实时交易系统中做聚合查询,即便是离线任务,聚合查询对数据库的压力会非常大,会严重影响在线的业务,甚至搞崩整个数据库。因此,如果想做聚合查询,需要把所有的数据离线聚合到其他地方,如单一的聚合库,或者列式存储数据库Hbase,分布式数据库,如ClickHouse,TiDB等。
?5、单库单表拆分成分库分表后,如何做好历史数据的迁移?
数据迁移包括存量和增量数据。
主要方案:
??1、分库全量数据写入
思想:数据向所有分库全量写入,随后删除。
优点:迁移简单,在切换前完成数据同步后,停掉交易即可完成切换。
缺点:这种方案需要后续删除非对应分片的数据,风险较大。
???2、业务双写
思想:存量数据全量导入分库,增量数据通过业务系统双写到原库和分库中从而达到一致;
优点:保证
缺点:业务系统实现双写改动比较麻烦。
??3、存量导入,增量同步
具体操作流程如下:
1、将数据库分成64个子库;
2、找一台Slave从库,停下来;
3、根据我们的分片规则 uid % 64 dump出 64个源文件;
4、将文件分别source导入到对应的子库中,该步骤是非常耗时的,而且中间不能停,这个要DBA操作执行的;
5、重启该台slave,并将新增的数据实时同步到分库中;
6、在切换前,停止线上交易;
7、上线分库数据源代码;
8、内网验证;
9、开启线上交易;
6、随着业务的发展如何做好数据库的扩缩容?
????????数据库的扩缩容涉及到的就是数据的存储位置变化,通常我们通过对子库或者子表数量取余,当节点数量发生变化后,可能会出现运算位置的改变。
????????7?% 2,取模是1,落在第二个子库;7%4,取余是3,落在第四个库,这就导致同样的序号在不同的规模下会落在不同的子库上。正常情况,我们需要根据新的取余进行数据的迁移。数据迁移可借助第三方的工具来实现,如Cannal,来实现每个子库的数据迁移。
????????数据迁移可能会为系统带来复杂度和不可预估的影响,因此,为了避免扩容带来的数据迁移问题,可以采用其他分片的规则,比如,可以基于数据范围分库,类似于Redis-Cluster,为每个节点,子库分配一定范围的槽(行数据),这样在扩容后,不必进行数据迁移,只需要为新的子库分配新的数据范围即可。但要维护好子库所负责的数据范围的这个对应关系。不过还要注意一个问题,就是数据倾斜的问题,因为原子库的数据可能已经很多了,新数据是均匀分配的,那么原子库一直都是要存储比其他子库更多的数据。
?7、使用分库分表有什么约束限制?
????尽量不要做跨库操作,SQL要带上分片键,不要联查。
????减少聚合操作;包括分组,分页,排序等操作。
(3)分布式数据库:分库分表是当前较好的通用提升性能的方案,但却不是最终的解决方案。下一代解决方案应该是原生分布式数据库。使用了分布式数据库可以实现自动扩容,也不用手动去带上分片键。
????????目前国内也有很多比较好的分布式数据库产品,如国产的TiDB,OceanBase,还有俄罗斯的ClickHouse。其中TiDB目前也是在很多银行得到了应用,像中国银行,北京银行,光大银行。杭州银行更是将其应用到了核心系统中。
? ? ? ? 关于缓存之前已经有过介绍了,具体可见我之前写的文章,这里摘抄了部分。
????????在计算机领域,缓存已经是无处不在的了,像CPU缓存,磁盘缓存,操作系统缓存(TLB块表,PageCache)以及应用层面的缓存,比如客户端缓存、CDN、Nginx缓存、DB缓存,DNS缓存等等。尤其是数据库缓存,这个应该是我们开发系统时必不可少的。在DB层上方增加缓存层,一是可以提升数据读取的速度,二是可以避免大量数据请求落到DB上,减轻数据库的压力。
????????通常数据库缓存包括两层,一是进程内缓存;二是分布式缓存。
?????????比如上面提到的某营销活动,就完全可以把活动库存放到Redis中,因为像满减,满赠等活动本质上都是营销的一种手段。营销最重要的目的就是引流,造势。所以对于活动库存,可以少卖,但不能超卖,这是最基本的原则。这里就完全可以通过Redis+lua脚本的方式来防止超卖,并且相对于数据库的操作有更高的性能。我们知道Redis本身的性能瓶颈从来不在CPU,而是在IO上,所以在官方在Redis4引入了后台线程,做一些lazy free等异步操作,Redis6引入了IO多线程,但Redis的命令执行一直是单线程的,所以结合lua脚本,实现命令的原子性,这样就很完美的实现了库存的扣减,也可防止超卖,这也是很多平台都在使用的主要方案。
????????除了上面提到的Redis分布式缓存,还有进程内缓存。本地缓存是直接、最简单的缓存方式,但使用需谨慎,因为进程内缓存主要使用的是主机的内存,如果缓存数据过多且未及时清理,可能会引起OOM,甚至操作系统会Kill掉对应进程。我们可以借助第三方库实现本地缓存,比如Guava,Encache等。其中Guava比较简洁,但只能实现堆缓存,Guava主要借鉴了ConcurrentHashMap的思想。堆缓存最大的问题就是容易引起GC,如果缓存数据过多,频繁的GC会影响整个应用系统。
???????缓存是提升系统性能的一把利器,但使用过程中页要注意一些细节问题。比如我们是否有热点数据,bigkey,这些都是可能会影响性能的地方。
???一致性:怎么保证缓存数据和原始数据的一致性?是强一致性还是弱一致性?像CPU缓存,它是通过MESI一致性协议以及内存屏障来保证一致性的,这个不需要开发者关注,但应用级别的缓存一致性需要我们自己实现。
???空间消耗:如何能够利用更小的空间存储更多的数据,因为缓存空间毕竟是有限的,是否可以考虑位图bitmap来存储一些统计类的数据。
???雪崩、穿透、击穿:另外还有缓存雪崩,穿透,击穿等等。这些也都是我们平时在使用缓存时需要注意的地方。在第二季度的一个生产事故案例中就出现了缓存击穿的问题,某个热点key过期了, 导致查询全落到数据库上,然后就出现生产事故了。对于这种热点key完全可以加上自动续期方案,或者干脆永不过期,后期可以手动清理。
???淘汰机制:我们该采用何种淘汰算法来保证缓存内的数据都是有效的,LRU,LFU,FIFO。不过淘汰机制都是内置在分布式缓存或者本地缓存第三方库中的。
????????并发编程主要的目的是能够充分利用系统资源。常见的并发编程方式包括:IO多路复用,多进程,多线程,协程,异步IO等。在我们开发时选择哪种并发编程的方式还是要结合自己的实际场景来决定。
????????IO多路复用:在常见的软件里基本上都可以看到IO多路复用的影子,像Netty,Redis,nginx,它是通过某种机制保证一旦指定的IO文件描述符准备就绪了,内核会通知进程处理,从而提升并发处理能力。机制包括select,poll,epoll,目前实际使用的都是epoll机制。主要是因为其不需要轮询描述符,而是采用事件驱动的方式来进行水平或者边缘触发,内部也是用红黑树去存储事件,性能是最好的。我们在开发系统时可以选择性地使用Netty作为底层通信框架,利用其主从Reactor多线程模型进行请求处理,其结合了IO多路复用与多线程的并发处理能力,可满足高并发的场景。
????????多进程:多进程现在依然比较常见。python有个Web服务器gunicorn,还有nginx,都是采用pre-fork机制,系统启动时master会提前派生出多个worker进程。但我们都知道相比于其他的并发编程来讲,多进程性能并不是最优的。因此多数情况下,多进程会和其他的并发编程方式结合使用。比如Nginx,实际上是采用多进程+IO多路复用的方式来实现的。
????????对于写Java的同学,可能多进程编程应用的并不多,但其在很多应用中都有广泛的应用,如Php语言,数据库PG等。
????????多线程:多线程就比较常见了,很多公司目前基本上都是JAVA技术栈,JAVA玩的就是多线程。相比于多进程来讲,可能会比多进程的上下文切换付出的代价更小。这里也说一个典型的应用,Netty,是一个标准的主从Reactor多线程模型,boss group负责接收请求,封装channel,并转发给work group,work group中的selector会进行IO处理,这里实际上就是IO多路复用的机制,随后会将就绪的事件丢给worker线程池去做处理。netty是一个非常优秀的通信框架,几乎很多有名的框架都采用netty,Dubbo,Lettuce,RocketMQ等等。当然netty的高性能不仅仅因为多线程,还有其他的技术结合,如零拷贝,对象内存池,CAS,无锁队列等。
????????协程:说完多线程,再看一下另外一个并发方式,协程。写Java的应该接触不到,写过python或者golang的应该都接触过,尤其是golang,在go语言中, ?一个简单的go func()语句就可以开启协程了。相比于多线程,其上下文切换的成本更低,所以你会看到很多高性能的软件都使用go语言来开发,像云原生的K8s以及衍生产品ETCD都是用go写的。这些都是谷歌的产品。
当需要我们在istio下开发扩展插件时,就会应用到go,扩展插件最重要的职责就是处理高并发流量的分发,协程的并发处理技术是至关重要的。
????????异步AIO:异步编程目前基本上都是框架或者语言自己在应用层面实现的,操作系统层面支持得并不是特别好,Linux也是才在2019年引入了io_uring的原生AIO 框架。主要是因为相比于同步,异步IO实现起来更加复杂一些。
????????说了这么多并发编程的方式,他们在提高效率的同时,也会引入其他的问题,比如安全性,当多个任务同时处理一个共享变量时,如果不实现资源同步,结果就是不可预测的。比如Nginx里,一个master,多个worker,当有请求过来时,如果不加任何同步机制,就很容易出现惊群效应。
?锁的介绍
????为了解决资源同步的问题,一个最常见的解决方案就是加锁。比如Linux中有信号量、互斥锁、自旋锁、RCU,JAVA各种内置锁;数据库中的各种锁,表锁,行锁等。这就涉及到并发编程中的可见性、原子性和有序性三大问题。
这里就举两个简单的例子。
public?Integer a = 0;
public?void?getA(){return?a;}
public?voic setA(Integer value){
????a = value;
}
public?void?modify(){
?????Integer a = getA();
?????setA(++a);
}
????????上面代码中,主要是获取变量,并将值进行更新。当串行执行时,没有任何问题。但如果同时有两个任务进行处理,两个任务同时先取a,结果取到的都是0,随后更新a的值,最后a的值并不是想要的2,而是1。
另外一个例子,说明因为指令重排序导致的问题。
int?a = 1;
int?b = 2;
public?void?read(){if?(b == 3){int?y = a*a;
???}
public?void?write(){
????a=2;
????b = 3;
}
?
write方法中的两个语句没有依赖关系,因此在单线程内会被重排序。这就可能导致b=3先执行,随后read方法先读取到b==3,然后执行y=a*a,这里 的a的当前值是1,即y=1。
????????为了解决上述的问题,一个最常见的解决方案就是加锁。通过加锁,保证了操作的原子性,只有获得锁的任务才能执行相关流程,从而保证最后得到的结果是可预知的。比如在电商系统中,下单支付后更改库存,通过使用悲观锁或者乐观锁来保证不会超售。锁是用于控制多任务同时访问共享资源的一种同步机制。
通常,我们会将锁分成两大类,悲观锁和乐观锁。悲观锁即认为一定存在竞争,所以在处理之前加上互斥锁,存在排他性,只有获得到该锁的任务才能处理,其他的会被挂起。
相比于悲观锁,乐观锁认为不是总是存在冲突,它在处理之前不会像悲观锁一样加上锁,而是在执行数据更新时,利用版本号和CAS来实现数据的更新。比如PG中的MVVC,Java中的CAS更是随处可见,比如atomic下的多种原子类,locks下的锁等等。
上面提到的是进程内使用的锁,还有用于进程间资源同步的锁,即分布式锁。分布式锁的种类包括基于数据库的锁、Redis分布式锁、Zookeeper分布式锁。
从性能上来看,Redis>ZK>PG,Redis做为一个NoSQL中间件,性能不可比拟,Mysql最差。
从可靠性来看:ZK>Redis>PG
从实现难易来看:ZK>Redis>PG。
我在2023年在我们中心发明了一个专利,一种是基于ETCD的实现的分布式锁的方法,可实现互斥锁,读写锁,锁的降级,升级等等,感兴趣的可以联系我进行交流。
死锁、活锁
????????锁是在并发编程中最重要的同步机制,但是也不能贪杯,不能过度使用锁,也不能随意使用。过度使用锁可能会降低整体性能,如果锁的粒度比较大,那就会导致几乎一直都是单个任务处理,此外由于上下文切换导致其性能还不如串行化处理方式。另外随意使用锁可能会出现死锁现象,这是一个比较常见,且严重的问题。想要避免死锁,就是要避免产生死锁的几大条件的发生。
总结几个尽量预防死锁的方式:
1)尽量避免大事务,避免长时间占用资源;
2)要保证顺序性,这个在开始死锁的介绍也提到过。比如每个事务都是处理资源1,资源2,资源3,要保证每个事务都是按照同样的顺序去处理;
3)避免一个事务同时占用过多的资源。这可以通过优化索引设计,sql等环节来实现。
4)事务中要使用索引或者主键来定位数据,避免占用过多的行,尤其是尽量避免表锁, 记录锁最好。
说完并发编程,再说另外一种提升性能的手段,零拷贝。我们现在开发业务系统,基本上都是IO密集型的,很少有CPU密集型的,因此I/O是成为系统瓶颈的主要因素,零拷贝可以减少CPU拷贝次数,减少用户态和内核态的切换,从而提升IO传输效率。像Nginx就有用到,如果大家有配置过nginx的,一定会在http模块下看到一个配置,即 sendfile on,sendfile主要用于文件传输的场景,对于一些js,css,图片等静态资源的传输效率非常高。还有Netty, ?RocketMQ也都使用了零拷贝技术,RocketMQ用到的是mmap。
?????数据库连接池,Redis连接池,线程池,进程池等等。尤其是线程池,是JAVA技术栈最常用的。
如何配置线程池的参数直接影响着资源的使用率高低、应用系统的性能。
线程池的配置本身并没有一个统一的标准,而是需要开发者在实际开发过程中根据实际的业务场景、业务量不断调试,直到在高并发的场景下可以获得最优性能。
本文会结合着工作经验,给出一些指导性的意见和建议,但并不是唯一的、严格的标准。
????线程池的参数包括核心线程数、最大线程数、阻塞队列等参数。
???1、核心线程数corePoolSize
??????线程池的配置和我们所开发的任务类型是强相关的,CPU密集型任务和IO密集型任务对于线程池的使用是完全不同的。 ??
????????CPU密集型任务重要集中在CPU的计算使用上,主要进行CPU执行逻辑处理,因此建议的线程数是CPU核心数+1,此时如果设置再多的线程不仅提高不了资源使用率,反而会因为线程频繁的切换带来资源的消耗,从而影响性能。线程数加1的目的是防止某些线程因为缺页异常或者其他故障导致当前线程不可用。
????????IO密集型任务,大部分时间主要是集中在I/O操作上,IO阻塞过程可以通过线程切换提高CPU的使用率。因此,对于IO密集型任务来说,正常的和核心线程数设置为CPU核数乘以2。如果按照更严格的标准来设置,可以参考《Java并发编程实战》给出的公式。
??2、阻塞队列
????????阻塞队列的建议是不要用无界队列,如果任务执行时间较长或者核心线程数较少,导致高并发时大量任务堆积到队列中,如果是无界队列,会随着不断堆积导致OOM。建议队列大小控制在万级以内。
???Java阻塞队列:
?3、最大线程数
????????????????当任务队列已满时(jdk原生,不包括Tomcat线程池),才会继续创建线程,直到达到最大线程数。最大线程数可以设置为和核心线程数一致,或者比核心线程数大一些。如果设置的和核心线程数一致,就完全依靠阻塞队列和拒绝策略来控制任务的处理情况,如果设置的比核心线程数稍微大一点,那可能对于一些突然流量的场景更使用。
4、KeepAliveTime
????该参数指的是超过核心线程数的线程空闲的持续时间,超过这个时间,空闲线程会被回收。
????该参数也和任务的类型有关。
????对于 CPU 密集型任务,并不需要过多超过CPU核心数以外的线程,KeepAliveTime时间可以设置得短一些;
? ? ? ?对于 I/O 密集型任务,线程大部分时间都处于 I/O阻塞的状态,可以适当增大 keepAliveTime,保证有足够线程并发处理任务。
????????以上只是给出的参考性建议,线程池的最优配置还是要根据实际的业务场景、实际应用进行调试,并没有一个标准配置。 此外,除了线程池配置,也要注意在实际开发中不要在线程池中执行耗时过长的任务,否则可能会导致并发较高时出现OOM(无界队列场景)或者线程池耗尽(有界队列场景)等问题。 ?
预处理技术也是比较常见的一种提升性能的方式,预强调的就是提前加载,像CPU的缓存行,数据库或者操作系统的按页读取都会有一个预读,都是利用空间或者时间的局部性原理。那在我们应用层呢,我们做的预处理可以是缓存预热、文件预热等,通过这种预处理的方式为高并发流量的到来做好准备。
? 比如营销活动就可以提前将活动数据加载到缓存中,交易配置信息也可以提前加载到jvm里。常用的消息队列RocketMQ,它就实现了文件预热。
以上是软件相关的一些高性能提升手段,再说下硬件相关的。硬件这里就强调的是应用系统所在的运行环境。包括服务器还有网络等。
??对于服务器来讲,主要包括两个方面,一个纵向扩展,一个横向扩展。
???1)纵向扩展
???单机配置:CPU,磁盘,内存、网卡、网络带宽
???2)横向扩展 ?
????????集群:单服务多实例,以集群的方式部署,通过负载均衡实现请求分摊,减少单台服务器的压力。那选择合适的负载均衡技术也是关键,硬件解决方案有F5,软件的有Nginx,HAProxy,LVS等等;
????????分布式:我们在开发应用时,经常通过服务拆分,划分出多个微服务,微服务之间实现业务和系统隔离,从而达到分而治之的效果,这也是提升整个系统性能的手段。还有开源软件像RedisCluster也是分布式系统。
????????当然分布式系统相对要复杂一些,要考虑很多因素,像分布式一致性,容错性等。我们经常会听到分布式系统的CAP,BASE理论,还有就是采用何种共识算法,常见的像Raft,ZAB,Gossip协议,区块链里有工作量证明等等。
????????在实际工作中我们也是要将纵向扩展与横向扩展相结合来用,要找到一个平衡,我之前看到有些项目组在上线前可能申请了很多的服务器,但每台服务器的配置却极其的差,我感觉这并不是特别好。
????????高可用即整个系统在高并发的情况下在大部分时间都是可用的。保证高可用的手段有,降级,熔断,异步,消息队列,集群、分布式等等。
????????集群的作用除了分摊流量压力,同样是一种冗余备份,当集群中一部分机器挂掉,会自动将其摘除,完成隔离,有状态的服务可能还会实现故障切换,从而保证整个服务仍然可用。目前有些框架会自己实现高可用,有些框架会借助一些成熟的工具来实现,比如KeepAlived,Zookeeper等等。
????????自治的典型例子是RedisCluster,RedisCluster每个master节点存储一定槽位的数据,master又挂着几个slave,如果集群某个master节点down掉了(发现机制是通过PING-PONG实现),它会根据Raft协议自动选举出新的master节点,从而保证整个系统的高可用。下图是RedisCluster的标注部署架构图。
????????这里关于集群还要提到一点,就是要注意机房的异地多活。这个是很重要的,我之前也接触过通信运营商相关工作,就遇到过很多奇葩事件。比如光纤断了,设备端口down了,光模块坏了,机房断电等等,尤其是运气不好赶上网络传输设备是阿朗或者爱立信的,他们设备经常坏,不过现在几大运营商也基本上都换成华为的了,他们设备比较稳定。
????????通过消息队列,可以削弱流量高峰,比如营销活动中,可以将请求放在消息队列里,随后异步处理,避免并发处理所带来的压力。使用消息队列要充分了解不同消息队列的优缺点,技术原理。如RabbitMQ要注意重启后队列丢失、不可回溯的问题。
????????大促期间通常会将系统中不重要的服务降级,避免占用系统资源。如电商系统基本上在大促之前,会将评论,历史订单查询等子服务降级等。就比如幻灯片上的这个案例,它本应该在活动开始前,将卡券中心整合促销查询功能进行降级的,这些查询在活动进行中已经影响了整个系统的性能。他们的解决方案中也已经把降级加上了。
????????这在高并发场景中是必不可少的。限流有两个目的:
? ? ? ?1、防止瞬时高并发流量同时打到我们系统上,超过了服务的负载能力;
? ? ? ?2、撇去多余的无效流量。比如案例中实际上每期只发放2000张券来抢,但活动开始时瞬时流量可能10万以上,或者几十万以上的流量,大多数很多都是无用的,这些流量完全没有必要再去走我们系统的业务逻辑。
????????限流算法有滑动窗口计数器,令牌,漏桶等等。进程内限流有google的GUAVA,分布式的有sentinel等等。
???????熔断是保证当后端业务有异常时,可以及时摘除,避免影响整个链路。市面上也有很多成熟的方案,像sentinel或者Hytrix。
???????云原生主要是包括容器化,微服务架构和容器的编排。这里只说容器的编排,当前实现方式就是k8s,使用 k8s 可以进行容器编排、服务发现和负载均衡。此外,可实现弹性的扩缩容,系统的自动恢复和拉起。目前比较火的Devops平台,它不仅仅是消除了我们开发和运维之间的壁垒,更是提升了开发效率,减小了交付周期。因为它可以提供从开发、构建、测试、发布、部署全流程的持续交付和部署。你只需要在Devops构建自己的任务流水线即可。
? ? ?灾备建设包括应用级和数据级两个维度。