高并发问题

面对超高并发问题,首先要考虑物理层面机器是否能扛得住,其次架构设计做好微服务的拆分,代码层面各种缓存、消峰和解耦等问题都要处理好,数据库方面做好读写分离和分库分表,稳定性方面要保证有监控、熔断限流降级等该有的都要有,发生问题能及时处理。

1.微服务架构

在互联网早些时候,单体架构就可以支撑日常的业务需求,所以大家所有的服务都在一个项目里,部署在一台物理机上,所有的业务都夹杂在一起,当流量一起来以后单体架构问题就出来了,机器挂了所有业务都没有办法使用了。
在这里插入图片描述
于是集群架构开始出现,单机无法抗住的压力,最简单的解决办法就是水平拓展和横向扩容。通过负载均衡把压力流分摊打不同的机器上,暂时解决了单点不可用的情况。
在这里插入图片描述

但是随着业务的发展,在一个项目里维护所有的业务场景使开发和代码维护越来越困难。一个简单的需求修改就需要发布一个新的版本,代码合并冲突也会越来越频繁,同时线上出现问题的可能性越大。微服务架构就诞生了。
在这里插入图片描述

把每一个独立的业务拆分开部署,开发和维护的成本降低,集群能承受的压力也提高了,再也不会因为一个小小的改动牵一发而动全身了。
以上的点从高并发而言,都可以归类为通过服务拆分和集群物理机器的扩展提高了整体系统的抗压能力。那么,随之拆分而带来的问题就是高并发系统需要解决的问题。

2.RPC

微服务化的拆分带来的好处和便利性是显而易见的,但是与此同时各个微服务之间的通讯就需要考虑了。传统的HTTP通讯方式性能首先不太好,大量的请求头之类的无效信息是对性能的浪费。此外,对服务治理、追踪和易用性要求更高,这时候需要引用dubbo类的RPC框架
在这里插入图片描述

我们假设原来来自客户端的QPS是9000的话,那么通过负载均衡分散到每台机器就是3000,而HTTP改为RPC之后接口的耗时缩短了,单机和整体的QPS就提升了。而RPC框架本身一般都自带负载均衡和熔断降级机制,可以更好的维护整个系统的高可用性。

3.Dubbo工作原理

1.服务启动的时候,provider和consumer根据配置信息,连接到注册中兴register,分别向注册中心注册和订阅服务。
2.register根据服务订阅关系,返回provider信息到consumer,同时consumer会把provider信息缓存到本地。如果信息有变更consumer会收到register的推送。
3.consumer生成代理对象,同时根据负载均衡策略,选择一台provider,同时定时向monitor记录接口的调用次数和调用时间信息。
4.拿到代理之后,consumer通过代理对象发起接口调用。
5.provider收到请求后对数据进行反序列化,然后通过代理调用具体的接口实现。在这里插入图片描述

4.Dubbo负载均衡策略

1.加权随机:假设我们有一组服务器servers=[A,B,C] 他们对应的权重weights=[5,3,2] ,权重总和为10,现在把这些权重值平铺在一维坐标值上,[0,5)区间属于服务器A,[5,8)区间属于服务器B,[8,10)区间属于服务器C。接下来通过一个随机数生成器生成一下范围在[0,10)之间的数,然后计算这个数会落在那个区间上就可以了。
2.最小活跃数:每个服务提供者对应一个活跃数active,初始情况下,所有服务提供者的活跃数都为0。每收到一个请求,活跃数加1,完成请求后活跃数减1。在服务运行一段时间后,性能好的服务提供者处理请求的速度更快,因此活跃数下降的也越快,此时这样的服务提供者可以优先获取到新的服务请求。
3.一致性hash:通过hash算法,把provider的invoke和随机节点生成的hash并将这个hash映射到[0,2^32-1]的圆环上,查询的时候根据key进行MD5然后进行hash,得到第一个节点的值大于等于当前hash的invoke。
在这里插入图片描述
4.加权轮询:比如服务器的A、B、C的权重比为5:2:1,那么8次请求中,服务器A将收到其中的5次请求,服务器B将收到其中的2次请求,服务器C将收到其中的1次请求。

5.集群容错

1.Failover Cluster失败自动切换:Dubbo默认容错调度方案,当调用失败时自动调用其他可用节点,具体的重试次数和间隔时间可用通过引用服务的时候配置,默认重试次数为1,也就是只调用一次。
2.Failback Cluster快速失败:在调用失败时,记录日志和调用信息,然后返回空结果给consumer,并且通过定时任务每个5秒对失败的调用进行重试。
3.Failfast Cluster失败自动回复:只会调用一次,失败后立即抛出异常。
4.Failsafe Cluster失败安全:调用出现异常,记录日志不抛出,返回空结果。
5.Forking Cluster并行调用多个服务提供者:通过线程池创建多个线程,并发调用多个provide,结果保存到阻塞队列,只要有一个provider成功返回结果,就会立即返回结果
6.Broadcast Cluster广播模式:逐个调用每个provider,如果其中一台报错,在循环调用结束后,抛出异常

6.消息队列

MQ的作用:削峰填谷、解耦。依赖消息队列,同步转异步的方式,可以降低微服务之间的耦合。
对于一些不需要同步执行的接口,可以通过引用消息队列的方式异步执行以提高接口的响应时间。在完成交易后需要扣库存,然后可能要给会员发放积分,本质上,发放积分的动作应该属于履约服务,对实时性的要求也不高,我们只要保证最终一致性也就是能履约成功就行了。对于这种同类性质的请求就可以走MQ异步,也就提高了系统的抗压能力。
在这里插入图片描述
对于消息队列而言,怎么在使用的时候保证消息的可靠性和不丢失。

7.消息可靠性

消息丢失可能发生在生产者发送消息,MQ本身丢失消息,消费者丢失消息三个方面。

7.1生产者丢失

生产者丢失消息的可能点在于程序发送失败抛出异常了没有重试处理,或者发送的过程成功但过程中网络闪断MQ没有收到,消息就丢失了。
由于同步发送一般不会出现这种使用方式,所以我们不考虑同步发送问题,我们给予异步发送的场景来说。
异步发送分为两种方式:异步有回调和异步无回调。无回调方式,生产者发送消息后不管结果可能就会造成消息丢失,而通过异步发送+回调通知+本地消息表的形式我们就可以做出一个解决方案。
已下单的场景举例。
1.下单后先保存本地数据和MQ消息表,这时候消息的状态是发送中,如果本地事务失败,那么下单失败,事务回滚。
2.下单成功,直接返回客户端成功,异步发送MQ消息。
3.MQ回调通知消息发送结果,对应更新数据库MQ发送状态
4.JOB轮询超过一点时间(时间根据业务配置)还未成功发送的消息去重试
5.在监控平台配置或者JOB程序处理超过一定次数一直发送不成功的消息,告警,人工介入。
在这里插入图片描述

一般而言,对于大部分场景来说异步调用的方式就可以了,只有那种需要完全保证消息不能丢失的场景我们做一套完整的解决方案。

7.2MQ丢失

如果生产者保证消息发送到MQ,而MQ收到消息后还在内存中,这时候宕机了又没来得及同步给从节点,就可能导致消息丢失。
比如RocketMQ :
RocketMQ分为同步刷盘和异步刷盘两种方式,默认是异步刷盘,就有可能导致消息还未刷到硬盘上就丢失了,可以通过设置为同步刷盘的方式来保证消息的可靠性,这样即使MQ挂了,恢复的时候也可以从磁盘中回复消息。
比如kafka也可以通过配置做到

acks=all   只有参与复制的所有节点全部收到消息,才返回生产者成功。这样的话除非所有节点都挂了,消息才会丢失。
replication.factor=N 设置大于1的数,这回要求每个partion至少有2个副本
min.insync.replicas=N 设置大于1的数,这会要求leader至少感应到一个follower还保持着连接
retries=N 设置一个非常大的值,让生产者发送失败一直重试

虽然我们可以通过配置的方式来达到MQ本身高可用的目的,但是都对性能有损耗,需要怎样配置需要根据业务作出权衡。

7.3消费者丢失

消费者丢失消息的场景:消费者刚收到消息,此时服务器宕机,MQ认为消费者已经消费,不会重复发送消息,消息丢失。
RocketMQ默认是需要消费者回复ack确认,而kafka需要手动开启配置关闭自动offset。
消费方不返回ack确认,重发的机制根据MQ类型的不同发送时间间隔、次数都不尽相同,如果重试超过次数之后会进入死信队列,需要手工处理了。(kafka没有这些)
在这里插入图片描述

8.消息的最终一致性

事务消息可以达到分布式事务的最终一致性,事务消息就是MQ提供的类似XA的分布式事务能力。
半事务消息就是MQ收到了生产者的消息,没有收到二次确认,不能投递的消息。
实现原理如下:
1.生产者发送一条半事务消息给MQ
2.MQ收到消息后返回ack确认
3.生产者开始执行本地事务
4.如果事务执行成功发送commit到MQ,失败发送rollback
5.如果MQ长时间未收到生产者的二次确认commit或者rollback,MQ对生产者发起消息回查
6.生产者查询事务执行最终状态
7.根据查询事务状态再次提交二次确认

最终,如果MQ收到二次确认commit,就可以把消息投递给消费者,反之如果是rollback,消息会保存下来并且在3天后被删除。

在这里插入图片描述

9.数据库

对于整个系统而言,最终所有的流量的查询和写入都落在数据库上,数据库是支撑系统高并发能力的核心。怎么降低数据库的压力,提升数据库的性能是支撑高并发的基石。主要的方式就是通过读写分离和分库分表来解决这个问题。
对于整个系统而言,流量应该是一个漏斗的形式。比如我们的日活用户DAU有20万,实际可能每天来到提单页的用户只有3万QPS,最终转化到下单支付成功的QPS只有1万。那么对于系统来说读是大于写的,这时候可以通过读写分离的方式来降低数据库的压力。

在这里插入图片描述

读写分离也就是相当于数据库集群的方式降低了单节点的压力,而面对数据的急剧增长,原来的单库单表的存储方式已经无法支撑整个业务的发展,这时候就需要对数据库进行分库分表了。针对微服务而言垂直的分库本身已经做过的,剩下大部分都是分表的方案了。

9.1 水平分表

首先根据业务场景来决定使用什么字段作为分表的字段,比如我们现在日订单1000万,我们大部分的场景来源于C端,我们可以用user_id作为sharding_key,数据查询支持到最近3个月的订单,超过三个月的做归档处理,那么3个月的数据量就是9亿,可以分1024张表,那么每张表的数据大概就在100万左右。
比如用户id为100,那我们都经过hash(100),然后对1024取模,就可以落到对应的表上了。

9.2分表后的ID唯一性

因为我们主键默认都是自增的,那么分表后的主键在不同表就肯定会有冲突了。有几个办法考虑:
1.设定步长,比如1-1024张表我们分别设定1-1024的基础步长,这样主键落到不同的表就不会冲突了。
2.分布式ID,自己实现一套分布式ID生成算法或者使用开源的比如雪花算法
3.分表后不使用主键作为查询依据,而是每张表单独新增一个字段作为唯一主键使用,比如订单表订单号是唯一的,不管最终落在哪张表都基于订单号作为查询依据,更新也是一样。

9.3主从同步原理

1.master提交完事务后,写入binlog
2.slave连接到master,获取binlog
3.master创建dump线程,推送binlog到slave
4.slave启动一个IO线程读取同步过来的master的binlog,记录到relay log中继日志中
5.slave在启动一个sql线程读取的relay log事件并在slave执行,完成同步
6.slave记录自己的binlog
在这里插入图片描述
由于MySQL默认的复制方式是异步的,主库把日志发送给从库后不关心从库是否已经处理,这样会产生一个问题就是假设主库挂了,从库处理失败,这时候从库升为主库后,日志就丢失了。由此产生两个概念。

9.3.1全同步复制

主库写入binlog后强制同步日志到从库,所有的从库都执行完成后才返回给客户端,但是很明显这种方式会严重影响性能。

9.3.2半同步复制

和全同步不同的是,半同步复制的路基是这样的,从库写入日志成功后返回ACK确认给主库,主库收到至少一个从库的确认就认为写操作完成。

10.缓存

缓存作为高性能的代表,在某些特殊业务可能承担90%以上的热点流量,对于一些活动比如秒杀这种并发QPS可能几十万的场景,引入缓存事先预热可以大幅度降低对数据库的压力。10万的QPS对于单机的数据库来说可能就挂了,但是对于如Redis这样的缓存来说就完全不是问题。
在这里插入图片描述
以秒杀系统举例,活动预热商品信息可以提前缓存提供查询服务,活动库存数据可以提前缓存,下单流程可以完全走缓存扣减,秒杀结束后再异步写入数据库,数据库承担的压力就小的太多了。当然,引用缓存之后还要考虑缓存击穿、雪崩、热点一系列的问题了。

10.1热key问题

所谓的热key问题就是,突然有几十万的请求访问Redis上的某个特定key,那么这样会造成流量过于集中,达到物理网卡上限,从而导致这台Redis的服务器宕机引发雪崩。
在这里插入图片描述
针对热key的解决方案:
1.提前把热key打散到不同的服务器,降低压力
2.加入二级缓存,提前加载热key数据到内存中,如果Redis宕机,走内存查询

10.2缓存击穿

缓存击穿的概念就单个key并发访问过高,过期时导致所有请求直接打到DB上,这个和热key的问题比较类似,只是说的点在于过期导致请求全部打到DB上而已。
解决方案:
1.加锁更新,比如请求查询A,发现缓存中没有,对A这个key加锁,同时去数据库查询数据,写入缓存,在返回给用户,这样后面的请求就可以从缓存中拿到数据了。
2.将近期时间组合写在value中,通过异步的方式不断的刷新过期的时间,防止此类现象。

在这里插入图片描述

10.3缓存穿透

缓存穿透是指查询不存在缓存中的数据,每次请求都会达到DB,就像缓存不存在一样。
在这里插入图片描述
针对这个问题,加一层布隆过滤器。布隆过滤器的原理是在你存入数据的时候,会通过散列函数将他映射为一个位数组中的K个点。同时把他们设置为1.
这样当用户再次来查询A,而A在布隆过滤器值为0,直接返回,就不会产生击穿请求达到DB了。
显然,使用布隆过滤器之后会有一个问题就是误判,因为他本身就是一个数组,可能会有多个值落到同一个位置,那么理论上来说只要我们的数组长度够长,误判的概率就会越低,这种问题就根据实际情况来就好了。
在这里插入图片描述

10.4缓存雪崩

当某一时刻发生大规模的缓存失效的情况,比如你的缓存服务宕机了,会有大量的请求进来直接打到DB上,这样可能导致整个系统的崩溃,称为雪崩。雪崩和击穿、热key的问题不太一样的是,他是指大规模的缓存都过期失效了。
在这里插入图片描述
针对雪崩几个解决方案:
1.针对不同的key设置不同的过期时间,避免同时失效
2.限流,如果Redis宕机,可以限流,避免同时刻大量请求打崩DB
3.二级缓存,同热key的方案。

11.稳定性

在这里插入图片描述

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值