高并发应对措施

连接处理层面应对高并发的思路就是:

阻塞变非阻塞,同步变异步,核心就是充分利用单机性能,压榨CPU。

一、系统分层

这一节,我们将从系统分层这个层面来看每一层可以采用的应对之策。

二、业务层

本节主要关注在业务层,面对高并发场景下对于业务逻辑实现相关的处理方案。

三、缓存

缓存是一种存储数据的组件,它的作用就是让对数据的请求能更快的返回。高并发的场景下,如果能快速的返回请求所需要的数据,对于系统持久层是一种相当好的保护措施,对于系统来说也能提升吞吐量。常见的缓存技术有静态缓存、分布式缓存和热点本地缓存。

静态缓存在 Web 1.0 时期是非常著名的,它一般通过生成 Velocity 模板或者静态 HTML 文件来实现静态缓存,在 Nginx 上部署静态缓存可以减少对于后台应用服务器的压力。这种缓存只能针对静态数据来缓存,对于动态请求就无能为力了。那么我们如何针对动态请求做缓存呢?这时你就需要分布式缓存了。

分布式缓存性能强劲,通过一些分布式的方案组成集群可以突破单机的限制。所以在整体架构中,分布式缓存承担着非常重要的角色。

对于静态的资源的缓存可以选择静态缓存,对于动态的请求可以选择分布式缓存,那么什么时候要考虑热点本地缓存呢?答案是当我们遇到极端的热点数据查询的时候。热点本地缓存主要部署在应用服务器的代码中,用于阻挡热点查询对于分布式缓存节点或者数据库的压力。

四、消息队列

系统的初级阶段基本上是读多写少,所以在应对高并发时业务层会先加入缓存组件,希望通过缓存阻挡大量的读请求。随着系统的不断发展,高并发系统开始涌入大量的写请求,此时会使用到消息队列来应对高并发的场景。

异步解耦和削峰填谷是消息队列的主要作用,其中异步处理可以简化业务流程中的步骤,提升系统性能;削峰填谷可以削去到达秒杀系统的峰值流量,让业务逻辑的处理更加缓和;解耦可以将系统和其他系统解耦开,这样一个系统的任何变更都不会影响到另一个系统。

五、业务拆分

正常情况下,初期为了系统能尽快上线,都会是以单体架构的形式出现,而随着流量和请求的增多,单体架构开始出现一些问题:

数据库连接数可能会成为系统的瓶颈;

内部成员的沟通成本问题;

代码提交,分支管理,项目编译等问题;

模块相互依赖,强耦合,一旦出问题容易牵一发而动全身;

………

这个时候,为了系统能达到支撑高并发的效果,便会对系统做业务拆分,以应对上述众多问题,拿我们保险系统来说,最早期的保险核心系统,其实契约、保全、财务、理赔甚至是各种报表都是集中在一个系统中,虽然保险是一个低频行为,不太具有高并发的特性,但是在系统发展的过程中,也会有类似的拆分,将契约、保全、财务、理赔等从一个系统拆分成多个系统,而高并发系统则是使用了更细粒度的拆分,以提供服务的形式来应对高并发,在流量高峰期可以动态扩容,平稳流量,避免系统崩溃。

RPC

服务拆分之后,原本的单体系统就会变成分布式系统,原来在同一个进程里面两个方法调用就会变成跨进程跨网络的两个方法调用,此时就会引入RPC框架来解决跨网络通信问题。RPC框架封装了网络调用的细节,可以实现像调用本地服务一样调用远程部署的服务。

一个完整的RPC的步骤如下:

在一次 RPC 调用过程中,客户端首先会将调用的类名、方法名、参数名、参数值等信息,序列化成二进制流;

然后客户端将二进制流通过网络发送给服务端;

服务端接收到二进制流之后将它反序列化,得到需要调用的类名、方法名、参数名和参数值,再通过动态代理的方式调用对应的方法得到返回值;

服务端将返回值序列化,再通过网络发送给客户端;

客户端对结果反序列化之后,就可以得到调用的结果了。

从这个过程中,我们可以提取出RPC的核心过程就是网络传输和序列化。其中网络传输可以选择Netty,而序列化则可以选择Thrift或者Protobuf。

RPC能够解决服务之间跨网络通信的问题,但是对于RPC来说,又是如何知道自己该调用谁或者谁会调用自己呢?这个时候就会引入注册中心来解决这个问题,比如ZooKeeper、ETCD、Eureka等。

API****网关

API 网关(API Gateway)不是一个开源组件,而是一种架构模式,它是将一些服务共有的功能整合在一起,独立部署为单独的一层,用来解决一些服务治理的问题。你可以把它看作系统的边界,它可以对出入系统的流量做统一的管控。对于高并发系统,API网关的搭建是非常有必要的,因为它可以实现如下作用:

1、提供客户端一个统一的接入地址,API 网关可以将用户的请求动态路由到不同的业务服务上,并且做一些必要的协议转换工作。在系统中,微服务对外暴露的协议可能不同:有些提供的是 HTTP 服务;有些已经完成 RPC 改造,对外暴露 RPC 服务;有些遗留系统可能还暴露的是 Web Service 服务。API 网关可以对客户端屏蔽这些服务的部署地址以及协议的细节,给客户端的调用带来很大的便捷。

2、在 API 网关中,可以植入一些服务治理的策略,比如服务的熔断、降级、流量控制和分流等等。

3、客户端的认证和授权的实现,也可以放在 API 网关中。不同类型的客户端使用的认证方式是不同的。手机 APP 可以使用 Oauth 协议认证,HTML5 端和 Web 端使用 Cookie 认证,内部服务使用自研的 Token 认证方式。这些认证方式在 API 网关上可以得到统一处理,应用服务不需要了解认证的细节。

4、API 网关还可以做一些与黑白名单相关的事情,比如针对设备 ID、用户 IP、用户 ID 等维度的黑白名单。

5、在 API 网关中也可以做一些日志记录的事情,比如记录 HTTP 请求的访问日志。

持久层

本节主要关注在高并发场景下,数据持久化层所需要关注的内容。

六、读写分离

持久层最先可能会用到的应对高并发的方法就是读写分离。对于高并发系统来说,一开始都是读多写少,大量的读请求经过业务层到达持久层之后对数据库产生了极大的压力,此时就会准备一个和生产库一致的数据库来单独接收处理读请求,一般这个叫做从库,也会被称为读库,高并发流量中的读请求会被引流到从库,而写请求还是在主库写,主库和从库之间依靠主从复制机制来确保两个库的数据近乎实时一致,这样用户在读请求的时候几乎发现不了数据的不一致。

七、分库分表

读写分离主要是为了应对读请求,那如果写请求的流量大,此时又会有什么应对的方案呢?常见的一种处理方法就是分库分表。

分库分表是一种常见的将数据分片的方式,它的基本思想是依照某一种策略将数据尽量平均地分配到多个数据库节点或者多个表中。不同于主从复制时数据是全量地被拷贝到多个节点,分库分表后,每个节点只保存部分的数据,这样可以有效地减少单个数据库节点和单个数据表中存储的数据量,在解决了数据存储瓶颈的同时也能有效地提升数据查询的性能。同时,因为数据被分配到多个数据库节点上,那么数据的写入请求也从请求单一主库变成了请求多个数据分片节点,在一定程度上也会提升并发写入的性能。

数据库分库分表的方式有两种:一种是垂直拆分,另一种是水平拆分。垂直拆分的原则一般是按照业务类型来拆分,核心思想是专库专用,将业务耦合度比较高的表拆分到单独的库中。比如前面业务拆分时提到的将保险核心拆成契约、保全、财务和理赔等各大核心系统,这就是一种垂直拆分。和垂直拆分的关注点不同,垂直拆分的关注点在于业务相关性,而水平拆分指的是将单一数据表按照某一种规则拆分到多个数据库和多个数据表中,关注点在数据的特点。拆分的规则有下面这两种:

  1. 按照某一个字段的哈希值做拆分,这种拆分规则比较适用于实体表,比如说用户表,内容表,我们一般按照这些实体表的 ID 字段来拆分。
  2. 另一种比较常用的是按照某一个字段的区间来拆分,比较常用的是时间字段。你知道在内容表里面有“创建时间”的字段,而我们也是按照时间来查看一个人发布的内容。我们可能会要看昨天的内容,也可能会看一个月前发布的内容,这时就可以按照创建时间的区间来分库分表,比如说可以把一个月的数据放入一张表中,这样在查询时就可以根据创建时间先定位数据存储在哪个表里面,再按照查询条件来查询。
    负载均衡和动静分离
    负载均衡将是大型网站解决高负荷访问和大量并发请求采用的终极解决办法。
    负载均衡技术发展了多年,有很多专业的服务提供商和产品可以选择,我个人接触过一些解决方法,其中有两个架构可以给大家做参考。

NoSQL

读写分离和分库分表是改善数据库持久层应对并发能力的利器,但是在高并发的持续不断的积累下,传统关系型数据库已经很难应对数据量上的瓶颈,此时便可以利用NoSQL来帮助解决这些问题,因为它有着天生分布式的能力,能够提供优秀的读写性能,可以很好地补充传统关系型数据库的短板。