当前位置: 首页 > 科技观察

做一个worker,支撑一个亿级高并发的系统是什么感觉?

时间:2023-03-13 14:11:33 科技观察

这是一个很常见的面试问题,但是大多数人都不知道怎么回答。我见过它,但似乎无法弄清楚。图片来自Pexels如何应对业务的快速增长?业务量增长10倍、100倍如何应对?你的系统是如何支持高并发的?如何设计高并发系统?高并发系统有什么特点?.....等等,出题方式有很多种,但是这类面试题好像很难无处下手,但是我们可以有一个常规的方式思考回答,即围绕支持高并发业务场景,如何设计一个系统才是合理的?如果你能想到这一点,那么我们就可以详细阐述如何在硬件和软件层面支持高并发的话题。从本质上讲,这道题是综合考查你是否懂得处理每一个细节,是否有处理过的经验。面对超高并发,首先硬件层面的机器要能承受,其次要做架构设计,拆分微服务,缓存、削峰、解耦等各种问题在必须妥善处理代码级别。在数据库层面,要做好读写分离,分库分表。在稳定性方面,应确保监控。一些必要的保险丝,限流降级一定要有,发现问题及时处理。这样,整个系统的设计就会有一个初步的概念。微服务架构的演进互联网早期,单一架构足以支撑日常业??务需求。我们所有的业务服务都部署在一台物理机器上的一个项目中。所有的业务,包括你的交易系统、会员信息、库存、商品等等都混在一起了。一旦流量上来,架构单一的问题就会暴露出来,宕机所有业务都不可用。于是,集群架构的架构开始出现。单机无法承受的压力,最简单的方法就是横向、横向扩张。这样通过负载均衡的方式将压力流量分配到不同的机器上,暂时解决了单点导致服务不可用的问题。但是随着业务的发展,在一个项目中维护所有的业务场景,使得开发和代码维护的难度越来越大。一个简单的需求变更需要发布整个服务,代码合并冲突会变得越来越困难。越频繁,越有可能同时出现在线故障。微服务的架构模式诞生了。通过拆分各个独立的业务,独立部署,降低了开发和维护的成本,同时也增加了集群能够承受的压力。不会再有一个小的变化需要牵动全身。从高并发的角度来看,以上几点似乎可以归为通过服务拆分和集群物理机扩展来提高系统整体的抗压能力。那么,拆分带来的问题是高并发系统需要解决的问题。RPC微服务拆分带来的好处和便利是显而易见的,但同时需要考虑各个微服务之间的通信。传统的HTTP通信方式是对性能的极大浪费。这时候就需要引入Dubbo这样的RPC框架来提高整个集群基于TCP长连接方式的通信效率。我们假设如果客户端原来的QPS是9000,那么通过负载均衡策略分配给每台机器3000。HTTP改为RPC后,缩短了接口时间,提高了单机和整体QPS。RPC框架本身一般都有自己的负载均衡和断路器降级机制,可以更好的维护整个系统的高可用。那么说完RPC,接下来就是Dubbo这种在国内基本是普遍选择的一些基本原理了。Dubbo的工作原理:服务启动时,提供者和消费者根据配置信息连接到注册中心进行注册,分别向注册中心注册和订阅服务。注册器根据服务订阅关系返回提供者信息给消费者,消费者会在本地缓存提供者信息。如果信息发生变化,消费者将收到来自收银机的推送。消费者生成一个代理对象,同时根据负载均衡策略选择一个提供者,并定时记录接口的调用次数和时间信息给监控器。消费者拿到代理对象后,通过代理对象发起接口调用。提供者收到请求后将数据反序列化,然后通过代理调用具体的接口实现。Dubbo负载均衡策略:加权随机:假设我们有一组服务器servers=[A,B,C],它们对应的权重为weights=[5,3,2],权重之和为10。现在拉平这些权重值在一个一维坐标值上,区间[0,5)属于服务器A,区间[5,8)属于服务器B,区间[8,10)属于服务器C。接下来通过随机数生成器生成一个[0,10)范围内的随机数,然后计算这个随机数会落在哪个区间。最小活跃数:每个服务商对应一个活跃的活跃数。最初,所有服务提供者的活跃数都是0,每收到一个请求,活跃数就加1,请求完成后,活跃数就减1。服务运行一段时间后,性能好的服务提供者处理请求的速度会更快,所以活跃度会下降得更快。这时,此类服务提供者可以优先获得新的服务请求。Consistencyhash:通过hash算法,利用provider的invoke和random节点生成hash,并将hash投影到[0,2^32-1]的环上。查询时先进行md5,然后根据key进行hash,得到第一个节点的值大于等于当前hash的invoker。Weightedroundrobin:比如服务器A、B、C的权重比为5:2:1,那么在8个请求中,服务器A收到5个,服务器B收到2个,服务器A将收到其中的2个。C收到其中一个请求。集群容错:Failover集群故障自动切换:Dubbo默认的容错方案,当调用失败时,会自动切换到其他可用节点。具体的重试次数和间隔时间可以在引用服务的时候配置。默认重试次数为1次。仅调用一次。FailbackClusterfailedquickly:当调用失败时,记录log和调用信息,然后返回空结果给消费者,通过定时任务每5秒重试失败的调用。FailfastCluster故障自动恢复:只会调用一次,故障后会立即抛出异常。FailsafeClusterfail-safe:调用发生异常,不抛出日志,返回空结果。ForkingCluster并行调用多个服务提供者:通过线程池创建多个线程,并发调用多个提供者,结果保存在阻塞队列中。只要有一个提供者成功返回结果,就会立即返回结果。BroadcastCluster广播方式:逐个调用每个provider,如果其中一个报错,循环调用结束后抛出异常。消息队列对于MQ的作用大家应该都知道,削峰填谷,解耦。依赖消息队列,从同步切换到异步,可以降低微服务之间的耦合度。对于一些不需要同步执行的接口,可以通过引入消息队列异步执行,提高接口响应时间。交易完成后,需要扣除库存,然后可能需要向会员发放积分。本质上,发积分这个动作应该属于fulfillmentservice,对实时性要求不高。我们只需要保证最终的一致性,即合约的成功履行。.对于相同性质的请求,可以异步使用MQ,提高了系统的抗压能力。对于消息队列,如何保证消息的可靠性,在使用的时候不丢失呢?消息可靠性消息丢失可能发生在三个方面:生产者发送消息、MQ自身丢失消息、消费者丢失消息。①生产者丢失。producer丢消息的可能原因是程序发送失败抛出异常而没有重试,或者发送过程成功但是过程中网络断开,没有收到MQ,消息是丢失的。由于这种使用方式一般不会出现同步发送,所以我们不考虑同步发送的问题,我们是基于异步发送的场景。异步发送分为异步有回调和异步无回调两种方式,不带回调,生产者发送后无论结果如何都可能丢失消息,通过异步发送+回调通知+本地消息表的形式我们将解决可以制作。下面是一个下单场景的例子:下单后,先保存本地数据和MQ消息表。此时消息的状态为发送中。如果本地事务失败,则订单失败并回滚事务。如果下单成功,则直接返回客户端成功,并异步发送MQ消息。MQ回调通知消息发送结果,并相应更新数据库MQ发送状态。JOB轮询超过一定时间(时间以业务配置为准)未发送成功消息重试。配置监控平台或JOB程序处理发送失败次数超过一定次数的消息、告警、人工干预。一般来说,对于大部分场景来说,异步回调的形式就足够了,只有那些需要充分保证消息不丢失的场景,我们才会提供完整的解决方案。②MQ丢失如果producer保证消息发送给MQ,而MQ收到消息后还在内存中,此时宕机,还没来得及同步到slave节点,可能会导致消息丢失.比如RocketMQ:RocketMQ分为同步刷机和异步刷机两种方式。默认是异步刷盘,可能会导致消息还没刷到硬盘就丢失了。可以通过设置为同步刷消息可靠性来保证,这样即使MQ挂了,恢复的时候也可以从磁盘中恢复消息。比如Kafka也可以这样配置:acks=all只有所有参与复制的节点都收到消息,生产者才会返回成功。在这种情况下,除非所有节点都宕机,否则消息将丢失。replication.factor=N,设置一个大于1的数,会要求每个partition至少有2份retries=N,设置一个非常大的值,这样producer一直发送失败,重试。虽然我们可以通过配置来达到MQ本身高可用的目的,但是会造成性能的损失。如何配置需要根据业务进行取舍。③消费者丢失消息的场景:消费者刚收到消息,此时服务器宕机,MQ认为消费者已经消费过,不会重复发送消息,消息丢失。默认情况下,RocketMQ需要消费者回复ack确认,而Kafka需要手动开启配置,关闭自动偏移。消费者不返回ack确认,重传机制根据MQ类型的发送时间间隔和次数不同而不同。如果重试次数超过了次数,就会进入死信队列,需要人工处理。(Kafka没有这些)消息的最终一致性事务消息可以实现分布式事务的最终一致性,事务消息是MQ提供的类XA分布式事务能力。半事务消息是MQ已经收到生产者的消息,但是还没有收到二次确认,无法投递。实现原理如下:生产者先向MQ发送半事务消息。MQ收到消息后返回ack确认。生产者开始执行本地交易。事务执行成功则发送commit给MQ,失败则发送rollback。如果MQ长时间没有收到producer的二次确认commit或rollback,MQ会向producer发起消息checkback。生产者查询事务执行的最终状态。根据查询交易状态再次提交二次确认。最后,如果MQ收到第二次确认提交,则可以将消息传递给消费者。否则,如果是回滚,消息将被保存并在3天后删除。就数据库而言,对于整个系统而言,所有的流量查询和写入最终都落在了数据库上。数据库是支撑系统高并发能力的核心。如何降低数据库的压力,提高数据库的性能,是支撑高并发的基石。主要的方式是通过读写分离,分库分表来解决这个问题。对于整个系统,流动应该是漏斗的形式。比如我们的日活跃用户DAU是20万。事实上,每天来到提单页面的用户量只有30000QPS,最终转化为订单支付成功的QPS也只有10000。那么对于系统来说,读大于写。这时候可以通过读写分离来减轻数据库的压力。读写分离相当于通过数据库集群的方式来降低单个节点的压力。面对数据的高速增长,原来单库单表的存储方式已经无法支撑整??个业务的发展。这时候就需要将数据库分为数据库和表。对于微服务来说,垂直分库本身就已经做好了,剩下的大部分都是分表解决方案。水平分片首先根据业务场景决定使用什么字段作为分片字段(sharding_key)。比如我们现在每天有1000万个订单,我们大部分的场景都是从C端来的。我们可以使用user_id作为sharding_key。数据查询支持最近3个月的订单,超过3个月的归档,那么3个月的数据量是9亿条,可以分为1024张表,所以每张表的数据大概是100万条。比如userid是100,那么我们都通过hash(100),然后对1024取模,就可以落到对应的表中了。分表后的ID唯一性是因为我们的主键默认是自增的,所以分表后的主键在不同的表中肯定会冲突。有几种方式可以考虑:设置步长,比如1-1024的表,我们分别设置1-1024的基本步长,这样主键落在不同的表就不会冲突。分布式ID,自己实现一套分布式ID生成算法或者使用开源的比如雪花算法。分表后,不再以主键作为查询依据,而是在每张表中增加一个新的字段作为唯一的主键。比如订单表的订单号是唯一的,无论它最终在哪个表中,都是以订单号为查询依据,更新相同。主从同步原理主从同步的原理是这样的:master提交事务后,写入binlog。slave连接master获取binlog。master创建转储线程并将binglog推送到slave。slave启动一个IO线程读取同步的master的binlog,记录到relaylog中。slave启动另一个sql线程读取relaylogevent并在slave上执行完成同步。从站记录自己的binglog。由于MySQL默认的复制方式是异步的,主库不关心从库发送日志到从库后是否处理过。主库后,日志丢失。由此产生了两个概念。①完全同步复制主库,将同步日志写入binlog后强制同步到从库,待所有从库执行完毕后返回给客户端,但显然这种方式会严重影响性能。②半同步复制和全同步复制的区别在于,半同步复制的逻辑如下。从库写入日志成功后,向主库返回ACK确认。主库至少收到一个从库的确认,才认为写操作完成。缓存作为高性能的代表,在某些特殊业务中,缓存可能会承担90%以上的热点流量。对于秒杀等一些并发QPS可能达到几十万的活动,引入缓存预热可以大大降低数据库的压力。10万的QPS对于单机数据库来说可能会挂掉,但是对于RedisCache这样的数据库来说完全不是问题。以秒杀系统为例,活动预热商品信息可以提前缓存提供查询服务,活动库存数据可以提前缓存,下单过程完全可以从缓存中扣除。秒杀完成后会异步写入数据库,对数据库的压力小。太多了。当然,引入缓存后,还得考虑缓存击穿、雪崩、热点等一系列问题。热键问题所谓热键问题就是突然有几十万个请求去访问redis上的某个特定的键,会导致流量过于集中,达到物理网卡的上限,会导致redis服务器宕机导致雪崩。热键解决方案:提前将热键分配到不同的服务器上,减轻压力。加入二级缓存,将hotkey数据提前加载到内存中,如果redis宕机,使用内存查询。缓存击穿缓存击穿的概念是单个key的并发访问过高,过期后,所有的请求都会直接发给DB。这个和hotkey的问题类似,但是重点是当key过期的时候所有的请求都会发给DB。.解决方案:加锁更新,比如请求查询A,发现没有缓存,加锁A的key,同时去数据库查询数据,写入缓存,然后返回用户,以便后续请求可以从缓存数据中获取。将过期时间组合写入value,异步刷新过期时间,防止此类现象。缓存穿透缓存穿透是指查询缓存中不存在的数据,每次请求都会命中DB,就好像缓存不存在一样。为了解决这个问题,加了一层Bloomfilter。Bloomfilter的原理是当你存储数据的时候,会通过一个hash函数映射到位数组中的K个点,同时将它们置1。这样当用户再次查询A时,A在Bloomfilter中的值为0,则直接返回,不会有击穿DB的breakdown请求。很明显,使用Bloomfilter后,会出现误判的问题,因为它本身就是一个数组,多个值可能会落到同一个位置。理论上只要我们数组的长度足够长,误判的概率越低,这种问题还是根据实际情况来问比较好。缓存雪崩当某个时刻缓存发生大规模故障时,比如你的缓存服务宕机了,大量的请求会进来,直接发给DB,可能会导致整个系统崩溃,这就是所谓的雪崩。雪崩问题与击穿和热键问题不同。它指的是大型缓存的到期。Avalanche的几种解决方案:为不同的key设置不??同的过期时间,避免同时过期,限流。如果redis宕机了,可以限流,避免同时有大量请求导致二级DB缓存崩溃。解决方法同热键。稳定性熔断器:比如营销服务挂掉或者接口大量超时等异常情况,不能影响订单的主链接,一些扣分相关的操作可以事后补救。限流:对于大促、闪购等高并发,如果部分接口不做限流处理,可能会直接挂掉服务。对各接口的压力测试性能的评估,做出适当的限流尤为重要。重要的。降级:熔断后其实就是降级的一种,比如营销接口坏掉后的降级解决方案就是短时间内不调用营销服务,等营销恢复后再调用。预案:一般来说,即使有统一的配置中心,在业务高峰期也不允许更改,但可以通过预案的合理配置,在紧急情况下进行一些修改。检查:对于各种分布式系统产生的分布式事务的一致性或攻击造成的数据异常,非常有必要检查平台做最后的数据验证。比如检查下游支付系统和订单系统中的金额是否正确,如果受到中间人攻击,是否保证存储在仓库中的数据是正确的。综上所述,其实我们可以看出,如何设计一个高并发系统的问题本身并不难。无非就是根据你知道的知识点,从物理硬件层面到软件架构,代码层面的优化,用什么中间件来不断完善。系统弹性。但是这个问题本身会带来更多的问题。微服务的拆分本身带来了分布式事务的问题,而HTTP和RPC框架的使用带来了通信效率、路由、容错等问题。MQ的引入带来了消息丢失、积压、事务性消息、顺序消息的问题,而缓存的引入又带来了一致性、雪崩、崩溃的问题。分库分表读写分离会带来主从同步延迟、分布式ID、事务一致性等问题。为了解决这些问题,我们必须不断加入熔断、限流、降级、离线验证、预案处理等各种措施来预防和追溯这些问题。作者:科技喵喵编辑:陶佳龙来源:科技喵喵(ID:kejimiumiu)