我理解的高并发
什么是高并发?
我认为大流量就是高并发。应用在某时刻有大量请求涌入,就是高并发。至于电商里的超卖问题则是大流量带来的数据同步问题。类似的问题还有接口响应超时、CPU负载升高、GC频繁、死锁、大数据量存储等等。
每个业务系统都有自己的高并发,有读多写少的信息流场景、有读多写多的交易场景等等。
为什么要设计高并发应用
就三点:高性能、高可用,以及高扩展。
- 高性能:响应要快
- 高可用:应用不能天天宕机
- 高扩展:双十一得支持服务扩容
高并发衡量指标
- 平均响应时间:字如其意。
- TP90、TP99等分位值:将响应时间按照从小到大排序,TP90表示排在第90分位的响应时间。
- 吞吐量:每秒钟平均处理多少请求。
- 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协议中,会话分为三个阶段:
- 客户端建立一条 TCP 连接(如果传输层不是 TCP,也可以是其他适合的连接)。
- 客户端发送请求并等待应答。
- 服务器处理请求并送回应答,回应包括一个状态码和对应的数据。
从 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相关的配置参数有:
- acceptCount,当accept队列中连接的个数达到acceptCount时,队列满,进来的请求一律被拒绝。
- maxConnections,当Tomcat接收的连接数达到maxConnections时,accept队列中的线程会一直阻塞着。
- 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
通用的高并发手段
通用的设计方法主要是从「纵向」和「横向」两个维度出发,俗称高并发处理的两板斧:纵向扩展和横向扩展。
纵向扩展
它的目标是提升单机的处理能力,方案又包括:
提升单机的硬件性能:通过增加内存、 CPU核数、存储容量、或者将磁盘 升级成SSD 等堆硬件的方式来提升 。
提升单机的软件性能:使用缓存减少IO次数,使用并发或者异步的方式增加吞吐量。
横向扩展
因为单机性能总会存在极限,所以最终还需要引入横向扩展,通过集群部署以进一步提高并发处理能力,又包括以下2个方向:
- 做好分层架构:这是横向扩展的提前,因为高并发系统往往业务复杂,通过分层处理可以简化复杂问题,更容易做到横向扩展。
上面这种图是互联网最常见的分层架构,当然真实的高并发系统架构会在此基础上进一步完善。比如会做动静分离并引入CDN,反向代理层可以是LVS+Nginx,Web层可以是统一的API网关,业务服务层可进一步按垂直业务做微服务化,存储层可以是各种异构数据库。
- 各层进行水平扩展:无状态水平扩容,有状态做分片路由。业务集群通常能设计成无状态的,而数据库和缓存往往是有状态的,因此需要设计分区键做好存储分片,当然也可以通过主从同步、读写分离的方案提升读性能。
具体的实践方案
下面再结合我的个人经验,针对高性能、高可用、高扩展3个方面,总结下可落地的实践方案。
高性能的实践方案
- 集群部署,通过负载均衡减轻单机压力。
- 多级缓存,包括静态数据使用CDN、本地缓存、分布式缓存等,以及对缓存场景中的热点key、缓存穿透、缓存并发、数据一致性等问题的处理。
- 分库分表和索引优化,以及借助搜索引擎解决复杂查询问题。
- 考虑NoSQL数据库的使用,比如HBase、TiDB等,但是团队必须熟悉这些组件,且有较强的运维能力。
- 异步化,将次要流程通过多线程、MQ、甚至延时任务进行异步处理。
- 限流,需要先考虑业务是否允许限流(比如秒杀场景是允许的),包括前端限流、Nginx接入层的限流、服务端的限流。
- 对流量进行削峰填谷 ,通过 MQ承接流量。
- 并发处理,通过多线程将串行逻辑并行化。
- 预计算,比如抢红包场景,可以提前计算好红包金额缓存起来,发红包时直接使用即可。
- 缓存预热 ,通过异步 任务 提前 预热数据到本地缓存或者分布式缓存中。
- 减少IO次数,比如数据库和缓存的批量读写、RPC的批量接口支持、或者通过冗余数据的方式干掉RPC调用。
- 减少IO时的数据包大小,包括采用轻量级的通信协议、合适的数据结构、去掉接口中的多余字段、减少缓存key的大小、压缩缓存value等。
- 程序逻辑优化,比如将大概率阻断执行流程的判断逻辑前置、For循环的计算逻辑优化,或者采用更高效的算法。
- 各种池化技术的使用和池大小的设置,包括HTTP请求池、线程池(考虑CPU密集型还是IO密集型设置核心参数)、数据库和Redis连接池等。
- JVM优化,包括新生代和老年代的大小、GC算法的选择等,尽可能减少GC频率和耗时。
- 锁选择,读多写少的场景用乐观锁,或者考虑通过分段锁的方式减少锁冲突。
上述方案无外乎从计算和 IO 两个维度考虑所有可能的优化点,需要有配套的监控系统实时了解当前的性能表现,并支撑你进行性能瓶颈分析,然后再遵循二八原则,抓主要矛盾进行优化。
高可用的实践方案
对等节点的故障转移,Nginx和服务治理框架均支持一个节点失败后访问另一个节点。
非对等节点的故障转移,通过心跳检测并实施主备切换(比如redis的哨兵模式或者集群模式、MySQL的主从切换等)。
接口层面的超时设置、重试策略和幂等设计。
降级处理:保证核心服务,牺牲非核心服务,必要时进行熔断;或者核心链路出问题时,有备选链路。
限流处理:对超过系统处理能力的请求直接拒绝或者返回错误码。
MQ场景的消息可靠性保证,包括producer端的重试机制、broker侧的持久化、consumer端的ack机制等。
灰度发布,能支持按机器维度进行小流量部署,观察系统日志和业务指标,等运行平稳后再推全量。
监控报警:全方位的监控体系,包括最基础的CPU、内存、磁盘、网络的监控,以及Web服务器、JVM、数据库、各类中间件的监控和业务指标的监控。
灾备演练:类似当前的“混沌工程”,对系统进行一些破坏性手段,观察局部故障是否会引起可用性问题。
高可用的方案主要从冗余、取舍、系统运维3个方向考虑,同时需要有配套的值班机制和故障处理流程,当出现线上问题时,可及时跟进处理。
高扩展的实践方案
合理的分层架构:比如上面谈到的互联网最常见的分层架构,另外还能进一步按照数据访问层、业务逻辑层对微服务做更细粒度的分层(但是需要评估性能,会存在网络多一跳的情况)。
存储层的拆分:按照业务维度做垂直拆分、按照数据特征维度进一步做水平拆分(分库分表)。
业务层的拆分:最常见的是按照业务维度拆(比如电商场景的商品服务、订单服务等),也可以按照核心接口和非核心接口拆,还可以按照请求源拆(比如To C和To B,APP和H5 )。
作者:Lowry
链接:https://zhuanlan.zhihu.com/p/279147404
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
最后附上携程技术团队的一篇文章:
干货 | 携程门票秒杀系统的设计与实践