选择最小的结构,满足业务和流量的要求,避免过度设计带来的成本上升。
参照阿里发布的《阿里巴巴 Java 开发手册 v1.4.0(详尽版)》
三高”,也就是“高并发”“高性能”“高可用”,它们是互联网系统架构设计永恒的主题
高并发:运用设计手段让系统能够处理更多的用户并发请求,也就是承担更大的流量。它是一切架构设计的背景和前提。
性能和可用性,是我们实现高并发系统必须要考虑的因素。性能反应了系统的使用体验。可用性则表示系统可以正常服务用户的时间
**“可扩展性”,**它同样是高并发系统设计需要考虑的因素
平均响应时间
最大响应时间
分位值(很适合作为时间段内,响应时间统计值来使用),我们把这段时间请求的响应时间从小到大排序,,排除了偶发性。
P99的计算方式,是将1000笔请求的RT从小到大进行排序,然后取排在第99%位的数值,基于以上举例数据来进行计算,P99=50ms,其他分位值的计算方式类似
响应时间 1s 时,吞吐量是每秒 1 次,响应时间缩短到 10ms,那么吞吐量就上升到每秒 100 次。所以,一般我们度量性能时都会同时兼顾吞吐量和响应时间,比如我们设立性能优化的目标时通常会这样表述**:在每秒 1 万次的请求量下,响应时间 99 分位值在 10ms 以下**。
主要有两种思路:
MTBF(Mean Time Between Failure)是平均故障间隔的意思,代表两次故障的间隔时间,也就是系统正常运转的平均时间。这个时间越长,系统稳定性越高。
**MTTR(Mean Time To Repair)**表示故障的平均恢复时间,也可以理解为平均故障时间。这个值越小,故障对于用户的影响越小。
Availability = MTBF / (MTBF + MTTR)
Design for failure
可以从灰度发布和故障演练两个方面来考虑如何提升系统的可用性。
高扩展是一个设计指标,但是提高扩展性会很复杂,无状态的服务和组件更易于扩展,而像 MySQL 这种存储服务是有状态的,就比较难以扩展。因为向存储集群中增加或者减少机器时,会涉及大量数据的迁移,而一般传统的关系型数据库都不支持。
数据库、缓存、依赖的第三方、负载均衡、交换机带宽等等都是系统扩展时需要考虑的因素
拆分是提升系统扩展性最重要的一个思路,它会把庞杂的系统拆分成独立的,有单一职责的模块。相对于大系统来说,考虑一个一个小模块的扩展性当然会简单一些
存储和业务拆分扩展。
通过观察tcp数据包的时间,可以发现tcp连接握手断开挥手的时间,远大于sql执行的时间(常规条件下),所以考虑用池化技术,来减少这种频繁的连接断开操作,说白了是一种空间换时间的方式。
如果无视磁盘和网络,那么结论非常简单。有几个核心就创建几个线程,再增加连接数就会因为上下文切换损耗而导致性能下降。
连接数 = ((核心数 * 2) + 有效磁盘数)
tips:在重复创建链接的情况下,大概4/5的时间消耗在创建和销毁链接,加入sql执行时间是1ms,链接的时间是4ms,那么1s中执行查询是200次。用上池化技术,查询就到了1000次。(数据库是单核单线程,忽略io和网络;这样估算比较好理解)
引申出:数据库优化,减少io(三层树、pool_buffer等);加快扫描速度和行数=》(索引)
读写请求量的差距可能达到几个数量级,将读写流量分开,做主从的读写分离。
主从分离的技术点:
一主多从,写只发生在主库,读发生在从库。
**是不是我无限制地增加从库的数量就可以抵抗大量的并发呢?**实际上并不是的。因为随着从库数量增加,从库连接上来的 IO 线程比较多,主库也需要创建同样多的 log dump 线程来处理复制的请求,对于主库资源消耗比较高,同时受限于主库的网络带宽,所以在实际使用中,一般一个主库最多挂 3~5 个从库
对于主从延迟的问题:
分库分表+主从,导致配置非常复杂,所以业界涌现很多中间件。
垂直拆分,顾名思义就是对数据库竖着拆分,也就是将数据库的表拆分到多个不同的数据库中
垂直拆分的原则一般是按照业务类型来拆分,核心思想是专库专用。
…
基于 Snowflake 算法搭建发号器
id是时间有序的,排序可以直接使用。时间有序,插入的时候性能也更高
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-957XrTAD-1690529784375)(C:\Users\Administrator\Desktop\interviews\高并发40问总结.assets\image-20230627111309281.png)]
如果你的系统部署在多个机房,那么 10 位的机器 ID 可以继续划分为 2~3 位的 IDC 标示(可以支撑 4 个或者 8 个 IDC 机房)和 7~8 位的机器 ID(支持 128-256 台机器);12 位的序列号代表着每个节点每毫秒最多可以生成 4096 的 ID
一般来说我们会有两种算法的实现方式
对于机械磁盘的访问方式有两种:一种是随机 IO;另一种是顺序 IO。随机 IO 就需要花费时间做昂贵的磁盘寻道,一般来说,它的读写效率要比顺序 IO 小两到三个数量级,所以我们想要提升写入的性能就要尽量减少随机 IO
实际上,凡是位于速度相差较大的两种硬件之间,用于协调两者数据传输速度差异的结构,均可称之为缓存。
常见的缓存主要就是静态缓存(nginx)、分布式缓存(Redis)和热点本地缓存(Guava Cache 等)这三种
针对不同的业务场景,缓存的读写策略是不同的。
更新缓存再更新数据库(导致数据不一致)
产生问题的原因:变更数据库和变更缓存是两个独立的操作。
处理:
读策略的步骤是:
写策略的步骤是:
因为写的速度要小于读的速度,且删除缓存很快,所以很少出问题。
缓存旁观最大问题:写入频繁时候,缓存被频繁删除,导致缓存命中有问题。两种解决方案:
用户只与缓存打交道,由缓存和数据库通信,写入或者读取数据
这个策略的核心思想是在写入数据时只写入缓存,并且把缓存块儿标记为“脏”的。而脏块儿只有被再次使用时才会将其中的数据写入到后端存储中
写比较简单,写到缓存然后直接然后;对于读,如果命中返回,不命中,找缓存块,如果这个换存块是脏的,将数据写入后面存储,并后面存储拉取新的数据。
适合写多读少情况。
数据库部署的时候常选用的机器配置最低在8核16G以上,正常在16核32G。
一般Java应用系统部署在4核8G的机器上,每秒钟抗下500左右的并发访问量,差不多是比较合适的,当然这个也不一定。 一台机器能抗下每秒多少请求,往往是跟你每个请求处理耗费多长时间是关联的。大体上来说,根据我们大量的经验观察而言,4核8G的机器部署普通的Java应用系统,每秒大致就是抗下几百的并发访问,从每秒一两百请求到每秒七八百请求,都是有可能的,关键是看你每个请求处理需要耗费多长时间。
**一般8核16G的机器部署的MySQL数据库,每秒抗个一两千并发请求是没问题的,**但是如果你的并发量再高一些,假设每秒有几千并发请求,那么可能数据库就会有点危险了,因为数据库的CPU、磁盘、IO、内存的负载都会很高,弄不数据库压力过大就会宕机。
对于16核32G的机器部署的MySQL数据库,每秒抗个两三千,甚至三四千的并发请求也都是可以的,但是如果你达到每秒上万请求,那么数据库的CPU、磁盘、IO、内存的负载瞬间都会飙升到很高,数据库也是可能会扛不住宕机的
分布式缓存的高可用方案
缓存数据如何分片?
分片之后,保证某个节点出现问题,以它节点仍然可用,保证了一定的可用性。
解决方案:
向缓存回种空值+使用布隆过滤器
布隆过滤器:一个超大的数组,hash之后,指定位置是0或者1,1代表又数据。节省空间
主要有两个缺陷:
对于击穿的问题:用分布式锁,只有一个线程能到达数据库,其它线程挂起(sleep),然后再调回(reture getData() 这里面获取到数据,直接return数据了)这个方法
核心思想是:减少对于数据库的并发请求
静态资源的关键点就是就近访问。
CDN就是将静态的资源分发到,位于多个地理位置机房中的服务器上,因此它能很好的解决就近访问的问题。
搭建CDN系统:
一般来说,有两种方案可以做数据库的迁移。
“双写”方案
级联同步方案
适合自建机房向云上迁移的场景。
我们大部分的优化,都停留在读的环节,我们优化数据库、缓存都是提高读的能力(绝大部分场景都是高并发的读)。但是有些情况是高并发写场景,引出了消息队列。
把消息队列看做:暂时存储数据的一个容器。
将秒杀的请求暂时放到消息队列中,然后业务服务器响应用户“秒杀结果正在计算中”。在后台启动若干个队列处理程序(有限个线程)执行
1.在生产的过程中丢失消息
网络可能抖动,导致丢失,超时重传,可能导致重复
2.在消息队列中丢失消息
例如Kafka异步刷盘,如果宕机之类的。但是提高刷盘频率,太影响性能,可以通过集群多几个副本备份。建议给一个Follower发送就可以返回成功,不能是所有的,太影响性能。
3.在消费过程中消息丢失
以Kafka为例,消息的过程分为3步:接收、处理、更新消费进度。一定要等到接收和处理完之后,再更新消息。但是可能会出现重复,比如,处理完,刚好宕机,这条消息没能更新。
宕机等不可避免,所以保证消息的消费结果幂等(结果和只消费一次等同)即可。
Kafka中消息存储包含生成者id和消息id,如果同一个生产者,消息id重复,回丢失掉。
通用层:
消息生成的时候,有一个唯一id ,等消费完了,插入数据库,消费下一个的时候,查查库,看看是否消费过了。有个问题是:处理完,宕机了,没写入库,如果不是要求特别严格,可以使用,而不考虑引入事务。
业务层:
生产消息的时候,给要操作的逻辑相关数据,弄个version,使用乐观锁,将版本号和和消息一块发送,更新的时候,比较version,就不会重复了。比如转账的时候,哪个账户加钱,用乐观锁。
需要在消费端和消息队列两个层面完成。
消费端:
提高了处理能力,处理的快了,消息延迟就低了。
不过,这两种方式受限于消息队列的实现。如果使用Kafka就无法通过增加消费者数量的方式,提升消费能力。(Kafka一个分区只能有一个消费者消费),可以通过增加分区来提高消费者数量。
如果不增加消费者,可以在一个consumer中提升处理消息的并发度。把拉取到的消息丢到线程池中处理。可以提升消息处理的吞吐量。
消息队列本身:
消息队列本身读取性能优化做了哪些事情。
随着功能的复杂、开发团队规模越来越大,单体架构有一些缺陷。
1.数据库连接数成为瓶颈
MySQL客户端连接数据,上限16384。如果机器太多,可能不够用
2.一体架构增加研发成本,抑制研发效率提升。《人月神话》,沟通成本和人数有关,呈指数增加。代码都在一起,配合容易出问题,一个小修改,可能影响整个项目。微服务,分模块。
3.运维影响**:单体项目太大,几十万代码,编译,单元测试、打包运行,都要花很长时间,而且小修改就要构建整个项目,不灵活
如何利用微服务解决这些痛点:
功能内聚,维护开发人员责任明确,每个系统是之前的子集,构建速度较大提升,即使出现问题,也可以熔断降级处理,不至于一个小bug响应整个项目,加上云原生的技术,部署运维很方便高效。
微服务拆分的原则
微服务带来的问题和优化思路
如果要提升RPC框架的性能,需要从网络传输和序列化两方面来优化
选择一种高性能的I/O模型。
首先,IO会经历一个等待资源的阶段,比如等待网络传输数据可用,有两种处理方式:
然后是使用资源的阶段,比如从网络接收数据,拷贝到应用缓冲区里面。这时候,有两种处理方式:
使用最广泛的是:多路复用(同步非阻塞的升级、同时监控好几个通道)
默认的采用JSON就行,如果要求高性能的话,可以考虑谷歌的Protobuf序列化协议
注册中心的基本功能有两点:
可以通过requestId+打印时间,查看耗时的情况。当然这其中,有用aop,有觉着数据量太大直接requestId%10==0,有发送到mq,然后抽取到es中。
跨越几个服务或者分布式组件。
分布式监控的原理:
traceId+spanId(每层+1)
将traceId 和spanId 放到请求体中,发送给下面的服务器。最终算出时间,放到消息队列,最终抽到es中,或者开源组件中。
追踪主要有两点:
单体或者微服务架构,需要traceId(requestId),可以在客户端生成,这样服务端+客户端都能串起来。
高并发三个通用的方法:缓存、异步(消息队列)、横向扩展
有些服务器还提供了节点的故障检测功能。淘宝开源的nginx_upstream_check_module 模块,定期探测后盾服务的一个指定接口,根据状态码判断,到达阀值,移除掉。
部署在负载均衡服务器和应用服务器之间。主要有以下作用:
服务依赖于第三方,我们的出口网关位于服务和第三方服务之间,在出口网关中,对调用外部的API做统一认证、授权、审计等。
首先考虑性能—》核心在IO模型上。
考虑扩展性:热插拔(用责任链去做)
提高处理能力,加入线程池技术(避免某个服务出现问题,耗尽线程池)
在不同的IDC机房中,部署多套服务,这些服务共享一份业务数据,并且都可以承受来着用户的流量。
机房之间的调用,延迟是客观存在的,都在北京几毫秒,北京上海接近30ms,北京广东50ms
核心思想是:尽量避免跨机房的调用。
数据写入时候,保证只写本机房的数据存储服务,采用同步方案将数据同步到异地机房。一般有两种同步方案:
主要处理服务间的通信,主要实现方式是在应用程序同主机上部署一个代理程序-Sidecar(边车),服务由直连,变成通过边车。
四个黄金信号:延迟、通信量、错误和饱和度
一般先将消息写入到队列来承接数据,主要作用是削峰填谷
一般会部署两个队列来处理程序,来消费队列中的数据。
一般会形成以下的报表:
APM:对应用各个层面做全方位的监控,期望及时发现可能存在的问题,并加以解决,从而提升系统的性能和可用性。
由于压力测试是在正式环境运行,所以需要区分压力测试流量和正式流量,这样可以针对压力测试流量做单独的处理。
在流量高峰,拷贝一些流量,清洗后放到HBase、MongoDB 这些NoSQL存储组件,称为为流量数据工厂。
当需要压测的时候,从这些工厂获取数据,将数据切分成多分下发到测试节点。
GoReplay 劫持流量,拷贝到流量工厂。
压测流量拷贝下来的同时,我们也需要改造系统,实现将正式流量和压测流量隔离。
压测,有些行为影响到用户的统计分析,推荐等,还有一些插入的操作。这些就需要特殊的处理
MySQL放入影子库、Redis设置不同的前缀,es放到单独索引表中。
写操作是上行流量,读操作是下行流量,都要有特殊处理
不会一次加完,缓慢加,跑跑,看看监控,有瓶颈,回退,扩容,再加。
主要有两种配置:
局部故障最终导致全局故障-雪崩。
服务调用等待服务提供方的响应时间变长,它的资源被耗尽,发生级联反应,引发雪崩
当服务调用者使用 同步调用 时, 会产生大量的等待线程占用系统资源. 一旦线程资源被耗尽,服务调用者提供的服务也将处于不可用状态, 于是服务雪崩效应产生了.
熔断指:返回错误或者超时的次数超过一定阀值,后续的请求不在发想远程服务而暂时返回错误。有限状态机,关键是三种状态转换。
打开状态到半打卡,超过一定阀值,才关闭
相比熔断来讲,降级是一个更大的概念。站在整体系统负载的角度上,放弃部分非核心功能或者服务,保证整体的可用性,是一种有损的系统容错方式。
开关降级是在代码预设一些“开关”,用来控制服务调用的返回值。可以通过配置中心实现。降级的是非核心业务。
限流指的是通过限制到达系统的最大并发请求数据量,保证系统能够正常响应,对于超过限制的流量,拒绝保证系统整体的可用性。一般部署在入口,如API网关。
简单记录1分钟内的请求,超过了就不让请求了。有个缺陷:无法限制短时间之内的集中流量。限制100,前10秒100次了,后面50秒就不用用了。
原理:将时间的窗口划分为多个小窗口,每个小窗口中都有独立的请求计数。
假如在将1s划分成5份,那么在1s-1.2s之间来了一次请求,那么就算的区间就是0.2-1.2这个区间的总请求量。
解决了窗口边界的大流量问题,但是无法限制短时间内的集中流量,也就是说流量控制不能更平滑。
漏桶算法:在流量产生端和接收端增加一个漏桶,流量会进去和暂存到漏桶里,漏桶出口会按照一个固定速率将流量漏到接收端(服务端。)
如果流入的流量某段时间馁大增,超过漏桶能承载的极限,触发 限流策略,被拒绝服务。
一般会使用消息队列作为漏桶的实现。思想和削峰填谷类似。
令牌桶能面对一次突发流量,直接拿完令牌桶的令牌去请求。
漏桶算法能够强行限制数据的传输速率,令牌桶算法能够在限制数据的平均传输速率的同时还允许某种程度的突发传输
在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流
计数的话,刚开始使用MySQL,如果要求速度快,使用Redis,考虑到存储成本的问题,可以对原生Redis做一些改造
一些比较老的数据,落盘
避免操作全量数据未读数。为没一个用户存储一个时间戳,点了共享的数据,时间戳更新为现在的,不再显示标记,如红点。以后比较时间戳和共享数据的时间戳就行了,看看有没有新数据。