什么是高并发?

我认为大流量就是高并发。应用在某时刻有大量请求涌入,就是高并发。至于电商里的超卖问题则是大流量带来的数据同步问题。类似的问题还有接口响应超时、CPU负载升高、GC频繁、死锁、大数据量存储等等。

每个业务系统都有自己的高并发,有读多写少的信息流场景、有读多写多的交易场景等等。

为什么要设计高并发应用

就三点:高性能、高可用,以及高扩展。

  1. 高性能:响应要快
  2. 高可用:应用不能天天宕机
  3. 高扩展:双十一得支持服务扩容

高并发衡量指标

  1. 平均响应时间:字如其意。
  2. TP90、TP99等分位值:将响应时间按照从小到大排序,TP90表示排在第90分位的响应时间。

  1. 吞吐量:每秒钟平均处理多少请求。
  2. n个9:

大量连接的接收处理

单纯的高并发就是应用只有大量连接涌入,类似ddos攻击。就像大量用户打开手机淘宝挂着不操作,服务端需要维护每个连接。因为每个连接可能会有请求事件发生。当然超时连接优化又是另外一件事了。

一种优化做法就是采用高效的IO模型,不能是BIO导致同步串行处理连接。

Linux里有select、poll、epoll三种NIO模型,最为高效的当属epoll了。它用红黑树这种高效的查询结构保存所有连接的文件描述符(fd)。并向内核注册回调函数,网卡收到数据包后产生中断通知网络子系统,由网络子系统调用回调函数把数据写入到文件描述符对应的socket缓冲区里。这意味着该连接产生了IO事件,回调函数还会把该连接的文件描述符封装为就绪队列里的一个节点。应用层调用epoll_wait函数就能通过就绪队列获取到所有有IO事件产生的连接。

既然谈到连接和请求,就顺便写一个以前困挠过我的问题:

SpringBoot请求和线程一一对应吗?

主从Reactor模型

先用netty的连接处理过程引出主从Reactor模型:

主Reactor就是BossEventLoopGroup,负责处理连接。

从Reactor就是WorkerEventLoopGroup,负责处理有IO事件的连接。

关于Reactor模型参考这篇文章:https://zhuanlan.zhihu.com/p/347779760

知乎的回答

Spring Boot中线程的维护是由servlet容器或Netty负责,所以题主应该问的是servlet容器的线程模型。而Spring Boot是一个自动配置的框架。目前Spring Boot对web开发目前有两种解决方案:1. 传统的web框架基于Spring MVC + Tomcat,2. Spring 5新增的web-reactive框架基于Spring Webflux + Netty。这两个框架都支持大家熟用的注释,比如@Controller。技术栈可以看官网图,

先不谈web-reactive,通过分析Spring MVC和Tomcat的交互,来浅析一下Spring与servlet容器交互的原理——Spring MVC基于Java EE的Servlet API,Servlet API定义了Servlet容器和具体的servlet代码交互的约定,Spring MVC通过注册一个名为DispatcherServlet的servlet到servlet容器中处理请求,并把实际工作交给Spring提供的组件bean执行。

Spring和servlet容器的交互

了解了Spring和servlet容器的交互之后再回到问题。

连接和请求的区别

首先题主可能对连接和请求的概念混淆了,这里强调一下连接(TCP)是传输层的,请求(HTTP)是应用层的。在像 HTTP 这样的Client-Server协议中,会话分为三个阶段:

  1. 客户端建立一条 TCP 连接(如果传输层不是 TCP,也可以是其他适合的连接)。
  2. 客户端发送请求并等待应答。
  3. 服务器处理请求并送回应答,回应包括一个状态码和对应的数据。

从 HTTP/1.1 开始,连接在完成第三阶段后不再关闭,客户端可以再次发起新的请求。这意味着第二步和第三步可以连续进行数次。

请求的处理过程

其次真的是一个线程处理一个HTTP请求吗?我觉得这个说法也不准确。

Tomcat支持三种运行模式(BIO, NIO, APR),大致流程均是:

当客户端向服务器建立TCP连接,发送请求,服务器操作系统将该连接放入accept队列,Tomcat在accept队列中接收连接;在连接中获取请求的数据,生成request;调用servlet容器处理请求;返回response,完成一次HTTP会话。在Tomcat 8.0前默认使用BIO,Tomcat在accept队列中接受TCP连接并获得HTTP Request,从线程池中取出空闲的线程来处理请求,如果无空闲线程则阻塞。Tomcat 8.0起默认启用NIO模式,在从accept获得request之后,注册到nio.Selector中后不阻塞继续获取连接,Tomcat遍历找到selector中可用的request,再从线程池中取出空闲的线程来处理请求。Tomcat相关的配置参数有:

  1. acceptCount,当accept队列中连接的个数达到acceptCount时,队列满,进来的请求一律被拒绝。
  2. maxConnections,当Tomcat接收的连接数达到maxConnections时,accept队列中的线程会一直阻塞着。
  3. maxThreads,线程池线程的最大数量。

所以无论是BIO,还是NIO,当请求数量大于acceptCount,接收的连接数大于maxConnection时,Tomcat都不会分配线程服务。

作者:ptyin
链接:https://www.zhihu.com/question/502043167/answer/2268721516
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

通过引用的知乎回答和上文中的epoll机制以及Reactor主从模型,可以猜想:在tomcat8.0作为servlet容器的SpringBoot里,维护一个线程池。一次调用epoll_wait获取的就绪队列,由线程池里的一个线程负责处理。

再引用我的语雀知识库里关于netty的一段话:

Selector是Java NIO(New I/O)库中的一个核心组件,是对Java平台上的IO多路复用机制的封装. 它内部会根据运行的操作系统选择不同的IO多路复用实现(如select、epoll等)

监听到channel有事件发生后通知netty,netty将该事件封装成高级事件,如read,write,accept事件等

根据不同的事件类型选择Channelhandle的不同方法,如ChanelRead方法就是用于处理read事件

netty服务端有两个EventLoopGroup线程池,Boss和Worker

Boss只负责处理Accept事件,Cilent被accept之后,它对应的channel会被Worker以负载均衡的模式被注册进eventloopgroup里的某个eventloop的任务队列中

eventloop需要处理对应队列里每个channel的事件,是无锁串行化执行的,使得系统吞吐量最大化,避免了线程不安全和线程创建销毁的开销。也是由于其串行化执行,一旦有耗时较长的IO操作,就会导致队列阻塞

可以理解为:eventloopgroup是一个线程池,eventloop是一个线程。

当然确实如此。只是eventloopgroup这个线程池和JUC里的那些线程池不太一样。

所以,假如有两个客户端各发了一个请求到达使用web-reactive框架的SpringBoot后端,并且它们的channel被注册进同一个eventloop,那么这两个请求确实是由同一个线程处理的。

超卖问题

直接参考这篇文章吧

https://developer.aliyun.com/article/1055458

通用的高并发手段

通用的设计方法主要是从「纵向」和「横向」两个维度出发,俗称高并发处理的两板斧:纵向扩展和横向扩展。

纵向扩展

它的目标是提升单机的处理能力,方案又包括:

  1. 提升单机的硬件性能:通过增加内存、 CPU核数、存储容量、或者将磁盘 升级成SSD 等堆硬件的方式来提升 。

  2. 提升单机的软件性能:使用缓存减少IO次数,使用并发或者异步的方式增加吞吐量。

横向扩展

因为单机性能总会存在极限,所以最终还需要引入横向扩展,通过集群部署以进一步提高并发处理能力,又包括以下2个方向:

  1. 做好分层架构:这是横向扩展的提前,因为高并发系统往往业务复杂,通过分层处理可以简化复杂问题,更容易做到横向扩展。

上面这种图是互联网最常见的分层架构,当然真实的高并发系统架构会在此基础上进一步完善。比如会做动静分离并引入CDN,反向代理层可以是LVS+Nginx,Web层可以是统一的API网关,业务服务层可进一步按垂直业务做微服务化,存储层可以是各种异构数据库。

  1. 各层进行水平扩展:无状态水平扩容,有状态做分片路由。业务集群通常能设计成无状态的,而数据库和缓存往往是有状态的,因此需要设计分区键做好存储分片,当然也可以通过主从同步、读写分离的方案提升读性能。

具体的实践方案

下面再结合我的个人经验,针对高性能、高可用、高扩展3个方面,总结下可落地的实践方案。

高性能的实践方案

  1. 集群部署,通过负载均衡减轻单机压力。
  2. 多级缓存,包括静态数据使用CDN、本地缓存、分布式缓存等,以及对缓存场景中的热点key、缓存穿透、缓存并发、数据一致性等问题的处理。
  3. 分库分表和索引优化,以及借助搜索引擎解决复杂查询问题。
  4. 考虑NoSQL数据库的使用,比如HBase、TiDB等,但是团队必须熟悉这些组件,且有较强的运维能力。
  5. 异步化,将次要流程通过多线程、MQ、甚至延时任务进行异步处理。
  6. 限流,需要先考虑业务是否允许限流(比如秒杀场景是允许的),包括前端限流、Nginx接入层的限流、服务端的限流。
  7. 对流量进行削峰填谷 ,通过 MQ承接流量。
  8. 并发处理,通过多线程将串行逻辑并行化。
  9. 预计算,比如抢红包场景,可以提前计算好红包金额缓存起来,发红包时直接使用即可。
  10. 缓存预热 ,通过异步 任务 提前 预热数据到本地缓存或者分布式缓存中。
  11. 减少IO次数,比如数据库和缓存的批量读写、RPC的批量接口支持、或者通过冗余数据的方式干掉RPC调用。
  12. 减少IO时的数据包大小,包括采用轻量级的通信协议、合适的数据结构、去掉接口中的多余字段、减少缓存key的大小、压缩缓存value等。
  13. 程序逻辑优化,比如将大概率阻断执行流程的判断逻辑前置、For循环的计算逻辑优化,或者采用更高效的算法。
  14. 各种池化技术的使用和池大小的设置,包括HTTP请求池、线程池(考虑CPU密集型还是IO密集型设置核心参数)、数据库和Redis连接池等。
  15. JVM优化,包括新生代和老年代的大小、GC算法的选择等,尽可能减少GC频率和耗时。
  16. 锁选择,读多写少的场景用乐观锁,或者考虑通过分段锁的方式减少锁冲突。

上述方案无外乎从计算和 IO 两个维度考虑所有可能的优化点,需要有配套的监控系统实时了解当前的性能表现,并支撑你进行性能瓶颈分析,然后再遵循二八原则,抓主要矛盾进行优化。

高可用的实践方案

  1. 对等节点的故障转移,Nginx和服务治理框架均支持一个节点失败后访问另一个节点。

  2. 非对等节点的故障转移,通过心跳检测并实施主备切换(比如redis的哨兵模式或者集群模式、MySQL的主从切换等)。

  3. 接口层面的超时设置、重试策略和幂等设计。

  4. 降级处理:保证核心服务,牺牲非核心服务,必要时进行熔断;或者核心链路出问题时,有备选链路。

  5. 限流处理:对超过系统处理能力的请求直接拒绝或者返回错误码。

  6. MQ场景的消息可靠性保证,包括producer端的重试机制、broker侧的持久化、consumer端的ack机制等。

  7. 灰度发布,能支持按机器维度进行小流量部署,观察系统日志和业务指标,等运行平稳后再推全量。

  8. 监控报警:全方位的监控体系,包括最基础的CPU、内存、磁盘、网络的监控,以及Web服务器、JVM、数据库、各类中间件的监控和业务指标的监控。

  9. 灾备演练:类似当前的“混沌工程”,对系统进行一些破坏性手段,观察局部故障是否会引起可用性问题。

高可用的方案主要从冗余、取舍、系统运维3个方向考虑,同时需要有配套的值班机制和故障处理流程,当出现线上问题时,可及时跟进处理。

高扩展的实践方案

  1. 合理的分层架构:比如上面谈到的互联网最常见的分层架构,另外还能进一步按照数据访问层、业务逻辑层对微服务做更细粒度的分层(但是需要评估性能,会存在网络多一跳的情况)。

  2. 存储层的拆分:按照业务维度做垂直拆分、按照数据特征维度进一步做水平拆分(分库分表)。

  3. 业务层的拆分:最常见的是按照业务维度拆(比如电商场景的商品服务、订单服务等),也可以按照核心接口和非核心接口拆,还可以按照请求源拆(比如To C和To B,APP和H5 )。

作者:Lowry
链接:https://zhuanlan.zhihu.com/p/279147404
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

最后附上携程技术团队的一篇文章:
干货 | 携程门票秒杀系统的设计与实践