何为高并发系统?
在理解高并发系统之前,我们先来理解几个相关概念。
什么是并发(Conurrent)?
在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个宿主机上运行,但任一个时刻点上只有一个程序在宿主机上运行。
什么是高并发(Hight Concurrnet)?
从字面上来理解就是让 单位时间同时处理任务的能力尽可能的高。对应到我们研发系统中,也就是说:我们所开发的系统,要在 短时间能能支持大量访问请求的情况。这种情况比如:双十一或者 12306 的抢票、以及秒杀等活动。
这要求我们的业务系统,在短时间内,尽可能多的接收来自客户端的请求,并做出准确的响应。
高并发的衡量指标有哪些?
响应时间:系统对请求做出响应的时间,既然是高并发系统,这个响应时间就不可能太长,需要尽可能的短。
吞吐量:系统单位时间内支持的最大请求数,当然越多越好。QPS/TPS 是吞吐量最常用的量化指标之一。
并发数:系统同时承载的正常使用功能的用户数量。如通信系统的同时在线人数。反应了系统的负载能力。这个指标当然越大越好。
=》 QPS(TPS) = 并发数/平均响应时间
此外还有些相关的指标也需要了解:
PV(Page View):页面访问量,即页面浏览量或点击量。
UV(Unique Visitor):独立访客,统计一天内访问某站点的用户数。即按人按天去重。
DAU(Daily Active User):日活跃用户数量。通常统计一日(统计日)之内,登录或使用了某个产品的用户数,与 UV概念相似。
MAU(Month Active User):月活跃用户数量,指网站、app 等去重后的月活跃用户数量。
上述指标内容,主要是反映了高并发系统在 高性能 上的要求。做为 高并发系统,需要实现的目标为:
高性能:这体现了系统的并行处理能力,在有限资源的情况下,提升性能能节省成本。同时也给用户带来了更好的用户体验。
高可用性:系统可以正常服务的时间,尽量避免系统的事故和宕机从而影响正常的业务。
高扩展性:表示系统的扩展能力,系统具备更好的弹性,在流量高峰期能否短时间完成扩容,更平稳的承接流量峰值。
所谓设计 高并发 系统,就是设计一个系统,保证它 整体可用的同时,能够 处理很高的并发用户请求,能够 承受很大的负载流量冲击。
实现高并发系统的两大板块
我们要设计高并发的系统,那就需要处理好一些常见的系统瓶颈问题:
高并发系统
硬件资源侧:如内存(空间或主频)不足、磁盘(I/O 性能或空间)不足,连接数不够,网络宽带不够等等。
应用程序侧:如语言框架性能,代码规范化,高可用性和高可扩展性的架构设计等等(后面细说,也是本篇文章尽可能阐述的方向)。
此外还有一些考虑因素,硬件资源与 OS 系统的默契配合,选择一款高性能的 OS 搭配合适(最佳实践)的硬件资源,为应用程序铺平部署底座,如鱼得水。
因此设计高性能系统需要需要考虑综上这些因素(“三高”:高性能,高可用性和高可扩展性),以应对突发的流量洪峰。
高并发系统应用程序侧的设计方法论
- 分而治之,横向扩展
如果你只部署一个应用,只部署一台服务器,那抗住的流量请求是非常有限的(毕竟硬件资源的能力提供有限)。并且,单体的应用,有单点的风险,如果它挂了,那服务就不可用了。
因此,设计一个高并发系统,我们可以分而治之,横向扩展。也就是说,把原来 集中式的部署 调整采用 分布式部署 的方式,部署多台服务器,把流量分流开,让每个服务器都承担一部分的并发和流量,提升整体系统的并发能力。
- ‘大’ 系统 ‘小’ 做(微服务拆分)
要提高系统的吞吐,提高系统的处理并发请求的能力。除了采用分布式部署的方式外,还可以把大单体项目做模块化的微服务拆分,这样就可以达到分摊请求流量的目的,提高了并发能力。
所谓的 微服务拆分,其实就是把一个单体的应用,按功能单一性,拆分为多个服务模块。比如一个电商系统,拆分为用户系统、订单系统、商品系统、支付系统等等。
- 分库分表(分担数据读写压力)
当业务量暴增的话,DB 单机磁盘容量会撑爆。并且数据库连接数是有限的。在高并发的场景下,大量请求访问数据库,DB 单机是扛不住的!高并发场景下,会出现异常报错。
所以高并发的系统,需要考虑拆分为多个数据库,来抗住高并发的 “毒打”。假如你的单表数据量非常大,存储和查询的性能就会遇到瓶颈了,如果你做了很多优化之后还是无法提升效率的时候,就需要考虑做分表了。一般千万级别数据量,就需要分表,每个表的数据量少一点,提升 SQL 查询性能。
- 池化技术(资源复用)
在高并发的场景下,数据库连接数可能成为瓶颈,因为连接数是有限的。
我们的请求调用数据库时,都会先获取数据库的连接,然后依靠这个连接来查询数据,搞完收工,最后关闭连接,释放资源。如果我们不用数据库连接池的话,每次执行 SQL,都要创建连接和销毁连接,这就会导致每个查询请求都变得更慢了,相应的,系统处理用户请求的能力就降低了。
比如:PosgtreSQL 就推荐使用连接池,目前很多第三方 SDK 都集成了数据库连接池。
因此,需要 使用池化技术,即:数据库连接池、HTTP 连接池、Redis 连接池 等等。使用数据库连接池,可以避免每次查询都新建连接,减少不必要的资源开销,通过复用连接池,提高系统处理高并发请求的能力。
同理,我们 使用线程池,也能让任务并行处理,更高效地完成任务。
- DB 主从架构(数据读写分离)
通常来说,一台单机的 DB 服务器支撑的请求访问(TPS/QPS)是有限的。因此你做了分布式部署,部署了多台机器,部署了主数据库、从数据库。
但是,如果双十一搞活动,流量肯定会猛增的。如果所有的查询请求,都走主库的话,主库肯定扛不住,因为查询请求量是非常非常大的。因此一般都要求做主从分离,然后实时性要求不高的读请求,都去读从库,写的请求或者实时性要求高的请求,才走主库。这样就很好保护了主库,也提高了系统的吞吐。
上面谈到的是关于传统关系型数据库(RDBMS)的主从架构,除了使用 关系型数据库,我们还可以使用 分布式数据库,而国产分布式数据库(TDSQL,GaussDB,OceanBase,PolarDB 等)在国内外市场都有着巨大的潜力和市场需求。 随着大数据和云计算技术的普及,国产分布式数据库将成为未来数据存储和处理的核心技术。
- 使用缓存(系统性能提升立竿见影)
无论是操作系统,浏览器,还是一些复杂的中间件,你都可以看到缓存的影子。我们使用缓存,主要是提升系统接口的性能,这样高并发场景,你的系统就可以支持更多的用户同时访问。
常用的缓存包括:分布式缓存(Redis/Memcached),localhost 本地缓存(App 缓存、HTTP 缓存),应用缓存(前端进程缓存,后端进程缓存),数据库缓存,反向代理缓存,Web 服务器缓存,浏览器缓存,CDN 缓存等等。就拿 Redis 来说,它单机就能轻轻松松应对几万的并发,特别是针对读取数据多(查询)场景的业务,可以用 缓存 来抗住高并发。通常情况下大多数的业务系统,都遵循 ”二八定律“,读多(80%)写入(20%)。
在高并发系统的全链路设计环节,都可以依据情况设计 多级缓存 功能,提升系统性能显著。下面是一个 五级缓存 设计例子:
缓存虽然可以立即提升系统性能,但是也要注意使用缓存的一些潜在问题:
数据一致性
即缓存数据与数据库的数据是否一致性问题;
缓存雪崩
当某一个时刻出现大规模的缓存失效的情况,那么就会导致大量的请求直接打在数据库上面,导致数据库压力巨大,如果在高并发的情况下,可能瞬间就会导致数据库宕机。这时候如果运维马上又重启数据库,马上又会有新的流量把数据库打死,这种现象就叫做缓存雪崩。
缓存击穿
其实跟缓存雪崩有点类似,缓存雪崩是大规模的 key 失效,而缓存击穿是一个热点的 key ,有大并发集中对其进行访问,突然间这个 key 失效了,导致大并发全部打在数据库上,导致数据库压力剧增,这种现象就叫做缓存击穿。
缓存穿透
我们使用 Redis 大部分情况都是通过 key 查询对应的值,假如发送的请求传进来的 key 是不存在 Redis 中的,那么就查不到缓存,查不到缓存就会去数据库查询。假如有大量这样的请求,这些请求像 “穿透” 了缓存一样直接打在数据库上,这种现象就叫做缓存穿透。
- 动静分离,静态资源压缩 & CDN 加速静态资源访问
商品图片(.jpg,.jpeg,.png,.gif,.webp,.svg),icon,wwwroot(部分 html + css + js) 等等静态资源,可以对 页面做静态化处理,减少访问服务端的请求。如果用户分布在全国各地,有的在上海,有的在深圳,地域相差很远,网速也各不相同。为了让用户最快访问到页面,可以使用 CDN。CDN 可以让用户就近获取所需内容。
什么是 CDN?
Content Delivery Network/Content Distribution Network,翻译过来就是内容分发网络,它表示将静态资源分发到位于多个地理位置机房的服务器,可以做到数据就近访问,加速了静态资源的访问速度,因此让系统更好处理正常别的动态请求。
什么是 Web “动静分离” ?
所谓 “动静分离” 就是通过 nginx(或 apache 等)来处理用户端请求的静态页面,iis/kestrel/apache/tomcat/weblogic 处理动态页面,从而达到动静页面访问时通过不同的容器来处理。
静态资源压缩
在宿主资源(CPU)充分的情况下,我们还可以静态资源压缩,通常的做法例如:nginx 开启 gzip 压缩,将 js、css 等文件,生成对应的 .gz 文件。访问静态资源文件越小,耗时越短,速度越快,带宽损耗越小。
减少 http 请求数量
另外在每次 http 请求的繁琐过程(交换机查找的过程,DNS 解析的过程)是十分耗时的,所以 减少 http 请求数量 也可以减少耗时,从而提升用户体验,达到性能优化的目的。
- 消息异步通信(MQ 流量削锋)
比如某电商公司搞双十一、双十二等运营活动时,需要 避免流量暴涨,打垮应用系统的风险。因此一般会引入 消息队列,来应对高并发的场景。
假设你的应用系统每秒最多可以处理 2k 个请求,每秒却有 5k 的请求过来,可以引入消息队列,应用系统每秒从消息队列拉 2k 请求处理,在瞬时峰值下保障了服务能够平滑的延缓处理消息。
常用的 MQ 中间件产品有:ActiveMQ、RocketMQ、RabbitMQ、Kafka、Apache Pulsar,对于 中小型 Web 消息处理个人比较推荐使用 RabbitMQ,大型 Web 海量数据消息处理推荐使用 Apache Pulsar。
-
ElasticSearch(分担数据库查询)
ElasticSearch,简称 ES,是一个分布式的搜索和分析引擎,以其强大的扩展能力和高并发处理能力而著称。由于其分布式特性,ES 可以轻松地通过增加节点来扩展性能和容量,从而自然有效地支撑高并发场景。这使得 ES 成为承载简单查询、统计操作以及全文搜索等任务的理想选择,一般搜索功能都会用到它。当然还可以使用 ES 存储日志信息等。因为 ES 可以扩容方便,天然支撑高并发。当数据量大的时候,不用动不动就加机器扩容,分库等等。 -
降级熔断(避免内部服务不可用造成的雪崩效应)
熔断降级 是保护系统的一种手段。俗话说得好 “一粒老鼠屎坏了一锅汤”,而 熔断降级 策略就是为了避免高并发系统中偶尔产生的这 “一粒老鼠屎” 设计的手段。
当前互联网系统一般都是 分布式部署 的。而分布式系统中偶尔会出现某个基础 服务不可用,最终会演变为导致 整个系统不可用 的情况,这种现象被称为 服务雪崩效应。
在分布式系统中,某一个服务的调用链路 ”A -> B -> C …“ 如下图所示:
如果 服务 C 出现问题,比如是因为 慢 SQL 导致调用缓慢,那将导致 服务 B 也会延迟,从而 服务 A 也会延迟。堵住的 A 请求会消耗占用系统的线程、I/O、CPU、内存等资源。当请求 A 的服务越来越多,占用计算机的资源也越来越多,最终因为该条服务调用链阻塞会导致系统瓶颈出现,造成其他的请求同样不可用,最后导致整个业务系统崩溃。
为了有效避免 服务雪崩效应,常见的做法是 熔断和降级。最简单是加阈值开关控制,当下游系统出问题时,开关自动打开降级,不再调用下游系统。针对 .NET 技术栈小伙伴们可以使用开源组件 Ocelot 网关来实现此功效。
- 限流(预防接口服务请求过载打垮系统)
针对我们单个宿主机的资源(系统的 CPU、网络带宽、内存、线程等)提供的能力是有限的。在高并发大流量过来时,我们的预期都希望系统能对所有请求都正常处理,但是有时候没办法,因为宿主机资源的上限,对应环境部署的服务也受到一定的限制。
如果我们设计的系统服务每秒可以抗住 2k 的请求,就算该服务有多个副本存活并且做了负载均衡策略等,在突然的流量高峰负载(比如:10w 甚至更多)请求过来时,分摊到每个副本上面的数据量瞬时内承担的量也会很大,有可能就刚好超过了单副本处理能力的上限 2k。这个时候如果我们不提前做出针对性的预防措施,放任大量的流量负载请求到服务接口上,同样会造成 系统性能急剧下降,最终导致整个 系统崩溃。
这个时候,为了 有效的保护系统请求过载,我们可以采取 限流 的方案,负载超过请求阈值,多余的请求直接舍弃。
在高并发系统设计中,限流 & 降级熔断 策略是保证 系统高可用性 必不可少的手段。
什么是限流?
在计算机网络中,限流就是控制网络接口发送或接收请求的速率,它可防止 DoS 攻击 和 限制 Web 爬虫 。限流,也称流量控制。是指系统在面临高并发或者大流量请求的情况下,限制新的请求对系统的访问,从而保证系统的稳定性。
.net 技术栈中,常见的限流中间件有 Microsoft.AspNetCore.RateLimiting(单机版限流),也可以使用 Ocelot 网关或 Redis 分布式限流等。
关于《ASP.NET Core 中的速率限制中间件》请查看,https://learn.microsoft.com/zh-cn/aspnet/core/performance/rate-limit?view=aspnetcore-8.0
12. 异步(提升系统吞吐量)
我们在设计接口服务的时候,针对耗时的操作,尽量异步化处理,比如:http 网络请求,文件 I/O 操作,数据库访问操作等。
同步和异步如何区分?
以方法的调用为例,它代表 调用方要阻塞等待被调用方法中的逻辑执行完成。这种方式下,当被调用方法响应时间较长时,会造成调用方长久的阻塞,在 高并发下会造成整体系统性能下降甚至发生雪崩。异步 调用恰恰相反,调用方不需要等待方法逻辑执行完成 就可以返回执行其他的逻辑,在被调用方法执行完毕后再通过回调、事件通知等方式将结果反馈给调用方。
在高并发系统的设计中,在需要的场景中合理恰当的使用异步处理是必不可少的环节。那么我们如何使用异步呢?.NET 后端接口设计可以使用 async/await & Task 实现更彻底的异步操作,或者借用第三方组件 MQ 消息队列 实现。比如在海量秒杀请求过来时,先放到消息队列中,快速相应用户,告诉用户请求正在处理中,这样就可以释放资源来处理更多的请求。秒杀请求处理完后,通知用户秒杀抢购成功或者失败。
- 常规性的优化(提升单个模块服务性能)
当 “单兵” 作战能力上去了,“打群架” 还需担心么?
针对 “单兵” (单服务)作战能力(接口性能)的优化,通常的手段有:数据接口整合批量操作,慢 sql 优化,数据表建立索引,异步线程阻塞(异步不够彻底),数据结构/集合类型合理使用,数据多线程并行处理,大表多字段查询 等等。
- 系统性压力测试(查找性能瓶颈)
当我们设计高性能系统时,该处理的手段都处理了,最后是骡子是马,总得拉出来溜溜,这也是预估系统整体性承受上限的有效手段,在高性能系统设计中也是不可或缺的重要一环,这就是系统性的压力测试。
就是在系统上线前,需要对系统进行压力测试,测清楚你的系统支撑的最大并发是多少,确定系统的瓶颈点,让自己心里有底,做好预防措施。
压测完要分析整个调用链路,性能可能出现问题是网络层(如带宽)、Nginx 层、服务层、还是数据路缓存等中间件等等。
通过系统性的压测,可以获取到系统一些关键性的度量指标,也好对设计的系统性能方面查缺补漏。
- 部署模式集群横向扩容 & 流量切换(应对突发大流量峰值)
比如新浪微博,某一天某一时刻突然的热点话题,遇到突发的大流量高峰,除了上面我们提到的 降级熔断、限流 保证系统不跨,但并不代表我们设计的系统能吞入更多的请求负载,针对这种情况,我们可以保证系统尽最大量可能的服务用户,此时对系统设计就要求有集群快速扩容能力和流量分销切换能力。
集群扩容:比如增加从库、提升配置、添加新节点的方式,提升系统/组件的流量承载能力。比如增加 PostgreSQL、Redis 从库来处理查询请求。
流量分销和切换:服务多机房部署(同城多活,异地多活),如果高并发流量来了,把流量从一个机房切换到另一个机房。
特别是针对 云原生项目设计原则,可以把服务器托管给云平台(公有云,私有云,混合云),有能力的公司组织可以自建同城/异地 IDC,应用服务可以容器化部署在 k8s 平台,让服务运行底座更加有保障。
总结
需要注意的是,高并发系统的设计和实现远比上述几点要复杂得多。在实际业务系统中,需要 根据具体的业务需求和场景来选择合适的架构和技术。对于复杂的 分布式系统,还需要考虑数据的 一致性、可用性、分布式事务、故障恢复 等问题。
如同老年人的 三高(高血压,高血脂,高血糖)并发症问题,需要主治医师有丰富的知识体系积累和临床实践经验,才能把控患者情况。
同时,对一个有几十万行代码的复杂分布式系统进行高并发架构的设计和实践是非常具有挑战性的。这需要对系统有深入的理解和分析,以及对 高并发、分布式、缓存、消息队列 等相关技术有深厚的积累和实践经验。这样的经验是非常宝贵的,也是很多公司在招聘高级技术人才时所看重的。
当然设计一个完整的高并发系统,除了上面提到的一些设计方法,比如还有保证系统工程化和可维护性的一些功能,比如:分布式微服务下的 网关中心,配置中心,服务治理(注册与发现)中心,日志中心(合适的日志系统,为高并发系统提供有力的数据分析),链路监控中心,数据备份中心 等等。