高并发系统分析与大型互联网架构介绍
in JavaDevelop with 0 comment

高并发系统分析与大型互联网架构介绍

in JavaDevelop with 0 comment

大型系统的技术基石-高并发

目前互联网分布式系统架构设计中必须得考虑高并发因素的影响,我们给它一个定义的话它通常是指通过设计来保证系统能够同时并行处理很多的请求。小到门户网站的并发阅读量、在线聊天功能,大到春运期间 12306 官网的并发购票业务、每年的双十一电商大促时的并发交易量、电商秒杀、除夕夜微信红包的并发等,这些都体现了现实业务开发的高并发技术的刚性需求。

在双十一等电商大促活动之后,除了屡创新高的交易额外,还有一个看点就是各大电商平台整点交易的并发量。很显然,对于高并发的掌握能力在一定程度上反映了一个电商平台的技术水平,往往我们都对于一个能抗的住高并发的系统格外的青睐。一些国内外的互联网企业都在频繁地推出各种高并发相关的新技术框架,由此可见对于高并发的极致追求一直是各大互联网企业不断挑战、乐此不疲的研究方向。

“系统繁忙,请稍后再试”等异常反馈相信大家都在生活中碰到过,高并发技术是很重要的,但是也存在着巨大的技术挑战和提升的价值空间。它是一个广义的概念可以包含架构设计、SOA 面向服务的架构、分布式、微服务、数据处理、多线程等很多细分出来的知识。

现在给大家从技术的角度简要地介绍一下如何处理高并发的请求。

例如,电商的秒杀活动会带来非常大的高并发请求,为了避免超额的高并发请求冲垮电商的服务器,就需要对所有的并发请求进行处理。一般而言,可以先通过验证码和 IP 限制等手段拦截非法的用户请求,然后搭建服务集群,将合法的并发请求进行分流。之后还可以在服务器内部设置最大的连接数、最大并发数等服务参数,并通过消息队列对海量的并发请求进行削峰填谷处理。此外,为了让数据库稳定地处理高并发请求,还需要通过缓存中间件减少用户请求数据库的次数,并通过服务降级等策略减轻高并发峰值期间对系统的访问压力。最后,为了在极端情况下仍然能够保障数据的安全性能,还需要搭建数据库集群并设置合理的隔离机制。由此可见,高并发贯穿在项目设计的方方面面,从网关到服务器开发再到数据设计等环节都需要考虑高并发情况下的应对策略,本课程会涉及到很多高并发环境下的解决方案。

大家在本课程的学习过程中一定要在看懂文案、读懂代码的基础上多实践,多输出,这样子才可以后期自己实现“写出来”。不要只是动眼、动耳但是不动手,这也违背了我们实验楼的学习宗旨,希望大家参与到实践中来,多练习,多总结。

系统分析原则

在市面上存在各种优秀的软件系统,他们都遵循着相同的设计原则。大型的系统在设计的时候需要重点考虑一些原则和设计要点,这也是我们在这里想要强调的,这些原则会对系统架构的演进方案和具体的架构设计提供基础的支持,这也是我们作为系统开发设计人员在设计一个系统刚开始需要掌握的理论基础。

开发大型系统时,我们除了根据业务需求实现相应的功能模块外,还需要从高性能和高可用等多个维度来思考如何对系统进行设计,遵循高并发、容错性和可扩展等多个设计分析原则是我们重要需要考虑的。

高并发原则

高并发是每个大型项目都无法回避的问题,保证项目在高并发的环境下正常运行可以通过垂直扩展和水平扩展来实现,我们简述一下垂直和水平扩展。

垂直扩展:直接通过提升单机的性能的层面去升级硬件或者软件技术,可以用一个比喻来说明,比如当一个小牛无法拉动货物的时候,就可以将小牛换成体能强壮的大牛。

水平扩展:通过增加服务器的节点个数来横向扩展系统的性能,这也是分布式的一个说明方式,还是上面的这个比喻,当一头牛无法拉动货物的时候,就可以把货物进行拆分,然后用多头牛去拉这些货物,或者用多头牛一起去拉这批货物。

对比上面这两种方案,我们可以很显然的发现,处置扩展是最快的方法,只需要购买性能更加强大的硬件设备就可以迅速地提升性能,但是单机的能力还是有限的,如果货物很多的话,比如我们用一座山来类比,一头牛再厉害也是拉不动一座小山的,因此,大型互联网系统对于高并发的最终解决方案是水平扩展。

从技术层面讲,可以使用缓存来减少对数据库的访问,用熔断或者降级来提高响应的速度,通过流量削峰等手段在项目的入口限流,先拆分项目或者使用微服务技术快速构建功能模块,然后再用 Spring Cloud 或 Dubbo 等来统一治理这些模块,通过中间件搭建基于读写分离的高可用的数据库集群等。在系统的测试阶段还可以使用 JProfiler 等工具进行性能分析,寻找性能瓶颈,从而有针对性地优化。

衡量高并发的常见指标包括:响应时间、吞吐量或 QPS(Query per Second,每秒查询率)、并发用户数等。

容错原则

如果高并发使用不当的话,就容易造成各种逻辑混乱的情景,因此我们就需要对各种潜在的问题做好预案,也就是要确保系统要拥有一定的容错性。比如使用 Spring Boot 和 Redis 来实现分布式缓存,使用 MQ 实现分布式场景下的事务一致性,使用 MQ、PRG 模式、Token 等解决重复提交的问题,使用“去重表”实现操作的幂等性,使用集群或 Zookeeper 解决失败迁移的问题。

在分布式系统中,网络时延也是不可避免的,一般情况可以在长连接的情况下通过“心跳监测机制”来处理,我们下面简单阐述一下心跳监测机制。

在正常的网络环境下面,当用户点击了手机上某个 App 的“退出”按钮后,就会调用服务端的 exit()等退出方法,从而注销掉用户的状态,如果用户的手机信号中断、关机或者处于飞行模式话该如何判断用户的状态呢?除了通过 Session 有效时长来进行判断外,就可以通过心跳检测来判断,客户端每隔 60s 向服务端发送一次心跳,如果服务端能够接收到德华就说明客户端的状态正常,如果服务端没有接收到就等待客户端下一个 60s 发来的心跳,如果连续 N 次都没有接收到来自某个客户端的心跳的话就可以认定此客户端已经断线了。

为了进一步提高系统的容错性,还可以预先采用“隔离的手段”,对于秒杀等可预知时间的流量暴增情况,就可以提前将秒杀隔离成独立的服务,防止秒杀带来的流量问题影响到系统中的其他服务,当然如果预计的流量的增加不是很多的话,也可以使用多级缓存来解决高并发问题。

CAP 原则

分布式系统包含了多个节点,多个节点的数据之间应该如何实现同步,在数据同步的时候需要考虑哪些因素呢?CAP 原则是理解和设计分布式系统的基础,包含了 C(Consistency)一致性、A(Availability)可用性、P(Partition tolerance)分区容错性三个部分。

一致性 C:在同一时刻所有节点中的数据都是相同的,当客户端发出读请求之后,立刻能从分布式的所有节点中读取到相同的数据。

可用性 A:在合理的时间范围内,系统能够提供正常的服务,不会出现异常或者超时等不可用的现象。

分区容错性 P:当分布式系统中的一个或者多个节点发生网络故障(网络分区),从而脱离整个系统的网络环境时,系统仍然能够提供可靠的服务,也就是说,当部分节点故障时,系统还是可以正常运行的。

著名的 CAP 原则是在任何一个分布式系统中,C、A、P 三者不可兼得,最多只能同时满足两个。为什么这么说呢,我们可以看一下下面的这个图。

image-1658451880663

如果客户端发出了写的请求,成功更新了服务 A,但由于网络故障没有更新服务 B,那么下一次客户端的读请求如何处理呢?此时要么牺牲数据的一致性,要么在设计阶段就严格要求数据必须得一致,当像任何一个服务中写失败的时候,就撤销全部的写操作并提示失败,即牺牲了写操作的可用性。

一般而言分布式肯定会遇到网络问题的,分区容错性是最基本的要求,因此在实际设计的时候往往是在一致性和可用性之间根据具体的业务来权衡。

幂等性原则

上面提到了网络问题,网络问题会对用户的调用服务的次数造成影响,我们看下面的由于返回失败而导致用户重复支付的例子:

image-1658451893281

在分布式系统中,如果模块 A(如商品服务)已经成功调用了模块 B(如支付服务),但是由于网络故障等问题造成模块 B 在返回时出错,就可能导致用户因为无法感知模块 A 是否成功执行从而多次主动执行模块 A,造成模块 A 的重复执行。例如,当用户在购买商品你时,如果在点击“支付”按钮之后不能看到“支付成功”等提示,就可能再次点击“支付”按钮造成用户多次支付的异常行为。然而实际上,“支付”操作在用户第一次点击时就已经成功执行了,只是在给用户返回结果时出了错。所以我们需要考虑某个触发的动作会不会被重复执行的问题,使用幂等性原则就是对调用服务的次数进行的一种限制,即无论对某个服务提供的接口调用多次或者是一次,其结果都是相同的。

对于分布式或者微服务系统,为了实现幂等性,可以在写操作之前,先通过执行读操作来实现。还是以上面提到的案例为例,当商品服务调用支付服务时,严格按照虾米阿尼的步骤执行就可以实现幂等性:

实现幂等性的方法还可以通过 CAS 算法、分布式锁、悲观锁等方式。

特殊的是查询和删除是不会出现幂等性问题的,查询一次或者多次,删除一次或者多次的结果都是一样的。

除了幂等性外,还需要注意表单重复提交的问题,幂等性是由于网络等故障,用户不知道第一次操作是否成功,因此发送了多次重复的操作,意图在于确保第一次的操作成功,而表单重复提交是指用户已经看到了第一次操作成功的结果,但是由于误操作或者其他原因点击了“刷新页面”等功能按钮,导致多次发送相同的请求。可通过 Token 令牌机制、PRG 模式、数据库唯一约束等方法避免表单的重复提交问题。

可扩展原则

项目的规模会随着用户的数量的增加而增大,因此大型系统务必在设计的时候就要考虑到项目的扩展解决方案,可扩展原则要从项目架构、数据库设计、技术选型和编码规范等多方面考量。例如,我们可以使用面向接口、前后端分离以及模块化的编程风格,采用无状态化服务,使用高内聚低耦合的编程规范等,力争在设计时预留一些后期扩展时可能会使用到的接口,或者提前设计好项目扩展的解决方案。下面我们给大家列举了一些可扩展原则实现的具体措施:

如果在系统设计时就考虑了可扩展的各种手段,就能在系统想遇到瓶颈或业务需求改变时快速做出更改,从而大幅提高开发的效率。

可维护原则与可监控原则

一个设计优良的项目不仅能够加速项目的研发,而且还可以在项目竣工后提供良好的可维护性与可监控性,因此在系统的设计阶段,要考虑项目的可维护原则与可监控原则。

可维护原则是指系统在开发完毕后,维护人员能够方便地改进或者修复系统中存在的问题。可维护原则包含了可理解性、可修改性和可移植性等多方面因素。通常可以通过以下 5 个方面来实现项目的可维护原则。

可监控原则是指对系统中的流量、性能、服务、异常等情况进行实时的监控。理想的状态是既有仪表盘形状的图形化全局监控数据,又有基线型用于显示各个时间段的历史轨迹,还有一些关键业务的变动对比图(对比业务变更前后,用户流量的变动情况等)。

此外还需要对项目中的一些关键技术做性能监控,确保新技术的引入的确能带来性能的提升。

系统设计阶段提前规避问题

上面阐述了关于系统设计时的一些需要注意的原则,如果能够预先看到的话,就需要我们在设计层面提前解决,这样子就会给后期的开发带来很大的便捷。我们在这里简单给大家介绍几个景点的问题,如果不注意的话将会导致后期的开发工作十分艰难,甚至会造成“推倒重来”的情形。

Session 共享问题

在 Web 项目中,Session 是服务端用于保存客户端信息的重要对象。单系统中的 Session 对象可以直接保存在内存中,但是在分布式的或集群环境下,多个不同的节点就需要采取措施来共享 Session 对象,一般可以使用下面三种方式。

Session Replication

Session Replication 是指在客户端第一次发出请求后,处理该请求的服务端就会创建一个与之对应的 Session 对象,用于保存客户端的状态信息,之后为了让其他服务器也能保存一份此 Session 对象,就需要将此 Session 对象在各个服务端节点之间进行同步。

image-1658451912369

但是这个也会造成一定的问题就是严重的冗余,如果有多个用户在同时访问,那么每个服务节点中都会保存很多用户的 Session 对象。服务节点的个数越多,Session 冗余的问题就越严重,因此这个方式只适用于服务节点比较少的场景。

Session Sticky

Session Sticky 是通过 Nginx 等负载均衡工具对各个用户进行标记比如对 Cookie 进行标记,使每个用户在经过负载均衡工具之后都请求固定的服务节点。

image-1658451930359

客户端 A 和客户端 B 的请求都被分别转发到了不同的应用服务上面。因此各个服务中的 Session 就无需同步。但是这种做法也会有弊端,如果某个服务节点宕机,那么该节点上的所有 Session 对象都会丢失。

独立 Session 服务器

除了 Session Replication 和 Session Sticky 两种方式外,还可以将系统中所有的 Session 对象都存放到一个独立的 Session 服务中,之后各个应用服务再分别从这个 Session 服务中获取需要的 Session 对象。在大规模分布式系统中,推荐使用这种独立的 Session 服务方式。这种方式在存储 Session 对象时,既可以用数据库,又可以使用各种分布式或集群存储系统。

image-1658451945188

在使用了独立的 Session 服务器之后,应用服务就是一种无状态服务了,换句话说,此时的应用服务与用户的状态是无关的,无论是哪个用户在什么时间发出的请求,所有的应用服务都会进行相同的处理。先从 Session 服务中获取 Session 对象,再进行相同的业务处理。

有状态服务是指不同的应用服务与用户的状态有着密切的关系,例如假设应用服务 A 中保存着用户的 Session,应用服务 B 中没有保存,之后如果用户发出一个请求,经过 Nginx 转发到了应用服务 A 中,那么就可以直接进行下单、结算等业务,而如果用户的请求被 Nginx 转发到了应用服务 B 中,就会提示用户“请先登录······”,类似这种不同应用服务因为对用户状态的持有情况不同,从而导致的执行方式不同就可以理解为“有状态服务”。

“无状态服务”具有数据同步、快速部署的优势,但是也不能盲目地将其作为唯一的选择,任何技术或者架构的选择都得看具体的业务场景,如在小型项目中或仅有一个服务的项目中,就可以采用有状态的服务来降低开发的难度,缩短开发的周期。

技术选型原则与数据库设计

在做技术选型时,既要注意技术的性能,又要注意安全性,并预估这些技术是否有足够长的生命力。我们在这里以 MySQL 数据库为例,介绍一种数据库的选型的思路。

各种版本的 MySQL 默认的并发连接数是一到两百,单机可配置的最大连接数为 16384(一般情况下,由于计算机自身硬件的限制,单机实际能够负载的并发数最多为一千左右)。因此高并发系统面临的最大的性能瓶颈就是数据库可,我们之前设计各种缓存的目的就是尽可能地减少对数据库的访问的。

除了在页面、应用程序中增加缓存以外,我们还可以在应用程序和数据库之间加一层 Redis 高速缓存,从而提高数据的访问速度并且减少对数据库的访问次数,具体如下:

image-1658451959958

缓存穿透与缓存雪崩问题

缓存可以在一定程度上缓解高并发造成的性能问题,但在一些特定场景下缓存自身也会带来一些问题,比较典型的就是缓存穿透与缓存雪崩问题。为了讲解的方便,我们使用 MySQL 代指所有的关系型数据库,用 Redis 代指所有数据库的缓存组件。

缓存穿透

缓存穿透是指大量查询一些数据库中不存在的数据,从而影响数据库的性能。例如 Redis 等 KV 存储结构的中间件可以作为 MySQL 等数据库的缓存组件,但如果某些数据没有被 Redis 缓存却被大量的查询,就会对 MySQL 带来巨大压力。

image-1658451973412

我们在前面介绍过,单机 MySQL 最大能够承受的并发连接数只有一千左右,因此无论是设计失误(例如某个高频访问的缓存对象过期)、恶意攻击(例如频繁查询某个不存在的数据),还是偶然事件(例如由于社会新闻导致某个热点的搜索量大增)等,都可能让 MySQL 遭受缓存穿透,从而宕机。

相信大家在理解了缓存穿透的原因后,解决思路就已经明确了,举例如下。

缓存雪崩

除了缓存穿透以外,在使用缓存时还需要考虑缓存雪崩的情况。缓存雪崩是指由于某种原因造成 Redis 突然失效,从而造成 MySQL 瞬间压力骤增,进而严重影响 MySQL 性能甚至造成 MySQL 服务宕机。以下是造成缓存雪崩的两个常见原因:

为了避免缓存雪崩的发生,可参考使用以下解决方案:

不同类型服务器的选择

对于大型项目来说,为了保证较高的性能,至少需要三台服务器:

并且,不同类型的服务器对硬件的需求也各不相同,例如,应用服务器主要是处理业务逻辑,因此需要强大的 CPU 支持;数据库服务器的性能依赖于磁盘检索的速度和数据缓存的容量,因此需要速度较快的硬盘(如固态硬盘)和大容量的内存;文件服务器主要是储存文件,因此需要大容量的硬盘。

集群服务与动静分离

现在,虽然将服务器分成了应用服务器,数据库服务器和文件服务器。但是每个子服务器仅仅只有一个,并且单一应用服务器能够处理的请求连接是有限的,例如单个 Tomcat 最佳的并发量是 250 左右,如果并发的请求大于 250 就要考虑搭建 Tomcat 集群了(这里我们推荐可以使用 docker 快速搭建)。使用集群除了能解决负载均衡以外,还有另一个优势:失败迁移。当某一个应用服务器宕机时,其他应用服务器可以继续处理客户请求。

集群虽然有了,是否就适合存放全部数据并处理全部请求了呢?其实我们发现还可以进一步优化为“动静分离”。下面阐述一下“动”与“静”的概念。

静:如果是观看电视剧/电影、微视频、高清图片等这种大文件的静态请求,最好使用 CDN 将这些静态资源部署在各地的边缘服务器上,让用户可以就近获取所需内容,以此降低网络拥塞,提高用户访问响应速度。如果是 html、css、js 或小图片等这类小文件的静态资源,就可以直接缓存在反向代理服务器上,当用户请求时直接给予响应。还可以用 webpack 等工具将 css、javascript、html 进行压缩、组合,并将多个图片合成一张雪碧图,最终将静态资源进行统一打包,从而优化前端资源的体积。

动:如果是搜索商品、增、删、改等动态请求,就需要访问我们自己编写并搭建的应用服务器。

之后,如果并发数继续增加、项目继续扩大,就可以使用分布式对项目进行拆分,接下来我们给大家具体介绍一下分布式系统的概念。

分布式系统

分布式系统可以理解为将一个系统拆分为多个模块并部署到不同的计算机上,然后通过网络将这些模块进行整合,从而形成的一个完整系统。在实际应用时,分布式系统包含了分布式应用、分布式文件系统和分布式数据库等类型,具体如下。

分布式应用:将项目根据业务拆分成不同的模块,各个模块之间再通过 dubbo、Spring Cloud 等微服务架构或 SOA 技术进行整合。像这种根据业务功能,将项目拆分成多个模块的方式,也成为垂直拆分。此外,我们也可以对项目进行水平拆分,例如可以将项目根据三层架构拆分成 View 层、Service 层、Dao 层等,然后将各层部署在不同的机器上。

分布式文件系统:将所有文件分散存储到多个计算机上。

分布式数据库系统:将所有数据分散存储到多个计算机上。

分布式系统搭建完毕后,接下来可能遇到的问题是,如何提高数据的访问速度?一种解决方案就是在应用服务器上使用缓存的方式,缓存分为本地缓存和远程缓存。本地缓存的访问速度最快,但是本地机器的内存容量、CPU 能力等有限。远程缓存就是用于缓存分布式和集群上的数据,可以无限制的扩充内存容量、CPU 能力,但是远程缓存需要进行远程 IO 操作,因此缓存的速度相比本地缓存慢。

如果缓存没有命中,就需要直接请求数据库,当请求量较大时,就需要对数据库进行读写分离,并且用主从同步对数据库进行热备份。如果是海量数据,除了可以用关系型数据库搭建分布式数据库以外,还可以使用 redis、Hbase 等 NoSQL 数据库。相应的,在处理海量数据时,也推荐使用 ElasticSearch 或 Lucene 等搜索引擎,并用 Hadoop、Spark、Storm 等大数据技术进行处理。

值得一提的是,在微服务架构中,数据库的使用比较灵活:每个微服务都可以有自己独立的数据库,或者多个微服务之间也可以交叉使用或共享同一份数据库。并且这些服务之间可以使用 thrift、grpc 等 RPC 技术来进行整合。但要注意,使用 RPC 会给整个系统增加一层跨语言的中转结构,因此必然会带来一定程序的性能损耗。一般来讲,进行 RPC 跨语言调用是一种不得已而为之的办法。

大型系统架构设计

在设计大型系统的架构时,要特别注意对流量的控制,可以采取降级、限流和缓存等策略。在这里我们会给大家介绍一种常见的流量负载架构,然后再给出一个流行的软件技术的选型。

服务预处理-限流与多层负载

为了保证亿级流量下的高性能及高可用,除了精湛的编码功底和巧妙的算法以外,还需要对海量请求进行多级限流和多层负载。在这里我们给大家介绍一种处理客户端海量请求的思路,供读者们参考,具体如下。

image-1658452011326

我们接下来要做的事情是进一步扩容,以上架构已经能够扛得住千万级的流量了,但如果遇到“双十一购物”、“12306 抢票”、“大型电商秒杀”等亿级流量,还需要做进一步改进。可以进行以下两点扩充,搭建最终的 Lvs+Nginx+DNS 负载架构:

image-1658452026569

值得注意的是,如果系统本身不是很大,那么是没有必要使用以上所有组件的。例如软负载均衡 LVS 的确可以减轻后面单个组件的压力,但 LVS 自身也存在着不足:LVS 会使原本直接到达 Nginx 的请求在 LVS 自身中转了一次,因此会增加系统中的网络流量,并且带来一定的延迟。简言之,任何一个中间件都可能会增加系统的稳定性及性能,但同时也会存在着一定的弊端,在实际使用时一定要结合项目的实际情况慎重权衡。

后面会针对这样的架构模型给大家引进复杂的大型系统采用分布式或者微服务架构是如何搭建具体的应用服务的,本章节针对高并发系统和大型互联网架构的介绍就是这些,大家多体会一下解决问题时层层递进的方案是如何提出来的。