如何设计一个支持高并发的系统? #
设计一个能够支持高并发的系统需要考虑多方面的因素,包括架构、性能优化、容错和可伸缩性等。以下是一些一般性的建议和实践。
-
分布式架构:将系统分解成多个模块,采用分布式架构来降低单点故障的风险,并提高系统的可伸缩性和性能。
-
集群部署:将一个服务通过集群进行部署,来提升系统整体的吞吐量及响应速度,并使用负载均衡技术将请求均衡分配给多个服务器,以提高系统的性能和可用性。
-
利用缓存:使用缓存、NoSQL等技术,以提高数据读写的性能和可靠性。
-
异步处理:采用异步处理机制,如使用消息队列、事件驱动等技术,以降低请求响应时间和提高系统吞吐量。
-
预加载:使用预加载技术来提前加载需要的资源,以减少用户等待时间。
-
代码优化和调优:对系统代码进行优化和调优,如采用异步I/O、避免锁(减小锁的粒度)、减少循环和递归、避免长事务等,以提高系统性能。
-
数据库优化:合理的数据库设计和优化,包括合理的索引设计、分库分表、读写分离、缓存优化等,可以有效提高系统的并发度和响应速度。
-
读写分离:读写分离是一种常用的数据库优化技术,它将读操作和写操作分配到不同的数据库实例上处理。通过读写分离,主库主要负责写操作,从库则负责读操作,从而提高了系统的并发度和可扩展性。同时,读写分离还可以提高系统的可用性和容错能力,因为即使主库出现故障,从库仍然可以提供读服务。
-
防止雪崩:通过使用限流、熔断、降级等技术,可以防止系统因为某个组件出现故障而导致整个系统崩溃的雪崩效应。
-
容错和监控:实现容错机制,如备份、容灾、负载降级等,以保障系统的可用性。同时,使用监控工具来实时监测系统的运行状况和性能瓶颈,及时做出调整和优化。
-
测试和评估:进行全面的性能测试和评估,包括压力测试、负载测试、安全测试等,以发现并解决系统的性能瓶颈和安全隐患。
什么是服务降级? #
限流和降级都是对系统的保护功能,一般用户在流量高峰时期,比如双十一大促。
降级是通过开关配置将某些不重要的业务功能屏蔽掉,以提高服务处理能力。在大促场景中经常会对某些服务进行降级处理,大促结束之后再进行复原。
区别于熔断机制,降级一般并不是彻底功能不可用,而是用一种默认返回、异步执行、延迟处理等方式进行降低处理。
比如:在大促期间减少反馈问卷等功能推送
降级方式 #
异步处理:先让用户填手机号,等用户离店后,再短信推送调查问卷
延迟处理:门口放一个问卷表,用户离店时自愿去填写。
什么是熔断? #
现在很多网站的背后都是一个庞大的分布式系统,多个系统之间的交互大多数都是采用RPC的方式,但是因为是远程调用,所以被调用者的服务的可用情况其实是不可控的。
而越是庞大的系统,上下游的调用链就会越长,而如果在一个很长的调用链中,某一个服务由于某种原因导致响应时间很长,或者完全无响应,那么就可能把整个分布式系统都拖垮。
如果其中某一个服务由于自身原因导致响应很慢,那么就可能导致上游的服务相应也很慢,这样循环往复,就会导致整个系统全线崩溃,这就是服务雪崩。
在服务的依赖调用中,当被调用方出现故障时,出于自我保护的目的,调用方会主动停止调用,并根据业务需要进行相应处理。调用方这种主动停止调用的行为我们称之为熔断。
为什么需要熔断 #
其实,在分布式系统中,为了保证整体服务可用性和一致性,很多系统都会引入重试机制,在有些情况下,重试其实是可以解决问题的,比如网络问题等,都可以通过重试来解决。
但是,有些情况下,重试并不能解决问题,反而会加剧问题的严重性,比如下游系统因为请求量太大,导致CPU已经被打满,数据库连接池被占满,这时候上游系统调不通就会不断进行重试,这种重试请求,对于下游系统来说,无疑是雪上加霜,给下游系统造成二次伤害。
一个比较完善的熔断器,一般包含三种状态:
- 关闭:熔断器在默认情况下下是呈现关闭的状态,而熔断器本身带有计数功能,每当错误发生一次,计数器也就会进行“累加”的动作,到了一定的错误发生次数断路器就会被“开启”,这个时候亦会在内部启用一个计时器,一旦时间到了就会切换成半开启的状态。
- 开启:在开启的状态下任何请求都会“直接”被拒绝并且抛出异常讯息。
- 半开启:在此状态下断路器会允许部分的请求,如果这些请求都能成功通过,那么就意味着错误已经不存在,则会被切换回关闭状态并重置计数。倘若请求中有“任一”的错误发生,则会恢复到“开启”状态,并且重新计时,给予系统一段休息时间。
什么是预热?有何作用? #
缓存预热是指在系统启动之前或系统达到高峰期之前,通过预先将常用数据加载到缓存中,以提高缓存命中率和系统性能的过程。缓存预热的目的是尽可能地避免缓存击穿和缓存雪崩,还可以减轻后端存储系统的负载,提高系统的响应速度和吞吐量。
- 减少冷启动影响:当系统重启或新启动时,缓存是空的,这被称为冷启动。冷启动可能导致首次请求处理缓慢,因为数据需要从慢速存储(如数据库)检索。
- 提高数据访问速度:通过预先加载常用数据到缓存中,可以确保数据快速可用,从而加快数据访问速度。
- 平滑流量峰值:在流量高峰期之前预热缓存可以帮助系统更好地处理高流量,避免在流量激增时出现性能下降。
- 保证数据的时效性:定期预热可以保证缓存中的数据是最新的,特别是对于高度依赖于实时数据的系统。
- 减少对后端系统的压力:通过缓存预热,可以减少对数据库或其他后端服务的直接查询,从而减轻它们的负载。
什么是限流?常见的限流算法有哪些? #
- 漏桶算法(常用):系统请求先进入漏桶,再从漏桶中逐一取出请求执行,控制漏桶的流量。
- 令牌桶算法(常用):系统请求会得到一个令牌,从令牌桶中取出一个令牌执行,控制令牌桶中令牌的数量。
- 计数器算法(简单):系统请求被计数,通过比较当前请求数与限流阈值来判断是否限流。
- 可以阻塞算法:当系统达到限流阈值时,不再接受新请求,等到限流阈值降下来再接受请求。
- 令牌环算法:与令牌桶算法类似,但是在多个令牌桶之间形成环形结构,以便在不同的请求处理速率之间进行平衡。
- 最小延迟算法:基于预测每个请求的处理时间,并在处理完请求后进行延迟,以控制请求的速率。
- 滑动窗口(常用):基于一个固定大小的时间窗口,允许在该时间窗口内的请求数不超过设定的阈值。这个时间窗口随着时间的推移不断滑动,以适应不同时间段内的请求流量。
单机限流和集群限流的区别是什么? #
一个是针对单台服务器限流,一个是针对整个集群做限流。比如单机限100,那么就是单机最大就能抗100QPS,如果是集群限100,那么就意味着集群不管有多少台机器,总共 QPS 只能抗100。
集群限流通常需要使用分布式的限流算法和工具,比如 Redis、Sentinel、Hystrix等,以确保每个服务实例或节点都遵守全局的流量控制策略。
有了集群限流,还需要做单机限流吗? #
需要
因为单机限流主要关注保护单个服务节点。即使集群级别有流量控制,单个实例依然可能因为本地的请求过多而出现性能问题或崩溃。
有了单机限流,还需要做集群限流吗? #
一般来说也需要
单机限流主要关注于保护单个服务实例不被本地请求压力过大而打挂。但是,整个服务集群可能面临的是全局的流量波动和峰值,需要集群级别的流量控制策略来确保整体系统的稳定性和可靠性。
尤其是在面对突发的大流量或者恶意攻击时,单个实例的限流策略可能不足以应对全局性的挑战。集群限流可以通过全局的流量监控和调整,协调各个节点的响应策略,有效地防止全局服务不可用。
什么是自适应限流? #
所谓自适应限流,就是限流器结合服务器实例的Load、CPU、内存、接口的RT、QP、并发线程数等指标,进行的一种自适应的流控策略。即通过监控这些指标的变化,来动态的调整限流,来达到保证系统稳定性的目的。
什么是滑动窗口限流? #
滑动窗口限流是一种流量控制策略,用于控制在一定时间内允许执行的操作数量或请求频率。它的工作方式类似于一个滑动时间窗口,在窗口内允许的操作数量是固定的,窗口会随着时间的推移不断滑动。
首先需要把时间划分成多个连续的时间片段,每一个片段都有一个固定的时间间隔,如1s、1h等。
然后再定义一个时间窗口,比如10s,随着时间的推移,这个窗口不断的向右移动。为了实现限流的功能,我们通常需要定义一个计数器,统计时间窗口内的请求数。
当时间窗口移动时,需要把上一个时间片段中的请求数减掉,当有新的请求或操作到达系统时,系统会检查窗口内的计数是否已满。如果计数未满,请求被允许执行;如果计数已满,请求被拒绝或进入等待队列,或执行其他限流操作。
滑动窗口限流的主要优点是可以在时间内平滑地控制流量,而不是简单地设置固定的请求数或速率。这使得系统可以更灵活地应对突发流量或峰值流量,而不会因为固定速率的限制而浪费资源或降低系统性能。
高并发场景中,乐观锁和悲观锁那个更适合? #
乐观锁的基本思想是假设冲突很少发生,每个线程在修改数据之前,先获取一个版本号或时间戳,并在更新时检查这个版本号或时间戳,以确保其他线程没有同时修改数据。
**乐观锁适用于读操作频繁,写操作相对较少的场景。**当冲突较少,且并发写入的概率较低时,乐观锁的性能可能更好。
悲观锁则是假设冲突经常发生,因此在访问共享资源之前,线程会先获取锁,确保其他线程无法同时访问相同的数据。这可能导致并发性降低,因为只有一个线程能够访问数据。
**悲观锁适用于写操作较为频繁,且并发写入的概率较高的场景。**悲观锁可以有效地避免多个线程同时修改相同数据的情况。
乐观锁和悲观锁还有个区别:乐观锁因为比较乐观,所以一般是先做业务逻辑操作,比如参数处理,内存中进行模型组装调整,然后再去更新数据库。悲观锁因为比较悲观,所以会先尝试加锁,然后再去做业务逻辑操作。
也就是说,乐观锁是先干活,后加锁。悲观锁是先加锁,再干活。
**在高并发场景中,一般来说并发写入的冲突较为频繁,所以建议优先考虑悲观锁。**即在做并发操作前,先尝试获取锁,如果获取锁成功,在进行业务操作,否则就直接返回失败。
比如,我们通常在并发场景下都使用分布式锁,即先加分布式锁,然后再操作。这个就是一个悲观锁的思想,我认为冲突一定很大,所以我先尝试加锁。拿到锁再开始干活。
漏桶和令牌桶有啥区别? #
漏桶的这个桶,一秒钟流入一滴水,同样一秒钟漏出一滴水。那么,一秒钟就只能处理一个请求,超过的请求会被拒绝掉,达到限流的效果。
也就是说,漏桶这种算法,在5秒钟只能可以处理5个请求,并且每秒钟一个。但是如果出现这种情况,前4秒钟都没有请求,第5秒同时来了5个请求,漏桶是无法处理5个请求的,他只能处理1个,因为这一秒钟只会有一滴水漏出来。
令牌桶的实现逻辑是同样1秒钟产生一个令牌放到桶中,但是如果这个令牌没有被消费的话,他就会一直在桶中,不会被漏出去。还是刚刚那个例子,前4秒没有请求要处理的话,那么5秒钟就可以积攒5个令牌,这时候第5秒来了5个请求的时候,他去桶中是可以一次取出5个令牌,然后把这5个请求都给处理掉的。这就很好地应对了突发流量的问题。
所以,漏桶算法适合于需要限制数据的平均传输速率并确保数据传输的平滑性的场景。令牌桶算法更加灵活,适合于那些既需要限制数据平均传输速率,又需要允许一定程度突发传输的场景。
什么是QPS,什么是RT? #
QPS,指的是系统每秒能处理的请求数(Query Per Second) ,在Web应用中我们更关注的是Web应用每秒能处理的request数量。这个是衡量系统性能的重要指标。
RT,指的是响应时间(Response Time),是指从客户端发一个请求开始计时,到客户端接收到从服务器端返回的响应结果结束所经历的时间。