高并发系统设计
- 高并发通用设计方法
- Scale-out(横向扩展)
- 缓存
- 异步
- 指导原则
- 分层架构
- 如何提升系统性能
- 高并发系统设计的三大目标:高性能、高可用、可扩展
- 性能优化原则
- 性能的度量指标
- 高并发下的性能优化
- 系统怎样做到高可用
- 池化技术:如何减少频繁创建数据库连接的性能损耗
- 数据库优化方案
- 主从分离
- 写入数据量增加时,如何实现分库分表
- NoSQL:在高并发场景下,数据库和NoSQL如何做到互补
- 缓存设计
- 缓存的使用姿势(一):如何选择缓存的读写策略
- Cache Aside 策略
- (读穿 / 写穿)策略
- 缓存的使用姿势(二):缓存如何做到高可用
- 客户端方案
- 缓存数据如何分片
- 主从机制
- 多副本
- 中间代理层方案
- 服务端方案
- 缓存的使用姿势
- CDN:静态资源如何加速
- 消息队列
- 如何保证消息仅仅被消费一次
- 消息队列:如何降低消息队列系统中消息的延迟
- 系统架构:每秒1万次请求的系统要做服务化拆分吗
- 微服务架构:微服务化后系统架构要如何改造
- 微服务拆分的原则
- 微服务化带来的问题和解决思路
- 服务调用
- 依赖关系
- 问题追踪
- 服务注册和发现的核心
- 负载均衡:怎样提升系统的横向扩展能力
- 服务端监控要怎么做
- 服务端限流量
- 资料参考
高并发通用设计方法
Scale-out(横向扩展)
分而治之是一种常见的高并发系统设计方法,采用分布式部署的方式把流量分流开,让每个服务器都承担一部分并发和流量。
Scale-up(纵向扩展)
通过购买性能更好的硬件来提升系统的并发处理能力,比方说目前系统 4 核 4G 每秒可以处理 200 次请求,那么如果要处理 400 次请求呢?很简单,我们把机器的硬件提升到 8 核 8G(硬件资源的提升可能不是线性的,这里仅为参考)。
缓存
使用缓存来提高系统的性能,就好比用“拓宽河道”的方式抵抗高并发大流量的冲击
异步
在某些场景下,未处理完成之前我们可以让请求先返回,在数据准备好之后再通知请求方,这样可以在单位时间内处理更多的请求
什么是同步
以方法调用为例,同步调用代表调用方要阻塞等待被调用方法中的逻辑执行完成。这种方式下,当被调用方法响应时间较长时,会造成调用方长久的阻塞,在高并发下会造成整体系统性能下降甚至发生雪崩。
什么是异步
异步调用恰恰相反,调用方不需要等待方法逻辑执行完成就可以返回执行其他的逻辑,在被调用方法执行完毕后再通过回调、事件通知等方式将结果反馈给调用方
指导原则
高并发原则
- 无状态设计:因为有状态可能涉及锁操作,锁又可能导致并发的串行化
- 保持合理的粒度:无论拆分还是服务化,其实就是服务粒度控制,控制粒度为了分散请求提高并发,或为了从管理等角度提高可操性
- 缓存、队列、并发等技巧在高并发设计上可供参考,但需依场景使用
高可用原则
本质诉求:高可用就是抵御不确定性,保证系统7*24小时健康服务
- 系统的任何发布必须具有可回滚能力
- 系统任何外部依赖必须准确衡量是否可降级,是否可无损降级,并提供降级开关
- 系统对外暴露的接口必须配置好限流,限流值必须尽量准确可靠
业务设计原则
- 安全性:防抓取,防刷单、防表单重复提交,等等等等。
- at least 消费,应考虑是否采用幂等设计
- 业务流程动态化,业务规则动态化
- 系统owner负责制、人员备份制、值班制
- 系统文档化
- 后台操作可追溯
分层架构
软件架构分层在软件工程中是一种常见的设计方式,它是将整体系统拆分成 N 个层次,每个层次有独立的职责,多个层次协同提供完整的功能
“MVC”(Model-View-Controller)架构。它将整体的系统分成了 Model(模型),View(视图)和 Controller(控制器)三个层次
另外一种常见的分层方式是将整体架构分为表现层、逻辑层和数据访问层
- 表现层:展示数据结果和接受用户指令的,是最靠近用户的一层
- 逻辑层:复杂业务的具体实现
- 数据访问层:则是主要处理和存储之间的交互
分层有什么好处
- 分层的设计可以简化系统设计,让不同的人专注做某一层次的事情
- 分层之后可以做到很高的复用
- 分层架构可以让我们更容易做横向扩展
如何来做系统分层
参照阿里发布的《阿里巴巴 Java 开发手册 v1.4.0(详尽版)》
分层架构中的每一层的作用
- 终端显示层:各端模板渲染并执行显示的层。当前主要是 Velocity 渲染,JS 渲染, JSP 渲染,移动端展示等
- 开放接口层:将 Service 层方法封装成开放接口,同时进行网关安全控制和流量控制等
- Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等
- Service 层:业务逻辑层。Manager 层:通用业务处理层。这一层主要有两个作用,其一,你可以将原先 Service 层的一些通用能力下沉到这一层,比如与缓存和存储交互策略,中间件的接入;其二,你也可以在这一层封装对第三方接口的调用,比如调用支付服务,调用审核服务等
- DAO 层:数据访问层,与底层 MySQL、Oracle、HBase 等进行数据交互。外部接口或第三方平台:包括其它部门 RPC 开放接口,基础平台,其它公司的 HTTP 接口。
在这个分层架构中主要增加了 Manager 层,它与 Service 层的关系是:Manager 层提供原子的服务接口,Service 层负责依据业务逻辑来编排原子接口。以上面的例子来说,Manager 层提供创建用户和获取用户信息的接口,而 Service 层负责将这两个接口组装起来。这样就把原先散布在表现层的业务逻辑都统一到了 Service 层,每一层的边界就非常清晰了
如何提升系统性能
高并发系统设计的三大目标:高性能、高可用、可扩展
性能优化原则
- 首先,性能优化一定不能盲目,一定是问题导向的。脱离了问题,盲目地提早优化会增加系统的复杂度,浪费开发人员的时间,也因为某些优化可能会对业务上有些折中的考虑,所以也会损伤业务。
- 其次,性能优化也遵循八二原则,即你可以用 20% 的精力解决 80% 的性能问题。所以我们在优化过程中一定要抓住主要矛盾,优先优化主要的性能瓶颈点。
- 再次,性能优化也要有数据支撑。在优化过程中,你要时刻了解你的优化让响应时间减少了多少,提升了多少的吞吐量。最后,性能优化的过程是持续的。高并发的系统通常是业务逻辑相对复杂的系统,那么在这类系统中出现的性能问题通常也会有多方面的原因。
- 因此,我们在做性能优化的时候要明确目标,比方说,支撑每秒 1 万次请求的吞吐量下响应时间在 10ms,那么我们就需要持续不断地寻找性能瓶颈,制定优化方案,直到达到目标为止
性能的度量指标
平均值
平均值是把这段时间所有请求的响应时间数据相加,再除以总请求数。
最大值
请求响应时间最长的值
分位值
分位值有很多种,比如 90 分位、95 分位、75 分位。以 90 分位为例,我们把这段时间请求的响应时间从小到大排序,假如一共有 100 个请求,那么排在第 90 位的响应时间就是 90 分位值。分位值排除了偶发极慢请求对于数据的影响,能够很好地反应这段时间的性能情况,分位值越大,对于慢请求的影响就越敏感。
高并发下的性能优化
系统怎样做到高可用
可用性的度量
MTBF 和 MTTR
- MTBF(Mean Time Between Failure)是平均故障间隔的意思,代表两次故障的间隔时间,也就是系统正常运转的平均时间。这个时间越长,系统稳定性越高
- MTTR(Mean Time To Repair)表示故障的平均恢复时间,也可以理解为平均故障时间。这个值越小,故障对于用户的影响越小。可用性与 MTBF 和 MTTR 的值息息相关,
我们可以用下面的公式表示它们之间的关系:Availability = MTBF / (MTBF + MTTR)
高可用系统设计的思路
高可用的本质诉求:高可用就是抵御不确定性,保证系统7*24小时健康服务
高可用系统不同的阶段应有着不同的技巧:
- 事前:副本、隔离、配额、提前预案、探知
- 事发:监控、报警
- 事中:降级、回滚、应急预案,failXXX系列
- 事后:复盘、思考、技改
系统设计
负载均衡
- 前端服务器的负载均衡: 用户流量应该最优地分布于多个网络链路上、多个数据中心中、以及多台服务器上。最优通常值得是: 最小化用户的请求延迟;低于带宽的95线峰值;基于可用服务容量平衡流量;
- 数据中心内部的负载均衡: 在理想情况下,某个服务的负载会完全均匀地分发给所有的后端任务。在
任何时刻,最忙和最不忙的节点永远消耗同样数量的CPU
限流
返回低质量的结果,提供有损服务。在最差的情况,妥善的限流来保证服务本身稳定。
重试
当请求返回错误(例:配额不足、超时、内部错误等),对于后端部分节点过载的情况下,倾向于立刻重试,需要留意重试带来的流量放大:
超时
应对连锁故障
系统运维
灰度发布、故障演练
池化技术:如何减少频繁创建数据库连接的性能损耗
这是一种常见的软件设计思想,叫做池化技术,它的核心思想是空间换时间,期望使用预先创建好的对象来减少频繁创建对象的性能开销,同时还可以对对象进行统一的管理,降低了对象的使用的成本,总之是好处多多。不过,池化技术也存在一些缺陷,比方说存储池子中的对象肯定需要消耗多余的内存,如果对象没有被频繁使用,就会造成内存上的浪费。再比方说,池子中的对象需要在系统启动的时候就预先创建完成,这在一定程度上增加了系统启动时间。可这些缺陷相比池化技术的优势来说就比较微不足道了,只要我们确认要使用的对象在创建时确实比较耗时或者消耗资源,并且这些对象也确实会被频繁地创建和销毁,我们就可以使用池化技术来优化。
用连接池预先建立数据库连接
数据库连接池有两个最重要的配置:最小连接数和最大连接数,它们控制着从连接池中获取连接的流程:
- 如果当前连接数小于最小连接数,则创建新的连接处理数据库请求;
- 如果连接池中有空闲连接则复用空闲连接;
- 如果空闲池中没有连接并且当前连接数小于最大连接数,则创建新的连接处理请求;
- 如果当前连接数已经大于等于最大连接数,则按照配置中设定的时间(C3P0 的连接池配置是 checkoutTimeout)等待旧的连接可用;
- 如果等待超过了这个设定时间则向用户抛出错误
用线程池预先创建线程
数据库优化方案
主从分离
主从读写分离
主从读写分离有两个技术上的关键点:
- 一个是数据的拷贝,我们称为主从复制;
- 在主从分离的情况下,我们如何屏蔽主从分离带来的访问数据库方式的变化,让开发同学像是在使用单一数据库一样
主从复制
Mysql主从复制的过程
Slave上面的IO进程连接上Master,并请求从指定日志文件的指定位置(或者从最开始的日志)之后的日志内容。
Master接收到来自Slave的IO进程的请求后,负责复制的IO进程会根据请求信息读取日志指定位置之后的日志信息,返回给Slave的IO进程。返回信息中除了日志所包含的信息之外,还包括本次返回的信息已经到Master端的bin-log文件的名称以及bin-log的位置。
Slave的IO进程接收到信息后,将接收到的日志内容依次添加到Slave端的relay-log文件的最末端,并将读取到的Master端的 bin-log的文件名和位置记录到master-info文件中,以便在下一次读取的时候能够清楚的告诉Master从何处开始读取日志。
Slave的Sql进程检测到relay-log中新增加了内容后,会马上解析relay-log的内容成为在Master端真实执行时候的那些可执行的内容,并在自身执行。
。
主从复制也有一些缺陷,除了带来了部署上的复杂度,还有就是会带来一定的主从同步的延迟,这种延迟有时候会对业务产生一定的影响。这个问题解决的思路有很多,核心思想就是尽量不去从库中查询信息.
- 第一种方案是数据的冗余
- 第二种方案是使用缓存
- 最后一种方案是查询主库
写入数据量增加时,如何实现分库分表
如何对数据库做垂直拆分
分库分表是一种常见的将数据分片的方式,它的基本思想是依照某一种策略将数据尽量平均地分配到多个数据库节点或者多个表中。
垂直拆分的原则一般是按照业务类型来拆分,核心思想是专库专用,将业务耦合度比较高的表拆分到单独的库中。
如何对数据库做水平拆分
和垂直拆分的关注点不同,垂直拆分的关注点在于业务相关性,而水平拆分指的是将单一数据表按照某一种规则拆分到多个数据库和多个数据表中,关注点在数据的特点
拆分方式
- 按照某一个字段的哈希值做拆分
- 按照某一个字段的区间来拆分
NoSQL:在高并发场景下,数据库和NoSQL如何做到互补
对于存储服务来说,我们一般会从两个方面对它做改造:
- 提升它的读写性能,尤其是读性能
- 增强它在存储上的扩展能力
NoSQL 数据库是如何做到与关系数据库互补的
MySQL 的 InnoDB 存储引擎来说,更新 binlog、redolog、undolog 都是在做顺序 IO,而更新 datafile 和索引文件则是在做随机 IO,而为了减少随机 IO 的发生,关系数据库已经做了很多的优化,比如说写入时先写入内存,然后批量刷新到磁盘上,但是随机 IO 还是会发生。
NoSQL 数据库是怎么解决这个问题的呢?绝大部分NoSQL 使用基于 LSM 树(Log-Structured Merge Tree)的存储引擎避免随机IO
- 使用 NoSQL 提升扩展性
- 副本集:理解为备份
- 分片: 理解为分库分表
- 负载均衡: 发现 分片 之间数据分布不均匀,会对数据做重新的分配,最终让不同 Server 的数据可以尽量的均衡。当我们的 Server 存储空间不足需要扩容时,数据会自动被移动到新的 Server 上,减少了数据迁移和验证的成本
缓存设计
缓存,是一种存储数据的组件,它的作用是让对数据的请求更快地返回
缓存的使用姿势(一):如何选择缓存的读写策略
Cache Aside 策略
- 读策略的步骤是:
- 从缓存中读取数据;如果缓存命中,则直接返回数据;
- 如果缓存不命中,则从数据库中查询数据;查询到数据后,将数据写入到缓存中,并且返回给用户
- 写策略的步骤是:
(读穿 / 写穿)策略
核心原则是用户只与缓存打交道,由缓存和数据库通信,写入或者读取数据
缓存的使用姿势(二):缓存如何做到高可用
客户端方案
在客户端配置多个缓存的节点,通过缓存写入和读取算法策略来实现分布式,从而提高缓存的可用性。
- 写入数据时,需要把被写入缓存的数据分散到多个节点中,即进行数据分片;
- 读数据时,可以利用多组的缓存来做容错,提升缓存系统的可用性。关于读数据,这里可以使用主从和多副本两种策略,两种策略是为了解决不同的问题而提出的。
缓存数据如何分片
分片算法常见的就是 Hash 分片算法和一致性 Hash 分片算法两种
- Hash 分片算法:对缓存的 Key 做哈希计算,然后对总的缓存节点个数取余
- 一致性 Hash 分片算法: 将整个哈希值空间组织成一个虚拟的圆环
主从机制
主从机制
多副本
在缓存前面再加一次缓存,形成多副本
中间代理层方案
在应用代码和缓存节点之间增加代理层,客户端所有的写入和读取的请求都通过代理层,而代理层中会内置高可用策略,帮助提升缓存系统的高可用
服务端方案
Redis 2.4 版本后提出的 Redis Sentinel 方案
缓存的使用姿势
缓存清理策略
- 根据过期时间,清理最长时间没用过的
- 根据过期时间,清理即将过期的
- 根据过期时间,任意清理一个
- 无论是否过期,随机清理
- 无论是否过期,根据LRU原则清理
- LRU:就是Least Recently Used,最近最久未使用过。这个原则的思想是:如果一个数据在最近一段时间没有被访问到,那么在将来他被访问的可能性也很小。LRU是在操作系统中很常见的一种原则,比如内存的页面置换算法(也包括FIFO,LFU等),对于LRU的实现,还是非常有技巧的
缓存穿透了怎么办
-
回种空值
-
布隆过滤器: 简单理解为 HASH + 数组
- 先查布隆过滤器,如果不存在就直接返回空值,而不需要继续查询数据库和缓存,这样就可以极大地减少异常查询带来的缓存穿透
CDN:静态资源如何加速
- DNS 技术是 CDN 实现中使用的核心技术,可以将用户的请求映射到 CDN 节点上;
- DNS 解析结果需要做本地缓存,降低 DNS 解析过程的响应时间;
- GSLB 可以给用户返回一个离着他更近的节点,加快静态资源的访问速度。
消息队列
消息队列主要作用:削峰填谷、解耦合、异步
- 削峰填谷是消息队列最主要的作用,但是会造成请求处理的延迟
- 解耦合可以提升你的整体系统的鲁棒性
- 异步处理是提升系统性能的神器,但需要分清同步流程和异步流程的边界,同时消息存在着丢失的风险,我们需要考虑如何确保消息一定到达
如何保证消息仅仅被消费一次
- 消息的丢失可以通过生产端的重试、消息队列配置集群模式以及消费端合理处理消费进度三种方式来解决;为了解决消息的丢失通常会造成性能上的问题以及消息的重复问题;
- 通过保证消息处理的幂等性可以解决消息的重复问题。
消息队列:如何降低消息队列系统中消息的延迟
系统架构:每秒1万次请求的系统要做服务化拆分吗
什么时候做系统拆分:
- 系统中使用的资源出现扩展性问题,尤其是数据库的连接数出现瓶颈
- 大团队共同维护一套代码,带来研发效率的降低和研发成本的提升
- 系统部署成本越来越高
微服务架构:微服务化后系统架构要如何改造
微服务拆分的原则
-
原则一:做到单一服务内部功能的高内聚和低耦合。也就是说每个服务只完成自己职责之内的任务,对于不是自己职责的功能交给其它服务来完成。说起来你可能觉得理所当然对这一点不屑一顾,但很多人在实际开发中,经常会出现一些问题。
-
原则二:你需要关注服务拆分的粒度,先粗略拆分再逐渐细化
-
原则三: 拆分的过程,要尽量避免影响产品的日常功能迭代
-
原则四: 服务接口的定义要具备可扩展性
微服务化带来的问题和解决思路
服务调用
服务接口的调用不再是同一进程内的方法调用而是跨进程的网络调用,这会增加接口响应时间的增加。此时我们就要选择高效的服务调用框架,同时接口调用方需要知道服务部署在哪些机器的哪个端口上,这些信息需要存储在一个分布式一致性的存储中,于是就需要引入服务注册中心,注册中心管理的是服务完整的生命周期,包括对于服务存活状态的检测。
依赖关系
多个服务之间有着错综复杂的依赖关系。一个服务会依赖多个其它服务也会被多个服务所依赖,那么一旦被依赖的服务的性能出现问题产生大量的慢请求,就会导致依赖服务的工作线程池中的线程被占满,依赖的服务也会出现性能问题。接下来问题就会沿着依赖网逐步向上蔓延,直到整个系统出现故障为止。为了避免发生这种情况,我们需要引入服务治理体系针对出问题的服务采用熔断、降级、限流、超时控制的方法,使问题被限制在单一服务中,保护服务网络中的其它服务不受影响。
问题追踪
服务拆分到多个进程后,一条请求的调用链路上涉及多个服务,那么一旦这个请求的响应时间增长或者是出现错误,我们就很难知道是哪一个服务出现的问题。另外,整体系统一旦出现故障,很可能外在的表现是所有服务在同一时间都出现了问题,你在问题定位时很难确认哪一个服务是源头,这就需要引入分布式追踪工具,以及更细致的服务端监控报表。
服务注册和发现的核心
引入一个注册中心的中间件,来统一管理 服务端 和 客户端。采用的保证服务端正常运行的手段是——心跳机制。定时向注册中心发送心跳包表明自己运行正常。
负载均衡:怎样提升系统的横向扩展能力
如何检测节点是否故障
服务端监控要怎么做
- 耗时、请求量和错误数是三种最通用的监控指标,不同的组件还有一些特殊的监控指标,你在搭建自己的监控系统的时候可以直接使用
- Agent、埋点和日志是三种最常见的数据采集方式
- 访问趋势报表用来展示服务的整体运行情况,性能报表用来分析资源或者依赖的服务是否出现问题,资源报表用来追查资源问题的根本原因
服务端限流量
限流方式有如下:
- 固定窗口
- 滑动窗口
- 漏斗:一般用队列来实现,但是会造成请求有延迟并且也对处理突发流量不友好。
- 令牌桶:通过往桶内定时放入一个令牌,请求过来时先要申请到令牌才能继续,否则请求失败,这个对于处理突发流量时比较友好,即平时可以攒,到突发流量时可以直接用起来,guava的ratelimiter就是令牌桶算法实现的,分布式令牌桶可以用redis来实现,可以一次申请多个而不是一个这样可以降低每次请求的开销
参考
- 谈谈服务限流算法的几种实现
- 后端服务之接口流量控制
- Golang 基于IP地址的HTTP限速请求
资料参考