高并发系统设计思路

标签: 并发 系统 设计 | 发表时间:2023-07-29 11:52 | 作者:刻意思考
出处:https://juejin.cn/backend

theme: geek-black

不管是哪一门语言,并发都是程序员们最为头疼的部分。同样,对于一个软件而言也是这样,你可以很快增删改查做出一个秒杀系统,但是要让它支持高并发访问就没那么容易了。比如说:

  • 如何让系统面对百万级的请求流量不出故障?
  • 如何保证高并发情况下数据的一致性写?
  • 完全靠堆服务器来解决吗?

基本原则

作为一个架构师,首先要勾勒出一个轮廓,如何构建一个超大流量并发读写、高性能,以及高可用的系统,这其中有哪些要素需要考虑。

  • 数据要尽量少:首先是指用户请求的数据能少就少(请求的数据包括上传给系统的数据和系统返回给用户的数据),其次还要求系统依赖的数据能少就少(包括系统完成某些业务逻辑需要读取和保存的数据,这些数据一般是和后台服务以及数据库打交道)。
  • 请求数要尽量少:用户请求的页面返回后,浏览器渲染这个页面还要包含其他的额外请求,减少请求数最常用的一个实践就是合并 CSS 和 JavaScript 文件,把多个 JavaScript 文件合并成一个文件。
  • 路径要尽量短:用户发出请求到返回数据这个过程中,需求经过的中间的节点数。
  • 依赖要尽量少:要完成一次用户请求必须依赖的系统或者服务,这里的依赖指的是强依赖。
  • 不要有单点:避免将服务的状态和机器绑定,把服务无状态化。

做好动静分离

所谓“动静分离”,其实就是把用户请求的数据划分为“动态数据”和“静态数据”,动态数据还是静态数据区分主要是:确认数据中是否含有和访问者相关的个性化数据。

怎样对静态数据做缓存呢?

  1. 把静态数据缓存到离用户最近的地方。比如调用端,客户端等等。
  2. 缓存静态数据的方式也很重要。不同语言写的 Cache 软件处理缓存数据的效率也各不相同。以 Java 为例,因为 Java 系统本身也有其弱点(比如不擅长处理大量连接请求,每个连接消耗的内存较多,Servlet 容器解析 HTTP 协议较慢),所以你可以不在 Java 层做缓存,而是直接在 Web 服务器层上做,这样你就可以屏蔽 Java 语言层面的一些弱点;而相比起来,Web 服务器(如 Nginx、Apache、Varnish)也更擅长处理大并发的静态文件请求。

动态内容的处理通常有两种方案:ESI(Edge Side Includes)方案和 CSI(Client Side Include)方案。

  1. ESI 方案(或者 SSI):即在 Web 代理服务器上做动态内容请求,并将请求插入到静态页面中,当用户拿到页面时已经是一个完整的页面了。这种方式对服务端性能有些影响,但是用户体验较好。
  2. CSI 方案。即单独发起一个异步 JavaScript 请求,向服务端获取动态内容。这种方式服务端性能更佳,但是用户端页面可能会延时,体验稍差。

针对性的处理系统“热点数据”

热点数据就是用户的热点请求对应的数据。而热点数据又分为“静态热点数据”和“动态热点数据”。“静态热点数据”,就是能够提前预测的热点数据;“动态热点数据”,就是不能被提前预测到的,系统在运行过程中临时产生的热点。

由于热点数据会引起大量的热点操作,对系统产生很大的性能压力,需要提前识别和处理。处理热点数据通常有几种思路:一是优化,二是限制,三是隔离。

  • 优化:优化热点数据最有效的办法就是缓存热点数据,如果热点数据做了动静分离,那么可以长期缓存静态数据。
  • 限制:限制更多的是一种保护机制,限制的办法也有很多,例如对被访问热点数据的 ID 做一致性 Hash,然后根据 Hash 做分桶,每个分桶设置一个处理队列,这样可以把热点限制在一个请求队列里,防止因某些热点占用太多的服务器资源,而使其他请求始终得不到服务器的处理资源。
  • 隔离:将这种热点数据隔离出来,不要让 1% 的请求影响到另外的 99%,隔离出来后也更方便对这 1% 的请求做针对性的优化。
    1. 业务隔离。把热点做成一种营销活动,请求方需要报名参加,从技术上来说,报名后对我们来说就有了已知热点,因此可以提前做好预热。
    2. 系统隔离。系统隔离更多的是运行时的隔离,可以通过分组部署的方式和另外 99% 分开。秒杀可以申请单独的域名,目的也是让请求落到不同的集群中。
    3. 数据隔离。秒杀所调用的数据大部分都是热点数据,比如会启用单独的 Cache 集群或者 MySQL 数据库来放热点数据,目的也是不想 0.01% 的数据有机会影响 99.99% 数据。

流量削峰

我们知道服务器的处理资源是恒定的,你用或者不用它的处理能力都是一样的,所以出现峰值的话,很容易导致忙到处理不过来,闲的时候却又没有什么要处理。但是由于要保证服务质量,我们的很多处理资源只能按照忙的时候来预估,而这会导致资源的一个浪费。

这就好比因为存在早高峰和晚高峰的问题,所以有了错峰限行的解决方案。削峰的存在,一是可以让服务端处理变得更加平稳,二是可以节省服务器的资源成本。针对热点这一场景,削峰从本质上来说就是更多地延缓用户请求的发出,以便减少和过滤掉一些无效请求,它遵从“请求数要尽量少”的原则。

流量削峰的一些操作思路:排队、分层过滤。这几种方式都是无损(即不会损失用户的发出请求)的实现方案,当然还有些有损的实现方案,包括我们后面要介绍的关于稳定性的一些办法,比如限流和机器负载保护等一些强制措施也能达到削峰保护的目的,当然这都是不得已的一些措施。

排队

要对流量进行削峰,最容易想到的解决方案就是用消息队列来缓冲瞬时流量,把同步的直接调用转换成异步的间接推送,中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去。在这里,消息队列就像“水库”一样,拦蓄上游的洪水,削减进入下游河道的洪峰流量,从而达到减免洪水灾害的目的。

image.png

但是,如果流量峰值持续一段时间达到了消息队列的处理上限,例如本机的消息积压达到了存储空间的上限,消息队列同样也会被压垮,这样虽然保护了下游的系统,但是和直接把请求丢弃也没多大的区别。就像遇到洪水爆发时,即使是有水库恐怕也无济于事。

除了消息队列,类似的排队方式还有很多:

  • 利用线程池加锁等待也是一种常用的排队方式;
  • 先进先出、先进后出等常用的内存排队算法的实现方式;
  • 把请求序列化到文件中,然后再顺序地读文件(例如基于 MySQL binlog 的同步机制)来恢复请求等方式。

这些方式都有一个共同特征,就是把“一步的操作”变成“两步的操作”,其中增加的一步操作用来起到缓冲的作用。

分层过滤

对请求进行分层过滤,从而过滤掉一些无效的请求。分层过滤其实就是采用“漏斗”式设计来处理请求的,如下图所示。

image.png

分层过滤的核心思想是:在不同的层次尽可能地过滤掉无效请求,让“漏斗”最末端的才是有效请求。而要达到这种效果,我们就必须对数据做分层的校验。

分层校验的基本原则是:

  1. 将动态请求的读数据缓存(Cache)在 Web 端,过滤掉无效的数据读;
  2. 对读数据不做强一致性校验,减少因为一致性校验产生瓶颈的问题;
  3. 对写数据进行基于时间的合理分片,过滤掉过期的失效请求;
  4. 对写请求做限流保护,将超出系统承载能力的请求过滤掉;
  5. 对写数据进行强一致性校验,只保留最后有效的数据。

如何提高系统的性能

对 Java 系统来说,可以优化的地方很多,这里我重点说一下比较有效的几种手段,供你参考,它们是:减少编码、减少序列化、Java 极致优化、并发读优化。

  • 减少编码:Java 的编码运行比较慢,这是 Java 的一大硬伤。
    • 在很多场景下,只要涉及字符串的操作(如输入输出操作、I/O 操作)都比较消耗 CPU 资源,不管它是磁盘 I/O 还是网络 I/O,因为都需要将字符转换成字节,而这个转换必须编码。那么如何才能减少编码呢?例如,网页输出是可以直接进行流输出的,即用 resp.getOutputStream() 函数写数据,把一些静态的数据提前转化成字节,等到真正往外写的时候再直接用 OutputStream() 函数写,就可以减少静态数据的编码转换。
  • 减少序列化:序列化也是 Java 性能的一大天敌,减少 Java 中的序列化操作也能大大提升性能。又因为序列化往往是和编码同时发生的,所以减少序列化也就减少了编码。
    • 序列化大部分是在 RPC 中发生的,因此避免或者减少 RPC 就可以减少序列化,当然当前的序列化协议也已经做了很多优化来提升性能。有一种新的方案,就是可以将多个关联性比较强的应用进行“合并部署”,而减少不同应用之间的 RPC 也可以减少序列化的消耗。
  • 并发读优化:集中式缓存为了保证命中率一般都会采用一致性 Hash,所以同一个 key 会落到同一台机器上。虽然单台缓存机器也能支撑 30w/s 的请求,但还是远不足以应对像“大秒”这种级别的热点。采用应用层的 LocalCache,即在热点系统的单机上缓存热点相关的数据。

兜底方案

系统的高可用建设,它其实是一个系统工程,需要考虑到系统建设的各个阶段,也就是说它其实贯穿了系统建设的整个生命周期。

image.png

在遇到大流量时,应该从哪些方面来保障系统的稳定运行,所以更多的是看如何针对运行阶段进行处理,这就引出了接下来的内容:降级、限流和拒绝服务。

降级

所谓“降级”,就是当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。

它是一个有目的、有计划的执行过程,所以对降级我们一般需要有一套预案来配合执行。如果我们把它系统化,就可以通过预案系统和开关系统来实现降级。它分为两部分,一部分是开关控制台,它保存了开关的具体配置信息,以及具体执行开关所对应的机器列表;另一部分是执行下发开关数据的 Agent,主要任务就是保证开关被正确执行,即使系统重启后也会生效。

image.png

执行降级无疑是在系统性能和用户体验之间选择了前者,降级后肯定会影响一部分用户的体验。

限流

如果说降级是牺牲了一部分次要的功能和用户的体验效果,那么限流就是更极端的一种保护措施了。限流就是当系统容量达到瓶颈时,我们需要通过限制一部分流量来保护系统,并做到既可以人工执行开关,也支持自动化保护的措施。

总体来说,限流既可以是在客户端限流,也可以是在服务端限流。此外,限流的实现方式既要支持 URL 以及方法级别的限流,也要支持基于 QPS 和线程的限流。 image.png

  • 客户端限流,好处可以限制请求的发出,通过减少发出无用请求从而减少对系统的消耗。缺点就是当客户端比较分散时,没法设置合理的限流阈值:如果阈值设的太小,会导致服务端没有达到瓶颈时客户端已经被限制;而如果设的太大,则起不到限制的作用。
  • 服务端限流,好处是可以根据服务端的性能设置合理的阈值,而缺点就是被限制的请求都是无效的请求,处理这些无效的请求本身也会消耗服务器资源。

拒绝服务

如果限流还不能解决问题,最后一招就是直接拒绝服务了。

当系统负载达到一定阈值时,例如 CPU 使用率达到 90% 或者系统 load 值达到 2*CPU 核数时,系统直接拒绝所有请求,这种方式是最暴力但也最有效的系统保护方式。

拒绝服务可以说是一种不得已的兜底方案,用以防止最坏情况发生,防止因把服务器压跨而长时间彻底无法提供服务。像这种系统过载保护虽然在过载时无法提供服务,但是系统仍然可以运作,当负载下降时又很容易恢复,所以每个系统和每个环节都应该设置这个兜底方案,对系统做最坏情况下的保护。

相关 [并发 系统 设计] 推荐:

高并发系统设计思路

- - 掘金 后端
不管是哪一门语言,并发都是程序员们最为头疼的部分. 同样,对于一个软件而言也是这样,你可以很快增删改查做出一个秒杀系统,但是要让它支持高并发访问就没那么容易了. 如何让系统面对百万级的请求流量不出故障. 如何保证高并发情况下数据的一致性写. 作为一个架构师,首先要勾勒出一个轮廓,如何构建一个超大流量并发读写、高性能,以及高可用的系统,这其中有哪些要素需要考虑.

高并发服务端分布式系统设计概要

- - 博客 - 伯乐在线
写这篇文章的目的,主要是把今年以来学习的一些东西积淀下来,同时作为之前文章《 高性能分布式计算与存储系统设计概要》的补充与提升,然而本人水平非常有限,回头看之前写的文章也有许多不足,甚至是错误,希望同学们看到了错误多多见谅,更欢迎与我讨论并指正. 我大概是从2010年底起开始进入高并发、高性能服务器和分布式这一块领域的研究,到现在也差不多有三年,但其实很多东西仍然是一知半解,我所提到的许许多多概念,也许任何一个我都不能讲的很清楚,还需要继续钻研.

高性能、高并发网络通信系统的架构设计

- - CSDN博客推荐文章
    随着互联网和物联网的高速发展,使用网络的人数和电子设备的数量急剧增长,其也对互联网后台服务程序提出了更高的性能和并发要求. 本文的主要目的是阐述如何进行高并发、高性能通信系统的架构设计,以及这样的系统需要用到的常用技术,但不对其中的技术细节进行讨论和实现. 本篇只起抛砖引玉的之效,如有更好的设计方案和思路,望您不舍赐教.

高并发操作和查询的数据采集和查询系统的oracle数据库设计建议

- - CSDN博客架构设计推荐文章
(1) 使用分布式垂直切分. 由于已经使用了Oracle RAC 提供分布式的集群服务. 所以对于产生大数据和高并发的表,可以采用数据库垂直分片(比如1-500号集中器的数据采集到数据库A、500-1000到B). 数据分片,是将整体数据分摊在多个存储设备上,这样每个存储设备的数据量相对就会小很多,以此满足系统的性能需求.

京东售后系统架构设计:专治多端并发、数据不一致的臭毛病

- -
通过阅读本文,您将了解到一个售后系统应该具备的一些能力、在整个上下游系统中的定位、基本的系统架构,以及针对售后业务场景中常见问题的解决方案. 京东到家售后系统作为逆向流,强依赖京东到家业务域,目前涵盖了:退款、退货、换货、维修等四大类场景,并且为用户与商家提供申诉、仲裁场景支持,为计费与结算系统提供逆向金额数据支持.

评价系统设计篇

- - 互联网 - ITeye博客
评论系统大家都见得非常多了,大到京东、淘宝、亚马逊,小到个人网站、博客都有评论系统,小型网站采用传统PHP+Mysql方式就能很快将系统搭建起来,同时采用单库单表方式就能轻松解决数据存储、数据查询等问题,但是对于上述中大型网站而言,已经远远不能支撑系统正常运行了. 接下来将从系统架构、数据存储、高性能服务等方面来揭示京东的评价系统在面对海量数据、海量请求的情况是如何处理的.

高并发系统之限流特技

- - 编程语言 - ITeye博客
在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流. 缓存的目的是提升系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹;而降级是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉,待高峰或者问题解决后再打开;而有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页),因此需有一种手段来限制这些场景的并发/请求量,即限流.

并发程序设计详解

- - 企业架构 - ITeye博客
Java性能优化系列之三--并发程序设计详解. 线程安全 设计模式 多线程 并行计算 并发.   (1)、Future-Callable模式:FutureTask类实现了Runnable接口,可以作为单独的线程运行,其Run方法中通过Sync内部类调用Callable接口,并维护Callable接口的返回值.

思考系统API设计的问题

- edware_love - C++博客-首页原创精华区
最近正好在思考系统API设计中考量的一些问题,. 我现在的理解是这样的,假设有巨大的真实内存. windows首先将高2G的内存自己占了,用作各种内核对象. 这2G内存共享给每个进程,但进程不能直接访问,只能通过windows给定的函数访问. : 然后每个进程都给他2G内存,进程如果创建自己的对象就放到自己那2G内存里面,如果要建立内核对象就放到共享的那高2G里面去.

系统设计中的简单法则

- - 酷勤网-挖经验 [expanded by feedex.net]
最近,包云岗在自己的 博客中总结了系统设计中的基本法则——简单之美,列举了不少经典观点和案例. 他首先总结了麻省理工方法(MIT Approach)和新泽西方法(New Jersey Approach)的异同:. 简单性:两种方法都强调设计必须简单,这既是对实现的要求,也是对接口的要求. 但是,MIT方法认为接口的简单要比实现的简单更加重要,而NJ方法认为实现的简单要比接口的简单更加重要.