面试官:面对千万级、亿级流量怎么处理?

这个《我想进大厂》系列的最后一篇,终结篇。可能有点标题党了,可是我想要表达的意思和目的是一致的。java

这是一道很常见的面试题,可是大多数人并不知道怎么回答,这种问题其实能够有不少形式的提问方式,你必定见过并且感受无从下手:mysql

面对业务急剧增加你怎么处理?面试

业务量增加10倍、100倍怎么处理?redis

大家系统怎么支撑高并发的?算法

怎么设计一个高并发系统?sql

高并发系统都有什么特色?数据库

… …数组

诸如此类,问法不少,可是面试这种类型的问题,看着很难无处下手,可是咱们能够有一个常规的思路去回答,就是围绕支撑高并发的业务场景怎么设计系统才合理?若是你能想到这一点,那接下来咱们就能够围绕硬件和软件层面怎么支撑高并发这个话题去阐述了。本质上,这个问题就是综合考验你对各个细节是否知道怎么处理,是否有经验处理过而已。缓存

面对超高的并发,首先硬件层面机器要能扛得住,其次架构设计作好微服务的拆分,代码层面各类缓存、削峰、解耦等等问题要处理好,数据库层面作好读写分离、分库分表,稳定性方面要保证有监控,熔断限流降级该有的必需要有,发生问题能及时发现处理。这样从整个系统设计方面就会有一个初步的概念。安全

微服务架构演化

在互联网早期的时候,单体架构就足以支撑起平常的业务需求,你们的全部业务服务都在一个项目里,部署在一台物理机器上。全部的业务包括你的交易系统、会员信息、库存、商品等等都夹杂在一块儿,当流量一旦起来以后,单体架构的问题就暴露出来了,机器挂了全部的业务所有没法使用了。

因而,集群架构的架构开始出现,单机没法抗住的压力,最简单的办法就是水平拓展横向扩容了,这样,经过负载均衡把压力流量分摊到不一样的机器上,暂时是解决了单点致使服务不可用的问题。

可是随着业务的发展,在一个项目里维护全部的业务场景使开发和代码维护变得愈来愈困难,一个简单的需求改动都须要发布整个服务,代码的合并冲突也会变得愈来愈频繁,同时线上故障出现的可能性越大。微服务的架构模式就诞生了。

把每一个独立的业务拆分开独立部署,开发和维护的成本下降,集群能承受的压力也提升了,不再会出现一个小小的改动点须要牵一发而动全身了。

以上的点从高并发的角度而言,彷佛均可以归类为经过服务拆分和集群物理机器的扩展提升了总体的系统抗压能力,那么,随之拆分而带来的问题也就是高并发系统须要解决的问题。

RPC

微服务化的拆分带来的好处和便利性是显而易见的,可是与此同时各个微服务之间的通讯就须要考虑了。传统HTTP的通讯方式对性能是极大的浪费,这时候就须要引入诸如Dubbo类的RPC框架,基于TCP长链接的方式提升整个集群通讯的效率。

咱们假设原来来自客户端的QPS是9000的话,那么经过负载均衡策略分散到每台机器就是3000,而HTTP改成RPC以后接口的耗时缩短了,单机和总体的QPS就提高了。而RPC框架自己通常都自带负载均衡、熔断降级的机制,能够更好的维护整个系统的高可用性。

那么说完RPC,做为基本上国内广泛的选择Dubbo的一些基本原理就是接下来的问题。

Dubbo工做原理

  1. 服务启动的时候,provider和consumer根据配置信息,链接到注册中心register,分别向注册中心注册和订阅服务

  2. register根据服务订阅关系,返回provider信息到consumer,同时consumer会把provider信息缓存到本地。若是信息有变动,consumer会收到来自register的推送

  3. consumer生成代理对象,同时根据负载均衡策略,选择一台provider,同时定时向monitor记录接口的调用次数和时间信息

  4. 拿到代理对象以后,consumer经过代理对象发起接口调用

  5. provider收到请求后对数据进行反序列化,而后经过代理调用具体的接口实现

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的invoker。

图片来自dubbo官方

  1. 加权轮询:好比服务器 A、B、C 权重比为 5:2:1,那么在8次请求中,服务器 A 将收到其中的5次请求,服务器 B 会收到其中的2次请求,服务器 C 则收到其中的1次请求。

集群容错

  1. Failover Cluster失败自动切换:dubbo的默认容错方案,当调用失败时自动切换到其余可用的节点,具体的重试次数和间隔时间可用经过引用服务的时候配置,默认重试次数为1也就是只调用一次。

  2. Failback Cluster快速失败:在调用失败,记录日志和调用信息,而后返回空结果给consumer,而且经过定时任务每隔5秒对失败的调用进行重试

  3. Failfast Cluster失败自动恢复:只会调用一次,失败后马上抛出异常

  4. Failsafe Cluster失败安全:调用出现异常,记录日志不抛出,返回空结果

  5. Forking Cluster并行调用多个服务提供者:经过线程池建立多个线程,并发调用多个provider,结果保存到阻塞队列,只要有一个provider成功返回告终果,就会马上返回结果

  6. Broadcast Cluster广播模式:逐个调用每一个provider,若是其中一台报错,在循环调用结束后,抛出异常。

消息队列

对于MQ的做用你们都应该很了解了,削峰填谷、解耦。依赖消息队列,同步转异步的方式,能够下降微服务之间的耦合。

对于一些不须要同步执行的接口,能够经过引入消息队列的方式异步执行以提升接口响应时间。在交易完成以后须要扣库存,而后可能须要给会员发放积分,本质上,发积分的动做应该属于履约服务,对实时性的要求也不高,咱们只要保证最终一致性也就是能履约成功就好了。对于这种同类性质的请求就能够走MQ异步,也就提升了系统抗压能力了。

对于消息队列而言,怎么在使用的时候保证消息的可靠性、不丢失?

消息可靠性

消息丢失可能发生在生产者发送消息、MQ自己丢失消息、消费者丢失消息3个方面。

生产者丢失

生产者丢失消息的可能点在于程序发送失败抛异常了没有重试处理,或者发送的过程成功可是过程当中网络闪断MQ没收到,消息就丢失了。

因为同步发送的通常不会出现这样使用方式,因此咱们就不考虑同步发送的问题,咱们基于异步发送的场景来讲。

异步发送分为两个方式:异步有回调和异步无回调,无回调的方式,生产者发送完后无论结果可能就会形成消息丢失,而经过异步发送+回调通知+本地消息表的形式咱们就能够作出一个解决方案。如下单的场景举例。

  1. 下单后先保存本地数据和MQ消息表,这时候消息的状态是发送中,若是本地事务失败,那么下单失败,事务回滚。
  2. 下单成功,直接返回客户端成功,异步发送MQ消息
  3. MQ回调通知消息发送结果,对应更新数据库MQ发送状态
  4. JOB轮询超过必定时间(时间根据业务配置)还未发送成功的消息去重试
  5. 在监控平台配置或者JOB程序处理超过必定次数一直发送不成功的消息,告警,人工介入。


通常而言,对于大部分场景来讲异步回调的形式就能够了,只有那种须要彻底保证不能丢失消息的场景咱们作一套完整的解决方案。

MQ丢失

若是生产者保证消息发送到MQ,而MQ收到消息后还在内存中,这时候宕机了又没来得及同步给从节点,就有可能致使消息丢失。

好比RocketMQ:

RocketMQ分为同步刷盘和异步刷盘两种方式,默认的是异步刷盘,就有可能致使消息还未刷到硬盘上就丢失了,能够经过设置为同步刷盘的方式来保证消息可靠性,这样即便MQ挂了,恢复的时候也能够从磁盘中去恢复消息。

好比Kafka也能够经过配置作到:

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

虽然咱们能够经过配置的方式来达到MQ自己高可用的目的,可是都对性能有损耗,怎样配置须要根据业务作出权衡。

消费者丢失

消费者丢失消息的场景:消费者刚收到消息,此时服务器宕机,MQ认为消费者已经消费,不会重复发送消息,消息丢失。

RocketMQ默认是须要消费者回复ack确认,而kafka须要手动开启配置关闭自动offset。

消费方不返回ack确认,重发的机制根据MQ类型的不一样发送时间间隔、次数都不尽相同,若是重试超过次数以后会进入死信队列,须要手工来处理了。(Kafka没有这些)

消息的最终一致性

事务消息能够达到分布式事务的最终一致性,事务消息就是MQ提供的相似XA的分布式事务能力。

半事务消息就是MQ收到了生产者的消息,可是没有收到二次确认,不能投递的消息。

实现原理以下:

  1. 生产者先发送一条半事务消息到MQ
  2. MQ收到消息后返回ack确认
  3. 生产者开始执行本地事务
  4. 若是事务执行成功发送commit到MQ,失败发送rollback
  5. 若是MQ长时间未收到生产者的二次确认commit或者rollback,MQ对生产者发起消息回查
  6. 生产者查询事务执行最终状态
  7. 根据查询事务状态再次提交二次确认

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

数据库

对于整个系统而言,最终全部的流量的查询和写入都落在数据库上,数据库是支撑系统高并发能力的核心。怎么下降数据库的压力,提高数据库的性能是支撑高并发的基石。主要的方式就是经过读写分离和分库分表来解决这个问题。

对于整个系统而言,流量应该是一个漏斗的形式。好比咱们的日活用户DAU有20万,实际可能天天来到提单页的用户只有3万QPS,最终转化到下单支付成功的QPS只有1万。那么对于系统来讲读是大于写的,这时候能够经过读写分离的方式来下降数据库的压力。

读写分离也就至关于数据库集群的方式下降了单节点的压力。而面对数据的急剧增加,原来的单库单表的存储方式已经没法支撑整个业务的发展,这时候就须要对数据库进行分库分表了。针对微服务而言垂直的分库自己已是作过的,剩下大部分都是分表的方案了。

水平分表

首先根据业务场景来决定使用什么字段做为分表字段(sharding_key),好比咱们如今日订单1000万,咱们大部分的场景来源于C端,咱们能够用user_id做为sharding_key,数据查询支持到最近3个月的订单,超过3个月的作归档处理,那么3个月的数据量就是9亿,能够分1024张表,那么每张表的数据大概就在100万左右。

好比用户id为100,那咱们都通过hash(100),而后对1024取模,就能够落到对应的表上了。

分表后的ID惟一性

由于咱们主键默认都是自增的,那么分表以后的主键在不一样表就确定会有冲突了。有几个办法考虑:

  1. 设定步长,好比1-1024张表咱们分别设定1-1024的基础步长,这样主键落到不一样的表就不会冲突了。
  2. 分布式ID,本身实现一套分布式ID生成算法或者使用开源的好比雪花算法这种
  3. 分表后不使用主键做为查询依据,而是每张表单独新增一个字段做为惟一主键使用,好比订单表订单号是惟一的,无论最终落在哪张表都基于订单号做为查询依据,更新也同样。

主从同步原理

  1. master提交完事务后,写入binlog
  2. slave链接到master,获取binlog
  3. master建立dump线程,推送binglog到slave
  4. slave启动一个IO线程读取同步过来的master的binlog,记录到relay log中继日志中
  5. slave再开启一个sql线程读取relay log事件并在slave执行,完成同步
  6. slave记录本身的binglog

因为mysql默认的复制方式是异步的,主库把日志发送给从库后不关心从库是否已经处理,这样会产生一个问题就是假设主库挂了,从库处理失败了,这时候从库升为主库后,日志就丢失了。由此产生两个概念。

全同步复制

主库写入binlog后强制同步日志到从库,全部的从库都执行完成后才返回给客户端,可是很显然这个方式的话性能会受到严重影响。

半同步复制

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

缓存

缓存做为高性能的表明,在某些特殊业务可能承担90%以上的热点流量。对于一些活动好比秒杀这种并发QPS可能几十万的场景,引入缓存事先预热能够大幅下降对数据库的压力,10万的QPS对于单机的数据库来讲可能就挂了,可是对于如redis这样的缓存来讲就彻底不是问题。

以秒杀系统举例,活动预热商品信息能够提早缓存提供查询服务,活动库存数据能够提早缓存,下单流程能够彻底走缓存扣减,秒杀结束后再异步写入数据库,数据库承担的压力就小的太多了。固然,引入缓存以后就还要考虑缓存击穿、雪崩、热点一系列的问题了。

热key问题

所谓热key问题就是,忽然有几十万的请求去访问redis上的某个特定key,那么这样会形成流量过于集中,达到物理网卡上限,从而致使这台redis的服务器宕机引起雪崩。

针对热key的解决方案:

  1. 提早把热key打散到不一样的服务器,下降压力
  2. 加入二级缓存,提早加载热key数据到内存中,若是redis宕机,走内存查询

缓存击穿

缓存击穿的概念就是单个key并发访问太高,过时时致使全部请求直接打到db上,这个和热key的问题比较相似,只是说的点在于过时致使请求所有打到DB上而已。

解决方案:

  1. 加锁更新,好比请求查询A,发现缓存中没有,对A这个key加锁,同时去数据库查询数据,写入缓存,再返回给用户,这样后面的请求就能够从缓存中拿到数据了。
  2. 将过时时间组合写在value中,经过异步的方式不断的刷新过时时间,防止此类现象。

缓存穿透

缓存穿透是指查询不存在缓存中的数据,每次请求都会打到DB,就像缓存不存在同样。

针对这个问题,加一层布隆过滤器。布隆过滤器的原理是在你存入数据的时候,会经过散列函数将它映射为一个位数组中的K个点,同时把他们置为1。

这样当用户再次来查询A,而A在布隆过滤器值为0,直接返回,就不会产生击穿请求打到DB了。

显然,使用布隆过滤器以后会有一个问题就是误判,由于它自己是一个数组,可能会有多个值落到同一个位置,那么理论上来讲只要咱们的数组长度够长,误判的几率就会越低,这种问题就根据实际状况来就行了。

缓存雪崩

当某一时刻发生大规模的缓存失效的状况,好比你的缓存服务宕机了,会有大量的请求进来直接打到DB上,这样可能致使整个系统的崩溃,称为雪崩。雪崩和击穿、热key的问题不太同样的是,他是指大规模的缓存都过时失效了。

针对雪崩几个解决方案:

  1. 针对不一样key设置不一样的过时时间,避免同时过时
  2. 限流,若是redis宕机,能够限流,避免同时刻大量请求打崩DB
  3. 二级缓存,同热key的方案。

稳定性

熔断

好比营销服务挂了或者接口大量超时的异常状况,不能影响下单的主链路,涉及到积分的扣减一些操做能够在过后作补救。

限流

对突发如大促秒杀类的高并发,若是一些接口不作限流处理,可能直接就把服务打挂了,针对每一个接口的压测性能的评估作出合适的限流尤其重要。

降级

熔断以后实际上能够说就是降级的一种,以熔断的举例来讲营销接口熔断以后降级方案就是短期内再也不调用营销的服务,等到营销恢复以后再调用。

预案

通常来讲,就算是有统一配置中心,在业务的高峰期也是不容许作出任何的变动的,可是经过配置合理的预案能够在紧急的时候作一些修改。

核对

针对各类分布式系统产生的分布式事务一致性或者受到***致使的数据异常,很是须要核对平台来作最后的兜底的数据验证。好比下游支付系统和订单系统的金额作核对是否正确,若是收到中间人***落库的数据是否保证正确性。

总结

其实能够看到,怎么设计高并发系统这个问题自己他是不难的,无非是基于你知道的知识点,从物理硬件层面到软件的架构、代码层面的优化,使用什么中间件来不断提升系统的抗压能力。可是这个问题自己会带来更多的问题,微服务自己的拆分带来了分布式事务的问题,http、RPC框架的使用带来了通讯效率、路由、容错的问题,MQ的引入带来了消息丢失、积压、事务消息、顺序消息的问题,缓存的引入又会带来一致性、雪崩、击穿的问题,数据库的读写分离、分库分表又会带来主从同步延迟、分布式ID、事务一致性的问题,而为了解决这些问题咱们又要不断的加入各类措施熔断、限流、降级、离线核对、预案处理等等来防止和追溯这些问题。

这篇文章结合了以前的文章的一些内容,实际上最开始的时候就是想写这一篇,发现篇幅实在太大了并且内容很差归纳,因此就拆分了几篇开始写,这一篇算是对前面内容的一个概括和总结吧,不是我为了水。

相关文章
相关标签/搜索