福州白癜风医院 http://pf.39.net/bdfyy/zjft/180509/6223398.html
这是一道很常见的面试题,但是大多数人并不知道怎么回答,这种问题其实可以有很多形式的提问方式,你一定见过而且感觉无从下手:
面对业务急剧增长你怎么处理?
业务量增长10倍、倍怎么处理?
你们系统怎么支撑高并发的?
怎么设计一个高并发系统?
高并发系统都有什么特点?
......
诸如此类,问法很多,但是面试这种类型的问题,看着很难无处下手,但是我们可以有一个常规的思路去回答,就是围绕支撑高并发的业务场景怎么设计系统才合理?如果你能想到这一点,那接下来我们就可以围绕硬件和软件层面怎么支撑高并发这个话题去阐述了。本质上,这个问题就是综合考验你对各个细节是否知道怎么处理,是否有经验处理过而已。
面对超高的并发,首先硬件层面机器要能扛得住,其次架构设计做好微服务的拆分,代码层面各种缓存、削峰、解耦等等问题要处理好,数据库层面做好读写分离、分库分表,稳定性方面要保证有监控,熔断限流降级该有的必须要有,发生问题能及时发现处理。这样从整个系统设计方面就会有一个初步的概念。
微服务架构演化在互联网早期的时候,单体架构就足以支撑起日常的业务需求,大家的所有业务服务都在一个项目里,部署在一台物理机器上。所有的业务包括你的交易系统、会员信息、库存、商品等等都夹杂在一起,当流量一旦起来之后,单体架构的问题就暴露出来了,机器挂了所有的业务全部无法使用了。
于是,集群架构的架构开始出现,单机无法抗住的压力,最简单的办法就是水平拓展横向扩容了,这样,通过负载均衡把压力流量分摊到不同的机器上,暂时是解决了单点导致服务不可用的问题。
但是随着业务的发展,在一个项目里维护所有的业务场景使开发和代码维护变得越来越困难,一个简单的需求改动都需要发布整个服务,代码的合并冲突也会变得越来越频繁,同时线上故障出现的可能性越大。微服务的架构模式就诞生了。
把每个独立的业务拆分开独立部署,开发和维护的成本降低,集群能承受的压力也提高了,再也不会出现一个小小的改动点需要牵一发而动全身了。
以上的点从高并发的角度而言,似乎都可以归类为通过服务拆分和集群物理机器的扩展提高了整体的系统抗压能力,那么,随之拆分而带来的问题也就是高并发系统需要解决的问题。
RPC微服务化的拆分带来的好处和便利性是显而易见的,但是与此同时各个微服务之间的通信就需要考虑了。传统HTTP的通信方式对性能是极大的浪费,这时候就需要引入诸如Dubbo类的RPC框架,基于TCP长连接的方式提高整个集群通信的效率。
我们假设原来来自客户端的QPS是的话,那么通过负载均衡策略分散到每台机器就是,而HTTP改为RPC之后接口的耗时缩短了,单机和整体的QPS就提升了。而RPC框架本身一般都自带负载均衡、熔断降级的机制,可以更好的维护整个系统的高可用性。
那么说完RPC,作为基本上国内普遍的选择Dubbo的一些基本原理就是接下来的问题。
Dubbo工作原理服务启动的时候,provider和consumer根据配置信息,连接到注册中心register,分别向注册中心注册和订阅服务
register根据服务订阅关系,返回provider信息到consumer,同时consumer会把provider信息缓存到本地。如果信息有变更,consumer会收到来自register的推送
consumer生成代理对象,同时根据负载均衡策略,选择一台provider,同时定时向monitor记录接口的调用次数和时间信息
拿到代理对象之后,consumer通过代理对象发起接口调用
provider收到请求后对数据进行反序列化,然后通过代理调用具体的接口实现
Dubbo负载均衡策略加权随机:假设我们有一组服务器servers=[A,B,C],他们对应的权重为weights=[5,3,2],权重总和为10。现在把这些权重值平铺在一维坐标值上,[0,5)区间属于服务器A,[5,8)区间属于服务器B,[8,10)区间属于服务器C。接下来通过随机数生成器生成一个范围在[0,10)之间的随机数,然后计算这个随机数会落到哪个区间上就可以了。
最小活跃数:每个服务提供者对应一个活跃数active,初始情况下,所有服务提供者活跃数均为0。每收到一个请求,活跃数加1,完成请求后则将活跃数减1。在服务运行一段时间后,性能好的服务提供者处理请求的速度更快,因此活跃数下降的也越快,此时这样的服务提供者能够优先获取到新的服务请求。
一致性hash:通过hash算法,把provider的invoke和随机节点生成hash,并将这个hash投射到[0,2^32-1]的圆环上,查询的时候根据key进行md5然后进行hash,得到第一个节点的值大于等于当前hash的invoker。
图片来自dubbo官方加权轮询:比如服务器A、B、C权重比为5:2:1,那么在8次请求中,服务器A将收到其中的5次请求,服务器B会收到其中的2次请求,服务器C则收到其中的1次请求。集群容错FailoverCluster失败自动切换:dubbo的默认容错方案,当调用失败时自动切换到其他可用的节点,具体的重试次数和间隔时间可用通过引用服务的时候配置,默认重试次数为1也就是只调用一次。
FailbackCluster快速失败:在调用失败,记录日志和调用信息,然后返回空结果给consumer,并且通过定时任务每隔5秒对失败的调用进行重试
FailfastCluster失败自动恢复:只会调用一次,失败后立刻抛出异常
FailsafeCluster失败安全:调用出现异常,记录日志不抛出,返回空结果
ForkingCluster并行调用多个服务提供者:通过线程池创建多个线程,并发调用多个provider,结果保存到阻塞队列,只要有一个provider成功返回了结果,就会立刻返回结果
BroadcastCluster广播模式:逐个调用每个provider,如果其中一台报错,在循环调用结束后,抛出异常。
消息队列对于MQ的作用大家都应该很了解了,削峰填谷、解耦。依赖消息队列,同步转异步的方式,可以降低微服务之间的耦合。
对于一些不需要同步执行的接口,可以通过引入消息队列的方式异步执行以提高接口响应时间。在交易完成之后需要扣库存,然后可能需要给会员发放积分,本质上,发积分的动作应该属于履约服务,对实时性的要求也不高,我们只要保证最终一致性也就是能履约成功就行了。对于这种同类性质的请求就可以走MQ异步,也就提高了系统抗压能力了。
对于消息队列而言,怎么在使用的时候保证消息的可靠性、不丢失?
消息可靠性消息丢失可能发生在生产者发送消息、MQ本身丢失消息、消费者丢失消息3个方面。
生产者丢失
生产者丢失消息的可能点在于程序发送失败抛异常了没有重试处理,或者发送的过程成功但是过程中网络闪断MQ没收到,消息就丢失了。
由于同步发送的一般不会出现这样使用方式,所以我们就不考虑同步发送的问题,我们基于异步发送的场景来说。
异步发送分为两个方式:异步有回调和异步无回调,无回调的方式,生产者发送完后不管结果可能就会造成消息丢失,而通过异步发送+回调通知+本地消息表的形式我们就可以做出一个解决方案。以下单的场景举例。
下单后先保存本地数据和MQ消息表,这时候消息的状态是发送中,如果本地事务失败,那么下单失败,事务回滚。下单成功,直接返回客户端成功,异步发送MQ消息MQ回调通知消息发送结果,对应更新数据库MQ发送状态JOB轮询超过一定时间(时间根据业务配置)还未发送成功的消息去重试在监控平台配置或者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收到了生产者的消息,但是没有收到二次确认,不能投递的消息。
实现原理如下:
生产者先发送一条半事务消息到MQMQ收到消息后返回ack确认生产者开始执行本地事务如果事务执行成功发送