这是一道很常见的面试题,但是大多数人并不知道怎么回答,这种问题其实可以有很多形式的提问方式,你一定见过而且感觉无从下手:
面对业务急剧增长你怎么处理?
业务量增长10倍、100倍怎么处理?
你们系统怎么支撑高并发的?
怎么设计一个高并发系统?
高并发系统都有什么特点?
... ...
诸如此类,问法很多,但是面试这种类型的问题,看着很难无处下手,但是我们可以有一个常规的思路去回答,就是围绕支撑高并发的业务场景怎么设计系统才合理?如果你能想到这一点,那接下来我们就可以围绕硬件和软件层面怎么支撑高并发这个话题去阐述了。本质上,这个问题就是综合考验你对各个细节是否知道怎么处理,是否有经验处理过而已。
面对超高的并发,首先硬件层面机器要能扛得住,其次架构设计做好微服务的拆分,代码层面各种缓存、削峰、解耦等等问题要处理好,数据库层面做好读写分离、分库分表,稳定性方面要保证有监控,熔断限流降级该有的必须要有,发生问题能及时发现处理。这样从整个系统设计方面就会有一个初步的概念。
微服务架构演化
在互联网早期的时候,单体架构就足以支撑起日常的业务需求,大家的所有业务服务都在一个项目里,部署在一台物理机器上。所有的业务包括你的交易系统、会员信息、库存、商品等等都夹杂在一起,当流量一旦起来之后,单体架构的问题就暴露出来了,机器挂了所有的业务全部无法使用了。
于是,集群架构的架构开始出现,单机无法抗住的压力,简单的办法就是水平拓展横向扩容了,这样,通过负载均衡把压力流量分摊到不同的机器上,暂时是解决了单点导致服务不可用的问题。
但是随着业务的发展,在一个项目里维护所有的业务场景使开发和代码维护变得越来越困难,一个简单的需求改动都需要发布整个服务,代码的合并冲突也会变得越来越频繁,同时线上故障出现的可能性越大。微服务的架构模式就诞生了。
把每个独立的业务拆分开独立部署,开发和维护的成本降低,集群能承受的压力也提高了,再也不会出现一个小小的改动点需要牵一发而动全身了。
以上的点从高并发的角度而言,似乎都可以归类为通过服务拆分和集群物理机器的扩展提高了整体的系统抗压能力,那么,随之拆分而带来的问题也就是高并发系统需要解决的问题。
RPC
微服务化的拆分带来的好处和便利性是显而易见的,但是与此同时各个微服务之间的通信就需要考虑了。传统HTTP的通信方式对性能是极大的浪费,这时候就需要引入诸如Dubbo类的RPC框架,基于TCP长连接的方式提高整个集群通信的效率。
我们假设原来来自客户端的QPS是9000的话,那么通过负载均衡策略分散到每台机器就是3000,而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。
加权轮询:比如服务器 A、B、C 权重比为 5:2:1,那么在8次请求中,服务器 A 将收到其中的5次请求,服务器 B 会收到其中的2次请求,服务器 C 则收到其中的1次请求。
集群容错
Failover Cluster失败自动切换:dubbo的默认容错方案,当调用失败时自动切换到其他可用的节点,具体的重试次数和间隔时间可用通过引用服务的时候配置,默认重试次数为1也就是只调用一次。 Failback Cluster快速失败:在调用失败,记录日志和调用信息,然后返回空结果给consumer,并且通过定时任务每隔5秒对失败的调用进行重试 Failfast Cluster失败自动恢复:只会调用一次,失败后立刻抛出异常 Failsafe Cluster失败安全:调用出现异常,记录日志不抛出,返回空结果 Forking Cluster并行调用多个服务提供者:通过线程池创建多个线程,并发调用多个provider,结果保存到阻塞队列,只要有一个provider成功返回了结果,就会立刻返回结果 Broadcast Cluster广播模式:逐个调用每个provider,如果其中一台报错,在循环调用结束后,抛出异常。
消息队列
消息可靠性
下单后先保存本地数据和MQ消息表,这时候消息的状态是发送中,如果本地事务失败,那么下单失败,事务回滚。 下单成功,直接返回客户端成功,异步发送MQ消息 MQ回调通知消息发送结果,对应更新数据库MQ发送状态 JOB轮询超过一定时间(时间根据业务配置)还未发送成功的消息去重试 在监控平台配置或者JOB程序处理超过一定次数一直发送不成功的消息,告警,人工介入。
acks=all 只有参与复制的所有节点全部收到消息,才返回生产者成功。这样的话除非所有的节点都挂了,消息才会丢失。
replication.factor=N,设置大于1的数,这会要求每个partion至少有2个副本
min.insync.replicas=N,设置大于1的数,这会要求leader至少感知到一个follower还保持着连接
retries=N,设置一个非常大的值,让生产者发送失败一直重试
消息的终一致性
生产者先发送一条半事务消息到MQ MQ收到消息后返回ack确认 生产者开始执行本地事务 如果事务执行成功发送commit到MQ,失败发送rollback 如果MQ长时间未收到生产者的二次确认commit或者rollback,MQ对生产者发起消息回查 生产者查询事务执行终状态 根据查询事务状态再次提交二次确认
数据库
水平分表
分表后的ID性
设定步长,比如1-1024张表我们分别设定1-1024的基础步长,这样主键落到不同的表就不会冲突了。 分布式ID,自己实现一套分布式ID生成算法或者使用开源的比如雪花算法这种 分表后不使用主键作为查询依据,而是每张表单独新增一个字段作为主键使用,比如订单表订单号是的,不管终落在哪张表都基于订单号作为查询依据,更新也一样。
主从同步原理
master提交完事务后,写入binlog slave连接到master,获取binlog master创建dump线程,推送binglog到slave slave启动一个IO线程读取同步过来的master的binlog,记录到relay log中继日志中 slave再开启一个sql线程读取relay log事件并在slave执行,完成同步 slave记录自己的binglog
缓存
热key问题
提前把热key打散到不同的服务器,降低压力 加入二级缓存,提前加载热key数据到内存中,如果redis宕机,走内存查询
缓存击穿
加锁更新,比如请求查询A,发现缓存中没有,对A这个key加锁,同时去数据库查询数据,写入缓存,再返回给用户,这样后面的请求就可以从缓存中拿到数据了。 将过期时间组合写在value中,通过异步的方式不断的刷新过期时间,防止此类现象。
缓存穿透
缓存雪崩
针对不同key设置不同的过期时间,避免同时过期 限流,如果redis宕机,可以限流,避免同时刻大量请求打崩DB 二级缓存,同热key的方案。