高并发概述
高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。
高并发常用的衡量指标有:
- 响应时间
- 用TP指标来衡量,TP指标是将一个时间段内的响应时间从小到大排序,并取某个百分点的响应时间作为值,比如TP99=50ms和TP999=80ms,就是指90%和99.9%的响应时间分别低于50和80毫秒.
QPS
每秒响应请求数;在互联网领域,这个指标可以代表吞吐量。
并发用户数
- 同时承载正常使用系统功能的用户数量。
- 理论上响应时间越短,QPS就会越高,支持的并发用户数就越高,比如redis,但是也和redis底层采用IO多路复用有关。
IO模型
阻塞IO
linux默认socket是阻塞的,配合线程池可以高效的解决小规模的服务请求,但是遇到大规模的服务请求,多线程模型会遇到瓶颈。 Thrift框架就提供了阻塞线程池server,通过设置线程池大小控制并发上限,线程池的数目会成为并发量的一个瓶颈。
非阻塞IO
调用读写接口立即返回,如果返回失败,需要不断轮询,会增加cpu占用。 单纯的非阻塞IO不实用,需要配合select/epoll等实现多路复用。
多路复用IO
利用select/epoll可以对同时多个非阻塞socket进行轮询,然后对有数据到达的socket执行读操作;select/epoll_wait的调用是阻塞的,可以设置超时时间。 如果连接数不高,多路复用的性能并不比阻塞IO高,多路复用的优势不是针对单个连接能处理的更快,而是在于能处理更多的连接。 多路复用IO可以用单个线程来对所有连接进行读写,效率极高,它适用于业务等待少的服务(比如redis),如果是IO密集型服务,仍然需要开启额外的线程池来处理业务,性能不高。
select和epoll
- select接口内部需要轮询各个socket,时间复杂度是O(n);而epoll则是由内核异步通知,时间复杂度O(1),当连接数增加时,epoll的性能不受影响。
- select的连接数受制于FD_SET数组的长度,默认是1024,而epoll的连接数没有限制,并发能力更强。
高并发实现
选对IO模型
- 针对计算密集型服务:采用(多路复用IO + 少量线程池)就可以支持很高的并发,使用线程池是为了利用多核,如果不考虑多核,单线程就可以处理,参考redis。
- 针对IO密集型服务:并发不高的情况下,可以使用阻塞线程池模型(Thrift),性能反而更好;如果追求高并发,则使用(多路IO复用+异步IO)实现,但实现复杂,可以使用用户线程替代异步IO。
使用缓存
业务代码中的阻塞等待会阻碍高并发,所以尽量避免有太多的阻塞等待,能使用缓存的尽量使用缓存。
分布式架构
单机优化的再好,也是有上限的;所以高并发系统都会采用分布式架构,但是需要处理服务发现、负载均衡这类问题。
流量突增
分布式架构的机器资源毕竟是有限的,遇到流量突发情况,可能会因为系统负载太高,而导致雪崩;通常的办法是通过限流和熔断降级来解决,再不行就动态扩容。
限流
在服务提供方的场景下,为了保护本服务不被突增流量拖垮,设计一个QPS模式的流控规则,当每秒的请求量超过设定的阈值时,会自动拒绝多余的请求。 阿里开源的Sentinel支持适配gin框架,通过gin支持的中间件,创建一个带有流控规则的拦截器,拦截器中调用Sentinel,Entry验证流控规则,如果触发则执行拦截回调函数,直接返回400错误。
熔断降级
在服务调用端的场景下,为了保护本服务及下游服务不被突增流量拖垮,设计一个基于超时比例或慢调用比例的熔断规则,当超过阈值时自动熔断,一段时间后尝试恢复,熔断期间执行默认的降级函数。 阿里开源的Sentinel支持适配gRpc框架,借助gRpc的拦截器(UnaryClientInterceptor)机制,创建一个带有熔断规则的拦截器,实现熔断降级。
动态扩容
采用docker和k8s来实现动态的扩容和缩容。