介绍
不能说并发量大就认为是高并发,要结合业务场景。不能说10W QPS的秒杀是高并发,而1W QPS的信息流就不是高并发。
信息流场景涉及复杂的推荐模型和各种人工策略,它的业务逻辑可能比秒杀场景复杂10倍不止。因此,不在同一个维度,没有任何比较意义。
业务都是从0到1做起来的,并发量和QPS只是参考指标,最重要的是:在业务量逐渐变成原来的10倍、100倍的过程中,你是否用到了高并发的处理方法去演进你的系统,从架构设计、编码实现、甚至产品方案等维度去预防和解决高并发引起的问题?而不是一味的升级硬件、加机器做水平扩展。
高并发系统设计目标
宏观目标
- 高性能。性能体现了系统的并行处理能力,在有限的硬件投入下,提高性能意味着节省成本。同时,性能也反映了用户体验。
- 高可用。表示系统可以正常服务的时间。
- 高扩展性。表示系统的扩展能力,流量高峰时能否在短时间内完成扩容,更平稳地承接峰值流量。
这3个目标是需要通盘考虑的,因为它们互相关联、甚至也会相互影响。
比如说:考虑系统的扩展能力,你会将服务设计成无状态的,这种集群设计保证了高扩展性,其实也间接提升了系统的性能和可用性。
再比如说:为了保证可用性,通常会对服务接口进行超时设置,以防大量线程阻塞在慢请求上造成系统雪崩,那超时时间设置成多少合理呢?一般,我们会参考依赖服务的性能表现进行设置。
微观目标
性能指标
通过性能指标可以度量目前存在的性能问题,同时作为性能优化的评估依据。一般来说,会采用一段时间内的接口响应时间作为指标。
- 平均响应时间:最常用,但是缺陷很明显,对于慢请求不敏感。比如1万次请求,其中9900次是1ms,100次是100ms,则平均响应时间为1.99ms,虽然平均耗时仅增加了0.99ms,但是1%请求的响应时间已经增加了100倍。
- TP90、TP99等分位值:将响应时间按照从小到大排序,TP90表示排在第90分位的响应时间, 分位值越大,对慢请求越敏感。
- 吞吐量:和响应时间呈反比,比如响应时间是1ms,则吞吐量为每秒1000次。
通常,设定性能目标时会兼顾吞吐量和响应时间,比如这样表述:在每秒1万次请求下,AVG控制在50ms以下,TP99控制在100ms以下。对于高并发系统,AVG和TP分位值必须同时要考虑。
另外,从用户体验角度来看,200毫秒被认为是第一个分界点,用户感觉不到延迟,1秒是第二个分界点,用户能感受到延迟,但是可以接受。
因此,对于一个健康的高并发系统,TP99应该控制在200毫秒以内,TP999或者TP9999应该控制在1秒以内。
可用性指标
高可用性是指系统具有较高的无故障运行能力,可用性 = 平均故障时间 / 系统总运行时间,一般使用几个9来描述系统的可用性。
对于高并发系统来说,最基本的要求是:保证3个9或者4个9。原因很简单,如果你只能做到2个9,意味着有1%的故障时间,像一些大公司每年动辄千亿以上的GMV或者收入,1%就是10亿级别的业务影响。
可扩展性指标
面对突发流量,不可能临时改造架构,最快的方式就是增加机器来线性提高系统的处理能力。
对于业务集群或者基础组件来说,扩展性 = 性能提升比例 / 机器增加比例,理想的扩展能力是:资源增加几倍,性能提升几倍。通常来说,扩展能力要维持在70%以上。
考虑系统的扩展能力,你会将服务设计成无状态的,这种集群设计保证了高扩展性,其实也间接提升了系统的性能和可用性。但是从高并发系统的整体架构角度来看,扩展的目标不仅仅是把服务设计成无状态就行了,因为当流量增加10倍,业务服务可以快速扩容10倍,但是数据库可能就成为了新的瓶颈。像MySQL这种有状态的存储服务通常是扩展的技术难点,如果架构上没提前做好规划(垂直和水平拆分),就会涉及到大量数据的迁移。
因此,高扩展性需要考虑:服务集群、数据库、缓存和消息队列等中间件、负载均衡、带宽、依赖的第三方等,当并发达到某一个量级后,上述每个因素都可能成为扩展的瓶颈点。
高并发系统设计原则
高并发设计同样要秉承架构设计的3个原则:简单、合适和演进。“过早的优化是万恶之源”,不能脱离业务的实际情况,更不要过度设计,合适的方案就是最完美的。
高并发通用实践方案
纵向扩展
(scale-up)
目标是提升单机的处理能力,方案又包括:
- 提升单机的硬件性能:通过增加内存、CPU核数、存储容量、或者将磁盘升级成SSD等堆硬件的方式来提升。
- 提升单机的软件性能:使用缓存减少IO次数,使用并发或者异步的方式增加吞吐量。
横向扩展
(scale-out)
因为单机性能总会存在极限,所以最终还需要引入横向扩展,通过集群部署以进一步提高并发处理能力,又包括以下2个方向:
- 做好分层架构:这是横向扩展的提前,因为高并发系统往往业务复杂,通过分层处理可以简化复杂问题,更容易做到横向扩展。
- 各层进行水平扩展:无状态水平扩容,有状态做分片路由。业务集群通常能设计成无状态的,而数据库和缓存往往是有状态的,因此需要设计分区键做好存储分片,当然也可以通过主从同步、读写分离的方案提升读性能。
上面这种图是互联网最常见的分层架构,当然真实的高并发系统架构会在此基础上进一步完善。比如会做动静分离并引入CDN,反向代理层可以是LVS+Nginx,Web层可以是统一的API网关,业务服务层可进一步按垂直业务做微服务化,存储层可以是各种异构数据库。
高并发的具体实践方案
围绕高并发系统的三大设计目标来总结。
高性能方案
以下方案无外乎从计算和 IO 两个维度考虑所有可能的优化点,需要有配套的监控系统实时了解当前的性能表现,并支撑你进行性能瓶颈分析,然后再遵循二八原则,抓主要矛盾进行优化。
- 集群部署,通过负载均衡减轻单机压力。
- 多级缓存,包括静态数据使用CDN、本地缓存、分布式缓存等,以及对缓存场景中的热点key、缓存穿透、缓存并发、数据一致性等问题的处理。
- 分库分表和索引优化,以及借助搜索引擎解决复杂查询问题。
- 考虑NoSQL数据库的使用,比如HBase、TiDB等,但是团队必须熟悉这些组件,且有较强的运维能力。
- 异步化,将次要流程通过多线程、MQ、甚至延时任务进行异步处理。
- 限流,需要先考虑业务是否允许限流(比如秒杀场景是允许的),包括前端限流、Nginx接入层的限流、服务端的限流。
- 对流量进行削峰填谷,通过MQ承接流量。
- 并发处理,通过多线程将串行逻辑并行化。
- 预计算,比如抢红包场景,可以提前计算好红包金额缓存起来,发红包时直接使用即可。
- 缓存预热,通过异步任务提前预热数据到本地缓存或者分布式缓存中。
- 减少IO次数,比如数据库和缓存的批量读写、RPC的批量接口支持、或者通过冗余数据的方式干掉RPC调用。
- 减少IO时的数据包大小,包括采用轻量级的通信协议、合适的数据结构、去掉接口中的多余字段、减少缓存key的大小、压缩缓存value等。
- 程序逻辑优化,比如将大概率阻断执行流程的判断逻辑前置、For循环的计算逻辑优化,或者采用更高效的算法。
- 各种池化技术的使用和池大小的设置,包括HTTP请求池、线程池(考虑CPU密集型还是IO密集型设置核心参数)、数据库和Redis连接池等。
- JVM优化,包括新生代和老年代的大小、GC算法的选择等,尽可能减少GC频率和耗时。
- 锁选择,读多写少的场景用乐观锁,或者考虑通过分段锁的方式减少锁冲突。
高可用方案
高可用的方案主要从冗余、取舍、系统运维3个方向考虑,同时需要有配套的值班机制和故障处理流程,当出现线上问题时,可及时跟进处理。
- 对等节点的故障转移,Nginx和服务治理框架均支持一个节点失败后访问另一个节点。
- 非对等节点的故障转移,通过心跳检测并实施主备切换(比如redis的哨兵模式或者集群模式、MySQL的主从切换等)。
- 接口层面的超时设置、重试策略和幂等设计。
- 降级处理:保证核心服务,牺牲非核心服务,必要时进行熔断;或者核心链路出问题时,有备选链路。
- 限流处理:对超过系统处理能力的请求直接拒绝或者返回错误码。
- MQ场景的消息可靠性保证,包括producer端的重试机制、broker侧的持久化、consumer端的ack机制等。
- 灰度发布,能支持按机器维度进行小流量部署,观察系统日志和业务指标,等运行平稳后再推全量。
- 监控报警:全方位的监控体系,包括最基础的CPU、内存、磁盘、网络的监控,以及Web服务器、JVM、数据库、各类中间件的监控和业务指标的监控。
- 灾备演练:类似当前的“混沌工程”,对系统进行一些破坏性手段,观察局部故障是否会引起可用性问题。
高扩展
- 合理的分层架构:比如上面谈到的互联网最常见的分层架构,另外还能进一步按照数据访问层、业务逻辑层对微服务做更细粒度的分层(但是需要评估性能,会存在网络多一跳的情况)。
- 存储层的拆分:按照业务维度做垂直拆分、按照数据特征维度进一步做水平拆分(分库分表)。
- 业务层的拆分:最常见的是按照业务维度拆(比如电商场景的商品服务、订单服务等),也可以按照核心接口和非核心接口拆,还可以按照请求源拆(比如To C和To B,APP和H5)。
高并发系统演进
最简单的系统架构
刚刚开始你的系统就部署在一台机器上,背后就连接了一台数据库,数据库部署在一台服务器上。例如你的系统部署的机器是4核8G,数据库服务器是16核32G。
此时假设系统用户量总共就10万,每天活跃的用户10%即1万。
按照二八法则,每天高峰期算4个小时,高峰期活跃的用户占比达到80%,就是8000人活跃在4小时内。每个人对系统发起请求,按照每天20次算,那么高峰期8000人发起的请求也才16万次,平均到4小时内的每秒(14400秒),每秒也就10次请求。
系统层面每秒是10次请求,一次请求对应3次数据库请求,那么数据库层每秒也就30次请求。

以上架构可以满足需求。
系统集群化部署
假设此时用户数开始快速增长,比如注册用户量增长了50倍,上升到了500万。
日活用户是50万,高峰期对系统每秒请求是500/s。然后对数据库的每秒请求数量是1500/s
4核8G的机器每秒请求达到500/s的时候,很可能你的机器CPU负载较高。数据库层面,以上述的配置而言,其实基本上1500/s的高峰请求压力的话,还算可以接受。
此时的解决方案:
- 添加负载均衡层,将请求均匀打到系统层。
- 系统层采用集群化部署多台机器,扛住初步的并发压力。

数据库分库分表 + 读写分离
假设此时用户量继续增长,达到了1000万注册用户,然后每天日活用户是100万。
此时对系统层面的请求量会达到每秒1000/s,系统层面,可以继续通过集群化的方式来扩容,反正前面的负载均衡层会均匀分散流量过去的。但是,这时数据库层面接受的请求量会达到3000/s,可能会出现问题。
一般来说,对那种普通配置的线上数据库,建议就是读写并发加起来,按照上述我们举例的那个配置,不要超过3000/s。
并发量持续增长时,关注点要给到数据库层面,对系统做分库分表 + 读写分离。
把一个库拆分为多个库,部署在多个数据库服务上,主库承载写入请求,然后每个主库都挂载至少一个从库,由从库来承载读请求。
此时假设对数据库层面的读写并发是3000/s,其中写并发占到了1000/s,读并发占到了2000/s。

缓存集群引入
注册用户量越来越大,此时你可以横向扩展,比如说系统层面不停加机器,就可以承载更高的并发请求。但是数据库层面就不能盲目加机器,数据库其实本身不是用来承载高并发请求的,单机每秒承载的并发就在几千的数量级,而且数据库使用的机器都是比较高配置,比较昂贵的机器,成本很高。
在高并发架构里通常都有缓存这个环节,缓存系统的设计就是为了承载高并发而生。单机承载的并发量都在每秒几万,甚至每秒数十万,对高并发的承载能力比数据库系统要高出一到两个数量级。
对那种写少读多的请求,可以引入缓存集群。在写数据库的时候同时写一份数据到缓存集群里,然后用缓存集群来承载大部分的读请求。通过缓存集群,就可以用更少的机器资源承载更高的并发。

消息中间件集群引入
当写数据库的并发量也升高时,也要考虑优化,同样不能盲目加机器,针对高写入的压力,可以引入消息中间件集群。
消息中间件系统本身也是为高并发而生,所以通常单机都是支撑几万甚至十万级的并发请求的。可以用很少的资源支撑很高的并发请求,用来支撑部分允许异步化的高并发写入。
假如说,现在每秒是1000/s次写请求,其中比如500次请求是必须请求过来立马写入数据库中的,但是另外500次写请求是可以允许异步化等待个几十秒,甚至几分钟后才落入数据库内的。
那么此时完全可以引入消息中间件集群,把允许异步化的每秒500次请求写入MQ,然后基于MQ做一个削峰填谷。比如就以平稳的100/s的速度消费出来然后落入数据库中即可,此时就会大幅度降低数据库的写入压力。

总结
这只是简单的演进过程,中间还有很多细节,引入一个新的技术或者组件,很可能会带来新的问题。
The end.